骨骼动画—从基础建模到Threejs渲染
导语
骨骼动画作为一种常见的模型动画,在公司的很多 3d 场景都有涉及。本文会介绍用建模软件制作骨骼动画到 web 渲染整个流程,以及骨骼动画的原理。
01.
基本介绍|
骨骼动画是模型动画中的一种。在骨骼动画中,模型具有互相连接的“骨骼”组成的骨架结构,通过改变骨骼的朝向和位置来为模型生成动画。
简言之,就是用骨骼的变化带动相关顶点的变化。效果如下:
上面小人的走路动画就是身上细细的骨骼带动的
02.
用建模软件制作骨骼动画并在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();
渲染效果:
03. 纯用代码创建一个骨骼动画
为了了解 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);
/**
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();
04.
骨骼动画的原理
在 Threejs 中每个顶点受 4 个骨头控制,且各自有权重。所以,一个顶点的变化 = weight[0]*bone[0] + weight[1]*bone[1] + weight[2]*bone[2] +weight[3]*bone[3]
当我们向 x 轴正方向平移 Bone[0] 时,会带动整个物体移动,同时 matrix 由单位矩阵变为平移矩阵。
综上,骨骼其实就是一个4 * 4的齐次矩阵,该矩阵包含了平移,缩放和旋转变换。
附录
演示代码:https://github.com/Zack921/bone-animation
Blender教学:https://www.bilibili.com/video/BV1WW411v7Ji?t=571
Three.js骨骼动画:http://www.yanhuangxueyuan.com/doc/Three.js/SkinnedMesh.html