Skip to content

Instantly share code, notes, and snippets.

@mgerhardy
Created October 30, 2025 16:25
Show Gist options
  • Select an option

  • Save mgerhardy/48adb8a9ccaa2079779ac38a4324fa8e to your computer and use it in GitHub Desktop.

Select an option

Save mgerhardy/48adb8a9ccaa2079779ac38a4324fa8e to your computer and use it in GitHub Desktop.
Blockbench model bbmodel format spec

Blockbench BBModel Format Specification

Overview

The BBModel (.bbmodel) format is a JSON-based 3D model format created by Blockbench. It supports hierarchical scene graphs, multiple element types (cubes, meshes), animations, textures, and various model formats (Java Block, Bedrock, Free).

Current Format Version: 5.0 (with backwards compatibility to 3.2+)


File Structure

{
  "meta": { /* Metadata */ },
  "resolution": { /* Texture resolution */ },
  "elements": [ /* Geometry elements */ ],
  "groups": [ /* Hierarchy groups (v5.0+) */ ],
  "outliner": [ /* Scene hierarchy */ ],
  "textures": [ /* Texture definitions */ ],
  "animations": [ /* Animation data */ ],
  "animation_controllers": [ /* Animation state machines */ ],
  "display": { /* Display transformations */ }
}

1. Metadata (meta)

Defines the format version and model type.

{
  "meta": {
    "format_version": "5.0",      // Format specification version
    "model_format": "free",        // Model type: "free", "java_block", "bedrock", "bedrock_old"
    "box_uv": false,              // UV mapping mode
    "backup": false               // Optional: backup flag
  }
}

Key Fields:

  • format_version: Must be ≤ 5.0 for compatibility
  • model_format: Determines available features and constraints
  • box_uv: true for box UV mapping, false for per-face UV

2. Resolution

Texture dimensions for UV mapping.

{
  "resolution": {
    "width": 64,    // Texture width in pixels
    "height": 64    // Texture height in pixels
  }
}

3. Elements

Elements are the geometric primitives. Two main types: Cube and Mesh.

3.1 Cube Element

Box primitive with 6 faces.

{
  "name": "cube_name",
  "type": "cube",
  "uuid": "c8eb7afc-1e80-c3b9-07e9-0e96b2c28979",
  "from": [-3, 18.9, -1.5],      // Corner position [x, y, z]
  "to": [3, 22.9, 1.5],          // Opposite corner [x, y, z]
  "origin": [0, 10.9, 0],        // Pivot point [x, y, z]
  "rotation": [0, 0, 0],         // Rotation [x, y, z] in degrees
  "color": 0,                    // Color index (0-7)
  "export": true,                // Include in export
  "visibility": true,            // Visibility flag
  "locked": false,               // Lock editing
  "box_uv": false,               // UV mapping mode
  "rescale": false,              // Scale flag
  "autouv": 0,                   // Auto UV generation
  "faces": {                     // Face definitions
    "north": {
      "uv": [8, 16, 14, 20],    // UV coordinates [u_min, v_min, u_max, v_max]
      "texture": 0               // Texture index (or null)
    },
    "east": { /* ... */ },
    "south": { /* ... */ },
    "west": { /* ... */ },
    "up": { /* ... */ },
    "down": { /* ... */ }
  }
}

Coordinate System:

  • Origin: Bottom-left-back corner
  • Axes: X (right), Y (up), Z (forward)
  • Units: Minecraft-style units (1 unit = 1/16 block)

Rotation:

  • Applied around the origin point
  • Order: X → Y → Z (Euler angles)
  • In degrees (not radians)

3.2 Mesh Element

Freeform mesh with arbitrary vertices and faces.

{
  "name": "cuboid",
  "type": "mesh",
  "uuid": "b102ab22-619d-e67b-f6ed-89755675cdad",
  "origin": [44, 0, 0],
  "rotation": [0, 0, 0],
  "color": 3,
  "export": true,
  "visibility": true,
  "locked": false,
  "render_order": "default",
  "allow_mirror_modeling": true,
  "vertices": {                 // Vertex positions
    "s1u1": [8, 8, 8],          // vertex_id: [x, y, z]
    "pzwl": [8, 8, -8],
    "zjiK": [8, 0, 8],
    /* ... more vertices ... */
  },
  "faces": {                    // Face definitions
    "ZgMOQeDR": {               // face_id
      "uv": {                   // Per-vertex UV mapping
        "s1u1": [0, 0],         // vertex_id: [u, v]
        "zjiK": [0, 8],
        "pzwl": [16, 0],
        "7KuF": [16, 8]
      },
      "vertices": [             // Vertex order (CCW for front-facing)
        "s1u1", "zjiK", "pzwl", "7KuF"
      ],
      "texture": 0              // Texture index (or null)
    }
    /* ... more faces ... */
  }
}

