"""LOG496 用:看護師(大城さん)とハコブくんを同じ床に置いて描く(記事の画像とサムネイルの素材)。 python3 scene_render.py <出力png> <場面> [幅 高さ] 場面:drawer(引き出しの前でかがむ)・talk(立って話しかける) 背景は透明で描き、あとで #eceef2 の地に重ねる(影は床の影だけ残す)""" import math import os import sys import bpy from mathutils import Matrix, Vector nurse_blend, hakobu_blend, out, scene = sys.argv[1:5] W, H = (int(sys.argv[5]), int(sys.argv[6])) if len(sys.argv) > 6 else (1920, 1080) bpy.ops.wm.open_mainfile(filepath=nurse_blend) for o in [o for o in bpy.data.objects if o.type in ("CAMERA", "LIGHT")]: bpy.data.objects.remove(o) with bpy.data.libraries.load(hakobu_blend) as (src, dst): dst.objects = [n for n in src.objects if n.startswith("Hakobu")] sc = bpy.context.scene for o in dst.objects: sc.collection.objects.link(o) hk = bpy.data.objects["Hakobu"] for img in bpy.data.images: # ハコブくんの画像は blend と同じフォルダにある if img.name.startswith("hakobu_"): img.filepath = os.path.join(os.path.dirname(os.path.abspath(hakobu_blend)), os.path.basename(img.filepath)) rig = bpy.data.objects["Nurse_Oshiro"] X, Y, Z = (1, 0, 0), (0, 1, 0), (0, 0, 1) def upd(): bpy.context.view_layer.update() def rot_world(bone, axis, deg): pb = rig.pose.bones[bone] h = pb.head.copy() R = Matrix.Rotation(math.radians(deg), 4, Vector(axis).normalized()) pb.matrix = Matrix.Translation(h) @ R @ Matrix.Translation(-h) @ pb.matrix upd() def twist(bone, deg): pb = rig.pose.bones[bone] rot_world(bone, pb.tail - pb.head, deg) def curl(side, deg, thumb): for f in ("Index", "Middle", "Ring", "Little"): for seg, k in (("Proximal", 1.0), ("Intermediate", 1.2)): pb = rig.pose.bones[side + f + seg] pb.rotation_mode = "XYZ" pb.rotation_euler.x = math.radians(deg * k) for seg in ("Proximal", "Intermediate"): pb = rig.pose.bones[side + "Thumb" + seg] pb.rotation_mode = "XYZ" pb.rotation_euler.x = math.radians(thumb) upd() def mouth(kind): for ob in bpy.data.objects: if ob.name.startswith("Nurse_Mouth_"): ob.hide_render = ob.name != "Nurse_Mouth_" + kind # 姿勢はすべて、看護師が -Y を向いた作った時の向きで付けてから、体ごと回す if scene == "drawer": drawer_open = 0.26 rot_world("Spine", X, 16) rot_world("Chest", X, 14) rot_world("RightUpperArm", X, -48) rot_world("RightLowerArm", X, -18) rot_world("LeftUpperArm", X, -30) rot_world("LeftLowerArm", X, -30) curl("Right", 50, 30) curl("Left", 20, 10) rot_world("Head", X, 14) mouth("smile") nurse_xy, nurse_rot = (0.98, -0.30), -60 cam = ((0.55, -3.5, 1.5), (0.5, -0.2, 0.80), 36) else: # talk drawer_open = 0.0 rot_world("LeftUpperArm", X, -30) rot_world("LeftLowerArm", X, -55) twist("LeftLowerArm", -30) twist("LeftHand", -30) curl("Left", 10, 0) rot_world("Head", X, 10) rot_world("Head", Z, -6) mouth("talk") nurse_xy, nurse_rot = (1.05, -0.25), -60 cam = ((0.55, -3.5, 1.5), (0.5, -0.2, 0.84), 36) for o in bpy.data.objects: if o.name.startswith(("Hakobu_Drawer", "Hakobu_Tray")): o.location.y = -drawer_open # ハコブくんの顔(-Y)を右手前へ、看護師の顔を左手前へ。向かい合いつつ、両方とも 30° カメラへ向ける hk.rotation_euler = (0, 0, math.radians(60)) rig.location = (*nurse_xy, 0) rig.rotation_euler = (0, 0, math.radians(nurse_rot)) upd() # 薬の箱(白に青の帯)をトレーの上に if drawer_open > 0: bpy.ops.mesh.primitive_cube_add(size=1, location=(0.04, -0.50, 0.455)) box = bpy.context.object box.parent = hk box.scale = (0.12, 0.08, 0.05) box.rotation_euler = (0, 0, math.radians(8)) mw = bpy.data.materials.new("Box") mw.use_nodes = True mw.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.92, 0.92, 0.9, 1) box.data.materials.append(mw) bpy.ops.mesh.primitive_cube_add(size=1, location=(0.04, -0.50, 0.455)) band = bpy.context.object band.parent = hk band.scale = (0.03, 0.082, 0.052) band.rotation_euler = box.rotation_euler mb = bpy.data.materials.new("Band") mb.use_nodes = True mb.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.05, 0.25, 0.75, 1) band.data.materials.append(mb) # 床:影だけを受ける bpy.ops.mesh.primitive_plane_add(size=12, location=(0, 0, 0)) floor = bpy.context.object floor.is_shadow_catcher = True sc.render.engine = "CYCLES" sc.cycles.device = "CPU" sc.cycles.samples = 48 sc.cycles.use_denoising = True sc.render.film_transparent = True sc.view_settings.view_transform = "Standard" w = bpy.data.worlds.new("W") sc.world = w w.use_nodes = True bg = w.node_tree.nodes["Background"] bg.inputs["Color"].default_value = (0.86, 0.88, 0.92, 1) bg.inputs["Strength"].default_value = 0.8 sun = bpy.data.lights.new("Sun", "SUN") sun.energy = 1.6 sun.angle = math.radians(25) so = bpy.data.objects.new("Sun", sun) so.rotation_euler = (math.radians(50), math.radians(-15), math.radians(-35)) sc.collection.objects.link(so) L = bpy.data.lights.new("F", "AREA") L.energy, L.size = 120, 4 o = bpy.data.objects.new("F", L) o.location = (0.6, -3.0, 1.6) o.rotation_euler = (Vector((0.5, 0, 0.8)) - Vector(o.location)).to_track_quat("-Z", "Y").to_euler() sc.collection.objects.link(o) loc, tgt, lens = cam c = bpy.data.cameras.new("C") c.lens = lens co = bpy.data.objects.new("C", c) co.location = loc co.rotation_euler = (Vector(tgt) - Vector(loc)).to_track_quat("-Z", "Y").to_euler() sc.collection.objects.link(co) sc.camera = co sc.render.resolution_x, sc.render.resolution_y = W, H sc.render.image_settings.color_mode = "RGBA" sc.render.filepath = out bpy.ops.render.render(write_still=True) print("wrote", out)