2. 虚拟形象骨骼绑定
任务说明
本案例为3D虚拟形象(以标准T-pose人体网格为例)添加骨骼系统并完成蒙皮,使模型能够通过骨骼旋转驱动顶点变形,实现基本的肢体动画(如手臂摆动)。采用Three.js的SkinnedMesh与Bone实现,核心目标:给定一个顶点网格和骨骼层级,正确建立骨骼-顶点权重绑定,并验证骨骼旋转能带动对应区域顶点平滑移动。
实现方案
使用Three.js内置的骨骼动画系统。构建一个简化的上肢骨骼链(肩→肘→腕),创建对应的圆柱体网格作为蒙皮对象,通过SkinnedMesh将骨骼与网格关联。蒙皮权重按顶点到骨骼线的距离自动计算(最近骨骼权重为1),实现基本的刚性蒙皮效果。最终通过旋转肩部骨骼,观察前臂与手部顶点跟随运动。
代码实现
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// --- 1. 场景基础 ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(5, 3, 8);
camera.lookAt(0, 1, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
new OrbitControls(camera, renderer.domElement);
// 光照
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(2, 5, 3);
scene.add(dirLight);
// --- 2. 创建骨骼层级(肩→肘→腕)---
const shoulder = new THREE.Bone();
const elbow = new THREE.Bone();
const wrist = new THREE.Bone();
shoulder.add(elbow);
elbow.add(wrist);
// 骨骼位置(局部坐标)
shoulder.position.set(0, 2.2, 0); // 肩部
elbow.position.set(0, -1.2, 0); // 肘部(相对肩)
wrist.position.set(0, -1.0, 0); // 腕部(相对肘)
// 骨骼辅助显示
const skeletonHelper = new THREE.SkeletonHelper(new THREE.Object3D());
// 稍后绑定到SkinnedMesh
// --- 3. 创建蒙皮网格(手臂圆柱体)---
// 分段数:径向8,高度20,保证顶点密度足够
const geometry = new THREE.CylinderGeometry(0.25, 0.25, 2.4, 8, 20);
geometry.translate(0, 1.2, 0); // 使圆柱底部位于原点,顶部在y=2.4
// 计算蒙皮权重:每个顶点归属最近的骨骼
const boneList = [shoulder, elbow, wrist];
const skinIndices = [];
const skinWeights = [];
const positionAttr = geometry.attributes.position;
for (let i = 0; i < positionAttr.count; i++) {
const v = new THREE.Vector3().fromBufferAttribute(positionAttr, i);
// 将顶点变换到骨骼局部空间比较距离(简化:用世界坐标近似)
// 骨骼世界坐标(此时尚未加入场景,手动计算)
const shoulderPos = new THREE.Vector3(0, 2.2, 0);
const elbowPos = new THREE.Vector3(0, 1.0, 0); // 2.2 - 1.2
const wristPos = new THREE.Vector3(0, 0.0, 0); // 1.0 - 1.0
const dists = [
v.distanceTo(shoulderPos),
v.distanceTo(elbowPos),
v.distanceTo(wristPos)
];
const minDist = Math.min(...dists);
const boneIndex = dists.indexOf(minDist);
// 刚性权重:最近骨骼权重1,其余0
skinIndices.push(boneIndex, 0, 0, 0);
skinWeights.push(1, 0, 0, 0);
}
geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(skinIndices, 4));
geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(skinWeights, 4));
// --- 4. 构建SkinnedMesh ---
const material = new THREE.MeshStandardMaterial({
color: 0x4a90d9,
skinning: true, // 启用蒙皮
side: THREE.DoubleSide
});
const mesh = new THREE.SkinnedMesh(geometry, material);
mesh.add(shoulder); // 骨骼加入mesh
mesh.bind(new THREE.Skeleton([shoulder, elbow, wrist]));
// 调整mesh位置使肩部在原点附近
mesh.position.y = -1.0;
scene.add(mesh);
// 添加骨骼可视化
const skeleton = mesh.skeleton;
const helper = new THREE.SkeletonHelper(mesh);
helper.material.linewidth = 2;
scene.add(helper);
// --- 5. 动画:旋转肩部骨骼实现手臂摆动 ---
let angle = 0;
function animate() {
requestAnimationFrame(animate);
// 肩部绕Z轴摆动(手臂前后摆动)
angle = Math.sin(Date.now() * 0.002) * 0.6;
shoulder.rotation.z = angle;
// 肘部轻微弯曲
elbow.rotation.z = Math.sin(Date.now() * 0.003) * 0.3;
renderer.render(scene, camera);
}
animate();
// 窗口自适应
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
落地与运行
依赖:Node.js环境,安装three(^0.160.0)。使用Vite或直接通过importmap方式在浏览器运行。
npm init -y
npm install three
# 将上述代码保存为 main.js,配合index.html引入
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>骨骼绑定演示</title>
<style>body{margin:0;overflow:hidden}</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module" src="./main.js"></script>
</body>
</html>
启动本地服务器(如npx vite),浏览器打开页面。
验证方法
判定标准:
- 页面显示一个蓝色圆柱体,肩部(顶部)、肘部(中部)、腕部(底部)有黄色骨骼线框标记。
- 圆柱体自动往复摆动:肩部骨骼绕Z轴旋转,圆柱体上半部分(上臂)随之整体摆动;肘部骨骼弯曲时,圆柱体下半部分(前臂)相对上臂产生弯曲。
- 蒙皮无撕裂:圆柱体表面在关节处平滑过渡,没有顶点飞散或网格断裂。
- 通过OrbitControls旋转视角,从不同方向观察骨骼与网格的贴合度,骨骼旋转时对应顶点跟随运动。
预期输出:圆柱体呈现周期性的摆臂动作,摆动幅度约±34°(肩部),肘部弯曲约±17°,网格保持连续完整。
关键说明
本案例采用刚性蒙皮(每顶点仅受最近骨骼影响),适用于演示骨骼绑定核心机制。实际生产级蒙皮通常使用平滑权重(多骨骼加权混合),可在skinWeights中分配多组非零权重实现。当前实现中骨骼位置与网格顶点坐标在同一坐标系下计算权重,确保绑定正确。通过SkeletonHelper可视化骨骼层级,便于直观验证骨骼与网格的联动关系。