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.
/**
* 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>;
