Skip to main content

Chapter 59: Scene Graphs

场景图要解决的主问题,是把一帧中的对象归属、层级变换、组件数据、资源引用、可见性结果和渲染提交拆成可追踪的数据路径。读完本章后,读者应能定位一个画面物体从编辑器层级进入世界矩阵、空间索引、可见列表和 render queue 的完整过程,并能判断某个 scene graph 设计卡在变换更新、资源绑定、剔除查询还是运行时组织上。

本章使用一个贯穿场景:一帧城市街角画面。场景根节点 CityBlock_Root 下有 Street_AShop_GroupCar_RigLamp_GroupCamera_MainSunLight。其中 Car_Rig 包含车身、四个车轮和车灯;Shop_Group 包含建筑主体、招牌和玻璃门;Lamp_Group 包含多盏路灯。这个场景足够小,可以手工推导 world matrix;它又足够接近真实引擎,可以暴露 dirty flag、resource reference、visibility list、LOD 和 render queue 的分工。

场景图(scene graph)在图形工程中通常表现为一棵或多棵有根树。每个节点有自己的 local transform,并通过 parent-child 关系继承父节点的变换。Unity 6.4 的 Transform Manual 把 Transform 解释为 GameObject 的位置、旋转、缩放和 parenting 状态;Godot stable 文档中的 SceneTree 把节点接入树之后的激活、回调和顺序作为核心运行规则;glTF 2.0 的 Nodes and Hierarchy 也规定 node hierarchy 是严格树,每个节点最多一个 parent,且全局矩阵由父节点全局矩阵乘本地矩阵得到。这些资料支持同一个工程判断:场景图的核心价值在于稳定表达层级关系,并把层级关系转化为运行时可更新、可查询、可提交的数据。

现代实时引擎通常会把 scene graph 拆成多个运行时视图。编辑器看到的是命名层级;transform 系统维护的是 parent index、children range 和 world matrix;可见性系统维护的是 spatial index 和 visibility list;渲染器消费的是按 material、pipeline state、distance 或 pass 排序后的 render queue。场景图在这些系统之间提供对象身份和层级来源,最终 draw call 通常来自 render queue,这让渲染排序、状态合并和 GPU 提交脱离编辑器树顺序。

59.1 场景图的概念与组成要素

场景图可以定义为一组带父子关系的 scene node,用来表达对象身份、局部变换、组件挂载和资源引用。node 是场景组织单元;transform 是空间关系;component 是行为或渲染能力;resource reference 指向 mesh、material、texture、animation clip 等可复用资产。把这四类对象分清,后续才能判断问题来自层级结构、数学变换、资源生命周期还是渲染提交。

在城市街角这一帧中,Car_Rig 是 node,Car_Rig.local 表示汽车相对街道的位置,Wheel_FL.local 表示左前轮相对汽车的位置。车身 mesh、轮胎 mesh 和车漆 material 都应作为资源被引用。这样做的结果是:移动汽车时只改 Car_Rig 的 local transform;旋转轮胎时只改车轮子节点;同一套轮胎 mesh 可以被四个节点引用;渲染器在构建 draw item 时把 node 的 world matrix 和 mesh/material reference 合并成一次可提交对象。

一个最小 scene node 可以抽象成下面的数据。示例只表达关键关系,省略线程安全、内存池、句柄版本号和序列化细节。

type NodeId = number;
type ResourceId = number;

type Transform = {
translation: [number, number, number];
rotation: [number, number, number, number];
scale: [number, number, number];
};

type SceneNode = {
id: NodeId;
name: string;
parent: NodeId | null;
children: NodeId[];
local: Transform;
world: number[];
components: string[];
mesh: ResourceId | null;
material: ResourceId | null;
dirty: boolean;
};

这段结构展示了场景图的边界。parentchildren 负责树关系,localworld 负责变换,components 负责把节点接入脚本、动画、灯光或碰撞系统,meshmaterial 只是资源引用。node 本身保存组织关系;几何数据保存在 mesh 资源中,draw call 由可见性、LOD、pass、material、pipeline state 和实例化策略共同生成。

