@babylonjs/loaders
Version:
For usage documentation please visit https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes/.
329 lines • 11.7 kB
JavaScript
import { Animation } from "@babylonjs/core/Animations/animation.js";
import { Bone } from "@babylonjs/core/Bones/bone.js";
import { Skeleton } from "@babylonjs/core/Bones/skeleton.js";
import { Matrix, Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector.js";
import { Tools } from "@babylonjs/core/Misc/tools.js";
const _XPosition = "Xposition";
const _YPosition = "Yposition";
const _ZPosition = "Zposition";
const _XRotation = "Xrotation";
const _YRotation = "Yrotation";
const _ZRotation = "Zrotation";
const _HierarchyNode = "HIERARCHY";
const _MotionNode = "MOTION";
class LoaderContext {
constructor(skeleton) {
this.loopMode = Animation.ANIMATIONLOOPMODE_CYCLE;
this.list = [];
this.root = CreateBVHNode();
this.numFrames = 0;
this.frameRate = 0;
this.skeleton = skeleton;
}
}
function CreateBVHNode() {
return {
name: "",
type: "",
offset: new Vector3(),
channels: [],
children: [],
frames: [],
parent: null,
};
}
function CreateBVHKeyFrame() {
return {
frame: 0,
position: new Vector3(),
rotation: new Quaternion(),
};
}
/**
* Converts the BVH node's offset to a Babylon matrix
* @param node - The BVH node to convert
* @returns The converted matrix
*/
function BoneOffset(node) {
const x = node.offset.x;
const y = node.offset.y;
const z = node.offset.z;
return Matrix.Translation(x, y, z);
}
/**
* Creates animations for the BVH node
* @param node - The BVH node to create animations for
* @param context - The loader context
* @returns The created animations
*/
function CreateAnimations(node, context) {
if (node.frames.length === 0) {
return [];
}
const animations = [];
// Create position animation if there are position channels
const hasPosition = node.channels.some((c) => c === _XPosition || c === _YPosition || c === _ZPosition);
// Create rotation animation if there are rotation channels
const hasRotation = node.channels.some((c) => c === _XRotation || c === _YRotation || c === _ZRotation);
const posAnim = new Animation(`${node.name}_pos`, "position", context.frameRate, Animation.ANIMATIONTYPE_VECTOR3, context.loopMode);
const rotAnim = new Animation(`${node.name}_rot`, "rotationQuaternion", context.frameRate, Animation.ANIMATIONTYPE_QUATERNION, context.loopMode);
const posKeys = [];
const rotKeys = [];
for (let i = 0; i < node.frames.length; i++) {
const frame = node.frames[i];
if (hasPosition && frame.position) {
posKeys.push({
frame: frame.frame,
value: frame.position.clone(),
});
}
if (hasRotation) {
rotKeys.push({
frame: frame.frame,
value: frame.rotation.clone(),
});
}
}
if (posKeys.length > 0) {
posAnim.setKeys(posKeys);
animations.push(posAnim);
}
if (rotKeys.length > 0) {
rotAnim.setKeys(rotKeys);
animations.push(rotAnim);
}
return animations;
}
/**
* Converts a BVH node to a Babylon bone
* @param node - The BVH node to convert
* @param parent - The parent bone
* @param context - The loader context
*/
function ConvertNode(node, parent, context) {
const matrix = BoneOffset(node);
const bone = new Bone(node.name, context.skeleton, parent, matrix);
// Create animation for this bone
const animations = CreateAnimations(node, context);
for (const animation of animations) {
if (animation.getKeys() && animation.getKeys().length > 0) {
bone.animations.push(animation);
}
}
for (const child of node.children) {
ConvertNode(child, bone, context);
}
}
/**
* Recursively reads data from a single frame into the bone hierarchy.
* The bone hierarchy has to be structured in the same order as the BVH file.
* keyframe data is stored in bone.frames.
* @param data - splitted string array (frame values), values are shift()ed
* @param frameNumber - playback time for this keyframe
* @param bone - the bone to read frame data from
* @param tokenIndex - the index of the token to read
*/
function ReadFrameData(data, frameNumber, bone, tokenIndex) {
if (bone.type === "ENDSITE") {
// end sites have no motion data
return;
}
// add keyframe
const keyframe = CreateBVHKeyFrame();
keyframe.frame = frameNumber;
keyframe.position = new Vector3();
keyframe.rotation = new Quaternion();
bone.frames.push(keyframe);
let combinedRotation = Matrix.Identity();
// parse values for each channel in node
for (let i = 0; i < bone.channels.length; ++i) {
const channel = bone.channels[i];
const value = data[tokenIndex.i++];
if (!value) {
continue;
}
const parsedValue = parseFloat(value.trim());
if (channel.endsWith("position")) {
switch (channel) {
case _XPosition:
keyframe.position.x = parsedValue;
break;
case _YPosition:
keyframe.position.y = parsedValue;
break;
case _ZPosition:
keyframe.position.z = parsedValue;
break;
}
}
else if (channel.endsWith("rotation")) {
const angle = Tools.ToRadians(parsedValue);
let rotationMatrix;
switch (channel) {
case _XRotation:
rotationMatrix = Matrix.RotationX(angle);
break;
case _YRotation:
rotationMatrix = Matrix.RotationY(angle);
break;
case _ZRotation:
rotationMatrix = Matrix.RotationZ(angle);
break;
}
combinedRotation = rotationMatrix.multiply(combinedRotation);
}
}
Quaternion.FromRotationMatrixToRef(combinedRotation, keyframe.rotation);
// parse child nodes
for (const child of bone.children) {
ReadFrameData(data, frameNumber, child, tokenIndex);
}
}
/**
* Recursively parses the HIERARCHY section of the BVH file
* @param lines - all lines of the file. lines are consumed as we go along
* @param firstLine - line containing the node type and name e.g. "JOINT hip"
* @param parent - the parent node for hierarchy
* @param context - the loader context containing the list of nodes and other data
* @returns a BVH node including children
*/
function ReadNode(lines, firstLine, parent, context) {
const node = CreateBVHNode();
node.parent = parent;
context.list.push(node);
// parse node type and name.
let tokens = firstLine.trim().split(/\s+/);
if (tokens[0].toUpperCase() === "END" && tokens[1].toUpperCase() === "SITE") {
node.type = "ENDSITE";
node.name = "ENDSITE"; // bvh end sites have no name
}
else {
node.name = tokens[1];
node.type = tokens[0].toUpperCase();
}
// opening bracket
if (lines.shift()?.trim() != "{") {
throw new Error("Expected opening { after type & name");
}
// parse OFFSET
const tokensSplit = lines.shift()?.trim().split(/\s+/);
if (!tokensSplit) {
throw new Error("Unexpected end of file: missing OFFSET");
}
tokens = tokensSplit;
if (tokens[0].toUpperCase() != "OFFSET") {
throw new Error("Expected OFFSET, but got: " + tokens[0]);
}
if (tokens.length != 4) {
throw new Error("OFFSET: Invalid number of values");
}
const offset = new Vector3(parseFloat(tokens[1]), parseFloat(tokens[2]), parseFloat(tokens[3]));
if (isNaN(offset.x) || isNaN(offset.y) || isNaN(offset.z)) {
throw new Error("OFFSET: Invalid values");
}
node.offset = offset;
// parse CHANNELS definitions
if (node.type != "ENDSITE") {
tokens = lines.shift()?.trim().split(/\s+/);
if (!tokens) {
throw new Error("Unexpected end of file: missing CHANNELS");
}
if (tokens[0].toUpperCase() != "CHANNELS") {
throw new Error("Expected CHANNELS definition");
}
const numChannels = parseInt(tokens[1]);
// Skip CHANNELS and the number of channels
node.channels = tokens.splice(2, numChannels);
node.children = [];
}
// read children
while (lines.length > 0) {
const line = lines.shift()?.trim();
if (line === "}") {
// Finish reading the node
return node;
}
else if (line) {
node.children.push(ReadNode(lines, line, node, context));
}
}
throw new Error("Unexpected end of file: missing closing brace");
}
/**
* Reads a BVH file, returns a skeleton
* @param text - The BVH file content
* @param scene - The scene to add the skeleton to
* @param assetContainer - The asset container to add the skeleton to
* @param loadingOptions - The loading options
* @returns The skeleton
*/
export function ReadBvh(text, scene, assetContainer, loadingOptions) {
const lines = text.split("\n");
const { loopMode } = loadingOptions;
scene._blockEntityCollection = !!assetContainer;
const skeleton = new Skeleton("", "", scene);
skeleton._parentContainer = assetContainer;
scene._blockEntityCollection = false;
const context = new LoaderContext(skeleton);
context.loopMode = loopMode;
// read model structure
const firstLine = lines.shift();
if (!firstLine || firstLine.trim().toUpperCase() !== _HierarchyNode) {
throw new Error("HIERARCHY expected");
}
const nodeLine = lines.shift();
if (!nodeLine) {
throw new Error("Unexpected end of file after HIERARCHY");
}
const root = ReadNode(lines, nodeLine.trim(), null, context);
// read motion data
const motionLine = lines.shift();
if (!motionLine || motionLine.trim().toUpperCase() !== _MotionNode) {
throw new Error("MOTION expected");
}
const framesLine = lines.shift();
if (!framesLine) {
throw new Error("Unexpected end of file before frame count");
}
const framesTokens = framesLine.trim().split(/[\s]+/);
if (framesTokens.length < 2) {
throw new Error("Invalid frame count line");
}
// number of frames
const numFrames = parseInt(framesTokens[1]);
if (isNaN(numFrames)) {
throw new Error("Failed to read number of frames.");
}
context.numFrames = numFrames;
// frame time
const frameTimeLine = lines.shift();
if (!frameTimeLine) {
throw new Error("Unexpected end of file before frame time");
}
const frameTimeTokens = frameTimeLine.trim().split(/[\s]+/);
if (frameTimeTokens.length < 3) {
throw new Error("Invalid frame time line");
}
const frameTime = parseFloat(frameTimeTokens[2]);
if (isNaN(frameTime)) {
throw new Error("Failed to read frame time.");
}
if (frameTime <= 0) {
throw new Error("Failed to read frame time. Invalid value " + frameTime);
}
context.frameRate = 1 / frameTime;
// read frame data line by line
for (let i = 0; i < numFrames; ++i) {
const frameLine = lines.shift();
if (!frameLine) {
continue;
}
const tokens = frameLine.trim().split(/[\s]+/) || [];
ReadFrameData(tokens, i, root, { i: 0 });
}
context.root = root;
ConvertNode(context.root, null, context);
context.skeleton.returnToRest();
return context.skeleton;
}
//# sourceMappingURL=bvhLoader.js.map