vue3 ts three 动画 骨骼动画 人物动画 模型动画

4 篇文章 0 订阅

在这里插入图片描述
three vue3 ts 练手 人物动画 骨骼动画

<template>
  <div>
    <div class="main" ref="box"></div>
  </div>
</template>

安装three插件 需要装ts版本

npm i -s -d @types/three

安装dat.gui调试插件

 npm i --save-dev @types/dat.gui

将里面的文件地址 换成你的文件地址
并且保存在 public下在这里插入图片描述
文件可以在https://www.mixamo.com/#/下载

<script lang="ts">
import {
  WebGLRenderer, //渲染器
  Scene, //场景
  AxesHelper, //坐标轴
  PerspectiveCamera, //相机
  Plane, //平面
  PlaneHelper, //平面辅助
  Vector3, //3维向量
  DirectionalLight, //平行光 太阳光
  DirectionalLightHelper, //平行光辅助
  Color, //颜色 three格式化
  PlaneGeometry, //矩形
  MeshStandardMaterial, //材质
  MeshPhongMaterial, //材质
  DoubleSide, //渲染两个面
  FrontSide, //只渲染正面
  BackSide, //只渲染背面
  Mesh, //组合
  AnimationMixer, //动画的播放器
  Clock, //时间插件
  SkeletonHelper, //骨骼辅助
} from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; //镜头控制
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader"; //加载文件loader
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; //加载文件loader

