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/glb.ts
// Minimal glTF binary reader. parseGlb is the DOM-free core (level derivation,
// headless sims, tests); readGlb builds three.js geometry and textures on top.
// Covers the GLBs from macromelt + build_level.py, tools/assets and Blender's
// exporter: static meshes, TRS or matrix nodes, extras, embedded images and
// samplers, base-colour and emissive textures (KHR_texture_transform,
// KHR_materials_emissive_strength), TEXCOORD_1 (lightmap) and COLOR_0 (baked
// light). Images decode through <img> + blob URLs, so no fetch is involved.
import { BufferAttribute, BufferGeometry, Matrix4, Quaternion, Texture, Vector3 } from 'three';

type Extras = Record<string, unknown>;

interface TextureInfo {
  index: number;
  texCoord?: number;
  extensions?: { KHR_texture_transform?: { offset?: number[]; rotation?: number; scale?: number[]; texCoord?: number } };
}

export interface GltfDoc {
  asset?: { generator?: string };
  extensionsRequired?: string[];
  accessors: Array<{ bufferView?: number; byteOffset?: number; componentType: number; count: number; type: string; normalized?: boolean }>;
  bufferViews: Array<{ byteOffset?: number; byteLength: number; byteStride?: number }>;
  images?: Array<{ bufferView?: number; mimeType?: string; name?: string; uri?: string }>;
  samplers?: Array<{ wrapS?: number; wrapT?: number }>;
  textures?: Array<{ source?: number; sampler?: number; extensions?: { EXT_texture_webp?: { source: number } } }>;
  materials?: Array<{
    name?: string;
    extras?: Extras;
    alphaMode?: string;
    alphaCutoff?: number;
    pbrMetallicRoughness?: { baseColorTexture?: TextureInfo; baseColorFactor?: number[] };
    emissiveTexture?: TextureInfo;
    emissiveFactor?: number[];
    extensions?: { KHR_materials_emissive_strength?: { emissiveStrength?: number } };
  }>;
  meshes?: Array<{
    name?: string;
    extras?: Extras;
    primitives: Array<{ attributes: Record<string, number>; indices?: number; material?: number; mode?: number }>;
  }>;
  nodes?: Array<{
    name?: string;
    extras?: Extras;
    mesh?: number;
    children?: number[];
    matrix?: number[];
    translation?: number[];
    rotation?: number[];
    scale?: number[];
  }>;
  scene?: number;
  scenes?: Array<{ name?: string; extras?: Extras; nodes?: number[] }>;
}

type ComponentArray = Float32Array | Uint32Array | Uint16Array | Int16Array | Uint8Array | Int8Array;

export interface AccessorData {
  array: ComponentArray;
  itemSize: number;
  normalized: boolean;
}

export interface ParsedGlb {
  doc: GltfDoc;
  /** A bufferView's bytes. */
  view(index: number): Uint8Array;
  /** An accessor's elements, tightly packed (strides and missing views resolved). */
  accessor(index: number): AccessorData;
}

/** Extensions this reader understands; any other required one is refused. */
const SUPPORTED_EXTENSIONS = new Set(['KHR_texture_transform', 'KHR_materials_emissive_strength', 'EXT_texture_webp']);
const COMPONENTS: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT4: 16 };
const TRIANGLES = 4;

function componentArray(type: number, buffer: ArrayBuffer, length: number): ComponentArray {
  switch (type) {
    case 5126: return new Float32Array(buffer, 0, length);
    case 5125: return new Uint32Array(buffer, 0, length);
    case 5123: return new Uint16Array(buffer, 0, length);
    case 5122: return new Int16Array(buffer, 0, length);
    case 5121: return new Uint8Array(buffer, 0, length);
    case 5120: return new Int8Array(buffer, 0, length);
    default: throw new Error(`glTF: unsupported component type ${type}`);
  }
}

function byteSize(componentType: number): number {
  return componentType === 5126 || componentType === 5125 ? 4 : componentType === 5123 || componentType === 5122 ? 2 : 1;
}