Mesh Topology:

  • Vertices: Dictionary of unique IDs → 3D positions
  • Faces: Quads (4 vertices) or triangles (3 vertices)
  • Winding Order: Counter-clockwise for outward-facing normals
  • UV Mapping: Per-vertex, allowing arbitrary UV layouts

4. Groups (v5.0+)

Groups organize elements hierarchically. In v5.0+, groups are stored separately from the outliner.

{
  "name": "arm_right",
  "uuid": "afd758ba-59c8-6090-8f81-628d06cca15c",
  "origin": [4, 21.9, 0],        // Pivot point [x, y, z]
  "rotation": [0, 0, 0],         // Rotation [x, y, z] in degrees
  "color": 0,
  "export": true,
  "mirror_uv": false,
  "isOpen": true,                // Expanded in UI
  "locked": false,
  "visibility": true,
  "autouv": 0,
  "nbt": "{}"                    // NBT data (Minecraft)
}

Note: Groups don't store children directly; hierarchy is in outliner.


5. Outliner

Defines the scene hierarchy tree. Elements can be:

  • String (UUID): Reference to an element
  • Object: Group with nested children
"outliner": [
  "element-uuid-1",              // Direct element reference
  {
    "name": "upper",
    "uuid": "19029bcd-1a1d-f0b9-e7d8-1e4b9cf49e26",
    "origin": [0, 18.9, 0],
    "rotation": [0, 0, 0],
    /* ... group properties ... */
    "children": [
      "element-uuid-2",
      {
        "name": "nested_group",
        "uuid": "b9fe46b0-8468-c9f1-9f7b-6de7fa5d2268",
        /* ... */
        "children": [
          "element-uuid-3"
        ]
      }
    ]
  }
]

Hierarchy Rules:

  1. Root level can contain elements and groups
  2. Groups can nest infinitely
  3. Elements are leaves (no children)
  4. UUIDs must be unique across elements and groups

6. Transformations

6.1 Transform Stack

Transformations accumulate down the hierarchy:

World_Transform = Parent_Transform × Local_Transform

Where Local_Transform:

  1. Translate to origin (pivot point)
  2. Rotate around origin (X → Y → Z order)
  3. Translate back from origin
  4. Position element geometry

6.2 Pivot Points (origin)

  • Purpose: Rotation center for the node
  • Inheritance: Child transforms are relative to parent's pivot
  • Default: [0, 0, 0] if not specified

Example: Rotating an arm

{
  "name": "upper_arm",
  "origin": [4, 20, 0],    // Shoulder position
  "rotation": [0, 0, -30]  // Rotate 30° around Z-axis at shoulder
}

6.3 Element Positioning

For Cubes:

  • from and to define AABB in local space
  • Center = (from + to) / 2

For Meshes:

  • Vertices are in local space
  • Transformed by group hierarchy

6.4 Rotation Order

Blockbench uses Extrinsic XYZ Euler angles:

  1. Rotate around X-axis
  2. Rotate around Y-axis
  3. Rotate around Z-axis

Matrix: R = Rz × Ry × Rx

Version Note: Pre-v3.2 used inverted Z-axis; compatibility layer applies -z correction.


7. Textures

{
  "textures": [
    {
      "uuid": "096a4435-3bfa-4f75-6f44-41e728ea692e",
      "name": "texture_name",
      "id": "0",                    // Texture index
      "path": "/absolute/path.png", // Absolute path
      "relative_path": "textures/skin.png", // Relative path (v4.10+)
      "width": 64,
      "height": 64,
      "uv_width": 64,               // UV coordinate space
      "uv_height": 64,
      "particle": false,            // Particle texture (Minecraft)
      "render_mode": "default",     // "default", "emissive", "layered"
      "render_sides": "auto",       // "auto", "front", "double"
      "internal": true,             // Embedded in file
      "source": "data:image/png;base64,...", // Base64 image data
      "mode": "bitmap",
      "saved": false,
      "visible": true,
      "folder": "block",
      "namespace": ""
    }
  ]
}

Texture Resolution:

  • path/relative_path: External texture file
  • source: Embedded Base64-encoded PNG/JPEG
  • Priority: source > relative_path > path

8. Animations

8.1 Animation Structure

