gutter_runner/blender/batch_export.py

114 lines
4.0 KiB
Python

import bpy
import os
import pathlib
import contextlib
# Ensure blend file is saved
blend_filepath = bpy.data.filepath
if not blend_filepath:
raise Exception("Blend file is not saved")
assert bpy.context.scene is not None
@contextlib.contextmanager
def temp_scene(name: str):
original_scene = bpy.context.scene
assert original_scene is not None
temp_scene = bpy.data.scenes.new(name)
assert bpy.context.window is not None
try:
bpy.context.window.scene = temp_scene
yield temp_scene
finally:
assert bpy.context.window is not None
bpy.context.window.scene = original_scene
print(f'removing scene {temp_scene}')
bpy.data.scenes.remove(temp_scene)
scene = bpy.context.scene
export_scene = bpy.data.scenes.new('__export')
def clean_blend_path(blend_path: str) -> str:
return os.path.join(os.path.dirname(blend_path), bpy.path.clean_name(os.path.basename(blend_path)))
# Directory setup
abs_filepath = bpy.path.abspath(blend_filepath)
src_assets_path = os.path.dirname(abs_filepath)
project_path = os.path.dirname(src_assets_path)
assets_path = os.path.join(project_path, "assets")
rel_blend_path = os.path.relpath(abs_filepath, src_assets_path)
clean_rel_path = clean_blend_path(rel_blend_path)
scene_name = bpy.context.scene.name
instance_output_path = os.path.join(assets_path, "blender", clean_rel_path, f"{scene_name}.scn")
# Utility: returns path to store the glb
def get_model_export_path(obj: bpy.types.Object) -> str:
if obj.instance_type == 'COLLECTION' and obj.instance_collection:
lib_path = obj.instance_collection.library.filepath
collection_name = bpy.path.clean_name(obj.instance_collection.name)
rel_lib_path = os.path.relpath(bpy.path.abspath(lib_path), src_assets_path)
rel_lib_path = clean_blend_path(rel_lib_path)
return os.path.join("blender", rel_lib_path, f"{collection_name}.glb")
else:
obj_name = bpy.path.clean_name(obj.name)
return os.path.join("blender", clean_rel_path, f"{obj_name}.glb")
# Utility: writes an object to glb
def export_object_as_glb(obj: bpy.types.Object, export_path: str):
full_export_path = os.path.join(assets_path, export_path)
pathlib.Path(os.path.dirname(full_export_path)).mkdir(parents=True, exist_ok=True)
if obj.instance_type == 'COLLECTION' and obj.instance_collection:
with temp_scene('__export') as scn:
scn.collection.children.link(obj.instance_collection)
bpy.ops.export_scene.gltf(filepath=full_export_path, use_active_scene=True, export_apply=True)
else:
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
assert bpy.context.view_layer is not None
bpy.context.view_layer.objects.active = obj
bpy.ops.export_scene.gltf(filepath=full_export_path, use_selection=True, export_apply=True)
print(f"Exported {obj.name} -> {export_path}")
# Collect all visible, non-hidden objects in the scene
visible_objects = [
obj for obj in bpy.context.scene.objects
if obj.visible_get()
]
exported = set()
instances = []
for obj in visible_objects:
model_path = get_model_export_path(obj)
if model_path not in exported:
export_object_as_glb(obj, model_path)
exported.add(model_path)
loc = obj.location
rot = obj.rotation_quaternion
scale = obj.scale
instance_sexpr = (
'(inst'
f'\n\t:model "assets/{model_path}"'
f'\n\t:pos ({loc.x:.6f} {loc.z:.6f} {-loc.y:.6f})'
f'\n\t:rot ({rot.x:.6f} {rot.y:.6f} {rot.z:.6f} {rot.w:.6f})'
f'\n\t:scale ({scale.x:.6f} {scale.y:.6f} {scale.z:.6f}))'
)
instances.append(instance_sexpr)
# Save instances to scene file
instance_output_abs = os.path.join(project_path, instance_output_path)
pathlib.Path(os.path.dirname(instance_output_abs)).mkdir(parents=True, exist_ok=True)
with open(instance_output_abs, "w", encoding="utf-8") as f:
for line in instances:
f.write(line + "\n")
print(f"Written instances to {instance_output_abs}")