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