{
  "animations": [
    {
      "uuid": "b1498c3c-f8f0-4983-0d22-155f5b0cc2bf",
      "name": "walk",
      "loop": "loop",              // "once", "loop", "hold"
      "override": false,
      "length": 1.0,               // Duration in seconds
      "snapping": 20,              // Keyframe snap interval
      "selected": false,
      "anim_time_update": "",      // Molang expression
      "blend_weight": "",          // Molang expression
      "start_delay": "",
      "loop_delay": "",
      "animators": {
        "bone-uuid": {             // Animated node UUID
          "name": "leg_right",
          "type": "bone",
          "keyframes": [ /* ... */ ]
        }
      }
    }
  ]
}

8.2 Keyframes

{
  "channel": "rotation",         // "position", "rotation", "scale"
  "uuid": "e41ca207-18ba-c337-6455-12d474c59fc8",
  "time": 0.5,                   // Time in seconds
  "color": -1,                   // UI color
  "interpolation": "linear",     // "linear", "step", "catmullrom", "bezier"
  "data_points": [
    {
      "x": 45.0,                 // Channel-specific value
      "y": 0.0,
      "z": 0.0
    }
  ],
  "bezier_linked": true,
  "bezier_left_time": [-0.1, -0.1, -0.1],
  "bezier_left_value": [0, 0, 0],
  "bezier_right_time": [0.1, 0.1, 0.1],
  "bezier_right_value": [0, 0, 0]
}

Channels:

  • position: Translation in local space (units)
  • rotation: Euler angles (degrees)
  • scale: Scale factors (unitless)

Interpolation:

  • linear: Linear interpolation
  • step: Hold value until next keyframe
  • catmullrom: Smooth spline (overshoots)
  • bezier: Cubic Bezier with custom handles

Data Points:

  • Single value: Continuous keyframe
  • Two values: Discontinuous (instant jump)

8.3 Animation Evaluation

For time t between keyframes kf1 (time t1) and kf2 (time t2):

Linear:

alpha = (t - t1) / (t2 - t1)
value = lerp(kf1.value, kf2.value, alpha)

Bezier (per-component):

Solve: t = cubicBezier(alpha, t1, t1+right_time, t2+left_time, t2)
value = cubicBezier(alpha, v1, v1+right_value, v2+left_value, v2)

Rotation: In v5.0+, rotations use inverted X and Y components for internal consistency (exported correctly).


9. Display Settings (Minecraft)

Item display transformations for Minecraft formats.

{
  "display": {
    "thirdperson_righthand": {
      "rotation": [75, 45, 0],
      "translation": [0, 2.5, 0],
      "scale": [0.375, 0.375, 0.375]
    },
    "firstperson_righthand": { /* ... */ },
    "gui": { /* ... */ },
    "ground": { /* ... */ },
    "fixed": { /* ... */ },
    "head": { /* ... */ }
  }
}

Slots: thirdperson_righthand, thirdperson_lefthand, firstperson_righthand, firstperson_lefthand, gui, ground, fixed, head


10. Collections (v5.0+)

Organize elements into exportable groups.

{
  "collections": [
    {
      "uuid": "collection-uuid",
      "name": "LOD0",
      "children": ["element-uuid-1", "element-uuid-2"],
      "export_path": "/path/to/export.gltf"
    }
  ]
}

11. Version Compatibility

11.1 Version History

The .bbmodel format may be subject to breaking changes. Each file contains a format_version field to identify and handle these changes. This section describes breaking changes only; additions and non-breaking changes are not listed.

Version Release Date Key Changes
5.0 October 12, 2025 (Blockbench 5.0) Groups/outliner separation; keyframe value direction fixed
4.10 May 9, 2024 (Blockbench 4.10) Relative texture paths fixed; name field handling; default texture handling
4.9 December 2, 2023 (Blockbench 4.9) Per-texture UV size (formats with per_texture_uv_size feature)
4.5 Earlier shade attribute replaced by mirror_uv
3.2 Earlier Z-axis rotation sign convention changed

11.2 Breaking Changes Detail

Version 5.0 (October 12, 2025)

Groups and Outliner Separation

Groups and outliner hierarchy are now saved as separate fields:

{
  "groups": [
    {
      "uuid": "b147a4cf-3f58-9e94-8f4c-b7b0657c3c56",
      "name": "group_name",
      "origin": [0, 0, 0],
      /* ... group properties ... */
    }
  ],
  "outliner": [
    {
      "uuid": "0396ecb8-33d8-ca4a-d1d2-d756c50123ae",
      "isOpen": true,
      "children": [
        {
          "uuid": "b147a4cf-3f58-9e94-8f4c-b7b0657c3c56",
          "isOpen": true,
          "children": [
            "36d606d8-1baa-08f0-e4e6-f70f2f2e3657",
            "7594f374-dece-aa7b-e553-4dca3c243b1f"
          ]
        }
      ]
    }
  ]
}

