The BVH loader provides support for BVH (Biovision Hierarchy) motion capture files used for skeletal animation. It creates Babylon.js skeletons with bone hierarchies and animation data from motion capture recordings.
Main loader class for BVH motion capture files.
/**
* BVH file loader for importing motion capture data as Babylon.js skeletons
*/
class BVHFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPluginFactory {
// Core properties
readonly name: string; // "bvh"
readonly extensions: { [key: string]: { isBinary: boolean } }; // { ".bvh": { isBinary: false } }
/**
* Create BVH loader with optional loading configuration
* @param loadingOptions - Optional BVH loading configuration
*/
constructor(loadingOptions?: Partial<Readonly<BVHLoadingOptions>>);
/**
* Create a new loader plugin instance
* @param options - Scene loader plugin options
* @returns New loader plugin instance
*/
createPlugin(options: SceneLoaderPluginOptions): ISceneLoaderPluginAsync;
/**
* Check if data can be loaded directly as BVH
* @param data - File data to check
* @returns True if data appears to be BVH format
*/
canDirectLoad(data: string): boolean;
/**
* Check if text contains BVH header
* @param text - Text content to check
* @returns True if text starts with BVH header
*/
isBvhHeader(text: string): boolean;
/**
* Check if text does not contain BVH header
* @param text - Text content to check
* @returns True if text does not start with BVH header
*/
isNotBvhHeader(text: string): boolean;
/**
* Import BVH data as mesh (creates skeleton)
* @param meshesNames - Names of meshes to import (ignored for BVH)
* @param scene - Target scene
* @param data - BVH file data as string
* @param rootUrl - Root URL for the file
* @param onProgress - Progress callback
* @param fileName - Name of the BVH file
* @returns Promise resolving to loader result with skeleton
*/
importMeshAsync(
meshesNames: any,
scene: Scene,
data: string,
rootUrl: string,
onProgress?: (event: ISceneLoaderProgressEvent) => void,
fileName?: string
): Promise<ISceneLoaderAsyncResult>;
/**
* Load BVH data into scene
* @param scene - Target scene
* @param data - BVH file data
* @param rootUrl - Root URL for the file
* @param onProgress - Progress callback
* @param fileName - Name of the BVH file
* @returns Promise resolving when load completes
*/
loadAsync(
scene: Scene,
data: string,
rootUrl: string,
onProgress?: (event: ISceneLoaderProgressEvent) => void,
fileName?: string
): Promise<void>;
/**
* Load BVH data into asset container
* @param scene - Target scene
* @param data - BVH file data
* @param rootUrl - Root URL for the file
* @param onProgress - Progress callback
* @param fileName - Name of the BVH file
* @returns Promise resolving to asset container with skeleton
*/
loadAssetContainerAsync(
scene: Scene,
data: string,
rootUrl: string,
onProgress?: (event: ISceneLoaderProgressEvent) => void,
fileName?: string
): Promise<AssetContainer>;
}Core utility function for parsing BVH text data directly into Babylon.js skeletons.
/**
* Parse BVH text data directly into a Babylon.js skeleton
* @param text - BVH file content as string
* @param scene - Target Babylon.js scene
* @param assetContainer - Optional asset container for isolation
* @param loadingOptions - BVH loading configuration options
* @returns Parsed skeleton with bone hierarchy and animations
*/
function ReadBvh(
text: string,
scene: Scene,
assetContainer: Nullable<AssetContainer>,
loadingOptions: BVHLoadingOptions
): Skeleton;Usage Examples:
import { BVHFileLoader } from "@babylonjs/loaders";
import { SceneLoader } from "@babylonjs/core";
// Basic usage with automatic loader selection
const result = await SceneLoader.ImportMeshAsync("", "/motion/", "walking.bvh", scene);
const skeleton = result.skeletons[0];
console.log(`Loaded skeleton: ${skeleton.name}`);
console.log(`Bones: ${skeleton.bones.length}`);
console.log(`Animation groups: ${result.animationGroups.length}`);
// Create configured loader
const loader = new BVHFileLoader({
loopMode: Animation.ANIMATIONLOOPMODE_CYCLE
});
SceneLoader.RegisterPlugin(loader);
const result2 = await SceneLoader.ImportMeshAsync("", "/motion/", "dance.bvh", scene);Direct BVH parsing function for advanced usage.
/**
* Parse BVH text data directly and create a Babylon.js skeleton
* @param text - BVH file content as text
* @param scene - Target scene for the skeleton
* @param assetContainer - Optional asset container for isolated loading
* @param loadingOptions - BVH loading configuration options
* @returns Parsed skeleton with bone hierarchy and animation
*/
function ReadBvh(
text: string,
scene: Scene,
assetContainer: Nullable<AssetContainer>,
loadingOptions: BVHLoadingOptions
): Skeleton;Usage Example:
import { ReadBvh, BVHLoadingOptions } from "@babylonjs/loaders";
// Load BVH text data
const bvhText = await fetch("/motion/capture.bvh").then(r => r.text());
// Parse directly
const options: BVHLoadingOptions = {
loopMode: Animation.ANIMATIONLOOPMODE_CYCLE
};
const skeleton = ReadBvh(bvhText, scene, null, options);
console.log(`Parsed skeleton with ${skeleton.bones.length} bones`);
// Access bone hierarchy
skeleton.bones.forEach((bone, index) => {
console.log(`Bone ${index}: ${bone.name}, Parent: ${bone.getParent()?.name || "None"}`);
});Configuration options for BVH loading behavior.
type BVHLoadingOptions = {
/** Animation loop mode (default: Animation.ANIMATIONLOOPMODE_CYCLE) */
loopMode: number;
};The loader supports the standard BVH file format with the following structure:
Defines the bone hierarchy with joint names, offsets, and channel specifications:
HIERARCHY
ROOT Hips
{
OFFSET 0.00 0.00 0.00
CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
JOINT Chest
{
OFFSET 0.00 5.21 0.00
CHANNELS 3 Zrotation Xrotation Yrotation
...
}
}Contains frame count, frame time, and animation data:
MOTION
Frames: 120
Frame Time: 0.033333
0.00 0.00 0.00 0.00 0.00 0.00 ...
0.05 0.10 0.00 1.23 0.45 0.67 ...
...import { BVHFileLoader, Animation } from "@babylonjs/loaders";
// Configure animation loop behavior
const loader = new BVHFileLoader({
loopMode: Animation.ANIMATIONLOOPMODE_CYCLE
});
const result = await SceneLoader.ImportMeshAsync("", "/motion/", "routine.bvh", scene);
// Access animation groups
const animationGroup = result.animationGroups[0];
if (animationGroup) {
// Control playback
animationGroup.play(true); // Loop animation
animationGroup.setWeightForAllAnimatables(0.8); // Reduce intensity
// Monitor animation events
animationGroup.onAnimationGroupEndObservable.add(() => {
console.log("Animation cycle completed");
});
}import { BVHFileLoader, SkeletonViewer } from "@babylonjs/loaders";
const result = await SceneLoader.ImportMeshAsync("", "/motion/", "walk.bvh", scene);
const skeleton = result.skeletons[0];
// Visualize skeleton bones
const skeletonViewer = new SkeletonViewer(skeleton, scene.meshes[0], scene);
skeletonViewer.isEnabled = true;
// Customize bone display
const skeletonViewerOptions = {
pauseAnimations: false,
returnToRest: false,
computeBonesUsingShaders: true,
useAllBones: true,
displayMode: SkeletonViewer.DISPLAY_LINES,
displayOptions: {
midStep: 0.5,
midStepFactor: 0.8,
sphereBaseSize: 0.15,
sphereScaleUnit: 2,
sphereFactor: 0.85,
spurFollowsChild: true,
showLocalAxes: false,
localAxesSize: 0.075
}
};
const detailedViewer = new SkeletonViewer(skeleton, scene.meshes[0], scene, false, 3, skeletonViewerOptions);Load BVH files into isolated containers:
import { BVHFileLoader } from "@babylonjs/loaders";
const loader = new BVHFileLoader();
const bvhData = await fetch("/motion/performance.bvh").then(r => r.text());
const assetContainer = await loader.loadAssetContainerAsync(scene, bvhData, "/motion/");
console.log(`Loaded ${assetContainer.skeletons.length} skeletons`);
console.log(`Loaded ${assetContainer.animationGroups.length} animation groups`);
// Skeletons are loaded but not active in scene
// Activate when ready
const skeleton = assetContainer.skeletons[0];
const animationGroup = assetContainer.animationGroups[0];
// Add to scene and start animation
assetContainer.addAllToScene();
animationGroup?.play(true);import { BVHFileLoader } from "@babylonjs/loaders";
// Load character mesh with existing skeleton
const meshResult = await SceneLoader.ImportMeshAsync("", "/characters/", "character.glb", scene);
const characterMesh = meshResult.meshes[0];
const characterSkeleton = meshResult.skeletons[0];
// Load BVH motion data
const motionResult = await SceneLoader.ImportMeshAsync("", "/motion/", "run.bvh", scene);
const motionSkeleton = motionResult.skeletons[0];
const motionAnimation = motionResult.animationGroups[0];
// Map motion to character skeleton (requires bone name matching)
// This is a simplified example - real usage may need custom bone mapping
if (characterSkeleton.bones.length === motionSkeleton.bones.length) {
// Transfer animation data
motionAnimation?.setWeightForAllAnimatables(1.0);
scene.beginAnimation(characterSkeleton, 0, motionAnimation.to, true);
}import { ReadBvh } from "@babylonjs/loaders";
const bvhText = await fetch("/motion/data.bvh").then(r => r.text());
// BVH files contain frame time information
// Extract frame rate from BVH header
const frameTimeMatch = bvhText.match(/Frame Time:\s*([0-9.]+)/);
const frameTime = frameTimeMatch ? parseFloat(frameTimeMatch[1]) : 0.033333;
const frameRate = 1.0 / frameTime;
console.log(`BVH frame rate: ${frameRate} fps`);
// Load with custom loop mode
const skeleton = ReadBvh(bvhText, scene, null, {
loopMode: Animation.ANIMATIONLOOPMODE_RELATIVE
});import "@babylonjs/loaders/BVH";
import { SceneLoader, SkeletonViewer } from "@babylonjs/core";
// Load and preview motion capture data
const result = await SceneLoader.ImportMeshAsync("", "/mocap/", "session01.bvh", scene);
const skeleton = result.skeletons[0];
const animation = result.animationGroups[0];
// Visualize skeleton
const viewer = new SkeletonViewer(skeleton, scene.createGround("ground", 10, 10), scene);
viewer.isEnabled = true;
// Play animation
animation?.play(true);
console.log(`Motion: ${animation?.name}, Duration: ${animation?.to - animation?.from} frames`);import { BVHFileLoader } from "@babylonjs/loaders";
// Load multiple BVH files for animation library
const motionFiles = ["walk.bvh", "run.bvh", "jump.bvh", "idle.bvh"];
const motionLibrary: { [name: string]: { skeleton: Skeleton, animation: AnimationGroup } } = {};
for (const filename of motionFiles) {
const result = await SceneLoader.ImportMeshAsync("", "/motion/", filename, scene);
const motionName = filename.replace(".bvh", "");
motionLibrary[motionName] = {
skeleton: result.skeletons[0],
animation: result.animationGroups[0]
};
}
console.log(`Loaded ${Object.keys(motionLibrary).length} motion capture clips`);
// Use in animation system
function playMotion(motionName: string, targetSkeleton: Skeleton) {
const motion = motionLibrary[motionName];
if (motion) {
// Apply motion to target skeleton (requires bone mapping implementation)
motion.animation.play(true);
}
}