From e99c7e624e34adc5d7e049b85728083dac5c6f91 Mon Sep 17 00:00:00 2001 From: Evan Scamehorn Date: Tue, 16 Dec 2025 19:31:58 -0600 Subject: [PATCH] optimize raycasting --- src/InputManager.js | 67 +++++++++++++++++++++++++++++++++++---------- src/main.js | 2 +- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/InputManager.js b/src/InputManager.js index 41d8187..09cc47e 100644 --- a/src/InputManager.js +++ b/src/InputManager.js @@ -15,17 +15,27 @@ export class InputManager { this.dragObject = null; this.isPanning = false; + // Optimization: Cache the ground mesh so we don't search for it every frame + this.groundMesh = null; + // Callbacks - this.onClick = null; // (point, object) -> void - this.onDrag = null; // (object, newPoint) -> void - this.onDragEnd = null; // () -> void - this.onHover = null; // (point) -> void <-- NEW + this.onClick = null; + this.onDrag = null; + this.onDragEnd = null; + this.onHover = null; } init() { this.domElement.addEventListener('pointerdown', this.onPointerDown.bind(this)); this.domElement.addEventListener('pointermove', this.onPointerMove.bind(this)); this.domElement.addEventListener('pointerup', this.onPointerUp.bind(this)); + + // OPTIMIZATION 1: Find and cache the ground mesh once. + // We assume the object is named "GROUND" as per main.js + this.groundMesh = this.scene.getObjectByName("GROUND"); + if (!this.groundMesh) { + console.warn("InputManager: Ground mesh not found during init. Raycasting may fail."); + } } onPointerDown(event) { @@ -48,6 +58,7 @@ export class InputManager { onPointerMove(event) { // Case A: Dragging a Marker if (this.dragObject) { + // Use the optimized ground check const hit = this.raycastGround(event); if (hit && this.onDrag) { this.onDrag(this.dragObject, hit.point); @@ -55,8 +66,8 @@ export class InputManager { return; } - // Case B: Hovering (Ghost Marker Logic) <-- NEW - // We only care about hovering the ground for placing new nodes + // Case B: Hovering (Ghost Marker Logic) + // OPTIMIZATION: This runs every frame. It must be blazing fast. const hit = this.raycastGround(event); if (hit && this.onHover) { this.onHover(hit.point); @@ -94,19 +105,47 @@ export class InputManager { return new THREE.Vector2(x, y); } + /** + * OPTIMIZATION 2: Filtered Raycast + * Instead of checking scene.children (which checks 300k triangles in the city mesh), + * we construct a small list of interactable objects: [Ground, ...Markers]. + */ raycast(event) { + if (!this.groundMesh) this.groundMesh = this.scene.getObjectByName("GROUND"); + this.raycaster.setFromCamera(this.getMouse(event), this.camera); - // Ignore Ghost Marker in standard raycast interaction - const intersects = this.raycaster.intersectObjects(this.scene.children, true); - return intersects.find(obj => - (obj.object.name === "GROUND" || obj.object.userData.isMarker) && - obj.object.name !== "GHOST_MARKER" - ); + + // 1. Gather interactables + const interactables = [this.groundMesh]; + + // Efficiently gather markers without traversing deep hierarchies + // We scan the top level children. If markers are grouped, update this loop. + for (let i = 0; i < this.scene.children.length; i++) { + const child = this.scene.children[i]; + if (child.userData.isMarker && child.name !== "GHOST_MARKER") { + interactables.push(child); + } + } + + // 2. Raycast against ONLY these objects + // Recursive = false (we assume markers and ground are simple meshes) + const intersects = this.raycaster.intersectObjects(interactables, false); + + return intersects.length > 0 ? intersects[0] : null; } + /** + * OPTIMIZATION 3: Dedicated Ground Raycast + * Only checks the ground plane. + * Used heavily in onPointerMove. + */ raycastGround(event) { + if (!this.groundMesh) return null; + this.raycaster.setFromCamera(this.getMouse(event), this.camera); - const intersects = this.raycaster.intersectObjects(this.scene.children, true); - return intersects.find(obj => obj.object.name === "GROUND"); + + // intersectObject (singular), recursive = false + const intersects = this.raycaster.intersectObject(this.groundMesh, false); + return intersects.length > 0 ? intersects[0] : null; } } diff --git a/src/main.js b/src/main.js index 93a459a..048a091 100644 --- a/src/main.js +++ b/src/main.js @@ -52,6 +52,7 @@ let currentViewMode = 'none'; // 'none', 'zoning', 'approval' function init() { setupScene(); + // 1. Core Systems routeManager = new RouteManager(scene, SETTINGS); uiManager = new UIManager(routeManager); @@ -381,7 +382,6 @@ function renderCity(data) { // 7. Material Setup const mat = new THREE.MeshLambertMaterial({ vertexColors: true, // IMPORTANT - roughness: 0.6, shadowSide: THREE.BackSide });