☰
VRM模型配Mixamo动画:从Blender烘焙到three.js播放的完整流程
2026/10/16 4:30:12 网站建设 项目流程

做网页端3D虚拟形象展示的时候,最卡人的一环往往不是建模,也不是渲染,而是“怎么让角色动起来”。尤其是VRoid Studio导出的模型配Mixamo动画这套组合,几乎每个刚入门的开发者都会卡在同一道坎上:模型能显示,动画一播放就变形、穿模或者干脆不动。这篇文章就把我从零跑通的全过程拆开讲,包括为什么直接导入会出问题、中间为什么必须经过Blender、以及three.js端最省事的加载方式。

整个过程走下来,我发现核心问题只有一个:VRM和Mixamo用的是两套完全不同的骨骼系统,前者是标准化的人形骨架规范,后者是Adobe自家的绑定格式,直接混用必然出问题。搞懂这个“为什么”,后面所有步骤就顺理成章了。

1. 先说清楚:VRM和Mixamo的动画为什么不能直接用

VRM格式是面向虚拟形象发行的一套开源3D模型标准,由VRoid Studio等工具原生支持。它最大的特点是内置了标准化的人形骨架(Humanoid),包含头部、颈部、脊椎、双臂、双腿等结构,且每个骨骼都有固定的语义定义。比如J_Bip_C_Head就是头部骨骼,J_Bip_L_UpperArm就是左上臂,所有VRM模型无论长什么样,骨骼命名都一致。

Mixamo呢,它本身是人形角色动画库,用户上传模型或者选择官方角色后,系统会自动生成一套绑定了蒙皮的模型,并配套提供大量FBX/GLB格式的动画文件。Mixamo生成的骨骼也有固定命名,像mixamorig:Hips、mixamorig:Spine、mixamorig:LeftArm这类。从骨骼结构的抽象层级来看,VRM和Mixamo几乎是一一对应的——髋、脊椎、手臂、手指都有——但骨骼节点的名称完全不同,层级结构也有细微差异,比如VRM的脊椎是展开的三段结构,Mixamo则更紧凑一些。这就是问题所在:three.js的AnimationMixer在播放动画clip时,是通过骨骼名字来匹配对应节点的。你把Mixamo的动画直接加载到VRM模型的骨骼上,系统找不到mixamorig:Hips,动画自然就失效了。

所以,所有可行的方案本质都是做一次骨骼重定向(Retargeting),把Mixamo动画的骨骼映射到VRM模型的骨骼上。映射可以放在Blender里做,烘焙成全新的GLB文件后再让three.js播放,这个过程最可控、效果也最稳。

这一篇就按这个思路往下走:VRoid Studio导出模型 → Blender里完成重心对齐与骨骼映射 → 导出带动画的GLB → three.js加载播放。我会把每一步为什么这么操作、哪里容易翻车都写清楚。

2. 工具链准备:官方库版本选择与Blender作为“翻译官”的定位

动手之前先要把环境理清楚,版本踩错坑会多到怀疑人生。

2.1 three.js、VRM库与Blender的版本搭配

three.js这边,我推荐用r160以上但别追最新的版本。太新的three.js有时会出现GLTFLoader API变动,导致网上查到的旧代码直接暴毙。我自己用的组合是:

  • three@0.160.x
  • @pixiv/three-vrm@0.9.x
  • vite构建工具
  • typescript(可选,写JS也行)

VRM库有两种:@pixiv/three-vrm是官方推荐的运行时加载库,负责解析VRM的骨骼信息和表情、VRM Spring Bone等,配合three.js使用。Blender端需要安装VRM Addon(Blender的VRM格式导入导出插件),下载最新版安装包后,在Blender的Edit → Preferences → Add-ons里导入zip启用。

Blender版本这里要特别注意:插件作者目前对Blender 4.x的适配比较快,但3.6 LTS是最稳妥的版本。很多网上教程直接录屏用的还是2.93,年份一久操作菜单位置就对不上号,非常容易绕晕。建议用3.6 LTS,菜单布局稳定,教程资源也多。

2.2 为什么中间站必须是Blender

你可能想绕开Blender,直接用代码做骨骼映射。从技术上这是可行路径,比如把Mixamo动画的clip导入后,手动写一个骨骼名称映射表,再重新构造AnimationClip的KeyframeTrack。但这里有个致命难点:VRM和Mixamo虽然骨骼数量接近,但坐标系(Y-up vs Z-up)、T-Pose/A-Pose差异、蒙皮权重绑定这些都是代码里极难自动处理的。更不要说某些VRM模型还带裙子、头发这些动态附属物,用手写代码搞映射的时间足够用Blender做完几十个角色。

