☰
WebGL人体3D动态模型:骨骼驱动、glTF数据链与动画性能优化
2026/10/14 1:39:26 网站建设 项目流程

简介:这份WebGL人体3D动态模型资源,面向网页前端开发者、三维动画学习者和游戏开发人员,用于在浏览器中无插件地展示逼真的人体行走动画。资源包共27个文件,以21个JavaScript脚本为主,覆盖模型加载、材质处理与骨骼动画驱动,辅以htm和css构建演示界面,png图片提供贴图与状态按钮,整包仅1.16MB,轻量完整,便于快速部署。已有3010人学习下载,是理解骨骼动画与WebGL渲染的实用参考。通过阅读源码,可掌握将3D人体模型按解剖学结构绑定虚拟骨架、用关键帧设定行走姿态并插值过渡、再借助生物力学数据矫正动作自然度的完整流程,对学习人体运动学、动画设计及游戏角色开发均具直接参考价值。

1. 为什么说 WebGL 人体 3D 动态模型的关键不在建模而在骨骼驱动

WebGL人体3D动态模型最容易被低估的部分不是模型本身,而是骨骼驱动。一个静态人体模型,顶点再多也只是一堆三角形;一旦要让手臂摆动、躯干旋转,每一帧都要重新计算几十个关节的矩阵,再把这些矩阵作用到上万顶点上。很多人把精力花在精细化建模上,结果导入网页后模型要么僵在原地,要么扭曲成麻花。这篇文章面向要在前端项目里接入人体动态模型的开发者,从渲染选型、glTF数据链、动画播放到性能排错,讲清楚一条能落地的路径。适合正在做数字人、运动展示、虚拟形象或动作回放的人,新手可以照着步骤跑通,熟手可以跳过基础直接看避坑和预算。

2. 渲染管线选型:原生 WebGL 还是 Three.js 封装,哪个更适合人体动态模型

WebGL本身是一套管线的状态机,不是场景图编辑器。人体动态模型涉及骨骼矩阵、蒙皮权重、动画播放和交互,如果直接从原生WebGL开始,第一件事不是画模型,而是把数学库、加载器和Uniform管理全部手写一遍。所以我的习惯是:凡是项目目标是“人体3D动态模型”,默认选用成熟的WebGL封装库,只有在需要精确控制每一帧绘制或者做性能极限优化时才回到原生。下面先拆一下原生需要扛住的部分,再说明封装库把哪些活替代了。

2.1 原生 WebGL 要自己扛的四个模块

第一个模块是相机矩阵。透视相机需要投影矩阵和视图矩阵,还得处理窗口resize时宽高比变化。人体模型通常站在原点附近,相机需要跟随模型或围绕模型旋转,这些操作在原生里都要自己维护矩阵并传给uniform。第二个模块是顶点布局。人体网格除了position、normal、uv之外,还要多两个attribute:JOINTS_0存顶点关联的骨骼索引,WEIGHTS_0存对应的蒙皮权重。这意味着缓冲区布局要重新设计,shader的attribute声明也要加两行,任何一个attribute偏移写错,顶点就会乱飞。

第三个模块是骨骼矩阵上传。人形角色即使精简到30根骨骼,静态有30个mat4,动态动画每一帧还要更新这些矩阵并上传到GPU。原生WebGL里要么用uniform数组,要么用UniformBuffer,自己做归一化和排序时很容易踩中列主序与行主序的差异。第四个模块是绘制循环与动画时间。动画不是把模型加载出来就完了,而是每帧计算时间增量、更新骨骼、提交绘制调用。如果忘了绑program或没有按骨骼属性开启attribute,整个画面就是黑的。这些模块单独看都不算难,组合在一起以后排查问题非常耗时。

我最早的项目就是裸写WebGL,结果在骨骼矩阵的uniform数组布局上栽了一个星期,最后发现是mat4数组的偏移多写了16个float。这种问题在封装库中已经被处理过,完全没有必要自己再趟一遍。如果你是做产品而不是做引擎,血泪经验就是:不要把一个角色的蒙皮计算也当成“从零开始”的乐趣。

2.2 为什么我默认用封装库:从“画一个三角形”到“加载整个人体”的成本对比

封装库最直接的价值是把场景图、加载器、渲染循环这些样板代码固定下来。下面这段是最小可运行的加载逻辑:

