Spaces:
Sleeping
Sleeping
Update index.html
Browse files- index.html +251 -73
index.html
CHANGED
|
@@ -82,115 +82,293 @@
|
|
| 82 |
const gridHelper = new THREE.GridHelper(100, 100, 0xcccccc, 0xcccccc);
|
| 83 |
scene.add(gridHelper);
|
| 84 |
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
const maxPoints = 22;
|
| 87 |
const boneConnections = [
|
| 88 |
-
0,1,
|
| 89 |
-
1,4,
|
| 90 |
-
4,7,
|
| 91 |
-
7,10,
|
| 92 |
-
9,13,
|
| 93 |
-
13,16,
|
| 94 |
-
16,18,
|
| 95 |
-
18,20,
|
| 96 |
];
|
| 97 |
-
|
| 98 |
-
// Give the mesh a sleek, modern plastic look
|
| 99 |
const jointMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.2, metalness: 0.8 });
|
| 100 |
-
const boneMaterial
|
| 101 |
-
|
| 102 |
const jointMeshes = [];
|
| 103 |
-
// Make joints larger to look like robotic hinges
|
| 104 |
-
const sphereGeo = new THREE.SphereGeometry(0.08, 32, 32);
|
| 105 |
for (let i = 0; i < maxPoints; i++) {
|
| 106 |
-
const
|
| 107 |
-
|
| 108 |
-
scene.add(
|
| 109 |
-
jointMeshes.push(
|
| 110 |
}
|
| 111 |
-
|
| 112 |
const boneMeshes = [];
|
| 113 |
-
// UPGRADE: Use CapsuleGeometry instead of thin cylinders for realistic body volume
|
| 114 |
-
// Arguments: radius, length, cap segments, radial segments
|
| 115 |
const capsuleGeo = new THREE.CylinderGeometry(0.06, 0.06, 1, 16);
|
| 116 |
capsuleGeo.rotateX(Math.PI / 2);
|
| 117 |
-
|
| 118 |
for (let i = 0; i < boneConnections.length; i += 2) {
|
| 119 |
-
const
|
| 120 |
-
|
| 121 |
-
scene.add(
|
| 122 |
-
boneMeshes.push(
|
| 123 |
}
|
| 124 |
-
|
| 125 |
-
//
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
let currentFrame = 0;
|
| 128 |
-
let isPlaying
|
| 129 |
let lastFrameTime = 0;
|
| 130 |
-
|
| 131 |
const pA = new THREE.Vector3();
|
| 132 |
const pB = new THREE.Vector3();
|
| 133 |
const cameraTarget = new THREE.Vector3(0, 1, 0);
|
| 134 |
-
|
| 135 |
function animate(timestamp) {
|
| 136 |
requestAnimationFrame(animate);
|
| 137 |
-
|
| 138 |
if (isPlaying && motionData.length > 0) {
|
| 139 |
-
if (timestamp - lastFrameTime > 33) {
|
| 140 |
-
const frameData = motionData[currentFrame];
|
| 141 |
-
|
| 142 |
-
// Update
|
| 143 |
for (let i = 0; i < maxPoints; i++) {
|
| 144 |
-
jointMeshes[i].position.set(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
let boneIndex = 0;
|
| 149 |
for (let i = 0; i < boneConnections.length; i += 2) {
|
| 150 |
-
const
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
boneMesh.scale.set(1, 1, distance);
|
| 162 |
-
boneMesh.lookAt(pB);
|
| 163 |
-
|
| 164 |
-
boneIndex++;
|
| 165 |
}
|
| 166 |
-
|
| 167 |
-
//
|
| 168 |
-
|
| 169 |
-
//
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
controls.target.copy(cameraTarget);
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
if (currentFrame === 0) {
|
| 175 |
-
camera.position.set(rootJoint[0], rootJoint[1] + 1, rootJoint[2] + 4);
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
currentFrame = (currentFrame + 1) % motionData.length;
|
| 179 |
lastFrameTime = timestamp;
|
| 180 |
}
|
| 181 |
}
|
| 182 |
-
|
| 183 |
-
controls.update();
|
| 184 |
renderer.render(scene, camera);
|
| 185 |
}
|
| 186 |
animate(0);
|
| 187 |
-
|
| 188 |
window.addEventListener('resize', () => {
|
| 189 |
camera.aspect = window.innerWidth / window.innerHeight;
|
| 190 |
camera.updateProjectionMatrix();
|
| 191 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 192 |
});
|
| 193 |
-
|
|
|
|
| 194 |
// --- 5. WEBSOCKET PIPELINE ---
|
| 195 |
const socketStatus = document.getElementById('socket-status');
|
| 196 |
const statusText = document.getElementById('status');
|
|
|
|
| 82 |
const gridHelper = new THREE.GridHelper(100, 100, 0xcccccc, 0xcccccc);
|
| 83 |
scene.add(gridHelper);
|
| 84 |
|
| 85 |
+
|
| 86 |
+
// ============================================================
|
| 87 |
+
// 3. SKELETON (placed at x = +1.5, right side)
|
| 88 |
+
// ============================================================
|
| 89 |
+
const SKELETON_OFFSET_X = 1.5;
|
| 90 |
+
|
| 91 |
const maxPoints = 22;
|
| 92 |
const boneConnections = [
|
| 93 |
+
0,1, 0,2, 0,3,
|
| 94 |
+
1,4, 2,5, 3,6,
|
| 95 |
+
4,7, 5,8, 6,9,
|
| 96 |
+
7,10, 8,11, 9,12,
|
| 97 |
+
9,13, 9,14, 12,15,
|
| 98 |
+
13,16,14,17,
|
| 99 |
+
16,18,17,19,
|
| 100 |
+
18,20,19,21
|
| 101 |
];
|
| 102 |
+
|
|
|
|
| 103 |
const jointMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.2, metalness: 0.8 });
|
| 104 |
+
const boneMaterial = new THREE.MeshStandardMaterial({ color: 0xe0e0e0, roughness: 0.5, metalness: 0.1 });
|
| 105 |
+
|
| 106 |
const jointMeshes = [];
|
|
|
|
|
|
|
| 107 |
for (let i = 0; i < maxPoints; i++) {
|
| 108 |
+
const s = new THREE.Mesh(new THREE.SphereGeometry(0.08, 16, 16), jointMaterial);
|
| 109 |
+
s.castShadow = true;
|
| 110 |
+
scene.add(s);
|
| 111 |
+
jointMeshes.push(s);
|
| 112 |
}
|
| 113 |
+
|
| 114 |
const boneMeshes = [];
|
|
|
|
|
|
|
| 115 |
const capsuleGeo = new THREE.CylinderGeometry(0.06, 0.06, 1, 16);
|
| 116 |
capsuleGeo.rotateX(Math.PI / 2);
|
|
|
|
| 117 |
for (let i = 0; i < boneConnections.length; i += 2) {
|
| 118 |
+
const b = new THREE.Mesh(capsuleGeo, boneMaterial);
|
| 119 |
+
b.castShadow = true;
|
| 120 |
+
scene.add(b);
|
| 121 |
+
boneMeshes.push(b);
|
| 122 |
}
|
| 123 |
+
|
| 124 |
+
// ============================================================
|
| 125 |
+
// 4. BONE REMAPPING β GLB bone order β FloodDiffusion joint index
|
| 126 |
+
// ============================================================
|
| 127 |
+
// The SMPL Blender addon exports bones depth-first (left leg, right
|
| 128 |
+
// leg, spine, arms), but FloodDiffusion uses breadth-first SMPL-22.
|
| 129 |
+
// Confirmed from console: 25 bones (root + 22 SMPL + 2 hand bones).
|
| 130 |
+
//
|
| 131 |
+
// GLB idx : bone name β FD joint
|
| 132 |
+
// 0: root β FD 0 (use pelvis pos)
|
| 133 |
+
// 1: Pelvis β FD 0
|
| 134 |
+
// 2: L_Hip β FD 1
|
| 135 |
+
// 3: L_Knee β FD 4
|
| 136 |
+
// 4: L_Ankle β FD 7
|
| 137 |
+
// 5: L_Foot β FD 10
|
| 138 |
+
// 6: R_Hip β FD 2
|
| 139 |
+
// 7: R_Knee β FD 5
|
| 140 |
+
// 8: R_Ankle β FD 8
|
| 141 |
+
// 9: R_Foot β FD 11
|
| 142 |
+
// 10: Spine1 β FD 3
|
| 143 |
+
// 11: Spine2 β FD 6
|
| 144 |
+
// 12: Spine3 β FD 9
|
| 145 |
+
// 13: Neck β FD 12
|
| 146 |
+
// 14: Head β FD 15
|
| 147 |
+
// 15: L_Collar β FD 13
|
| 148 |
+
// 16: L_Shoulder β FD 16
|
| 149 |
+
// 17: L_Elbow β FD 18
|
| 150 |
+
// 18: L_Wrist β FD 20
|
| 151 |
+
// 19: L_Hand β FD 20 (no FD equivalent, reuse wrist)
|
| 152 |
+
// 20: R_Collar β FD 14
|
| 153 |
+
// 21: R_Shoulder β FD 17
|
| 154 |
+
// 22: R_Elbow β FD 19
|
| 155 |
+
// 23: R_Wrist β FD 21
|
| 156 |
+
// 24: R_Hand β FD 21 (no FD equivalent, reuse wrist)
|
| 157 |
+
const BONE_TO_FD = [0, 0, 1, 4, 7, 10, 2, 5, 8, 11, 3, 6, 9, 12, 15, 13, 16, 18, 20, 20, 14, 17, 19, 21, 21];
|
| 158 |
+
|
| 159 |
+
// GLB bone parent hierarchy (depth-first, 25 bones)
|
| 160 |
+
const GLB_PARENTS = [-1, 0, 1, 2, 3, 4, 1, 6, 7, 8, 1, 10, 11, 12, 13, 12, 15, 16, 17, 18, 12, 20, 21, 22, 23];
|
| 161 |
+
|
| 162 |
+
// ============================================================
|
| 163 |
+
// 5. SMPL RIGGED MESH (placed at x = -1.5, left side)
|
| 164 |
+
// ============================================================
|
| 165 |
+
const SMPL_OFFSET_X = -1.5;
|
| 166 |
+
|
| 167 |
+
let smplSkinnedMesh = null;
|
| 168 |
+
let smplSkeleton = null;
|
| 169 |
+
let restLocalDirs = [];
|
| 170 |
+
let smplLoaded = false;
|
| 171 |
+
|
| 172 |
+
// This matrix converts FloodDiffusion world-space joint positions
|
| 173 |
+
// into the SMPL scene's LOCAL space (fixes Blender Z-up vs Y-up mismatch)
|
| 174 |
+
let smplInvRotMatrix = new THREE.Matrix4();
|
| 175 |
+
|
| 176 |
+
// Reusable vectors
|
| 177 |
+
const _v1 = new THREE.Vector3();
|
| 178 |
+
const _v2 = new THREE.Vector3();
|
| 179 |
+
|
| 180 |
+
const gltfLoader = new THREE.GLTFLoader();
|
| 181 |
+
|
| 182 |
+
gltfLoader.load('smpl.glb', (gltf) => {
|
| 183 |
+
|
| 184 |
+
// ββ Find the SkinnedMesh ββββββββββββββββββββββββββββββββββ
|
| 185 |
+
gltf.scene.traverse((child) => {
|
| 186 |
+
if (child.isSkinnedMesh && !smplSkinnedMesh) {
|
| 187 |
+
smplSkinnedMesh = child;
|
| 188 |
+
smplSkeleton = child.skeleton;
|
| 189 |
+
child.material = new THREE.MeshStandardMaterial({
|
| 190 |
+
color: 0x88bbff, roughness: 0.5, metalness: 0.1, skinning: true
|
| 191 |
+
});
|
| 192 |
+
child.castShadow = true;
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
if (!smplSkinnedMesh) {
|
| 197 |
+
const el = document.getElementById('smplStatus'); if (el) el.textContent = "SMPL Mesh: β οΈ Not rigged";
|
| 198 |
+
gltf.scene.position.set(SMPL_OFFSET_X, 0, 0);
|
| 199 |
+
scene.add(gltf.scene);
|
| 200 |
+
return;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// ββ Place scene, then extract its rotation ββββββββββββββββ
|
| 204 |
+
// Blender exports with a -90Β° X rotation to convert Z-up β Y-up.
|
| 205 |
+
// We need the inverse of that rotation to bring FloodDiffusion's
|
| 206 |
+
// Y-up joint positions into the bone's local coordinate space.
|
| 207 |
+
gltf.scene.position.set(SMPL_OFFSET_X, 0, 0);
|
| 208 |
+
scene.add(gltf.scene);
|
| 209 |
+
gltf.scene.updateWorldMatrix(true, true);
|
| 210 |
+
|
| 211 |
+
// Extract only the rotation part (no translation) of the inverse
|
| 212 |
+
const fullInv = new THREE.Matrix4().copy(gltf.scene.matrixWorld).invert();
|
| 213 |
+
smplInvRotMatrix.extractRotation(fullInv);
|
| 214 |
+
|
| 215 |
+
// ββ Log bones to verify ordering βββββββββββββββββββββββββ
|
| 216 |
+
console.log("[SMPL] Bones:", smplSkeleton.bones.map((b, i) => `${i}: ${b.name}`).join(', '));
|
| 217 |
+
|
| 218 |
+
// ββ Capture rest-pose local bone directions βββββββββββββββ
|
| 219 |
+
// IMPORTANT: read AFTER the scene is added so matrixWorld is correct
|
| 220 |
+
restLocalDirs = smplSkeleton.bones.map(bone => bone.position.clone().normalize());
|
| 221 |
+
|
| 222 |
+
smplLoaded = true;
|
| 223 |
+
const el = document.getElementById('smplStatus');
|
| 224 |
+
if (el) el.textContent = `SMPL Mesh: β
Rigged (${smplSkeleton.bones.length} bones)`;
|
| 225 |
+
|
| 226 |
+
}, undefined, (err) => {
|
| 227 |
+
console.error("[SMPL] Failed to load smpl.glb:", err);
|
| 228 |
+
document.getElementById('smplStatus').textContent = "SMPL Mesh: β Load failed";
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
// ============================================================
|
| 232 |
+
// 6. APPLY POSE TO SMPL SKELETON FROM JOINT POSITIONS
|
| 233 |
+
// Forward-kinematics top-down pass.
|
| 234 |
+
// frameData shape: [22][3] β Y-up world-space joint positions.
|
| 235 |
+
//
|
| 236 |
+
// KEY FIX: Blender exports with a -90Β° X rotation baked into
|
| 237 |
+
// gltf.scene to convert its Z-up world to glTF Y-up.
|
| 238 |
+
// All bone local positions live in Blender's Z-up space BEFORE
|
| 239 |
+
// that scene rotation. So we must transform FloodDiffusion's
|
| 240 |
+
// Y-up positions through smplInvRotMatrix before comparing
|
| 241 |
+
// them to rest-pose bone directions.
|
| 242 |
+
// ============================================================
|
| 243 |
+
const _worldRots = new Array(25);
|
| 244 |
+
|
| 245 |
+
// Convert a FloodDiffusion Y-up world joint into SMPL scene local space
|
| 246 |
+
function toLocal(x, y, z) {
|
| 247 |
+
return new THREE.Vector3(x, y, z).applyMatrix4(smplInvRotMatrix);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
function applyPoseToSMPL(frameData) {
|
| 251 |
+
if (!smplLoaded || !smplSkeleton) return;
|
| 252 |
+
|
| 253 |
+
const bones = smplSkeleton.bones;
|
| 254 |
+
const n = bones.length; // 25
|
| 255 |
+
|
| 256 |
+
// Pre-convert all 22 FD joints to SMPL scene local space
|
| 257 |
+
const localJoints = frameData.map(j => toLocal(j[0], j[1], j[2]));
|
| 258 |
+
|
| 259 |
+
// ββ ROOT BONE (no FD equivalent, park at pelvis position) β
|
| 260 |
+
bones[0].position.copy(localJoints[0]);
|
| 261 |
+
bones[0].quaternion.identity();
|
| 262 |
+
_worldRots[0] = new THREE.Quaternion();
|
| 263 |
+
|
| 264 |
+
// ββ CHILD BONES ββββββββββββββββββββββββββββββββββββββββββ
|
| 265 |
+
for (let i = 1; i < n; i++) {
|
| 266 |
+
const p = GLB_PARENTS[i];
|
| 267 |
+
const Q_parent = _worldRots[p];
|
| 268 |
+
const restLocal = restLocalDirs[i];
|
| 269 |
+
|
| 270 |
+
if (restLocal.lengthSq() < 1e-6) {
|
| 271 |
+
bones[i].quaternion.identity();
|
| 272 |
+
_worldRots[i] = Q_parent.clone();
|
| 273 |
+
continue;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// Rest direction of this bone in SMPL-local world space
|
| 277 |
+
_v1.copy(restLocal).applyQuaternion(Q_parent);
|
| 278 |
+
|
| 279 |
+
// Target: FD joint of this bone minus FD joint of parent
|
| 280 |
+
const myFD = BONE_TO_FD[i];
|
| 281 |
+
const parFD = BONE_TO_FD[p];
|
| 282 |
+
_v2.copy(localJoints[myFD]).sub(localJoints[parFD]);
|
| 283 |
+
|
| 284 |
+
if (_v2.lengthSq() < 1e-6) {
|
| 285 |
+
// Zero-length (e.g. hand bone shares FD index with wrist)
|
| 286 |
+
bones[i].quaternion.identity();
|
| 287 |
+
_worldRots[i] = Q_parent.clone();
|
| 288 |
+
continue;
|
| 289 |
+
}
|
| 290 |
+
_v2.normalize();
|
| 291 |
+
|
| 292 |
+
const Q_needed = new THREE.Quaternion().setFromUnitVectors(_v1, _v2);
|
| 293 |
+
const localRot = Q_parent.clone().invert().multiply(Q_needed);
|
| 294 |
+
bones[i].quaternion.copy(localRot);
|
| 295 |
+
_worldRots[i] = Q_needed;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
smplSkeleton.update();
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
// ============================================================
|
| 302 |
+
// 7. ANIMATION LOOP
|
| 303 |
+
// ============================================================
|
| 304 |
+
let motionData = [];
|
| 305 |
let currentFrame = 0;
|
| 306 |
+
let isPlaying = false;
|
| 307 |
let lastFrameTime = 0;
|
| 308 |
+
|
| 309 |
const pA = new THREE.Vector3();
|
| 310 |
const pB = new THREE.Vector3();
|
| 311 |
const cameraTarget = new THREE.Vector3(0, 1, 0);
|
| 312 |
+
|
| 313 |
function animate(timestamp) {
|
| 314 |
requestAnimationFrame(animate);
|
| 315 |
+
|
| 316 |
if (isPlaying && motionData.length > 0) {
|
| 317 |
+
if (timestamp - lastFrameTime > 33) { // ~30 fps
|
| 318 |
+
const frameData = motionData[currentFrame]; // shape [22][3]
|
| 319 |
+
|
| 320 |
+
// ββ Update skeleton (offset to the right) ββββββββ
|
| 321 |
for (let i = 0; i < maxPoints; i++) {
|
| 322 |
+
jointMeshes[i].position.set(
|
| 323 |
+
frameData[i][0] + SKELETON_OFFSET_X,
|
| 324 |
+
frameData[i][1],
|
| 325 |
+
frameData[i][2]
|
| 326 |
+
);
|
| 327 |
}
|
| 328 |
+
|
| 329 |
+
let boneIdx = 0;
|
|
|
|
| 330 |
for (let i = 0; i < boneConnections.length; i += 2) {
|
| 331 |
+
const iA = boneConnections[i], iB = boneConnections[i+1];
|
| 332 |
+
pA.set(frameData[iA][0] + SKELETON_OFFSET_X, frameData[iA][1], frameData[iA][2]);
|
| 333 |
+
pB.set(frameData[iB][0] + SKELETON_OFFSET_X, frameData[iB][1], frameData[iB][2]);
|
| 334 |
+
|
| 335 |
+
const dist = pA.distanceTo(pB);
|
| 336 |
+
const mid = pA.clone().lerp(pB, 0.5);
|
| 337 |
+
|
| 338 |
+
boneMeshes[boneIdx].position.copy(mid);
|
| 339 |
+
boneMeshes[boneIdx].scale.set(1, 1, dist);
|
| 340 |
+
boneMeshes[boneIdx].lookAt(pB);
|
| 341 |
+
boneIdx++;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
}
|
| 343 |
+
|
| 344 |
+
// ββ Drive the SMPL skinned mesh βββββββββββββββββββ
|
| 345 |
+
// frameData joints are raw (skeleton origin = 0,0,0).
|
| 346 |
+
// The gltf.scene is positioned at SMPL_OFFSET_X so
|
| 347 |
+
// we pass raw frameData β Three.js handles the offset.
|
| 348 |
+
applyPoseToSMPL(frameData);
|
| 349 |
+
|
| 350 |
+
// ββ Camera follows root joint βββββββββββββββββββββ
|
| 351 |
+
const root = frameData[0];
|
| 352 |
+
cameraTarget.lerp(new THREE.Vector3(root[0], root[1], root[2]), 0.1);
|
| 353 |
controls.target.copy(cameraTarget);
|
| 354 |
+
|
| 355 |
+
currentFrame = (currentFrame + 1) % motionData.length;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
lastFrameTime = timestamp;
|
| 357 |
}
|
| 358 |
}
|
| 359 |
+
|
| 360 |
+
controls.update();
|
| 361 |
renderer.render(scene, camera);
|
| 362 |
}
|
| 363 |
animate(0);
|
| 364 |
+
|
| 365 |
window.addEventListener('resize', () => {
|
| 366 |
camera.aspect = window.innerWidth / window.innerHeight;
|
| 367 |
camera.updateProjectionMatrix();
|
| 368 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 369 |
});
|
| 370 |
+
|
| 371 |
+
|
| 372 |
// --- 5. WEBSOCKET PIPELINE ---
|
| 373 |
const socketStatus = document.getElementById('socket-status');
|
| 374 |
const statusText = document.getElementById('status');
|