You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Replace the scattered geometry management variables (_all_geometries, _geometry_layer_order, _layer_positions) with a GeometryLayerManager that owns layer grouping, ordered visibility cycling, and chunk loading coordination.
Why not a scene graph?
The original proposal was a full scene graph with parent-child transform inheritance, dirty-flag propagation, AABB computation, and frustum culling. This is the wrong abstraction for a hardware ray tracer:
OptiX IAS is inherently flat. The Instance Acceleration Structure takes a flat list of instances, each with a 3×4 transform matrix and a GAS handle. There is no parent-child nesting in OptiX — a CPU-side tree would need to be flattened to the IAS on every change anyway, adding complexity without GPU benefit.
Frustum culling is redundant. OptiX's BVH traversal already skips geometry outside the ray frustum at the hardware level. CPU-side frustum culling on top of hardware BVH traversal only saves IAS rebuild cost, which is negligible for the instance counts rtxpy deals with (<1000 instances).
Dirty-flag transform propagation solves a rasterizer problem. In a rasterizer, the CPU computes the model-view-projection matrix chain and dirty flags avoid redundant matrix multiplies. In OptiX, instance transforms are uploaded as flat 3×4 matrices — there's no transform chain to propagate through. The existing _render_needed flag already handles "something changed, re-render."
Parent-child transform inheritance doesn't match the data. Buildings don't inherit terrain transforms — they're placed at absolute world positions with Z sampled from terrain. When vertical exaggeration changes, meshes are rebuilt with new Z values, not re-transformed via a parent node. Observers, overlays, and UI elements don't have spatial parent-child relationships.
Group visibility already works._geometry_layer_order cycling and per-GAS visible flags handle this. The issue is just that the data structures are scattered across InteractiveViewer instance variables.
What's actually needed: a single manager class that consolidates the scattered layer state and provides clean APIs for visibility cycling, layer ordering, and chunk coordination.
Current State
Scattered geometry management across InteractiveViewer:
_all_geometries: flat list of geometry IDs (engine.py:1249)
_geometry_layer_order: flat list of layer names for cycling visibility (engine.py:1252)
_layer_positions: dict[layer_name → list of (x, y, z, geom_id)] (engine.py:1254)
_active_geometry_layer: currently visible geometry layer index (engine.py:1255)
RTX multi-GAS: _GeometryState.gas_entries is a flat dict[str → _GASEntry] (rtx.py:141-212)
Each _GASEntry stores its own 3×4 transform matrix and visible flag (rtx.py:28-45)
Chunk loading via _MeshChunkManager (engine.py) — independently positioned, managed separately from the layer system.
Layer cycling logic is duplicated across terrain layers, basemap layers, geometry layers, and point cloud color modes — each with its own index, order list, and wrap-around logic.
Proposed Design
GeometryLayerManager
classGeometryLayer:
"""A named group of related geometries (e.g., 'buildings', 'roads')."""name: strgeometry_ids: list[str] # GAS entry IDs in this layerpositions: list[tuple] # (x, y, z, geom_id) per instancevisible: bool=Truechunk_manager: Optional[MeshChunkManager] =NoneclassGeometryLayerManager:
"""Owns all geometry layer state, replaces scattered instance variables."""layers: dict[str, GeometryLayer]
layer_order: list[str] # Ordered for cyclingactive_index: int=0defadd_layer(self, name: str, geometry_ids: list[str]) ->GeometryLayer: ...
defremove_layer(self, name: str): ...
defcycle_layer(self, direction: int=1): ...
defset_visible(self, name: str, visible: bool): ...
deftoggle_layer(self, name: str): ...
defload_chunks_near(self, camera_pos: np.ndarray, rtx: RTX): ...
defunload_distant_chunks(self, camera_pos: np.ndarray, rtx: RTX): ...
Generic cycle helper
Unify the four separate cycling implementations (terrain, basemap, geometry, point cloud) with a shared helper:
defcycle_index(current: int, count: int, direction: int=1) ->int:
"""Cycle through 0..count-1, wrapping around. Returns new index."""ifcount==0:
return0return (current+direction) %count
Each manager (terrain, overlay, geometry) uses this internally — no generic CycleSystem or event needed.
Integration with RTX
GeometryLayerManager calls into RTX for visibility changes:
_MeshChunkManager instances become owned by their respective GeometryLayer, not by the viewer directly
The _GASEntry dataclass in rtx.py stays unchanged — it already has the right fields (transform, visible, handle)
IAS rebuild (rtx.py:_rebuild_ias()) continues to iterate the flat gas_entries dict — no scene graph traversal needed
place_mesh() / place_buildings() should register new geometries with the layer manager
If profiling ever shows IAS rebuild is a bottleneck with very large instance counts, frustum culling can be added as a filter in the manager's chunk loading path — but it's not needed now
Acceptance Criteria
GeometryLayerManager class consolidating _all_geometries, _geometry_layer_order, _layer_positions, _active_geometry_layer
GeometryLayer groups related GAS entries with shared visibility toggle
cycle_layer() replaces the current scattered cycling logic
Chunk managers owned by their respective layers
Existing explore() functionality preserved — no user-visible behavior change
Unit tests for layer add/remove, visibility toggle, cycling
Parent: #57 — Phase 1 Architecture
Summary
Replace the scattered geometry management variables (
_all_geometries,_geometry_layer_order,_layer_positions) with aGeometryLayerManagerthat owns layer grouping, ordered visibility cycling, and chunk loading coordination.Why not a scene graph?
The original proposal was a full scene graph with parent-child transform inheritance, dirty-flag propagation, AABB computation, and frustum culling. This is the wrong abstraction for a hardware ray tracer:
OptiX IAS is inherently flat. The Instance Acceleration Structure takes a flat list of instances, each with a 3×4 transform matrix and a GAS handle. There is no parent-child nesting in OptiX — a CPU-side tree would need to be flattened to the IAS on every change anyway, adding complexity without GPU benefit.
Frustum culling is redundant. OptiX's BVH traversal already skips geometry outside the ray frustum at the hardware level. CPU-side frustum culling on top of hardware BVH traversal only saves IAS rebuild cost, which is negligible for the instance counts rtxpy deals with (<1000 instances).
Dirty-flag transform propagation solves a rasterizer problem. In a rasterizer, the CPU computes the model-view-projection matrix chain and dirty flags avoid redundant matrix multiplies. In OptiX, instance transforms are uploaded as flat 3×4 matrices — there's no transform chain to propagate through. The existing
_render_neededflag already handles "something changed, re-render."Parent-child transform inheritance doesn't match the data. Buildings don't inherit terrain transforms — they're placed at absolute world positions with Z sampled from terrain. When vertical exaggeration changes, meshes are rebuilt with new Z values, not re-transformed via a parent node. Observers, overlays, and UI elements don't have spatial parent-child relationships.
Group visibility already works.
_geometry_layer_ordercycling and per-GASvisibleflags handle this. The issue is just that the data structures are scattered acrossInteractiveViewerinstance variables.What's actually needed: a single manager class that consolidates the scattered layer state and provides clean APIs for visibility cycling, layer ordering, and chunk coordination.
Current State
Scattered geometry management across
InteractiveViewer:_all_geometries: flat list of geometry IDs (engine.py:1249)_geometry_layer_order: flat list of layer names for cycling visibility (engine.py:1252)_layer_positions: dict[layer_name → list of (x, y, z, geom_id)] (engine.py:1254)_active_geometry_layer: currently visible geometry layer index (engine.py:1255)_GeometryState.gas_entriesis a flat dict[str → _GASEntry] (rtx.py:141-212)_GASEntrystores its own 3×4 transform matrix andvisibleflag (rtx.py:28-45)Chunk loading via
_MeshChunkManager(engine.py) — independently positioned, managed separately from the layer system.Layer cycling logic is duplicated across terrain layers, basemap layers, geometry layers, and point cloud color modes — each with its own index, order list, and wrap-around logic.
Proposed Design
GeometryLayerManager
Generic cycle helper
Unify the four separate cycling implementations (terrain, basemap, geometry, point cloud) with a shared helper:
Each manager (terrain, overlay, geometry) uses this internally — no generic CycleSystem or event needed.
Integration with RTX
GeometryLayerManagercalls intoRTXfor visibility changes:Transforms remain flat 3×4 matrices on
_GASEntry— no hierarchy, matching OptiX's native model.Implementation Notes
GeometryLayerManageris one of the subsystem objects held byInteractiveViewer_MeshChunkManagerinstances become owned by their respectiveGeometryLayer, not by the viewer directly_GASEntrydataclass inrtx.pystays unchanged — it already has the right fields (transform, visible, handle)rtx.py:_rebuild_ias()) continues to iterate the flatgas_entriesdict — no scene graph traversal neededplace_mesh()/place_buildings()should register new geometries with the layer managerAcceptance Criteria
GeometryLayerManagerclass consolidating_all_geometries,_geometry_layer_order,_layer_positions,_active_geometry_layerGeometryLayergroups related GAS entries with shared visibility togglecycle_layer()replaces the current scattered cycling logicexplore()functionality preserved — no user-visible behavior change