Observe · Change · Recompute · Explain
先读数据契约,再读加载与批次
Read-only · exact teaching model
代码页只保留用于建立因果模型的最小片段;面试时要能指出边界、证据和回滚点。
这一课怎么学
- 观察
- 预测
- 改一个量
- 看证据
- 再读代码
导入不是按钮,是数据契约
把 UV seam、normal 重算、命名和 QA gate 拆成可观察的成本。
// SECTION:import
ImportReport importAsset(Mesh mesh) {
Mesh split = uvSplit ? splitByUvSeams(mesh) : mesh;
Mesh normals = normalRecalc ? recalculateNormals(split) : split;
QAResult qa = strictNaming ? validateNaming(normals) : warnOnly(normals);
return report(normals, qa);
}
版本漂移要在运行时前被拦截
schema 和 material function 版本必须能被自动比较。
// SECTION:sync
bool compatible(DataAsset asset, Schema expected, MaterialFunction fn) {
return asset.schemaVersion == expected.version
&& asset.materialFunctionVersion == fn.version;
}
先闭包依赖,再谈异步
AssetBundle / Addressables 的差异最终要落到依赖、缓存和并发。
// SECTION:streaming
async Task<Scene> loadScene(Key key) {
Catalog catalog = await catalogHandle;
DependencyGraph graph = catalog.closeDependencies(key);
await cache.ensure(graph, cacheBudget);
return await addressables.loadAsync(key, concurrency);
}
命中率决定卡顿形状
把热更新、缓存预算和依赖深度放到同一个 stall 模型。
// SECTION:cache
float stallCost(Cache cache, Graph graph) {
float miss = graph.bytes * (1.0 - cache.hitRate);
return miss / bandwidth + graph.depth * dependencyLatency;
}
批次是资产组织问题
atlas、instancing、LOD 和 collider 共同改变 draw call 与内存。
// SECTION:batch
BatchReport batch(Renderer[] renderers) {
Atlas atlas = buildMaterialAtlas(renderers);
int merged = enableInstancing ? instanceCompatible(renderers) : 0;
return report(renderers.length - merged - atlas.mergedDraws, atlas.coverage);
}
时间线才是最后的裁判
用 pass 时间和目标帧预算,而不是凭感觉说“优化了”。
// SECTION:profiler
Timeline frame = profiler.capture(profilerFrames);
float gpuMs = frame.pass('shadow') + frame.pass('material') + frame.pass('post');
if (gpuMs > targetMs) markRisk('budget');