Files
knsoandClaude Opus 5 fb38652e7f chore(controller): three-player-controller als Ordner statt Submodul
Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur
lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json.
Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser
Stand als normaler Ordner im Repo, ohne Verweis auf GitHub.

dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher
lokal gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:27:03 +02:00

214 lines
6.3 KiB
JavaScript

import {
ACESFilmicToneMapping,
AmbientLight,
EquirectangularReflectionMapping,
PerspectiveCamera,
Scene,
Vector3,
WebGLRenderer,
} from "three";
import { MapControls } from "three/examples/jsm/Addons.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
import { playerController } from "../src/PlayerController";
import { SplatMesh, SparkRenderer } from "@sparkjsdev/spark";
import Stats from 'three/examples/jsm/libs/stats.module.js';
let player;
const scene = new Scene();
let camera;
let renderer;
let controls;
let gltfLoader;
let stats;
const pos = new Vector3(1.235, 1.21, -3.9);
const scaleNormal = 0.01;
const scaleSmall = scaleNormal / 9;
let isSmallScale = false;
let scaleAnimFrame = null;
let isScaling = false;
init();
async function init() {
const cont = document.querySelector("#container");
// 渲染器
renderer = new WebGLRenderer({ antialias: true });
renderer.setSize(cont.clientWidth, cont.clientHeight);
renderer.shadowMap.enabled = true;
renderer.toneMapping = ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
renderer.setAnimationLoop(animate);
cont.appendChild(renderer.domElement);
// 相机
camera = new PerspectiveCamera(60, cont.clientWidth / cont.clientHeight, 0.01, 1000);
camera.position.copy(pos);
camera.lookAt(pos.x, pos.y, pos.z + 1);
// 控制器
controls = new MapControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.maxDistance = 2000;
controls.dampingFactor = 0.1;
controls.rotateSpeed = 1;
controls.maxPolarAngle = Math.PI / 2;
controls.target.set(pos.x, pos.y, pos.z + 1);
// 环境光
const ambient = new AmbientLight(0xffffff, 5);
scene.add(ambient);
// 背景
new HDRLoader().load(
"./img/env.hdr",
(texture) => {
texture.mapping = EquirectangularReflectionMapping;
scene.background = texture;
},
undefined,
(err) => console.warn("HDR 加载失败:", err)
);
// 帧率
stats = new Stats();
Object.assign(stats.dom.style, {
position: "fixed",
bottom: "0px",
left: "0px",
top: "auto",
zIndex: "9998",
display: "block",
});
document.body.appendChild(stats.dom);
// 加载3DGS场景
initGltfLoader();
const spark = new SparkRenderer({ renderer });
scene.add(spark);
const splatURL = './3DGS/3DGS.sog'
const splat = new SplatMesh({
url: splatURL,
onProgress: (e) => window.setLoaderProgress?.(e.loaded, e.total),
onLoad: (mesh) => { mesh.rotateX(-Math.PI / 2); },
});
scene.add(splat);
// 加载碰撞体
const colliderGltf = await gltfLoader.loadAsync("./glb/3dgs_collider.glb");
const playerGltf = await gltfLoader.loadAsync("./glb/josh.glb");
// 人物控制器
player = new playerController();
await player.init({
scene,
camera,
controls,
playerModelConfig: {
model: playerGltf.scene,
animations: playerGltf.animations,
scale: scaleNormal,
idleAnim: "idle1",
walkAnim: "walk",
runAnim: "run",
jumpAnim: ["jump", "Jump_Loop", "Jump_Land"],
flyAnim: "fly",
flyIdleAnim: "flyIdle",
flyHoverForwardAnim: "flyHoverForward",
flyHoverBackAnim: "flyHoverBack",
flyHoverLeftAnim: "flyHoverLeft",
flyHoverRightAnim: "flyHoverRight",
flyHoverUpAnim: "flyHoverUp",
flyHoverDownAnim: "flyHoverDown",
drivingAnim: "Driving_Loop",
headBoneName: "mixamorigHead",
speed: 150,
flySpeed: 1000,
jumpHeight: 400,
},
initPos: pos,
minCamDistance: 50,
maxCamDistance: 180,
camLookAtHeightRatio: 0.8,
colliders: [
{ motion: "static", shape: { kind: "mesh", source: colliderGltf.scene } },
],
enableOverShoulderView: true,
});
window.addEventListener("resize", onWindowResize, false);
// 监听按键
window.addEventListener("keydown", (e) => {
if (e.code !== "KeyZ") return;
if (isScaling) return; // 缩放中不运行
isSmallScale = !isSmallScale;
animateToScale(isSmallScale ? scaleSmall : scaleNormal, 1);
});
// 等待高斯泼溅模型加载完毕再隐藏 loader
await splat.initialized.catch(() => { });
window.hideLoader();
}
function animateToScale(targetScale, duration = 0.5) {
if (scaleAnimFrame !== null) {
cancelAnimationFrame(scaleAnimFrame);
scaleAnimFrame = null;
}
isScaling = true;
const fromScale = player.getScale?.() ?? (isSmallScale ? scaleNormal : scaleSmall);
const startTime = performance.now();
const tick = (now) => {
const t = Math.min((now - startTime) / (duration * 1000), 1);
const current = fromScale + (targetScale - fromScale) * t;
player.setPlayerScale(current);
if (t < 1) {
scaleAnimFrame = requestAnimationFrame(tick);
} else {
scaleAnimFrame = null;
isScaling = false;
}
};
scaleAnimFrame = requestAnimationFrame(tick);
}
// 初始化glb加载器
function initGltfLoader() {
gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/");
gltfLoader.setDRACOLoader(dracoLoader);
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/basis/");
ktx2Loader.detectSupport(renderer);
gltfLoader.setKTX2Loader(ktx2Loader);
}
// 每帧调用
function animate() {
if (player) {
player.update();
} else {
controls.update();
}
renderer.render(scene, camera);
stats?.update();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
renderer.setSize(window.innerWidth, window.innerHeight);
camera.updateProjectionMatrix();
renderer.setPixelRatio(window.devicePixelRatio * 1);
}