Technical note

显示隐藏的快速状态切换

记录 CAD/CAE Viewer 中隐藏、显示、隔离、ShowAll 和 ResetStyle 这类高频交互的优化过程:为什么大模型下不能总是逐节点修改或全场景重建,以及如何通过 DisplayBucket、RangeTable 和 Selection Layer 控制状态恢复。


背景:显示隐藏是高频操作

在 CAD/CAE Viewer 里,隐藏、显示、隔离、ShowAll、ResetStyle 不是边缘功能。

工程用户会频繁做这些操作:

隐藏外壳,看内部结构;
隔离一个零件或一个 body;
临时隐藏选中的面或边;
调整透明度观察内部;
框选一批对象后隐藏;
修复或检查完成后 ShowAll;
材质和透明度改乱后 ResetStyle;
从工程树切换不同对象的可见状态。

这些操作看起来只是“显示状态变化”,但在大模型下很容易变成性能瓶颈。

原因是它们不仅影响渲染,还会影响拾取、高亮、工程树状态、RangeTable 语义和后续恢复路径。

所以显示隐藏优化的重点,不只是让某一次 Hide 更快,而是让整个状态系统可控。

这篇文章想记录的就是这个过程:在合批、RangeTable、Selection Layer 和空间分桶之后,显示状态应该怎么组织,才能避免每次操作都退回逐节点修改或全场景重建。

旧做法:直接切节点或改状态

旧 Viewer 中,显示状态通常和 OSG 节点直接绑定。

如果一个 face 或 edge 有自己的 Geode,那么隐藏它可以直接关节点;改颜色可以直接改节点颜色;调透明度可以直接改节点 StateSet 或材质状态。

这套路径可以简化成:

select face
    -> find face node
    -> set node mask / color / opacity

它直观、实现简单,也方便调试。

早期功能验证时,这种结构能快速打通工程树、拾取和三维显示联动。

但大模型会放大它的问题。

如果隐藏一个复杂对象需要逐个切换大量 face node 和 edge node,前台操作就会变慢。如果颜色、透明度、选择高亮、hover 都直接改基础节点,状态恢复就会变乱。

最典型的问题是:用户做了几轮隐藏、透明、选择、高亮之后,再执行 ShowAll 或 ResetStyle,系统必须知道每个对象应该恢复到什么状态。

旧结构里,这些状态容易混在同一层,恢复逻辑会越来越复杂。

合批之后的新问题

合批显示减少了 Geode 和 Drawable 数量,但也改变了隐藏逻辑。

旧结构里,一个 face 可以对应一个节点。隐藏它时,关掉这个节点就行。

合批之后,一个 Geometry 里可能包含很多 face。隐藏其中一个 face,不能直接关掉整个 Geometry,否则同 batch 里的其他 face 也会一起消失。

这带来新的判断:

如果整个 bucket 都要隐藏:
    可以直接切 bucket 状态。

如果只隐藏 bucket 里的部分 face:
    不能关整个 Geometry,
    需要局部处理或重建。

如果目标规模太大:
    不能阻塞前台,
    需要 fast path 或 fallback。

所以合批之后,显示隐藏不再是简单的 node mask 操作,也不能每次都全场景重建。

它需要根据目标范围、bucket 覆盖情况和当前状态选择路径。

这也是大模型显示引擎里比较典型的变化:过去一个直观操作,后来会变成一套分层状态系统。

状态要分层

显示隐藏变复杂后,一个核心工程判断是:状态必须分层。

至少要区分几类状态:

Base Display State:
    模型的基础颜色、材质、透明度、边显示等。

Visibility State:
    当前对象、face、edge 是否隐藏或隔离。

Temporary Override:
    临时颜色、透明度、局部显示覆盖。

Selection / Hover State:
    选择高亮、鼠标悬停、临时提示层。

如果这些状态混在一起,就会产生污染。

例如用户先把对象改透明,再选择高亮,再隐藏,最后 ShowAll。如果选择高亮和透明度都直接写到基础颜色里,恢复时就很难判断到底应该回到原始材质、用户修改后的透明度,还是清除所有覆盖。

更稳的方式是:

基础显示保持稳定;
隐藏和隔离由 visibility state 管理;
selection 和 hover 进入 Selection Layer;
临时样式覆盖有明确生命周期。

这样 ShowAll 只恢复可见性,不应该误改基础材质。

ResetStyle 可以清理样式覆盖,但不应该残留选择高亮。

ClearSelection 应该清理 Selection Layer,但不应该重建基础 Geometry。