import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; const scene = new THREE.Scene(); // 45° 视场角,近裁面 0.1,远裁面 1000 const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(2, 1.5, 3); camera.lookAt(0, 1, 0); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.outputColorSpace = THREE.SRGBColorSpace; document.body.appendChild(renderer.domElement); // 没有光源时PBR材质会呈现为黑色 scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(3, 5, 2); scene.add(dirLight); const loader = new GLTFLoader(); loader.load('human.glb', (gltf) => { scene.add(gltf.scene); // gltf.animations 保存模型自带的动画剪辑 // gltf.scene 里已经包含 SkinnedMesh 和骨骼节点 });

这段代码里要特别注意三个参数。第一是PerspectiveCamera的第二个参数,它必须随着窗口大小变化更新,否则模型会被拉伸;通常监听resize事件并重设camera.aspect和renderer.setSize。第二是renderer.outputColorSpace,如果保持默认的线性色彩空间,皮肤贴图会明显发灰。第三是光源强度,环境光0.6、方向光0.8是我常用的起点,具体要根据材质调节,太亮会像过曝,太暗又会丢失形体。

使用封装库的收益从成本对比上看得更清楚:

对比项原生WebGL封装库
加载glTF自己写buffer和accessor解析一条load调用
骨骼矩阵更新手动上传uniform数组SkinnedMesh自动处理
动画混合自己算权重AnimationMixer提供
包体积无额外依赖增加几百KB JS
性能控制完全可控抽象层有一定开销

但是要泼一盆冷水:封装库不会替你做蒙皮计算。蒙皮算法仍然在顶点阶段执行,SkinnedMesh只是把矩阵推导、权重绑定和attribute上传封装好了。如果动作出现错位或变形,一样要回到骨骼数据本身去排查,而不是把锅甩给库。所以选型上我的结论是:WebGL原生活着是手段,封装库是更快达到目的的工具。两者可以共存,陷入“原生才是硬核”的误区反而会让项目周期失控。

3. 人体模型的数据链:glTF 里的骨骼、蒙皮权重和动画剪辑怎么读

人体3D动态模型在Web上最常用的格式是glTF 2.0,GLB就是它的二进制封装版本。相比OBJ和FBX,glTF有两个好处:第一是文件内同时包含网格、材质、骨骼和动画,加载器一个请求就能拿到全部数据;第二是它是面向GPU的格式,很多数据结构可以直接对接顶点缓冲。所以不管你是自己写解析器还是用封装库,了解glTF里的骨骼与蒙皮组织方式,都是排查问题的基本功。

3.1 glTF 如何组织骨骼与蒙皮数据

glTF的蒙皮信息集中在skin对象里。一个skin会声明骨骼节点列表joints,以及一个inverseBindMatrices,它存的是每个骨骼在绑定姿态下的逆矩阵。网格的每个顶点通过JOINTS_0和WEIGHTS_0两个属性声明自己受哪几根骨骼影响、影响权重是多少。下面把结构抽成最小可读的形式:

{ "skins": [{ "joints": [0, 1, 2], "inverseBindMatrices": 3 }], "nodes": [ { "name": "Hips", "children": [1] }, { "name": "LeftUpperLeg", "children": [2] }, { "name": "LeftLowerLeg" } ], "meshes": [{ "primitives": [{ "attributes": { "POSITION": 4, "JOINTS_0": 5, "WEIGHTS_0": 6 } }] }], "animations": [{ "channels": [ { "target": { "node": 1, "path": "rotation" } } ] }] }

这个JSON是便于理解的简化,不是完整规范。joints里的数字是nodes数组的索引,inverseBindMatrices指向一个accessor,里面是每个骨骼的逆绑定矩阵;JOINTS_0每个顶点最多有4个整数索引,对应joints列表的位置,WEIGHTS_0则是同样的4个浮点权重。动画通道中的target.node告诉引擎哪根骨骼在第几秒被旋转或平移。所以如果动作错位,先检查动画通道指向的节点是不是我们希望控制的那根骨骼,而不是先怀疑材质或相机。

关于权重,有一个常备常识:GPU蒙皮要求每个顶点最多4个骨骼影响,WEIGHTS_0必须归一化,也就是4个数值加起来等于1。如果权重总和大于1,模型在关节弯曲时会显得僵硬,小于1则会让顶点被其他骨骼“拽”得很软。加载到封装库后,JOINTS_0和WEIGHTS_0会被转成skinIndex和skinWeight两个BufferAttribute,不过这属于内部转换,我们调试时更多是直接读这两个attribute。

