import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'; import { createVRMAnimationClip, VRMAnimationLoaderPlugin, VRMLookAtQuaternionProxy } from '@pixiv/three-vrm-animation'; import { VRM_PATH, WS_URL } from './config.js'; import { loadVRM } from './vrmLoader.js'; import { AudioManager } from './audioManager.js'; import { AnimationManager } from './animationManager.js'; import { loadMixamoAnimation } from './loadMixamoAnimation.js'; import { showSubtitleStreaming } from './subtitles.js'; import { initWebcam, takePictureAndUpload } from './webcam.js'; import { connectWS, initUI } from "./ui.js"; import { initVRMClickDetector } from './vrmClickDetector.js'; // app.js — add near top with your other imports (no extra imports needed) // Better trimAnimationClip: construct new tracks (compatible with more three.js builds) function trimAnimationClip(clip, startTime, endTime) { // startTime/endTime in seconds (endTime is exclusive) startTime = Math.max(0, startTime || 0); const fullDuration = clip.duration; endTime = (typeof endTime === 'number' && endTime >= 0) ? Math.min(fullDuration, endTime) : fullDuration; if (endTime <= startTime) { console.warn('trimAnimationClip: invalid range', { startTime, endTime, duration: fullDuration }); return null; } const newTracks = []; for (const track of clip.tracks) { // Use the typed arrays directly for speed/compatibility const times = track.times; // Float32Array or Array const values = track.values; // Float32Array or Array const stride = track.getValueSize(); const keptTimes = []; const keptValues = []; // iterate keyframes and pick those within range // times.length should equal values.length / stride for (let i = 0; i < times.length; i++) { const t = times[i]; if (t >= startTime && t <= endTime) { keptTimes.push(t - startTime); // shift so new clip starts at 0 const baseIndex = i * stride; for (let s = 0; s < stride; s++) { keptValues.push(values[baseIndex + s]); } } } if (keptTimes.length > 0) { // Create typed arrays expected by KeyframeTrack constructors const TimesArray = Float32Array; const ValuesArray = Float32Array; const newTimes = new TimesArray(keptTimes); const newValues = new ValuesArray(keptValues); // Build a new track using the same constructor (works for Number/Vector/Quaternion) // Signature: new TrackConstructor(name, times, values, interpolation?) let NewTrack; try { NewTrack = new track.constructor(track.name, newTimes, newValues, track.getInterpolation ? track.getInterpolation() : undefined); } catch (err) { // Fallback: try without interpolation argument NewTrack = new track.constructor(track.name, newTimes, newValues); } newTracks.push(NewTrack); } } const newDuration = endTime - startTime; const newName = `${clip.name || 'vrma_clip'}_trimmed_${startTime.toFixed(3)}-${endTime.toFixed(3)}`; return new THREE.AnimationClip(newName, newDuration, newTracks); } // Handle messages from server function handleServerMessage(msg) { //console.log("📩 Received from server:", msg); if (msg.type === "start_animation") { // Play animation on VRM } else if (msg.type === "take_picture") { // Trigger snapshot from camera } } // Connect WebSocket + UI connectWS(handleServerMessage); initUI(); // Your existing VRM rendering logic continues here... // Initialize webcam once on startup if needed await initWebcam(); // VRM LOADERS // Global variables let currentMixer = null; let vrm = null; let renderer = null; let scene = null; let camera = null; let controls = null; let animationMgr = null; // ADD THIS const clock = new THREE.Clock(); let currentVrm = null; let currentAction = null; (async () => { // — Renderer / Scene / Camera — // TRANSPARENT BACKGROUND ALPHA renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement); // SCENE scene = new THREE.Scene(); // CAMERA camera = new THREE.PerspectiveCamera(30, window.innerWidth/window.innerHeight, 0.1, 20); camera.position.set(0,1,0.9); controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0,1.1,0); controls.update(); // helpers // const gridHelper = new THREE.GridHelper( 10, 10 ); // scene.add( gridHelper ); // const axesHelper = new THREE.AxesHelper( 5 ); // scene.add( axesHelper ); // — Light — const dirLight = new THREE.DirectionalLight(0xffffff, 1); dirLight.position.set(3,15,-5); scene.add(dirLight); scene.add(new THREE.AmbientLight(0xffffff, 2.1)); // — Load VRM — const vrmData = await loadVRM(VRM_PATH, scene); vrm = vrmData.vrm; const loader = vrmData.loader; currentMixer = new THREE.AnimationMixer(vrm.scene); // Assuming `vrm` is already loaded PRINT ALL BONES // vrm.scene.traverse((object) => { // if (object.isBone) { // console.log(object.name); // } // }); // Init click detection initVRMClickDetector(renderer, camera, vrm, (region, partName) => { // This is where you can send it to LLM or server console.log(`📩 Sending region click to server: ${region}`); // Example: ws.send(JSON.stringify({ type: "region_click", region })); }); // — Managers — const audioMgr = new AudioManager(vrm); animationMgr = new AnimationManager(vrm, audioMgr, renderer, scene, camera, controls); // Unlock AudioContext on first user interaction function unlockAudio() { if (audioMgr.audioContext && audioMgr.audioContext.state === "suspended") { audioMgr.audioContext.resume().then(() => { console.log("🔓 AudioContext unlocked"); // also unlock