Blender Sprite Atlas Cutter - From Texture Atlas to 3D Planes
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
- Update
JSON_PATHandTEXTURE_PATH - Blender → Scripting workspace
- Paste chosen script
- 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