还要说一下inverseBindMatrices的作用。很多人误以为它就是骨骼的模型矩阵,实际上它是在绑定姿态下每个骨骼世界矩阵的逆。每个顶点先被这个矩阵从绑定姿态切回骨骼局部空间,再被当前动画的jointMatrix变换到新位置。如果导出时模型应用了旋转但逆绑定矩阵没更新,动作就会带着一个固定偏移,看起来像是模型骨架和网格“错位”。所以导出检查里的“应用变换”必须做,目的就是保证绑定姿态干净。

3.2 从建模工具导出:导出参数和加载前的检查清单

很多开发者拿到一个模型就丢给加载器,结果各种问题。我的一般做法是先过一遍导出检查,能省掉后面一半的排错时间。下面这份清单来自我和建模同事长期的合作经验,不是软件帮助文档的搬抄:

  1. 场景单位设置成米。WebGL默认1单位等于1米,如果模型以厘米为单位,加载后尺寸会大100倍,相机根本框不住。
  2. 全选模型与骨骼,执行清零变换。位置归零、旋转归零、缩放归1,这是最影响动作的一步。
  3. 检查顶点组命名。骨骼名和顶点组名必须一致,不要有空格、点号或中文字符。
  4. 动画烘焙后再导出。IK解算器一般不会被glTF保存,必须把动画转成骨骼关键帧。
  5. 所有纹理都嵌入二进制文件。不要在导出后手工去猜相对路径,移动端加载少一个请求就少一个故障点。

导出时的参数集中在几个容易踩的位置,我列在下面:

检查项为什么重要排查方式
单位模型尺寸会差100倍加载后看包围盒边长
应用变换骨骼局部坐标乱了会导致动作偏移检查T-pose是否与建模软件一致
骨骼命名动画通道靠名称区分打印场景全部骨骼名
动画烘焙未烘焙的IK不是有效关键帧检查动画是否有持续位移

导出完成以后,我在项目里会先打印一次模型结构,再决定后续开发。打印骨骼名和动画名看起来不起眼,却能提前发现文件版本的差异:

loader.load('human.glb', (gltf) => { console.log('animations:', gltf.animations.map(a => a.name)); gltf.scene.traverse((obj) => { if (obj.isBone) console.log('bone:', obj.name); if (obj.isSkinnedMesh) { console.log('mesh:', obj.name, 'weightCount:', obj.geometry.attributes.skinWeight.count); } }); });

这里traverse会走遍场景树,打印所有骨骼节点。如果输出的骨骼名和DCC工具里不一致,说明模型混入了多余物体,或者加载器因为某种原因没有拿到完整骨架。skinWeight.count应该等于网格顶点数,如果它缺失,说明导出的网格没绑骨骼,加载器只能把它当静态模型处理,动作自然也就永远不会生效。

这一章以数据链为主线。从glTF结构到导出检查再到运行期验证,三个环节都确认过,基本能挡住八成数据问题。

4. 让模型动起来:AnimationMixer 播放、动作切换与动捕数据映射

静态展示模型很容易,难的是让动态模型真正按你的节奏动。这个“动”分两层:第一层是把模型自带的骨骼动画播放出来,第二层是把外部动作数据映射到骨骼。前者用封装库的动画混合器就能做,后者需要理解骨骼旋转的坐标系与插值。下面逐个讲。

4.1 用 AnimationMixer 控制动作:播放、暂停、循环和切换

封装库里的AnimationMixer负责把AnimationClip应用到SkinnedMesh的骨骼上。它会维护每一个Action的权重,并在每一帧更新骨骼的局部矩阵。换句话说,动态模型每一帧的骨骼姿态都是由mixer计算出来的,我们只需要告诉它播什么、播多快、怎么切换。

// 加载完成回调中创建mixer const mixer = new THREE.AnimationMixer(gltf.scene); const idle = mixer.clipAction(gltf.animations[0]); const walk = mixer.clipAction(gltf.animations[1]); idle.play(); // 动画循环中更新 const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); mixer.update(delta); renderer.render(scene, camera); }

