""" Blender optimization script for game-ready assets. Run with: blender --background --python blender_optimize.py -- input.glb output.glb """ import bpy import sys from pathlib import Path from mathutils import Vector def optimize_asset(input_path: str, output_path: str): """Optimize GLB for game engine use.""" print(f"[Blender] Optimizing: {input_path}") # Clear scene bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() # Import GLB bpy.ops.import_scene.gltf(filepath=input_path) # Get imported object obj = bpy.context.selected_objects[0] bpy.context.view_layer.objects.active = obj # 1. Normalize scale to 2m height bbox = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box] height = max(v.z for v in bbox) - min(v.z for v in bbox) if height > 0: scale_factor = 2.0 / height obj.scale = (scale_factor, scale_factor, scale_factor) bpy.ops.object.transform_apply(scale=True) # 2. Clean mesh topology bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles(threshold=0.0001) bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.mesh.delete_loose() bpy.ops.mesh.dissolve_degenerate(threshold=0.0001) bpy.ops.object.mode_set(mode='OBJECT') # 3. Quad remesh for better topology mod = obj.modifiers.new(name="Remesh", type='REMESH') mod.mode = 'SHARP' mod.octree_depth = 7 # ~7,500 polygons mod.sharpness = 1.0 mod.use_smooth_shade = True bpy.ops.object.modifier_apply(modifier="Remesh") # 4. Smart UV unwrap bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project( angle_limit=66.0, island_margin=0.02, area_weight=1.0, correct_aspect=True, scale_to_bounds=True ) bpy.ops.object.mode_set(mode='OBJECT') # 5. Apply smooth shading bpy.ops.object.shade_smooth() obj.data.use_auto_smooth = True obj.data.auto_smooth_angle = 0.523599 # 30 degrees # 6. Generate LOD levels lod_levels = [ ("LOD0", 1.0), # 100% - original ("LOD1", 0.5), # 50% - medium distance ("LOD2", 0.25), # 25% - far distance ] for lod_name, ratio in lod_levels: lod_obj = obj.copy() lod_obj.data = obj.data.copy() lod_obj.name = f"{obj.name}_{lod_name}" bpy.context.collection.objects.link(lod_obj) if ratio < 1.0: # Apply decimate modifier bpy.context.view_layer.objects.active = lod_obj mod = lod_obj.modifiers.new(name="Decimate", type='DECIMATE') mod.ratio = ratio bpy.ops.object.modifier_apply(modifier="Decimate") # 7. Generate collision mesh collision_obj = obj.copy() collision_obj.data = obj.data.copy() collision_obj.name = f"{obj.name}_collision" bpy.context.collection.objects.link(collision_obj) bpy.context.view_layer.objects.active = collision_obj # Simplify heavily for collision mod = collision_obj.modifiers.new(name="Decimate", type='DECIMATE') mod.ratio = 0.1 # 10% of original bpy.ops.object.modifier_apply(modifier="Decimate") # Convex hull for physics bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.convex_hull() bpy.ops.object.mode_set(mode='OBJECT') # 8. Export with Draco compression bpy.ops.export_scene.gltf( filepath=output_path, export_format='GLB', export_draco_mesh_compression_enable=True, export_draco_mesh_compression_level=6, export_draco_position_quantization=14, export_draco_normal_quantization=10, export_draco_texcoord_quantization=12, export_materials='EXPORT', export_colors=True, export_cameras=False, export_lights=False, export_apply=True, export_yup=True ) print(f"[Blender] Optimization complete: {output_path}") if __name__ == "__main__": # Parse command line arguments argv = sys.argv argv = argv[argv.index("--") + 1:] # Get args after -- if len(argv) < 2: print("Usage: blender --background --python blender_optimize.py -- input.glb output.glb") sys.exit(1) input_path = argv[0] output_path = argv[1] optimize_asset(input_path, output_path)