今天讲threejs里比较复杂的一个功能,骨骼动画,骨骼动画也是应用场景很多的一种,因为无论是机器还是人都会存在骨骼动画,尤其是对设备或者人细节化的展示时候,以前的动画都是一个独立的模型进行移动或者旋转,但是物理世界中很多都是一个物体的移动是受制于另一个物体的,比如一个人的胳膊运动,大臂带动小臂,小臂上下摆动却不影响大臂,但是小臂的一头却固定在大臂上。
threejs中引入了bone来制作骨骼,不过要注意的是这个Bone并非是一个模型,而是指一个关节,这个不理解的话就很难理解骨骼动画了,比如一根圆柱形分为四段,大臂,小臂,手掌,手指,那么就需要5个节点,分别是肩膀处,胳膊肘,手腕,手指根部,手指尖部,然后移动或者旋转其中一个关节,会影响到边上的两段。
这里用threejs的代码实例,首先创建一个基础的3D场景:
initScene(){
scene = new THREE.Scene();
const axesHelper = new THREE.AxesHelper( 100 );
axesHelper.position.set(0,0,0)
scene.add( axesHelper );
},
initCamera(){
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
this.camera.position.set(200,400,200);
},
initLight(){
//添加两个平行光
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight1.position.set(300,300,300)
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight2.position.set(-300,300,300)
scene.add(directionalLight2);
},
initRenderer(){
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.container = document.getElementById("container")
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setClearColor('#AAAAAA', 1.0);
this.container.appendChild(this.renderer.domElement);
},
initControl(){
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.maxPolarAngle = Math.PI / 2.2; // // 最大角度
},
initAnimate() {
requestAnimationFrame(this.initAnimate);
this.renderer.render(scene, this.camera);
}
然后在模型中创建一个圆柱形,作为接下来作为骨骼动画掩饰的对象,不过这里的网格要设置为SkinnedMesh对象了,否则没办法制作骨骼动画:
// 圆柱体
const geometry = new THREE.CylinderGeometry(2, 2, 40, 8, 12)
const material = new THREE.MeshLambertMaterial({color:'#FCE6C9',metalness: 0.5,transparent: true, opacity:0.7});
// 蒙皮 - 皮肤
this.mesh = new THREE.SkinnedMesh(geometry, material)
this.mesh.position.set(0,20,0)
scene.add(this.mesh);
再创建5个Bone作为关节,为了方便观察加入骨骼辅助对象skeletonHelper,这个对象就类似threejs场景对象
/* 这段代码作用是现实骨骼的节点,方便查看骨骼运动过程中节点的位置*/
const group1 = new THREE.Group(); // 骨骼关节可以和普通网格模型一样作为其他模型子对象,添加到场景中
group1.add(b1);
const skeletonHelper = new THREE.SkeletonHelper(group1); // SkeletonHelper会可视化参数模型对象所包含的所有骨骼关节
group1.add(skeletonHelper);
scene.add(skeletonHelper);
这里已经能够看到一些绿色和蓝色组成的线条了,这个线条可以当做是骨头,颜色交汇处就是关节
然后重要的一部是给骨骼动画添加权重,因为模型本质上是无数个三角形组成的,每个三角形都有顶点,在骨骼动画进行拉伸旋转时,实际上是改变每个三角形的顶点位置,也就改变了三角形的形状和大小,设置权重就是关联骨骼动画时,每个关节运动对每个三角形顶点的影响程度,也就是上面提到的小臂的运动不会影响大臂,但是大臂会影响小臂,
const skeleton = new THREE.Skeleton([b1, b2, b3, b4, b5])
this.mesh.add(b1)
this.mesh.bind(skeleton)
// 添加权重 设置的就是蒙皮的权重, 顶点的蒙皮索引
const index = [] // 索引
const weight = [] // 权重
const arr = geometry.attributes.position.array;
//设置模型每个节点受骨骼的影响程度,如果是已有的模型文件会有自带的权重,这里是个圆柱体需要手动设置
for (let i = 0; i < arr.length; i += 3) {
const y = arr[i + 1] + 20
const weightValue = (y % 10) / 10
index.push(Math.floor(y / 10), Math.floor(y / 10) + 1, 0, 0)
weight.push(1 - weightValue, weightValue, 0, 0);
}
geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(index, 4));
geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(weight, 4));
最终设置完成还需要让整个骨骼动起来,可以在渲染里添加动画效果:
这里设置让圆柱来回摇摆,并且到一定程度后再设置负值进行反向摇摆,
if (this.mesh.skeleton.bones[0].rotation.x > 0.3 || this.mesh.skeleton.bones[0].rotation.x < -0.3 ) {
this.step = -this.step
}
//修改骨骼关节的位置角度来实现动画效果
for (let i = 0; i < this.mesh.skeleton.bones.length; i++) {
this.mesh.skeleton.bones[i].position.x += this.step;
this.mesh.skeleton.bones[i].rotation.x += this.step * Math.PI / 180;
}
最终效果如下:
简单的骨骼动画
这里不支持上传视频,我就只能上传个图片了,如果想看动态效果可以私我,我发给你视频
全部代码如下:
<template>
<div>
<div id="container"></div>
</div>
</template>
<script>
import * as THREE from 'three'
import {OrbitControls} from "three/addons/controls/OrbitControls";
let scene;
export default {
name: "bone-single",
data() {
return{
camera:null,
cameraCurve:null,
renderer:null,
container:null,
controls:null,
mesh:null,
step: 0.1,
}
},
methods:{
initScene(){
scene = new THREE.Scene();
const axesHelper = new THREE.AxesHelper( 100 );
axesHelper.position.set(0,0,0)
scene.add( axesHelper );
},
initCamera(){
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
this.camera.position.set(200,400,200);
},
initLight(){
//添加两个平行光
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight1.position.set(300,300,300)
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight2.position.set(-300,300,300)
scene.add(directionalLight2);
},
initBone(){
// 圆柱体
const geometry = new THREE.CylinderGeometry(2, 2, 40, 8, 12)
const material = new THREE.MeshLambertMaterial({color:'#FCE6C9',metalness: 0.5,transparent: true, opacity:0.7});
// 蒙皮 - 皮肤
this.mesh = new THREE.SkinnedMesh(geometry, material)
this.mesh.position.set(0,20,0)
scene.add(this.mesh);
// 首先,创建一个起点. 创建骨骼系统,
let b1 = new THREE.Bone(); //b1作为根
b1.position.set(0, -20, 0);
let b2 = new THREE.Bone();//基于bone1的-20位置多10
b1.add(b2)
b2.position.set(0, 10, 0);//基于bone2的-10位置多10
let b3 = new THREE.Bone();
b2.add(b3)
b3.position.set(0, 10, 0);//基于bone3的0位置多10
let b4 = new THREE.Bone();
b3.add(b4)
b4.position.set(0, 10, 0);//基于bone4的10位置多10
let b5 = new THREE.Bone();
b4.add(b5)
b5.position.set(0, 10, 0);//基于bone5的10位置多10,到达圆柱最顶部
/* 这段代码作用是现实骨骼的节点,方便查看骨骼运动过程中节点的位置*/
const group1 = new THREE.Group(); // 骨骼关节可以和普通网格模型一样作为其他模型子对象,添加到场景中
group1.add(b1);
const skeletonHelper = new THREE.SkeletonHelper(group1); // SkeletonHelper会可视化参数模型对象所包含的所有骨骼关节
group1.add(skeletonHelper);
scene.add(skeletonHelper);
// 创建骨架
const skeleton = new THREE.Skeleton([b1, b2, b3, b4, b5])
this.mesh.add(b1)
this.mesh.bind(skeleton)
// 添加权重 设置的就是蒙皮的权重, 顶点的蒙皮索引
const index = [] // 索引
const weight = [] // 权重
const arr = geometry.attributes.position.array;
//设置模型每个节点受骨骼的影响程度,如果是已有的模型文件会有自带的权重,这里是个圆柱体需要手动设置
for (let i = 0; i < arr.length; i += 3) {
const y = arr[i + 1] + 20
const weightValue = (y % 10) / 10
index.push(Math.floor(y / 10), Math.floor(y / 10) + 1, 0, 0)
weight.push(1 - weightValue, weightValue, 0, 0);
}
geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(index, 4));
geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(weight, 4));
},
initRenderer(){
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.container = document.getElementById("container")
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setClearColor('#AAAAAA', 1.0);
this.container.appendChild(this.renderer.domElement);
},
initControl(){
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.maxPolarAngle = Math.PI / 2.2; // // 最大角度
},
initAnimate() {
requestAnimationFrame(this.initAnimate);
this.renderer.render(scene, this.camera);
// 添加边界,如果弯曲超过一定的幅度,就设置负值向另一侧弯曲
if (this.mesh.skeleton.bones[0].rotation.x > 0.3 || this.mesh.skeleton.bones[0].rotation.x < -0.3 ) {
this.step = -this.step
}
//修改骨骼关节的位置角度来实现动画效果
for (let i = 0; i < this.mesh.skeleton.bones.length; i++) {
this.mesh.skeleton.bones[i].position.x += this.step;
this.mesh.skeleton.bones[i].rotation.x += this.step * Math.PI / 180;
}
},
initPage(){
this.initScene();
this.initCamera();
this.initLight();
this.initRenderer();
this.initControl();
this.initBone();
this.initAnimate();
}
},
mounted() {
this.initPage()
}
}
</script>
<style scoped>
#container{
position: absolute;
width:100%;
height:100%;
overflow: hidden;
}
</style>