import { defineComponent, ref, onMounted, reactive } from "vue";
import * as dat from "dat.gui"; //调试插件
import Stats from "three/examples/jsm/libs/stats.module.js"; //性能监控插件
export default defineComponent({
  setup() {
    //提前创建好调试插件
    const gioData = new dat.GUI(); //测试插件
    const floorFaces: dat.GUI = gioData.addFolder("地板配置"); //测试插件新增组
    const Folder: dat.GUI = gioData.addFolder("平行光控制"); // //测试插件组
    const box = ref<HTMLElement | null>(null); //ref获取dom
    const scence: Scene = new Scene(); //场景
    const camera = new PerspectiveCamera( //新建相机
      45, //摄像机视锥体垂直视野角度
      window.innerWidth / window.innerHeight, //摄像机视锥体长宽比
      0.1, //摄像机视锥体近端面
      10000 //摄像机视锥体远端面
    );
    const clock = new Clock(); //时钟 动画用
    const stats = Stats(); //性能监控
    var rander: WebGLRenderer; //渲染器
    var xAxis: Object3D<Event> | AxesHelper; //坐标轴
    var directionalLight: DirectionalLight; //平行光
    var lightHelper: DirectionalLightHelper; //平行光辅助
    var mesh: AnimationMixer; //人物
    var plan: Mesh; //地板
    //定义接口 gui用
    interface guiType {
      [e: string]: any;
      sunColor: "#ffffff";
      floorFaces: "正面";
      routerFaces: 0;
      randerColor: "#eeeeee";
    }
    //定义接口 动作数组用
    interface actionsType {
      [e: string]: { action: any; play: boolean };
    }
    //定义插件控制的值
    const gui: guiType = reactive({
      sunColor: "#ffffff",
      floorFaces: "正面",
      routerFaces: 0,
      randerColor: "#eeeeee",
      action: "停止",
      speed: 0.5,
      xAxis: true,
      lightHelper: false,
      planeHelper: false,
      skeletonHelper: true,
    });

    // 渲染器方法
    function randerFun() {
      rander = new WebGLRenderer({ antialias: true }); //新建渲染器 antialias 否执行抗锯齿
      rander.setClearColor(new Color(gui.randerColor), 1); //更改渲染器颜色为默认
      rander.shadowMap.enabled = true; //开启阴影贴图 没用上
      //场景颜色
      gioData //测试插件
        .addColor(gui, "randerColor") //添加监听 gui里的randerColor
        .name("场景颜色") //名字为场景颜色
        .onChange((e) => {
          //更改以后的回调
          rander.setClearColor(new Color(e), 1); //更改场景颜色
        });
    }
    randerFun();

    //新建场景
    function scenceFun() {
      scence.name = "场景"; //场景名字
      camera.name = "相机"; //相机名字
      camera.position.set(-10, 40, 100); //相机位置
      new OrbitControls(camera, rander.domElement); //相机控制插件 实现拖拽渲染
    }
    scenceFun();

    //坐标轴     红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
    function xAxisFun() {
      xAxis = new AxesHelper(29); //长度29的坐标轴
      xAxis.name = "坐标轴"; //坐标轴名字
      scence.add(xAxis); //添加实例
      gioData
        .add(gui, "xAxis") //添加监听
        .name("显示坐标轴") //名字
        .onChange((e) => {
          //更改后的回调
          xAxis.visible = e; //隐藏或显示
        });
    }
    xAxisFun();

    //平行光
    function directionalLightFun() {
      directionalLight = new DirectionalLight(new Color(gui.sunColor)); //添加一个平行光 初始化颜色
      directionalLight.name = "太阳光"; //名字
      directionalLight.position.set(-10, 20, 20); //更改位置
      directionalLight.castShadow = true; //开启阴影
      directionalLight.shadow.camera.near = 0; //产生阴影的最近距离
      directionalLight.shadow.camera.far = 200; //产生阴影的最远距离
      directionalLight.shadow.camera.left = -50; //产生阴影距离位置的最左边位置
      directionalLight.shadow.camera.right = 50; //最右边
      directionalLight.shadow.camera.top = 50; //最上边
      directionalLight.shadow.camera.bottom = -50; //最下面
      scence.add(directionalLight); //添加实例
      Folder.addColor(gui, "sunColor") //添加方法 数据controls 根据传入数据不同渲染不同
        .name("颜色") //更改名字
        .onChange((e) => {
          directionalLight.color = new Color(e);
        }); //操作后的回调 更改颜色
    }
    directionalLightFun();

    //平行光辅助
    function lightHelperFun() {
      lightHelper = new DirectionalLightHelper(directionalLight, 10); //添加辅助线 辅助实例directionalLight 大小
      lightHelper.name = "平行光辅助"; //名字
      lightHelper.visible = gui.lightHelper; //是否显示
      scence.add(lightHelper); //添加实例
      Folder.add(gui, "lightHelper") //添加监听 在folder组内
        .name("平行光辅助") //名字
        .onChange((e) => {
          lightHelper.visible = e; //显示/隐藏
        });
    }
    lightHelperFun();

    //平面 这个玩意是用来计算交点的 所以... 未使用
    const plane = new Plane();
    plane.normal = new Vector3(0, 1, 0);
    plane.constant = 0; //距离中心点的位置

    //平面辅助
    const planeHelper = new PlaneHelper(plane, 555);
    planeHelper.name = "平面辅助";
    planeHelper.visible = false;
    scence.add(planeHelper);

    //地板 正方形
    function planFun() {
      // 地板
      // var planSty: any;
      // var planeGeometry: any;

      const planSty: any = new PlaneGeometry(100, 100, 10, 1); //添加矩形
      // 更改材质
      const planeGeometry = new MeshPhongMaterial({
        color: 0xcccccc,
        side: BackSide, //单双面
        fog: true, //是否受雾影响
      });
      // 合称
      plan = new Mesh(planSty, planeGeometry);
      plan.name = "地板"; //名字
      plan.rotation.x = Math.PI / 2; //旋转
      // plan.position.y = 0.1;
      plan.receiveShadow = true; //阴影
      floorFaces
        .add(gui, "floorFaces", ["正面", "背面", "两面"])
        .name("显示哪个面")
        .onChange((e) => {
          if (e == "正面") {
            planSty.side = FrontSide; //正面渲染
          } else if (e == "背面") {
            planSty.side = BackSide; //背面渲染
          } else if (e == "两面") {
            planSty.side = DoubleSide; //两面渲染
          }
        });
      floorFaces
        .add(gui, "routerFaces", -1, 1) //添加监听 最小值 最大值 拖动
        .name("旋转角度")
        .step(0.1)
        .onChange((e) => {
          plan.rotation.x = Math.PI * e;
        });
      floorFaces
        .add(gui, "planeHelper")
        .name("平面辅助")
        .onChange((e) => {
          planeHelper.visible = e;
        });
      scence.add(plan);
    }
    planFun();

    // 加载动画
    async function flash() {
      const fbx = new FBXLoader(); //加载器
      let actionOption = gioData.addFolder("动作控制"); //测试插件
      let actions: actionsType = reactive({}); //空对象
      //使用这个fbx里的人物
      await new Promise<void>((resolve, reject) => {
        //新建一个promise对象
        try {
          //捕获错误
          fbx.load("文件地址", (gltf) => {
            //加载器load以后加载fbx文件 gltf为文件
            gltf.scale.x = 0.1; //x大小 可以压缩
            gltf.scale.y = 0.1;
            gltf.scale.z = 0.1;
            let skeletonHelper = new SkeletonHelper(gltf); //骨骼辅助
            skeletonHelper.name = "骨骼辅助";
            scence.add(skeletonHelper); //添加实例
            actionOption //添加监听
              .add(gui, "skeletonHelper")
              .name("骨骼辅助")
              .onChange((e) => {
                skeletonHelper.visible = e;
              });
            gltf.name = "角色";
            scence.add(gltf); //添加实例
            mesh = new AnimationMixer(gltf); //新建动画混合器 绑定模型
            //动画中循环
            for (let i of gltf.animations) {
              //如果不是空动画
              if (i.duration && i.tracks.length) {
                actions[i.name] = { action: i, play: false }; //添加到动作对象中
              }
            }
            //对各个部位开启阴影
            //遍历gltf中所有对象
            gltf.traverse((e: any) => {
              if (e.isMesh) {
                //是骨骼则开启效果
                e.castShadow = true; //阴影投射
                e.receiveShadow = true; //接受阴影
              }
            });
            resolve();
          });
        } catch (error) {
          console.error("加载glb文件错误", error);
          reject(error);
        }
      });
      //使用glb中的动画
      await new Promise<void>((resolve, reject) => {
        try {
          new GLTFLoader().load("文件地址", (gltf) => {
            for (let i of gltf.animations) {
              //在自带的动画中循环
              if (i.duration && i.tracks.length) {
                //不为空
                let action = mesh.clipAction(i); //绑定动画到人物 混合器
                actions[i.name] = { action: i, play: false }; //添加到动作数组
                action.setEffectiveTimeScale(gui.speed); //设置时间比例
                action.setEffectiveWeight(0); //设置权重
                resolve();
              }
            }
          });
        } catch (error) {
          console.error("加载glb文件错误", error);
          reject(error);
        }
      });
      let accc: any; //用于替换当前动作的变量
      actionOption //监听
        .add(gui, "speed", 0.1, 1)
        .name("动作速度")
        .onChange((e) => {
          accc?.setEffectiveTimeScale(e); //调整当前动画的时间比例
        });
      // 动作控制
      actionOption
        .add(gui, "action", ["停止", ...Object.keys(actions)]) //所有的下标 中选择 ...ags
        .name("展示动作")
        .onChange((e) => {
          //如果是停止
          if (e == "停止") {
            accc.fadeOut(3); //3s内缓出
            accc = null; //当前动画为空
            return;
          }
          actions[e].play = true; //讲播放状态更改为true 播放状态暂时无用
          let ac = mesh.clipAction(actions[e].action); //绑定动画
          ac.enabled = true; //是否禁用
          ac.setEffectiveTimeScale(gui.speed); //设置时间比例
          ac.setEffectiveWeight(1); //设置权重
          ac.play(); //开启播放
          if (!accc) {
            //如果为空 但是新增了动画
            ac.fadeIn(3); //缓动
          } else {
            accc.crossFadeTo(ac, 3); //狗则正常切换过度
          }
          accc = ac; //保存当前动作
        });
    }
    flash();
    //生命周期 页面加载完
    onMounted(() => {
      rander.setSize(window.innerWidth, window.innerHeight); //更改渲染大小
      box.value?.append(rander.domElement); //box渲染完成则添加
      box.value?.appendChild(stats.dom); //box渲染完成则添加性能监控

      //aim定时执行 动画
      function aim() {
        stats.update(); //刷新性能监控
        mesh?.update(clock.getDelta()); //如果mesh存在 则刷新 传入时间
        rander.render(scence, camera); //更新试图
        requestAnimationFrame(aim); //定时器 到时间调用自己
      }
      aim();
    });
    //使画布动态大小
    window.onresize = () => {
      camera.aspect = window.innerWidth / window.innerHeight; //更改比例
      camera.updateProjectionMatrix(); //更新摄像机投影矩阵
      rander.setSize(window.innerWidth, window.innerHeight); //更改场景大小
    };
    return {
      box,
    };
  },
});
</script>
<style>
* {
  padding: 0;
  margin: 0;
}
</style>
<style lang="less" scoped>
.main {
  height: 100%;
  width: 100%;
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

瑕疵​

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

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

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

打赏作者

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

抵扣说明:

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

余额充值