CD Code Digest Hub /GTA V

大世界、实体模型、场景图与可见性

范围:Framework entity/fwscene/fwgeovis,结合 Base spatial data、streaming 与渲染扫描。静态分析,未运行。入口:Summary。

14 分钟阅读 13 个章节 1 张图

1. 大世界不是单一“世界分块”算法#

RAGE 同时维护几种不同的世界表示:可共享 archetype、实例 entity、可装卸 map data、用于扫描的 scene node/container、用于渲染的精简 descriptor、物理空间结构,以及独立资源驻留图。

这种多表示让每个系统按自己的访问模式处理世界,避免所有查询都遍历重量级游戏对象。它同时要求各表示在对象创建、移动、LOD 切换、卸载时保持一致。世界 bug 往往来自表示间更新失配,而非某一棵树的查找错误。

表示主要回答的问题
Archetype这是什么模型/类型,有哪些共享资源和默认属性?
Entity这是哪一个实例,它的位置、状态和附加能力是什么?
MapData/MapTypes哪一批放置内容可以加载/卸载,哪些类型可共享?
SceneGraph/EntityContainer扫描或搜索哪些空间集合?
EntityDesc/SoA用最少数据判断某实体是否需要进一步处理?
LOD hierarchy哪个尺度的替代表示可显示,子对象是否齐备?
Streaming graph需要哪些字节资源,是否可实例化与回收?
Physics level哪些实例可碰撞、可查询、需要模拟?

2. Archetype、entity 与按需扩展#

Framework 将 archetype、动态 archetype、entity、dynamic entity component 和 extension list 分开。共享模型信息避免每个路灯或建筑复制资源描述;扩展让少量实体附带摄像机相对、附件、替代 skeleton 等能力。

它不是可直接等同于现代 archetype ECS 的整齐 SoA 全实体存储。这里同时存在类层次、指针关系、扩展对象和局部数据导向容器。性能优化是按热点逐层应用,而非所有对象统一变成一种 ECS 布局。

fwTransform 还有 identity/simple/matrix/quaternion 及 scaled 变体,使用 type 分派和对象池。只需简化表示的静态物件可以减少内存;动态复杂实例使用更完整变换。池耗尽有明确错误处理入口,容量需要随内容预算设计。

3. MapData 的装载不止于读取放置列表#

MapDataStore 是 streaming module,维护数据定义和内容,负责异步装配、主线程完成、实体加入世界以及对应清理。其高/中/低及 critical 地图资产类别,配有不同 box-streamer 请求策略。

地图块之间有 parent/child 与 LOD 关系,archetype 又可能属于别的资源包。因此一个地图块“文件已读入”并不意味着它的模型、纹理、碰撞和所有子 LOD 都已经可用。

内容切块应兼顾四种粒度:IO、资源复用、场景加入/移除 CPU 和视距替代。过小块增加元数据、请求和注册成本;过大块增加峰值、无用驻留及发布卡顿。代码提供机制,但最佳块大小取决于内容分布与目标硬件。

4. 多层场景节点#

fwWorldRepMulti 持有 exterior root,并提供 Add/Remove/Update entity、增加 streamed scene node、PreScan/PostScan 等入口。SceneGraph 定义 exterior、streamed、interior、room、portal 各类节点。

图是类型关系示意,不意味着每个房间与 portal 都严格形成无环树。portal 表达空间连通与观察路径;straddling portal container 专门处理跨边界实体,说明“物体只属于一个房间”的简化并不够。

室内分区减少不可见室内内容的扫描和绘制,也为室内外边界提供语义。但是门口、跨房间大型对象、镜面/反射和阴影需要额外可见性条件;最终可见集合不只由主摄像机所在房间决定。

5. 面向扫描的数据布局#

fwEntityDesc 缓存类型、可见性、LOD、包围体相关数据。flags 中有明确注释:特定位域被向量代码访问,不能随意移动。fwSoAEntityContainer 提供按四个 descriptor 成块转换/读取的方法,分别排列 flags、visibility、LOD 等字段。

它的目标是先用连续小数据批量判断候选,再访问完整 entity。这样减少随机指针追逐、cache miss 和跨平台数据传送。SPU 路径中的 fwPointerFixer 又根据整体内存区间重定位内部指针,说明数据可搬运性是结构设计的一部分。

限制是布局耦合:修改位域、对齐、32/64 位指针或 SIMD 读取假设,需要同时更新序列化、SPU/CPU 遍历和大小断言。这样的热结构应该通过统一描述或生成代码维护,避免“在 C++ 结构末尾加一个 bool”看似小改动却破坏资源 ABI。

6. 扫描分为任务提交与结果接收#

fwScan 定义 node/entity/occlusion 的 fragment 接口,保存 scan base info、pending/current results、hi-Z buffers 和 pending task 计数。StartScan 准备查询形状并启动依赖,JoinScan 等待后交接结果。

这提供 CPU 并行可见性处理,并可按 render phase 使用不同 cull shape。主视图、级联阴影、反射等不能一律复用主摄像机可见性:画面外对象仍可能投下画面内阴影或出现在反射中。

源码中确有 hi-Z/occlusion 通路以及独立软光栅模块,但不能据此把整条管线称为现代 GPU-driven indirect culling。要证明 GPU-driven,必须另查 GPU 数据、命令生成与 indirect submission 调用;当前这里主要证据是框架 CPU/SPU 风格任务扫描。

阶段“锁”值得特别注意:LockSceneGraph 只增加整数计数。它用于检查某阶段不允许修改,不会为多线程访问建立互斥。新项目若采用类似只读阶段,必须确保结构修改、卸载与 descriptor 更新被严格安排在扫描前后。

7. LOD 是资源就绪、层级和渐变的联合状态#

