UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

122 lines (100 loc) 4.26 kB
import { Frustum as ThreeFrustum } from "three"; import { read_three_planes_to_array } from "../../../../core/geom/3d/frustum/read_three_planes_to_array.js"; import { frustum_from_camera } from "./frustum_from_camera.js"; const CLIPPING_EPSILON = 0.001; const CLIPPING_NEAR_MIN = 0.5; const CLIPPING_FAR_DEFAULT = 100; const scratch_three_frustum = new ThreeFrustum(); const scratch_frustum_planes = new Float32Array(24); const scratch_range = { near: 0, far: 0 }; /** * Hysteresis prevents the clipping planes from thrashing as content moves * slightly. A shrink is only honored when it's at least this fraction of the * current near-to-far span; smaller shrinks are ignored, so an object briefly * crossing the frustum boundary doesn't trigger a re-projection. * * Loosenings (when content moves further away or closer) always apply * immediately — never delay including content. * * @type {number} */ const HYSTERESIS = 0.33; /** * Tighten the camera's near/far planes to the world-space depth range of all * BVH-backed render layers' content visible inside the camera's frustum. Falls * back to (clip_near, clip_far) when no layer contributes content. * * @param {Camera} c camera ECS component (its `object` is the THREE.Camera) * @param {RenderLayerManager} layers */ export function auto_set_camera_clipping_planes(c, layers) { const camera = c.object; if (camera === null) { return; } // Run visibility against the user-specified outer bounds, not the previous // frame's tightened range — otherwise objects could be culled before we // ever see them and the autoClip would shrink toward nothing. camera.near = c.clip_near; camera.far = c.clip_far; frustum_from_camera(camera, scratch_three_frustum, true); read_three_planes_to_array(scratch_three_frustum.planes, scratch_frustum_planes); // Depth plane: passes through camera position, normal pointing along the // view direction (-Z in camera local). A world-space point's distance to // this plane equals its signed depth in front of the camera. const m = camera.matrixWorld.elements; const nx = -m[8]; const ny = -m[9]; const nz = -m[10]; const cam_x = m[12]; const cam_y = m[13]; const cam_z = m[14]; const plane_constant = -(nx * cam_x + ny * cam_y + nz * cam_z); scratch_range.near = Number.POSITIVE_INFINITY; scratch_range.far = Number.NEGATIVE_INFINITY; layers.traverse((layer) => { if (!layer.state.visible) { return; } const compute = layer.compute_depth_range; if (compute === null) { return; } compute(scratch_range, scratch_frustum_planes, nx, ny, nz, plane_constant); }); let new_near = scratch_range.near - CLIPPING_EPSILON; let new_far = scratch_range.far + CLIPPING_EPSILON; if (!isFinite(new_near) || new_near < CLIPPING_NEAR_MIN) { new_near = CLIPPING_NEAR_MIN; } if (!isFinite(new_far) || new_far <= new_near) { new_far = Math.max(new_near + CLIPPING_EPSILON, CLIPPING_FAR_DEFAULT); } // Clamp inside the user-specified outer bounds so the tightened range // never expands past what the camera is configured to render. if (new_near < c.clip_near) { new_near = c.clip_near; } if (new_far > c.clip_far) { new_far = c.clip_far; } // Hysteresis: only shrink each plane if the shrink is meaningful relative // to the current span. Loosening always applies immediately. const old_near = camera.near; const old_far = camera.far; const old_span = old_far - old_near; const shrink_threshold = HYSTERESIS * old_span; if (new_near > old_near && new_near - old_near < shrink_threshold) { new_near = old_near; } if (new_far < old_far && old_far - new_far < shrink_threshold) { new_far = old_far; } camera.near = new_near; camera.far = new_far; // Rebuild the projection matrix from the tightened range — CameraView // reads camera.projectionMatrix directly when capturing the frame's // frustum, and the wide-range matrix from the depth-query step above is // now stale. camera.updateProjectionMatrix(); }