第三篇:松散八叉树、osg::Polytope 与 LRU 缓存
摘要
本文结合SceneObjectIndexManager与SceneLooseOctree,说明松散八叉树如何管理要素与临时对象;结合FeatureDispatcher中osg::Polytope与View*Proj 逆构造视锥,说明对queryFeatureObjs/queryTempObjs的调度前粗可见集;并概括FeatureLRUCache、密集点云 DPCLRU、PyrmidImgLRUCache* 在节点子图与图像数据上的分工。
1. 松散八叉树在工程中的位置
- SceneObjectIndexManager内维护SceneLooseOctree等结构,expandLooseOctree、addSceneObjectToLooseOctree等实现根扩展与子树挂接。
- 临时对象:若初始无有效包围盒,可先标记待加载后入树;有盒则addTempSceneObjectWorkEntity直接入索引树。
- 作用:拾取/查询/调度候选集的空间一级过滤,不替代 OSG 绘制级裁剪。
2. 视锥:osg::Polytope 与双查询接口
在FeatureDispatcher的更新路径中,典型步骤为:
viewfrustum.setToUnitFrustum();ViewMatrix * ProjMatrix合成矩阵,transformProvidingInverse到世界空间;- SceneObjectIndexManager::queryFeatureObjs与queryTempObjs各返回一列表。
头文件中query* 的注释为「用于调度时查询可见的…」,与第三篇主题一致。
3. 流程图:从相机到「可见对象再调度」
4. LRU 缓存分层
| 缓存 | 键/值思想 | 说明 |
|---|---|---|
| FeatureLRUCache | 以要素场景对象相关 key 缓存osg::Transform | FeatureDispatcher可配默认容量/开关;缓解重复符号化/节点构建。 |
| DPCLRUCache(密集点云) | 层 id、level、code 等 | 命中复用MatrixTransform;未命中再读块数据。 |
| PyrmidImgLRUCache | 金字塔图 key | 值内OpenCV Mat,与点云/金字塔体渲染相关。 |
与「分级精度」关系:分级决定要哪一级数据;LRU决定该级/该块在内存中是否复用,二者相乘做性能。
5. 类图(空间索引 + 查询,概念)
- 八叉树管空间归属,Polytope 管当前帧粗可见性,LRU 管时间局部性,三者不互相替代。
- 先 query 再 append 调度对大场景要素的意义,避免全量对象参与加载决策。
6:典型踩坑
1. 认为「进八叉树 = 能拾取、能参与所有查询」
对临时对象,若初始包围体无效(资源未加载、盒未算好),会走isNeedUpdateQuadtreeOrOctree一类路径,先不入树;待加载成功、盒有效再补登记。工程里在屏选/求交时,除了queryCanBePickTempObjs(走树)之外,还显式再扫一遍「因无有效包围盒而从未进八叉树的临时对象」列表。若你只做树查询、忘记这条旁路,会出现:模型已显示但点不中或拾取结果缺对象。这是数据状态与空间索引不同步的经典坑。
2. LOD 与八叉树:用「当前子图算出来的盒」硬塞回树
在屏幕拾取对临时模型的补充逻辑里,注释写得很直:若已是 LOD 一类节点,包围盒不固定,不能用当前算出的盒去重新加入八叉树,相关行甚至被注释成「先不 setBound、不 remove+add 回场景」。若强行用单帧的包围盒更新树,会造成树结构抖动、查询错误或与调度状态打架。这属于分级 / 动态范围与静态空间划分混用的坑。
3. 视锥体构造:矩阵顺序与transformProvidingInverse的含义
调度里用setToUnitFrustum+ViewMatrix * ProjMatrix的逆把标准视棱台变到世界/一致的空间中,再交给queryFeatureObjs/queryTempObjs。若左右手系、行主列主、先 View 后 Proj 还是反过来写错,表现是**「粗可见集」全空或全满**,随后加载/调度全部反常。这坑通常出在从别处抄矩阵时未与当前工程的CameraParams定义对齐。
4. 把「八叉树可见」等同「GPU 要画」
树 +Polytope粗筛是为业务调度/加载服务的;OSG仍有自己的裁切和 LOD。排障时要么分两层看(业务可见 vs 绘制),要么加日志区分index 层与cull 层。
5. 视口增删、八叉树、图层
要素侧注释提到:视口增删时会从八叉树和图层里同步移除部分对象,相机一动可能又加回——并标明为扩展预留、实现待收束。若你在内存/闪烁上感到别扭,要意识到这里存在**「粗调度策略」与「产品期望常驻」** 的潜在冲突,不全是 LRU 或树深度的问题。
6. LRU:键变=缓存失效+泄漏感
若同一逻辑对象在图层级、块编码、层级上因状态变化换了缓存键,会表现为反复读盘、却命中不了;若键过粗又会导致大颗粒淘汰,误伤别的块。这属于键设计坑,和第三篇的「空间与时间要一致」直接相关。
7:参数调优心得
1. 松散八叉树:深度与根范围
- 过深:节点管理开销、锁竞争和分裂次数上升;过浅:叶子里对象过多,视锥查询退化成「半暴力」。工程侧若有maxDepth、根扩展/收缩等入口,调参要配合单场景对象尺度分布和视口尺度做折中。
- 根盒扩展策略若偏激进,空场景里树也很胖,第一次加载的代价大;若偏保守,对象一出界就动根,要关注锁与全树更新频率。
2. 与「盒何时有效」打配合
心得:入树/更新树尽量与资源加载后重算包围体同一临界区或同一回调阶段,并统一isNeedUpdateQuadtreeOrOctree的置位/清除,否则调树参数也救不了时序性偶发问题。临时大模型在入树前的拾取、框选,要接受走旁路列表的额外成本,或在产品上弱化为「载完才能精拾取」。
3. 视锥查询:Polytope与交类型SceneObjectIndexManager的query系* 带交型枚举(如相交/包含的语义若工程里有区分),从视」调度若过严会少载,过松会多载。调的是「业务能接受多预载多少」而不是一味追求数学严。屏选用屏幕子区域构造Polytope+VPW逆变换时,窗口坐标上下颠倒、左右界要与当前视口约定一致。
4.FeatureLRUCache:容量与开关FeatureDispatcher侧对要素节点缓存有默认容量与开关接口。
- 容量过大:内存与单帧淘汰成本上升;过小:重复符号化/挂接,CPU 和锁开销上升。
- 全关缓存适合排障或强一致刷新的调试阶段,不适合作为默认发版。心得:以典型场景(同屏对象数、同符号重复度)压一版,再定容量档。
5. 密集点云 / 分块类 LRU
键里通常含层、级、块编码:与瓦片/分级加载一致时,命中率最高。调优时优先保证生成键的规则稳定,再动容量;避免在键里混进会每帧变的相机相关量(除非你就想做帧缓存,但那一般是另一套策略)。
6. 金字塔/图像 LRU(PyrmidImgLRUCache)
与OpenCV 矩阵生命周期绑定,心得:释放路径要把clearUp/ release跑全,避免在高分辨率屏上多图层时Mat 堆满内存。容量往往按单张图估计 × 可并行层数留余量。
7. 调优观
第三篇的旋钮不是单点,而是三角关系:八叉树参数管空间分桶是否合适,视锥+查询语义管本帧进调度集合是否合适,LRU管时间局部性;三者只调其一往往指标改善有限,联动看日志才像资深实现者。
8. 关键词
SceneObjectIndexManager、SceneLooseOctree、osg::Polytope、queryFeatureObjs、queryTempObjs、FeatureLRUCache、DPCLRUCache、PyrmidImgLRUCache