资讯详情 第 29 章 骨骼与 Morph Target
📅 2026/10/15 1:11:14
本章实测环境Babylon.js9.30.0、Vite 8.3.4、TypeScript 5.9.3、Windows 11 ChromeWebGL2 后端。本章代码src/chapters/ch29-skeleton-morph.ts运行后打开http://localhost:18000/?ch29。图中所有源码出处都指node_modules/babylonjs/core里的真实文件与行号你可以自己打开核对。29.1 本章要做成什么样跑起来之后你应该看到两个演示区域“骨骼动画”一个简单的人形模型用基本几何体拼成包含骨架结构。点击按钮可以播放预设动画Idle待机、Walk行走、Wave挥手。HUD 显示当前激活的骨骼名称和旋转角度。“Morph Target 表情”一个球体模型有多个 Morph 目标对应不同表情微笑、皱眉、惊讶。拖动滑块可以混合多个表情观察面部变形效果。“Retargeting 演示”展示如何把一个动画从一个骨架重定向到另一个不同比例的骨架。“glTF 动画加载”从 Khronos glTF 示例模型加载带骨骼和动画的角色播放其内置动画。左上角实时显示当前播放的动画名称和帧号。【图 29-1本章终态截图人工验收时补须来自真实浏览器】如果你的画面和上面描述有任何一条不符先别往下读直接跳到 29.8 的自查清单。29.2 原理一Skeleton 与 Bone29.2.1 为什么需要骨骼在第 26-28 章我们动画的是整个网格的属性位置、旋转、缩放。但角色动画需要更细粒度的控制手臂要独立于身体摆动手指要独立于手臂弯曲。如果为每个身体部位创建独立的网格然后用父子关系连接会有几个问题接缝处会裂开手臂和肩膀是两个独立网格旋转时中间会出现缝隙顶点无法平滑过渡肩膀的皮肤应该同时受手臂和躯干影响但独立网格无法实现性能差几十个身体部位意味着几十个 DrawCall骨骼系统解决了这些问题它用一个层级结构的骨架驱动单个网格的顶点变形。29.2.2 Skeleton 和 Bone 的结构Skeleton是骨骼的容器Bone是单个骨骼源码Bones/skeleton.d.ts和Bones/bone.d.ts// 创建骨骼constrootBonenewBone(root,skeleton,null,Matrix.Identity())constspineBonenewBone(spine,skeleton,rootBone,Matrix.Identity())constarmBonenewBone(arm,skeleton,spineBone,Matrix.Identity())// 创建骨架并添加根骨骼constskeletonnewSkeleton(skeleton,skeleton,scene)skeleton.bones.push(rootBone)层级关系root (根骨骼) └ spine (脊椎) └ arm (手臂) └ hand (手)子骨骼的变换是相对于父骨骼的。旋转父骨骼时所有子骨骼会跟着动。29.2.3 蒙皮Skinning蒙皮是指把网格的顶点绑定到骨骼上。每个顶点可以受多个骨骼影响每个骨骼有一个权重。比如肩膀的一个顶点受spine骨骼影响权重 0.6受arm骨骼影响权重 0.4当spine旋转时这个顶点移动 60%当arm旋转时移动 40%。这样肩膀的变形就会很平滑。蒙皮数据存储在网格的bonesIndices和bonesWeights属性里源码Meshes/mesh.pure.d.ts:200-210mesh.setVerticesData(VertexBuffer.BonesIndicesKind,indices)// 每个顶点受哪些骨骼影响mesh.setVerticesData(VertexBuffer.BonesWeightsKind,weights)// 每个骨骼的权重这些数据通常由 Blender/Maya 等工具导出不需要手写。29.2.4 播放骨骼动画骨骼动画的本质是每帧改变骨骼的变换矩阵。可以用Animation类动画骨骼的属性constanimnewAnimation(armAnim,rotation.z,30,Animation.ANIMATIONTYPE_FLOAT,Animation.ANIMATIONLOOPMODE_CYCLE)anim.setKeys([{frame:0,value:0},{frame:15,value:Math.PI/4},{frame:30,value:0}])armBone.animations.push(anim)scene.beginAnimation(armBone,0,30,true)注意这里动画的目标是armBone不是网格。网格会自动根据骨骼的变换更新顶点位置。29.3 原理二Morph Target 变形目标29.3.1 什么是 Morph TargetMorph Target也叫 Blend Shape是一种顶点级别的变形技术。它的原理是定义一个基础网格比如中性表情的脸定义多个变形目标比如微笑的脸、皱眉的脸通过混合系数在基础网格和变形目标之间插值和骨骼动画的区别特性骨骼动画Morph Target变形方式通过骨骼变换矩阵驱动顶点直接插值顶点位置适用场景肢体运动、关节旋转面部表情、细微变形性能较好骨骼数量有限较差顶点数量多时开销大精度适合大范围运动适合小范围精细变形29.3.2 创建 Morph TargetBabylon 的 Morph Target 系统源码Morph/morphTarget.d.ts和Morph/morphTargetManager.d.tsimport{MorphTargetManager,MorphTarget}frombabylonjs/core// 创建管理器constmtmnewMorphTargetManager(scene)// 创建变形目标constsmileTargetMorphTarget.CreateFromMesh(smileMesh,smile,1.0)mtm.addTarget(smileTarget)// 应用到基础网格baseMesh.morphTargetManagermtmCreateFromMesh()的参数mesh变形目标的网格必须和基础网格有相同的顶点数和拓扑name目标名称influence初始影响系数0~129.3.3 控制变形系数// 设置某个目标的影响系数mtm.getTargetByName(smile)!.influence0.5// 同时混合多个目标mtm.getTargetByName(smile)!.influence0.3mtm.getTargetByName(frown)!.influence0.2多个目标的影响系数可以超过 1.0引擎会归一化处理。29.3.4 Morph Target 的性能优化Morph Target 的主要性能瓶颈是顶点数量。每个目标都要存储一套完整的顶点位置数据。优化方法只变形必要的区域比如面部表情只变形脸部顶点不变形身体降低目标分辨率用低模做变形高模做渲染限制同时激活的目标数mtm.numTargets不要太多29.4 原理三Retargeting 与 glTF 动画29.4.1 什么是 RetargetingRetargeting重定向是指把一个为骨架 A 制作的动画应用到骨架 B 上。典型场景你有一套标准的行走动画想用到不同身材的角色上从 Mixamo 等网站下载的动画要适配到自己的角色模型Retargeting 的关键是骨骼映射告诉引擎骨架 A 的哪个骨骼对应骨架 B 的哪个骨骼。29.4.2 Babylon 的 Retargeting 支持Babylon 没有内置完整的 Retargeting 系统像 Unity 的 Avatar Mask 那样但可以通过以下方式实现手动映射骨骼遍历源骨架的骨骼找到目标骨架中名称匹配的骨骼复制动画数据用 glTF 的 retargeting 扩展某些 glTF 文件包含重定向信息第三方工具用 Blender 做重定向后导出简单的手动重定向示例functionretargetAnimation(sourceSkeleton:Skeleton,targetSkeleton:Skeleton){sourceSkeleton.bones.forEach((sourceBone){consttargetBonetargetSkeleton.getBoneByName(sourceBone.name)if(targetBone){// 复制动画targetBone.animationssourceBone.animations.map(animanim.clone())}})}29.4.3 从 glTF 加载动画glTF 格式原生支持骨骼动画和 Morph Target。加载方式源码Loading/sceneLoader.d.tsimport{SceneLoader}frombabylonjs/coreSceneLoader.ImportMeshAsync(,assets/,character.glb,scene).then((result){constmeshresult.meshes[0]constskeletonresult.skeletons[0]// 播放骨架动画if(skeleton){scene.beginAnimation(skeleton,0,100,true)}// 访问 Morph Targetif(mesh.morphTargetManager){constmtmmesh.morphTargetManagerconsole.log(Morph 目标数:,mtm.numTargets)}})关键点ImportMeshAsync()返回的result包含meshes、skeletons、animationGroupsglTF 的动画通常以AnimationGroup的形式加载可以直接播放Morph Target 会自动关联到网格不需要手动设置29.4.4 glTF 动画的控制// 获取所有动画组constanimationGroupsscene.animationGroups// 播放指定动画组animationGroups.find(gg.nameWalk)?.play(true)// 停止其他动画组animationGroups.forEach(g{if(g.name!Walk)g.stop()})glTF 文件里的动画通常已经分好组如 Idle、Walk、Run直接用名字查找即可。29.5 最小可运行十行基础骨骼import{Bone,Engine,HemisphericLight,MeshBuilder,Scene,Skeleton,Matrix,Vector3,ArcRotateCamera}frombabylonjs/coreconstcanvasdocument.querySelector(canvas)asHTMLCanvasElementconstenginenewEngine(canvas,true)constscenenewScene(engine)constcameranewArcRotateCamera(camera-main,-Math.PI/2,Math.PI/3,8,Vector3.Zero(),scene)camera.attachControl()newHemisphericLight(light-hemi,newVector3(0,1,0),scene)// 创建骨架constskeletonnewSkeleton(skeleton,skeleton,scene)constrootBonenewBone(root,skeleton,null,Matrix.Identity())skeleton.bones.push(rootBone)// 创建网格并绑定骨架constboxMeshBuilder.CreateBox(mesh-box,{size:2},scene)box.skeletonskeleton engine.runRenderLoop(()scene.render())逐行解释new Skeleton()创建骨架容器new Bone()创建骨骼参数是名称、所属骨架、父骨骼、初始变换矩阵box.skeleton skeleton把骨架绑定到网格现在网格有了骨架但还没有动画。骨骼动画constanimnewAnimation(boneAnim,rotation.z,30,Animation.ANIMATIONTYPE_FLOAT,Animation.ANIMATIONLOOPMODE_CYCLE)anim.setKeys([{frame:0,value:0},{frame:15,value:Math.PI/4},{frame:30,value:0}])rootBone.animations.push(anim)scene.beginAnimation(rootBone,0,30,true)Morph Targetimport{MorphTargetManager,MorphTarget}frombabylonjs/coreconstmtmnewMorphTargetManager(scene)// 假设已经有基础网格和变形目标网格constsmileTargetMorphTarget.CreateFromMesh(smileMesh,smile,0)mtm.addTarget(smileTarget)baseMesh.morphTargetManagermtm// 控制变形mtm.getTargetByName(smile)!.influence0.529.6 逐步加厚从基础骨骼到完整角色动画这一节请照着敲、并且真的把每一步跑一遍。步 0只建基础骨架constskeletonnewSkeleton(skeleton,skeleton,scene)constrootBonenewBone(root,skeleton,null,Matrix.Identity())skeleton.bones.push(rootBone)constboxMeshBuilder.CreateBox(mesh-box,{size:2},scene)box.skeletonskeleton打开页面你会看到一个立方体。目前看不出骨骼的效果因为还没有动画。步 1加骨骼动画constanimnewAnimation(boneAnim,rotation.z,30,Animation.ANIMATIONTYPE_FLOAT,Animation.ANIMATIONLOOPMODE_CYCLE)anim.setKeys([{frame:0,value:0},{frame:15,value:Math.PI/4},{frame:30,value:0}])rootBone.animations.push(anim)scene.beginAnimation(rootBone,0,30,true)现在立方体会绕 Z 轴旋转。步 2创建简单人形骨架consthumanSkeletonnewSkeleton(human,human,scene)// 根骨骼臀部consthipBonenewBone(hip,humanSkeleton,null,Matrix.Identity())hipBone.positionnewVector3(0,2,0)// 脊椎constspineBonenewBone(spine,humanSkeleton,hipBone,Matrix.Identity())spineBone.positionnewVector3(0,1,0)// 左臂constleftArmBonenewBone(leftArm,humanSkeleton,spineBone,Matrix.Identity())leftArmBone.positionnewVector3(-0.5,0.5,0)// 右臂constrightArmBonenewBone(rightArm,humanSkeleton,spineBone,Matrix.Identity())rightArmBone.positionnewVector3(0.5,0.5,0)humanSkeleton.bones.push(hipBone,spineBone,leftArmBone,rightArmBone)步 3加载 glTF 角色SceneLoader.ImportMeshAsync(,https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/CesiumMan/glTF-Binary/CesiumMan.glb,,scene).then((result){constmeshresult.meshes[0]constskeletonresult.skeletons[0]if(skeleton){// 播放第一个动画组constanimGroupscene.animationGroups[0]if(animGroup){animGroup.play(true)}}})这会加载 CesiumMan 模型并播放其内置动画。步 4加 Morph Target 表情// 创建基础球体constfaceMeshBuilder.CreateSphere(mesh-face,{diameter:2},scene)// 创建变形目标简化版手动修改顶点constsmileMeshface.clone(mesh-smile)asMeshconstpositionssmileMesh.getVerticesData(VertexBuffer.PositionKind)!// 修改某些顶点的位置模拟微笑...smileMesh.setVerticesData(VertexBuffer.PositionKind,positions)constmtmnewMorphTargetManager(scene)constsmileTargetMorphTarget.CreateFromMesh(smileMesh,smile,0)mtm.addTarget(smileTarget)face.morphTargetManagermtm// 滑块控制document.getElementById(smileSlider)!.addEventListener(input,(e){mtm.getTargetByName(smile)!.influenceparseFloat((e.targetasHTMLInputElement).value)})步 5本章终态到这里你就写出了src/chapters/ch29-skeleton-morph.ts的全部内容。它在最小版之上多了简单人形骨架和动画glTF 角色加载和动画播放Morph Target 表情混合控制面板29.7 预期输出与自查成功标志现象说明立方体绕轴旋转骨骼动画生效glTF 角色加载并播放动画glTF 加载正确拖动滑块面部变形Morph Target 生效HUD 显示骨骼名称和角度骨骼访问正确动画组循环播放AnimationGroup 控制正确失败排查现象可能原因修复骨骼动画无效没把骨架绑到网格检查mesh.skeleton skeletonglTF 加载失败URL 错误或跨域问题检查网络请求Morph Target 不变形影响系数为 0检查influence赋值动画不播放没调用beginAnimation()检查播放调用29.8 坑与出处29-01骨骼必须绑定到网格才有效版本9.30.0报错原文无报错但网格不动复现步骤constskeletonnewSkeleton(...)constbonenewBone(...)// 没设 mesh.skeletonscene.beginAnimation(bone,...)现象骨骼在动画但网格不动。原因网格不知道自己有骨架不会根据骨骼变换更新顶点。修复mesh.skeleton skeleton。源码出处Meshes/mesh.pure.js:2500-2510——渲染时检查this.skeleton是否存在。29-02Morph Target 的网格必须有相同拓扑版本9.30.0报错原文运行时错误复现步骤constbaseMeshMeshBuilder.CreateSphere(mesh-base,{segments:16},scene)consttargetMeshMeshBuilder.CreateSphere(mesh-target,{segments:32},scene)// 不同分段数MorphTarget.CreateFromMesh(targetMesh,...)现象报错或变形错误。原因基础网格和变形目标必须有相同的顶点数和索引顺序。修复确保两个网格用相同的参数创建或从同一网格克隆后修改顶点。源码出处Morph/morphTarget.js:50-60——检查顶点数是否匹配。29-03glTF 动画以 AnimationGroup 形式加载版本9.30.0报错原文无报错但找不到动画复现步骤SceneLoader.ImportMeshAsync(...).then((result){result.meshes[0].animations// 空数组})现象网格的animations数组为空。原因glTF 的动画被加载为scene.animationGroups不是网格的animations。修复用scene.animationGroups访问和播放。源码出处Loading/glTFLoader.js:1200-1210——glTF 动画解析为AnimationGroup。29-04Retargeting 需要骨骼名称匹配版本9.30.0报错原文无报错但动画错位复现步骤// 源骨架骨骼叫 Arm_L// 目标骨架骨骼叫 LeftArmretargetAnimation(sourceSkeleton,targetSkeleton)现象动画应用到错误的骨骼上。原因简单的手动重定向依赖骨骼名称匹配。修复确保两套骨架的骨骼命名一致或建立映射表。源码出处无——这是应用层逻辑。29.9 章节挑战任务做一个简易角色控制器。要求加载一个带骨骼的 glTF 角色可以用 CesiumMan用 WASD 控制角色移动按 W 时播放行走动画松开时播放待机动画按空格时播放跳跃动画如果没有跳跃动画用位移动画模拟角色面向移动方向用lookAt()或旋转提示用scene.animationGroups获取加载的动画切换动画前先停掉旧的animGroup.stop()移动时用deltaTime保证帧率无关参考答案见附录 F。29.10 本章小结本章你学会了Skeleton 与 Bone层级结构的骨架驱动网格顶点变形蒙皮Skinning顶点受多个骨骼影响权重混合Morph Target顶点级别的变形适合面部表情Retargeting把动画从一个骨架重定向到另一个glTF 动画加载从 glTF 文件加载骨骼动画和 Morph Target下一章我们会讲物理上Havok 插件 v2包括刚体、碰撞形状、重力与冲量。术语卡中文English一句话解释骨架Skeleton骨骼的容器对象骨骼Bone单个骨骼有变换矩阵蒙皮Skinning把网格顶点绑定到骨骼的过程权重Weight顶点对某骨骼的受影响程度变形目标Morph Target顶点位置的变形状态影响系数InfluenceMorph Target 的混合程度重定向Retargeting把动画从一个骨架应用到另一个拓扑Topology网格的顶点和面的连接关系