Skip to content

BabylonJS - 骨骼

骨骼动画(Skeletal animation)是用于动画化复杂角色和 3D 图形对象的基本技术。它涉及定义一个由“骨骼”(变换节点,transform nodes)组成的内部层级结构,形成一个“骨架”(skeleton)。然后,可见的几何体(网格“皮肤”,mesh ‘skin’)被附着或“蒙皮”(skinned)到这些骨骼上。当骨骼移动或旋转时,网格会相应地变形,从而实现角色逼真的运动,例如行走、奔跑或挥手。

Babylon.js 提供了 Bone 和 Skeleton 类,以及相关的动画功能。虽然你可以使用这些类通过编程方式创建骨架和骨骼,但在 3D 建模软件(如 Blender、3ds Max、Maya)中创建它们并与网格和动画一起导出(通常使用 glTF 格式,.glb 或 .gltf)更为常见和实用。

当你加载包含骨骼动画的模型文件(如 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();

一个常见的用例是将配件或装备(如帽子、剑或手电筒)附加到动画角色的特定骨骼上,以便配件能随着角色的动画正确移动。

你可以使用 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 模型加载正确,示例将展示一个站在地面平面上的动画角色。一个小红球将附加到角色的右手上,一个小蓝方块将附加到其头部。当角色执行其闲置(或第一个可用的)动画时,红球和蓝方块将随相应的骨骼刚性移动。

虽然对于复杂角色来说不太常见,但你可以通过编程方式定义骨骼和骨架:

// 创建一个骨架
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;
// 这部分(设置权重)很复杂,通常在建模软件中完成。