Blender Sprite Atlas Cutter - From Texture Atlas to 3D Planes

• 5 min read

Overview

Import a sprite atlas into Blender with sprites positioned exactly as they appear in the texture sheet. This script reads a JSON atlas file and creates 3D planes that mirror the original sprite layout.

What This Script Does:

  • Reads TexturePacker-style JSON atlas files
  • Creates individual plane meshes for each sprite
  • Positions sprites using their atlas coordinates
  • Automatically calculates and applies correct UV mapping
  • Sets up materials with proper transparency
  • Centers the entire layout around the origin

Use Cases

  • Game Development: Import UI layouts maintaining their original positions
  • Motion Graphics: Animate sprite sequences in their natural arrangement
  • VFX Compositing: Mix 2D sprite elements with 3D renders
  • Asset Organization: Visualize sprite atlases in 3D space

Two Approaches

Approach 1: Individual Planes

Each sprite = separate object. Best for animation and individual control.

Approach 2: Single Mesh

All sprites in one mesh. Best for performance with 50+ sprites.

Script 1: Individual Planes (Atlas Layout)

Sprites are positioned exactly as they appear in the texture atlas.

import bpy
import json

# ============================================
# CONFIGURATION - UPDATE THESE PATHS!
# ============================================
JSON_PATH = r"C:\path\to\your\atlas.json"
TEXTURE_PATH = r"C:\path\to\your\texture.png"
COLLECTION_NAME = "Texture_Atlas_Sprites"

def load_atlas_data(json_path):
    """Load and parse the texture atlas JSON file"""
    with open(json_path, 'r') as f:
        return json.load(f)

def create_collection(name):
    """Create or get a collection for organizing sprites"""
    if name in bpy.data.collections:
        return bpy.data.collections[name]
    collection = bpy.data.collections.new(name)
    bpy.context.scene.collection.children.link(collection)
    return collection

def create_material_with_texture(name, texture_path, frame_data, atlas_size):
    """Create a material with UV mapping for the specific frame"""
    mat = bpy.data.materials.new(name=f"Atlas_{name}")
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    nodes.clear()
    
    tex_coord = nodes.new('ShaderNodeTexCoord')
    tex_coord.location = (-800, 300)
    
    mapping = nodes.new('ShaderNodeMapping')
    mapping.location = (-600, 300)
    
    tex_image = nodes.new('ShaderNodeTexImage')
    tex_image.location = (-300, 300)
    
    bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.location = (0, 300)
    
    output = nodes.new('ShaderNodeOutputMaterial')
    output.location = (300, 300)
    
    # Load texture
    if texture_path in bpy.data.images:
        tex_image.image = bpy.data.images[texture_path]
    else:
        tex_image.image = bpy.data.images.load(texture_path)
    
    # Calculate UV offset and scale
    x = frame_data['frame']['x'] / atlas_size['w']
    y = 1.0 - (frame_data['frame']['y'] + frame_data['frame']['h']) / atlas_size['h']
    scale_x = frame_data['frame']['w'] / atlas_size['w']
    scale_y = frame_data['frame']['h'] / atlas_size['h']
    
    mapping.inputs['Location'].default_value = (x, y, 0)
    mapping.inputs['Scale'].default_value = (scale_x, scale_y, 1)
    
    # Connect nodes
    links = mat.node_tree.links
    links.new(tex_coord.outputs['UV'], mapping.inputs['Vector'])
    links.new(mapping.outputs['Vector'], tex_image.inputs['Vector'])
    links.new(tex_image.outputs['Color'], bsdf.inputs['Base Color'])
    links.new(tex_image.outputs['Alpha'], bsdf.inputs['Alpha'])
    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
    
    mat.blend_method = 'BLEND'
    mat.show_transparent_back = False
    
    return mat

