blender-agent / render_script.py
suvadityamuk's picture
suvadityamuk HF Staff
Upload render_script.py with huggingface_hub
7a8d450 verified
Raw
History Blame Contribute Delete
8.77 kB
"""
Render script executed inside a fresh ``blender -b scene.blend --python render_script.py``
subprocess. Not imported by the app.
Usage (args after ``--``):
out_path engine samples res_x res_y [cam_mode azimuth elevation zoom focal]
[mode fps frame_start frame_end]
engine: CYCLES_GPU | CYCLES_CPU | EEVEE | WORKBENCH
cam_mode: "scene" (use/repair the scene camera) | "orbit" (place a camera on an orbit
around the scene bounds at the given azimuth/elevation, zoom factor and focal length)
mode: "still" (default) | "anim" (H.264 MP4 via Blender's FFmpeg encoder; fps /
frame_start / frame_end of 0 mean "use the scene's own settings")
"""
import sys
import bpy # type: ignore # noqa: F401 - only importable inside Blender
MAX_ANIM_FRAMES = 600
argv = sys.argv[sys.argv.index("--") + 1:]
out_path, engine = argv[0], argv[1]
samples, res_x, res_y = int(argv[2]), int(argv[3]), int(argv[4])
cam_mode = argv[5] if len(argv) > 5 else "scene"
if len(argv) > 9:
cam_azimuth, cam_elevation = float(argv[6]), float(argv[7])
cam_zoom, cam_focal = float(argv[8]), float(argv[9])
else:
cam_azimuth, cam_elevation, cam_zoom, cam_focal = 315.0, 30.0, 1.0, 50.0
if len(argv) > 13:
render_mode = argv[10]
arg_fps, arg_f_start, arg_f_end = int(argv[11]), int(argv[12]), int(argv[13])
else:
render_mode, arg_fps, arg_f_start, arg_f_end = "still", 0, 0, 0
scene = bpy.context.scene
scene.render.resolution_x = res_x
scene.render.resolution_y = res_y
scene.render.resolution_percentage = 100
if render_mode == "anim":
if arg_fps > 0:
scene.render.fps = arg_fps
if arg_f_start > 0:
scene.frame_start = arg_f_start
if arg_f_end > 0:
scene.frame_end = arg_f_end
if scene.frame_end <= scene.frame_start:
scene.frame_end = scene.frame_start + max(scene.render.fps * 2, 24)
if scene.frame_end - scene.frame_start + 1 > MAX_ANIM_FRAMES:
scene.frame_end = scene.frame_start + MAX_ANIM_FRAMES - 1
# Blender 5.x selects video output via media_type; older builds via file_format.
if hasattr(scene.render.image_settings, "media_type"):
scene.render.image_settings.media_type = "VIDEO"
else:
scene.render.image_settings.file_format = "FFMPEG"
scene.render.ffmpeg.format = "MPEG4"
scene.render.ffmpeg.codec = "H264"
scene.render.ffmpeg.constant_rate_factor = "MEDIUM"
scene.render.ffmpeg.audio_codec = "NONE"
# Blender appends its own frame-range suffix; render to a prefix and rename after.
_out_base = out_path[:-4] if out_path.lower().endswith(".mp4") else out_path
scene.render.filepath = _out_base
else:
scene.render.image_settings.file_format = "PNG"
scene.render.filepath = out_path
def _scene_bounds():
import mathutils
points = []
for obj in scene.objects:
if obj.type in {"MESH", "CURVE", "FONT", "SURFACE", "META"} and obj.visible_get():
for corner in obj.bound_box:
points.append(obj.matrix_world @ mathutils.Vector(corner))
if not points:
return mathutils.Vector((0, 0, 0)), 2.0
lo = mathutils.Vector((min(p[i] for p in points) for i in range(3)))
hi = mathutils.Vector((max(p[i] for p in points) for i in range(3)))
center = (lo + hi) / 2
size = max((hi - lo).length, 0.5)
return center, size
def _setup_orbit_camera(center, size):
"""Place a dedicated camera on an orbit around the scene bounds, aimed at center."""
import math
import mathutils
az = math.radians(cam_azimuth)
el = math.radians(max(-89.0, min(89.0, cam_elevation)))
radius = (size * 1.6) / max(cam_zoom, 0.2) + 0.4
location = mathutils.Vector((
center.x + radius * math.cos(el) * math.cos(az),
center.y + radius * math.cos(el) * math.sin(az),
center.z + radius * math.sin(el),
))
cam = bpy.data.objects.get("OrbitCamera")
if cam is None or cam.type != "CAMERA":
cam_data = bpy.data.cameras.new("OrbitCamera")
cam = bpy.data.objects.new("OrbitCamera", cam_data)
scene.collection.objects.link(cam)
cam.location = location
cam.data.lens = max(10.0, min(250.0, cam_focal))
cam.data.clip_end = max(100.0, size * 10)
cam.rotation_euler = (center - location).to_track_quat("-Z", "Y").to_euler()
scene.camera = cam
def _ensure_camera_and_light():
"""Guarantee a renderable scene: camera framing the content, a light, a world."""
import math
import mathutils
center, size = _scene_bounds()
if cam_mode == "orbit":
_setup_orbit_camera(center, size)
elif scene.camera is None:
cam_data = bpy.data.cameras.new("AutoCamera")
cam = bpy.data.objects.new("AutoCamera", cam_data)
scene.collection.objects.link(cam)
direction = mathutils.Vector((1.0, -1.0, 0.65)).normalized()
cam.location = center + direction * (size * 1.8 + 0.5)
look = center - cam.location
cam.rotation_euler = look.to_track_quat("-Z", "Y").to_euler()
cam_data.clip_end = max(100.0, size * 10)
scene.camera = cam
else:
# An agent-placed camera sometimes points nowhere near the content, which
# yields a black frame. If the scene center is behind the camera or far off
# the view axis, re-aim (keep the location) and back off if too close.
cam = scene.camera
view_dir = (cam.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))).normalized()
to_center = center - cam.matrix_world.translation
if to_center.length < size * 0.5:
cam.location = center - view_dir * (size * 1.8 + 0.5)
to_center = center - cam.matrix_world.translation
if to_center.length > 1e-6 and view_dir.dot(to_center.normalized()) < math.cos(
math.radians(45)
):
cam.rotation_euler = to_center.to_track_quat("-Z", "Y").to_euler()
cam.data.clip_end = max(cam.data.clip_end, size * 10)
scene.render.film_transparent = False
if not any(o.type == "LIGHT" for o in scene.objects):
sun_data = bpy.data.lights.new("AutoSun", type="SUN")
sun_data.energy = 3.0
sun = bpy.data.objects.new("AutoSun", sun_data)
scene.collection.objects.link(sun)
sun.location = center + mathutils.Vector((0, 0, size * 3 + 2))
sun.rotation_euler = (math.radians(35), math.radians(10), math.radians(25))
if scene.world is None:
scene.world = bpy.data.worlds.new("AutoWorld")
scene.world.use_nodes = True
bg = scene.world.node_tree.nodes.get("Background")
if bg is not None and tuple(bg.inputs[0].default_value)[:3] == (0.0, 0.0, 0.0):
bg.inputs[0].default_value = (0.05, 0.05, 0.06, 1.0)
def _configure_cycles_gpu() -> str:
prefs = bpy.context.preferences.addons["cycles"].preferences
for device_type in ("OPTIX", "CUDA"):
try:
prefs.compute_device_type = device_type
except TypeError:
continue
try:
prefs.get_devices()
except Exception as ex: # noqa: BLE001
print(f"get_devices({device_type}) failed: {ex}")
gpus = [d for d in prefs.devices if d.type == device_type]
if gpus:
for d in prefs.devices:
d.use = d.type in {device_type, "CPU"}
scene.cycles.device = "GPU"
print(f"Cycles using {device_type}: {[d.name for d in gpus]}")
return device_type
scene.cycles.device = "CPU"
print("No GPU compute device found, falling back to CPU")
return "CPU"
_ensure_camera_and_light()
if engine.startswith("CYCLES"):
scene.render.engine = "CYCLES"
scene.cycles.samples = samples
scene.cycles.use_denoising = True
if engine == "CYCLES_GPU":
used = _configure_cycles_gpu()
else:
scene.cycles.device = "CPU"
used = "CPU"
elif engine == "WORKBENCH":
scene.render.engine = "BLENDER_WORKBENCH"
used = "WORKBENCH"
else:
scene.render.engine = "BLENDER_EEVEE_NEXT"
scene.eevee.taa_render_samples = samples
used = "EEVEE"
if render_mode == "anim":
bpy.ops.render.render(animation=True)
# Locate whatever Blender actually wrote (it may add a frame-range suffix)
# and move it to the exact requested path.
import glob
import os
if not os.path.exists(out_path):
candidates = sorted(
glob.glob(scene.render.filepath + "*"), key=os.path.getmtime, reverse=True
)
candidates = [c for c in candidates if c.lower().endswith((".mp4", ".mov", ".avi"))]
if candidates:
os.replace(candidates[0], out_path)
else:
bpy.ops.render.render(write_still=True)
print(f"RENDER_DONE device={used} mode={render_mode} path={out_path}")