Blender在这里干的事情本质上是“翻译官”:它导入VRM模型后会重建绑定的骨架,然后你导入Mixamo的FBX动画,在Blender里用自动骨骼映射功能(Bone Retargeting)把两者对应起来,再把Mixamo动画烘焙到VRM骨骼上。烘焙后的结果导出为GLB文件,GLB自带完整的骨骼动画数据,three.js加载它就和加载普通GLB动画没有任何区别。这比任何代码层面的workaround都要干净利落。

3. 实操路线:从VRoid模型到Mixamo动画的完整迁移流程

这一步是整个流程的核心。我按顺序拆成四个阶段,每个阶段都有需要留神的细节。

3.1 第一阶段:VRoid Studio导出VRM并导入Blender

VRoid Studio导出时,模型会自动带一套标准化的VRM骨骼。导出设置里建议:

  • 身体质量选择“高质量”
  • 打开“使用透明材质”和“导出纹理”
  • 保存格式选.vrm
  • 版本建议选 1.0 而不是 0.x,Blender插件对1.0的支持已经很完善

然后在Blender里直接File → Import → VRM(导入VRM),模型会带着骨骼和材质一起出现,导入时选择“导入为普通模型,不转换为Mixamo格式”。此时如果你切到姿态模式(Pose Mode)选中骨骼,会发现所有骨骼的名字都是J_Bip_开头的VRM标准命名。

这里要立即检查一点:模型脚底是否贴在地平面上。VRM导出后模型的原点通常位于模型脚底的中心,但Blender导入后有时会因为缩放单位问题导致模型悬空或陷地。在Blender的物体模式下,选中模型,在右侧变换面板中把Transform → Location的Z轴归到0或模型脚底正好贴地。这个会影响最终GLB里模型的落地高度,后面three.js里还得手动调。

3.2 第二阶段:去Mixamo下载动画,注意统一T-Pose

Mixamo的操作流程是:登录Adobe账号 → 上传要做目标的VRM模型或直接选官方角色 → 在Animations标签页里挑动画 → 下载。

这里有一个大多数教程不会强调的细节:上传到Mixamo的角色模型,最好和你要绑定的骨骼姿态保持一致。更简单可靠的做法是,直接使用Mixamo官方自带的骨骼角色(比如那个经典的白模女性角色)下载动画,然后在Blender里做骨骼映射时,不需要处理模型蒙皮的差异,因为动画数据是骨骼动画(Skeleton Animation),跟蒙皮网格的顶点位置无关。也就是说,你只需要关注两套骨架的骨骼名称和结构对应,蒙皮会自动跟随骨架运动。

下载动画时注意三点:

  • 格式选FBX for Unity (.fbx),不要选带“(.bvh)”的版本。Blender对FBX的骨骼动画支持非常成熟,BVH格式反而可能在导入时丢失关键帧或产生偏移。
  • 帧速率选择:Mixamo默认是30fps,Blender场景帧速率建议也改成30fps,这样导入后时间轴完全对齐,不用再处理变速问题。
  • 下载前预览动画:挑一个动作力度合适、不需要额外剪辑的Clip。后续所有动画处理都在Blender里做,Mixamo端不需要做任何“二次加工”。

3.3 第三阶段:Blender里完成骨骼重定向与动画烘焙

这是最关键的环节,分成四步。

第一步:场景清理。把VRM模型和Mixamo的FBX动画同时导入一个Blender场景。导入FBX时保持默认设置即可,导入后你会看到场景里多了一个带Mixamo骨骼的模型。此时两个模型可能在同一个位置,为了操作方便,把Mixamo模型移动到一边,并且两者骨骼关系互不干扰。

第二步:选择VRM模型作为目标骨架。选中VRM模型,进入姿态模式(Pose Mode),确保它的骨架激活。然后去右侧Properties面板的“骨骼”选项卡里找到Rigify相关按钮?其实不是——VRM Addon自带一个专门的面板。在3D视口的右侧工具栏(N键弹出),可以在“VRM”标签下找到“Humanoid Normalized Pose”或类似选项,这是VRM的标准T-Pose工具。

