| |
| |
| |
| |
|
|
| class Skeleton3D { |
| constructor(scene) { |
| this.scene = scene; |
| this.joints = []; |
| this.bones = []; |
| |
| |
| this.trailPoints = []; |
| this.maxTrailPoints = 200; |
| this.trailLine = null; |
| this.trailGeometry = null; |
| this.trailMaterial = null; |
| |
| |
| this.chains = [ |
| [0, 2, 5, 8, 11], |
| [0, 1, 4, 7, 10], |
| [0, 3, 6, 9, 12, 15], |
| [9, 14, 17, 19, 21], |
| [9, 13, 16, 18, 20], |
| ]; |
| |
| |
| 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]]); |
| } |
| } |
| |
| |
| this.boneRadius = 0.015; |
| this.jointSize = 0.03; |
| |
| |
| this.chainColors = [ |
| 0xFEB21A, |
| 0x00AAFF, |
| 0x134686, |
| 0xFFB600, |
| 0x00D47E, |
| ]; |
| |
| |
| this.jointMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x00809D, |
| metalness: 0.2, |
| roughness: 0.5, |
| }); |
| |
| |
| this.boneMaterials = this.chainColors.map(color => |
| new THREE.MeshStandardMaterial({ |
| color: color, |
| metalness: 0.2, |
| roughness: 0.5, |
| }) |
| ); |
| |
| this.initSkeleton(); |
| this.initTrail(); |
| } |
| |
| initTrail() { |
| |
| this.trailGeometry = new THREE.BufferGeometry(); |
| |
| |
| const positions = new Float32Array(this.maxTrailPoints * 3); |
| const colors = new Float32Array(this.maxTrailPoints * 4); |
| |
| 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() { |
| |
| 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); |
| } |
| |
| |
| 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) { |
| |
| 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) { |
| |
| |
| |
| |
| if (!jointPositions || jointPositions.length !== 22) { |
| console.error('Invalid joint positions:', jointPositions); |
| return; |
| } |
| |
| |
| for (let i = 0; i < 22; i++) { |
| const pos = jointPositions[i]; |
| this.joints[i].position.set(pos[0], pos[1], pos[2]); |
| } |
| |
| |
| 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); |
| } |
| |
| |
| this.updateTrail(jointPositions[0]); |
| } |
| |
| updateTrail(rootPos) { |
| |
| const trailPoint = { |
| x: rootPos[0], |
| y: 0.01, |
| z: rootPos[2] |
| }; |
| |
| |
| 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) { |
| this.trailPoints.push(trailPoint); |
| } |
| } |
| |
| |
| if (this.trailPoints.length > this.maxTrailPoints) { |
| this.trailPoints.shift(); |
| } |
| |
| |
| 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; |
| |
| |
| const alpha = i / (numPoints - 1); |
| const opacity = Math.pow(alpha, 1.5) * 0.8; |
| |
| |
| colors[i * 4] = 0.0; |
| colors[i * 4 + 1] = 0.67; |
| colors[i * 4 + 2] = 0.85; |
| colors[i * 4 + 3] = opacity; |
| } else { |
| |
| 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) { |
| |
| |
| |
| const direction = new THREE.Vector3().subVectors(endPos, startPos); |
| const length = direction.length(); |
| |
| if (length < 0.001) return; |
| |
| |
| const midpoint = new THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5); |
| bone.position.copy(midpoint); |
| |
| |
| bone.scale.y = length; |
| |
| |
| 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() { |
| |
| 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()); |
| } |
| } |
|
|
| |
| window.Skeleton3D = Skeleton3D; |
|
|
|
|