跳到主要内容
engine-pipeline · 代码
查看代码目录
Graphics Learning Lab Engine Pipeline / Source

Observe · Change · Recompute · Explain

先读数据契约,再读加载与批次

Read-only · exact teaching model

代码页只保留用于建立因果模型的最小片段;面试时要能指出边界、证据和回滚点。

这一课怎么学

  1. 观察
  2. 预测
  3. 改一个量
  4. 看证据
  5. 再读代码
Import contract

导入不是按钮,是数据契约

把 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);
}

DCC ↔ engine

版本漂移要在运行时前被拦截

schema 和 material function 版本必须能被自动比较。

// SECTION:sync
bool compatible(DataAsset asset, Schema expected, MaterialFunction fn) {
  return asset.schemaVersion == expected.version
      && asset.materialFunctionVersion == fn.version;
}

Async loading

先闭包依赖,再谈异步

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);
}

Cache budget

命中率决定卡顿形状

把热更新、缓存预算和依赖深度放到同一个 stall 模型。

// SECTION:cache
float stallCost(Cache cache, Graph graph) {
  float miss = graph.bytes * (1.0 - cache.hitRate);
  return miss / bandwidth + graph.depth * dependencyLatency;
}

Batch / LOD

批次是资产组织问题

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);
}

GPU profiler

时间线才是最后的裁判

用 pass 时间和目标帧预算,而不是凭感觉说“优化了”。

// SECTION:profiler
Timeline frame = profiler.capture(profilerFrames);
float gpuMs = frame.pass('shadow') + frame.pass('material') + frame.pass('post');
if (gpuMs > targetMs) markRisk('budget');