对同一个贯穿场景,可以先列出几类节点承担的职责。

节点层级职责变换职责资源职责渲染入口
CityBlock_Root管理整块街区提供世界原点或街区偏移通常无 mesh通常作为分组节点
Shop_Group组织店铺子对象控制整栋店铺位置引用建筑、招牌、玻璃资源子节点生成 draw item
Car_Rig组织车身与车轮控制车辆整体移动车身和轮胎资源挂在子节点子节点进入队列
Lamp_Group聚合路灯节点控制批量摆放基准灯柱 mesh、灯光组件mesh 与 light 分别处理
Camera_Main作为相机对象提供 view matrix 来源引用 camera component产生裁剪体和 pass 参数

这个表能帮助定位常见错误。物体位置错乱时,先看 transform 链;材质错乱时,先看 resource reference;物体未进入画面时,先看可见性结果和 render queue;编辑器选中错对象时,先看 node identity 和层级路径。场景图的节点名称、父子关系和组件挂载提供了人能理解的组织方式,runtime 系统再把这些关系转成数组、矩阵、包围盒和提交项。

场景图还承担“编辑器语义”和“运行时语义”的桥接。编辑器中的 Shop_Group/Sign_Neon 让美术可以选中招牌;运行时需要的是 Sign_Neon 的 world matrix、bounds、mesh handle、material handle 和可见性标记。好的 scene graph 设计会保留可读层级,同时让高频路径使用紧凑数组,减少每帧在树结构上做随机指针访问。

下面的图展示本章使用的主路径。图的边界是一个节点从编辑器层级进入渲染提交前的数据转换,重点是 scene graph 只提供来源关系,最终提交还要经过 transform、spatial index 和 render queue。

关键路径有两条。空间路径从 local transform 进入 world matrix,再更新 world bounds 和 spatial index;资源路径从 node 的 mesh/material reference 进入 render queue。渲染提交需要两条路径汇合:可见列表说明“哪些对象需要提交”,资源引用说明“用什么几何和材质提交”,world matrix 说明“提交到哪里”。

59.2 层级变换与继承逻辑

层级变换的核心规则是:子节点的世界矩阵由父节点世界矩阵和自身本地矩阵组合得到。用行文中最常见的列向量约定表达,可以写成 $world(child) = world(parent) * local(child)$。根节点没有 parent 时,world(root) = local(root)。如果项目使用行向量或另一套矩阵约定,乘法顺序会随约定调整;工程中必须保持数学库、shader、资产导入和调试显示使用同一约定。

Car_Rig 中,车轮的局部坐标通常以车身为参考。例如 Wheel_FL.local.translation = [-0.8, -0.45, 1.2],表示左前轮相对车身中心的位置。汽车整体向前移动时,只改 Car_Rig.local.translation;四个车轮的 local translation 保持稳定。下一次 transform update 会先得到 Car_Rig.world,再用它乘各车轮的 local matrix。画面上看到的是四个车轮跟随车身整体移动,同时保留各自相对车身的位置。

层级继承带来的第一条工程约束,是更新顺序必须让父节点先于子节点完成 world matrix。最直接的做法是从根节点做 preorder traversal。只要父节点 world matrix 已经稳定,子节点就能在一次乘法后得到自己的 world matrix。Godot 文档中提到 SceneTree 的许多操作按 tree order 或 top-to-bottom 顺序执行,这和 transform 更新中的父先子后逻辑处在同一类问题上:树关系决定了某些运行时计算的依赖顺序。

function composeLocalMatrix(local: Transform): number[] {
return composeTRS(local.translation, local.rotation, local.scale);
}

function updateWorld(nodeId: NodeId, parentWorld: number[] | null): void {
const node = nodes[nodeId];
const localMatrix = composeLocalMatrix(node.local);
node.world = parentWorld === null ? localMatrix : multiply(parentWorld, localMatrix);
node.dirty = false;

for (const childId of node.children) {
updateWorld(childId, node.world);
}
}