Pre-5.0 format (groups embedded in outliner):

{
  "outliner": [
    {
      "name": "group_name",
      "origin": [0, 0, 0],
      /* ... all group properties inline ... */
      "children": [ /* ... */ ]
    }
  ]
}

Keyframe Value Direction Fixed

Animation keyframe values were previously inverted:

  • Position keyframes: X-axis values were inverted
  • Rotation keyframes: X and Y axis values were inverted

Starting with v5.0, values are stored correctly without inversion.

Migration: When loading pre-5.0 files, apply corrections:

// For position and rotation keyframes
if (keyframe.channel == 'position' || keyframe.channel == 'rotation') {
    data_point.x = -data_point.x;  // Invert X
}
// For rotation keyframes only
if (keyframe.channel == 'rotation') {
    data_point.y = -data_point.y;  // Invert Y
}

Version 4.10 (May 9, 2024)

Relative Texture Paths Fixed

Relative paths are now correctly saved relative to the directory containing the .bbmodel file, not relative to the file as if it were a directory.

Pre-4.10:

{
  "path": "/project/model.bbmodel",
  "relative_path": "/textures/skin.png"  // Wrong: treated model.bbmodel as directory
}

4.10+:

{
  "path": "/project/model.bbmodel",
  "relative_path": "textures/skin.png"  // Correct: relative to /project/
}

Name Field Handling

The name field is now ignored when loading and overwritten with the .bbmodel filename (without extension).

Default Texture Handling

In formats with a default texture, textures are no longer specified per face if they were not explicitly assigned to that face.

Version 4.9 (December 2, 2023)

Per-Texture UV Size

In generic models and formats with the per_texture_uv_size feature enabled, UV size is now set per texture instead of globally.

Pre-4.9 format (global resolution):

{
  "resolution": {
    "width": 16,
    "height": 16
  },
  "textures": [
    { "id": "0", /* ... */ },
    { "id": "1", /* ... */ }
  ]
}

4.9+ format (per-texture):

{
  "resolution": {
    "width": 16,    // Default/fallback
    "height": 16
  },
  "textures": [
    {
      "id": "0",
      "uv_width": 32,   // Individual texture UV space
      "uv_height": 32,
      /* ... */
    },
    {
      "id": "1",
      "uv_width": 64,
      "uv_height": 64,
      /* ... */
    }
  ]
}

Migration: When UV coordinates are stored per-face, they must be rescaled:

// Old UV space: project.resolution.width × project.resolution.height
// New UV space: texture.uv_width × texture.uv_height
scale_u = texture.uv_width / old_resolution.width;
scale_v = texture.uv_height / old_resolution.height;
face.uv = [u0 * scale_u, v0 * scale_v, u1 * scale_u, v1 * scale_v];

Version 4.5

Mirror UV Attribute

The shade attribute on elements was replaced by mirror_uv:

// Pre-4.5
{ "shade": false }  // Used for UV mirroring in box_uv mode

// 4.5+
{ "mirror_uv": true }  // Explicit mirror UV flag

Version 3.2

Z-Axis Rotation Sign

Z-axis rotation sign convention changed. Pre-3.2 files use inverted Z rotations.

Migration:

if (format_version < 3.2) {
    group.rotation[2] *= -1;  // Invert Z rotation
}

11.3 Reading Legacy Files

When reading files from different versions, apply the appropriate compatibility transforms:

Version Detection:

if (!model.meta.format_version) {
    model.meta.format_version = model.meta.format || "3.0";
}

Upgrade Path for Pre-5.0 Files:

  1. Extract groups from outlinergroups array
  2. Replace group objects with { uuid, children }
  3. Apply rotation sign corrections for animations
  4. Fix Z-axis rotations if pre-3.2
  5. Handle relative texture paths if pre-4.10
  6. Rescale UVs if pre-4.9 and using per-texture UV sizes

12. Implementation Notes

12.1 Coordinate Transformations

Blockbench → Standard 3D:

// Blockbench uses Y-up, same as most 3D engines
position = [x, y, z]  // No conversion needed

// Rotations: degrees → radians
rotation_rad = [rx, ry, rz] * (PI / 180)

Matrix Construction (Column-major):

T = translate(origin)
R = rotateZ(rz) * rotateY(ry) * rotateX(rx)
T_inv = translate(-origin)
LocalMatrix = T * R * T_inv

