Tutorial

Parsing BVH in TypeScript

12 min read

Parsing BVH in TypeScript

It’s 2 AM, your game demo is in nine hours, and your hero’s left arm is doing the macarena during every walk cycle. You’re wrestling with a Mixamo animation, trying to get that beautiful 3D motion capture data to play nice with your 2D character rig. The culprit is often a finicky BVH file parser, specifically when working in TypeScript.

Many indie devs hit this exact wall, feeling like they need a PhD in computer graphics just to get a character to wave. But it doesn't have to be this way. We’re going to break down parsing BVH in TypeScript step-by-step, focusing on the practical solutions that actually survive your game’s second build, not just the first compile.

1.Why BVH files are both a blessing and a curse for indie devs

The Biovision Hierarchy (BVH) format is a plain-text file that describes skeletal animation. It’s been around since the 90s, making it a venerable standard for motion capture data. Its simplicity is its biggest strength and its greatest weakness.

Illustration for "Why BVH files are both a blessing and a curse for indie devs"
Why BVH files are both a blessing and a curse for indie devs

However, BVH files come with a lot of implicit assumptions about coordinate systems, joint hierarchies, and rotation orders. These hidden details are what often lead to frustrating bugs where limbs twist unnaturally or characters float inexplicably. Understanding these underlying assumptions is key to a robust parser.

a.The two halves of every BVH file

Every BVH file is cleanly split into two main sections: the HIERARCHY and the MOTION data. The hierarchy defines the skeleton's structure, including joint names, their parent-child relationships, and their initial offsets. This skeletal definition is the blueprint for your character's movement.

The motion section contains the actual animation data. It’s a series of frames, each specifying the translation and rotation values for each joint. These values are applied sequentially to animate the skeleton. Frame rate and the total number of frames are also declared here, giving you the animation's timing.

2.Building your BVH data structure in TypeScript

Before we parse anything, we need a TypeScript data structure to hold the parsed BVH data. Think of it as mapping the text file's structure directly into your code. We'll need classes or interfaces for the overall BVH, individual joints, and motion frames. A clear data model prevents later headaches.

Illustration for "Building your BVH data structure in TypeScript"
Building your BVH data structure in TypeScript
  • `BvhFile`: Holds the root joint, frame count, frame time, and motion data.
  • `BvhJoint`: Stores its name, offset, channels (e.g., Xposition, Yrotation), parent, and children.
  • `BvhFrame`: An array of floating-point numbers, representing all channel values for a single frame.

a.Representing the joint hierarchy

Each `BvhJoint` needs to know its place in the skeletal tree. A `parent` property (nullable for the root) and an array of `children` `BvhJoint` instances are crucial. The `offset` property, a `Vector3`, defines the joint's position relative to its parent in the bind pose. This relative positioning is fundamental to skeletal animation.

The `channels` array in `BvhJoint` is also critical. It tells you which transformation properties (Xposition, Yrotation, Zrotation, etc.) apply to this specific joint, and in what order. This order, especially for rotations, is a major source of bugs if not handled correctly during parsing and application.

3.The parsing workflow: Step-by-step BVH ingestion

Parsing a BVH file is essentially reading text line by line and building your data structures. It involves a state machine or recursive descent approach to handle the nested hierarchy. Don't try to parse everything at once; break it down into logical steps. A methodical approach prevents missing crucial details.

Illustration for "The parsing workflow: Step-by-step BVH ingestion"
The parsing workflow: Step-by-step BVH ingestion
  1. 1Read the HIERARCHY keyword: This signals the start of the skeleton definition.
  2. 2Parse the ROOT joint: Create the initial `BvhJoint` object.
  3. 3Recursively parse child joints: Handle `JOINT` and `END SITE` blocks, building the parent-child relationships.
  4. 4Read the MOTION keyword: Transition to parsing animation data.
  5. 5Extract `Frames` and `Frame Time`: Store these global animation properties.
  6. 6Parse each motion frame: Read the numerical values for each channel in each frame.

a.Parsing the hierarchy: A recursive journey

The hierarchy section is tree-like. When you encounter a `ROOT` or `JOINT` keyword, you create a new `BvhJoint` and add it as a child to the current parent. Keywords like `OFFSET` and `CHANNELS` provide essential properties for that joint. The `END SITE` keyword signifies a leaf joint, which has an offset but no further children.

