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.
import {
BufferAttribute,
Color,
Group,
IcosahedronGeometry,
Mesh,
MeshLambertMaterial,
type BufferGeometry,
} from 'three';
import { PLANET_RADIUS } from '../sim/constants.ts';
import { mulberry32 } from '../sim/noise.ts';
import { RIFT, riftCoordinates, terrainHeight } from '../sim/terrain.ts';
import { splitIntoChunks, type HorizonCuller } from './chunks.ts';
import { merge } from './geometry.ts';
import { PALETTE } from './palette.ts';
/** Icosphere detail 63 → 64² triangles per face, ~82k flat triangles total. */
const DETAIL = 63;
/** Land this far above sea level hides the sea under it completely. */
const DRY = 4;
function displaced(detail: number): { geometry: BufferGeometry; heights: Float32Array } {
const geometry = new IcosahedronGeometry(1, detail);
const pos = geometry.getAttribute('position') as BufferAttribute;
const heights = new Float32Array(pos.count);
const cache = new Map<string, number>();
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const y = pos.getY(i);
const z = pos.getZ(i);
const key = `${x.toFixed(5)},${y.toFixed(5)},${z.toFixed(5)}`;
let h = cache.get(key);
if (h === undefined) {
h = terrainHeight(x, y, z);
cache.set(key, h);
}
heights[i] = h;
const r = PLANET_RADIUS + h;
pos.setXYZ(i, x * r, y * r, z * r);
}
return { geometry, heights };
}
function terrainColor(h: number, slope: number, x: number, y: number, z: number, jitter: number, out: Color): Color {
const { across, along } = riftCoordinates(x, y, z);
const inRift = Math.abs(across) < 0.17 && along > RIFT.arcStart - 0.2 && along < RIFT.arcEnd + 0.2;
if (h < -6) out.setHex(PALETTE.seaFloor);
else if (h < 3) out.setHex(PALETTE.sand);
else if (inRift && h > 8) out.setHex(Math.floor(h / 7) % 2 ? PALETTE.riftA : PALETTE.riftB);
else if (inRift) out.setHex(PALETTE.riftFloor);
else if (h > 64) out.setHex(slope > 0.9 ? PALETTE.rock : PALETTE.snow);
else if (slope > 0.75) out.setHex(jitter > 0.5 ? PALETTE.rock : PALETTE.rockDark);
else if (h > 30) out.setHex(slope > 0.45 ? PALETTE.rockDark : PALETTE.forest);
else out.setHex(jitter > 0.55 ? PALETTE.grass : PALETTE.grassDark);
return out.offsetHSL(0, 0, (jitter - 0.5) * 0.05);
}
/**
* Land and sea as horizon-culled patches. `extras` (static props, same
* material) are baked into the land patch they stand on, so a patch is one
* draw. `maxHeight` is the highest land vertex.
*/
export function createPlanet(
culler: HorizonCuller,
extras: ReadonlyMap<number, BufferGeometry>,
): { land: Group; sea: Group; maxHeight: number } {
const { geometry, heights } = displaced(DETAIL);
const pos = geometry.getAttribute('position') as BufferAttribute;
const colors = new Float32Array(pos.count * 3);
const random = mulberry32(99);
const color = new Color();
for (let f = 0; f < pos.count; f += 3) {
const h = (heights[f]! + heights[f + 1]! + heights[f + 2]!) / 3;
const hMin = Math.min(heights[f]!, heights[f + 1]!, heights[f + 2]!);
const hMax = Math.max(heights[f]!, heights[f + 1]!, heights[f + 2]!);
const cx = pos.getX(f) + pos.getX(f + 1) + pos.getX(f + 2);
const cy = pos.getY(f) + pos.getY(f + 1) + pos.getY(f + 2);
const cz = pos.getZ(f) + pos.getZ(f + 1) + pos.getZ(f + 2);
const len = Math.hypot(cx, cy, cz);
// Edge length ≈ 9 m at this detail; slope as rise over run.
const slope = (hMax - hMin) / 9;
terrainColor(h, slope, cx / len, cy / len, cz / len, random(), color);
for (let k = 0; k < 3; k++) {
colors[(f + k) * 3] = color.r;
colors[(f + k) * 3 + 1] = color.g;
colors[(f + k) * 3 + 2] = color.b;
}
}
geometry.setAttribute('color', new BufferAttribute(colors, 3));
geometry.deleteAttribute('normal');
geometry.deleteAttribute('uv');
const landMaterial = new MeshLambertMaterial({ vertexColors: true, flatShading: true });
const land = new Group();
const landChunks = splitIntoChunks(geometry);
for (const index of new Set([...landChunks.keys(), ...extras.keys()])) {
const parts = [landChunks.get(index), extras.get(index)].filter((g): g is BufferGeometry => !!g);
const chunk = parts.length > 1 ? merge(parts) : parts[0]!;
const mesh = new Mesh(chunk, landMaterial);
land.add(mesh);
culler.add(mesh, chunk);
}
geometry.dispose();
const seaGeometry = new IcosahedronGeometry(PLANET_RADIUS, 40);
seaGeometry.deleteAttribute('uv');
seaGeometry.deleteAttribute('normal');
const seaMaterial = new MeshLambertMaterial({ color: PALETTE.sea, flatShading: true, transparent: true, opacity: 0.88 });
const sea = new Group();
// Sea well under dry land is never seen: leave it out.
const seaPos = seaGeometry.getAttribute('position');
const landAt = new Map<string, number>();
const heightAt = (x: number, y: number, z: number) => {
const r = Math.hypot(x, y, z);
const key = `${(x / r).toFixed(5)},${(y / r).toFixed(5)},${(z / r).toFixed(5)}`;
let h = landAt.get(key);
if (h === undefined) {
h = terrainHeight(x / r, y / r, z / r);
landAt.set(key, h);
}
return h;
};
const wet = (f: number) => {
let cx = 0;
let cy = 0;
let cz = 0;
for (let k = 0; k < 3; k++) {
const x = seaPos.getX(f + k);
const y = seaPos.getY(f + k);
const z = seaPos.getZ(f + k);
if (heightAt(x, y, z) < DRY) return true;
cx += x;
cy += y;
cz += z;
}
return heightAt(cx, cy, cz) < DRY;
};
for (const chunk of splitIntoChunks(seaGeometry, wet).values()) {
const mesh = new Mesh(chunk, seaMaterial);
mesh.renderOrder = 1;
sea.add(mesh);
culler.add(mesh, chunk);
}
seaGeometry.dispose();
let maxHeight = 0;
for (const h of heights) maxHeight = Math.max(maxHeight, h);
return { land, sea, maxHeight };
}