def create_sprite_plane(name, frame_data, atlas_data, collection):
    """Create a plane mesh for a sprite with proper dimensions"""
    mesh = bpy.data.meshes.new(name=name)
    obj = bpy.data.objects.new(name, mesh)
    collection.objects.link(obj)
    
    # Scale factor: 1 pixel = 0.01 Blender units
    w = frame_data['sourceSize']['w'] * 0.01
    h = frame_data['sourceSize']['h'] * 0.01
    
    # Create plane vertices (centered on object origin)
    verts = [(-w/2, 0, -h/2), (w/2, 0, -h/2), (w/2, 0, h/2), (-w/2, 0, h/2)]
    faces = [(0, 1, 2, 3)]
    
    mesh.from_pydata(verts, [], faces)
    mesh.update()
    
    # Create UV map
    uv_layer = mesh.uv_layers.new(name='UVMap')
    mesh.uv_layers[0].data[0].uv = (0, 0)
    mesh.uv_layers[0].data[1].uv = (1, 0)
    mesh.uv_layers[0].data[2].uv = (1, 1)
    mesh.uv_layers[0].data[3].uv = (0, 1)
    
    # Assign material
    mat = create_material_with_texture(name, TEXTURE_PATH, frame_data, atlas_data['meta']['size'])
    obj.data.materials.append(mat)
    
    return obj

def main():
    """Main function to process the texture atlas"""
    atlas_data = load_atlas_data(JSON_PATH)
    collection = create_collection(COLLECTION_NAME)
    
    atlas_width = atlas_data['meta']['size']['w']
    atlas_height = atlas_data['meta']['size']['h']
    
    # Scale factor for positioning
    position_scale = 0.01  # 1 pixel = 0.01 Blender units
    
    objects = []
    
    # Create all sprites
    for sprite_name, frame_data in atlas_data['frames'].items():
        clean_name = sprite_name.replace('.png', '')
        print(f"Creating sprite: {clean_name}")
        
        obj = create_sprite_plane(clean_name, frame_data, atlas_data, collection)
        
        # Position sprite using its atlas coordinates
        # Get the CENTER position of the sprite in the atlas
        sprite_x = frame_data['frame']['x'] + frame_data['frame']['w'] / 2
        sprite_y = frame_data['frame']['y'] + frame_data['frame']['h'] / 2
        
        # Convert to Blender coordinates
        # X stays the same, Y needs to be flipped (atlas is top-left origin, Blender is bottom-left)
        pos_x = sprite_x * position_scale
        pos_z = -(sprite_y * position_scale)  # Negative because Y goes down in atlas
        
        obj.location = (pos_x, 0, pos_z)
        objects.append(obj)
    
    # Center all sprites around origin
    if objects:
        # Calculate the center of all sprites
        center_x = (atlas_width / 2) * position_scale
        center_z = -(atlas_height / 2) * position_scale
        
        # Offset all sprites so the atlas center is at origin
        for obj in objects:
            obj.location.x -= center_x
            obj.location.z -= center_z
    
    print(f"\n✓ Created {len(objects)} sprites successfully!")
    print(f"✓ Sprites positioned as in atlas (centered)")
    print(f"✓ Organized in collection: {COLLECTION_NAME}")
    
    # Set viewport to Material Preview
    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            for space in area.spaces:
                if space.type == 'VIEW_3D':
                    space.shading.type = 'MATERIAL'
                    break

if __name__ == "__main__":
    main()

Script 2: Single Mesh (Atlas Layout)

All sprites in one mesh, positioned exactly as they appear in the texture atlas.

import bpy
import json

# ============================================
# CONFIGURATION - UPDATE THESE PATHS!
# ============================================
JSON_PATH = r"C:\path\to\your\atlas.json"
TEXTURE_PATH = r"C:\path\to\your\texture.png"

def load_atlas_data(json_path):
    """Load and parse the texture atlas JSON file"""
    with open(json_path, 'r') as f:
        return json.load(f)