export function parseGlb(buffer: ArrayBuffer): ParsedGlb {
  const data = new DataView(buffer);
  if (buffer.byteLength < 20 || data.getUint32(0, true) !== 0x46546c67) throw new Error('not a GLB');
  const jsonLength = data.getUint32(12, true);
  const doc = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, 20, jsonLength))) as GltfDoc;
  const binStart = 20 + jsonLength;
  const bin = binStart + 8 <= buffer.byteLength ? new Uint8Array(buffer, binStart + 8, data.getUint32(binStart, true)) : new Uint8Array(0);
  const unsupported = (doc.extensionsRequired ?? []).filter((e) => !SUPPORTED_EXTENSIONS.has(e));
  if (unsupported.length) throw new Error(`This model needs ${unsupported.join(', ')}; export it without compression.`);

  const view = (i: number) => {
    const bv = doc.bufferViews[i];
    return bin.subarray(bv.byteOffset ?? 0, (bv.byteOffset ?? 0) + bv.byteLength);
  };
  const accessor = (i: number): AccessorData => {
    const acc = doc.accessors[i];
    const itemSize = COMPONENTS[acc.type];
    const size = byteSize(acc.componentType);
    const length = acc.count * itemSize;
    const out = new ArrayBuffer(length * size);
    if (acc.bufferView !== undefined) {
      const bytes = view(acc.bufferView);
      const stride = doc.bufferViews[acc.bufferView].byteStride ?? itemSize * size;
      const element = itemSize * size;
      const dst = new Uint8Array(out);
      for (let e = 0; e < acc.count; e++) {
        const src = (acc.byteOffset ?? 0) + e * stride;
        dst.set(bytes.subarray(src, src + element), e * element);
      }
    }
    return { array: componentArray(acc.componentType, out, length), itemSize, normalized: acc.normalized ?? false };
  };
  return { doc, view, accessor };
}

/** The scene's root node indices. */
export function sceneRoots(doc: GltfDoc): number[] {
  const scene = doc.scenes?.[doc.scene ?? 0];
  return scene?.nodes ?? [];
}

/** A node's local transform. */
export function nodeLocalMatrix(n: NonNullable<GltfDoc['nodes']>[number]): Matrix4 {
  if (n.matrix) return new Matrix4().fromArray(n.matrix);
  const t = n.translation ?? [0, 0, 0];
  const r = n.rotation ?? [0, 0, 0, 1];
  const s = n.scale ?? [1, 1, 1];
  return new Matrix4().compose(new Vector3(t[0], t[1], t[2]), new Quaternion(r[0], r[1], r[2], r[3]), new Vector3(s[0], s[1], s[2]));
}

/** Primitives the readers can use: triangle lists only. */
export function isTriangles(p: { mode?: number }): boolean {
  return (p.mode ?? TRIANGLES) === TRIANGLES;
}

export interface GlbPrimitive {
  geometry: BufferGeometry;
  material: GlbMaterial;
}

export interface GlbMaterial {
  name: string;
  extras: Extras;
  color: [number, number, number, number];
  texture: Texture | null;
  /** Emissive colour (factor × strength) and texture; editor levels use texCoord 1 as a lightmap. */
  emissive: [number, number, number];
  emissiveTexture: Texture | null;
  emissiveTexCoord: number;
  blend: boolean;
  /** MASK materials' alpha cutoff, else null. */
  alphaCutoff: number | null;
}

export interface GlbMesh {
  name: string;
  extras: Extras;
  primitives: GlbPrimitive[];
}

export interface GlbNode {
  name: string;
  extras: Extras;
  /** Local transform relative to the parent node (Director Z-up for macromelt files). */
  matrix: Matrix4;
  mesh: GlbMesh | null;
  children: GlbNode[];
}

const WRAP = { 33071: 1001, 33648: 1002, 10497: 1000 } as const; // GL wrap → three ClampToEdge/MirroredRepeat/Repeat

