Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
1ba42f1ca1d6…
Author-recorded commit
baecad10b1cd…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f

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

tools/assets/mesh.ts
// Geometry builders for the custom asset bundle. Everything is authored in
// the game's own space: Director-style Z-up, inches (Havok scale 0.0254).
//
// A Piece is one level model: a visual mesh (one primitive per material), the
// copy the wheels and camera raycast against, and an optional Havok body.
// Visual faces are subdivided so the baked vertex lighting has detail; the ray
// and collision copies keep the coarse faces.

export type Vec3 = [number, number, number];

export interface Primitive {
  material: string;
  positions: number[];
  normals: number[];
  uvs: number[];
  indices: number[];
}

export interface Piece {
  name: string;
  /** convex: one hull of `coarse`; mesh: a static trimesh; none: visual only. */
  collide: 'convex' | 'mesh' | 'none';
  /** Whether the wheels and camera see it (rugs: yes; overhead decor: no). */
  ray: boolean;
  /** Whether it blocks the baked sunlight (the ceiling does not: it stands in for open sky). */
  castsShadow: boolean;
  prims: Map<string, Primitive>;
  /** Coarse world-space triangles (verts flat, faces flat). */
  coarse: { verts: number[]; faces: number[] };
}

export const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
export const add = (a: Vec3, b: Vec3): Vec3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
export const scale = (a: Vec3, s: number): Vec3 => [a[0] * s, a[1] * s, a[2] * s];
export const cross = (a: Vec3, b: Vec3): Vec3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
export const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
export const len = (a: Vec3) => Math.hypot(a[0], a[1], a[2]);
export const norm = (a: Vec3): Vec3 => scale(a, 1 / (len(a) || 1));
export const lerp = (a: Vec3, b: Vec3, t: number): Vec3 => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];

export function piece(name: string, collide: Piece['collide'] = 'convex', ray = true): Piece {
  return { name, collide, ray, castsShadow: true, prims: new Map(), coarse: { verts: [], faces: [] } };
}

function prim(p: Piece, material: string): Primitive {
  let out = p.prims.get(material);
  if (!out) p.prims.set(material, (out = { material, positions: [], normals: [], uvs: [], indices: [] }));
  return out;
}

function coarseIndex(p: Piece, v: Vec3): number {
  const c = p.coarse.verts;
  for (let i = 0; i < c.length; i += 3) if (c[i] === v[0] && c[i + 1] === v[1] && c[i + 2] === v[2]) return i / 3;
  c.push(v[0], v[1], v[2]);
  return c.length / 3 - 1;
}

export interface QuadOptions {
  /** Target edge length of the visual subdivision. */
  cell?: number;
  /** World units per texture repeat; UVs come from the quad's own axes. */
  uvScale?: number;
  /** Skip the ray/collision copy (purely decorative faces). */
  visualOnly?: boolean;
}

/**
 * A planar quad a→b→c→d, counter-clockwise seen from its front (outside).
 * Degenerate quads (c == d) make triangles.
 */
export function quad(p: Piece, material: string, a: Vec3, b: Vec3, c: Vec3, d: Vec3, o: QuadOptions = {}): void {
  const n = norm(cross(sub(b, a), sub(d, a)));
  const nn = len(n) > 0 ? n : norm(cross(sub(c, b), sub(a, b)));
  const cell = o.cell ?? 150;
  const nu = Math.max(1, Math.ceil(Math.max(len(sub(b, a)), len(sub(c, d))) / cell));
  const nv = Math.max(1, Math.ceil(Math.max(len(sub(d, a)), len(sub(c, b))) / cell));
  const pr = prim(p, material);
  const base = pr.positions.length / 3;
  const uAxis = norm(sub(b, a));
  const vAxis = norm(cross(nn, uAxis));
  const uvs = o.uvScale ?? 0;
  for (let j = 0; j <= nv; j++) {
    for (let i = 0; i <= nu; i++) {
      const top = lerp(d, c, i / nu);
      const bottom = lerp(a, b, i / nu);
      const v = lerp(bottom, top, j / nv);
      pr.positions.push(...v);
      pr.normals.push(...nn);
      if (uvs) {
        const rel = sub(v, a);
        pr.uvs.push(dot(rel, uAxis) / uvs, dot(rel, vAxis) / uvs);
      } else pr.uvs.push(i / nu, j / nv);
    }
  }
  for (let j = 0; j < nv; j++) {
    for (let i = 0; i < nu; i++) {
      const k = base + j * (nu + 1) + i;
      pr.indices.push(k, k + 1, k + nu + 2, k, k + nu + 2, k + nu + 1);
    }
  }
  if (o.visualOnly) return;
  const ia = coarseIndex(p, a);
  const ib = coarseIndex(p, b);
  const ic = coarseIndex(p, c);
  const id = coarseIndex(p, d);
  p.coarse.faces.push(ia, ib, ic);
  if (id !== ic) p.coarse.faces.push(ia, ic, id);
}

