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