这段代码表达了 transform 更新的最小链路:local TRS 被组合成本地矩阵,父矩阵和本地矩阵相乘得到 world matrix,子节点再使用父节点的新结果。真实引擎会把递归改成迭代队列或拓扑排序数组,以控制栈深度、线程切分和缓存访问;核心依赖关系保持稳定。

dirty flag 用来减少重复计算。一个节点 local transform 改变后,它自身和所有后代的 world matrix 都失效。若 Car_Rig 移动,四个车轮、车灯和车身都要重新计算 world matrix;若只旋转 Wheel_FL,其他车轮无需更新。dirty 传播的粒度决定了大场景的 transform 开销。粒度过粗会让一个父节点的小改动触发大量后代更新;粒度过细会增加标记、队列维护和并发同步成本。

function markTransformDirty(nodeId: NodeId): void {
const stack: NodeId[] = [nodeId];

while (stack.length > 0) {
const current = stack.pop() as NodeId;
const node = nodes[current];
if (node.dirty) {
continue;
}
node.dirty = true;
for (const childId of node.children) {
stack.push(childId);
}
}
}

这段 dirty 传播示例说明了一个判断顺序:先定位哪个 local transform 被写入,再找到它的后代范围,最后只更新 dirty 子树。对城市街角场景,路灯整体静止时 Lamp_Group 的子树可以跨帧保持 world matrix;汽车移动时 Car_Rig 子树每帧更新;相机移动时 camera 节点更新,并重新生成 view matrix 与 frustum。

循环依赖检查是场景图稳定性的底线。glTF 2.0 明确要求 node hierarchy 是 disjoint strict trees,每个 node 至多一个 parent,并保持无环。工程中 reparent 操作必须检查“新 parent 是否位于当前节点的后代中”。如果允许 Car_Rig 成为 Wheel_FL 的子节点,同时 Wheel_FL 仍然在 Car_Rig 下面,world matrix 计算会出现无限依赖。稳定做法是把 reparent 写成一个受控操作:移除旧 parent 关系、检查新 parent 合法性、计算是否保持 world transform、更新 dirty 子树、刷新编辑器路径。

非均匀缩放和旋转组合会带来额外边界。父节点使用不同轴 scale 时,子节点再旋转,最终矩阵可能产生 shear 形变。Unity Manual 在 Transform 与 scale 章节中说明了非均匀缩放对 collider、light、child transform 的影响;glTF 2.0 也在 transformations 中说明 matrix 需要可分解到 TRS,并提示不可逆 transform 可能产生 lighting 或 visibility artifacts。实践中,角色骨骼、物理碰撞、法线变换和包围盒更新都应单独检查非均匀 scale 的后果。

层级变换最终服务两个输出:渲染空间位置和查询空间包围体。world matrix 送给 shader 之后,顶点才能从 model space 进入 world/view/clip space;world bounds 送给空间索引之后,相机剔除和 LOD 才能工作。只更新 matrix 而没有同步 bounds,会出现物体已经移动但剔除还使用旧位置的现象;只更新 bounds 而没有刷新 render item 的 per-object constant,也会出现剔除正确但渲染位置错误的现象。

59.3 设计可扩展的场景图系统

可扩展 scene graph 的目标,是同时满足编辑器可组织、运行时可更新、资源可复用、序列化可稳定和渲染可高效。单棵面向编辑器的对象树很适合展示层级和处理选中路径;高频 runtime 系统更适合使用紧凑数组、句柄和分阶段缓存。设计时应先确定 scene graph 承担哪些职责,再把高频查询交给 transform system、component registry、spatial index 和 render graph。

一个工程化 scene graph 通常拆成五层数据。第一层是 node identity,包括稳定 ID、名称、层级路径和生命周期。第二层是 transform hierarchy,包括 parent index、children list、local transform、world matrix、dirty flag。第三层是 component attachment,包括 mesh renderer、light、camera、script、animation、physics proxy。第四层是 resource reference,包括 mesh、material、texture、skeleton、animation clip。第五层是 runtime cache,包括 world bounds、visibility state、render item handle、editor selection state。