A stack-based parser is often the easiest way to manage the nesting. Push the current joint onto a stack when you descend into a child, and pop it when you encounter a closing brace `}`. This ensures you always know which joint you're currently defining. This approach also works well for building a music video with mocap and 2D rigs where structure matters.

b.Parsing the motion data: Numbers, numbers, numbers

Once the `MOTION` keyword is hit, the parsing becomes more straightforward. Read the `Frames:` line to get the total frame count, and `Frame Time:` for the animation's duration per frame. Then, for each subsequent line, split it by whitespace to get the individual channel values. Each line represents one complete frame of animation data.

It's crucial to know the exact order of channels defined in the hierarchy. If a joint has `Xposition Yrotation Zrotation`, the first three numbers in the motion data for that joint will correspond to those specific channels. Mismatching this order is a classic source of animation glitches. This is a common pitfall in platformer character animation: a complete 2D guide when integrating external data.

4.The gotchas: Why your character's arm is orbiting Jupiter

Even with a seemingly perfect parser, BVH files are notorious for their subtle traps. These aren't syntax errors; they're semantic interpretations that vary between different software. Ignoring these nuances leads to bizarre, broken animations.

Illustration for "The gotchas: Why your character's arm is orbiting Jupiter"
The gotchas: Why your character's arm is orbiting Jupiter
  • Coordinate System Mismatches: BVH uses Y-up, Z-forward, but your engine might use Y-up, X-forward or Z-up. Rotations will be completely wrong without conversion.
  • Euler Rotation Order: BVH defines rotations as X, Y, Z, but the *order* in which these are applied matters (e.g., XYZ vs ZYX). Most BVH files assume ZXY, but it's not always explicit.
  • Joint Offsets vs. Positions: Offsets are relative to the parent. Motion data provides *absolute* translations for the root, but *relative* rotations for all joints. Don't confuse them.
  • Degrees vs. Radians: BVH rotations are almost always in degrees. Your math library likely expects radians. Convert early and often.
Trying to perfectly match a 3D BVH rig to a 2D character is often a waste of time; focus on the major joints and accept some visual compromise. Perfect 1:1 mapping is an illusion in 2D.

a.Tackling coordinate system conversions

This is arguably the biggest headache. BVH typically describes a character standing with arms outstretched, facing along the Z-axis, with Y as up. If your game engine (like Unity or Godot) uses a different convention, you need to transform every single joint's offset and rotation. Often, a root rotation of -90 degrees around the X-axis is a good starting point for Y-up to Z-up conversions.

For 2D, you're usually only concerned with X and Y positions, and Z rotation (rotation around the depth axis). You might even need to discard or average the other two rotation axes to get a natural 2D movement. This is a design decision, not a parsing bug. You're adapting 3D data to a 2D plane.

b.Handling Euler order for smooth rotations

Euler angles are prone to gimbal lock, where two axes align, losing a degree of rotational freedom. The order in which you apply X, Y, and Z rotations matters immensely. BVH files generally follow a ZXY order for rotations. If your animation looks like it's flipping wildly, check your Euler order.

In TypeScript, when converting Euler angles to quaternions (which are far more stable for interpolation), you'll need to specify the correct Euler order. Most math libraries will have an option for this. If you don't use quaternions, ensure your direct Euler applications respect the BVH's implicit order for each joint's channels.

5.Applying BVH data to your 2D character rig

This is where the magic happens, and also where the biggest compromises are made when going from 3D to 2D. You've parsed the data; now you need to map it to your 2D layered PNG character in Charios. The goal isn't perfect replication, but believable motion.

Illustration for "Applying BVH data to your 2D character rig"
Applying BVH data to your 2D character rig
  1. 1Identify key BVH joints: Focus on the hips, spine, shoulders, elbows, knees. Ignore tiny finger joints unless your character has detailed hands.
  2. 2Map BVH joints to 2D rig bones: Assign each relevant BVH joint to a corresponding bone in your 2D skeleton. Some BVH joints might not have a direct 2D equivalent.
  3. 3Calculate 2D position: For the root, use the BVH X and Y position. For other joints, use the relative rotation and apply it to your 2D bone's parent.
  4. 4Apply 2D rotation: Use the Z-axis rotation from the BVH data directly. For X or Y rotations, you might project them onto the Z-axis or simply ignore them if they don't contribute meaningfully to 2D perspective.
  5. 5Handle scaling (optional): BVH doesn't typically include scale, but you might apply uniform scale to the entire animation if needed.

a.The art of selective mapping for 2D