export async function readGlb(buffer: ArrayBuffer): Promise<GlbNode[]> {
  const glb = parseGlb(buffer);
  const { doc } = glb;

  // One decoded image per glTF image; textures are distinct (wrap and transform differ).
  const images = new Map<number, Promise<HTMLImageElement>>();
  const image = (i: number) => {
    if (!images.has(i)) {
      const img = doc.images![i];
      if (img.bufferView === undefined) throw new Error('glTF: external image files are not supported; export as GLB');
      images.set(i, decodeImage(new Blob([glb.view(img.bufferView).slice()], { type: img.mimeType ?? 'image/png' })));
    }
    return images.get(i)!;
  };
  const texture = async (info: TextureInfo | undefined): Promise<Texture | null> => {
    const t = info && doc.textures?.[info.index];
    const source = t?.extensions?.EXT_texture_webp?.source ?? t?.source;
    if (!t || source === undefined) return null;
    const texture = new Texture(await image(source));
    texture.name = doc.images![source].name ?? '';
    texture.flipY = false;
    // glTF wraps (repeats) unless a sampler says otherwise.
    const sampler = t.sampler !== undefined ? doc.samplers?.[t.sampler] : undefined;
    texture.wrapS = WRAP[(sampler?.wrapS ?? 10497) as keyof typeof WRAP] ?? 1000;
    texture.wrapT = WRAP[(sampler?.wrapT ?? 10497) as keyof typeof WRAP] ?? 1000;
    const transform = info!.extensions?.KHR_texture_transform;
    if (transform) {
      if (transform.offset) texture.offset.fromArray(transform.offset);
      if (transform.rotation) texture.rotation = transform.rotation;
      if (transform.scale) texture.repeat.fromArray(transform.scale);
      texture.updateMatrix();
    }
    texture.needsUpdate = true;
    return texture;
  };

  const materials: GlbMaterial[] = await Promise.all(
    (doc.materials ?? []).map(async (m) => {
      const pbr = m.pbrMetallicRoughness ?? {};
      const f = pbr.baseColorFactor ?? [1, 1, 1, 1];
      const e = m.emissiveFactor ?? [0, 0, 0];
      const strength = m.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
      return {
        name: m.name ?? '',
        extras: m.extras ?? {},
        color: [f[0], f[1], f[2], f[3]],
        texture: await texture(pbr.baseColorTexture),
        emissive: [e[0] * strength, e[1] * strength, e[2] * strength],
        emissiveTexture: await texture(m.emissiveTexture),
        emissiveTexCoord: m.emissiveTexture?.extensions?.KHR_texture_transform?.texCoord ?? m.emissiveTexture?.texCoord ?? 0,
        blend: m.alphaMode === 'BLEND' || (m.alphaMode !== 'MASK' && f[3] < 1),
        alphaCutoff: m.alphaMode === 'MASK' ? (m.alphaCutoff ?? 0.5) : null,
      };
    }),
  );
  const fallback: GlbMaterial = {
    name: 'default', extras: {}, color: [1, 1, 1, 1], texture: null,
    emissive: [0, 0, 0], emissiveTexture: null, emissiveTexCoord: 0, blend: false, alphaCutoff: null,
  };
  const attribute = (i: number) => {
    const a = glb.accessor(i);
    return new BufferAttribute(a.array, a.itemSize, a.normalized);
  };
  const meshes: GlbMesh[] = (doc.meshes ?? []).map((m) => ({
    name: (m.name ?? '').replace(/^mesh:/, ''),
    extras: m.extras ?? {},
    primitives: m.primitives.filter(isTriangles).map((p) => {
      const geometry = new BufferGeometry();
      geometry.setAttribute('position', attribute(p.attributes.POSITION));
      if (p.attributes.NORMAL !== undefined) geometry.setAttribute('normal', attribute(p.attributes.NORMAL));
      if (p.attributes.TEXCOORD_0 !== undefined) geometry.setAttribute('uv', attribute(p.attributes.TEXCOORD_0));
      if (p.attributes.TEXCOORD_1 !== undefined) geometry.setAttribute('uv1', attribute(p.attributes.TEXCOORD_1));
      // Baked vertex lighting; the shader uses it instead of its lights.
      if (p.attributes.COLOR_0 !== undefined) geometry.setAttribute('bake', attribute(p.attributes.COLOR_0));
      if (p.indices !== undefined) geometry.setIndex(attribute(p.indices));
      geometry.computeBoundingSphere();
      return { geometry, material: p.material !== undefined ? materials[p.material] : fallback };
    }),
  }));

  const build = (i: number): GlbNode => {
    const n = doc.nodes![i];
    return {
      name: n.name ?? '',
      extras: n.extras ?? {},
      matrix: nodeLocalMatrix(n),
      mesh: n.mesh !== undefined ? meshes[n.mesh] : null,
      children: (n.children ?? []).map(build),
    };
  };
  // macromelt wraps everything in a Y-up "SceneRoot"; the game stays Z-up, so
  // return the root's children with their Director-space transforms.
  const roots = sceneRoots(doc).map(build);
  return roots.flatMap((root) => (root.name === 'SceneRoot' ? root.children : [root]));
}

export function decodeImage(blob: Blob): Promise<HTMLImageElement> {
  const url = URL.createObjectURL(blob);
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      URL.revokeObjectURL(url);
      resolve(img);
    };
    img.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error('image decode failed'));
    };
    img.src = url;
  });
}