设计对象应保存的数据高频访问路径主要风险
Node identityID、name、parent、children编辑器选择、序列化、脚本查找重命名和复制导致引用漂移
Transform hierarchylocal、world、dirty、parent indextransform update、bounds updatereparent、循环、非均匀缩放
Component attachmentcomponent type、component handle系统分发、脚本访问组件顺序和生命周期混乱
Resource referencemesh/material/texture handlerender item 构建、资源加载热更新和异步加载状态不一致
Runtime cachebounds、visibility、queue handleculling、LOD、render queue缓存失效和跨线程同步错误

ECS 与 scene graph 可以组合使用。Entity Component System(ECS)把对象拆成 entity ID 与多类 component data,适合批量处理 Transform、MeshRenderer、Light、Camera 等同类数据。scene graph 提供 parent-child 和编辑器层级,ECS 提供数据并行和系统调度。一个常见布局是:node 持有 entity ID;TransformComponent 持有 parent entity 和 local/world;MeshRendererComponent 持有 mesh/material;可见性系统按 entity 生成 visibility list。这样,编辑器保留 Shop_Group/Sign_Neon 的层级,runtime 可以按 component 数组批量更新。

序列化需要稳定表达节点身份和资源引用。保存文件时,可以写出 node ID、parent ID、local transform、组件列表和资源路径或资源 GUID。加载时先创建所有 node,再建立 parent-child,再解析资源引用,最后执行一次全量 transform update 和 bounds update。这个顺序能处理子节点在文件中先出现、资源异步加载和 prefab 实例化等情况。若加载阶段直接依赖指针地址,文件版本升级、热重载和跨进程编辑器通信都会变得脆弱。

编辑器选择依赖 scene graph 的可读路径;渲染器通过 draw item handle、mesh handle 和 material handle 完成高频提交。选择 CityBlock_Root/Shop_Group/Sign_Neon 时,编辑器需要显示层级、gizmo、组件面板和资源引用;渲染器需要的是 Sign_Neon 的 draw item。把选择路径和 draw item handle 通过 node ID 关联,就能让编辑器操作影响 transform 和 material,同时让 render queue 继续按 pass、material 和 depth 排序。

运行时更新应按依赖分阶段组织。一个稳定的 frame update 可以分成:脚本和动画写 local transform;transform system 刷新 dirty world matrix;bounds system 刷新 world bounds;spatial index 接收移动对象更新;visibility system 根据 camera 查询可见对象;render system 构建并排序 draw items;GPU command recording 提交 pass。这个顺序让每一层都消费上一层稳定结果,调试时也能逐层定位。

下面的流程图展示可扩展 scene graph 在一帧中的分阶段路径。它的重点是每个系统消费明确输入并产生明确输出,scene graph 只承担来源关系和基础状态。

这个阶段划分可以直接用于排查。画面物体没有移动,先看脚本或动画是否写入 local transform;local 已变但画面未变,检查 dirty queue 和 world matrix;world matrix 已变但剔除错误,检查 bounds update 和 spatial index;visibility list 正确但提交错误,检查 render item 和资源引用。每一步都有明确数据证据,降低了跨系统猜测成本。

可扩展设计还要处理批量实例和异步资源。街道上的 80 盏路灯可以在 scene graph 中表现为 80 个 node,便于编辑器选择和单独调整;渲染器可以把它们合并成同一个 mesh/material 的实例化批次。此时 node 层级记录每盏灯的位置,renderer 生成 instance buffer。资源异步加载时,node 可以先存在,mesh/material handle 处于 pending 状态;等资源 ready 后,再生成 render item。这个分离让场景组织和 GPU 资源生命周期互相解耦。

59.4 可见性剔除与场景图的结合