clock.getDelta()返回的是两帧之间的间隔,单位是秒。把间隔直接传给mixer.update很重要,动画速度才不会受帧率影响。如果你误传了累计时间,动画会越播越快,这是新手最容易翻车的地方。clipAction的第一个参数可以是动画对象,也可以是索引;它返回一个Action,play()启动播放。

如果想控制动作是否循环,在播放前给Action设置循环模式:

// 只播放一次并停在最后一帧 walk.setLoop(THREE.LoopOnce, 1); walk.clampWhenFinished = true; walk.play();

setLoop的第二个参数是循环次数,LoopRepeat表示无限循环。clampWhenFinished配合LoopOnce使用,动画结束后停在最后一帧而不是跳回起点。切换动作时,直接stop()当前动作会让骨骼瞬间回到绑定姿态,看起来像闪断;正确的做法是交叉淡入淡出:

walk.reset().setEffectiveWeight(1).fadeIn(0.3).play(); idle.fadeOut(0.3);

这里fadeIn(0.3)和fadeOut(0.3)的0.3单位是秒,表示动作过渡时间。对人的动作来说,0.3秒到0.5秒最自然,太快会生硬,太慢又会让人觉得动作“粘”。reset()会把Action的时间轴归零,避免从上一帧的中间继续。

实际操作中还有一个隐藏点:mixer.update(delta)必须每一帧都调用,哪怕当前只播放一个动作。如果初始化了mixer但忘记在循环里更新,SkinnedMesh的骨骼矩阵不会刷新,模型会固定在一个奇怪的中间姿势上。这个问题我用一个很小的作业Demo反复调试时才意识到,属于典型的“黑匣子”问题。

4.2 把外部动作数据映射到骨骼:从 BVH 到局部旋转

模型自带的动作往往不够,很多项目需要动捕数据或程序生成的动作。常见做法是解析BVH格式,把每一帧关节的旋转欧拉角转成四元数,再赋给对应的骨骼。BVH里每个关节记录的是相对父节点的局部旋转,所以映射时要把四元数直接写到骨骼的quaternion上,而不是去改世界矩阵。

// 假设 motion 是解析完的浮点数组 // frameSize 是每帧数据的长度,前面4个是根节点位移,后面按关节顺序排旋转 function applyFrame(bones, motion, frameIndex, frameSize) { const offset = frameIndex * frameSize + 4; for (let i = 0; i < bones.length; i++) { // 多数BVH导出器把欧拉角顺序记为 ZXY const z = motion[offset + i * 3 + 0]; const x = motion[offset + i * 3 + 1]; const y = motion[offset + i * 3 + 2]; const euler = new THREE.Euler(x, y, z, 'ZXY'); const quat = new THREE.Quaternion().setFromEuler(euler); bones[i].quaternion.slerp(quat, 0.3); } }

这里索引偏移一定要根据实际BVH头部信息调,不能照抄。有些导出器的旋转顺序是XYZ或-YXZ,用错了模型会直接扭曲。我一般会先读取BVH的CHANNELS定义,按定义创建Euler的轴向顺序,再传给骨骼。slerp的第二个参数是插值比例,0.3表示当前帧骨骼姿态向目标姿态靠近30%,60FPS的数据用这个值足够平滑;如果数据只有30FPS,可以提高到0.5,但不要直接用硬赋值,否则动作会抖得像骨架都散了。

BVH的坐标系也需要处理。很多动捕软件导出时是Z轴向上,而glTF约定Y轴向上。常见做法是把整个BVH骨架先做一次90度旋转,再按根节点位移定位到场景,不要逐骨骼旋转某个局部轴,否则层级关系会乱。根节点的位置我一般只用Y轴做身高偏移,XZ平面的位移由自己的逻辑控制,防止模型在场景里乱跑。映射完成后,用一段走路循环做测试:如果脚部在地面滑步,说明根节点位移和动画时间不同步,优先检查BVH采样率与模型的动画帧率是否一致。

5. 避坑:WebGL 人体动态模型从黑屏到动作畸形的 5 个排查点

前几章讲的是怎么做,这一章把我在真实项目里反复遇见的坑集中说一遍。每条都按“现象→原因→解决”展开,你可以直接拿来做排错手册。

5.1 模型加载后黑屏:先查相机包围盒,再查光源

现象:页面打开没有报错,但画面全黑,模型看不到。这种问题最迷惑人,因为控制台不提示,容易让人去怀疑着色器或加载器。

