Back to Poly Dogfight
SOURCE / PINNED RELEASE

Made of little things.

Poly Dogfight

Release
b8511ee374e3…
Author-recorded commit
5579434b9d8b…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-44f63e5422b

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

src/world/chunks.ts
/**
 * Planet patches for horizon culling. The planet is small, so from flying
 * height most of it is behind the horizon: split planet-sized meshes into
 * patches and hide, per view, every patch the sphere hides.
 */
import {
  BufferAttribute,
  BufferGeometry,
  IcosahedronGeometry,
  Vector3,
  type Object3D,
} from 'three';
import { PLANET_RADIUS } from '../sim/constants.ts';

/** Patch centres: the 80 face centres of a once-subdivided icosahedron. */
export const CHUNK_DIRECTIONS: readonly Vector3[] = (() => {
  const pos = new IcosahedronGeometry(1, 1).getAttribute('position');
  const dirs: Vector3[] = [];
  for (let f = 0; f < pos.count; f += 3) {
    dirs.push(
      new Vector3(
        pos.getX(f) + pos.getX(f + 1) + pos.getX(f + 2),
        pos.getY(f) + pos.getY(f + 1) + pos.getY(f + 2),
        pos.getZ(f) + pos.getZ(f + 1) + pos.getZ(f + 2),
      ).normalize(),
    );
  }
  return dirs;
})();

const NONE = 0xffff;

/** Index of the patch whose centre is closest to the direction of (x, y, z). */
export function chunkOf(x: number, y: number, z: number): number {
  let best = 0;
  let bestDot = -Infinity;
  for (let i = 0; i < CHUNK_DIRECTIONS.length; i++) {
    const d = CHUNK_DIRECTIONS[i]!;
    const dot = d.x * x + d.y * y + d.z * z;
    if (dot > bestDot) {
      bestDot = dot;
      best = i;
    }
  }
  return best;
}

/**
 * Split a non-indexed geometry into one geometry per patch, by triangle
 * centroid, keyed by patch index. `keep` can drop triangles (first vertex index).
 */
export function splitIntoChunks(geometry: BufferGeometry, keep?: (first: number) => boolean): Map<number, BufferGeometry> {
  const pos = geometry.getAttribute('position');
  const triangles = pos.count / 3;
  const owner = new Uint16Array(triangles);
  const counts = new Uint32Array(CHUNK_DIRECTIONS.length);
  for (let t = 0; t < triangles; t++) {
    const f = t * 3;
    if (keep && !keep(f)) {
      owner[t] = NONE;
      continue;
    }
    const chunk = chunkOf(
      pos.getX(f) + pos.getX(f + 1) + pos.getX(f + 2),
      pos.getY(f) + pos.getY(f + 1) + pos.getY(f + 2),
      pos.getZ(f) + pos.getZ(f + 1) + pos.getZ(f + 2),
    );
    owner[t] = chunk;
    counts[chunk]!++;
  }
  const names = Object.keys(geometry.attributes);
  const chunks = new Map<number, BufferGeometry>();
  counts.forEach((count, chunk) => {
    if (!count) return;
    const out = new BufferGeometry();
    for (const name of names) {
      const source = geometry.getAttribute(name) as BufferAttribute;
      const size = source.itemSize;
      const array = new Float32Array(count * 3 * size);
      let w = 0;
      for (let t = 0; t < triangles; t++) {
        if (owner[t] !== chunk) continue;
        array.set((source.array as Float32Array).subarray(t * 3 * size, (t + 1) * 3 * size), w);
        w += 3 * size;
      }
      out.setAttribute(name, new BufferAttribute(array, size));
    }
    out.computeBoundingSphere();
    chunks.set(chunk, out);
  });
  return chunks;
}

/**
 * Hide objects that are entirely behind the planet. The occluder is a sphere
 * a little under sea level, so the test only ever errs toward drawing.
 */
const OCCLUDER = PLANET_RADIUS - 4;

type Entry = { object: Object3D; dir: Vector3; spread: number; top: number };

export function createHorizonCuller() {
  const entries: Entry[] = [];
  return {
    /** Track a static object whose geometry is in planet coordinates. */
    add(object: Object3D, geometry: BufferGeometry) {
      const pos = geometry.getAttribute('position');
      const dir = new Vector3();
      for (let i = 0; i < pos.count; i++) {
        dir.x += pos.getX(i);
        dir.y += pos.getY(i);
        dir.z += pos.getZ(i);
      }
      dir.normalize();
      let spread = 0;
      let top = 0;
      for (let i = 0; i < pos.count; i++) {
        const x = pos.getX(i);
        const y = pos.getY(i);
        const z = pos.getZ(i);
        const r = Math.hypot(x, y, z);
        top = Math.max(top, r);
        spread = Math.max(spread, Math.acos(Math.min(1, (x * dir.x + y * dir.y + z * dir.z) / r)));
      }
      entries.push({ object, dir, spread, top });
    },
    /** Show only what a camera at `eye` could see over the planet's curve. */
    cullFor(eye: Vector3) {
      const d = Math.max(OCCLUDER + 0.01, eye.length());
      const eyeHorizon = Math.acos(OCCLUDER / d);
      for (const e of entries) {
        const angle = Math.acos(Math.max(-1, Math.min(1, e.dir.dot(eye) / d)));
        const reach = eyeHorizon + (e.top > OCCLUDER ? Math.acos(OCCLUDER / e.top) : 0);
        e.object.visible = angle - e.spread < reach;
      }
    },
    get size() {
      return entries.length;
    },
  };
}

export type HorizonCuller = ReturnType<typeof createHorizonCuller>;