Threejs制作骨骼模型

本文详细介绍了如何在Three.js中创建和应用骨骼动画,包括创建骨骼节点、权重设置以及控制骨骼关节的动画效果,通过实例展示了如何为一个圆柱体模型添加骨骼并实现动态摇摆。
摘要由CSDN通过智能技术生成

        今天讲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>

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

baker_zhuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值