SOURCE / PINNED RELEASE
Made of little things.
Supersonic RC Revive
- Release
- 372375d18e3f…
- Author-recorded commit
- 6fbf09f88a0a…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f
Archive hash verified: 76e4eb30c67a6f45…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Minimal reader for the GLB files produced by macromelt + build_level.py and
// by tools/assets: static meshes, TRS nodes, embedded images, base-colour
// materials, an optional TEXCOORD_1 (lightmap) set and an optional COLOR_0
// (baked light). Images decode through <img> + blob URLs, so no fetch is involved.
import { BufferAttribute, BufferGeometry, Matrix4, Quaternion, Texture, Vector3 } from 'three';
interface GltfDoc {
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 }>;
textures?: Array<{ source: number; sampler?: number }>;
materials?: Array<{
name?: string;
alphaMode?: string;
pbrMetallicRoughness?: { baseColorTexture?: { index: number }; baseColorFactor?: number[] };
}>;
meshes: Array<{ name?: string; extras?: Record<string, unknown>; primitives: Array<{ attributes: Record<string, number>; indices?: number; material?: number }> }>;
nodes: Array<{ name?: string; mesh?: number; children?: number[]; translation?: number[]; rotation?: number[]; scale?: number[] }>;
scenes: Array<{ nodes: number[] }>;
}
export interface GlbPrimitive {
geometry: BufferGeometry;
material: GlbMaterial;
}
export interface GlbMaterial {
name: string;
color: [number, number, number, number];
texture: Texture | null;
blend: boolean;
}
export interface GlbMesh {
name: string;
extras: Record<string, unknown>;
primitives: GlbPrimitive[];
}
export interface GlbNode {
name: string;
/** Local transform relative to the parent node (Director Z-up). */
matrix: Matrix4;
mesh: GlbMesh | null;
children: GlbNode[];
}
const COMPONENTS: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
export async function readGlb(buffer: ArrayBuffer): Promise<GlbNode[]> {
const view = new DataView(buffer);
if (view.getUint32(0, true) !== 0x46546c67) throw new Error('not a GLB');
const jsonLength = view.getUint32(12, true);
const doc = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, 20, jsonLength))) as GltfDoc;
const binOffset = 20 + jsonLength + 8;
const bin = new Uint8Array(buffer, binOffset, view.getUint32(20 + jsonLength, true));
const viewBytes = (i: number) => {
const bv = doc.bufferViews[i];
return bin.subarray(bv.byteOffset ?? 0, (bv.byteOffset ?? 0) + bv.byteLength);
};
const accessor = (i: number): BufferAttribute => {
const acc = doc.accessors[i];
const width = COMPONENTS[acc.type];
const bytes = viewBytes(acc.bufferView);
const start = bytes.byteOffset + (acc.byteOffset ?? 0);
const copy = bin.buffer.slice(start, start + acc.count * width * byteSize(acc.componentType));
const array =
acc.componentType === 5126 ? new Float32Array(copy)
: acc.componentType === 5125 ? new Uint32Array(copy)
: acc.componentType === 5123 ? new Uint16Array(copy)
: new Uint8Array(copy);
return new BufferAttribute(array, width, acc.normalized ?? false);
};
const textures = await Promise.all(
(doc.textures ?? []).map(async (t) => {
const img = doc.images![t.source];
const blob = new Blob([viewBytes(img.bufferView).slice()], { type: img.mimeType });
const texture = new Texture(await decodeImage(blob));
texture.name = img.name ?? '';
texture.flipY = false;
texture.needsUpdate = true;
return texture;
}),
);
const materials: GlbMaterial[] = (doc.materials ?? []).map((m) => {
const pbr = m.pbrMetallicRoughness ?? {};
const f = pbr.baseColorFactor ?? [1, 1, 1, 1];
return {
name: m.name ?? '',
color: [f[0], f[1], f[2], f[3]],
texture: pbr.baseColorTexture ? textures[pbr.baseColorTexture.index] : null,
blend: m.alphaMode === 'BLEND' || f[3] < 1,
};
});
const fallback: GlbMaterial = { name: 'default', color: [1, 1, 1, 1], texture: null, blend: false };
const meshes: GlbMesh[] = doc.meshes.map((m) => ({
name: (m.name ?? '').replace(/^mesh:/, ''),
extras: m.extras ?? {},
primitives: m.primitives.map((p) => {
const geometry = new BufferGeometry();
geometry.setAttribute('position', accessor(p.attributes.POSITION));
if (p.attributes.NORMAL !== undefined) geometry.setAttribute('normal', accessor(p.attributes.NORMAL));
if (p.attributes.TEXCOORD_0 !== undefined) geometry.setAttribute('uv', accessor(p.attributes.TEXCOORD_0));
if (p.attributes.TEXCOORD_1 !== undefined) geometry.setAttribute('uv1', accessor(p.attributes.TEXCOORD_1));
// Baked vertex lighting (custom bundle); the shader uses it instead of its lights.
if (p.attributes.COLOR_0 !== undefined) geometry.setAttribute('bake', accessor(p.attributes.COLOR_0));
if (p.indices !== undefined) geometry.setIndex(accessor(p.indices));
geometry.computeBoundingSphere();
return { geometry, material: p.material !== undefined ? materials[p.material] : fallback };
}),
}));
const build = (i: number): GlbNode => {
const n = doc.nodes[i];
const t = n.translation ?? [0, 0, 0];
const r = n.rotation ?? [0, 0, 0, 1];
const s = n.scale ?? [1, 1, 1];
return {
name: n.name ?? '',
matrix: 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])),
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 = doc.scenes[0].nodes.map(build);
return roots.flatMap((root) => (root.name === 'SceneRoot' ? root.children : [root]));
}
function byteSize(componentType: number): number {
return componentType === 5126 || componentType === 5125 ? 4 : componentType === 5123 ? 2 : 1;
}
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;
});
}