可见性剔除要回答“当前 camera 和 pass 下哪些可渲染对象需要进入提交”。场景图提供对象归属、world matrix 和资源引用;spatial index 提供空间查询;visibility list 记录查询结果;LOD 系统根据距离、屏幕尺寸或质量策略选择具体资源;render queue 再按 pass 与状态排序。把这些对象分层后,剔除问题才能从编辑器树顺序中独立出来。

在城市街角场景中,camera 只看向街道右侧。Shop_Group 在视锥内,Car_Rig 在视锥边缘,部分路灯在视锥外。scene graph 递归遍历能找到所有节点,但高效剔除需要使用 world bounds 和 spatial index。大场景通常把可渲染节点的 bounds 插入 BVH、octree、grid、hierarchical grid 或 loose octree。camera 查询返回候选对象后,再进行 frustum test、occlusion、LOD 和 pass filter。

可见性路径可以写成一个稳定检查顺序:先确认可渲染节点是否有 world bounds;再确认 bounds 是否跟随 world matrix 更新;再确认对象是否进入 spatial index;再用 camera frustum 查询候选集;再根据 bounds 与 frustum 的关系生成 visibility list;最后由 render system 构建 render queue。这个顺序把“对象在编辑器树中存在”和“对象进入当前 pass”分成了两个事实。

图中的关键转换是 World Bounds -> Spatial Index -> Visibility List。world matrix 只说明物体在世界中的位置;world bounds 把物体转成可查询体积;spatial index 让查询规模从全场景扫描下降到候选集合;visibility list 是当前 camera、pass 和 quality setting 下的结果。render queue 以 visibility list 作为输入,再结合资源引用和 pass 信息生成提交顺序。

父子层级可以帮助 coarse culling。比如 Shop_Group 的 group bounds 覆盖建筑主体、招牌和玻璃门。如果 group bounds 整体在视锥外,子节点可以整组跳过;如果 group bounds 与视锥相交,才继续检查子节点。这个策略适合静态或弱动态组合。对 Car_Rig 这类移动对象,group bounds 要跟随车辆移动,并覆盖车身和车轮。若子节点动画幅度大,group bounds 需要留出保守范围,或在动画后重算。

场景图和空间索引的关系需要根据动态程度选择。静态建筑可以在加载时建立 spatial index,运行时很少更新;移动车辆需要每帧或按 dirty 状态更新索引;粒子、草、行人群体可能使用专门的 chunk bounds 或 instance bounds。把所有节点都逐帧从空间索引移除再插入,会造成 CPU 开销和内存写入压力;把动态对象长期留在旧位置,会造成剔除错误。稳定做法是按动态等级拆分索引:static index、dynamic index、streaming index 或 per-chunk index。

LOD 与 scene graph 的结合点是 node identity、world bounds 和 resource reference。一个 Shop_Sign 节点可以根据屏幕尺寸选择高模、低模或 billboard;同一节点仍然保持相同的 transform 和资源语义。LOD 选择输出的是当前 pass 使用的 mesh/material variant。这样,编辑器选择、脚本引用和动画关系保持稳定,渲染提交根据当前 camera 条件变化。

render queue 是 scene graph 到 GPU 的最后一层转换。它通常包含 mesh handle、material handle、pipeline state、world matrix 或 object constant offset、pass type、sort key 和 instance data offset。透明物体需要按距离或排序策略处理;不透明物体更关注 material/pipeline state 合并和 depth 友好的顺序;shadow pass 只需要投影到光源视角的可见对象。scene graph 提供对象来源,render queue 决定提交顺序。

工具观察也应跟随这条路径。RenderDoc 这类 frame capture 工具能看到 draw call、pipeline state、bound resource 和 shader input;完整编辑器 scene graph 通常需要引擎 debug view 配合观察。引擎自带 debug view 可以显示 world bounds、visibility list、LOD level 和 render queue reason。排查时应把工具结果放回路径中解释:draw call 中 mesh 正确但位置错误,优先查 world matrix;visibility debug 缺少该对象,优先查 bounds 和 spatial index;draw call 存在但材质错误,优先查 resource reference 和 material variant。

