optimize raycasting
All checks were successful
Deploy to GitHub Pages / deploy (push) Has been skipped

This commit is contained in:
Evan Scamehorn
2025-12-16 19:31:58 -06:00
parent b1616c293e
commit e99c7e624e
2 changed files with 54 additions and 15 deletions

View File

@@ -15,17 +15,27 @@ export class InputManager {
this.dragObject = null; this.dragObject = null;
this.isPanning = false; this.isPanning = false;
// Optimization: Cache the ground mesh so we don't search for it every frame
this.groundMesh = null;
// Callbacks // Callbacks
this.onClick = null; // (point, object) -> void this.onClick = null;
this.onDrag = null; // (object, newPoint) -> void this.onDrag = null;
this.onDragEnd = null; // () -> void this.onDragEnd = null;
this.onHover = null; // (point) -> void <-- NEW this.onHover = null;
} }
init() { init() {
this.domElement.addEventListener('pointerdown', this.onPointerDown.bind(this)); this.domElement.addEventListener('pointerdown', this.onPointerDown.bind(this));
this.domElement.addEventListener('pointermove', this.onPointerMove.bind(this)); this.domElement.addEventListener('pointermove', this.onPointerMove.bind(this));
this.domElement.addEventListener('pointerup', this.onPointerUp.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) { onPointerDown(event) {
@@ -48,6 +58,7 @@ export class InputManager {
onPointerMove(event) { onPointerMove(event) {
// Case A: Dragging a Marker // Case A: Dragging a Marker
if (this.dragObject) { if (this.dragObject) {
// Use the optimized ground check
const hit = this.raycastGround(event); const hit = this.raycastGround(event);
if (hit && this.onDrag) { if (hit && this.onDrag) {
this.onDrag(this.dragObject, hit.point); this.onDrag(this.dragObject, hit.point);
@@ -55,8 +66,8 @@ export class InputManager {
return; return;
} }
// Case B: Hovering (Ghost Marker Logic) <-- NEW // Case B: Hovering (Ghost Marker Logic)
// We only care about hovering the ground for placing new nodes // OPTIMIZATION: This runs every frame. It must be blazing fast.
const hit = this.raycastGround(event); const hit = this.raycastGround(event);
if (hit && this.onHover) { if (hit && this.onHover) {
this.onHover(hit.point); this.onHover(hit.point);
@@ -94,19 +105,47 @@ export class InputManager {
return new THREE.Vector2(x, y); 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) { raycast(event) {
if (!this.groundMesh) this.groundMesh = this.scene.getObjectByName("GROUND");
this.raycaster.setFromCamera(this.getMouse(event), this.camera); this.raycaster.setFromCamera(this.getMouse(event), this.camera);
// Ignore Ghost Marker in standard raycast interaction
const intersects = this.raycaster.intersectObjects(this.scene.children, true); // 1. Gather interactables
return intersects.find(obj => const interactables = [this.groundMesh];
(obj.object.name === "GROUND" || obj.object.userData.isMarker) &&
obj.object.name !== "GHOST_MARKER" // 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) { raycastGround(event) {
if (!this.groundMesh) return null;
this.raycaster.setFromCamera(this.getMouse(event), this.camera); 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;
} }
} }

View File

@@ -52,6 +52,7 @@ let currentViewMode = 'none'; // 'none', 'zoning', 'approval'
function init() { function init() {
setupScene(); setupScene();
// 1. Core Systems // 1. Core Systems
routeManager = new RouteManager(scene, SETTINGS); routeManager = new RouteManager(scene, SETTINGS);
uiManager = new UIManager(routeManager); uiManager = new UIManager(routeManager);
@@ -381,7 +382,6 @@ function renderCity(data) {
// 7. Material Setup // 7. Material Setup
const mat = new THREE.MeshLambertMaterial({ const mat = new THREE.MeshLambertMaterial({
vertexColors: true, // IMPORTANT vertexColors: true, // IMPORTANT
roughness: 0.6,
shadowSide: THREE.BackSide shadowSide: THREE.BackSide
}); });