three.js MorphAnimMesh 解析:为 Morph 目标模型提供极简动画播放接口
MorphAnimMesh 是 three.js 中的一个特殊网格(Mesh)子类,它为基于 Morph 目标(Morph Targets)驱动的模型动画提供了"开箱即用"的极简播放接口:只需把动画剪辑放到对象或几何体上,通过 playAnimation() 指定名称即可开始播放,无需手动创建 AnimationMixer、AnimationAction。本篇以 three.js 仓库中的 MorphAnimMesh 源码 为主体,结合 AnimationMixer、AnimationClip、MD2Loader 等底层实现,讲解其构造、属性、方法、动画数据来源与播放原理,帮助读者快速掌握这类"单剪辑、无过渡"的动画网格用法,以及它与 MorphBlendMesh、手动 AnimationMixer 的取舍。
一、MorphAnimMesh 的定位与设计目标
从 examples/jsm/misc/MorphAnimMesh.js 源码头部注释可知:
A special type of an animated mesh with a simple interface for animation playback. It allows to playback just one animation without any transitions or fading between animation changes.
即它是一种带简单播放接口的特殊动画网格,只允许播放单个动画,并且在切换动画时不存在任何过渡(transitions)或淡入淡出(fading)。与之相对,官方的动画系统(AnimationMixer + AnimationAction)虽然功能完整(支持多个动作叠加、权重混合、Crossfade),但使用成本更高;MorphAnimMesh 通过把 Mixer 封装在对象内部,把动画循环所需的 update、切换所需的停止与启动全部收敛成两三个方法。
其完整继承链为:
EventDispatcher → Object3D → Mesh → MorphAnimMesh
对应的类图信息可参考文档页 MorphAnimMesh 类文档 以及父类文档 Mesh、Object3D。这也意味着 MorphAnimMesh 天然具备 Object3D 的 transform(位置/旋转/缩放)管理能力与 Mesh 的光照、材质渲染能力,你仍然可以把它当作一个普通网格放入场景。
二、导入方式
MorphAnimMesh 是 three.js 的 addon(附加模块),并不包含在核心 three 包中,使用前必须显式导入。其导出位于 examples/jsm/misc/MorphAnimMesh.js,统一入口同时注册于 examples/jsm/Addons.js:
import { MorphAnimMesh } from 'three/addons/misc/MorphAnimMesh.js';
如果你的项目使用 npm 包 three,导入路径同样为 three/addons/misc/MorphAnimMesh.js(addons 即 examples/jsm 目录的对外映射);使用 CDN/静态文件时,则需要把 examples/jsm/ 目录暴露到服务器并指向对应文件。
三、构造函数与属性
3.1 构造:new MorphAnimMesh( geometry, material )
new MorphAnimMesh( geometry, material )
| 参数 | 类型 | 说明 |
|---|---|---|
geometry |
BufferGeometry | 网格几何体,需携带 Morph 目标数据(见下文动画数据来源) |
material |
Material 或 Array<Material> | 网格材质,与普通 Mesh 一致,可为数组 |
构造过程中(源码第 23~44 行)依次完成:
- 调用
super( geometry, material )走完 Mesh 的初始化; - 将
this.type标记为'MorphAnimMesh'(便于序列化与识别); - 立即为自身创建一个 AnimationMixer:
this.mixer = new AnimationMixer( this ),mixer 的根对象(root)就是网格自身,动画轨道将直接作用于网格的morphTargetInfluences等属性; - 将
this.activeAction初始化为null。
3.2 .mixer : AnimationMixer
内部的动画混合器实例,类型为 AnimationMixer。所有播放、更新、方向控制最终都委托给它。由于 mixer 是公开属性,你可以在需要时直接访问它(例如读取 mixer.time、暂停等),但 MorphAnimMesh 封装的方法已覆盖最常见用法。
3.3 .activeAction : AnimationAction
当前正在播放的动画动作(AnimationAction)。默认值是 null,表示当前没有播放任何动画;调用 playAnimation() 成功后会持有对应动作的引用,再次切换动画时旧动作会被停止。
四、方法详解与播放流程
4.1 .playAnimation( label, fps ) —— 播放指定剪辑
这是 MorphAnimMesh 最核心的方法,参数含义如下:
label:要播放的动画剪辑(AnimationClip)的名称;fps:该动画剪辑的播放帧率,用于把剪辑总时长折算为指定的播放速度。
方法内部实现(源码第 71~94 行)可以拆解为以下四个步骤:
第一步:停掉旧动画
if ( this.activeAction ) {
this.activeAction.stop();
this.activeAction = null;
}
每次切换动画前先调用上一个动作的 stop() 并置空引用,保证"同一时刻只播放一个剪辑",这正是文档所述"没有过渡/淡入淡出"的直接体现——直接停、直接切。
第二步:按名称查找剪辑
const clip = AnimationClip.findByName( this, label );
AnimationClip.findByName() 是 AnimationClip 的静态方法。它接受"剪辑数组或带有 animations 数组的对象"作为第一个参数,查找逻辑为:
const o = objectOrClipArray;
clipArray = o.geometry && o.geometry.animations || o.animations;
也就是说,它会同时检索网格自身的 this.animations(即 Object3D 的 animations 数组)以及 this.geometry.animations(几何体上挂载的剪辑数组)——这正是类文档中"implementation assumes the animation clips are stored in Object3D#animations or the geometry"这句话的源码依据。
第三步:找不到则抛出异常
throw new Error( 'THREE.MorphAnimMesh: animations[' + label + '] undefined in .playAnimation()' );
如果按名称找不到剪辑,会立即抛出 THREE.MorphAnimMesh: animations[xxx] undefined in .playAnimation() 错误,便于排查名称拼写错误或几何体/对象上漏挂剪辑的问题。
第四步:从 mixer 取动作并设置播放速度
const action = this.mixer.clipAction( clip );
action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
this.activeAction = action.play();
mixer.clipAction( clip ) 负责为剪辑在 mixer 中创建/复用对应的 AnimationAction;关键的一行是 timeScale 折算:
timeScale = (轨道数 × 目标fps) / 剪辑原始时长(duration)
通过调整动作的 timeScale,把剪辑自带的时间轴缩放到调用方期望的 fps 节奏上,从而实现"以指定帧率播放"。最后调用 action.play() 启动动作并记录为 activeAction。
典型调用(假定网格对象上挂有名为 walk、时长为 1 秒的剪辑):
mesh.playAnimation( 'walk', 24 ); // 以 24fps 播放 walk 剪辑
4.2 .setDirectionForward() / .setDirectionBackward() —— 播放方向
setDirectionForward() { this.mixer.timeScale = 1.0; }
setDirectionBackward() { this.mixer.timeScale = -1.0; }
两个方法分别把内部 mixer 的 timeScale 设为 1.0(正向播放)与 -1.0(反向播放),配合正向/倒放效果,例如用倒放实现"回退动作"。注意:它们设置的 mixer.timeScale 是全局倍率,与 playAnimation 内部为单个 action 设置的 action.timeScale 是两个不同层级的缩放,二者相乘得到最终速率。
4.3 .updateAnimation( delta ) —— 驱动动画前进
updateAnimation( delta ) {
this.mixer.update( delta );
}
透传调用内部 mixer 的 update( delta ),其中 delta 为以秒为单位的帧间隔时间。必须在渲染循环中每帧调用,否则动画不会前进。典型循环如下:
const clock = new THREE.Clock();
function animate() {
const delta = clock.getDelta();
mesh.updateAnimation( delta ); // 每帧推进动画
renderer.render( scene, camera );
}
4.4 .copy() —— 拷贝时重建 mixer
除文档列出的公开方法外,源码还重写了 copy( source, recursive )(第 107~115 行):在完成 super.copy() 后新建一个针对当前实例的 AnimationMixer。这样做的原因是 mixer 持有着绑定到特定 root 对象(原网格)的内部状态,拷贝网格后必须重新绑定到新对象上,否则动画会错误地作用于旧网格。
五、动画剪辑从哪来:Morph 目标驱动的数据流
MorphAnimMesh 面向的是 Morph 目标动画,即通过逐渐混合几何体的一组顶点位置来实现形变,因而需要满足两个前置条件:
- 几何体上存在 Morph 目标数据:即
geometry.morphAttributes.position(以及可选normal),对应若干目标形状; - 有描述"哪几帧怎么混合"的 AnimationClip,且存储在
geometry.animations或object.animations中。
仓库里最典型的生成链路是 MD2Loader。MD2 是一种以帧序列存储动画的老式模型格式,MD2Loader 解析后会把每一帧拆为 Morph 目标(见 源码第 423~424 行:把帧数据写入 morphAttributes.position / morphAttributes.normal),并通过:
geometry.animations = AnimationClip.CreateClipsFromMorphTargetSequences( frames, 10, false );
(第 427 行)以 10fps 把帧序列自动聚类成多个命名剪辑,挂到 geometry.animations 上。
AnimationClip.CreateClipsFromMorphTargetSequences() 是 AnimationClip 的另一个静态工厂方法(第 252 行起):它会把形态目标名称按动画分组模式排序聚类——例如形如 Walk_001, Walk_002, Run_001, Run_002… 的目标名会被自动归类为 walk、run 两个剪辑,剪辑名取自目标名前缀。MD2 的命名约定(动作名_帧序号)正与此契合,因此 MD2Loader 加载的模型与 MorphAnimMesh 是经典搭配。
需要说明的是,MorphAnimMesh 本身并不依赖 MD2Loader——只要你用任意方式往几何体/对象的 animations 数组里塞进合法的 AnimationClip(例如用 AnimationClip.CreateClipsFromMorphTargetSequences 自行从命名好的 Morph 目标生成,或用 AnimationLoader 从 .json 动画文件载入),就能交给 playAnimation( label, fps ) 播放。
一个端到端的组合示例
import { MorphAnimMesh } from 'three/addons/misc/MorphAnimMesh.js';
import { MD2Loader } from 'three/addons/loaders/MD2Loader.js';
new MD2Loader().load( 'models/ogro.md2', ( geometry ) => {
const material = new THREE.MeshPhongMaterial(); // 或含 morphTargets 的材质配置
const mesh = new MorphAnimMesh( geometry, material );
scene.add( mesh );
mesh.playAnimation( 'walk', 24 ); // 播放 walk(若剪辑名与帧前缀一致)
} );
渲染循环内再以 mesh.updateAnimation( delta ) 每帧推进即可。具体名称以加载出的剪辑名为准,可通过 geometry.animations.map( c => c.name ) 打印确认。
六、底层原理:负 timeScale 与方向控制
你可能好奇"倒放"是如何实现的。查看 AnimationMixer.update 的源码:
update( deltaTime ) {
deltaTime *= this.timeScale; // 全局倍率先作用于时间增量
const time = this.time += deltaTime;
const timeDirection = Math.sign( deltaTime ); // 方向 = 时间增量的符号
...
action._update( time, deltaTime, timeDirection, accuIndex );
...
}
mixer 先让 deltaTime 乘上 mixer.timeScale。当 setDirectionBackward() 把 timeScale 设为 -1.0 后,正的帧间隔被翻转为负的增量,mixer 内部时钟倒退,timeDirection(由 Math.sign 得到)变为 -1,动画因此反向播放。同时 AnimationMixer 文档注释还提到(源码第 39~49 行):把 timeScale 设为 0 再恢复为 1,还可以实现"暂停/恢复该 mixer 控制的所有动作"的效果——这也是可以在 MorphAnimMesh 上借用的暂停手段(直接访问其公开的 mixer 属性即可)。
七、局限性与同类方案对比
| 需求 | 推荐方案 |
|---|---|
| 仅播放单个 Morph 剪辑,接口越简单越好 | MorphAnimMesh |
| 需要同时混合多个 Morph 动画 | 兄弟类 MorphBlendMesh,但它同样"无淡入淡出"(源码注释原文:without fading options) |
| 需要多动作叠加、权重混合、Crossfade、事件等完整能力 | 自行组合 AnimationMixer + AnimationClip + AnimationAction |
MorphAnimMesh 有意为之的简化也带来明确限制:
- 一次只能播放一个剪辑,切换即
stop()+play(),没有任何过渡动画; - 不提供权重、混合时长、循环策略等细粒度控制;
- 面向 Morph 目标动画,不适合骨骼(Skinning)动画场景。
因此它最适合"用简单 API 驱动基于 Morph 目标/帧序列的老式模型动画"(如 MD2 资源)或原型验证;当动画复杂度上升(多动作叠加、平滑过渡、按权重混合),应转向官方动画系统或 MorphBlendMesh。
八、源码与文档索引
- MorphAnimMesh 实现源码
- MorphAnimMesh 类文档页
- addons 统一导出(Addons.js)
- 底层协作组件:AnimationMixer、AnimationClip(findByName / CreateClipsFromMorphTargetSequences)、Object3D.animations
- 配套实践:MD2Loader 的 Morph 剪辑生成
- 进阶对比:MorphBlendMesh(多动画同时播放)
引用 MorphAnimMesh 时建议同时查阅其类文档页 docs/pages/MorphAnimMesh.html(属性/方法签名以该页为准),并结合上述源码理解内部行为;若要验证各剪辑的实际名称与时长,可在运行期打印 mesh.geometry.animations 或 mesh.animations。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00