骨骼动画作为一种常见的模型动画,在公司的很多 3d 场景都有涉及。
本文会介绍用建模软件制作骨骼动画到 web 渲染整个流程,以及骨骼动画的原理
一. 基本介绍
骨骼动画是模型动画中的一种。在骨骼动画中,模型具有互相连接的“骨骼”组成的骨架结构,通过改变骨骼的朝向和位置来为模型生成动画。
简言之,就是用骨骼的变化带动相关顶点的变化。效果如下:
上面小人的走路动画就是身上细细的骨骼带动的
二. 用建模软件制作骨骼动画并在web渲染
1. blender建模
市面上的建模软件很多,本文采用免费的blender。
所谓建模,就是建立模型。这一块通常由专业的建模师完成。本文的模型比较简单,用 6 个立方体拼接成一个方块人。
2. 制作骨骼
建模软件中骨骼也是一个基本元素,和添加立方体一样,我们也可以手动添加骨骼。如下图就生成了四个骨骼。
之前说到,骨骼动画是由骨骼产生的变化。所以骨骼的变换类型决定了动画的变换类型。而一般建模软件里的骨骼,都具有平移,缩放,旋转三种变换,正好和动画的基础变换对应上。
下图演示骨骼旋转:
此时骨骼的运动并没有带动腿部顶点的运动,因为我们还没有将骨骼与顶点绑定。
3. 绑定骨骼
通过选中骨骼和顶点,可以将骨骼和顶点绑定到一起。同时可以设置骨骼影响某个顶点的程度,即权重。下图红色部门表示被骨骼剧烈影响的部分,蓝色表示不会被骨骼影响的部分:
绑骨之后的效果:
4. 制作骨骼动画
动画就是在物体的基础变换(平移,缩放和旋转)上引入时间,从而产生动态效果。
通常我们只需要在建模软件上插入几个关键帧,它就会帮我们自动线性补全其余的动画帧。
比如我在第 1 帧放入一个向前踢腿的关键帧,在第 60 帧放入一个后踢的关键帧,Blender 就会自动帮我补全其他的动画帧。
第1帧:
第60帧:
自动补帧之后的动画:
5. 在threejs中渲染
将模型导出为 gltf 文件,并用 threejs 自带的 GLTFLoader 加载渲染。
import { OrbitControls } from './three/orbitcontrols.js';
import './three/GLTFLoader.js';
let domElement = document.getElementById("avatarDom");
let canvasW = domElement.clientWidth;
let canvasH = domElement.clientHeight;
let renderer = new THREE.WebGLRenderer();
domElement.appendChild(renderer.domElement);
renderer.setSize(canvasW, canvasH);
let scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, 500 / 500, 1, 1000);
camera.position.set(5, 5, 10);
camera.lookAt(scene.position);
camera.aspect = canvasW / canvasH;
camera.updateProjectionMatrix();
scene.add(camera);
scene.add(new THREE.AmbientLight(0x333333));
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
const axisHelper = new THREE.AxisHelper(100);
scene.add(axisHelper);
let clock = new THREE.Clock();
let mixer,animationClip,clipAction = null;
var loader = new THREE.GLTFLoader();
loader.load('human.gltf', function (result) {
scene.add(result.scene);
mixer = new THREE.AnimationMixer( result.scene );
animationClip = result.animations[0];
clipAction = mixer.clipAction( animationClip ).play();
animationClip = clipAction.getClip();
});
function render() {
var delta = clock.getDelta();
requestAnimationFrame(render);
renderer.render(scene, camera)
if (mixer && clipAction) {
mixer.update( delta );
}
}
render();
渲染效果:
三. 纯用代码创建一个骨骼动画
为了了解 Threejs 是如何处理骨骼的,我们尝试用程序直接生成一个骨骼模型。
Threejs 中用 SkinnedMesh 来管理骨骼模型,其实就是多了骨骼数据的网格。SkinnedMesh 比 Mesh 多了两个数据:
-
geometry.skinWeights:表示几何体顶点所关联骨骼的权重。skinWeights 属性是一个权重值数组,对应于几何体中顶点的顺序。例如,第一个 skinWeight 将对应于几何体的第一个顶点。由于每个顶点可以被 4 个骨骼 Bone 修改,因此用 Vector4 表示作用于该顶点的四个骨骼的权重 weights.
-
geometry.skinIndices:对应于几何体顶点关联的骨骼索引。每个顶点最多可以有4个与之关联的骨骼。因此,如果查看第一个顶点和第一个骨骼索引skinIndex,它将告诉您与该顶点关联的骨骼。例如,第一个顶点坐标( 10.05, 30.10, 12.12 ), 第一个骨骼索引 skin index值 是( 10, 2, 0, 0 ),第一个皮肤权重 skin weight 值是( 0.8, 0.2, 0, 0 ),表达的意思是骨骼 skeleton.bones[10] 对第一个顶点坐标影响权重 80%,骨骼 skeleton.bones[2] 对第一个顶点的影响权重 20%。接下来的两个骨骼权重值的权重为 0,因此对顶点坐标没有任何影响.
以下用圆台 + 三个骨头模拟腿部运动:
import { OrbitControls } from './three/orbitcontrols.js';
let domElement = document.getElementById("avatarDom");
let canvasW = domElement.clientWidth;
let canvasH = domElement.clientHeight;
let renderer = new THREE.WebGLRenderer();
domElement.appendChild(renderer.domElement);
renderer.setSize(canvasW, canvasH);
let scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, 500 / 500, 1, 2000);
camera.position.z = 400;
camera.lookAt(scene.position);
camera.aspect = canvasW / canvasH;
camera.updateProjectionMatrix();
scene.add(camera);
scene.background = new THREE.Color(0xa0a0a0);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 25, 0);
controls.update();
const axisHelper = new THREE.AxisHelper(100);
scene.add(axisHelper);
/**
* 创建骨骼网格模型SkinnedMesh
*/
// 创建一个圆台几何体,高度120,顶点坐标y分量范围[-60,60]
let geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
geometry.translate(0, 60, 0); //平移后,y分量范围[0,120]
console.log("name", geometry.vertices); //控制台查看顶点坐标
/**
* 设置几何体对象Geometry的蒙皮索引skinIndices、权重skinWeights属性
* 实现一个模拟腿部骨骼运动的效果
*/
//遍历几何体顶点,为每一个顶点设置蒙皮索引、权重属性
//根据y来分段,0~60一段、60~100一段、100~120一段
for (let i = 0; i < geometry.vertices.length; i++) {
let vertex = geometry.vertices[i]; //第i个顶点
if (vertex.y <= 60) {
// 设置每个顶点蒙皮索引属性 受根关节Bone1(0)影响
geometry.skinIndices.push(new THREE.Vector4(0, 0, 0, 0));
// 设置每个顶点蒙皮权重属性
// 影响该顶点关节Bone1对应权重是1-vertex.y/60
geometry.skinWeights.push(new THREE.Vector4(1 - vertex.y / 60, 0, 0, 0));
} else if (60 < vertex.y && vertex.y <= 60 + 40) {
// Vector4(1, 0, 0, 0)表示对应顶点受关节Bone2影响
geometry.skinIndices.push(new THREE.Vector4(1, 0, 0, 0));
// 影响该顶点关节Bone2对应权重是1-(vertex.y-60)/40
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 60) / 40, 0, 0, 0));
} else if (60 + 40 < vertex.y && vertex.y <= 60 + 40 + 20) {
// Vector4(2, 0, 0, 0)表示对应顶点受关节Bone3影响
geometry.skinIndices.push(new THREE.Vector4(2, 0, 0, 0));
// 影响该顶点关节Bone3对应权重是1-(vertex.y-100)/20
geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 100) / 20, 0, 0, 0));
}
}
// 材质对象
var material = new THREE.MeshPhongMaterial({
skinning: true, //允许蒙皮动画
wireframe: true,
});
// 创建骨骼网格模型
var SkinnedMesh = new THREE.SkinnedMesh(geometry, material);
SkinnedMesh.position.set(50, 120, 50); //设置网格模型位置
SkinnedMesh.rotateX(Math.PI); //旋转网格模型
scene.add(SkinnedMesh); //网格模型添加到场景中
/**
* 骨骼系统
*/
var Bone1 = new THREE.Bone(); //关节1,用来作为根关节
var Bone2 = new THREE.Bone(); //关节2
var Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系 多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
// 设置关节之间的相对位置
//根关节Bone1默认位置是(0,0,0)
Bone2.position.y = 60; //Bone2相对父对象Bone1位置
Bone3.position.y = 40; //Bone3相对父对象Bone2位置
// 所有Bone对象插入到Skeleton中,全部设置为.bones属性的元素
var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
console.log('Bone1: ', Bone1);
//骨骼关联网格模型
SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统
console.log('SkinnedMesh: ', SkinnedMesh);
/**
* 骨骼辅助显示
*/
var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
scene.add(skeletonHelper);
// 转动关节带动骨骼网格模型出现弯曲效果 好像腿弯曲一样
skeleton.bones[1].rotation.x = 0.5;
skeleton.bones[2].rotation.x = 0.5;
// 渲染函数
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
渲染效果:
四. 骨骼动画的原理
在 Threejs 中每个顶点受 4 个骨头控制,且各自有权重。所以,一个顶点的变化 = weight[0]*bone[0] + weight[1]*bone[1] + weight[2]*bone[2] + weight[3]*bone[3]
而图形学中,刻画物体变化方式通常都是矩阵,所以我们猜测这个 Bone 是一个矩阵。
打印一下这个 Bone 对象,发现确实有一个 matrix 属性表示一个4 * 4的矩阵。
当我们向 x 轴正方向平移 Bone[0] 时,会带动整个物体移动,同时 matrix 由单位矩阵变为平移矩阵。
ps: Threejs 其实是 webgl 的一层封装,而 webgl 存储矩阵采用的是列主序,所以此时的平移矩阵为
综上,骨骼其实就是一个4 * 4的齐次矩阵,该矩阵包含了平移,缩放和旋转变换。
五. 附录
- 演示代码:github.com/Zack921/bon…
- Blender教學:www.bilibili.com/video/BV1WW…
- Three.js骨骼动画:www.yanhuangxueyuan.com/doc/Three.j…