原因:最常见是模型不在相机可视范围内。人体模型如果以厘米为单位导出,加载后就是几十米高,而相机视野只有10米宽,模型被整个推出屏幕。另外一种可能是模型中心不在原点,而相机固定看向(0,0,0),模型在视野外。光源也是一个因素,PBR材质在没有灯光的场景里会渲染成黑色。

解决:先动态计算包围盒再设置相机:

const box = new THREE.Box3().setFromObject(gltf.scene); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); camera.position.copy(center).add(size.clone().multiplyScalar(2)); camera.near = Math.max(size.length() / 100, 0.1); camera.far = Math.max(size.length() * 10, 100); camera.lookAt(center);

getCenter和getSize把模型的包围盒信息取出来,相机放在包围盒外侧,远近裁剪面随模型尺寸动态调整。这样即使单位错了,模型至少先显示出来。接下来补光源,用环境光加方向光让模型亮起来,再逐个调强度。我见过很多项目黑屏的根源只是没有加光,临时加一盏灯就能确认模型本身没问题。

5.2 动作显示为“手臂穿过躯干”:骨骼层级被建模工具改了名

现象:动画正常播放,但模型角色动作完全不对,比如手臂扭到背后、腿从胯部穿出。通常模型和动画可以播放,但动作“张冠李戴”。

原因:动画通道的目标节点和模型当前骨骼不匹配。glTF动画通过节点索引或名称指向目标,如果动画来自另一个骨架,索引偏移一位,手臂的旋转就被施加到了大腿上。另一个常见原因是建模工具导出时勾选了“仅导出可见对象”,把参与动画的辅助骨骼漏掉了。

解决:加载后先打印动画轨道和骨骼名:

gltf.animations.forEach((clip, i) => { clip.tracks.forEach(track => { console.log(`animation[${i}] track:`, track.name); }); });

track.name通常是“骨骼名.rotation”或“骨骼名.position”。如果输出的骨骼名在场景里找不到,就说明模型和动画的文件骨架不匹配。不要在代码里手写名字映射去硬凑,正确做法是回到DCC工具里把两套资源统一命名后重新导出。我一般在项目里固定一套骨骼命名规范,所有角色都从同一套模板开始,这样动作文件才能复用。

5.3 蒙皮顶点在关节处凹陷:权重没有归一化

现象:膝盖或手肘弯曲时,皮肤在关节处出现明显凹陷、塌陷,甚至裂开。表面看是重量问题,实际上多是蒙皮权重异常。

原因:glTF要求每个顶点4个权重之和为1,但很多建模工具在导出时不强制检查。如果某个顶点只有3个权重或者权重总和为1.5,GPU蒙皮计算出来的位置就会偏离原始网格,表现为关节塌陷或拉扯。

解决:在加载后先检查权重总和:

const mesh = gltf.scene.getObjectByProperty('isSkinnedMesh', true); const weights = mesh.geometry.attributes.skinWeight; for (let i = 0; i < weights.count; i++) { const sum = weights.getX(i) + weights.getY(i) + weights.getZ(i) + weights.getW(i); if (Math.abs(sum - 1) > 0.01) { console.warn(`vertex ${i} weight sum = ${sum}`); } }

这里skinWeight就是WEIGHTS_0转换后的属性,count等于顶点数。阈值0.01是允许的浮点误差,超过就说明需要修。正确修法是回到建模工具里对顶点组执行“权重归一化”,让软件重新分配权重总和;不要在加载后手动给数值做除法,那只会把错误平摊到其他动作上。权重校准是美术环节的活,程序能做的最好就是把它暴露出来。

5.4 动画播放时掉帧严重:CPU更新矩阵导致

现象:静态模型很流畅,FPS稳定60,一旦播放动画就掉到20甚至更低。尤其是角色跟着动作不断摆动身体时,卡顿会非常明显。

原因:常见原因是蒙皮计算没有走GPU。在WebGL2环境下SkinnedMesh默认把jointMatrix放到顶点着色器,走GPU蒙皮;但如果某些代码触发了CPU蒙皮路径,比如直接读取骨骼矩阵或强制使用CPU计算,顶点数上的开销就会爆炸。另一个原因是角色被拆成多个独立Mesh,一个手势切换可能引发几十个draw call。

