BabylonJS - 骨骼
Babylon.js - 骨骼和骨架
Section titled “Babylon.js - 骨骼和骨架”骨骼动画(Skeletal animation)是用于动画化复杂角色和 3D 图形对象的基本技术。它涉及定义一个由“骨骼”(变换节点,transform nodes)组成的内部层级结构,形成一个“骨架”(skeleton)。然后,可见的几何体(网格“皮肤”,mesh ‘skin’)被附着或“蒙皮”(skinned)到这些骨骼上。当骨骼移动或旋转时,网格会相应地变形,从而实现角色逼真的运动,例如行走、奔跑或挥手。
Babylon.js 提供了 Bone 和 Skeleton 类,以及相关的动画功能。虽然你可以使用这些类通过编程方式创建骨架和骨骼,但在 3D 建模软件(如 Blender、3ds Max、Maya)中创建它们并与网格和动画一起导出(通常使用 glTF 格式,.glb 或 .gltf)更为常见和实用。
从模型加载骨架
Section titled “从模型加载骨架”当你加载包含骨骼动画的模型文件(如 glTF)时,Babylon.js 会自动解析骨架和骨骼数据。你可以访问与加载的网格相关联的骨架。
import * as BABYLON from '@babylonjs/core';import '@babylonjs/loaders/glTF'; // 确保导入 glTF 加载器
// 假设 'scene' 已定义
async function loadModelWithSkeleton() { try { const result = await BABYLON.SceneLoader.ImportMeshAsync( null, // 加载所有网格 "models/", // 模型文件夹路径 "character.glb", // 你的动画模型文件 scene );
if (result.skeletons && result.skeletons.length > 0) { const mainSkeleton = result.skeletons[0]; console.log(`Loaded skeleton: ${mainSkeleton.name} with ${mainSkeleton.bones.length} bones.`);
// 通过索引或名称访问单个骨骼(例如) const headBone = mainSkeleton.bones.find(bone => bone.name === "mixamorig_Head"); // 示例名称 if (headBone) { console.log("Found head bone:", headBone.getIndex()); }
// 网格通常会自动链接到骨架 const characterMesh = result.meshes[1]; // 网格通常不是第一个元素(根节点可能是) if (characterMesh && characterMesh.skeleton === mainSkeleton) { console.log(`Mesh '${characterMesh.name}' is skinned to skeleton '${mainSkeleton.name}'.`); }
// 启动一个动画(如果模型有动画组) if (result.animationGroups && result.animationGroups.length > 0) { const walkAnimation = result.animationGroups.find(ag => ag.name === "Walk"); if (walkAnimation) { walkAnimation.start(true, 1.0, walkAnimation.from, walkAnimation.to, false); console.log("Playing 'Walk' animation."); } else { // 如果找不到特定名称,则播放第一个动画组 result.animationGroups[0].start(true, 1.0, result.animationGroups[0].from, result.animationGroups[0].to, false); console.log(`Playing first animation group: ${result.animationGroups[0].name}`); } } } else { console.log("No skeletons found in the loaded model."); }
} catch (error) { console.error("Error loading model:", error); }}
loadModelWithSkeleton();将网格附加到骨骼
Section titled “将网格附加到骨骼”一个常见的用例是将配件或装备(如帽子、剑或手电筒)附加到动画角色的特定骨骼上,以便配件能随着角色的动画正确移动。
你可以使用 mesh.attachToBone(bone, parentMesh) 方法来实现这一点。
// 假设 'mainSkeleton', 'characterMesh' 和 'scene' 已定义// 并且我们已经找到了 'headBone',如前面的示例所示
if (headBone && characterMesh) { // 创建一个帽子网格 const hatMesh = BABYLON.MeshBuilder.CreateCylinder("hat", { diameterTop: 0.4, diameterBottom: 0.8, height: 0.5, tessellation: 24 }, scene); const hatMaterial = new BABYLON.StandardMaterial("hatMat", scene); hatMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.2, 0.2); // 深色 hatMesh.material = hatMaterial;
// 将帽子附加到角色网格的头部骨骼 hatMesh.attachToBone(headBone, characterMesh);
// 可选:调整帽子相对于骨骼原点的位置/旋转 hatMesh.position.y = 0.2; // 相对于骨骼原点稍微向上移动 hatMesh.rotation.x = Math.PI / 12; // 稍微向前倾斜
console.log("Hat attached to head bone.");}现在,当角色的动画播放时,如果 headBone 移动或旋转,hatMesh 会自动跟随,因为它已附加。
示例:加载动画角色并附加物品
Section titled “示例:加载动画角色并附加物品”此示例加载一个动画角色模型(例如“Dude”模型,通常以 glTF 格式提供),并将其简单形状附加到其手部和头部骨骼上。
<!DOCTYPE html><html><head> <meta charset="utf-8"/> <title>Babylon.js - Bones and Skeletons Demo</title> <script src="https://cdn.babylonjs.com/babylon.js"></script> <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> <style> html, body { overflow: hidden; width: 100%; height: 100%; margin: 0; padding: 0; } #renderCanvas { width: 100%; height: 100%; touch-action: none; } </style></head><body> <canvas id="renderCanvas"></canvas> <script type="text/javascript"> const canvas = document.getElementById("renderCanvas"); const engine = new BABYLON.Engine(canvas, true);
const createScene = async function () { const scene = new BABYLON.Scene(engine); scene.clearColor = new BABYLON.Color4(0.8, 0.9, 1.0, 1.0); // 淡蓝色天空
const camera = new BABYLON.ArcRotateCamera("Camera", Math.PI / 2, Math.PI / 2.5, 3, new BABYLON.Vector3(0, 1, 0), scene); camera.attachControl(canvas, true); camera.wheelPrecision = 50; camera.lowerRadiusLimit = 1; camera.upperRadiusLimit = 10;
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene); light.intensity = 0.8; const pointLight = new BABYLON.PointLight("pointLight", new BABYLON.Vector3(0, 1.5, -1), scene); pointLight.intensity = 0.5;
const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 10, height: 10}, scene);
try { // 加载动画角色(替换路径/文件名) // 确保你的模型骨骼名称类似 'mixamorig_RightHand' 和 'mixamorig_Head' // 你可以使用 console.log(skeleton.bones) 来检查骨骼名称 const result = await BABYLON.SceneLoader.ImportMeshAsync(null, "models/", "dude.glb", scene);
const dudeMesh = result.meshes.find(m => m.skeleton); // 查找带有骨架的网格 const skeleton = result.skeletons[0];
if (dudeMesh && skeleton) { dudeMesh.position = new BABYLON.Vector3(0, 0, 0); // 将 dude 定位在地面上 dudeMesh.scaling.scaleInPlace(0.01); // dude 模型可能很大 camera.target = dudeMesh.position.add(new BABYLON.Vector3(0,1,0)); // 将相机目标对准 dude 中心附近
console.log("Dude model loaded.");
// 找到特定骨骼(名称取决于你的模型!在 Blender/console 中检查) const rightHandBone = skeleton.bones.find(b => b.name === "mixamorig_RightHand"); const headBone = skeleton.bones.find(b => b.name === "mixamorig_Head");
// 将一个小球附加到右手 if (rightHandBone) { const handItem = BABYLON.MeshBuilder.CreateSphere("handItem", { diameter: 0.05 }, scene); const itemMat = new BABYLON.StandardMaterial("itemMat", scene); itemMat.diffuseColor = BABYLON.Color3.Red(); handItem.material = itemMat; handItem.attachToBone(rightHandBone, dudeMesh); handItem.position = new BABYLON.Vector3(0.05, 0.05, 0); // 调整相对位置 console.log("Attached item to right hand."); } else { console.warn("Right hand bone not found."); }
// 将一个小方块附加到头部 if (headBone) { const headItem = BABYLON.MeshBuilder.CreateBox("headItem", { size: 0.08 }, scene); const headItemMat = new BABYLON.StandardMaterial("headItemMat", scene); headItemMat.diffuseColor = BABYLON.Color3.Blue(); headItem.material = headItemMat; headItem.attachToBone(headBone, dudeMesh); headItem.position.y = 0.15; // 调整相对位置 console.log("Attached item to head."); } else { console.warn("Head bone not found."); }
// 播放动画(找到一个合适的动画组名称) const idleAnim = result.animationGroups.find(ag => ag.name.toLowerCase().includes("idle")); // 尝试找到一个闲置动画 if (idleAnim) { idleAnim.start(true, 1.0, idleAnim.from, idleAnim.to, false); console.log(`Playing animation: ${idleAnim.name}`); } else if (result.animationGroups.length > 0) { result.animationGroups[0].start(true, 1.0, result.animationGroups[0].from, result.animationGroups[0].to, false); console.log(`Playing first animation group: ${result.animationGroups[0].name}`); }
} else { console.error("Could not find skinned mesh or skeleton in loaded model."); }
} catch (error) { console.error("Error loading character model:", error); // 如果加载失败,添加一个备用对象 const fallback = BABYLON.MeshBuilder.CreateBox("fallback", {}, scene); fallback.position.y = 0.5; }
return scene; };
createScene().then(sceneToRender => { engine.runRenderLoop(function () { if (sceneToRender) { sceneToRender.render(); } }); });
window.addEventListener("resize", function () { engine.resize(); }); </script></body></html>为了使示例工作,你需要一个 glTF 文件(例如 dude.glb),并将其放在 models 文件夹中。此模型必须包含一个蒙皮到骨架的网格,并且骨架应具有类似于 mixamorig_RightHand 和 mixamorig_Head 的骨骼名称。此外,文件中需要至少定义一个动画组。你可能需要在 Blender 中检查你的特定模型或将 skeleton.bones 输出到控制台以找到正确的骨骼名称。
如果 dude.glb 模型加载正确,示例将展示一个站在地面平面上的动画角色。一个小红球将附加到角色的右手上,一个小蓝方块将附加到其头部。当角色执行其闲置(或第一个可用的)动画时,红球和蓝方块将随相应的骨骼刚性移动。
手动创建 (进阶)
Section titled “手动创建 (进阶)”虽然对于复杂角色来说不太常见,但你可以通过编程方式定义骨骼和骨架:
// 创建一个骨架const skeleton = new BABYLON.Skeleton("mySkeleton", "skel01", scene);
// 创建骨骼 (名称, 父骨架, 父骨骼 (可选), 矩阵)const rootBone = new BABYLON.Bone("root", skeleton, null, BABYLON.Matrix.Identity());const childBone = new BABYLON.Bone("child", skeleton, rootBone, BABYLON.Matrix.Translation(0, 1, 0)); // 相对于父骨骼的位置
// ... 添加更多骨骼
// 将骨架链接到网格(需要匹配骨骼的顶点权重/索引)// mesh.skeleton = skeleton;// 这部分(设置权重)很复杂,通常在建模软件中完成。