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/planet.ts
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 };
}