PNG 图片拖进 Godot 工程就能 load(),但运行时引擎并没有解码 PNG——它读的是一个你从没打开过的文件:.godot/imported/ 下的 .ctex。导入管线是编辑器和运行时之间的翻译层,这一层藏在"拖进去就能用"的体验下面,出了问题(冷启动卡十分钟、贴图改了没生效、协作冲突)才会浮出水面。这篇按数据流拆解:扫描、导入、重映射三个环节。
三层结构
导入管线可以切成职责清晰的三层:
扫描层(EditorFileSystem)。 editor/editor_file_system.cpp,编辑器进程启动时跑。它遍历 res://,给每个文件确定类型(读头部几个字节判断,不是靠扩展名),并维护一棵缓存目录树。结果是 .godot/editor/filesystem_cache<N> 文件(N 是格式版本号,4.x 是 8)——工程里几百个目录、几万文件的层级结构、修改时间、类型全在这一个文件里。
导入层(各 EditorImportPlugin)。 每个可导入类型注册一个导入器:texture 管图片、scene 管 glTF/FBX、wav/ogg/mp3 管音频。导入器输入源文件加参数(压缩格式、mipmap、循环点……),输出转换产物。
重映射层(ResourceFormatImporter)。 运行时 load("res://hero.png") 时真正干活的 loader。它识别原路径,读旁边的 hero.png.import,按里面的记录改道去加载 .godot/imported/ 里的产物。对调用方完全透明——你 load() 的是 PNG 路径,拿到的是 CompressedTexture2D。
扫描:全量一次,之后靠 mtime
打开工程时 EditorFileSystem 先看缓存文件在不在。不在(首次打开、.godot 被删)→ 全量扫描:逐文件打开读头部定类型,能导入的立刻排队导入。工程大的时候这就是冷启动那几分钟的去向。
缓存存在则走增量:递归比较目录 mtime,mtime 没变整棵子树跳过。mtime 变了才进目录逐个比文件。mtime 是整个增量机制的地基,它的两个推论都是坑:
- 用版本控制时如果检出过程不保留 mtime(很多协作方的默认行为),每次拉代码都可能触发成片 reimport。缓解办法是把
.import文件和.godot/imported/一起提交(下面细说),以及避免反复git clean整个.godot。 - 用脚本批量改文件(比如命令行转码图片)会刷新 mtime,编辑器下次聚焦时触发重导,大工程可能卡一阵。批量处理尽量安排在收工前。
一份 .import 文件里的信息
hero.png 导入后旁边生成 hero.png.import,文本格式,关键段落:
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b3r4sxesy3xwo"
path="res://.godot/imported/hero.png-a1b2c3d4.ctex"
[deps]
source_file="res://hero.png"
[params]
compress/mode=1
mipmaps/generate=true
path 指向的产物文件名带一段哈希后缀,来自源文件路径的 MD5——所以同一个源文件在所有人机器上产物名一致,这是 .godot/imported 可以进版本库的前提。[params] 是导入参数的序列化,[remap] 里的 uid 就是 UID 那篇说的、登记进 uid_cache 的 ID。
改一张贴图的连锁反应
把 hero.png 改了存盘,编辑器这边的完整链路是:
检测到 mtime 变化 → 重新导入 hero.png
→ 旧 .ctex 删除,生成新 .ctex(文件名哈希不变)
→ uid_cache 里该 UID 的路径不变,无需更新
→ 引用它的材质 .tres 不重导(材质不含贴图数据,只存引用)
→ 场景里使用该材质的部分即时刷新
注意倒数第二步:材质不跟着重导。材质的 .tres 里对贴图的引用是 UID + 源路径,加载时通过重映射层解析到产物,产物换了内容,材质自然"看到"新贴图。这条链的关键是引用和数据的分离——源文件是数据,产物是缓存,引用只指向源。
两个实战判断由此而来。其一,改了贴图只影响这一张的导入,不用担心连锁 reimport 拖垮编辑器。其二,改了导入参数(比如压缩格式从 Lossless 改成 VRAM Compressed)必须触发重导,参数变化会改变产物类型,.import 的 [params] 段落就是比对的依据——编辑器打开工程时发现参数和缓存不一致会自动补导。
.godot/imported 该不该进版本库
官方立场:.import 文件必须提交(它们是源数据的元数据),.godot/imported/ 可以不提交但建议提交。理由就是上面那串链路:产物名是源路径的确定性哈希,提交后协作者拉下来不用重新导入,冷启动直接跳过导入阶段。代价是仓库体积变大——.ctex 是压缩后的 VRAM 格式,通常比源 PNG 小,但比文本 .import 大一个量级。我的取舍:小工程直接提交,省掉所有"你那边怎么在导入"的对话;资源量上 G 之后交给 CI 缓存而不是版本库。
还有一个反向的坑:不要把 .godot/imported 提交了又让 .gitignore 把 .godot 整个忽略——两者只能选一边。半提交状态(只有部分产物在库里)会造成部分文件加载新产物、部分加载旧产物,且无任何报错,排查时容易怀疑人生。