Spaces:
Running on T4
Running on T4
| /** | |
| * Skeleton visualization with capsule bones and sphere joints | |
| * Based on HumanML3D skeleton structure (22 joints) | |
| */ | |
| class Skeleton3D { | |
| constructor(scene) { | |
| this.scene = scene; | |
| this.joints = []; | |
| this.bones = []; | |
| // Trail settings | |
| this.trailPoints = []; // Store root positions | |
| this.maxTrailPoints = 200; // Maximum trail length | |
| this.trailLine = null; | |
| this.trailGeometry = null; | |
| this.trailMaterial = null; | |
| // HumanML3D skeleton chains (from render_skeleton.py) | |
| this.chains = [ | |
| [0, 2, 5, 8, 11], // Chain 0: spine | |
| [0, 1, 4, 7, 10], // Chain 1: left leg | |
| [0, 3, 6, 9, 12, 15], // Chain 2: right leg + torso | |
| [9, 14, 17, 19, 21], // Chain 3: left arm | |
| [9, 13, 16, 18, 20], // Chain 4: right arm | |
| ]; | |
| // Convert chains to bone connections | |
| this.boneConnections = []; | |
| for (const chain of this.chains) { | |
| for (let i = 0; i < chain.length - 1; i++) { | |
| this.boneConnections.push([chain[i], chain[i + 1]]); | |
| } | |
| } | |
| // Bone and joint sizes - stick figure style (thin and small) | |
| this.boneRadius = 0.015; // Thin cylinder | |
| this.jointSize = 0.03; // Small sphere | |
| // Colors from render_skeleton.py | |
| this.chainColors = [ | |
| 0xFEB21A, // orange (chain 0 - spine) | |
| 0x00AAFF, // cyan (chain 1 - left leg) | |
| 0x134686, // aquamarine (chain 2 - right leg) | |
| 0xFFB600, // amber (chain 3 - left arm) | |
| 0x00D47E, // aquamarine (chain 4 - right arm) | |
| ]; | |
| // Joint color: teal (0, 128, 157) | |
| this.jointMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x00809D, // teal | |
| metalness: 0.2, | |
| roughness: 0.5, | |
| }); | |
| // Will create multiple bone materials for different chains | |
| this.boneMaterials = this.chainColors.map(color => | |
| new THREE.MeshStandardMaterial({ | |
| color: color, | |
| metalness: 0.2, | |
| roughness: 0.5, | |
| }) | |
| ); | |
| this.initSkeleton(); | |
| this.initTrail(); | |
| } | |
| initTrail() { | |
| // Create trail line with gradient opacity | |
| this.trailGeometry = new THREE.BufferGeometry(); | |
| // Initialize with empty arrays | |
| const positions = new Float32Array(this.maxTrailPoints * 3); | |
| const colors = new Float32Array(this.maxTrailPoints * 4); // RGBA | |
| this.trailGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| this.trailGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 4)); | |
| this.trailMaterial = new THREE.LineBasicMaterial({ | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 1.0, | |
| linewidth: 2, | |
| }); | |
| this.trailLine = new THREE.Line(this.trailGeometry, this.trailMaterial); | |
| this.trailLine.frustumCulled = false; | |
| this.scene.add(this.trailLine); | |
| } | |
| initSkeleton() { | |
| // Create 22 joint spheres - uniform small spheres | |
| const jointGeometry = new THREE.SphereGeometry(this.jointSize, 16, 16); | |
| for (let i = 0; i < 22; i++) { | |
| const joint = new THREE.Mesh(jointGeometry, this.jointMaterial); | |
| joint.castShadow = true; | |
| joint.receiveShadow = true; | |
| this.joints.push(joint); | |
| this.scene.add(joint); | |
| } | |
| // Create bone cylinders - different color for each chain | |
| let boneIndex = 0; | |
| for (let chainIdx = 0; chainIdx < this.chains.length; chainIdx++) { | |
| const chain = this.chains[chainIdx]; | |
| const material = this.boneMaterials[chainIdx]; | |
| for (let i = 0; i < chain.length - 1; i++) { | |
| const bone = this.createBone(material); | |
| this.bones.push(bone); | |
| this.scene.add(bone); | |
| boneIndex++; | |
| } | |
| } | |
| } | |
| createBone(material) { | |
| // Create a simple thin cylinder with specified material | |
| const geometry = new THREE.CylinderGeometry(this.boneRadius, this.boneRadius, 1, 8); | |
| const bone = new THREE.Mesh(geometry, material); | |
| bone.castShadow = true; | |
| bone.receiveShadow = true; | |
| return bone; | |
| } | |
| updatePose(jointPositions) { | |
| /** | |
| * Update skeleton with new joint positions | |
| * jointPositions: array of shape [22, 3] | |
| */ | |
| if (!jointPositions || jointPositions.length !== 22) { | |
| console.error('Invalid joint positions:', jointPositions); | |
| return; | |
| } | |
| // Update joint positions | |
| for (let i = 0; i < 22; i++) { | |
| const pos = jointPositions[i]; | |
| this.joints[i].position.set(pos[0], pos[1], pos[2]); | |
| } | |
| // Update bone positions and orientations | |
| for (let i = 0; i < this.boneConnections.length; i++) { | |
| const [startIdx, endIdx] = this.boneConnections[i]; | |
| const startPos = new THREE.Vector3( | |
| jointPositions[startIdx][0], | |
| jointPositions[startIdx][1], | |
| jointPositions[startIdx][2] | |
| ); | |
| const endPos = new THREE.Vector3( | |
| jointPositions[endIdx][0], | |
| jointPositions[endIdx][1], | |
| jointPositions[endIdx][2] | |
| ); | |
| this.updateBone(this.bones[i], startPos, endPos); | |
| } | |
| // Update trail with root position (joint 0) | |
| this.updateTrail(jointPositions[0]); | |
| } | |
| updateTrail(rootPos) { | |
| // Add new point to trail (project to ground y=0.01) | |
| const trailPoint = { | |
| x: rootPos[0], | |
| y: 0.01, // Slightly above ground to avoid z-fighting | |
| z: rootPos[2] | |
| }; | |
| // Only add if moved significantly (avoid duplicate points) | |
| if (this.trailPoints.length === 0) { | |
| this.trailPoints.push(trailPoint); | |
| } else { | |
| const lastPoint = this.trailPoints[this.trailPoints.length - 1]; | |
| const dist = Math.sqrt( | |
| Math.pow(trailPoint.x - lastPoint.x, 2) + | |
| Math.pow(trailPoint.z - lastPoint.z, 2) | |
| ); | |
| if (dist > 0.02) { // Minimum distance threshold | |
| this.trailPoints.push(trailPoint); | |
| } | |
| } | |
| // Limit trail length | |
| if (this.trailPoints.length > this.maxTrailPoints) { | |
| this.trailPoints.shift(); | |
| } | |
| // Update geometry | |
| const positions = this.trailGeometry.attributes.position.array; | |
| const colors = this.trailGeometry.attributes.color.array; | |
| const numPoints = this.trailPoints.length; | |
| for (let i = 0; i < this.maxTrailPoints; i++) { | |
| if (i < numPoints) { | |
| const point = this.trailPoints[i]; | |
| positions[i * 3] = point.x; | |
| positions[i * 3 + 1] = point.y; | |
| positions[i * 3 + 2] = point.z; | |
| // Gradient: older points (lower index) are more transparent | |
| const alpha = i / (numPoints - 1); // 0 (oldest) to 1 (newest) | |
| const opacity = Math.pow(alpha, 1.5) * 0.8; // Fade out older points | |
| // Use cyan color (matching joint color) | |
| colors[i * 4] = 0.0; // R | |
| colors[i * 4 + 1] = 0.67; // G (cyan) | |
| colors[i * 4 + 2] = 0.85; // B | |
| colors[i * 4 + 3] = opacity; // A | |
| } else { | |
| // Hide unused vertices | |
| positions[i * 3] = 0; | |
| positions[i * 3 + 1] = 0; | |
| positions[i * 3 + 2] = 0; | |
| colors[i * 4 + 3] = 0; | |
| } | |
| } | |
| this.trailGeometry.attributes.position.needsUpdate = true; | |
| this.trailGeometry.attributes.color.needsUpdate = true; | |
| this.trailGeometry.setDrawRange(0, numPoints); | |
| } | |
| clearTrail() { | |
| this.trailPoints = []; | |
| this.trailGeometry.setDrawRange(0, 0); | |
| } | |
| updateBone(bone, startPos, endPos) { | |
| /** | |
| * Update a bone's position, rotation, and scale to connect two joints | |
| */ | |
| const direction = new THREE.Vector3().subVectors(endPos, startPos); | |
| const length = direction.length(); | |
| if (length < 0.001) return; | |
| // Position bone at midpoint | |
| const midpoint = new THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5); | |
| bone.position.copy(midpoint); | |
| // Scale bone to match distance | |
| bone.scale.y = length; | |
| // Rotate bone to point from start to end | |
| bone.quaternion.setFromUnitVectors( | |
| new THREE.Vector3(0, 1, 0), | |
| direction.normalize() | |
| ); | |
| } | |
| setVisible(visible) { | |
| this.joints.forEach(joint => joint.visible = visible); | |
| this.bones.forEach(bone => bone.visible = visible); | |
| if (this.trailLine) this.trailLine.visible = visible; | |
| } | |
| dispose() { | |
| // Clean up resources | |
| this.joints.forEach(joint => { | |
| this.scene.remove(joint); | |
| joint.geometry.dispose(); | |
| }); | |
| this.bones.forEach(bone => { | |
| bone.children.forEach(child => { | |
| if (child.geometry) child.geometry.dispose(); | |
| }); | |
| this.scene.remove(bone); | |
| }); | |
| if (this.trailLine) { | |
| this.scene.remove(this.trailLine); | |
| this.trailGeometry.dispose(); | |
| this.trailMaterial.dispose(); | |
| } | |
| this.jointMaterial.dispose(); | |
| this.boneMaterials.forEach(mat => mat.dispose()); | |
| } | |
| } | |
| // Export for use in main.js | |
| window.Skeleton3D = Skeleton3D; | |