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.
// Levels authored in Blender: one GLB holds the visible objects, hidden
// collision objects, markers and any baked light (docs/level-editing.md).
// The game derives what tools used to write as collision.json, raymesh.bin
// and markers.json from it at load. Blender exports glTF Y-up in metres; the
// game is Director Z-up in inches.
import { Matrix4, Vector3 } from 'three';
import { type GlbNode, type GltfDoc, isTriangles, nodeLocalMatrix, type ParsedGlb, parseGlb, sceneRoots } from '../render/glb.js';
import type { CollisionFile, LevelData, Markers, W3DNode } from './data.js';
export const LEVEL_VERSION = 1;
/** The original's Havok world: gravity in in/s² and the unit scale (physics constants, kept). */
export const DEFAULT_GRAVITY = [0, 0, -386.21970060020317];
export const DEFAULT_HAVOK_SCALE = 0.025400051847100258;
const DEFAULT_FRICTION = 0.3;
/** The custom bundle's car hull: a 15 × 30 × 10 box, used when neither the level nor the pack has one. */
export const DEFAULT_CAR_HULL: Markers['car_hull'] = {
name: 'car',
node_matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
positions: [-7.5, 7.5].flatMap((x) => [-15, 15].flatMap((y) => [0, 10].map((z) => [x, y, z]))),
};
const INCHES_PER_METRE = 1 / 0.0254;
/** glTF (Y-up, metres) to game space (Z-up, inches): (x, y, z) → (x, −z, y) / 0.0254. */
export const GLTF_TO_GAME = new Matrix4().set(
INCHES_PER_METRE, 0, 0, 0,
0, 0, -INCHES_PER_METRE, 0,
0, INCHES_PER_METRE, 0, 0,
0, 0, 0, 1,
);
export type Collide = 'none' | 'mesh' | 'convex';
export type Role = 'visual' | 'collision' | 'ray' | 'car_hull' | 'ignore';
export interface ObjectFlags {
/** The name the game uses (bodies, ray models). */
name: string;
role: Role;
render: boolean;
collide: Collide;
ray: boolean;
friction: number;
restitution: number;
/** Sort key for collision bodies and ray models (document order breaks ties). */
order: number;
/** COLOR_0 is baked light (not a stray colour attribute). */
baked: boolean;
}
export interface LevelSettings {
version: number;
gravity: number[];
scale: number;
title: string | null;
/** Converted from the 2004 game's files: local use only. */
original: boolean;
lighting: 'vertex' | 'lightmap' | 'lights' | null;
}
const ROLE_DEFAULTS: Record<Role, Pick<ObjectFlags, 'render' | 'collide' | 'ray'>> = {
visual: { render: true, collide: 'mesh', ray: true },
collision: { render: false, collide: 'mesh', ray: false },
ray: { render: false, collide: 'none', ray: true },
car_hull: { render: false, collide: 'none', ray: false },
ignore: { render: false, collide: 'none', ray: false },
};
/** Blender suffixes duplicate names with ".001"; the game ignores it. */
export function baseName(name: string): string {
return name.replace(/\.\d{3,}$/, '');
}
const flag = (v: unknown): boolean | undefined => (typeof v === 'boolean' ? v : typeof v === 'number' ? v !== 0 : undefined);
const num = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined);
/** How the game treats a mesh object, from its ssrc_* custom properties. */
export function objectFlags(nodeName: string, extras: Record<string, unknown>, lighting: LevelSettings['lighting'] = null): ObjectFlags {
const base = baseName(nodeName);
const roleProp = str(extras.ssrc_role);
const role: Role =
roleProp && roleProp in ROLE_DEFAULTS ? (roleProp as Role)
: /^COL_/i.test(base) ? 'collision'
: /^RAY_/i.test(base) ? 'ray'
: 'visual';
const d = ROLE_DEFAULTS[role];
const render = flag(extras.ssrc_render) ?? d.render;
const collideProp = str(extras.ssrc_collide);
const collide: Collide = collideProp === 'none' || collideProp === 'mesh' || collideProp === 'convex' ? collideProp : d.collide;
return {
name: str(extras.ssrc_name) ?? base.replace(/^(COL|RAY)_/i, ''),
role,
render: role === 'ignore' ? false : render,
collide: role === 'ignore' ? 'none' : collide,
ray: role === 'ignore' ? false : (flag(extras.ssrc_ray) ?? (role === 'visual' ? render : d.ray)),
friction: num(extras.ssrc_friction) ?? DEFAULT_FRICTION,
restitution: num(extras.ssrc_restitution) ?? DEFAULT_FRICTION,
order: num(extras.ssrc_order) ?? Infinity,
baked: flag(extras.ssrc_baked) ?? lighting === 'vertex',
};
}
/** The level's settings: scene custom properties, or a node carrying ssrc_level (collection exporters drop scene ones). Null for a plain GLB. */
export function levelSettings(doc: GltfDoc): LevelSettings | null {
const sources = [doc.scenes?.[doc.scene ?? 0]?.extras, ...(doc.nodes ?? []).map((n) => n.extras)];
const extras = sources.find((e) => e && num(e.ssrc_level) !== undefined);
if (!extras) return null;
const gravity = Array.isArray(extras.ssrc_gravity) && extras.ssrc_gravity.length === 3 && extras.ssrc_gravity.every((v) => num(v) !== undefined)
? (extras.ssrc_gravity as number[])
: DEFAULT_GRAVITY;
const lighting = str(extras.ssrc_lighting);
return {
version: num(extras.ssrc_level)!,
gravity,
scale: num(extras.ssrc_havok_scale) ?? DEFAULT_HAVOK_SCALE,
title: str(extras.ssrc_title) ?? null,
original: flag(extras.ssrc_original) ?? false,
lighting: lighting === 'vertex' || lighting === 'lightmap' || lighting === 'lights' ? lighting : null,
};
}
/** Collision vertices and marker positions are rounded to 1e-3 in: exact for the converted levels, below f32 noise for metres. */
const round = (v: number) => Math.round(v * 1000) / 1000 || 0;
/** The axis swap alone (exact 0 and ±1 entries), so conjugating by it adds no rounding. */
const AXES = new Matrix4().set(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1);
const AXES_INVERSE = AXES.clone().transpose();
/**
* A node matrix in game space (the W3D convention), C · M · C⁻¹: the
* orientation conjugated by the axis swap, the translation in inches (rounded).
*/
function gameNodeMatrix(gltfWorld: Matrix4): number[] {
const m = AXES.clone().multiply(gltfWorld).multiply(AXES_INVERSE).elements.slice();
for (const k of [12, 13, 14]) m[k] = round(m[k] * INCHES_PER_METRE);
return m;
}
export interface DerivedLevel {
level: LevelData;
settings: LevelSettings | null;
warnings: string[];
}
interface MeshObject {
flags: ObjectFlags;
index: number;
/** World-space game positions per triangle corner (3 per triangle). */
corners: number[];
/** A converted level's exact collision data (ssrc_verts / ssrc_faces), in world space. */
exact: { verts: number[]; faces: number[] } | null;
}
const numbers = (v: unknown): v is number[] => Array.isArray(v) && v.every((x) => num(x) !== undefined);
/**
* Collision data kept verbatim by the converter: the original's Havok bodies
* have duplicate, loose and degenerate vertices a Blender mesh cannot hold,
* and Rapier's hulls depend on them. Local to the object (inches, Z-up); the
* SSRC add-on drops them when the mesh is edited.
*/
function exactCollision(extras: Record<string, unknown> | undefined, toGame: number[]): MeshObject['exact'] {
const verts = extras?.ssrc_verts;
const faces = extras?.ssrc_faces;
if (!numbers(verts) || !numbers(faces) || verts.length % 3 || faces.length % 3 || faces.some((i) => i < 0 || i * 3 >= verts.length)) return null;
const identity = toGame.every((v, k) => v === (k % 5 === 0 ? 1 : 0));
if (identity) return { verts: verts.slice(), faces: faces.slice() };
const m = new Matrix4().fromArray(toGame);
const v = new Vector3();
const out: number[] = [];
for (let k = 0; k < verts.length; k += 3) {
v.set(verts[k], verts[k + 1], verts[k + 2]).applyMatrix4(m);
out.push(round(v.x), round(v.y), round(v.z));
}
const flip = m.determinant() < 0;
const f = flip ? faces.flatMap((_, k) => (k % 3 ? [] : [faces[k], faces[k + 2], faces[k + 1]])) : faces.slice();
return { verts: out, faces: f };
}
/**
* Build the level's physics and gameplay data from its GLB: collision bodies
* (welded, rounded), the wheel/camera ray mesh, marker groups from empties and
* the car hull. `carHull` is the fallback when the level has no car_hull object.
*/
export function deriveLevel(source: ArrayBuffer | ParsedGlb, options: { carHull?: Markers['car_hull'] } = {}): DerivedLevel {
const glb = source instanceof ArrayBuffer ? parseGlb(source) : source;
const { doc } = glb;
const settings = levelSettings(doc);
const warnings: string[] = [];
if (!settings) warnings.push('No ssrc_level marker: every mesh is solid and raycast. Export with the SSRC Blender add-on.');
else if (settings.version > LEVEL_VERSION) warnings.push(`Level format ${settings.version} is newer than this game understands (${LEVEL_VERSION}).`);
const objects: MeshObject[] = [];
const groups: W3DNode[] = [];
let carHull = null as Markers['car_hull'] | null;
const corners = (meshIndex: number, toGame: Matrix4): number[] => {
const out: number[] = [];
const flip = toGame.determinant() < 0;
const v = new Vector3();
for (const p of doc.meshes![meshIndex].primitives) {
if (!isTriangles(p)) continue;
const pos = glb.accessor(p.attributes.POSITION).array;
const count = pos.length / 3;
const idx = p.indices !== undefined ? glb.accessor(p.indices).array : null;
const n = idx ? idx.length : count;
for (let t = 0; t + 2 < n; t += 3) {
const tri = flip ? [t, t + 2, t + 1] : [t, t + 1, t + 2];
for (const k of tri) {
const i = idx ? idx[k] : k;
v.set(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]).applyMatrix4(toGame);
out.push(v.x, v.y, v.z);
}
}
}
return out;
};
const visit = (i: number, parent: Matrix4) => {
const n = doc.nodes![i];
const world = parent.clone().multiply(nodeLocalMatrix(n));
if (n.mesh !== undefined) {
const flags = objectFlags(n.name ?? '', n.extras ?? {}, settings?.lighting ?? null);
if (flags.role === 'car_hull') {
// The converter keeps the hull's points in their original order (ssrc_hull_points,
// inches): the convex hull Rapier builds, and so the handling, depends on it.
const listed = n.extras?.ssrc_hull_points;
const unique = new Map<string, number[]>();
if (Array.isArray(listed) && listed.length % 3 === 0 && listed.length && listed.every((v) => num(v) !== undefined)) {
for (let k = 0; k < listed.length; k += 3) unique.set(String(k), listed.slice(k, k + 3) as number[]);
} else {
const local = corners(n.mesh, GLTF_TO_GAME);
for (let k = 0; k < local.length; k += 3) {
const p = [round(local[k]), round(local[k + 1]), round(local[k + 2])];
unique.set(p.join(','), p);
}
}
const listedMatrix = n.extras?.ssrc_hull_matrix;
carHull = {
name: flags.name,
node_matrix: numbers(listedMatrix) && listedMatrix.length === 16 ? listedMatrix.slice() : gameNodeMatrix(world),
positions: [...unique.values()],
};
// The original also raycasts its parked car proxy.
if (flags.ray) objects.push({ flags, index: objects.length, corners: corners(n.mesh, GLTF_TO_GAME.clone().multiply(world)), exact: null });
} else if (flags.collide !== 'none' || flags.ray) {
const exact = flags.collide !== 'none' ? exactCollision(n.extras, gameNodeMatrix(world)) : null;
objects.push({ flags, index: objects.length, corners: corners(n.mesh, GLTF_TO_GAME.clone().multiply(world)), exact });
}
} else if (num(n.extras?.ssrc_level) === undefined) {
groups.push({ kind: 'group', name: baseName(n.name ?? ''), parent: 'World', user: str(n.extras?.ssrc_user) ?? '', matrix: gameNodeMatrix(world) });
}
for (const c of n.children ?? []) visit(c, world);
};
for (const root of sceneRoots(doc)) visit(root, new Matrix4());
objects.sort((a, b) => a.flags.order - b.flags.order || a.index - b.index);
const bodies: CollisionFile['bodies'] = [];
for (const o of objects) {
if (o.flags.collide === 'none') continue;
if (o.exact) {
bodies.push({ name: o.flags.name, convex: o.flags.collide === 'convex', friction: o.flags.friction, restitution: o.flags.restitution, ...o.exact });
continue;
}
const verts: number[] = [];
const faces: number[] = [];
const weld = new Map<string, number>();
const tri: number[] = [];
for (let k = 0; k < o.corners.length; k += 3) {
const p = [round(o.corners[k]), round(o.corners[k + 1]), round(o.corners[k + 2])];
const key = p.join(',');
let id = weld.get(key);
if (id === undefined) {
id = verts.length / 3;
weld.set(key, id);
verts.push(...p);
}
tri.push(id);
if (tri.length === 3) {
// Welding can collapse a sliver; a trimesh has no use for it.
if (tri[0] !== tri[1] && tri[1] !== tri[2] && tri[0] !== tri[2]) faces.push(...tri);
tri.length = 0;
}
}
if (!faces.length) {
warnings.push(`${o.flags.name} has no usable triangles for collision.`);
continue;
}
bodies.push({ name: o.flags.name, convex: o.flags.collide === 'convex', friction: o.flags.friction, restitution: o.flags.restitution, verts, faces });
}
if (!bodies.length) warnings.push('The level has no collision objects: the car will fall forever.');
const rayObjects = objects.filter((o) => o.flags.ray && o.corners.length);
const triangles = rayObjects.reduce((s, o) => s + o.corners.length / 9, 0);
const positions = new Float32Array(triangles * 9);
const indices = new Uint32Array(triangles * 3);
const owner = new Uint16Array(triangles);
let t = 0;
rayObjects.forEach((o, model) => {
positions.set(o.corners, t * 9);
for (let k = 0; k < o.corners.length / 9; k++, t++) {
indices.set([t * 3, t * 3 + 1, t * 3 + 2], t * 3);
owner[t] = model;
}
});
const seen = new Set<string>();
for (const g of groups) {
const key = g.name.toLowerCase();
if (/^(checkpoint|prizes)_\d+$|^ssrc_start$/.test(key) && seen.has(key)) warnings.push(`More than one marker is named ${g.name}.`);
seen.add(key);
}
const markers: Markers = {
raymesh_models: rayObjects.map((o) => o.flags.name),
car_hull: carHull ?? options.carHull ?? DEFAULT_CAR_HULL,
level: groups,
car: [],
};
const collision: CollisionFile = { gravity: settings?.gravity ?? DEFAULT_GRAVITY, scale: settings?.scale ?? DEFAULT_HAVOK_SCALE, bodies };
return { level: { markers, collision, rayMesh: { positions, indices, owner } }, settings, warnings };
}
/** A visible object of an editor level, with its world matrix in game space. */
export interface LevelVisual {
node: GlbNode;
matrix: Matrix4;
flags: ObjectFlags;
}
/** The objects an editor level draws (readGlb's tree, glTF space) and their game-space matrices. */
export function levelVisuals(roots: GlbNode[], settings: LevelSettings | null): LevelVisual[] {
const out: LevelVisual[] = [];
const visit = (node: GlbNode, parent: Matrix4) => {
const world = parent.clone().multiply(node.matrix);
if (node.mesh) {
const flags = objectFlags(node.name, node.extras, settings?.lighting ?? null);
if (flags.render) out.push({ node, matrix: world, flags });
}
for (const c of node.children) visit(c, world);
};
for (const root of roots) visit(root, GLTF_TO_GAME);
return out;
}
/** Whether a GLB is a Blender-authored level (as opposed to the legacy level.glb with JSON side files). */
export function isEditorLevel(buffer: ArrayBuffer): boolean {
try {
return levelSettings(parseGlb(buffer).doc) !== null;
} catch {
return false;
}
}