这些边界如果不提前分清,后续每个功能都会“顺手恢复”别的状态,最后显示状态会越来越难维护。

Fast Path:完整 bucket 直接切状态

大模型下,显示状态切换必须有 fast path。

最典型的 fast path 是完整 bucket 处理。

如果一个 DisplayBucket 里的所有 face 都要隐藏,那么不需要逐 primitive 更新,也不需要重建 Geometry。可以直接把这个 bucket 的显示状态切掉。

可以理解为:

target faces
    -> locate buckets
    -> classify buckets

bucket fully affected:
    -> switch bucket state

bucket partially affected:
    -> dirty bucket / partial rebuild / fallback

完整 bucket 的好处是路径短,恢复也清晰。

ShowAll 时把这些 bucket 的状态恢复即可,不需要重新生成所有 Geometry。

这类 fast path 对隔离也很重要。

隔离一个对象时,很多 bucket 可能完整在目标外或目标内。完整在外的 bucket 可以直接隐藏,完整在内的 bucket 可以保留。只有混合 bucket 才需要更细的处理。

这也是空间分桶对显隐优化的直接价值:它给 fast path 提供了可判断的局部边界。

局部目标:dirty bucket 和 partial rebuild

如果目标只影响 bucket 的一部分,就不能简单关掉整个 bucket。

这时需要 dirty bucket。

dirty bucket 的意义是把局部变化限制在受影响的 bucket 内:

hide selected faces
    -> find affected bucket ids
    -> mark dirty buckets
    -> rebuild or patch affected buckets
    -> update RangeTable

这样隐藏少量 face 不会退化成全场景重建。

partial rebuild 可以重建受影响 bucket 的 Geometry。partial replacement 可以把旧 bucket 批次替换成新的批次,同时保留其他 bucket 不变。

不过这里最难的不是生成新 Geometry,而是一致性:

RangeTable 要更新到新的 primitive range;
Selection Layer 里的旧高亮要清理;
hover 不能继续指向旧对象;
ShowAll 要能恢复基础状态;
edge display 也要和 face 可见性保持一致。

所以局部隐藏和局部重建不是单独的渲染优化,而是一套状态一致性问题。

画面更新只是第一步。更新后,拾取、高亮、恢复、边显示和基础状态都要对得上。

ResetStyle 和 ShowAll 的恢复路径

显示状态切换里,恢复路径往往比隐藏路径更重要。

Hide 失败,用户能看到对象没隐藏。

ShowAll 或 ResetStyle 失败,问题可能更隐蔽:某些对象颜色不对、透明度残留、边线状态不一致、选择高亮还在、拾取结果和可见状态不匹配。

所以恢复路径要明确区分:

ShowAll:
    恢复可见性和隔离状态,
    不应该破坏基础材质。

ClearIsolate:
    清除隔离目标,
    恢复隔离影响的 bucket。

ResetStyle:
    清理颜色、透明度、材质覆盖,
    按规则恢复基础样式。

ClearSelection:
    清理 Selection Layer,
    不应该重建 base display。

这几个操作如果混在一起,就会出现“ShowAll 顺便把颜色也重置了”,或者“ResetStyle 把隐藏状态也弄乱了”这类问题。

在大模型里,恢复路径还要尽量避免全量重建。

能恢复 bucket 状态,就不要重建 Geometry;能清理临时层,就不要动 base display;只有混合状态或局部替换失效时,才进入更重的 rebuild 路径。

这类判断不一定显眼,但它决定了 Viewer 是否能在长时间交互后保持稳定。

显隐、高亮和透明度不能互相污染

显示状态切换最容易出问题的地方,是隐藏、透明度和高亮叠加。

比如一个对象被设置为半透明,又被选中高亮,然后被隐藏。

此时系统至少要回答:

隐藏后是否还能被拾取?
高亮层是否需要清理?
ShowAll 后透明度是否应该保留?
ResetStyle 后透明度是否恢复默认?
ClearSelection 是否应该影响可见性?

这些问题没有一个放之四海皆准的答案,取决于产品语义。

但显示引擎必须让这些状态在结构上可区分。

一个比较稳的分层是:

Base material:
    模型默认或用户持久设置。

Visibility:
    hidden / isolated / visible。

Selection Layer:
    selected / hover / temporary feedback。

Style override:
    颜色、透明度、材质覆盖。

有了分层,状态恢复才有边界。

没有分层,每个功能都会试图“顺手恢复”别的功能,最后状态越来越乱。

和 RangeTable 的关系

RangeTable 让合批后的 Geometry 仍然能找回 CAD 语义。

显示隐藏会直接影响 RangeTable 的使用方式。