12.2 Mesh Triangulation

Quads must be triangulated for rendering:

Quad [v0, v1, v2, v3] →
  Triangle 1: [v0, v1, v2]
  Triangle 2: [v0, v2, v3]

Ensure winding order preserves CCW normals.

12.3 UV Coordinate System

  • Origin: Top-left corner
  • Axes: U (right), V (down)
  • Range: [0, texture.uv_width] × [0, texture.uv_height]
  • Normalization (for GPU): Divide by texture dimensions

12.4 UUID Format

UUIDs are lowercase hexadecimal with hyphens:

xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Example: c8eb7afc-1e80-c3b9-07e9-0e96b2c28979


13. Example: Complete Model

{
  "meta": {
    "format_version": "5.0",
    "model_format": "free",
    "box_uv": false
  },
  "name": "character",
  "resolution": { "width": 64, "height": 64 },

  "elements": [
    {
      "type": "cube",
      "uuid": "head-uuid",
      "name": "head",
      "from": [-4, 24, -4],
      "to": [4, 32, 4],
      "origin": [0, 24, 0],
      "faces": {
        "north": { "uv": [0, 0, 8, 8], "texture": 0 }
        /* ... other faces ... */
      }
    }
  ],

  "groups": [
    {
      "uuid": "body-group-uuid",
      "name": "body",
      "origin": [0, 24, 0],
      "rotation": [0, 0, 0]
    }
  ],

  "outliner": [
    {
      "uuid": "body-group-uuid",
      "children": ["head-uuid"]
    }
  ],

  "textures": [
    {
      "uuid": "tex-uuid",
      "id": "0",
      "name": "skin",
      "width": 64,
      "height": 64,
      "source": "data:image/png;base64,..."
    }
  ],

  "animations": [
    {
      "uuid": "anim-uuid",
      "name": "nod",
      "loop": "loop",
      "length": 1.0,
      "animators": {
        "head-uuid": {
          "name": "head",
          "type": "bone",
          "keyframes": [
            {
              "channel": "rotation",
              "time": 0.0,
              "data_points": [{ "x": 0, "y": 0, "z": 0 }],
              "interpolation": "linear"
            },
            {
              "channel": "rotation",
              "time": 0.5,
              "data_points": [{ "x": 15, "y": 0, "z": 0 }],
              "interpolation": "linear"
            },
            {
              "channel": "rotation",
              "time": 1.0,
              "data_points": [{ "x": 0, "y": 0, "z": 0 }],
              "interpolation": "linear"
            }
          ]
        }
      }
    }
  ]
}

14. Export Workflow

14.1 Building Scene Graph

  1. Parse Elements: Create geometry for each element
  2. Build Hierarchy: Process outliner to construct tree
  3. Apply Transforms:
    • Start from root
    • Accumulate transforms down the tree
    • Apply to element vertices/bounds
  4. Handle Animations: Link animators to scene nodes by UUID

14.2 Transform Application

def apply_transform(node, parent_matrix):
    # Local transformation
    T = translate(node.origin)
    R = rotate(node.rotation)  # XYZ order
    T_inv = translate(-node.origin)
    local_matrix = T @ R @ T_inv

    # World transformation
    world_matrix = parent_matrix @ local_matrix

    # Apply to geometry
    if node.is_element():
        transform_geometry(node.geometry, world_matrix)

    # Recurse to children
    for child in node.children:
        apply_transform(child, world_matrix)

14.3 Animation Export

For real-time engines (glTF, FBX):

  1. Sample keyframes at fixed intervals (e.g., 30 FPS)
  2. Bake transformations for non-linear interpolations
  3. Convert rotations to quaternions to avoid gimbal lock
  4. Pack data into animation tracks per-channel

15. Common Pitfalls

  1. Rotation Order: Always XYZ, never ZYX
  2. Version Checks: Apply compatibility transforms for pre-5.0 files
  3. UUID Resolution: Match animators to nodes, textures to faces
  4. UV Normalization: Divide by texture dimensions for shader input
  5. Mesh Winding: Maintain CCW winding during triangulation
  6. Transform Order: Translate to origin → Rotate → Translate back
  7. Float Precision: Use sufficient precision for small rotations/scales

References

  • Blockbench Source: https://github.com/JannisX11/blockbench
  • Format Codec: blockbench/js/io/formats/bbmodel.js
  • GLTF Exporter: blockbench/js/io/formats/gltf.js
  • Version History: See FORMATV constant (currently 5.0)

Document Version: 1.0 Last Updated: 2025-10-30 Specification Coverage: Blockbench Format v3.2–5.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment