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.

src/game/render/world.ts
// three.js view of the driving scene. Game state lives in the Lingo ports;
// this module only mirrors it (the original's W3D models and shaders).
import {
  Color,
  CanvasTexture,
  type Material,
  Matrix4,
  Mesh,
  Object3D,
  PlaneGeometry,
  Quaternion,
  RepeatWrapping,
  Scene,
  ShaderMaterial,
  SRGBColorSpace,
  Sprite,
  SpriteMaterial,
  Texture,
  Vector3,
} from 'three';
import { assetBytes, assetJson, assetUrl, levelIsEditor } from '../assets.js';
import { levelSettings, levelVisuals } from '../level/editorLevel.js';
import { Car } from '../lingo/car.js';
import type { RayWorld } from '../physics/rayWorld.js';
import type { DrivingManager } from '../lingo/drivingManager.js';
import type { GameType } from '../lingo/gameplay.js';
import { decodeImage, type GlbNode, parseGlb, readGlb } from './glb.js';
import { type LightRig, standardShader, type StandardShaderOptions, UNLIT } from './materials.js';

interface W3DMaterial {
  ambient: number[];
  diffuse: number[];
  emissive: number[];
  opacity: number;
}
type MaterialTable = Record<string, Record<string, W3DMaterial>>;
const rgb = (c: number[] | undefined, fallback: Color) => (c ? new Color(c[0], c[1], c[2]) : fallback);

const DEG = Math.PI / 180;
/** car2 children in clone order; wheel i (1-based) takes the i-th. */
const WHEEL_MESH_ORDER = ['Wheel2', 'Wheel1', 'Wheel3', 'Wheel4'];

/** Camera-facing player badge, drawn over geometry so walls cannot hide it. */
function playerMarker(label: string, color: string): Sprite {
  const canvas = document.createElement('canvas');
  canvas.width = 128;
  canvas.height = 160;
  const ctx = canvas.getContext('2d')!;
  ctx.fillStyle = '#10151f';
  ctx.beginPath();
  ctx.roundRect(8, 8, 112, 112, 24);
  ctx.fill();
  ctx.strokeStyle = color;
  ctx.lineWidth = 6;
  ctx.stroke();
  ctx.fillStyle = color;
  ctx.font = 'bold 54px Verdana, sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(label, 64, 64);
  ctx.beginPath();
  ctx.moveTo(44, 130);
  ctx.lineTo(84, 130);
  ctx.lineTo(64, 154);
  ctx.fill();
  const map = new CanvasTexture(canvas);
  map.colorSpace = SRGBColorSpace;
  const marker = new Sprite(new SpriteMaterial({
    map, transparent: true, opacity: 0.65, depthTest: false, depthWrite: false,
    sizeAttenuation: false, toneMapped: false,
  }));
  marker.name = `player-marker-${label}`;
  marker.scale.set(0.085, 0.106, 1);
  marker.renderOrder = 1000;
  return marker;
}

async function textureFromDataUrl(url: string): Promise<Texture> {
  const blob = new Blob([dataUrlBytes(url)], { type: url.slice(5, url.indexOf(';')) });
  const texture = new Texture(await decodeImage(blob));
  texture.colorSpace = SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}

