Files
ShortVideoGenerator/tools/blender/inside.py
T

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()