@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
122 lines (100 loc) • 4.26 kB
JavaScript
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();
}