"""骨の動きを確かめる:決めた姿勢を付けて描く。 python3 nurse_pose.py <出力フォルダ> [姿勢の名前...] 姿勢:rest give receive drawer point bow worried talk (Unity では Humanoid の動き・腕の IK で動かす。ここの姿勢は、骨と重みで服や肌が破れないかの確認用) """ import math import os import sys import bpy # noqa: I001 from mathutils import Matrix, Vector src, out = sys.argv[1], sys.argv[2] want = sys.argv[3:] os.makedirs(out, exist_ok=True) def setup_scene(): sc = bpy.context.scene sc.render.engine = "CYCLES" sc.cycles.device = "CPU" sc.cycles.samples = 20 sc.cycles.use_denoising = 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.80, 0.90, 0.89, 1) bg.inputs["Strength"].default_value = 0.65 for nm, en, sz, loc in (("K", 380, 3, (-2.2, -2.8, 3.0)), ("F", 130, 4, (2.8, -2.0, 1.4))): L = bpy.data.lights.new(nm, "AREA") L.energy, L.size = en, sz o = bpy.data.objects.new(nm, L) o.location = loc o.rotation_euler = (Vector((0, 0, 1.0)) - Vector(loc)).to_track_quat("-Z", "Y").to_euler() sc.collection.objects.link(o) return sc def rot_world(rig, bone, axis, deg, about=None): """骨を、ワールドの軸 axis のまわりに deg 度回す(回す中心は骨の根元)""" pb = rig.pose.bones[bone] h = Vector(about) if about is not None else pb.head.copy() R = Matrix.Rotation(math.radians(deg), 4, Vector(axis).normalized()) pb.matrix = Matrix.Translation(h) @ R @ Matrix.Translation(-h) @ pb.matrix bpy.context.view_layer.update() def twist(rig, bone, deg): """骨を自分の向きのまわりにねじる""" pb = rig.pose.bones[bone] rot_world(rig, bone, pb.tail - pb.head, deg) def curl(rig, side, deg, fingers=("Index", "Middle", "Ring", "Little"), thumb=None): """指を手のひらのほうへ曲げる(骨の z 軸を手のひらに向けてあるので、x 軸まわりに回す)""" for f in fingers: 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) if thumb is not None: for seg in ("Proximal", "Intermediate"): pb = rig.pose.bones[side + "Thumb" + seg] pb.rotation_mode = "XYZ" pb.rotation_euler.x = math.radians(thumb) bpy.context.view_layer.update() def mouth(kind): for ob in bpy.data.objects: if ob.name.startswith("Nurse_Mouth_"): on = ob.name == "Nurse_Mouth_" + kind ob.hide_render = not on ob.hide_viewport = not on def brows(rig, deg, lift=0.0): """眉:deg>0 で眉頭が上がる(困り顔)。lift は上へ(m)""" for tag, sgn in (("L", 1), ("R", -1)): pb = rig.pose.bones["Brow_" + tag] rot_world(rig, "Brow_" + tag, (0, 1, 0), deg * sgn) pb.matrix = Matrix.Translation((0, 0, lift)) @ pb.matrix bpy.context.view_layer.update() def reset(rig): for pb in rig.pose.bones: pb.matrix_basis = Matrix.Identity(4) bpy.context.view_layer.update() mouth("smile") X, Y, Z = (1, 0, 0), (0, 1, 0), (0, 0, 1) def arm_forward(rig, S, up_deg, elbow_deg, palm_up=0.0): """腕を前へ上げる(up_deg)。ひじを曲げて前腕をさらに上げる(elbow_deg)。palm_up で手のひらを上へ""" rot_world(rig, S + "UpperArm", X, -up_deg) rot_world(rig, S + "LowerArm", X, -elbow_deg) if palm_up: sgn = 1 if S == "Right" else -1 twist(rig, S + "LowerArm", sgn * palm_up * 0.5) twist(rig, S + "Hand", sgn * palm_up * 0.5) POSES = {} def pose(fn): POSES[fn.__name__] = fn return fn @pose def rest(rig): pass @pose def give(rig): # 片手で物を差し出す arm_forward(rig, "Right", 55, 30, palm_up=80) curl(rig, "Right", 12, thumb=5) rot_world(rig, "Head", X, 8) rot_world(rig, "LeftUpperArm", Y, 4) @pose def receive(rig): # 両手で受け取る for S in ("Left", "Right"): arm_forward(rig, S, 40, 35, palm_up=80) curl(rig, S, 15, thumb=5) rot_world(rig, "Chest", X, 6) rot_world(rig, "Head", X, 10) @pose def drawer(rig): # 前かがみで引き出しの中の物をつかむ rot_world(rig, "Spine", X, 12) rot_world(rig, "Chest", X, 12) for S in ("Left", "Right"): arm_forward(rig, S, 35, 10) curl(rig, S, 55, thumb=30) rot_world(rig, "Head", X, 12) @pose def point(rig): # 指で示す(話しながら) arm_forward(rig, "Right", 75, 5) rot_world(rig, "RightUpperArm", Z, -18) curl(rig, "Right", 85, fingers=("Middle", "Ring", "Little"), thumb=40) rot_world(rig, "Head", Z, -12) mouth("talk") @pose def bow(rig): # おじぎ rot_world(rig, "Spine", X, 18) rot_world(rig, "Chest", X, 14) rot_world(rig, "Head", X, 6) for S, sgn in (("Left", -1), ("Right", 1)): rot_world(rig, S + "UpperArm", Y, sgn * -6) rot_world(rig, S + "UpperArm", X, -12) @pose def worried(rig): # 困った:首をかしげ、手を口元へ rot_world(rig, "Head", Y, 10) rot_world(rig, "Head", X, 6) arm_forward(rig, "Right", 25, 115) rot_world(rig, "RightUpperArm", Z, -8) curl(rig, "Right", 30, thumb=10) brows(rig, 16, lift=0.002) mouth("worried") @pose def talk(rig): # 話しかける:手のひらを見せる arm_forward(rig, "Left", 30, 55, palm_up=60) curl(rig, "Left", 10, thumb=0) rot_world(rig, "Head", Z, 8) brows(rig, 0, lift=0.003) mouth("talk") bpy.ops.wm.open_mainfile(filepath=src) rig = bpy.data.objects["Nurse_Oshiro"] sc = setup_scene() H = 1.65 views = {"full": ((1.6, -3.0, 1.15), (0, -0.1, 0.95), 50, (520, 820)), "close": ((0.75, -1.25, 1.42), (0, -0.15, 1.22), 50, (560, 560))} for name in (want or list(POSES)): reset(rig) POSES[name](rig) for vname, (loc, tgt, lens, res) in views.items(): for o in [o for o in bpy.data.objects if o.type == "CAMERA"]: bpy.data.objects.remove(o) cam = bpy.data.cameras.new("C") cam.lens = lens co = bpy.data.objects.new("C", cam) 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 = res sc.render.filepath = os.path.join(out, f"pose_{name}_{vname}.png") bpy.ops.render.render(write_still=True) print("wrote", name)