def create_combined_mesh():
    """Create a single mesh with all sprites as UV islands"""
    atlas_data = load_atlas_data(JSON_PATH)
    atlas_size = atlas_data['meta']['size']
    
    atlas_width = atlas_size['w']
    atlas_height = atlas_size['h']
    
    # Create mesh object
    mesh = bpy.data.meshes.new(name="TextureAtlas_Combined")
    obj = bpy.data.objects.new("TextureAtlas_Combined", mesh)
    bpy.context.collection.objects.link(obj)
    
    verts, faces, uvs = [], [], []
    
    # Scale factors
    size_scale = 0.01      # Sprite size: 1 pixel = 0.01 Blender units
    position_scale = 0.01  # Position: 1 pixel = 0.01 Blender units
    
    print("Building combined mesh using atlas layout...")
    
    # Calculate offset to center the entire atlas around origin
    center_x = (atlas_width / 2) * position_scale
    center_z = (atlas_height / 2) * position_scale
    
    # Create geometry for each sprite
    for sprite_name, frame_data in atlas_data['frames'].items():
        # Get sprite dimensions
        w = frame_data['sourceSize']['w'] * size_scale
        h = frame_data['sourceSize']['h'] * size_scale
        
        # Get sprite position in atlas (center of sprite)
        sprite_x = frame_data['frame']['x'] + frame_data['frame']['w'] / 2
        sprite_y = frame_data['frame']['y'] + frame_data['frame']['h'] / 2
        
        # Convert to Blender coordinates and center around origin
        pos_x = (sprite_x * position_scale) - center_x
        pos_z = -(sprite_y * position_scale) + center_z
        
        # Add vertices for this sprite
        vert_offset = len(verts)
        verts.extend([
            (pos_x - w/2, 0, pos_z - h/2),
            (pos_x + w/2, 0, pos_z - h/2),
            (pos_x + w/2, 0, pos_z + h/2),
            (pos_x - w/2, 0, pos_z + h/2)
        ])
        
        # Add face
        faces.append((vert_offset, vert_offset+1, vert_offset+2, vert_offset+3))
        
        # Calculate UV coordinates
        x = frame_data['frame']['x'] / atlas_width
        y = 1.0 - frame_data['frame']['y'] / atlas_height
        w_uv = frame_data['frame']['w'] / atlas_width
        h_uv = frame_data['frame']['h'] / atlas_height
        
        uvs.extend([(x, y - h_uv), (x + w_uv, y - h_uv), (x + w_uv, y), (x, y)])
        
        print(f"  Added: {sprite_name.replace('.png', '')}")
    
    # Build mesh
    mesh.from_pydata(verts, [], faces)
    mesh.update()
    
    # Apply UV coordinates
    uv_layer = mesh.uv_layers.new(name='UVMap')
    for i, uv in enumerate(uvs):
        mesh.uv_layers[0].data[i].uv = uv
    
    print("\nCreating material...")
    
    # Create material
    mat = bpy.data.materials.new(name="TextureAtlas_Material")
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    
    tex_node = nodes.new('ShaderNodeTexImage')
    tex_node.image = bpy.data.images.load(TEXTURE_PATH)
    tex_node.location = (-300, 300)
    
    bsdf = nodes['Principled BSDF']
    links = mat.node_tree.links
    links.new(tex_node.outputs['Color'], bsdf.inputs['Base Color'])
    links.new(tex_node.outputs['Alpha'], bsdf.inputs['Alpha'])
    
    mat.blend_method = 'BLEND'
    obj.data.materials.append(mat)
    
    print(f"\n✓ Created single mesh with {len(faces)} sprite faces!")
    print(f"✓ Sprites positioned as in atlas (centered)")
    print(f"✓ All sprites in one object: '{obj.name}'")
    
    # Set viewport to Material Preview
    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            for space in area.spaces:
                if space.type == 'VIEW_3D':
                    space.shading.type = 'MATERIAL'
                    break
    
    return obj

if __name__ == "__main__":
    create_combined_mesh()

How Atlas Positioning Works

The key difference: sprites use their actual atlas coordinates instead of a new grid:

# Get sprite center position from atlas
sprite_x = frame_data['frame']['x'] + frame_data['frame']['w'] / 2
sprite_y = frame_data['frame']['y'] + frame_data['frame']['h'] / 2

# Convert to Blender 3D space
pos_x = sprite_x * 0.01
pos_z = -(sprite_y * 0.01)  # Flip Y axis

# Center around origin
pos_x -= (atlas_width / 2) * 0.01
pos_z += (atlas_height / 2) * 0.01

Result: If your atlas looks like this:

┌──────────────────┐
│  🟦              │  Sprite at (100, 50)
│                  │
│         🟥       │  Sprite at (400, 200)
│                  │
│              🟩  │  Sprite at (700, 350)
└──────────────────┘

In Blender, they’ll be positioned the same way in 3D space!

Usage

  1. Update JSON_PATH and TEXTURE_PATH
  2. Blender → Scripting workspace
  3. Paste chosen script
  4. Alt+P to run

Customization

size_scale = 0.01      # Sprite size (0.01 = 1 pixel = 0.01 units)
position_scale = 0.01  # Position scaling (increase for more spread)

Video tutorial coming soon!

#blender #python #sprites #textureatlas #gamedev