隐藏对象后,拾取不能再返回隐藏目标。隔离模式下,拾取也应该只接受当前可见范围内的对象。局部重建后,旧 primitive range 可能失效,RangeTable 必须同步更新。

所以 visibility state 不能只是一层视觉开关。

它还要影响交互语义。

可以理解为:

Display state:
    控制是否画出来。

RangeTable + visibility filter:
    控制是否能被解释为有效拾取目标。

否则就会出现画面上看不到,但鼠标还能点到;或者画面已经更新,但拾取返回旧 face 的问题。

这类问题在 CAD/CAE Viewer 里尤其危险,因为用户的后续操作通常依赖拾取结果。拾取语义错了,后续高亮、测量、修复、边界设置都有可能跟着错。

和 Selection Layer 的关系

Selection Layer 负责选择、hover 和临时高亮。

显示隐藏会影响它的生命周期。

隐藏一个对象时,如果它正被选中,高亮应该怎么处理?通常至少不能继续显示一个已经隐藏的对象。

ClearSelection 时,只应该清理选择层,不应该改变隐藏状态。

ShowAll 时,如果恢复了可见性,是否恢复选择高亮,取决于产品语义。但无论如何,Selection Layer 不能引用已经被局部重建替换掉的旧 Geometry。

所以状态切换里要有清理动作:

hide / isolate:
    update visibility
    clear or refresh affected highlights

partial rebuild:
    remove stale highlight batch
    rebuild highlight if needed

clear scene:
    cancel pending selection highlight
    clear temporary layers

这也是为什么 Selection Layer 不能散落在业务流程中,而要有统一入口和生命周期管理。

只要临时层没有清理干净,显示状态就很容易在多次操作后出现残留。

和 DisplayBucket 的关系

DisplayBucket 是显示隐藏 fast path 的基础。

没有 bucket 时,合批 Geometry 太大,局部隐藏容易变成全局操作。

有了 bucket 后,可以把目标映射到受影响 bucket:

完整 bucket 直接切状态;
混合 bucket 进入 dirty bucket;
局部变化走 partial rebuild 或 fallback;
ShowAll 恢复被修改的 bucket;
大范围操作按 chunk 分批处理。

bucket 粒度会直接影响状态切换效果。

bucket 太小,Geode / Drawable 数量又会上升,接近旧问题。

bucket 太大,局部隐藏和高亮不够轻,dirty bucket 的收益下降。

因此显示隐藏优化和空间分桶不是两件事。

前者依赖后者给出合适的局部边界。

边界和代价

快速状态切换不是免费得到的。

首先,它需要更多状态记录。

哪些 bucket 被隐藏过,哪些发生过局部替换,哪些样式被覆盖,哪些选择高亮还在,都要有明确归属。

其次,它需要多条路径。

完整 bucket 可以快,混合 bucket 要谨慎,大范围目标要分块,异常情况要 fallback。路径越多,一致性越难维护。

再次,恢复路径必须严格。

ShowAll、ClearIsolate、ResetStyle、ClearSelection 都是高频操作,它们不能互相污染。

最后,fast path 不能牺牲语义正确性。

隐藏对象后拾取仍然命中,就是错误;ResetStyle 后材质不对,也是错误;局部替换后 RangeTable 没同步,同样是错误。

所以这类优化的标准不是“某条路径最快”,而是:

常见操作足够快;
恢复路径足够可靠;
显示语义不会乱。

小结

在 CAD/CAE Viewer 中,隐藏、显示、隔离、ShowAll、ResetStyle 是高频交互,不是辅助功能。

旧 Viewer 直接切节点、改颜色、改透明度,在小模型上很合理。但大模型和合批显示之后,这种做法会遇到两个问题:逐节点更新太慢,状态恢复容易污染。

新的工程判断是:

显示状态要分层,
操作路径要分级。

基础显示、可见性、临时覆盖、选择高亮要分开。

完整 bucket 走 fast path,局部目标走 dirty bucket 或 partial rebuild,大范围操作必要时走 hint 或 fallback。

ShowAll 和 ResetStyle 要优先保证恢复路径清晰。

这部分优化连接了 DisplayBucket、RangeTable、Selection Layer 和 partial rebuild。它的目标不是让某一次 Hide 看起来更快,而是让大模型中的显示状态切换长期保持可控。

下一篇可以继续讲 CAD 拾取系统。因为当显示状态、RangeTable 和 Selection Layer 都拆开之后,鼠标点击、hover、边拾取、对象提升、框选和特征点捕捉也需要一套更清晰的语义解析流程。