@babylonjs/viewer
Version:
The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.
489 lines (484 loc) • 17.7 kB
JavaScript
import { aR as Animation, V as Vector3, M as Matrix, aY as Tools, aD as Quaternion, bG as BVHFileLoaderMetadata, bn as RegisterSceneLoaderPlugin } from './index-MZPybX0H.esm.js';
import { A as AssetContainer } from './assetContainer-Bm0vsreJ.esm.js';
import './animation-BfKed86k.esm.js';
import { B as Bone } from './bone.pure-CjsCww39.esm.js';
import { S as Skeleton } from './skeleton-D7Sw58cv.esm.js';
import './tools-DC6rsfz_.esm.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
*/
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;
}
/**
* @experimental
* BVH file type loader.
* This is a babylon scene loader plugin.
*/
class BVHFileLoader {
/**
* Creates loader for bvh motion files
* @param loadingOptions - Options for the bvh loader
*/
constructor(loadingOptions) {
/**
* Name of the loader ("bvh")
*/
this.name = BVHFileLoaderMetadata.name;
/** @internal */
this.extensions = BVHFileLoaderMetadata.extensions;
this._loadingOptions = { ...BVHFileLoader._DefaultLoadingOptions, ...(loadingOptions ?? {}) };
}
static get _DefaultLoadingOptions() {
return {
loopMode: Animation.ANIMATIONLOOPMODE_CYCLE,
};
}
/** @internal */
createPlugin(options) {
return new BVHFileLoader(options[BVHFileLoaderMetadata.name]);
}
/**
* If the data string can be loaded directly.
* @param data - direct load data
* @returns if the data can be loaded directly
*/
canDirectLoad(data) {
return this.isBvhHeader(data);
}
/**
* Returns whether the provided text starts with a BVH HIERARCHY header.
* @param text - the text to inspect
* @returns true if the text is a BVH header
*/
isBvhHeader(text) {
return text.split("\n")[0] == "HIERARCHY";
}
/**
* Returns whether the provided text does not start with a BVH HIERARCHY header.
* @param text - the text to inspect
* @returns true if the text is not a BVH header
*/
isNotBvhHeader(text) {
return !this.isBvhHeader(text);
}
/**
* Imports from the loaded gaussian splatting data and adds them to the scene
* @param _meshesNames a string or array of strings of the mesh names that should be loaded from the file
* @param scene the scene the meshes should be added to
* @param data the bvh data to load
* @returns a promise containing the loaded skeletons and animations
*/
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
importMeshAsync(_meshesNames, scene, data) {
if (typeof data !== "string") {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject("BVH loader expects string data.");
}
if (this.isNotBvhHeader(data)) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject("BVH loader expects HIERARCHY header.");
}
try {
const skeleton = ReadBvh(data, scene, null, this._loadingOptions);
return Promise.resolve({
meshes: [],
particleSystems: [],
skeletons: [skeleton],
animationGroups: [],
transformNodes: [],
geometries: [],
lights: [],
spriteManagers: [],
});
}
catch (e) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject(e);
}
}
/**
* Imports all objects from the loaded bvh data and adds them to the scene
* @param scene the scene the objects should be added to
* @param data the bvh data to load
* @returns a promise which completes when objects have been loaded to the scene
*/
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/promise-function-async
loadAsync(scene, data) {
if (typeof data !== "string") {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject("BVH loader expects string data.");
}
if (this.isNotBvhHeader(data)) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject("BVH loader expects HIERARCHY header.");
}
// eslint-disable-next-line github/no-then
return this.importMeshAsync(null, scene, data).then(() => {
// return void
});
}
/**
* Load into an asset container.
* @param scene The scene to load into
* @param data The data to import
* @returns The loaded asset container
*/
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
loadAssetContainerAsync(scene, data) {
if (typeof data !== "string") {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject("BVH loader expects string data.");
}
if (this.isNotBvhHeader(data)) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject("BVH loader expects HIERARCHY header.");
}
const assetContainer = new AssetContainer(scene);
try {
const skeleton = ReadBvh(data, scene, assetContainer, this._loadingOptions);
assetContainer.skeletons.push(skeleton);
return Promise.resolve(assetContainer);
}
catch (e) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject(e);
}
}
}
let _Registered = false;
/**
* Registers the BVHFileLoader scene loader plugin.
* Safe to call multiple times; only the first call has an effect.
*/
function RegisterBVHFileLoader() {
if (_Registered) {
return;
}
_Registered = true;
RegisterSceneLoaderPlugin(new BVHFileLoader());
}
/**
* Re-exports the pure implementation and applies the runtime registration side effect.
* Import "./bvhFileLoader.pure" for tree-shakeable, side-effect-free usage.
*/
RegisterBVHFileLoader();
export { BVHFileLoader, RegisterBVHFileLoader };
//# sourceMappingURL=bvhFileLoader-DT7acuCV.esm.js.map