Spaces:
Running on Zero
Running on Zero
File size: 8,766 Bytes
26cd2e7 3204f43 9e0f375 3204f43 9e0f375 26cd2e7 9e0f375 26cd2e7 3204f43 9e0f375 26cd2e7 9e0f375 7a8d450 9e0f375 26cd2e7 3204f43 26cd2e7 3204f43 26cd2e7 707f074 26cd2e7 9e0f375 26cd2e7 9e0f375 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | """
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}")
|