第三步:自动骨骼映射。选中开始动画的Mixamo骨架(或Mixamo模型本身),去N面板的VRM工具里点击“Retarget”相关按钮,插件会自动匹配两套骨骼。如果匹配结果不是100%正确,可以手动在“骨骼映射”列表里逐个调整。常见的映射错误在于左右手指、肩膀和脚跟这几个位置,检查确认后点击“Apply”。

第四步:动画烘焙。选中Mixamo模型,进入物体的动画数据面板,按A选中全部关键帧,再复制这部分动作,粘贴到VRM模型骨架上。具体操作是:选择Mixamo骨架 →Ctrl+C复制动画关键帧 → 选中VRM骨架 →Ctrl+V。Blender会询问是否覆盖骨骼动画,选择“Replace”(替换)。之后在时间轴(Timeline或Dope Sheet)里拖动播放头,如果一切正常,VRM模型已经开始按Mixamo的动作运动了。

如果烘焙后角色动画有滑步或部分关节扭曲,多半是骨骼映射时左右手臂/腿部对应反了,需要在骨骼映射列表里交换左右。还有一点经验:Mixamo的FBX默认坐标系是Z-up,而Blender是Z-up,但Blender导入FBX时有自动坐标系转换,一般不会出大问题。

烘焙完成后,选中VRM模型,File → Export → GLB/GLTF,导出选项里:

  • 格式选glTF Binary (.glb)
  • 勾选Animations
  • 勾选+Y Up(如果不勾,three.js里模型会侧躺)
  • 纹理选择“打包(Pack)”成一个独立GLB文件最方便

这时你手上的GLB已经是一个自带Mixamo动画的VRM模型,骨骼名全变成VRM标准命名,动画数据也已经烘焙到这些骨骼上。

3.4 第四阶段:three.js加载播放Demo

这个文件到手后,three.js端的代码就非常简单了。甚至可以不用先以前VRM格式加载,直接当作普通GLB处理。

import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // 场景、相机、渲染器的常规配置 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); camera.position.set(0, 1.2, 3.5); camera.lookAt(0, 1, 0); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 灯光,记得至少要有一个DirectionalLight或者HemisphereLight const dirLight = new THREE.DirectionalLight(0xffffff, 1.5); dirLight.position.set(1, 3, 2); scene.add(dirLight); const loader = new GLTFLoader(); let model, mixer; loader.load( 'your-character-with-animation.glb', (gltf) => { model = gltf.scene; scene.add(model); // 模型略微抬高,避免穿入地面 model.position.y = 0.01; // 取第一个动画clip,创建AnimationMixer if (gltf.animations && gltf.animations.length > 0) { mixer = new THREE.AnimationMixer(model); const action = mixer.clipAction(gltf.animations[0]); action.play(); // 循环播放 action.loop = THREE.LoopRepeat; } }, undefined, (error) => console.error('加载失败', error) ); const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); if (mixer) mixer.update(delta); renderer.render(scene, camera); } animate();

这段代码放到Vite环境里基本就能直接跑。如果发现动画没生效,优先检查GLB文件里的animations数组是否存在。Blender导出GLB时如果选错了“仅动画”或没勾选相关开关,出来就是个没有动画数据的静态模型。

4. three.js端加载播放:绕过VRM动画系统的关键代码

上面那段是最简实现,实际项目里通常还需要处理几个细节。这一节单独把它们讲清楚。

4.1 三种加载方式的取舍:GLB直载、VRM插件加载再绑定动画

这里有个常见分歧:你的源文件如果是.vrm,其实是可以用@pixiv/three-vrm直接加载的:

import { VRMLoaderPlugin } from '@pixiv/three-vrm';

然后:

const loader = new GLTFLoader(); loader.register(parser => new VRMLoaderPlugin(parser)); loader.load('character.vrm', (gltf) => { const vrm = gltf.userData.vrm; scene.add(vrm.scene); });

但这样做有一个麻烦:VRM的动画播放机制并不是简单的mixer.update(),而是要走VRM.update(delta),并且表情动画和骨骼动画走的机制不同。如果你手上有的是烘焙好的GLB,直接用最普通的GLTFLoader加载是最省心的,因为它把动画数据固化在骨骼上了,three.js的AnimationMixer可以直接处理。只有在你需要动态换装、实时控制VRM表情或者要访问VRM Spring Bone这类高级特性时,才需要引入VRM插件。

这两个方案我都实测过,结论是:

