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>