Your 2D rig likely has fewer bones than a typical 3D BVH skeleton. This is an advantage. Instead of trying to map every single joint, focus on the major contributors to the silhouette and primary movement. A character’s arm in 2D might only have an upper arm and forearm bone, while a 3D BVH might include a wrist and multiple finger joints. Simplification is your friend here.

For example, when retargeting Mixamo data, the BVH will have complex shoulder and hip rotations. For a 2D character, you might only need to apply the Z-rotation to your character’s shoulder bone and let the sprite art handle the rest of the visual nuance. This is how you achieve VTuber head-yaw from webcam with fewer bones.

6.Performance considerations for real-time applications

Parsing BVH is usually a one-time cost at load time. The real performance challenge comes from applying the motion data every frame. You're reading a lot of numbers and performing matrix math. Optimizing your animation loop is crucial for smooth gameplay.

Illustration for "Performance considerations for real-time applications"
Performance considerations for real-time applications
  • Pre-calculate matrices: If your engine uses matrices, pre-calculate and store the local transformation matrices for each joint for each frame. Don't re-calculate them every frame.
  • Interpolate sparingly: Only interpolate between frames if your frame rate is lower than the BVH animation's frame rate. Otherwise, direct application is faster.
  • Use typed arrays: Float32Array for motion data is more memory-efficient and often faster than generic arrays in JavaScript/TypeScript.
  • Avoid unnecessary DOM/Canvas updates: Only update what's necessary on screen. Don't redraw the entire character if only a hand moved.

a.Efficient BVH data storage

Once parsed, store your BVH data efficiently. Instead of an array of objects for frames, consider a flat `Float32Array` where each frame's data is contiguous. You can then use offsets to access specific joint channels. This reduces memory overhead and improves cache locality, leading to faster access.

Your `BvhJoint` objects should primarily store the hierarchy and static offsets. The dynamic motion data should be separate and optimized for quick lookup. For a deep dive into the format itself, check out our BVH file format deep dive.

7.A practical BVH parsing snippet for your game

Let’s look at a simplified conceptual example of how a BVH parser might begin. This isn't production-ready code, but it illustrates the core logic for handling the hierarchy. It demonstrates how to process keywords and build the joint tree.

Illustration for "A practical BVH parsing snippet for your game"
A practical BVH parsing snippet for your game

Conceptual Code Snippet:

`function parseBvh(bvhText: string): BvhFile {`

`const lines = bvhText.split('\n').map(line => line.trim()).filter(line => line.length > 0);`

`let currentLineIndex = 0;`

`const rootJoint = parseJoint(lines, currentLineIndex, null); // Start recursive parsing`

`// ... then parse motion data ...`

`return { rootJoint, frameCount, frameTime, motionData };`

`}`

`function parseJoint(lines: string[], index: number, parent: BvhJoint | null): BvhJoint {`

`const line = lines[index++];`

`const parts = line.split(' ');`

`const jointName = parts[1];`

`const joint: BvhJoint = { name: jointName, offset: new Vector3(), channels: [], children: [], parent };`

`// Expect '{', then parse OFFSET, CHANNELS`

`while (lines[index] !== '}') {`

`const subLine = lines[index];`

`if (subLine.startsWith('OFFSET')) {`

`joint.offset = parseOffset(subLine);`

`} else if (subLine.startsWith('CHANNELS')) {`

`joint.channels = parseChannels(subLine);`

`} else if (subLine.startsWith('JOINT') || subLine.startsWith('END SITE')) {`

`joint.children.push(parseJoint(lines, index, joint)); // Recursive call`

`}`

`index++;`

`}`

`return joint;`

`}`

This snippet highlights the recursive nature of parsing the hierarchy. You start at the root, then call `parseJoint` for each child, building the tree structure along the way. The actual parsing functions for `OFFSET` and `CHANNELS` would involve splitting the line and converting strings to numbers. This recursive pattern is fundamental to handling nested data structures.

8.Beyond parsing: What to do with your mocap data

Once you have your BVH data parsed and structured, you're ready to bring your 2D characters to life. This data opens up a world of possibilities, from quick walk cycles to complex combat animations. Don't let the parsing challenge overshadow the creative potential.

Illustration for "Beyond parsing: What to do with your mocap data"
Beyond parsing: What to do with your mocap data
  • Retargeting: Apply the motion to different 2D character rigs, even if they have varying proportions. Charios handles this by snapping to a fixed skeleton.
  • Procedural Animation: Blend mocap data with procedural effects for dynamic interactions.
  • Real-time IK/FK: Use the parsed data as input for inverse kinematics or forward kinematics solvers.
  • Animation Blending: Combine different BVH animations (e.g., a walk cycle with a wave emote) for seamless transitions.