路径动画稳定性代码复杂度插件依赖
方案一:Blender烘焙后GLB直载高,动画与骨骼一一对应低无
方案二:.vrm + VRM插件加载 + 手动骨骼映射中低,需要代码重定向高@pixiv/three-vrm
方案三:.vrm + VRM插件 + Blender烘焙混合无意义,绕路低无

所以默认方案就是第一个。下游项目如果需要表情控制,可以再额外加载一个单独的GLB表情动画,或者干脆用VRM的vrm.expressionManager实现。

4.2 动画时长、循环与播放控制的正确玩法

动画加载只是第一步,播放控制才是项目体验的关键。很多人会在这里踩坑。

首先,动画时长。Mixamo的动画Clip是固定时长的,比如“跑步”是1秒,“战斗待机”是2秒。three.js的AnimationAction默认会从头播到尾。如果你想要一个“跑完之后站住”的效果,就需要用action.loop = THREE.LoopOnce并设置action.clampWhenFinished = true:

const action = mixer.clipAction(animations[0]); action.loop = THREE.LoopOnce; action.clampWhenFinished = true; action.play();

这样播放完最后一帧就停住,不会瞬移回复位状态。

如果你要在多个动画间切换,比如“走路”切到“跑步”,需要先让旧动画淡出、新动画淡入,不然视觉上会很生硬:

const runAction = mixer.clipAction(runClip); const walkAction = mixer.clipAction(walkClip); // 切换函数 function switchToRun(instant = false) { runAction.reset(); if (instant) { runAction.setEffectiveTimeScale(1); runAction.setEffectiveWeight(1); } runAction.fadeIn(0.3).play(); walkAction.fadeOut(0.3); }

另外还有一个细节:如果同一个GLB文件里包含了多个动画Clip(比如你导出了两个动画合并成一个文件),gltf.animations会是一个数组,需要用mixer.clipAction(animations[i])分别取用,而不是默认只播放第一段。

4.3 手动偏移:解决角色悬空和半截入地

Blender里导出GLB时,模型的脚底是否贴地是你需要额外查的。我在3.3里提过在Blender里把Z轴归零,但several时候会因为Blender的坐标系转换导致导出后模型悬空。常见的修正办法是:

// 加载后根据BoundingBox计算脚底高度并修正 gltf.scene.traverse((child) => { if (child.isSkinnedMesh) { child.geometry.computeBoundingBox(); const box = child.geometry.boundingBox; // box.min.y 是模型最低点在世界坐标下的值,注意SkinnedMesh的几何体矩形是绑定姿势下的 } });

实际上SkinnedMesh的几何(Geometry)在蒙皮变形后会发生变化,不能直接用boundingBox来算脚底,因为它是绑定姿势下的静态框。更好的办法是直接给模型一个固定的position.y,比如0到0.02之间,然后根据预览效果微调。如果遇到的是骨骼导致的位置偏差(整个模型歪斜),那就是3.3里BG坐标轴未对齐,得回到Blender重新导出。

5. 实测中必踩的五个大坑与对应解法

整个链路我从0开始跑到能流畅播放,前后磨了两天,踩了五个比较大的坑。这里如实记录,能帮你省下至少大半天排错时间。

5.1 坑一:Blender插件版本与Blender版本不匹配导致VRM导入报错

第一次装插件时,我在Blender 4.1上装了最新版的VRM Addon,结果导入VRM时报“Python: RuntimeError: Error: ScriptError”之类的问题。排查后才发现是插件底层调用的库与Blender版本不兼容。

解决方法是锁定Blender 3.6 LTS,并使用与该版本兼容的VRM Addon。版本锁定后,VB和导出一路顺畅。

经验:安装插件前先去该插件GitHub页面的Release列表看历史版本记录,确认标注的“Blender 3.6 compatible”字样再装。不要想当然用最新版。

5.2 坑二:Mixamo动画导入Blender后动作错位、模型“劈叉”

这个坑非常典型。Mixamo下载的FBX导入Blender后,有时模型会处于T-Pose,但动画骨骼和模型骨骼对不上,导致角色动作幅度特别大或者“劈叉”。原因出在Blender导入FBX时的“Automatic Bone Orientation”选项。把它关闭,改成“Manual”,然后手动按X轴旋转骨骼,就能对齐。

