94 lines
5.5 KiB
Python
94 lines
5.5 KiB
Python
"""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 == "<builtin>" 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()
|