SamSak09 commited on
Commit
c3b8c3c
Β·
verified Β·
1 Parent(s): c9443b0

Update index.html

Browse files
Files changed (1) hide show
  1. 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
- // --- 3. CREATE THE 3D MANNEQUIN (UPGRADED MESH) ---
 
 
 
 
 
86
  const maxPoints = 22;
87
  const boneConnections = [
88
- 0,1, 0,2, 0,3,
89
- 1,4, 2,5, 3,6,
90
- 4,7, 5,8, 6,9,
91
- 7,10, 8,11, 9,12,
92
- 9,13, 9,14, 12,15,
93
- 13,16, 14,17,
94
- 16,18, 17,19,
95
- 18,20, 19,21
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 = new THREE.MeshStandardMaterial({ color: 0xe0e0e0, roughness: 0.5, metalness: 0.1 });
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 sphere = new THREE.Mesh(sphereGeo, jointMaterial);
107
- sphere.castShadow = true;
108
- scene.add(sphere);
109
- jointMeshes.push(sphere);
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 boneMesh = new THREE.Mesh(capsuleGeo, boneMaterial);
120
- boneMesh.castShadow = true;
121
- scene.add(boneMesh);
122
- boneMeshes.push(boneMesh);
123
  }
124
-
125
- // --- 4. ANIMATION & STATE LOGIC ---
126
- let motionData = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  let currentFrame = 0;
128
- let isPlaying = false;
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 Spheres
143
  for (let i = 0; i < maxPoints; i++) {
144
- jointMeshes[i].position.set(frameData[i][0], frameData[i][1], frameData[i][2]);
 
 
 
 
145
  }
146
-
147
- // Update Cylinders
148
- let boneIndex = 0;
149
  for (let i = 0; i < boneConnections.length; i += 2) {
150
- const idxA = boneConnections[i];
151
- const idxB = boneConnections[i+1];
152
-
153
- pA.set(frameData[idxA][0], frameData[idxA][1], frameData[idxA][2]);
154
- pB.set(frameData[idxB][0], frameData[idxB][1], frameData[idxB][2]);
155
-
156
- const distance = pA.distanceTo(pB);
157
- const midPoint = pA.clone().lerp(pB, 0.5);
158
-
159
- const boneMesh = boneMeshes[boneIndex];
160
- boneMesh.position.copy(midPoint);
161
- boneMesh.scale.set(1, 1, distance);
162
- boneMesh.lookAt(pB);
163
-
164
- boneIndex++;
165
  }
166
-
167
- // SMOOTH CHASE CAMERA
168
- const rootJoint = frameData[0];
169
- // Lerp the camera target smoothly instead of snapping it
170
- cameraTarget.lerp(new THREE.Vector3(rootJoint[0], rootJoint[1], rootJoint[2]), 0.1);
 
 
 
 
 
171
  controls.target.copy(cameraTarget);
172
-
173
- // Keep camera at a fixed offset so we can see the character move through space
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');