# scripts/generate_scene.py import bpy import json import sys import os import mathutils def clear_scene(): bpy.ops.wm.read_factory_settings(use_empty=True) def make_material(name, base_color=(1,1,1,1), texture_path=None): mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() output = nodes.new(type="ShaderNodeOutputMaterial") principled = nodes.new(type="ShaderNodeBsdfPrincipled") principled.inputs['Base Color'].default_value = base_color principled.inputs['Roughness'].default_value = 0.4 principled.inputs['Metallic'].default_value = 0.0 links.new(principled.outputs['BSDF'], output.inputs['Surface']) if texture_path and os.path.exists(texture_path): tex = nodes.new(type="ShaderNodeTexImage") tex.image = bpy.data.images.load(texture_path) links.new(tex.outputs['Color'], principled.inputs['Base Color']) return mat def add_mesh_object(mesh_obj, name, location=(0,0,0), material=None): """Helper to configure mesh object - objects from bpy.ops are already linked""" mesh_obj.name = name mesh_obj.location = location if material: if len(mesh_obj.data.materials) == 0: mesh_obj.data.materials.append(material) else: mesh_obj.data.materials[0] = material return mesh_obj def primitive_humanoid(name_prefix="Avatar", scale=1.0, material=None): # Create simple primitives for torso, head, limbs and join them parts = [] # Torso - cylinder bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=0.25*scale, depth=0.8*scale, location=(0,0,1.0*scale)) torso = bpy.context.object torso.name = f"{name_prefix}_Torso" parts.append(torso) # Head - sphere bpy.ops.mesh.primitive_uv_sphere_add(radius=0.18*scale, location=(0,0,1.9*scale)) head = bpy.context.object head.name = f"{name_prefix}_Head" parts.append(head) # Upper arms - cubes scaled bpy.ops.mesh.primitive_cube_add(size=0.2*scale, location=(0.45*scale,0,1.3*scale)) r_arm = bpy.context.object r_arm.scale[0] = 2.0 r_arm.name = f"{name_prefix}_R_UpperArm" parts.append(r_arm) bpy.ops.mesh.primitive_cube_add(size=0.2*scale, location=(-0.45*scale,0,1.3*scale)) l_arm = bpy.context.object l_arm.scale[0] = 2.0 l_arm.name = f"{name_prefix}_L_UpperArm" parts.append(l_arm) # Upper legs bpy.ops.mesh.primitive_cube_add(size=0.25*scale, location=(0.18*scale,0,0.5*scale)) r_leg = bpy.context.object r_leg.scale[2] = 1.5 r_leg.name = f"{name_prefix}_R_UpperLeg" parts.append(r_leg) bpy.ops.mesh.primitive_cube_add(size=0.25*scale, location=(-0.18*scale,0,0.5*scale)) l_leg = bpy.context.object l_leg.scale[2] = 1.5 l_leg.name = f"{name_prefix}_L_UpperLeg" parts.append(l_leg) # Join into single mesh (optional) bpy.ops.object.select_all(action='DESELECT') for p in parts: p.select_set(True) bpy.context.view_layer.objects.active = p bpy.ops.object.join() # join into active combined = bpy.context.object combined.name = f"{name_prefix}_Mesh" # Apply material if material: if len(combined.data.materials) == 0: combined.data.materials.append(material) else: combined.data.materials[0] = material return combined def create_armature_for_avatar(avatar_obj, name="AvatarArmature"): bpy.ops.object.select_all(action='DESELECT') # create armature bpy.ops.object.armature_add(enter_editmode=True, location=(0,0,0)) arm = bpy.context.object arm.name = name arm_data = arm.data arm_data.name = name + "_Data" # edit mode bones: root, spine, head, left_arm, right_arm, left_leg, right_leg eb = arm.data.edit_bones eb.remove(eb[0]) # remove default bone to start clean def add_bone(bname, head, tail, parent=None): b = eb.new(bname) b.head = head b.tail = tail if parent: b.parent = eb[parent] return b add_bone("root", (0,0,0), (0,0,0.5)) add_bone("spine", (0,0,0.5), (0,0,1.2), parent="root") add_bone("head", (0,0,1.2), (0,0,1.9), parent="spine") add_bone("left_arm", (-0.45,0,1.3), (-0.9,0,1.3), parent="spine") add_bone("right_arm", (0.45,0,1.3), (0.9,0,1.3), parent="spine") add_bone("left_leg", (-0.18,0,0.5), (-0.18,0,-0.5), parent="root") add_bone("right_leg", (0.18,0,0.5), (0.18,0,-0.5), parent="root") bpy.ops.object.mode_set(mode='OBJECT') # Parent mesh to armature with automatic weights bpy.context.view_layer.objects.active = avatar_obj avatar_obj.select_set(True) arm.select_set(True) bpy.ops.object.parent_set(type='ARMATURE_AUTO') return arm def add_ground(scene_desc): """Add ground plane""" if scene_desc.get('ground'): ground_data = scene_desc.get('ground') size = ground_data.get('size', 20) color = ground_data.get('color', [0.4, 0.3, 0.2, 1]) else: size = 20 color = [0.4, 0.3, 0.2, 1] bpy.ops.mesh.primitive_plane_add(size=size, location=(0,0,0)) ground = bpy.context.object ground.name = "Ground" mat_ground = make_material("GroundMat", base_color=tuple(color), texture_path=scene_desc.get('ground_texture')) # Object is already linked by primitive_plane_add, just set material if len(ground.data.materials) == 0: ground.data.materials.append(mat_ground) else: ground.data.materials[0] = mat_ground def add_objects(scene_desc): """Add scene objects""" for i, obj in enumerate(scene_desc.get('objects', [])): typ = obj.get('type','cube') pos = obj.get('location', [i*1.5, 0.0, 0.5]) obj_name = obj.get('name', f"EnvObj_{i}") if typ == 'cube': bpy.ops.mesh.primitive_cube_add(size=obj.get('size', 0.8), location=pos) o = bpy.context.object elif typ == 'sphere': bpy.ops.mesh.primitive_uv_sphere_add(radius=obj.get('radius',0.5), location=pos) o = bpy.context.object elif typ == 'cylinder': bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=obj.get('radius', 0.5), depth=obj.get('size', 1), location=pos) o = bpy.context.object elif typ == 'cone': bpy.ops.mesh.primitive_cone_add(vertices=32, radius1=obj.get('radius', 0.5), depth=obj.get('size', 1), location=pos) o = bpy.context.object elif typ == 'plane': bpy.ops.mesh.primitive_plane_add(size=obj.get('size', 10), location=pos) o = bpy.context.object else: bpy.ops.mesh.primitive_cube_add(size=0.5, location=pos) o = bpy.context.object # Object is already linked by bpy.ops, just set name, material, and transform o.name = obj_name o.location = tuple(pos) if obj.get('rotation'): o.rotation_euler = tuple(obj.get('rotation', [0, 0, 0])) if obj.get('scale'): o.scale = tuple(obj.get('scale', [1, 1, 1])) # Handle PBR material properties roughness = obj.get('roughness', 0.5) metallic = obj.get('metallic', 0.0) emission = obj.get('emission') emission_strength = obj.get('emissionStrength', 1.0) mat = make_material(f"objmat_{i}", base_color=tuple(obj.get('color', [1,1,1,1])), texture_path=obj.get('texture')) # Update material properties nodes = mat.node_tree.nodes principled = nodes.get('Principled BSDF') if principled: principled.inputs['Roughness'].default_value = roughness principled.inputs['Metallic'].default_value = metallic if emission: # Try different emission input names for different Blender versions try: if 'Emission Color' in principled.inputs: principled.inputs['Emission Color'].default_value = (*emission[:3], 1.0) elif 'Emission' in principled.inputs: principled.inputs['Emission'].default_value = (*emission[:3], 1.0) if 'Emission Strength' in principled.inputs: principled.inputs['Emission Strength'].default_value = emission_strength except KeyError: # Skip emission if not supported pass if len(o.data.materials) == 0: o.data.materials.append(mat) else: o.data.materials[0] = mat def add_lighting(scene_desc): """Add default lighting""" bpy.ops.object.light_add(type='SUN', location=(10, -10, 10)) sun = bpy.context.object sun.data.energy = scene_desc.get('sun_energy', 3.0) sun.data.angle = 0.5 # Optional point lights from scene_desc for i, L in enumerate(scene_desc.get('lights', [])): typ = L.get('type','POINT') loc = L.get('location', [0,0,2]) bpy.ops.object.light_add(type=typ, location=loc) light = bpy.context.object light.data.energy = L.get('energy', 50.0) def apply_camera(): # Camera bpy.ops.object.camera_add(location=(4.0, -4.0, 2.2)) cam = bpy.context.object cam.rotation_euler = (1.05, 0.0, 0.78) bpy.context.scene.camera = cam def main(): # CLI args: script.py -- argv = sys.argv if "--" in argv: idx = argv.index("--") args = argv[idx+1:] else: args = [] if len(args) < 2: print("Usage: blender --background --python generate_scene.py -- ") return input_json = args[0] output_glb = args[1] with open(input_json, 'r') as f: scene_desc = json.load(f) clear_scene() # Ground add_ground(scene_desc) # Objects add_objects(scene_desc) # Avatar creation (if present) if scene_desc.get('avatar') and scene_desc.get('avatar', {}).get('present'): tex_file = scene_desc.get('avatar_texture') # optional mat_avatar = make_material("AvatarMat", base_color=(1,0.8,0.6,1), texture_path=tex_file) avatar = primitive_humanoid(name_prefix="Hero", scale=1.0, material=mat_avatar) arm = create_armature_for_avatar(avatar, name="HeroArmature") # Set avatar position if specified avatar_pos = scene_desc.get('avatar', {}).get('position', [0, 0, 0]) avatar.location = tuple(avatar_pos) avatar_scale = scene_desc.get('avatar', {}).get('scale', 1.0) avatar.scale = (avatar_scale, avatar_scale, avatar_scale) # Lighting if scene_desc.get('lighting'): lighting_data = scene_desc.get('lighting') # Add ambient light via world bpy.context.scene.world.use_nodes = True world_nodes = bpy.context.scene.world.node_tree.nodes if 'Background' in world_nodes: world_nodes['Background'].inputs['Strength'].default_value = lighting_data.get('ambient_strength', 0.3) light_color = lighting_data.get('color', [1, 1, 1]) world_nodes['Background'].inputs['Color'].default_value = (*light_color[:3], 1.0) # Add lights from lighting.lights array if lighting_data.get('lights'): for i, L in enumerate(lighting_data.get('lights', [])): typ = L.get('type','POINT') loc = L.get('location', [0,0,2]) if typ == 'SUN': bpy.ops.object.light_add(type='SUN', location=loc) sun = bpy.context.object sun.data.energy = L.get('energy', 3.0) sun.data.angle = L.get('angle', 0.5) if L.get('color'): sun.data.color = tuple(L.get('color', [1,1,1])[:3]) elif typ == 'POINT': bpy.ops.object.light_add(type='POINT', location=loc) light = bpy.context.object light.data.energy = L.get('energy', 50.0) if L.get('color'): light.data.color = tuple(L.get('color', [1,1,1])[:3]) elif typ == 'SPOT': bpy.ops.object.light_add(type='SPOT', location=loc) light = bpy.context.object light.data.energy = L.get('energy', 50.0) if L.get('color'): light.data.color = tuple(L.get('color', [1,1,1])[:3]) else: # Default sun if no lights specified bpy.ops.object.light_add(type='SUN', location=(10, -10, 10)) sun = bpy.context.object sun.data.energy = lighting_data.get('sun_energy', 3.0) sun.data.angle = 0.5 else: # Default lighting add_lighting(scene_desc) # Camera if scene_desc.get('camera'): cam_data = scene_desc.get('camera') bpy.ops.object.camera_add(location=tuple(cam_data.get('position', [4.0, -4.0, 2.2]))) cam = bpy.context.object cam.rotation_euler = tuple(cam_data.get('rotation', [1.05, 0.0, 0.78])) bpy.context.scene.camera = cam else: apply_camera() # Scene settings (cycles or eevee) bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.cycles.device = 'CPU' # Export glb export_kwargs = { "filepath": output_glb, "export_format": "GLB", "export_apply": True, "export_texcoords": True, "export_normals": True, "export_materials": "EXPORT", "export_colors": True, "export_extras": True, } bpy.ops.export_scene.gltf(**export_kwargs) print("Exported:", output_glb) if __name__ == "__main__": main()