"""Blender 내부 전용. 사용자 startup 설정을 바꾸거나 원본을 저장하지 않는다.""" import json from pathlib import Path import sys import bpy def require(condition, message): if not condition: raise ValueError(message) def main(): job = json.loads(Path(sys.argv[sys.argv.index("--") + 1]).read_text(encoding="utf-8")) require(not bpy.data.libraries, "외부 .blend 라이브러리 링크를 허용하지 않습니다") for image in bpy.data.images: require(image.source in ("GENERATED", "VIEWER") or image.packed_file is not None, "외부 이미지/영상 참조를 허용하지 않습니다: " + image.name) require(not bpy.data.movieclips and not bpy.data.sounds and not bpy.data.volumes, "외부 미디어·볼륨을 포함한 원본을 허용하지 않습니다") for font in bpy.data.fonts: require(font.filepath == "" or font.packed_file is not None, "외부 글꼴은 원본에 포함해야 합니다") scene = bpy.data.scenes.get(job["scene"]) if job["scene"] else next(iter(bpy.data.scenes), None) require(scene is not None and scene.camera is not None, "실제 Scene과 카메라가 필요합니다") for name in job["require_object"]: collection = bpy.data.collections.get(name) require(name in scene.objects or (collection and any(o.name in scene.objects for o in collection.all_objects)), "Scene에 필요한 오브젝트/컬렉션이 없습니다: " + name) for item in bpy.data.objects: require(not any(m.type in ("MESH_CACHE", "MESH_SEQUENCE_CACHE", "FLUID") for m in item.modifiers), "외부 캐시를 쓰는 오브젝트를 허용하지 않습니다") require(not bpy.data.cache_files, "외부 캐시 파일을 허용하지 않습니다") require(all(not s.sequence_editor or not s.sequence_editor.strips for s in bpy.data.scenes), "외부 영상 시퀀스를 허용하지 않습니다") report = dict(blender_version=bpy.app.version_string, scene_name=scene.name, objects=sorted(o.name for o in scene.objects), frame_start=scene.frame_start, frame_end=scene.frame_end, logical_to_blender="logical_frame + 1", external_links=False) if job["operation"] == "render": profile = job["profile"] scene.render.engine = profile["engine"] scene.render.resolution_x = profile["width"] scene.render.resolution_y = profile["height"] scene.render.resolution_percentage = 100 scene.render.fps = profile["fps"] scene.render.fps_base = 1 scene.render.image_settings.file_format = "PNG" scene.render.image_settings.color_mode = "RGB" scene.render.image_settings.color_depth = "8" scene.render.film_transparent = False scene.render.use_file_extension = True scene.render.use_compositing = False scene.render.use_sequencer = False if profile["engine"] == "CYCLES": scene.cycles.seed = profile["seed"] scene.cycles.samples = profile["samples"] scene.cycles.use_adaptive_sampling = False scene.cycles.use_animated_seed = False scene.cycles.device = profile["device"] if profile["device"] == "GPU": preferences = bpy.context.preferences.addons["cycles"].preferences preferences.compute_device_type = "OPTIX" preferences.get_devices() gpus = [d for d in preferences.devices if d.type == "OPTIX"] require(gpus, "Cycles OPTIX GPU가 확인되지 않았습니다") for device in preferences.devices: device.use = device in gpus report["gpu_devices"] = [d.name for d in gpus] else: require(profile["device"] == "GPU", "EEVEE는 GPU 경로입니다") require(profile["seed"] == 0, "EEVEE는 기본 seed=0 경로만 지원합니다") require(hasattr(scene, "eevee") and hasattr(scene.eevee, "taa_render_samples"), "이 Blender의 EEVEE 샘플 설정을 확인할 수 없습니다") scene.eevee.taa_render_samples = profile["samples"] actual = dict(engine=scene.render.engine, device=scene.cycles.device if profile["engine"] == "CYCLES" else "GPU", fps=scene.render.fps, width=scene.render.resolution_x, height=scene.render.resolution_y, samples=scene.cycles.samples if profile["engine"] == "CYCLES" else scene.eevee.taa_render_samples, seed=scene.cycles.seed if profile["engine"] == "CYCLES" else 0) require(actual == profile, "요청 설정이 Blender에서 다르게 적용되었습니다") require(all(scene.frame_start <= f + 1 <= scene.frame_end for f in job["frames"]), "논리 프레임이 실제 Scene 범위 밖입니다") for frame in job["frames"]: scene.frame_set(frame + 1) scene.render.filepath = str(Path(job["directory"]) / ("frame-" + str(frame).zfill(8) + ".png")) bpy.ops.render.render(write_still=True, scene=scene.name) report["render_profile"] = actual report["seed_policy"] = "cycles_seed" if profile["engine"] == "CYCLES" else "eevee_default_no_seed_api" report["rendered_logical_frames"] = job["frames"] Path(job["result"]).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") if __name__ == "__main__": main()