解决:先确认角色是一个SkinnedMesh而不是多个网格拼合。多个材质组可以使用同一个geometry的groups特性,但绘制次数仍然按组计算,所以最好把贴图合成一张图集,减少draw call。骨骼数量也要控制在性能预算内,人形角色60根骨骼足够做自然动作,超过100根会显著增加每帧矩阵上传量。最后加阴影时要谨慎,方向光阴影在移动端经常是卡顿元凶,优先关掉阴影测试。

注意:如果项目必须支持WebGL1,默认的GPU蒙皮并不一定可用,这时候要把顶点数压到2万以下,并关闭阴影,否则动态模型很难在普通手机上流畅运行。

5.5 贴图发灰或反了:sRGB与Y轴翻转

现象:皮肤颜色发灰,整体像蒙了一层白雾;或者法线贴图的光影方向不对,光照从左来,阴影却出现在左边。

原因:WebGL纹理默认按线性色彩空间采样,而颜色贴图是sRGB。如果渲染器没有开启sRGB编码,贴图显示出来就会发灰。法线贴图不能做同样的转换,否则法线方向会被破坏。UV的Y轴方向在不同DCC工具里也存在分歧,glTF约定的是左上原点,部分工具默认左下原点,导出时没有翻转UV就会让贴图显示反了。

解决:Three.js里设置纹理色彩空间只需要一行:

const tex = new THREE.TextureLoader().load('skin.jpg'); tex.colorSpace = THREE.SRGBColorSpace;

颜色贴图设置sRGB,法线贴图和金属粗糙度贴图保持线性空间。如果模型是整体加载的glTF,GLTFLoader会根据材质定义自动处理大部分情况,但手工添加贴图时这份代码就要自己写。UV反了的问题,优先在导出设置里勾选“翻转V轴”,不要通过texture.offset或texture.repeat去硬改,那样做的代价是会连法线贴图一起翻转。模型动态展示前,先摆一个T-pose正对相机看一眼贴图方向,比后面在动画里发现再回头查快得多。

这5个坑不是全部,但覆盖了从加载、动画、蒙皮、性能到贴图的主要环节。遇到新问题时,建议按“数据层→渲染层→性能层”的顺序排查,大部分WebGL人体动态模型的问题都出在这三层交界处。

6. 进阶玩法:在低端设备上做人形动态模型的性能预算与动作融合

当模型能跑起来、坑也排得差不多时,真正决定项目上限的是两件事:性能和动作手感。这一章给两个可直接使用的进阶方案,尤其适合在移动端做数字人或动作展示的场景。

6.1 先定预算再动工:顶点数、骨骼数与纹理贴图

我一般会在项目启动时把下面这张预算表贴出来,让美术同学照着定资源规格:

设备档位顶点数骨骼数纹理尺寸阴影
千元手机2-3万30≤1024关闭
中端手机4-5万50-602048单方向光
桌面浏览器8万以上1004096PCF

这个表不是硬性标准,但能让所有人对“用什么设备跑”有共识。人体模型里头发和衣服是顶点消耗大头,如果只是上半身的动态模型,2万顶点完全够用。不参与蒙皮的饰品或装备不要挂在SkinnedMesh上,单独做一个静态Mesh挂到骨骼节点上,可以减少顶点缓冲更新量。

6.2 动作融合的正确姿势

动作切换用交叉淡入淡出,避免多个动作同时占用权重:

let currentAction = null; function playAction(name) { const next = mixer.clipAction(gltf.animations.find(a => a.name === name)); if (currentAction) { currentAction.fadeOut(0.4); } next.reset().setEffectiveWeight(1).fadeIn(0.4).play(); currentAction = next; }

fadeOut和fadeIn的时间一致,保证任意时刻只有一个动作在主导。0.4秒适合从站立切换到走路,如果想让人物从走路到跑步更柔和,可以把过渡时间拉到0.5秒。setEffectiveWeight(1)是为了防止上一个动作残留的权重影响新动作。

我早期在一个数字人Demo里,想把空闲、走路、跑步三个动作同时挂到模型上,结果角色手臂像抽风一样甩。调了一天发现是三个Action的权重没有互斥,骨骼同时被三套旋转叠加。后来给自己定了条规矩:人形动态模型的动作切换永远用crossfade,性能预算表贴在项目板上。先算账再动手,能省掉一整个星期的返工。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询