效果展示:
机械分解动画
一、核心原理概述
Three.js 中的模型动画实现基于骨骼动画系统和关键帧动画原理,通过动画混合器(AnimationMixer)控制模型的骨骼、变形目标或其他属性随时间变化,从而产生连续的动画效果。
二、动画实现关键组件
- 动画混合器 (AnimationMixer)
动画混合器是 Three.js 动画系统的核心组件,负责管理和播放模型中的动画: - 每个包含动画的 3D 模型需要一个对应的 AnimationMixer 实例
混合器可以同时管理多个动画轨道,并支持动画的混合、过渡等高级功能
混合器在模型加载完成后创建:
mixer = new THREE.AnimationMixer(gltf.scene);
2. 动画片段 (AnimationClips)
动画片段是存储动画数据的容器,包含了模型各部分如何随时间变化的信息:
通常从 GLTF/GLB 等模型文件中加载获取
每个动画片段包含一个或多个动画轨道
加载的模型中提取动画片段:
clips = gltf.animations;
三、动画播放流程
的动画实现遵循以下核心流程:
1. 动画初始化阶段
模型加载:通过 GLTFLoader 加载包含动画数据的 GLB 模型文件
创建混合器:为加载的模型场景创建 AnimationMixer 实例
提取动画片段:从加载的模型数据中获取预定义的动画片段
2. 动画播放阶段
动画动作创建:通过混合器的 clipAction() 方法为特定动画片段创建动画动作
启动动画:调用动画动作的 play() 方法开始播放动画
持续更新:在渲染循环中通过 mixer.update(deltaTime) 更新动画状态
if(mixer && clips){
// 创建动画动作并播放
mixer.clipAction(clips[0]).play();
// 更新混合器状态,推进动画时间
mixer.update(0.005);
}
- 渲染循环集成
动画系统与渲染循环紧密集成,确保每一帧都能显示更新后的模型状态: - 使用 requestAnimationFrame 创建浏览器原生的动画循环 在每一帧中,先更新动画状态,然后渲染场景
固定的时间步长(如示例中的 0.005)确保动画播放速度稳定
四、动画数据流转
动画实现的数据流遵循以下路径:
- 动画数据存储:动画关键帧数据存储在 GLB 模型文件中 数据提取:加载器解析模型文件,提取动画片段数据
- 状态管理:动画混合器管理当前动画状态和时间 属性插值:混合器根据当前时间和关键帧数据,计算模型各部分的中间状态
- 应用变换:计算出的变换应用到模型的骨骼、顶点或其他属性上 渲染显示:渲染器将更新后的模型状态绘制到屏幕上
五、关键技术点
时间步进机制:
-
动画通过固定或可变的时间步长推进代码中使用固定时间步长 0.005,简化实现但可能在不同设备上速度略有差异 动画动作重用:
-
目前代码在每一帧都创建并播放动画动作,存在性能优化空间 建议在实际项目中将动画动作创建与播放分离,避免重复创建 帧率与性能:
-
通过 requestAnimationFrame 同步浏览器刷新频率,通常可达 60fps 动画复杂度和模型细节会影响整体性能
总结:Three.js 的动画系统通过动画混合器、动画片段和渲染循环的协同工作,实现了基于关键帧插值的 3D 模型动画效果,为 WebGL 应用提供了丰富的动态展示能力。
<template>
<div>
<div>动画效果</div>
<div class="VRbox"></div>
</div>
</template>
<script setup>
import * as THREE from "three";
import { onMounted, nextTick, ref } from "vue";
let scene, camera, renderer, controls, domWidth, domHeight;
// 动画混合器
let mixer ,clips;
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// 解码器
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
onMounted(() => {
nextTick(() => {
init();
})
})
function init() {
const container = document.querySelector(".VRbox");
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
domWidth = container.clientWidth;
domHeight = container.clientHeight;
// 相机
camera = new THREE.PerspectiveCamera(75, domWidth / domHeight, 0.1, 1000);
// 渲染器
renderer = new THREE.WebGLRenderer();
renderer.setSize(domWidth, domHeight);
// 设置像素比,提高显示质量
renderer.setPixelRatio(window.devicePixelRatio);
// 消除锯齿
renderer.antialias = true;
// 轨道控制器
controls = new OrbitControls(camera, renderer.domElement);
// 坐标
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 相机位置
camera.position.set(-1.18, 0.35, 0.55);
// 光源
const light = new THREE.AmbientLight(0xffffff);
scene.add(light);
light.intensity = 2; // 光照强度
light.position.set(10, 10, 10);
// 平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
scene.add(directionalLight);
directionalLight.position.set(1, 1, 1);
// 解码器
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/public/draco/');
// 加载器添加解码器
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
//添加模型
loader.load(
// 资源路径
'/public/model/huawei.glb',
// 回调
(gltf) => {
scene.add(gltf.scene);
// 模型加载完成后立即触发一次渲染
render();
console.log('模型加载完成');
// 创建动画混合器
mixer = new THREE.AnimationMixer(gltf.scene);
// 获取动画片段
clips = gltf.animations;
},
// 加载进度
(xhr) => {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
// 加载错误
(error) => {
console.log('An error happened');
}
);
// 渲染器添加到容器
container.appendChild(renderer.domElement);
// 渲染
render();
// 在resize事件中也添加像素比设置
window.addEventListener("resize", () => {
domWidth = container.clientWidth;
domHeight = container.clientHeight;
camera.aspect = domWidth / domHeight;
camera.updateProjectionMatrix();
renderer.setSize(domWidth, domHeight);
renderer.setPixelRatio(window.devicePixelRatio);
})
}
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
// console.log('相机位置===',camera.position);
// 轨道控制器更新
controls.update();
// 如果有动画混合器和动画片段
if(mixer && clips){
// 播放动画
mixer.clipAction(clips[0]).play();
// 更新混合器
mixer.update(0.005);
}
// 旋转
// scene.rotation.y += 0.01;
}
</script>
<style lang="less" scoped>
.VRbox {
width: 100vw;
height: 100%;
background: #00f;
}
</style>
&spm=1001.2101.3001.5002&articleId=152046719&d=1&t=3&u=aac6a0f81a454a869917003d2d48cc42)
5393

被折叠的 条评论
为什么被折叠?



