HF-Racing / www /js /track.js
1024m's picture
Upload folder using huggingface_hub
0820aaf verified
Raw History Blame Contribute Delete
4.05 kB
/**
* Shanghai International Circuit loader + start/grid poses.
*/
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const MAP_URL = 'assets/maps/shanghai/track.glb';
export class Track {
constructor(scene) {
this.scene = scene;
this.root = new THREE.Group();
scene.add(this.root);
this._loader = new GLTFLoader();
this.mapId = null;
this.ready = false;
this.start = { x: 0, y: 0.4, z: 0, yaw: 0 };
this._bbox = null;
// Temporary ground until map loads
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(200, 200),
new THREE.MeshStandardMaterial({ color: 0x2a2622, roughness: 0.95 }),
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
ground.name = '__placeholder';
this.root.add(ground);
}
async loadShanghai() {
try {
const gltf = await this._loader.loadAsync(MAP_URL);
while (this.root.children.length) {
this.root.remove(this.root.children[0]);
}
const model = gltf.scene;
model.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});
this.root.add(model);
this.mapId = 'shanghai';
this.ready = true;
this._deriveStartFromMap(model);
return true;
} catch (err) {
console.warn('map load failed', err);
this.ready = false;
return false;
}
}
/**
* Find start/finish / pit / grid cues in node names; else use asphalt AABB heuristic.
* Shanghai SIC start/finish sits near the pit straight — we prefer named nodes,
* then fall back to the longest thin asphalt-ish bounds.
*/
_deriveStartFromMap(model) {
const nameHits = [];
const meshes = [];
model.updateWorldMatrix(true, true);
model.traverse((o) => {
if (!o.isMesh) return;
meshes.push(o);
const n = `${o.name || ''} ${o.parent?.name || ''}`.toLowerCase();
if (
n.includes('start') ||
n.includes('finish') ||
n.includes('grid') ||
n.includes('pit') ||
n.includes('s/f') ||
n.includes('sf_')
) {
nameHits.push(o);
}
});
let anchor = null;
let yaw = 0;
if (nameHits.length) {
const box = new THREE.Box3();
for (const m of nameHits) box.expandByObject(m);
const c = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
anchor = c;
// Face along the longer horizontal axis of the start strip.
yaw = size.x >= size.z ? 0 : Math.PI / 2;
} else {
// Fallback: whole-map center shifted toward a long strip (pit straight guess).
const box = new THREE.Box3().setFromObject(model);
this._bbox = box;
const c = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
// Shanghai-like: start straight often near one end of the long axis.
if (size.x >= size.z) {
anchor = new THREE.Vector3(c.x + size.x * 0.12, box.min.y, c.z);
yaw = 0;
} else {
anchor = new THREE.Vector3(c.x, box.min.y, c.z + size.z * 0.12);
yaw = Math.PI / 2;
}
}
const groundY = (() => {
const box = new THREE.Box3().setFromObject(model);
return box.min.y;
})();
this.start = {
x: anchor.x,
y: groundY + 0.35,
z: anchor.z,
yaw,
};
}
/** Grid slot relative to start line (GTA-style staggered pairs). */
getGridPose(index = 0) {
const i = Math.max(0, index | 0);
const row = Math.floor(i / 2);
const side = i % 2 === 0 ? -1 : 1;
const lateral = side * 3.2;
const back = -row * 7.5;
const yaw = this.start.yaw;
const fx = Math.sin(yaw);
const fz = Math.cos(yaw);
const rx = Math.cos(yaw);
const rz = -Math.sin(yaw);
return {
x: this.start.x + fx * back + rx * lateral,
y: this.start.y,
z: this.start.z + fz * back + rz * lateral,
yaw,
};
}
}