具体操作:File → Import → FBX,展开左侧导入面板里的“Bone Orientation”部分,勾选“Manual”模式,把旋转的X轴改为90度(或-90度),根据Mixamo的骨位情况微调。这个步骤我试了两三次才调对方向。

5.3 坑三:烘焙动画后导出的GLB没有动画数据

这个最隐蔽。Blender导出GLB时,窗口左下角的选项列表里有一个“Animations”复选框,默认是“已勾选”?不,我遇到的情况是:默认选项是“只导出Active Action(Euler)”或类似受限模式。必须把“导出动画”相关的模式改成“All Actions”或“Active Action + Slots”,并确保动画面板里没有把关键帧藏在未激活的Action里。

另外,如果你烘焙时没按A全选所有骨骼关键帧,只复制了部分骨骼的关键帧,GLB导出的动画就会缺胳膊少腿。确认方法是:在Blender的Dope Sheet(动作编辑器)窗口里,切换到“Action Editor”看有没有黄色的关键帧标记散布在时间轴上。

5.4 坑四:three.js播放时动画闪烁或抖动量巨大

这种情况往往是GLB文件里同时包含了蒙皮权重(Skin Weight)动画数据和骨骼动画数据,两者冲突。Blender导出时默认会同时包含“骨骼动画”和“蒙皮变形”信息,如果Mixamo动画事先又套过一次“蒙皮权重”变形,就会出现叠加抖动。

解决方案:Blender里选中VRM模型的蒙皮网格,在右侧修改器列表里确认“Armature”修改器的“顶点组”权重是否完整。一般来说只要不额外给模型添加Morph动画,就不会出问题。

5.5 坑五:Vite环境里GLTFLoader跨域加载失败

本地开发时,loader.load加载的GLB路径如果是放在public目录下,Vite开发服务器默认就能正常访问。但如果开了strictPort或者用了一些代理配置,可能会遇上CORS错误。这个和three.js本身无关,是开发环境的坑。

解决办法有两种:一是直接用Vite的/asset路径,二是把GLB文件放到public下,然后用相对路径引用。不要用文件协议(file://)直接打开HTML测试,three.js会拒绝加载本地资源。

6. 进阶方向:多动画切换、表情同步与性能优化

基础播放跑通后,再往前的坑基本都在“体验”层面。

6.1 多动画文件合并与Blend Tree

实际项目里不可能只有一个动画。更常见的做法是:在Blender里把多个动画Action放到同一个GLB文件里,然后在three.js端通过mixer.clipAction(animations[i])来切换。如果你希望过渡自然,可以再做一层动画状态机(Blend Tree)。three.js官方示例里有webgl_animation_skinning_blending可以参考,原理就是同时播放两个Action,各自分配权重:

walkAction.setEffectiveWeight(0.5); runAction.setEffectiveWeight(0.5);

这个权重可以随时间插值,实现从走到跑的无缝过渡。实际工程里,我会把角色状态变量管起来,定时计算权重。

6.2 VRM表情动画的联动

纯骨骼动画能搞定身体动作,但VRM模型的面部表情还是要走VRM的Expression Manager。你有两种办法:

一是把表情动画数据也烘焙进GLB,使用MorphTargetAnimation,在three.js里直接播放,优点是省事,缺点是可操控性差。

二是保留VRM Source模型,用@pixiv/three-vrm加载,然后通过vrm.expressionManager.setValue('happy', 1)这种API在代码里控制表情权重。这个方式适合做聊天互动场景,表情和声音能精确联动。

6.3 性能优化:合并Mesh与限制Draw Call

VRoid Studio导出的模型默认由多个Material分区(头发、皮肤、衣服等),单个Mesh数量往往在10到20个之间。直接把这些Mesh送上渲染管线,Draw Call会比较高。建议在Blender里把相同材质的Mesh合并(Ctrl+J),这样能大幅压缩Draw Call。合并网格的时候注意先应用旋转变换(Ctrl+A → Apply Rotation),不然合并后世界坐标会乱。

在three.js端还可以开renderer.outputColorSpace = THREE.SRGBColorSpace;,以及确保场景里有足够的环境光照,不然VRM模型的材质会显得很暗。

整个流程跑通之后,你会发现自己其实已经掌握了三条路径:Blender烘焙直接播放、VRM插件加载再绑定动画、以及手动代码重定向。三种方案对应不同的业务场景,最省心的永远是Blender烘焙GLB,最灵活的是VRM插件方案。根据自己的项目需求选就够了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询