LOD 类型包括 HD、LOD、SLOD1/2/3/4 以及 orphan HD。fwLodData 记录 parent、children 数、attached children 数、loaded、可见、alpha、子 LOD 距离和阴影强制显示等状态。

因此不能用“距离大于某阈值就换一个 mesh”概括。父替代表示是否可撤下,取决于子内容是否装入并挂接;多个容器的 SLOD 是更大尺度的替代表示。alpha 渐变避免突变,但一段过渡期可能同时维护两套数据并付出额外绘制/驻留成本。

源码有 8 位 alpha、最多 255 children、16 位 LOD distance 等布局限制。文件中 LODTYPES_MAX_NUM_LEVELS=6 的旧注释与列出的枚举类型数量不应混为一谈;orphan 类型和额外层级使“固定六种 LOD”这种简化不可靠,应按具体字段与使用处核对。

LODTYPES_TARGET_MILLIS=33 是这套 LOD 代码的时间参数,不是整个引擎锁 30 fps 的证据。

8. 多兴趣点预取#

fwBoxStreamer 为资产设置 bounding box、pre-stream distance 和类别;一次请求过程可以处理多个 search。游戏接口允许脚本实体等主动请求自己周围的静态碰撞,而不是只跟随主摄像机。

required old/new 双列表用于比较需求变化;pin by volume/user、ignore、culled 和 priority flags 又控制是否申请/回收。接口区分 Request、HasLoaded、EnsureLoaded,反映异步预测和强保证需求的区别。

推论:多个远离的兴趣点会把驻留集合变成多个区域的并集。例如远端任务、观战镜头、车辆高速移动和过场同时存在,预算可能大幅增加。解决方法要从玩法允许的兴趣点数、优先级和可见替代能力入手,而不是仅扩大搜索半径。

预取距离不是视距。最少需要考虑速度与方向、资产端到端加载延迟、依赖深度和最坏发布时间;详情见Streaming。

9. 世界范围、浮点精度与可扩展性边界#

WorldLimits.h 明确声明 ultimate X/Y 为 ±16000,Z 为 -1700 到 2700;world representation X/Y 为 ±8192。注释指出超出 representation 的对象被放入顶层 quadtree node,ultimate limits 则是代码不支持超越的界限。这是源码单位范围,不应自动转换成“实际地图大小”。

这证明此分支采用 有限范围世界表示。扩大常量不能自动获得无限世界:顶层候选膨胀、float 精度、量化 bound、物理 broadphase、LOD 距离字段、导航及存储格式都要审核。

若按 IEEE 754 单精度粗估,数值约 8192 时相邻可表示值步距约 0.0009766,约 16384 时约 0.0019531(均为坐标单位)。这只是数值分辨率,不是最终碰撞/动画误差;旋转、矩阵相乘和求解会引入额外误差。

本次没有在上述世界主路径确认通用双精度世界坐标或全系统 origin rebasing 协议。camera-relative extension 的存在也不能证明物理、脚本、网络、导航已一起 rebasing。新项目若需要跨大陆或行星尺度,应先定义 global/local 坐标层,明确转换和跨分区实体归属。

10. 固定容量与稠密热点#

SceneGraph 中示例池容量包括 1000 streamed nodes、150 interior nodes、256 rooms、400 portals;32/64 位结构有大小断言。这些是所读框架的静态默认,不等于内容文件中实例总数。世界可以流式复用槽位,但同时驻留复杂度仍受池容量限制。

一个面积很小的高密度区域可能比空旷大地图更昂贵:descriptor 数、portal 数、可见灯光、阴影投射者、物理查询和 map publication 同时变密。性能评估应同时覆盖“高速度穿越”和“静止看最复杂区域”,两者分别打击 streaming 与可见工作集。

11. 对新项目的可迁移设计#

  1. 分离资源身份、共享类型、实体实例和用于热点扫描的紧凑代理,明确每份状态的唯一写入者。
  2. 把块装入设计成“依赖满足—装配—主线程发布—加入空间索引”的事务;卸载反向处理并等待读者结束。
  3. LOD 替代不仅有几何,还应有资源可用性和交互降级策略;缺碰撞资源时不能仅显示一个代理 mesh 就宣告完成。
  4. 将兴趣点、预取、保护引用和预算纳入可视化工具,能回答某块为何仍驻留。
  5. 对世界范围/量化/索引位宽做系统级设计;需要无限世界时,不在有限世界常量上局部打补丁。
  6. 统一记录 scan candidates、各阶段存活数、加载/发布/删除数量,而非只看 draw calls。

12. 验证场景(未执行)#

沿固定路线高速移动、突然回头、室内外连续切换、传送到高密度块、观战远方玩家、多个脚本兴趣点、LOD 子块故意延迟、在扫描中申请卸载,分别观察资源 miss、可见性错误、主线程尖峰和池高水位。尤其应验证“文件已加载、世界尚未完全发布”窗口中 gameplay 的行为。

13. 源码证据#

文件关键内容
entity.h、archetype.h实例与共享类型边界
transform.htransform 类型分派与 pool
WorldRepMulti.h世界增删、扫描与 exterior root
SceneGraph.h指针修复、pool、128–131 阶段计数
WorldLimits.h15–27 有限世界与表示范围
EntityContainer.h292 起 SoA descriptor 和四元素块操作
EntityDesc.hflags、包围体、向量布局约束
Scan.cpppending/current、依赖任务和 StartScan/JoinScan
LodTypes.hLOD 类型、children、alpha、距离和加载标志
boxstreamer.h29–47 双列表;61–74 多兴趣点;99 起请求/保证/保护
mapdatastore.cpp106 起分类配置;FinishLoading/PlaceAsynchronously/SetResource

关联:渲染、物理、内存、性能。