function dataUrlBytes(url: string): Uint8Array<ArrayBuffer> {
  const bin = atob(url.slice(url.indexOf(',') + 1));
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

function prepareMap(t: Texture | null): Texture | null {
  if (!t) return null;
  t.wrapS = t.wrapT = RepeatWrapping;
  // Floors seen at grazing angles; three clamps this to what the GPU supports.
  t.anisotropy = 8;
  t.colorSpace = SRGBColorSpace;
  t.needsUpdate = true;
  return t;
}

/** A GLB colour texture as editor levels use it: wrap and tiling as exported. */
function prepareColorTexture(t: Texture | null): Texture | null {
  if (!t) return null;
  t.anisotropy = 8;
  t.colorSpace = SRGBColorSpace;
  t.needsUpdate = true;
  return t;
}

/** An RGB custom property (ssrc_diffuse etc.), else null. */
function colorProp(v: unknown): Color | null {
  return Array.isArray(v) && v.length >= 3 && v.slice(0, 3).every((c) => typeof c === 'number') ? new Color(v[0], v[1], v[2]) : null;
}

/** Shader colours from the W3D material table, falling back to the GLB's diffuse factor. */
function materialColors(table: MaterialTable | undefined, file: string, name: string, glbColor: number[]): Pick<StandardShaderOptions, 'diffuse' | 'ambient' | 'emissive'> {
  const m = table?.[file]?.[name];
  return {
    diffuse: rgb(m?.diffuse, new Color(glbColor[0], glbColor[1], glbColor[2])),
    ambient: rgb(m?.ambient, new Color(1, 1, 1)),
    emissive: rgb(m?.emissive, new Color(0, 0, 0)),
  };
}

export class WorldView {
  private materials: MaterialTable | undefined;
  readonly scene = new Scene();
  /** The arrow has its own camera pass ("arrowcam"), drawn over the world. */
  readonly arrowScene = new Scene();
  private carBody = new Object3D();
  private wheels: Object3D[] = [];
  private shadow!: Mesh;
  private starTemplate: GlbNode | null = null;
  private prizeTemplate: GlbNode | null = null;
  private stars: Object3D[] = [];
  private prizes = new Map<string, Object3D>();
  private arrow = new Object3D();
  private localCars = new Map<Car, { body: Object3D; wheels: Object3D[]; shadow: Mesh; marker: Sprite }>();

  syncPlayers(players: Array<{ car: Car; color?: string; label: string }>, rays: RayWorld): void {
    this.carBody.visible = false;
    this.wheels.forEach((w) => { w.visible = false; });
    this.shadow.visible = false;
    const live = new Set(players.map((p) => p.car));
    for (const [car, visual] of this.localCars) {
      if (live.has(car)) continue;
      this.releaseLocalCar(visual);
      this.localCars.delete(car);
    }
    for (const { car, color, label } of players) {
      let visual = this.localCars.get(car);
      if (!visual) {
        visual = { body: this.carBody.clone(true), wheels: this.wheels.map((w) => w.clone(true)), shadow: this.shadow.clone(), marker: playerMarker(label, color ?? '#ffcf33') };
        visual.body.traverse((object) => {
          if (!(object instanceof Mesh)) return;
          const tint = (material: Material) => {
            const copy = material.clone();
            if (color && copy instanceof ShaderMaterial && copy.uniforms.diffuse) copy.uniforms.diffuse.value = new Color(color);
            return copy;
          };
          object.material = Array.isArray(object.material) ? object.material.map(tint) : tint(object.material);
        });
        this.localCars.set(car, visual);
        this.scene.add(visual.body, ...visual.wheels, visual.shadow, visual.marker);
      }
      visual.body.visible = true;
      visual.wheels.forEach((w) => { w.visible = true; });
      this.syncCar(car, rays, visual.body, visual.wheels, visual.shadow);
      visual.marker.position.copy(car.rb.position).add(new Vector3(0, 0, 90));
    }
  }

  private releaseLocalCar(visual: { body: Object3D; wheels: Object3D[]; shadow: Mesh; marker: Sprite }): void {
    this.scene.remove(visual.body, ...visual.wheels, visual.shadow, visual.marker);
    visual.marker.material.map?.dispose();
    visual.marker.material.dispose();
    visual.body.traverse((object) => {
      if (object instanceof Mesh) for (const material of Array.isArray(object.material) ? object.material : [object.material]) material.dispose();
    });
  }

  clearPlayers(): void {
    for (const visual of this.localCars.values()) this.releaseLocalCar(visual);
    this.localCars.clear();
    this.carBody.visible = true;
    this.wheels.forEach((w) => { w.visible = true; });
  }

  static async create(): Promise<WorldView> {
    const view = new WorldView();
    await view.build();
    return view;
  }

  private async build(): Promise<void> {
    this.scene.background = new Color(0, 0, 0);
    const [levelBytes, carBytes, materials] = await Promise.all([
      assetBytes('level.glb'),
      assetBytes('car.glb'),
      assetJson<MaterialTable>('materials.json'),
    ]);
    if (!levelBytes || !carBytes) throw new Error('level assets missing');
    this.materials = materials;

    const lightmaps = new Map<string, Texture>();
    const lightmap = async (name: string) => {
      if (!lightmaps.has(name)) {
        const url = assetUrl(`lightmaps/${name}.jpg`);
        if (!url) return null;
        lightmaps.set(name, await textureFromDataUrl(url));
      }
      return lightmaps.get(name)!;
    };

    if (levelIsEditor(levelBytes)) await this.buildEditorLevel(levelBytes);
    else for (const node of await readGlb(levelBytes)) {
      if (!node.mesh) continue;
      // Hidden in the game: the car proxy box (made the rigid body, visibility #none).
      if (node.name === 'car') continue;
      const lmName = node.mesh.extras.lightmap as string | undefined;
      const group = new Object3D();
      group.name = node.name;
      group.matrixAutoUpdate = false;
      group.matrix.copy(node.matrix);
      for (const prim of node.mesh.primitives) {
        const m = prim.material;
        // lightMapManager tweaks: D_Clone (Dbody) loses its lightmap, diffuse #441111;
        // the original (un-cloned) "column" and "Ex_Main_02" shaders get diffuse #111133.
        const isD = m.name === 'D';
        const lm = lmName && !isD ? await lightmap(lmName) : null;
        const darkBlue = !lm && (m.name === 'column' || m.name === 'Ex_Main_02');
        const colors = materialColors(this.materials, '3d', m.name, m.color);
        const material = standardShader({
          ...colors,
          diffuse: isD ? new Color('#441111') : darkBlue ? new Color('#111133') : colors.diffuse,
          ambient: isD || darkBlue ? new Color(0, 0, 0) : colors.ambient,
          map: prepareMap(m.texture),
          lightMap: lm,
          alphaTest: 0.5,
          baked: !!prim.geometry.getAttribute('bake'),
        });
        group.add(new Mesh(prim.geometry, material));
      }
      this.scene.add(group);
    }

    // Car: holypolycrap's "Car" body via car2, wheels on their own groups.
    const carNodes = await readGlb(carBytes);
    const carRoot = carNodes.find((n) => n.name === 'Car');
    if (!carRoot) throw new Error('car model missing');
    this.carBody = this.meshObject(carRoot, false, 'holypolycrap');
    this.carBody.matrixAutoUpdate = false;
    this.scene.add(this.carBody);
    for (const name of WHEEL_MESH_ORDER) {
      const wheel = carRoot.children.find((c) => c.name === name);
      const obj = wheel ? this.meshObject(wheel, false, 'holypolycrap') : new Object3D();
      obj.matrixAutoUpdate = false;
      this.wheels.push(obj);
      this.scene.add(obj);
    }

    // Props cloned from their own cast members.
    const [star, prize, arrow] = await Promise.all([assetBytes('star.glb'), assetBytes('prize.glb'), assetBytes('arrow2.glb')]);
    this.starTemplate = star ? (await readGlb(star)).find((n) => n.mesh) ?? null : null;
    this.prizeTemplate = prize ? (await readGlb(prize)).find((n) => n.mesh) ?? null : null;
    const arrowNode = arrow ? (await readGlb(arrow)).find((n) => n.mesh) ?? null : null;
    if (arrowNode) {
      this.arrow = this.meshObject(arrowNode, false, 'arrow2', UNLIT);
      this.arrow.matrixAutoUpdate = false;
      this.arrow.visible = false;
      this.arrowScene.add(this.arrow);
    }

    // carshadow: a plane, texture carshadow (rgba), blend 50, emissive white.
    const shadowUrl = assetUrl('ui/carshadow.png');
    const shadowTex = shadowUrl ? await textureFromDataUrl(shadowUrl) : null;
    this.shadow = new Mesh(
      new PlaneGeometry(1, 1),
      standardShader({ diffuse: new Color(0, 0, 0), map: shadowTex, lightMap: null, opacity: 0.5, emissive: new Color(0, 0, 0) }),
    );
    (this.shadow.material as Material).depthWrite = false;
    this.shadow.matrixAutoUpdate = false;
    this.scene.add(this.shadow);
  }

  /**
   * A Blender-authored level (src/game/level/editorLevel.ts): the objects it
   * marks as rendered, coloured from the GLB's materials and their ssrc_*
   * properties. An emissive texture on the second UV set is the lightmap.
   */
  private async buildEditorLevel(levelBytes: ArrayBuffer): Promise<void> {
    const settings = levelSettings(parseGlb(levelBytes).doc);
    for (const { node, matrix, flags } of levelVisuals(await readGlb(levelBytes), settings)) {
      const group = new Object3D();
      group.name = flags.name;
      group.matrixAutoUpdate = false;
      group.matrix.copy(matrix);
      for (const prim of node.mesh!.primitives) {
        const m = prim.material;
        const x = m.extras;
        const lightMap = m.emissiveTexture && m.emissiveTexCoord === 1 && prim.geometry.getAttribute('uv1') ? prepareColorTexture(m.emissiveTexture) : null;
        const opacity = typeof x.ssrc_opacity === 'number' ? x.ssrc_opacity : m.blend ? m.color[3] : 1;
        const material = standardShader({
          diffuse: colorProp(x.ssrc_diffuse) ?? new Color(m.color[0], m.color[1], m.color[2]),
          ambient: colorProp(x.ssrc_ambient) ?? new Color(1, 1, 1),
          // A glow texture is not supported; only its colour applies when there is none.
          emissive: colorProp(x.ssrc_emissive) ?? (m.emissiveTexture ? new Color(0, 0, 0) : new Color(...m.emissive)),
          map: prepareColorTexture(m.texture),
          lightMap,
          opacity,
          alphaTest: m.alphaCutoff ?? (opacity < 1 ? 0.01 : 0.5),
          baked: flags.baked && !!prim.geometry.getAttribute('bake'),
        });
        group.add(new Mesh(prim.geometry, material));
      }
      this.scene.add(group);
    }
  }

  /** A node's own mesh only (children are handled separately). */
  private meshObject(node: GlbNode, withChildren: boolean, file: string, lights?: LightRig): Object3D {
    const obj = new Object3D();
    obj.name = node.name;
    for (const prim of node.mesh?.primitives ?? []) {
      const m = prim.material;
      obj.add(
        new Mesh(
          prim.geometry,
          standardShader({ ...materialColors(this.materials, file, m.name, m.color), map: prepareMap(m.texture), lightMap: null, alphaTest: 0.5, lights }),
        ),
      );
    }
    if (withChildren) for (const c of node.children) obj.add(this.meshObject(c, true, file, lights));
    return obj;
  }

  /** Free the GPU copies of every geometry, material and texture (before switching asset packs). */
  dispose(): void {
    this.clearPlayers();
    const geometries = new Set<{ dispose(): void }>();
    const materials = new Set<Material>();
    const textures = new Set<Texture>();
    const collect = (root: Object3D) =>
      root.traverse((o) => {
        if (!(o instanceof Mesh)) return;
        geometries.add(o.geometry);
        for (const m of Array.isArray(o.material) ? o.material : [o.material]) materials.add(m);
      });
    collect(this.scene);
    collect(this.arrowScene);
    for (const template of [this.starTemplate, this.prizeTemplate]) {
      for (const prim of template?.mesh?.primitives ?? []) {
        geometries.add(prim.geometry);
        if (prim.material.texture) textures.add(prim.material.texture);
      }
    }
    for (const m of materials) {
      const uniforms = (m as Material & { uniforms?: Record<string, { value: unknown }> }).uniforms ?? {};
      for (const u of Object.values(uniforms)) if (u.value instanceof Texture) textures.add(u.value);
    }
    for (const g of geometries) g.dispose();
    for (const m of materials) m.dispose();
    for (const t of textures) t.dispose();
    this.scene.clear();
    this.arrowScene.clear();
  }

  /** Rebuild per-race props (checkpoint stars, prizes) for a new game mode. */
  setMode(mode: GameType | null): void {
    for (const s of this.stars) this.scene.remove(s);
    for (const p of this.prizes.values()) this.scene.remove(p);
    this.stars = [];
    this.prizes.clear();
    this.arrow.visible = false;
    if (!mode) return;
    if (this.starTemplate) {
      for (const cp of mode.checkpoints) {
        const obj = this.meshObject(this.starTemplate, false, 'star');
        obj.matrixAutoUpdate = false;
        obj.userData.position = cp.position;
        this.stars.push(obj);
        this.scene.add(obj);
      }
    }
    if (this.prizeTemplate) {
      const base = this.prizeTemplate.matrix;
      for (const p of mode.prizes) {
        const obj = this.meshObject(this.prizeTemplate, false, 'prize');
        obj.matrixAutoUpdate = false;
        obj.matrix.copy(base).setPosition(p.position);
        this.prizes.set(p.name, obj);
        this.scene.add(obj);
      }
    }
  }

  /** Stars spin 3 degrees per chunk; prizes vanish when collected; the arrow points at the next star. */
  syncMode(mode: GameType, camera: { position: Vector3; quaternion: Quaternion; up: Vector3 }): void {
    mode.checkpoints.forEach((cp, i) => {
      const obj = this.stars[i];
      if (!obj) return;
      obj.visible = cp.inWorld;
      // tStar.scale(3, 3, 3) on the template, then rotate(0, 0, 3) per chunk about world Z.
      obj.matrix
        .makeRotationZ(cp.spin * DEG)
        .multiply(new Matrix4().makeScale(3, 3, 3))
        .multiply(this.starTemplate!.matrix.clone().setPosition(0, 0, 0))
        .setPosition(cp.position);
      obj.matrixWorldNeedsUpdate = true;
    });
    const remaining = new Set(mode.prizes.map((p) => p.name));
    for (const [name, obj] of this.prizes) obj.visible = remaining.has(name);

    this.arrow.visible = mode.arrow.visible;
    if (mode.arrow.visible) {
      // arrowcam follows the chase camera; the arrow sits at (0, -12, -50) in camera
      // space, scale 0.05, its +Y aimed horizontally at the target, +Z towards camera up.
      const camMatrix = new Matrix4().compose(camera.position, camera.quaternion, new Vector3(1, 1, 1));
      const at = new Vector3(0, -12, -50).applyMatrix4(camMatrix);
      const target = mode.arrow.target.clone();
      target.z = at.z;
      const front = target.sub(camera.position);
      front.z = at.z - camera.position.z;
      front.normalize();
      const upHint = new Vector3(0, 1, 0).applyQuaternion(camera.quaternion);
      const side = front.clone().cross(upHint).normalize();
      const up = side.clone().cross(front).normalize();
      this.arrow.matrix.makeBasis(side, front, up).setPosition(at).multiply(new Matrix4().makeScale(0.05, 0.05, 0.05));
      this.arrow.matrixWorldNeedsUpdate = true;
    }
  }

  /** Mirror the simulation into the scene graph. */
  sync(dm: DrivingManager): void {
    this.syncCar(dm.car, dm.rays, this.carBody, this.wheels, this.shadow);
  }

  private syncCar(car: Car, rays: RayWorld, body: Object3D, wheels: Object3D[], shadow: Mesh): void {
    const carT = car.modelTransform();
    body.matrix.copy(carT).multiply(Car.visualOffset());
    body.matrixWorldNeedsUpdate = true;

    const carRot = new Quaternion();
    carT.decompose(new Vector3(), carRot, new Vector3());
    car.wheels.forEach((w, i) => {
      const m = new Matrix4()
        .compose(w.visualGroupPosition, carRot, new Vector3(0.17, 0.17, 0.17))
        .multiply(i < 2 ? new Matrix4().makeRotationY(Math.PI) : new Matrix4())
        .multiply(new Matrix4().makeRotationZ(w.visualRotationZ * DEG))
        .multiply(new Matrix4().makeRotationX(w.pDistanceCovered * DEG));
      wheels[i].matrix.copy(m);
      wheels[i].matrixWorldNeedsUpdate = true;
    });

    // car.stepFrame shadow: ray straight down from the body position.
    const hit = rays.modelsUnderRay(car.rb.position, new Vector3(0, 0, -1));
    shadow.visible = !!hit;
    if (hit) {
      const n = hit.isectNormal.clone().normalize();
      const pos = hit.isectPosition.clone().addScaledVector(n, 2);
      const forward = new Vector3().setFromMatrixColumn(carT, 1);
      forward.addScaledVector(n, -forward.dot(n)).normalize();
      const side = forward.clone().cross(n);
      const basis = new Matrix4().makeBasis(side, forward, n);
      shadow.matrix.copy(basis.setPosition(pos)).multiply(new Matrix4().makeScale(32, 78, 1));
      shadow.matrixWorldNeedsUpdate = true;
    }
  }
}