Three.js 3D模型动画实现原理归纳(模型自带动画)

效果展示:

机械分解动画

一、核心原理概述
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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值