diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py index 4c94495b..6e6d893d 100644 --- a/ssbo_component/ssbo_controller.py +++ b/ssbo_component/ssbo_controller.py @@ -1,4 +1,5 @@  +import math from panda3d.core import GeomNode, GeomVertexFormat, GeomVertexWriter from panda3d.core import InternalName, LMatrix4f, NodePath, Vec3 import time @@ -12,8 +13,9 @@ class ObjectController: - 提交: 离开 chunk 时仅重建该 chunk 的静态表示 """ - def __init__(self, chunk_size=64): + def __init__(self, chunk_size=64, chunk_world_size=40.0): self.chunk_size = max(8, int(chunk_size)) + self.chunk_world_size = max(8.0, float(chunk_world_size)) self._reset_state() def _reset_state(self): @@ -39,6 +41,9 @@ class ObjectController: # } self.chunks = {} self.active_chunks = set() + self._next_chunk_id = 0 + # spatial cell key -> [chunk_id, ...] + self._cell_to_chunks = {} # UI hierarchy metadata (matches source model parent/child structure) self.tree_root_key = None @@ -138,6 +143,33 @@ class ObjectController: self.chunks[chunk_id] = chunk_data return chunk_data + def _get_cell_key_from_pos(self, pos): + inv = 1.0 / self.chunk_world_size + return ( + int(math.floor(pos.x * inv)), + int(math.floor(pos.y * inv)), + int(math.floor(pos.z * inv)), + ) + + def _allocate_spatial_chunk(self, root_np, world_pos): + """ + Allocate object into a spatially-local chunk for better frustum culling. + Objects in the same world cell are grouped together, and overflow creates + another chunk for that same cell. + """ + cell_key = self._get_cell_key_from_pos(world_pos) + chunk_ids = self._cell_to_chunks.setdefault(cell_key, []) + + for chunk_id in chunk_ids: + chunk = self.chunks.get(chunk_id) + if chunk and len(chunk["members"]) < self.chunk_size: + return chunk_id, chunk + + chunk_id = self._next_chunk_id + self._next_chunk_id += 1 + chunk_ids.append(chunk_id) + return chunk_id, self._ensure_chunk(root_np, chunk_id) + def _rebuild_static_chunk(self, chunk_id): chunk = self.chunks.get(chunk_id) if not chunk: @@ -241,8 +273,8 @@ class ObjectController: pick_gnode = GeomNode(f"pick_{global_id}") pick_gnode.add_geom(pick_geom, gnode.get_geom_state(gi)) - chunk_id = global_id // self.chunk_size - chunk = self._ensure_chunk(scene_root, chunk_id) + world_pos = world_mat.get_row3(3) + chunk_id, chunk = self._allocate_spatial_chunk(scene_root, world_pos) obj_np = chunk["dynamic_np"].attach_new_node(render_gnode) obj_np.set_mat(world_mat) @@ -269,6 +301,7 @@ class ObjectController: t2 = time.time() print(f"[控制器] Static chunk flatten took {(t2 - t1) * 1000:.0f}ms") print(f"[控制器] Built {len(self.chunks)} chunks, {global_id} objects") + print(f"[控制器] Spatial chunking: cell={self.chunk_world_size:.1f}, max_members={self.chunk_size}") # Fill per-node aggregate IDs and build deterministic preorder list for UI. self._aggregate_tree_ids(self.tree_root_key) diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index f323fd52..5a671f1a 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -83,6 +83,11 @@ class SSBOEditor: self._last_group_sync_mat = None self._last_single_sync_gid = None self._last_single_sync_mat = None + # Performance toggle: forcing shadow tasks every frame is expensive. + # Keep it off by default so frustum/content reduction has clearer FPS impact. + self.realtime_shadow_updates = False + self._scheduler_tasks_original = None + self._realtime_shadow_tasks_enabled = False # Initialize ImGui Backend if not already present if not hasattr(self.base, 'imgui_backend'): @@ -109,6 +114,13 @@ class SSBOEditor: if model_path: self.load_model(model_path) + def _capture_scheduler_tasks_snapshot(self): + scheduler = getattr(self.rp, "task_scheduler", None) + if not scheduler or not hasattr(scheduler, "_tasks"): + return + if self._scheduler_tasks_original is None: + self._scheduler_tasks_original = [list(frame_tasks) for frame_tasks in scheduler._tasks] + def _enable_realtime_shadow_tasks(self): """ Force PSSM-related scheduled tasks to run every frame to avoid visible @@ -118,6 +130,8 @@ class SSBOEditor: if not scheduler or not hasattr(scheduler, "_tasks"): return + self._capture_scheduler_tasks_snapshot() + required = { "pssm_scene_shadows", "pssm_distant_shadows", @@ -134,6 +148,26 @@ class SSBOEditor: if changed: print("[SSBOEditor] Realtime shadow tasks enabled (PSSM updates every frame).") + self._realtime_shadow_tasks_enabled = True + + def _disable_realtime_shadow_tasks(self): + """Restore scheduler layout captured before realtime shadow override.""" + scheduler = getattr(self.rp, "task_scheduler", None) + if not scheduler or not hasattr(scheduler, "_tasks"): + return + if self._scheduler_tasks_original is None: + self._realtime_shadow_tasks_enabled = False + return + scheduler._tasks[:] = [list(frame_tasks) for frame_tasks in self._scheduler_tasks_original] + self._realtime_shadow_tasks_enabled = False + + def set_realtime_shadow_updates(self, enabled): + """Public toggle for aggressive per-frame shadow updates.""" + self.realtime_shadow_updates = bool(enabled) + if self.realtime_shadow_updates: + self._enable_realtime_shadow_tasks() + else: + self._disable_realtime_shadow_tasks() def load_font(self): """Load custom font for ImGui""" @@ -186,8 +220,8 @@ class SSBOEditor: self.model.reparent_to(self.base.render) - # Keep shadow feedback responsive during interactive edits. - self._enable_realtime_shadow_tasks() + # Keep this off by default for better overall FPS/scaling with visibility. + self.set_realtime_shadow_updates(self.realtime_shadow_updates) # NO rp.set_effect() — use RP default rendering for max FPS # NO SSBO creation — vertex positions are baked