概述
精灵表要经过一条导入管线:PNG 源文件 -> 内嵌帧元数据的已编译 .tex 文件。所有帧都作为区域存在于父 .tex 文件内部,不会为每一帧生成单独的文件。
assets/sprites/player.png (source)
-> cache/{parentGuid} (compiled .tex with FrameList chunk)
与字体(每个字号生成一个单独的文件)不同,精灵帧是单张纹理的子区域。父 .tex 文件同时包含像素数据和一个列出所有帧坐标的 FrameList 元数据块。
GUID 生成
帧 GUID 由父 GUID + 帧索引确定性地生成:
frameGuid = GenerateDeterministicGuid(parentGuid + ":" + frameIndex)
这意味着:
- 相同的父资源 + 索引始终生成相同的帧 GUID
- 缓存可以从头重建,而不会破坏引用
- 不同的帧绝不会冲突
.tex 文件格式(2DTX)
[Header - 32 bytes]
magic: "2DTX" (4 bytes)
version: uint32_t
width, height: uint32_t
format: uint32_t (RGB565, RGB565A8, RGBA8888, ALPHA8)
dataSize: uint32_t
metadataSize: uint32_t
flags: uint32_t (HasTransparency, HasAlpha, IsSprite)
[Pixel Data]
Raw pixel data in the specified format
[Metadata Chunks]
numChunks: uint32_t
For each chunk:
chunkType: uint32_t (1=Sprite, 2=FrameList, 3=ChromaKey, 4=SourceSize)
chunkSize: uint32_t
...chunk data...
Sprite 块(类型 1)
基于网格的精灵表存储默认的帧尺寸:
frameWidth: int32_t
frameHeight: int32_t
FrameList 块(类型 2)
所有帧及其 GUID 和坐标:
frameCount: uint16_t
For each frame (52 bytes):
guid: 36 bytes (no null terminator)
x: int32_t
y: int32_t
width: int32_t
height: int32_t
SourceSize 块(类型 4)
仅当纹理的存储尺寸小于其图像时存在(最大大小):
width: uint32_t the image's size
height: uint32_t
Sprite 块和 FrameList 块、九宫格边框以及瓦片集都仍以图像的像素表示。Sprite::ApplySourceSize 在加载时将帧、边框和 pixelsPerMeter 换算为存储后的像素,因此精灵在世界中保持其尺寸;瓦片集用 Sprite::SourceToStoredX/Y 映射其瓦片矩形。两者都对矩形边缘使用 round(v x stored / image),即编码器重新采样每一帧时使用的规则,因此原本相接的帧仍然相接,也不会有帧渗入其他帧。
.data 附属文件
每个精灵表都可以有一个 .data JSON 附属文件(例如 player.png.data):
{
"guid": "random-uuid-for-png-file",
"slicing": {
"mode": "grid",
"frameWidth": 32,
"frameHeight": 32
}
}
图集模式(自定义帧定义):
{
"guid": "random-uuid-for-png-file",
"slicing": {
"mode": "atlas",
"frames": [
{ "x": 0, "y": 0, "width": 32, "height": 48 },
{ "x": 32, "y": 0, "width": 24, "height": 48 }
]
}
}
导入管线:TextureImporter
文件: deki-editor/src/TextureImporter.cpp
TextureImporter 负责带帧元数据的 PNG -> .tex 转换:
两遍导入
第 1 遍:发现
- 扫描 assets/ 中的所有图像文件
- 为每一帧生成确定性的 SubAsset GUID
- 在 s_FrameDataRegistry 中注册帧坐标
第 2 遍:编译
- 通过 stb_image 加载源图像
- 转换为目标格式(RGB565 等)
- 写入包含头部 + 像素数据 + 元数据块的 .tex 文件
- FrameList 块包含所有帧 GUID + 坐标
SubAsset 生成
for (row = 0; row < rows; row++) {
for (col = 0; col < cols; col++) {
int index = row * cols + col;
subAsset.guid = GenerateDeterministicGuid(parentGuid + ":" + std::to_string(index));
subAsset.parentGuid = parentGuid;
subAsset.subAssetIndex = index;
}
}
帧数据注册表
用于在编辑器中快速查找帧坐标的内存缓存:
static std::unordered_map<std::string, SpriteFrameData> s_FrameDataRegistry;
TextureImporter::RegisterFrameData(frameGuid, data);
TextureImporter::GetFrameData(frameGuid);
TextureImporter::ClearFrameData();
精灵的 AssetRef 字段
在 SpriteComponent 上:
sprite.guid = parent sprite GUID (the .tex file to load)
sprite.source = frame GUID (which sub-region to render)
为什么 guid = 父资源,source = 帧
精灵帧是同一个文件中的区域,而不是单独的文件。父 .tex 在其 FrameList 块中内嵌了所有帧。因此:
- sprite.guid 必须指向父 .tex(要从磁盘加载的实际文件)
- sprite.source 标识该文件中的哪个帧区域
这与字体不同,字体的每个变体都是单独的文件(font.guid = 变体文件,font.source = TTF 源文件)。
普通精灵(没有帧)
sprite.guid = sprite GUID (the .tex file)
sprite.source = (empty)
帧解析流程
1. Scene loaded from MessagePack
-> SpriteComponent.Deserialize()
-> sprite.guid = "parent-sprite-guid"
-> sprite.source = "frame-guid" (or empty)
2. SpriteComponent::ResolveObjectRefs()
-> sprite.Get() loads parent .tex via AssetManager
-> Sprite::Load() parses FrameList chunk -> populates frames vector
3. Choose the meaningful GUID:
-> If sprite.source is empty: use sprite.guid (full texture)
-> If sprite.source is set: use sprite.source (specific frame)
-> Call OnAssetRefResolved(propertyName, loadedSprite, frameGuid)
4. SpriteComponent::OnAssetRefResolved()
-> Sprite::FindFrame(frameGuid) -> linear search in frames vector
-> SetFrameRect(frame->x, frame->y, frame->width, frame->height)
5. SpriteComponent::RenderContent()
-> Uses frame_x, frame_y, frame_width, frame_height for QuadBlit source rect
生成的 ResolveObjectRefs
代码生成器生成如下模式:
void SpriteComponent::ResolveObjectRefs(Scene* scene) {
if (auto* _loaded_sprite = sprite.Get()) {
const char* _frameGuid = sprite.source.empty()
? sprite.guid.c_str()
: sprite.source.c_str();
OnAssetRefResolved("sprite", _loaded_sprite, _frameGuid);
}
}
这样 OnAssetRefResolved 就能收到有意义的标识符(精灵表为帧 GUID,普通精灵为精灵 GUID)。
FindFrame
const SpriteFrame* Sprite::FindFrame(const std::string& guid) const {
for (size_t i = 0; i < frames.size(); i++) {
if (guid == frames[i].guid) return &frames[i];
}
return nullptr;
}
线性查找:帧数量很少(通常 < 100)。
在编辑器中指定
文件: src/panels/inspector/AssetPropertyEditor.cpp
当用户在检视器中将一帧拖放到 SpriteComponent 上时:
assetRefBase.guid = subAsset->parentGuid;
assetRefBase.source = droppedGuid;
assetRefBase.guid = droppedGuid;
assetRefBase.source = "";
GUID 注册(编辑器)
SubAsset GUID 会注册到 AssetManager,以便 LookupPath 能够工作:
AssetManager::Get()->RegisterGuid(subAsset.guid, info.guid);
导出管线
在 AssetExporter::ExportToStorage() 期间:
1. CollectUsedAssets()
-> Scans all scenes for SpriteComponents
-> Collects parent GUIDs (sprite.guid)
2. CopyUsedAssetsToOutput()
-> Copies cache/{parentGuid} -> storage/{parentGuid}
-> Only the parent .tex is copied (frames are embedded)
3. ExportAssetLookupTable()
-> Adds parent GUID to binary lookup table
-> Frame GUIDs are NOT in the lookup table
-> At runtime, frames are found via FindFrame() after loading parent
与字体的关键区别: 字体变体是单独的文件,因此需要在查找表中有单独的条目。精灵帧则不需要:它们在父资源加载后通过 FindFrame() 找到。
比较:字体与精灵
| 方面 | 字体 | 精灵 |
| 虚拟资源 | 单独的文件(变体 + 图集) | 同一个文件中的区域 |
| guid 字段 | 变体 GUID(已烘焙的文件) | 父 GUID(.tex 文件) |
| source 字段 | TTF 源文件 GUID | 帧 GUID |
| 确定性种子 | source + ":" + size | parent + ":" + index |
| 导出的文件 | 每个字号一个变体 + 一个图集 | 每个精灵表一个 .tex |
| 查找表条目 | 变体 + 图集 GUID | 仅父 GUID |
| 运行时解析 | LookupPath(variantGuid) -> 文件 | LookupPath(parentGuid) -> 文件,然后 FindFrame(frameGuid) |
关键文件
| 文件 | 用途 |
| deki-editor/src/TextureImporter.cpp | PNG -> .tex 转换、SubAsset 生成 |
| deki-editor/include/deki-editor/TextureImporter.h | TextureImporter 接口、SpriteFrameData |
| ../../modules/deki-2d/Sprite.h | Sprite 类、SpriteFrame 结构体、FindFrame |
| ../../modules/deki-2d/Sprite.cpp | Sprite 加载、FrameList 解析 |
| ../../modules/deki-2d/SpriteComponent.h | 带帧矩形字段的 SpriteComponent |
| ../../modules/deki-2d/generated/Deki2DReflection0.gen.cpp | 生成的、使用 source 模式的 ResolveObjectRefs(每个包一个文件,而不是每个组件一个) |
| src/panels/inspector/AssetPropertyEditor.cpp | 帧指定 UI |
| src/services/AssetExporter.cpp | 导出管线 |