当前位置: 首页 > news >正文

第七篇:动画基础:requestAnimationFrame循环

第七篇:动画基础:requestAnimationFrame循环

引言

动画是赋予3D场景生命力的关键。Three.js提供了多种动画实现方式,从基础的旋转动画到复杂的骨骼系统。本文将深入解析动画核心原理,并通过Vue3实现一个功能完备的动画编辑器,让你掌握3D动态效果的实现技巧。


在这里插入图片描述

1. 动画循环原理
1.1 requestAnimationFrame

浏览器原生动画API,以60FPS(约16.7ms/帧)为基准:

function animate() {// 更新动画逻辑updateScene();// 渲染场景renderer.render(scene, camera);// 循环调用requestAnimationFrame(animate);
}
animate();
1.2 动画时间控制
const clock = new THREE.Clock();
let deltaTime = 0;function animate() {deltaTime = clock.getDelta(); // 获取上一帧耗时// 基于时间的动画(帧率无关)cube.rotation.y += 0.5 * deltaTime;requestAnimationFrame(animate);
}
1.3 性能优化策略
graph TDA[动画循环] --> B{场景变化?}B -->|无变化| C[跳过渲染]B -->|有变化| D[执行渲染]

2. 基础动画实现
2.1 旋转动画
function animate() {cube.rotation.x += 0.01;cube.rotation.y += 0.02;renderer.render(scene, camera);requestAnimationFrame(animate);
}
2.2 位移动画
const startPos = new THREE.Vector3(0, 0, 0);
const endPos = new THREE.Vector3(5, 3, -2);
const duration = 3; // 秒function animateMove() {const elapsed = clock.getElapsedTime();const progress = Math.min(elapsed / duration, 1);// 线性插值cube.position.lerpVectors(startPos, endPos, progress);if (progress < 1) {requestAnimationFrame(animateMove);}
}
2.3 缩放动画(脉动效果)
function pulse() {const scale = 1 + Math.sin(clock.getElapsedTime() * 2) * 0.2;cube.scale.set(scale, scale, scale);requestAnimationFrame(pulse);
}

3. 高级动画库:GSAP
3.1 安装与配置
npm install gsap
3.2 基础动画示例
<script setup>
import gsap from 'gsap';onMounted(() => {// 位移动画gsap.to(cube.position, {x: 5,duration: 2,repeat: -1, // 无限重复yoyo: true, // 往返运动ease: "power2.inOut"});// 颜色渐变动画gsap.to(cube.material.color, {r: 1, g: 0, b: 0, // 红色duration: 3,onComplete: () => console.log("颜色变化完成")});
});
</script>
3.3 动画时间线
const timeline = gsap.timeline({repeat: -1,repeatDelay: 1
});timeline.to(cube.position, { x: 5, duration: 2 }).to(cube.scale, { x: 2, y: 2, z: 2, duration: 1 }, "-=0.5") // 与前一动画重叠0.5秒.to(cube.rotation, { y: Math.PI * 2, duration: 1.5 }, "+=0.5"); // 延迟0.5秒

4. 骨骼动画系统
4.1 加载GLTF骨骼模型
<script setup>
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';const loader = new GLTFLoader();
loader.load('model.gltf', (gltf) => {const model = gltf.scene;scene.add(model);// 获取动画混合器mixer = new THREE.AnimationMixer(model);// 播放所有动画gltf.animations.forEach((clip) => {mixer.clipAction(clip).play();});
});
</script>
4.2 动画混合器更新
const mixer = null;function animate() {const delta = clock.getDelta();if (mixer) mixer.update(delta);requestAnimationFrame(animate);
}
4.3 动画控制
// 获取动画动作
const walkAction = mixer.clipAction(gltf.animations[0]);
const runAction = mixer.clipAction(gltf.animations[1]);// 配置动画
walkAction.setEffectiveTimeScale(0.8) // 80%速度.setLoop(THREE.LoopRepeat).play();// 动画过渡
function transitionToRun() {gsap.to(walkAction, {timeScale: 0,duration: 0.3,onComplete: () => walkAction.stop()});runAction.setEffectiveTimeScale(1.2).fadeIn(0.3).play();
}