59.5 实时引擎中的场景组织模式

实时引擎中的 scene graph 组织模式取决于场景资产的来源、动态程度、编辑需求和查询类型。游戏关卡、CAD 场景、影视资产和数据可视化场景都需要层级组织,但它们对 transform 更新、资源复用、可见性剔除、编辑器选择和序列化粒度的要求不同。一个稳定的设计会保留 scene graph 作为归属和变换来源,同时为不同查询建立专门索引。

游戏关卡通常混合静态环境、动态角色、VFX、灯光和触发器。静态环境适合按 chunk、room、sector 或 streaming cell 组织;动态角色适合把 rig、骨骼、装备和特效挂在同一对象层级下;VFX 和临时对象适合使用对象池与短生命周期 node。游戏场景的重点是 transform dirty 范围、动态空间索引、LOD、streaming 和 render queue 的稳定更新。

CAD 场景通常包含大量零件、装配层级和精确选择需求。scene graph 要保留 assembly、part、subpart 的层级与元数据,便于选择、测量、隐藏和隔离。渲染侧需要把大量相同螺丝、板件、管线转成实例化或合批数据。CAD 的剔除常常依赖层级包围盒和可见性缓存,编辑器树可能非常深,runtime 需要把递归遍历压缩成可迭代的节点区间。

影视资产和 DCC 导入场景强调命名、绑定、动画曲线、材质槽和镜头管理。资产层级往往来自 Maya、Blender、Houdini 或 USD/glTF 管线。导入后应保存原始层级和动画 target,同时生成 runtime-friendly transform 数组和资源表。对骨骼、约束、摄像机、灯光和材质变体,scene graph 需要保留足够语义供编辑和重导入对齐;渲染侧再根据 pass 和 quality setting 生成实际提交。

数据可视化场景常见大量点、线、标注、体素、城市建筑或工业设备。scene graph 可以组织数据集、图层、时间片和交互选择;GPU 资源通常是大 buffer、texture atlas、indirect draw 参数或 compute 输出。可见性查询常按 tile、cluster、time range 或 semantic layer 进行。此时 scene graph 更像控制层,真正的大规模数据访问放在 buffer layout 和 spatial/semantic index 中。

场景类型组织重点运行时重点常见结构
游戏关卡关卡块、角色、触发器、VFXstreaming、LOD、动态剔除、提交排序scene graph 加 chunk index
CAD 场景装配层级、零件元数据、选择隔离实例化、层级包围盒、深树压缩assembly tree 加 BVH
影视资产DCC 层级、动画 target、材质槽导入缓存、动画评估、pass 变体asset graph 加 transform arrays
可视化场景数据集、图层、语义分类、时间片大 buffer、tile query、indirect drawcontrol graph 加 spatial/semantic index

这些模式共享一个判断框架:先判断节点层级服务谁,再判断高频查询由谁完成。若层级主要服务编辑器和资产管理,就让 scene graph 保留可读结构;若查询主要服务剔除、碰撞、拾取或渲染排序,就建立专门索引。把两者通过 stable ID 连接,可以同时获得可维护层级和高效运行时路径。

对本章贯穿的城市街角帧,可以给出一套完整组织方案:CityBlock_Root 下面按街区、店铺、车辆、灯光和相机组织;静态建筑写入 static spatial index;车辆写入 dynamic spatial index;路灯 mesh 使用实例化;灯光组件进入 light list;相机生成 frustum;visibility query 输出可见 mesh renderer;LOD 选择 mesh/material variant;render queue 按 pass 和 state 排序。最终 GPU 看到的是一组 draw call 和资源绑定,编辑器仍然能看到清晰的对象层级。

工程上最可靠的 scene graph 设计,通常有三条边界。第一,scene graph 表达“对象属于谁、相对谁、引用什么资源”。第二,transform system 表达“当前世界空间结果是什么”。第三,renderer 表达“当前 pass 提交什么、按什么顺序提交、绑定哪些 GPU 资源”。当这三条边界清楚时,一个物体显示异常就能沿着 node、transform、bounds、visibility、LOD、render queue、draw call 逐层排查。