The ability to ingest and adapt external motion data is a huge time-saver for solo and small teams. It means you don't have to hand-animate every single frame for every character. Mocap can be a powerful accelerator for your animation pipeline.

9.The future of 2D mocap for indie games

The gap between affordable 3D mocap (like Rokoko suits or even iPhone apps) and accessible 2D animation tools is closing. Parsing BVH is a key bridge. It means you can leverage a vast library of existing animations and even create your own with relatively inexpensive hardware. This democratizes high-quality animation for everyone.

Illustration for "The future of 2D mocap for indie games"
The future of 2D mocap for indie games

Forget the days of spending weeks on a single walk cycle. With a solid BVH parser and a tool that can retarget it to your 2D sprites, you can rapidly prototype and iterate. This efficiency is what allows small teams to compete with larger studios, delivering polished animation without breaking the bank or sacrificing their sleep. It's how you make a shrug emote: 2D character in minutes instead of hours.

Parsing BVH in TypeScript might seem like a deep technical dive, but it’s a foundational skill that pays dividends. You'll gain a deeper understanding of animation data and unlock the potential of countless mocap libraries. Don't let the initial complexity deter you from this powerful workflow.

Ready to put this knowledge to the test? Grab a BVH file from Mixamo or the CMU motion capture database and start experimenting. If you're building a 2D game, head over to the Charios dashboard to try importing your own layered PNGs and snapping them to a skeleton. You'll be retargeting mocap to your custom characters in minutes.

Charios team

We build a browser-native 2D character animation tool — drop layered PNGs onto a fixed skeleton and retarget Mixamo or BVH mocap onto the rig. Try Charios →

Published May 11, 2026

FAQ

Frequently asked

  • How do I apply 3D BVH motion capture data to a 2D character rig in TypeScript?
    You'll need to parse the BVH hierarchy and motion data, then selectively map the 3D joint rotations to your 2D rig's bones. Focus on the relevant 2D plane (e.g., XY or XZ) and ensure your coordinate systems align. Charios helps by providing a visual interface for this mapping.
  • What are the common pitfalls when parsing BVH files in TypeScript for game development?
    The biggest issues are often incorrect coordinate system conversions, especially between Y-up and Z-up conventions, and misinterpreting Euler rotation orders. These lead to distorted or "exploding" character animations. Thorough validation of parsing logic and visual debugging are essential.
  • Why is coordinate system conversion so crucial when using BVH data with 2D animations?
    BVH files typically use a 3D coordinate system (often Y-up), while 2D animation often operates on a 2D plane (e.g., Y-down screen coordinates). Incorrectly converting between these can cause your character's limbs to rotate on the wrong axis or appear flipped. You must define your target 2D plane and project the 3D data accordingly.
  • Can I use Mixamo BVH animations directly with a 2D character in a game engine like Unity or Godot?
    Yes, but it requires custom parsing and retargeting logic. Mixamo provides 3D BVH data, which needs to be interpreted and then mapped to your 2D character's bone structure. Tools like Charios are designed to bridge this gap, allowing you to drop Mixamo BVH directly onto 2D rigs and export for Unity or other engines.
  • Does Charios simplify the process of retargeting Mixamo BVH data onto 2D layered PNG character rigs?
    Absolutely. Charios is specifically built to streamline this. You can import layered PNGs, snap them to a skeleton, and then retarget BVH or Mixamo mocap data directly onto that 2D rig with visual feedback. It handles the underlying parsing and coordinate system adjustments for you.
  • How can I handle Euler rotation order issues when interpreting BVH motion data?
    BVH files specify rotations as Euler angles, but the order in which these rotations are applied (e.g., XYZ, YXZ) is critical and often implicit or varies. If your parser assumes the wrong order, rotations will look incorrect or "gimbal lock" can occur. You may need to experiment or consult the BVH file's specific header if available, or convert to quaternions early.
  • What are the performance considerations for using BVH data in real-time 2D applications?
    Parsing BVH is usually a one-time load, but applying the motion data every frame can be intensive. Efficient storage of the parsed motion data (e.g., pre-calculated matrices or optimized arrays) and minimizing redundant calculations are key. Consider pre-baking animations for less dynamic scenarios to improve runtime performance.

Related