5. 变形动画(Morph Targets)
5.1 创建变形目标
const geometry = new THREE.BoxGeometry(1, 1, 1);// 添加变形目标(缩放效果)
geometry.morphAttributes.position = [new Float32Array([0,0,0,  0,0,0,  0,0,0,  // 原始顶点// 目标形状:Y方向拉伸0,2,0,  0,2,0,  0,2,0])
];const material = new THREE.MeshBasicMaterial({morphTargets: true,wireframe: true
});const cube = new THREE.Mesh(geometry, material);
5.2 控制变形权重
// 设置权重(0-1)
cube.morphTargetInfluences[0] = 0.5;// 动画变化
gsap.to(cube.morphTargetInfluences, {0: 1,duration: 2,yoyo: true,repeat: -1
});

6. Vue3实战:动画编辑器
6.1 项目结构
src/├── components/│    ├── AnimationEditor.vue  // 主编辑器│    ├── TimelineControl.vue  // 时间线│    ├── KeyframeEditor.vue   // 关键帧编辑│    └── AnimationPreview.vue // 3D预览└── App.vue
6.2 动画编辑器核心
<!-- AnimationEditor.vue -->
<script setup>
import { ref, reactive } from 'vue';
import gsap from 'gsap';const animations = ref([]);
const currentAnim = ref(null);
const timeline = ref(null);// 动画类型选项
const ANIM_TYPES = {POSITION: 'position',ROTATION: 'rotation',SCALE: 'scale',COLOR: 'color',MORPH: 'morph'
};// 添加新动画
function addAnimation(type, target) {const anim = {id: Date.now(),type,target,duration: 2,ease: 'power2.inOut',keyframes: [{ time: 0, value: getCurrentValue(target, type) },{ time: 2, value: getDefaultEndValue(type) }]};animations.value.push(anim);currentAnim.value = anim;
}// 应用动画到场景
function applyAnimation() {if (!currentAnim.value) return;timeline.value && timeline.value.kill();timeline.value = gsap.timeline();animations.value.forEach(anim => {const target = scene.getObjectById(anim.target.id);if (!target) return;const vars = {duration: anim.duration,ease: anim.ease};switch(anim.type) {case ANIM_TYPES.POSITION:timeline.value.to(target.position, {x: anim.keyframes[1].value.x,y: anim.keyframes[1].value.y,z: anim.keyframes[1].value.z,...vars}, 0);break;case ANIM_TYPES.COLOR:timeline.value.to(target.material.color, {r: anim.keyframes[1].value.r,g: anim.keyframes[1].value.g,b: anim.keyframes[1].value.b,...vars}, 0);break;// 其他类型处理...}});
}
</script><template><div class="animation-editor"><div class="animation-list"><button v-for="anim in animations" :class="{ active: anim === currentAnim }"@click="currentAnim = anim">{{ anim.type }} ({{ anim.duration }}s)</button><button @click="addAnimation(ANIM_TYPES.POSITION, selectedObject)">+ 添加动画</button></div><div v-if="currentAnim" class="animation-detail"><KeyframeEditor :animation="currentAnim" /><div class="controls"><button @click="applyAnimation">播放</button><button @click="timeline.pause()">暂停</button><button @click="timeline.seek(0)">重置</button></div></div></div>
</template>
6.3 关键帧编辑器
<!-- KeyframeEditor.vue -->
<script setup>
import { computed } from 'vue';const props = defineProps(['animation']);
const emit = defineEmits(['update']);// 当前选中的关键帧
const selectedKeyframe = ref(0);// 添加关键帧
function addKeyframe() {const newTime = prompt('输入时间(秒):');if (newTime && !isNaN(newTime)) {const newFrame = {time: parseFloat(newTime),value: getCurrentValue(props.animation.target, props.animation.type)};props.animation.keyframes.push(newFrame);sortKeyframes();}
}// 删除关键帧
function deleteKeyframe(index) {if (props.animation.keyframes.length > 2) {props.animation.keyframes.splice(index, 1);}
}// 按时间排序
function sortKeyframes() {props.animation.keyframes.sort((a, b) => a.time - b.time);props.animation.duration = Math.max(...props.animation.keyframes.map(k => k.time));
}// 更新关键帧值
function updateKeyframeValue(index, newValue) {props.animation.keyframes[index].value = { ...newValue };emit('update');
}
</script><template><div class="keyframe-editor"><div class="keyframe-list"><div v-for="(kf, i) in animation.keyframes" :class="{ selected: i === selectedKeyframe }"@click="selectedKeyframe = i"><span>时间: {{ kf.time.toFixed(2) }}s</span><button @click.stop="deleteKeyframe(i)">×</button></div><button @click="addKeyframe">+ 添加关键帧</button></div><div v-if="selectedKeyframe !== null" class="keyframe-detail"><ParamRange label="时间" v-model="animation.keyframes[selectedKeyframe].time":min="0" :max="animation.duration" :step="0.1"/><!-- 根据动画类型显示不同控件 --><template v-if="animation.type === 'position'"><VectorControl v-model="animation.keyframes[selectedKeyframe].value":labels="['X', 'Y', 'Z']"@change="updateKeyframeValue(selectedKeyframe, $event)"/></template><template v-else-if="animation.type === 'color'"><ColorPicker v-model="animation.keyframes[selectedKeyframe].value"@change="updateKeyframeValue(selectedKeyframe, $event)"/></template></div></div>
</template>
6.4 动画预览组件
<!-- AnimationPreview.vue -->
<script setup>
import { onMounted, ref } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';const canvasRef = ref(null);
const scene = new THREE.Scene();
const clock = new THREE.Clock();// 初始化场景
onMounted(() => {const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.value });const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);camera.position.z = 5;const controls = new OrbitControls(camera, renderer.domElement);// 添加测试物体const cube = new THREE.Mesh(new THREE.BoxGeometry(),new THREE.MeshStandardMaterial({ color: 0x00ff00 }));scene.add(cube);// 动画循环const animate = () => {requestAnimationFrame(animate);controls.update();renderer.render(scene, camera);};animate();
});// 接收外部动画指令
function playAnimation(timeline) {timeline.seek(0);timeline.play();
}
</script><template><div class="animation-preview"><canvas ref="canvasRef" width="600" height="400"></canvas></div>
</template>

7. 物理动画与碰撞
7.1 集成Cannon.js
npm install cannon
<script setup>
import * as CANNON from 'cannon';
import * as THREE from 'three';// 创建物理世界
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);// 创建物理物体
const sphereBody = new CANNON.Body({mass: 5,shape: new CANNON.Sphere(0.5)
});
sphereBody.position.set(0, 10, 0);
world.addBody(sphereBody);// 同步Three.js物体
const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(0.5),new THREE.MeshStandardMaterial()
);
scene.add(sphereMesh);// 物理更新循环
function physicsStep() {world.step(1/60); // 60FPS更新sphereMesh.position.copy(sphereBody.position);sphereMesh.quaternion.copy(sphereBody.quaternion);
}
7.2 碰撞检测
// 碰撞事件监听
sphereBody.addEventListener('collide', (e) => {console.log('碰撞强度:', e.contact.getImpactVelocityAlongNormal());
});// 射线检测
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();function checkIntersection(x, y) {mouse.set((x / window.innerWidth) * 2 - 1,-(y / window.innerHeight) * 2 + 1);raycaster.setFromCamera(mouse, camera);const intersects = raycaster.intersectObjects(scene.children);if (intersects.length > 0) {// 处理点击物体}
}

8. 动画状态机
8.1 状态机实现
class AnimationStateMachine {constructor() {this.states = {};this.currentState = null;}addState(name, onEnter, onUpdate, onExit) {this.states[name] = { onEnter, onUpdate, onExit };}setState(name) {if (this.currentState && this.states[this.currentState].onExit) {this.states[this.currentState].onExit();}this.currentState = name;if (this.states[name].onEnter) {this.states[name].onEnter();}}update(delta) {if (this.currentState && this.states[this.currentState].onUpdate) {this.states[this.currentState].onUpdate(delta);}}
}// 角色动画状态机
const characterAnim = new AnimationStateMachine();characterAnim.addState('idle', () => idleAction.play(),(delta) => mixer.update(delta),() => idleAction.stop()
);characterAnim.addState('walk', () => walkAction.play(),(delta) => mixer.update(delta),() => walkAction.stop()
);// 切换状态
characterAnim.setState('walk');
8.2 状态过渡
// 混合状态
characterAnim.addState('walk_to_run', () => {walkAction.fadeOut(0.2);runAction.fadeIn(0.2).play();
}, (delta) => {mixer.update(delta);
}, () => {runAction.stop();
});

9. 性能优化技巧
9.1 动画帧率控制
const targetFPS = 30;
const interval = 1000 / targetFPS;
let lastTime = 0;function animate(timestamp) {const deltaTime = timestamp - lastTime;if (deltaTime > interval) {// 执行更新updateAnimations(deltaTime / 1000);renderer.render(scene, camera);lastTime = timestamp;}requestAnimationFrame(animate);
}
9.2 不可见物体暂停
function animate() {// 检查物体是否在视锥内if (camera.frustumIntersectsObject(cube)) {cube.rotation.y += 0.01;}requestAnimationFrame(animate);
}
9.3 Web Workers 计算
// 主线程
const worker = new Worker('physics-worker.js');function updatePhysics() {worker.postMessage({type: 'step',dt: clock.getDelta(),bodies: getBodyStates()});worker.onmessage = (e) => {applyBodyStates(e.data);};
}// physics-worker.js
self.onmessage = (e) => {if (e.data.type === 'step') {// 执行物理计算world.step(e.data.dt);// 返回结果postMessage(getBodyStates());}
};

10. 常见问题解答

Q1:动画卡顿不流畅怎么办?

  1. 检查FPS(Stats.js)
  2. 减少场景复杂度(几何体/光源/阴影)
  3. 使用requestAnimationFrame时间戳
  4. 避免在动画循环中创建新对象

Q2:如何实现角色行走动画?

// 混合行走和空闲动画
const walkWeight = controls.speed / maxSpeed;
idleAction.setEffectiveWeight(1 - walkWeight);
walkAction.setEffectiveWeight(walkWeight);

Q3:动画结束后如何释放资源?

// GSAP动画
tl.eventCallback("onComplete", () => {cube.geometry.dispose();cube.material.dispose();scene.remove(cube);
});// 骨骼动画
mixer.addEventListener('finished', (e) => {mixer.uncacheRoot(e.action.getRoot());
});

11. 总结

通过本文,你已掌握:

  1. requestAnimationFrame动画循环原理
  2. GSAP高级动画库应用
  3. 骨骼动画与变形动画技术
  4. 物理动画与碰撞检测实现
  5. 动画状态机管理系统
  6. Vue3实现功能完备的动画编辑器
  7. 动画性能优化策略

核心原理:Three.js动画系统基于时间驱动,通过更新对象属性(位置/旋转/缩放)或变形权重,结合渲染循环实现动态效果。


下一篇预告

第八篇:交互入门:鼠标拾取物体
你将学习:

  • Raycaster射线检测原理
  • 鼠标点击/悬停交互实现
  • 3D物体拖拽与变换控制
  • 碰撞检测与物理交互
  • GUI控制面板集成
  • Vue3实现交互式3D展厅

准备好让你的3D场景响应用户操作了吗?让我们进入交互式3D开发的世界!

http://www.dtcms.com/a/321994.html

相关文章:

  • 解决多线程安全性问题的方法
  • 可编辑51页PPT | 某鞋服品牌集团数字化转型项目建议书
  • 相机Camera日志实例分析之十:相机Camx【萌拍调节AE/AF拍照】单帧流程日志详解
  • 基于MATLAB实现的毫米波大规模MIMO系统中继混合预编码设计
  • [windows]torchsig 1.1.0 gr-spectrumdetect模块安装
  • LeetCode 刷题【34. 在排序数组中查找元素的第一个和最后一个位置、35. 搜索插入位置】
  • 哈希法(Java)
  • 【数据结构】排序(sort) -- 计数排序
  • wstool和git submodule优劣势对比
  • select ... for update阻塞
  • 【感知机】感知机(perceptron)学习算法例题及详解
  • 任务管理器如何查看详细的命令行和路径?
  • 安科瑞能源管理系统在某新材料公司光储充一体化项目上的应用
  • 【C++空指针革命】nullptr:告别NULL的终极解决方案与底层实现剖析
  • 在超算中心,除了立式机柜(rack-mounted)还有哪些形式?
  • 官方Windows系统部署下载工具实践指南
  • 遥测自跟踪天线系统组成、特点、功能、工作流程
  • 【普通地质学】地质年代与地层系统
  • 无人机SN模块运行与功能详解
  • Vibe coding现在能用于生产吗?
  • 什么是0.8米分辨率卫星影像数据?
  • C++ WonderTrader源码分析之自旋锁实现
  • nflsoi 8.8 题解
  • CF每日3题(1400-1700)
  • 第9章 AI 安全、可解释性与伦理合规
  • 3天落地企业级应用,JNPF+AI重塑开发效率
  • sqli-labs靶场less46-less50
  • 传送带包裹漏检率↓78%!陌讯动态感知模型在物流分拣的实战优化
  • dMSA 滥用 (BadSuccessor) 导致权限升级:使用 ADAudit Plus 监控关键属性更改
  • Python使用LLM把自然语言翻译成SQL语句