最小自检任务

给定一个小型车库场景:Garage_Root 下有静态墙体、地面、两盏吊灯、一个可移动汽车、汽车下有四个车轮、一个主相机和一个反射探针。请设计一份 scene graph 组织方案,并回答下面四个问题:哪些节点保存 local transform;哪些对象引用 mesh/material 资源;汽车移动时哪些 world matrix 和 bounds 需要更新;从 camera 到 render queue 的可见性路径应经过哪些数据结构。

答案要点

Garage_Root 可以作为根节点,墙体和地面挂在 Static_Geometry 分组下,两盏吊灯挂在 Light_Group 下,汽车使用 Car_Rig 作为父节点,车身和四个车轮作为子节点。主相机和反射探针各自作为带组件的节点,保存 local transform 并生成 view/frustum 或 probe capture 参数。所有可移动和可选择对象都保存 local transform;根节点和分组节点也可以保存 local transform,用于整体偏移和编辑器组织。

墙体、地面、车身、车轮和灯具 mesh 节点引用 mesh/material 资源;吊灯还引用 light component;相机引用 camera component;反射探针引用 probe component 和相关 render target 配置。资源以 handle 或 GUID 形式保存在 component 中,node 只提供身份、层级和 transform 来源。

汽车移动时,Car_Rig 的 local transform 被写入 dirty,Car_Rig、车身、四个车轮以及挂在车上的灯光或特效子节点都需要刷新 world matrix。随后这些可渲染子节点的 world bounds 需要刷新,并把动态对象在 dynamic spatial index 中的位置更新。静态墙体、地面和吊灯如果未跟随汽车移动,可以保持原有 world matrix、bounds 和 static spatial index 条目。

camera 到 render queue 的路径应是:camera component 生成 view matrix 和 frustum;visibility system 用 frustum 查询 static spatial index 与 dynamic spatial index;候选对象经过 bounds test 和 pass filter 形成 visibility list;LOD 系统根据屏幕尺寸或距离选择 mesh/material variant;render system 把可见 mesh renderer 转成 render item;render queue 再按 pass、pipeline state、material 和距离等 sort key 排序。若画面中汽车缺失,应按 local transform、world matrix、world bounds、dynamic spatial index、visibility list、LOD selection、render queue 的顺序排查。

本章知识点总结

  • 场景图定义:场景图用 node、parent-child、local transform、component 和 resource reference 表达场景对象的组织关系。
  • 节点边界:node 提供身份和层级来源,mesh、material、texture 等资产应通过资源引用进入渲染路径。
  • 世界矩阵:子节点 world matrix 由父节点 world matrix 和自身 local matrix 组合得到,根节点 world matrix 等于自身 local matrix。
  • 更新顺序:transform update 需要父节点先于子节点完成,preorder traversal 或拓扑数组都可以表达这个依赖。
  • Dirty 传播:local transform 被写入后,应标记自身和后代,使 world matrix 与 bounds 只在失效范围内刷新。
  • 循环检查:reparent 操作必须检查新 parent 合法性,确保节点层级保持严格树结构。
  • 扩展设计:可扩展 scene graph 应把 node identity、transform hierarchy、component attachment、resource reference 和 runtime cache 分层保存。
  • ECS 组合:scene graph 可以保留编辑器层级,ECS 或组件数组负责高频批量更新和系统调度。
  • 剔除路径:可见性剔除应从 world bounds 和 spatial index 生成 visibility list,再进入 LOD 选择和 render queue。
  • 队列提交:render queue 消费可见对象、资源引用、world matrix 和 pass 信息,并按 GPU 提交需求排序。
  • 组织模式:游戏、CAD、影视资产和可视化场景共享层级组织需求,但高频查询应交给对应的空间或语义索引。
  • 排查顺序:显示异常应沿 node、local transform、world matrix、bounds、spatial index、visibility list、LOD、render queue 和 draw call 逐层定位。