export function tri(p: Piece, material: string, a: Vec3, b: Vec3, c: Vec3, o: QuadOptions = {}): void {
  quad(p, material, a, b, c, c, o);
}

export interface BoxMaterials {
  top?: string;
  bottom?: string;
  sides?: string;
}

/** Axis-aligned box from min to max corner. `skipBottom` for boxes resting on the floor. */
export function box(p: Piece, min: Vec3, max: Vec3, mat: string | BoxMaterials, o: QuadOptions & { skipBottom?: boolean } = {}): void {
  const m = typeof mat === 'string' ? { top: mat, bottom: mat, sides: mat } : mat;
  const [x0, y0, z0] = min;
  const [x1, y1, z1] = max;
  const side = m.sides ?? m.top ?? 'default';
  quad(p, m.top ?? side, [x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1], o);
  if (!o.skipBottom) quad(p, m.bottom ?? side, [x0, y1, z0], [x1, y1, z0], [x1, y0, z0], [x0, y0, z0], o);
  quad(p, side, [x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1], o); // -Y
  quad(p, side, [x1, y1, z0], [x0, y1, z0], [x0, y1, z1], [x1, y1, z1], o); // +Y
  quad(p, side, [x0, y1, z0], [x0, y0, z0], [x0, y0, z1], [x0, y1, z1], o); // -X
  quad(p, side, [x1, y0, z0], [x1, y1, z0], [x1, y1, z1], [x1, y0, z1], o); // +X
}

/**
 * A wedge ramp: footprint x0..x1 × y0..y1 rising along `dir` from height z0 to z1
 * ('+y' rises towards y1, '-y' towards y0, '+x'/'-x' likewise). Base at `floor`.
 */
export function ramp(
  p: Piece,
  x0: number, x1: number, y0: number, y1: number,
  dir: '+x' | '-x' | '+y' | '-y', z0: number, z1: number,
  mat: BoxMaterials, floor = 0, o: QuadOptions = {},
): void {
  // Corners counter-clockwise from (x0,y0); heights per corner.
  const xy: Array<[number, number]> = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
  const h = xy.map(([x, y]) => {
    const t = dir === '+y' ? (y - y0) / (y1 - y0) : dir === '-y' ? (y1 - y) / (y1 - y0) : dir === '+x' ? (x - x0) / (x1 - x0) : (x1 - x) / (x1 - x0);
    return z0 + (z1 - z0) * t;
  });
  const top = xy.map(([x, y], i): Vec3 => [x, y, h[i]]);
  const bot = xy.map(([x, y]): Vec3 => [x, y, floor]);
  quad(p, mat.top ?? 'default', top[0], top[1], top[2], top[3], o);
  const side = mat.sides ?? mat.top ?? 'default';
  for (let i = 0; i < 4; i++) {
    const j = (i + 1) % 4;
    if (h[i] - floor < 0.01 && h[j] - floor < 0.01) continue;
    if (h[i] - floor < 0.01) tri(p, side, bot[i], bot[j], top[j], o);
    else if (h[j] - floor < 0.01) tri(p, side, bot[i], bot[j], top[i], o);
    else quad(p, side, bot[i], bot[j], top[j], top[i], o);
  }
}

