首页
/ three.js MorphAnimMesh 解析:为 Morph 目标模型提供极简动画播放接口

three.js MorphAnimMesh 解析:为 Morph 目标模型提供极简动画播放接口

2026-09-07 13:40:08作者:平淮齐Percy

MorphAnimMesh 是 three.js 中的一个特殊网格(Mesh)子类,它为基于 Morph 目标(Morph Targets)驱动的模型动画提供了"开箱即用"的极简播放接口:只需把动画剪辑放到对象或几何体上,通过 playAnimation() 指定名称即可开始播放,无需手动创建 AnimationMixer、AnimationAction。本篇以 three.js 仓库中的 MorphAnimMesh 源码 为主体,结合 AnimationMixerAnimationClipMD2Loader 等底层实现,讲解其构造、属性、方法、动画数据来源与播放原理,帮助读者快速掌握这类"单剪辑、无过渡"的动画网格用法,以及它与 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 类文档 以及父类文档 MeshObject3D。这也意味着 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 行)依次完成:

  1. 调用 super( geometry, material ) 走完 Mesh 的初始化;
  2. this.type 标记为 'MorphAnimMesh'(便于序列化与识别);
  3. 立即为自身创建一个 AnimationMixerthis.mixer = new AnimationMixer( this ),mixer 的根对象(root)就是网格自身,动画轨道将直接作用于网格的 morphTargetInfluences 等属性;
  4. 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 目标动画,即通过逐渐混合几何体的一组顶点位置来实现形变,因而需要满足两个前置条件:

  1. 几何体上存在 Morph 目标数据:即 geometry.morphAttributes.position(以及可选 normal),对应若干目标形状;
  2. 有描述"哪几帧怎么混合"的 AnimationClip,且存储在 geometry.animationsobject.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… 的目标名会被自动归类为 walkrun 两个剪辑,剪辑名取自目标名前缀。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 时建议同时查阅其类文档页 docs/pages/MorphAnimMesh.html(属性/方法签名以该页为准),并结合上述源码理解内部行为;若要验证各剪辑的实际名称与时长,可在运行期打印 mesh.geometry.animationsmesh.animations

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391