106 lines
3.9 KiB
Python
106 lines
3.9 KiB
Python
|
|
import os
|
||
|
|
import math
|
||
|
|
import bpy
|
||
|
|
|
||
|
|
def render_model_to_grid(model_path, output_path):
|
||
|
|
# Clear default scene items
|
||
|
|
bpy.ops.object.select_all(action='SELECT')
|
||
|
|
bpy.ops.object.delete()
|
||
|
|
|
||
|
|
# Import the asset
|
||
|
|
ext = os.path.splitext(model_path)[1].lower()
|
||
|
|
if ext in ['.glb', '.gltf']:
|
||
|
|
bpy.ops.import_scene.gltf(filepath=model_path)
|
||
|
|
elif ext == '.obj':
|
||
|
|
bpy.ops.wm.obj_import(filepath=model_path)
|
||
|
|
elif ext == '.fbx':
|
||
|
|
bpy.ops.import_scene.fbx(filepath=model_path)
|
||
|
|
elif ext == '.stl':
|
||
|
|
bpy.ops.wm.stl_import(filepath=model_path)
|
||
|
|
else:
|
||
|
|
raise ValueError(f"Unsupported asset format extension: {ext}")
|
||
|
|
|
||
|
|
# Compute bounding metrics across all imported objects
|
||
|
|
obs = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||
|
|
if not obs:
|
||
|
|
raise ValueError("No valid mesh geometry loaded into the layout tree.")
|
||
|
|
|
||
|
|
# Frame geometry bounds
|
||
|
|
for obj in obs:
|
||
|
|
obj.select_set(True)
|
||
|
|
bpy.ops.view3d.camera_to_view_selected()
|
||
|
|
|
||
|
|
# Add a clean camera and tracking point
|
||
|
|
cam_data = bpy.data.cameras.new(name="ValidationCam")
|
||
|
|
cam_obj = bpy.data.objects.new("ValidationCam", cam_data)
|
||
|
|
bpy.context.scene.collection.objects.link(cam_obj)
|
||
|
|
bpy.context.scene.camera = cam_obj
|
||
|
|
|
||
|
|
# Create an anchor target at the origin
|
||
|
|
target = bpy.data.objects.new("Anchor", None)
|
||
|
|
bpy.context.scene.collection.objects.link(target)
|
||
|
|
|
||
|
|
# Add constraint to camera to always track the target origin
|
||
|
|
track = cam_obj.constraints.new(type='TRACK_TO')
|
||
|
|
track.target = target
|
||
|
|
track.track_axis = 'TRACK_NEGATIVE_Z'
|
||
|
|
track.up_axis = 'UP_Y'
|
||
|
|
|
||
|
|
# Light setup
|
||
|
|
light_data = bpy.data.lights.new(name="Sun", type='SUN')
|
||
|
|
light_obj = bpy.data.objects.new("Sun", light_data)
|
||
|
|
bpy.context.scene.collection.objects.link(light_obj)
|
||
|
|
light_data.energy = 3.0
|
||
|
|
|
||
|
|
# Shading options: render flat colors with basic ambient pass
|
||
|
|
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT' if hasattr(bpy.data, "version") and bpy.data.version >= (4, 2) else 'BLENDER_EEVEE'
|
||
|
|
bpy.context.scene.render.resolution_x = 512
|
||
|
|
bpy.context.scene.render.resolution_y = 512
|
||
|
|
|
||
|
|
# View configurations matrix [Pitch, Yaw]
|
||
|
|
views = [
|
||
|
|
{"name": "Front", "p": 5, "y": 0},
|
||
|
|
{"name": "Side", "p": 5, "y": 90},
|
||
|
|
{"name": "Top", "p": 80, "y": 45},
|
||
|
|
{"name": "Angle", "p": -15, "y": 30}
|
||
|
|
]
|
||
|
|
|
||
|
|
from PIL import Image
|
||
|
|
captured_frames = []
|
||
|
|
|
||
|
|
for idx, v in enumerate(views):
|
||
|
|
pitch = math.radians(v["p"])
|
||
|
|
yaw = math.radians(v["y"])
|
||
|
|
dist = 3.5 # Set normalized tracking distance offset
|
||
|
|
|
||
|
|
# Spherical coordinate conversion mechanics
|
||
|
|
cam_obj.location.x = dist * math.cos(pitch) * math.sin(yaw)
|
||
|
|
cam_obj.location.y = -dist * math.cos(pitch) * math.cos(yaw)
|
||
|
|
cam_obj.location.z = dist * math.sin(pitch)
|
||
|
|
|
||
|
|
# Execute render frame save pass
|
||
|
|
tmp_img_path = f"tmp_frame_{idx}.png"
|
||
|
|
bpy.context.scene.render.filepath = os.path.abspath(tmp_img_path)
|
||
|
|
bpy.ops.render.render(write_still=True)
|
||
|
|
captured_frames.append(Image.open(tmp_img_path))
|
||
|
|
os.remove(tmp_img_path)
|
||
|
|
|
||
|
|
# Compile the 2x2 grid collage
|
||
|
|
grid = Image.new('RGB', (1024, 1024), (30, 30, 35))
|
||
|
|
grid.paste(captured_frames[0], (0, 0))
|
||
|
|
grid.paste(captured_frames[1], (512, 0))
|
||
|
|
grid.paste(captured_frames[2], (0, 512))
|
||
|
|
grid.paste(captured_frames[3], (512, 512))
|
||
|
|
grid.save(output_path)
|
||
|
|
print(f"✅ Grid successfully compiled via Blender context -> {output_path}")
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
import sys
|
||
|
|
argv = sys.argv
|
||
|
|
try:
|
||
|
|
idx = argv.index("--")
|
||
|
|
m_path = argv[idx + 1]
|
||
|
|
out_path = argv[idx + 2]
|
||
|
|
render_model_to_grid(m_path, out_path)
|
||
|
|
except (ValueError, IndexError):
|
||
|
|
print("Missing argument bindings passed to background wrapper script.")
|