/** Vertical cylinder (smooth sides), centre (cx, cy), from z0 to z1. */
export function cylinder(p: Piece, cx: number, cy: number, r: number, z0: number, z1: number, mat: BoxMaterials, segments = 16, o: QuadOptions = {}): void {
  const ring = (z: number) => Array.from({ length: segments }, (_, i): Vec3 => {
    const a = (i / segments) * Math.PI * 2;
    return [cx + Math.cos(a) * r, cy + Math.sin(a) * r, z];
  });
  const lo = ring(z0);
  const hi = ring(z1);
  const side = mat.sides ?? 'default';
  for (let i = 0; i < segments; i++) {
    const j = (i + 1) % segments;
    quad(p, side, lo[i], lo[j], hi[j], hi[i], o);
  }
  const c: Vec3 = [cx, cy, z1];
  for (let i = 0; i < segments; i++) tri(p, mat.top ?? side, c, hi[i], hi[(i + 1) % segments], o);
  smoothNormals(p, side, [cx, cy]);
}

/** Replace the side primitive's normals with radial ones (round look, same faces). */
function smoothNormals(p: Piece, material: string, axis: [number, number]): void {
  const pr = p.prims.get(material);
  if (!pr) return;
  for (let i = 0; i < pr.positions.length; i += 3) {
    if (Math.abs(pr.normals[i + 2]) > 0.5) continue;
    const n = norm([pr.positions[i] - axis[0], pr.positions[i + 1] - axis[1], 0]);
    pr.normals[i] = n[0];
    pr.normals[i + 1] = n[1];
    pr.normals[i + 2] = n[2];
  }
}

/**
 * A curved ramp (quarter pipe) running along x0..x1: flat at y=y0, curving up
 * to vertical at y=y0+radius (dir '+y'), or mirrored ('-y'). Trimesh only.
 */
export function quarterPipe(p: Piece, x0: number, x1: number, y0: number, radius: number, dir: 1 | -1, mat: string, steps = 10, o: QuadOptions = {}): void {
  const prof: Array<[number, number]> = [];
  for (let i = 0; i <= steps; i++) {
    const a = (i / steps) * (Math.PI / 2) * 0.92;
    prof.push([y0 + dir * Math.sin(a) * radius, radius - Math.cos(a) * radius]);
  }
  for (let i = 0; i < steps; i++) {
    const [ya, za] = prof[i];
    const [yb, zb] = prof[i + 1];
    if (dir === 1) quad(p, mat, [x0, ya, za], [x1, ya, za], [x1, yb, zb], [x0, yb, zb], o);
    else quad(p, mat, [x1, ya, za], [x0, ya, za], [x0, yb, zb], [x1, yb, zb], o);
  }
  const [yEnd, zEnd] = prof[steps];
  // Back wall down to the floor and a lip on top.
  const back = yEnd + dir * 30;
  if (dir === 1) {
    quad(p, mat, [x0, yEnd, zEnd], [x1, yEnd, zEnd], [x1, back, zEnd], [x0, back, zEnd], o);
  } else {
    quad(p, mat, [x1, yEnd, zEnd], [x0, yEnd, zEnd], [x0, back, zEnd], [x1, back, zEnd], o);
  }
}

/** Every vertex of every primitive, transformed — for props authored at the origin. */
export function transformPiece(p: Piece, f: (v: Vec3) => Vec3, fn: (n: Vec3) => Vec3 = (n) => n): void {
  for (const pr of p.prims.values()) {
    for (let i = 0; i < pr.positions.length; i += 3) {
      const v = f([pr.positions[i], pr.positions[i + 1], pr.positions[i + 2]]);
      const n = norm(fn([pr.normals[i], pr.normals[i + 1], pr.normals[i + 2]]));
      pr.positions.splice(i, 3, ...v);
      pr.normals.splice(i, 3, ...n);
    }
  }
  const c = p.coarse.verts;
  for (let i = 0; i < c.length; i += 3) c.splice(i, 3, ...f([c[i], c[i + 1], c[i + 2]]));
}