Back to Pursuit PIT
SOURCE / PINNED RELEASE

Made of little things.

Pursuit PIT

Release
565f4981bdc3…
Author-recorded commit
197afdbed44b…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7

Archive hash verified: 3b0485589942fd5c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/game/cull.ts
// Frustum culling for the bent scene. three's own culling is off (bend.ts): it would test
// the flat, unbent bounds, which the shader moves somewhere else. Instead each object handed
// to the Culler gets a bounding sphere in its own frame, worked out once; every frame its
// centre goes through the same road bend as the shader (RoadFrames.bend), and before each
// render pass the objects outside that camera's view are hidden, then shown again.
import * as THREE from 'three';
import { MOUNTAIN_MIN_RADIUS, type RoadFrames } from './road.js';

/** A bounding sphere round an object's meshes, in its own frame. Meshes flagged `unbounded` are left out. */
export function boundsOf(root: THREE.Object3D): THREE.Sphere {
  root.updateMatrixWorld(true);
  const inv = root.matrixWorld.clone().invert();
  const box = new THREE.Box3(), part = new THREE.Box3(), m = new THREE.Matrix4();
  root.traverse((o) => {
    const mesh = o as THREE.Mesh;
    if (!mesh.isMesh || o.userData.unbounded) return;
    let bb: THREE.Box3 | null;
    if ((mesh as THREE.InstancedMesh).isInstancedMesh) {
      const inst = mesh as THREE.InstancedMesh;
      if (!inst.count) return;
      inst.computeBoundingBox();
      bb = inst.boundingBox;
    } else {
      if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
      bb = mesh.geometry.boundingBox;
    }
    if (bb) box.union(part.copy(bb).applyMatrix4(m.multiplyMatrices(inv, o.matrixWorld)));
  });
  return box.isEmpty() ? new THREE.Sphere(new THREE.Vector3(), 0) : box.getBoundingSphere(new THREE.Sphere());
}

interface Item {
  obj: THREE.Object3D;
  centre: THREE.Vector3; // bent, in the world
  radius: number;
}

export class Culler {
  private bounds = new WeakMap<THREE.Object3D, THREE.Sphere>();
  private items: Item[] = [];
  private used = 0;
  private hidden: THREE.Object3D[] = [];
  private frames!: RoadFrames;
  private originS = 0;
  private frustum = new THREE.Frustum();
  private m = new THREE.Matrix4();
  private v = new THREE.Vector3();
  private sphere = new THREE.Sphere();
  private bent = { x: 0, y: 0, z: 0 };

  /** Start this frame's list. */
  begin(frames: RoadFrames, originS: number): void {
    this.restore();
    this.frames = frames;
    this.originS = originS;
    this.used = 0;
  }

  /**
   * Cull `obj` this frame (call after it is placed). `local` is its bounding sphere in its own
   * frame; by default one is worked out from its meshes the first time it is seen.
   */
  add(obj: THREE.Object3D, local?: THREE.Sphere): void {
    if (!obj.visible) return;
    let s = local ?? this.bounds.get(obj);
    if (!s) this.bounds.set(obj, (s = boundsOf(obj)));
    if (s.radius <= 0) return;
    // Into the flat render space, through each parent's transform (cheaper than matrixWorld,
    // which isn't up to date until the renderer runs).
    const v = this.v.copy(s.center);
    let scale = 1;
    for (let o: THREE.Object3D | null = obj; o && o.parent; o = o.parent) {
      v.multiply(o.scale).applyQuaternion(o.quaternion).add(o.position);
      scale *= Math.max(Math.abs(o.scale.x), Math.abs(o.scale.y), Math.abs(o.scale.z));
    }
    const r = s.radius * scale;
    // Flat render space has z = -(s - originS); bend it onto the road. The bend can move the
    // ends of a long object off the line of its centre: allow for the tightest curve and
    // the steepest change of grade.
    const b = this.frames.bend(v.x, v.y, this.originS - v.z, this.bent);
    let item = this.items[this.used];
    if (!item) this.items.push((item = { obj, centre: new THREE.Vector3(), radius: 0 }));
    item.obj = obj;
    item.centre.set(b.x, b.y, b.z);
    item.radius = r + (r * r) / (2 * MOUNTAIN_MIN_RADIUS) + r * 0.1 + 1;
    this.used++;
  }

  /** Hide what `camera` can't see, until the next cull or restore. */
  cull(camera: THREE.Camera): void {
    this.restore();
    camera.updateMatrixWorld();
    this.frustum.setFromProjectionMatrix(this.m.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
    for (let i = 0; i < this.used; i++) {
      const it = this.items[i];
      this.sphere.center.copy(it.centre);
      this.sphere.radius = it.radius;
      if (it.obj.visible && !this.frustum.intersectsSphere(this.sphere)) {
        it.obj.visible = false;
        this.hidden.push(it.obj);
      }
    }
  }

  /** Show everything cull() hid. */
  restore(): void {
    for (const o of this.hidden) o.visible = true;
    this.hidden.length = 0;
  }
}