mirror of
https://github.com/PlayableIntelligence/game-creator.git
synced 2026-09-19 07:34:10 +08:00
90269327a5
World Labs SPZ environment with animated Soldier character (Three.js + SparkJS). Fixes Y-flip, panorama doubling, character scaling, third-person camera, and ground raycasting for navigable photorealistic arcade scene. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
95 lines
2.3 KiB
JavaScript
95 lines
2.3 KiB
JavaScript
import * as THREE from 'three';
|
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
|
|
|
|
const loader = new GLTFLoader();
|
|
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
const cache = new Map();
|
|
|
|
/**
|
|
* Load a GLTF/GLB model (static, no skeleton).
|
|
*/
|
|
export async function loadModel(path) {
|
|
const gltf = await _load(path);
|
|
const clone = gltf.scene.clone(true);
|
|
|
|
clone.traverse((child) => {
|
|
if (child.isMesh) {
|
|
child.material = child.material.clone();
|
|
child.castShadow = true;
|
|
child.receiveShadow = true;
|
|
}
|
|
});
|
|
|
|
return clone;
|
|
}
|
|
|
|
/**
|
|
* Load a GLTF/GLB model with skeleton + animations.
|
|
* Uses SkeletonUtils.clone() so bone bindings survive cloning.
|
|
*/
|
|
export async function loadAnimatedModel(path) {
|
|
const gltf = await _load(path);
|
|
|
|
// SkeletonUtils.clone properly re-binds SkinnedMesh to cloned Skeleton
|
|
const model = SkeletonUtils.clone(gltf.scene);
|
|
|
|
model.traverse((child) => {
|
|
if (child.isMesh) {
|
|
child.castShadow = true;
|
|
child.receiveShadow = true;
|
|
}
|
|
});
|
|
|
|
return { model, clips: gltf.animations };
|
|
}
|
|
|
|
/**
|
|
* Preload multiple paths in parallel. Returns when all are cached.
|
|
* @param {string[]} paths
|
|
* @param {(loaded: number, total: number) => void} [onProgress]
|
|
*/
|
|
export async function preloadAll(paths, onProgress) {
|
|
let loaded = 0;
|
|
const total = paths.length;
|
|
await Promise.all(paths.map((path) =>
|
|
_load(path).then(() => {
|
|
loaded++;
|
|
if (onProgress) onProgress(loaded, total);
|
|
})
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Dispose all cached models.
|
|
*/
|
|
export function disposeAll() {
|
|
cache.forEach((promise) => {
|
|
promise.then((gltf) => {
|
|
gltf.scene.traverse((child) => {
|
|
if (child.isMesh) {
|
|
child.geometry.dispose();
|
|
if (Array.isArray(child.material)) {
|
|
child.material.forEach((m) => m.dispose());
|
|
} else {
|
|
child.material.dispose();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
cache.clear();
|
|
}
|
|
|
|
function _load(path) {
|
|
if (!cache.has(path)) {
|
|
cache.set(path, new Promise((resolve, reject) => {
|
|
loader.load(path, resolve, undefined,
|
|
(err) => reject(new Error(`Failed to load: ${path} — ${err.message || err}`))
|
|
);
|
|
}));
|
|
}
|
|
return cache.get(path);
|
|
}
|