"""粘土のような簡略化した看護師(大城さん)を、形の式(距離の場)から作る。 python3 nurse_build.py [--vox 0.0025] 前が -y、上が z、単位は m。身長 1.65 m。 出力:nurse_clay.blend・nurse_oshiro.fbx・nurse_report.json(部品ごとの三角形の数・骨の名前) FBX は Unity 向け(顔が Unity の +Z、上が +Y)。骨は Unity の Humanoid の名前(Hips・LeftUpperArm など)。 口は5つの形(smile・flat・worried・talk・surprised)を別の部品で入れてあり、Unity で表示を切り替える。 """ import math import os import sys import bpy # noqa: I001 bpy を先に読む import numpy as np from skimage.measure import marching_cubes OUT = sys.argv[1] os.makedirs(OUT, exist_ok=True) VOX = float(sys.argv[sys.argv.index("--vox") + 1]) if "--vox" in sys.argv else 0.0025 # ---------- 形の式 ---------- def length(v): return np.sqrt(np.sum(v * v, axis=0)) def ellipsoid(p, c, r): q = (p - np.array(c).reshape(3, 1)) / np.array(r).reshape(3, 1) k0 = length(q) k1 = length(q / np.array(r).reshape(3, 1)) return k0 * (k0 - 1.0) / np.maximum(k1, 1e-9) def capsule(p, a, b, ra, rb): a = np.array(a, float).reshape(3, 1) b = np.array(b, float).reshape(3, 1) ba = b - a h = np.clip(np.sum((p - a) * ba, axis=0) / np.sum(ba * ba), 0, 1) return length(p - a - ba * h) - (ra + (rb - ra) * h) def smin(a, b, k): h = np.clip(0.5 + 0.5 * (b - a) / k, 0, 1) return b + (a - b) * h - k * h * (1 - h) def smax(a, b, k): return -smin(-a, -b, k) def union(ds, k): d = ds[0] for e in ds[1:]: d = smin(d, e, k) return d def shell(d, t): return np.maximum(d, -(d + t)) # ---------- 寸法 ---------- HC = np.array([0.0, 0.0, 1.516]) # 頭の中心 def H(x, y, z): return (HC + np.array([x, y, z])).tolist() SH = 0.150 # 肩の関節の横の位置 ARM_OUT = math.radians(9) # 腕の開き ARM_FWD = math.radians(4) def arm_points(side): s = 1 if side > 0 else -1 sh = np.array([s * SH, 0.01, 1.300]) d1 = np.array([s * math.sin(ARM_OUT), -math.sin(ARM_FWD), -math.cos(ARM_OUT)]) d1 /= np.linalg.norm(d1) el = sh + d1 * 0.250 d2 = np.array([s * math.sin(ARM_OUT * 0.6), -math.sin(math.radians(14)), -1.0]) d2 /= np.linalg.norm(d2) wr = el + d2 * 0.220 return sh, el, wr, d1, d2 def hand_geo(side): """手の形の骨組み(手首・手のひら・指の関節の位置)。形と骨の両方で使う。 u=手首から指先の向き、n=手のひらの向き(体のほう)、w=親指のほう(前)""" s = 1 if side > 0 else -1 _, _, wr, _, u = arm_points(side) n = np.array([-s, 0.0, 0.0]) # 手のひらは体のほうを向く n = n - u * np.dot(n, u) n /= np.linalg.norm(n) w = np.cross(u, n) * s # 前(-y)向きにそろえる if w[1] > 0: w = -w base = wr + u * 0.085 fingers = {} for name, off, ln, r in (("Index", 0.026, 0.070, 0.0105), ("Middle", 0.009, 0.078, 0.0108), ("Ring", -0.009, 0.074, 0.0105), ("Little", -0.025, 0.060, 0.0095)): a = base + w * off d_1 = u * math.cos(math.radians(14)) + n * math.sin(math.radians(14)) b = a + d_1 * ln * 0.52 d_2 = u * math.cos(math.radians(38)) + n * math.sin(math.radians(38)) c = b + d_2 * ln * 0.48 fingers[name] = (a, b, c, r) ta = wr + u * 0.03 + w * 0.024 + n * 0.006 td = u * 0.55 + w * 0.65 + n * 0.45 td /= np.linalg.norm(td) tb = ta + td * 0.04 td2 = u * 0.75 + w * 0.3 + n * 0.6 td2 /= np.linalg.norm(td2) tc = tb + td2 * 0.032 return {"wr": wr, "u": u, "n": n, "w": w, "palm_end": base, "fingers": fingers, "thumb": (ta, tb, tc)} def palm(p, side): g = hand_geo(side) wr, u, w = g["wr"], g["u"], g["w"] ds = [ellipsoid(p, (wr + u * 0.048).tolist(), (0.024, 0.024, 0.024))] # 手のひらは平たい楕円。軸に沿って並べた小さい楕円で作る for t in (0.02, 0.045, 0.07): c = wr + u * t ds.append(capsule(p, (c - w * 0.022).tolist(), (c + w * 0.022).tolist(), 0.019, 0.019)) return union(ds, 0.010) def finger_segs(p, side): """指の節ごとの形:{骨の名前の後ろ半分: 形}""" g = hand_geo(side) out = {} for name, (a, b, c, r) in g["fingers"].items(): out[name + "Proximal"] = capsule(p, a.tolist(), b.tolist(), r, r * 0.95) out[name + "Intermediate"] = capsule(p, b.tolist(), c.tolist(), r * 0.95, r * 0.88) ta, tb, tc = g["thumb"] out["ThumbProximal"] = capsule(p, ta.tolist(), tb.tolist(), 0.0135, 0.012) out["ThumbIntermediate"] = capsule(p, tb.tolist(), tc.tolist(), 0.012, 0.0105) return out def hand(p, side): return union([palm(p, side)] + list(finger_segs(p, side).values()), 0.010) def head_solid(p): cr = ellipsoid(p, H(0, 0, 0), (0.098, 0.106, 0.108)) jaw = ellipsoid(p, H(0, -0.010, -0.055), (0.080, 0.082, 0.072)) d = smin(cr, jaw, 0.03) cheeks = [ellipsoid(p, H(s * 0.044, -0.055, -0.045), (0.040, 0.036, 0.035)) for s in (-1, 1)] d = union([d] + cheeks, 0.03) nose = ellipsoid(p, H(0, -0.099, -0.026), (0.011, 0.011, 0.014)) d = smin(d, nose, 0.010) ears = [ellipsoid(p, H(s * 0.096, 0.008, -0.020), (0.014, 0.021, 0.029)) for s in (-1, 1)] return union([d] + ears, 0.008) def skin(p): ds = [head_solid(p)] ds.append(capsule(p, (0, 0.012, 1.30), (0, 0.006, 1.45), 0.044, 0.041)) ds.append(ellipsoid(p, (0, 0.012, 1.27), (0.10, 0.066, 0.08))) for sd in (-1, 1): # 首の付け根から肩へ(服の内側を肌で埋める) ds.append(capsule(p, (sd * 0.045, 0.015, 1.335), (sd * 0.12, 0.015, 1.31), 0.024, 0.022)) d = union(ds, 0.03) arms = [] for side in (-1, 1): sh, el, wr, _, _ = arm_points(side) a = smin(capsule(p, sh.tolist(), el.tolist(), 0.040, 0.037), capsule(p, el.tolist(), wr.tolist(), 0.036, 0.029), 0.02) arms.append(smin(a, hand(p, side), 0.012)) d = union([d] + arms, 0.01) # 袖の中に隠れる腕の付け根の肌は作らない(腕を上げて服がのびたとき、肌が服から突き出ないように) for side in (-1, 1): sh, _, _, d1, _ = arm_points(side) q = p - sh.reshape(3, 1) proj = np.sum(q * d1.reshape(3, 1), axis=0) radial = length(q - d1.reshape(3, 1) * proj) inside = np.maximum(np.maximum(-0.12 - proj, proj - 0.134), radial - 0.065) d = smax(d, -inside, 0.005) return d def hairline(p): """頭の中心から見た向きごとの、髪の下の端の高さ(頭の中心からの差)。""" x = p[0] - HC[0] y = p[1] - (HC[1] + 0.005) th = np.degrees(np.arctan2(x, -y)) a = np.abs(th) z = np.interp(a, [0, 35, 58, 68, 78, 88, 104, 120, 180], [0.056, 0.046, 0.030, 0.000, -0.012, 0.024, 0.024, -0.080, -0.100]) # 横に流した前髪:右(+x)が上がり、左(-x)へ下がる front = np.clip(np.cos(np.radians(th)), 0, 1) ** 2 z = z + 0.025 * np.sin(np.radians(th)) * front return z + HC[2] def hair(p): s = ellipsoid(p, H(0, 0.010, 0.006), (0.110, 0.118, 0.120)) nape = ellipsoid(p, H(0, 0.040, -0.062), (0.090, 0.078, 0.070)) s = smin(s, nape, 0.03) d = smax(s, hairline(p) - p[2], 0.010) # 耳の前に下りる短い毛束 bun = ellipsoid(p, H(0, 0.116, -0.088), (0.047, 0.041, 0.044)) return smin(d, bun, 0.012) def top(p): rr = 0.068 c = np.array([0.0, 0.010, 1.085]).reshape(3, 1) b = np.array([0.158 - rr, 0.098 - rr, 0.30 - rr]).reshape(3, 1) qq = np.abs(p - c) - b d = length(np.maximum(qq, 0)) + np.minimum(qq.max(axis=0), 0) - rr # 首から肩へのなで肩の線 ax = np.abs(p[0]) slope = (p[2] - (1.372 - 0.25 * np.clip(ax - 0.05, 0, 0.09) - 0.16 * np.maximum(ax - 0.14, 0))) / 1.03 d = smax(d, slope, 0.06) bust = [ellipsoid(p, (s * 0.060, -0.056, 1.175), (0.058, 0.055, 0.052)) for s in (-1, 1)] d = union([d] + bust, 0.045) outer = d d = shell(d, 0.007) d = smax(d, 0.80 - p[2], 0.004) # すそ neck = np.sqrt(p[0] ** 2 + (p[1] - 0.010) ** 2) - 0.051 vee = np.maximum(p[1] + 0.02, (1.288 + 1.15 * np.abs(p[0])) - p[2]) / 1.5 cut = smin(np.maximum(neck, 1.30 - p[2]), vee, 0.02) # 丸い首まわりと V を角なしでつなぐ d = smax(d, -cut, 0.009) # えりぐり(後ろは丸く、前は V) # ポケット:表面に貼った薄い板(胸の左に1つ、すその左右に1つずつ) for cx, cz, hw, hh in ((0.068, 1.19, 0.034, 0.038), (0.088, 0.875, 0.046, 0.046), (-0.088, 0.875, 0.046, 0.046)): rect = np.maximum(np.abs(p[0] - cx) - hw, np.abs(p[2] - cz) - hh) layer = np.maximum(outer - 0.0035, -outer - 0.004) d = np.minimum(d, np.maximum(np.maximum(rect, layer), p[1])) return d def sleeves(p): """袖は胴とつなげず、別の部品にする(胴とつなげると、腕を動かしたとき継ぎ目が裂ける)。 袖の上の丸みは胴の肩の中に入っていて、腕を上げても袖の筒がそのまま腕といっしょに回る""" sl = [] for side in (-1, 1): sh, el, _, d1, _ = arm_points(side) # 袖の上の丸みは胴の肩より低くして、肩の線から飛び出さないようにする c = capsule(p, (sh + d1 * 0.035).tolist(), (sh + d1 * 0.2).tolist(), 0.051, 0.051) c = shell(c, 0.006) proj = np.sum((p - sh.reshape(3, 1)) * d1.reshape(3, 1), axis=0) sl.append(smax(c, proj - 0.14, 0.004)) return np.minimum(sl[0], sl[1]) def pants(p): ds = [ellipsoid(p, (0, 0.012, 0.86), (0.148, 0.086, 0.10))] for s in (-1, 1): ds.append(capsule(p, (s * 0.080, 0.01, 0.82), (s * 0.095, 0.0, 0.08), 0.074, 0.062)) d = union(ds, 0.03) return smax(d, 0.075 - p[2], 0.004) def shoes(p): ds = [] for s in (-1, 1): ds.append(ellipsoid(p, (s * 0.096, -0.035, 0.028), (0.048, 0.115, 0.052))) d = union(ds, 0.01) return smax(d, -p[2], 0.006) def surface_y(x, z): """顔の正面の表面の y を、前から光線を進めて求める。""" y = -0.2 for _ in range(200): d = float(head_solid(np.array([[x], [y], [z]]))[0]) if d < 1e-5: break y += max(d * 0.8, 1e-5) return y EYE_Z = HC[2] - 0.012 def eye_center(side): x = side * 0.038 return np.array([x, surface_y(x, EYE_Z) + 0.0035, EYE_Z]) def eyes(p): ds = [] for s in (-1, 1): x, yc, z = eye_center(s) # ハコブくんの縦長の丸い目に寄せる:縦のカプセル(側面がまっすぐ)と楕円の中間の形 k = 0.0118 / 0.0062 ps = p.copy() ps[1] = (p[1] - yc) * k + yc pill = capsule(ps, (x, yc, z - 0.0072), (x, yc, z + 0.0072), 0.0118, 0.0118) / k ell = ellipsoid(p, (x, yc, z), (0.0128, 0.0062, 0.0188)) ds.append(0.65 * pill + 0.35 * ell) return union(ds, 0.002) def brow_points(side): xs = np.linspace(0.022, 0.056, 8) pts = [(side * x0, 0.0275 + 0.003 * (1 - ((x0 - 0.037) / 0.019) ** 2)) for x0 in xs] q = [(x0, surface_y(x0, HC[2] + z0) + 0.0012, HC[2] + z0) for x0, z0 in pts] rs = [0.0021 + 0.0005 * (1 - abs(x0 - 0.034) / 0.022) for x0 in xs] return q, rs def brows(p): ds = [] for s in (-1, 1): q, rs = brow_points(s) for i in range(len(q) - 1): ds.append(capsule(p, q[i], q[i + 1], rs[i], rs[i + 1])) return union(ds, 0.002) MOUTH_Z = HC[2] - 0.068 def mouth_line(p, half_w, curve, z0=MOUTH_Z, r=0.0017): """横の線の口。curve>0 で端が上がる(笑顔)、<0 で端が下がる(困り顔)""" xs = np.linspace(-half_w, half_w, 13) q = [] for x0 in xs: z = z0 + curve * (x0 / half_w) ** 2 q.append((x0, surface_y(x0, z) + 0.0012, z)) ds = [capsule(p, q[i], q[i + 1], r, r) for i in range(len(q) - 1)] return union(ds, 0.002) def mouth_open(p, rx, rz, flat_top=True, z0=MOUTH_Z): """開いた口(話す・驚く)。flat_top で上がまっすぐ・下が丸い形""" y = surface_y(0.0, z0) zc = z0 + (rz * 0.35 if flat_top else 0.0) d = ellipsoid(p, (0.0, y + 0.0028, zc), (rx, 0.0045, rz)) if flat_top: d = smax(d, p[2] - (zc - rz * 0.05), 0.0015) return d MOUTHS = { # 口の形(Unity で表示を切り替える。既定は smile) "smile": lambda p: mouth_line(p, 0.015, 0.006), "flat": lambda p: mouth_line(p, 0.012, 0.0), "worried": lambda p: mouth_line(p, 0.011, -0.004, z0=MOUTH_Z - 0.002), "talk": lambda p: mouth_open(p, 0.011, 0.0085), "surprised": lambda p: mouth_open(p, 0.0065, 0.0085, flat_top=False), } # ---------- 格子で面にする ---------- def mesh_of(fn, lo, hi, vox): lo, hi = np.array(lo, float) + vox * 0.37, np.array(hi, float) # 格子の点が平らな面にちょうど乗らないようにずらす n = np.ceil((hi - lo) / vox).astype(int) + 1 xs = lo[0] + np.arange(n[0]) * vox ys = lo[1] + np.arange(n[1]) * vox zs = lo[2] + np.arange(n[2]) * vox vol = np.empty((n[0], n[1], n[2]), np.float32) gx, gy = np.meshgrid(xs, ys, indexing="ij") step = 24 for k0 in range(0, n[2], step): zz = zs[k0:k0 + step] P = np.stack([np.repeat(gx[..., None], len(zz), 2), np.repeat(gy[..., None], len(zz), 2), np.broadcast_to(zz, (n[0], n[1], len(zz)))]).reshape(3, -1) vol[:, :, k0:k0 + step] = fn(P).reshape(n[0], n[1], len(zz)) v, f, _, _ = marching_cubes(vol, 0.0, spacing=(vox, vox, vox)) return v + lo, f def to_blender(name, v, f, col, rough=0.6, sss=0.0, ratio=None): me = bpy.data.meshes.new(name) me.from_pydata(v.tolist(), [], f[:, ::-1].tolist()) me.update() ob = bpy.data.objects.new(name, me) bpy.context.scene.collection.objects.link(ob) if ratio: m = ob.modifiers.new("dec", "DECIMATE") m.ratio = ratio bpy.context.view_layer.objects.active = ob bpy.ops.object.modifier_apply(modifier="dec") for poly in ob.data.polygons: poly.use_smooth = True mat = bpy.data.materials.new(name + "_Mat") mat.use_nodes = True b = mat.node_tree.nodes["Principled BSDF"] b.inputs["Base Color"].default_value = (*[(c / 255) ** 2.2 for c in col], 1) b.inputs["Roughness"].default_value = rough if sss: b.inputs["Subsurface Weight"].default_value = sss b.inputs["Subsurface Radius"].default_value = (0.02, 0.008, 0.005) ob.data.materials.append(mat) print(name, len(ob.data.polygons), "faces") return ob SKIN = (232, 180, 142) HAIR = (38, 30, 27) ROYAL = (44, 84, 170) WHITE = (238, 238, 234) DARK = (44, 34, 30) LIP = (150, 72, 66) # ---------- 骨 ---------- # Unity の Humanoid がそのまま読み取れる名前にする(Left=本人の左=+x。前は -y)。 # 目・眉の骨は Humanoid に入れない追加の骨(まばたき・眉の上げ下げをスクリプトで動かす)。 def leg_points(side): s = 1 if side > 0 else -1 hip = np.array([s * 0.085, 0.010, 0.860]) knee = np.array([s * 0.090, 0.000, 0.470]) ankle = np.array([s * 0.095, 0.004, 0.085]) ball = np.array([s * 0.096, -0.075, 0.025]) tip = np.array([s * 0.096, -0.140, 0.022]) return hip, knee, ankle, ball, tip FWD = (0, -1, 0) def bone_layout(): """{骨の名前: (根元, 先, 親, 骨の z 軸を向ける向き)}""" B = { "Hips": ((0, 0.012, 0.90), (0, 0.012, 0.98), None, FWD), "Spine": ((0, 0.012, 0.98), (0, 0.012, 1.11), "Hips", FWD), "Chest": ((0, 0.012, 1.11), (0, 0.012, 1.30), "Spine", FWD), "Neck": ((0, 0.012, 1.31), (0, 0.006, 1.42), "Chest", FWD), "Head": ((0, 0.006, 1.42), (0, 0.0, 1.62), "Neck", FWD), } for side, L, tag in ((1, "Left", "L"), (-1, "Right", "R")): s = side sh, el, wr, _, _ = arm_points(side) g = hand_geo(side) n = g["n"] B[L + "Shoulder"] = ((s * 0.030, 0.012, 1.325), sh, "Chest", FWD) B[L + "UpperArm"] = (sh, el, L + "Shoulder", FWD) B[L + "LowerArm"] = (el, wr, L + "UpperArm", FWD) B[L + "Hand"] = (wr, g["palm_end"], L + "LowerArm", n) for fname, (a, b, c, _) in g["fingers"].items(): B[L + fname + "Proximal"] = (a, b, L + "Hand", n) B[L + fname + "Intermediate"] = (b, c, L + fname + "Proximal", n) ta, tb, tc = g["thumb"] B[L + "ThumbProximal"] = (ta, tb, L + "Hand", n) B[L + "ThumbIntermediate"] = (tb, tc, L + "ThumbProximal", n) hip, knee, ankle, ball, tip = leg_points(side) B[L + "UpperLeg"] = (hip, knee, "Hips", FWD) B[L + "LowerLeg"] = (knee, ankle, L + "UpperLeg", FWD) B[L + "Foot"] = (ankle, ball, L + "LowerLeg", (0, 0, 1)) B[L + "Toes"] = (ball, tip, L + "Foot", (0, 0, 1)) ec = eye_center(side) B["Eye_" + tag] = (ec, ec + np.array([0, -0.015, 0]), "Head", (0, 0, 1)) q, _ = brow_points(side) bc = np.mean(np.array(q), axis=0) B["Brow_" + tag] = (bc, bc + np.array([0, -0.012, 0]), "Head", (0, 0, 1)) return B def band(p, z0, z1): return np.maximum(z0 - p[2], p[2] - z1) def regions(kind, p): """部品の種類ごとに、骨ごとの「その骨の受け持つ形までの距離」を返す。重みはここから決める""" R = {} if kind == "skin": R["Head"] = head_solid(p) R["Neck"] = capsule(p, (0, 0.012, 1.30), (0, 0.006, 1.45), 0.044, 0.041) R["Chest"] = ellipsoid(p, (0, 0.012, 1.27), (0.10, 0.066, 0.08)) for side, L in ((1, "Left"), (-1, "Right")): sh, el, wr, _, _ = arm_points(side) R[L + "Shoulder"] = capsule(p, (side * 0.045, 0.015, 1.335), (side * 0.12, 0.015, 1.31), 0.024, 0.022) R[L + "UpperArm"] = capsule(p, sh.tolist(), el.tolist(), 0.040, 0.037) R[L + "LowerArm"] = capsule(p, el.tolist(), wr.tolist(), 0.036, 0.029) R[L + "Hand"] = palm(p, side) for k, v in finger_segs(p, side).items(): R[L + k] = v elif kind == "top": rr = 0.068 c = np.array([0.0, 0.010, 1.085]).reshape(3, 1) b = np.array([0.158 - rr, 0.098 - rr, 0.30 - rr]).reshape(3, 1) qq = np.abs(p - c) - b torso = length(np.maximum(qq, 0)) + np.minimum(qq.max(axis=0), 0) - rr R["Hips"] = np.maximum(torso, band(p, 0.60, 0.94)) R["Spine"] = np.maximum(torso, band(p, 0.94, 1.10)) R["Chest"] = np.maximum(torso, band(p, 1.10, 1.45)) for side, L in ((1, "Left"), (-1, "Right")): sh, _, _, _, _ = arm_points(side) R[L + "Shoulder"] = capsule(p, (side * 0.045, 0.012, 1.33), sh.tolist(), 0.045, 0.045) elif kind == "pants": R["Hips"] = ellipsoid(p, (0, 0.012, 0.86), (0.148, 0.086, 0.10)) for side, L in ((1, "Left"), (-1, "Right")): hip, knee, ankle, _, _ = leg_points(side) R[L + "UpperLeg"] = capsule(p, hip.tolist(), knee.tolist(), 0.074, 0.066) R[L + "LowerLeg"] = capsule(p, knee.tolist(), ankle.tolist(), 0.066, 0.062) elif kind == "shoes": for side, L in ((1, "Left"), (-1, "Right")): _, _, ankle, ball, tip = leg_points(side) R[L + "Foot"] = capsule(p, ankle.tolist(), ball.tolist(), 0.05, 0.05) R[L + "Toes"] = capsule(p, ball.tolist(), tip.tolist(), 0.045, 0.04) return R def tau_of(bone): if any(k in bone for k in ("Index", "Middle", "Ring", "Little")): return 0.0022 if "Thumb" in bone: return 0.0025 if bone.endswith("Hand"): return 0.005 if bone in ("Hips", "Spine", "Chest"): return 0.018 if "Leg" in bone or "Foot" in bone or "Toes" in bone: return 0.018 return 0.011 def weight_mesh(ob, kind): P = np.array([v.co for v in ob.data.vertices]).T # 根は原点・回転なしなので形の座標そのまま R = regions(kind, P) names = list(R) D = np.stack([R[k] for k in names]) dmin = D.min(axis=0) W = np.stack([np.exp(-np.maximum(D[i] - dmin, 0) / tau_of(k)) for i, k in enumerate(names)]) W = W / W.sum(axis=0) top4 = np.argsort(-W, axis=0)[:4] groups = {k: ob.vertex_groups.new(name=k) for k in names} for vi in range(P.shape[1]): idx = top4[:, vi] w = W[idx, vi] w = w / w.sum() for j, ww in zip(idx, w): if ww > 0.01: groups[names[j]].add([vi], float(ww), "REPLACE") def weight_sleeves(ob): """袖はまるごと上腕の骨に付ける(付け根を肩の骨と分け合うと、腕を上げた時に袖の山がゆがんだ)。 袖の山は肩の関節のすぐ外にあるので、腕と一緒に回しても肩から離れない""" for side, L in ((1, "Left"), (-1, "Right")): g = ob.vertex_groups.new(name=L + "UpperArm") g.add([v.index for v in ob.data.vertices if v.co.x * side > 0], 1.0, "REPLACE") def weight_whole(ob, bone): g = ob.vertex_groups.new(name=bone) g.add(list(range(len(ob.data.vertices))), 1.0, "REPLACE") def weight_by_side(ob, prefix): for tag, sgn in (("L", 1), ("R", -1)): g = ob.vertex_groups.new(name=prefix + tag) g.add([v.index for v in ob.data.vertices if v.co.x * sgn > 0], 1.0, "REPLACE") def build_armature(): from mathutils import Vector arm = bpy.data.armatures.new("Nurse_Oshiro_Rig") rig = bpy.data.objects.new("Nurse_Oshiro", arm) # 根:床の中心 bpy.context.scene.collection.objects.link(rig) bpy.context.view_layer.objects.active = rig bpy.ops.object.mode_set(mode="EDIT") B = bone_layout() eb = {} for name, (h, t, _, _) in B.items(): b = arm.edit_bones.new(name) b.head = Vector([float(x) for x in h]) b.tail = Vector([float(x) for x in t]) eb[name] = b for name, (_, _, parent, zdir) in B.items(): if parent: eb[name].parent = eb[parent] eb[name].align_roll(Vector([float(x) for x in zdir])) bpy.ops.object.mode_set(mode="OBJECT") return rig SKIN = (232, 180, 142) HAIR = (38, 30, 27) ROYAL = (44, 84, 170) WHITE = (238, 238, 234) DARK = (44, 34, 30) LIP = (150, 72, 66) MOUTH_IN = (120, 48, 46) bpy.ops.wm.read_factory_settings(use_empty=True) FACE_LO, FACE_HI = (-0.08, -0.14, HC[2] - 0.04), (0.08, -0.06, HC[2] + 0.05) MOUTH_LO, MOUTH_HI = (-0.03, -0.13, HC[2] - 0.095), (0.03, -0.07, HC[2] - 0.05) # (名前, 形, 範囲, 格子, 色, 粗さ, 肌の透け, 三角形を残す割合, 重みの付け方) parts = [ ("Skin", skin, (-0.34, -0.18, 0.58), (0.34, 0.15, 1.67), VOX, SKIN, 0.55, 0.12, 0.07, ("mesh", "skin")), ("Hair", hair, (-0.15, -0.16, 1.32), (0.15, 0.21, 1.69), VOX, HAIR, 0.5, 0, 0.06, ("whole", "Head")), ("Top", top, (-0.30, -0.15, 0.78), (0.30, 0.15, 1.40), VOX, ROYAL, 0.75, 0, 0.025, ("mesh", "top")), ("Sleeves", sleeves, (-0.30, -0.12, 1.08), (0.30, 0.12, 1.40), VOX, ROYAL, 0.75, 0, 0.05, ("sleeves", None)), ("Pants", pants, (-0.20, -0.13, 0.05), (0.20, 0.14, 1.0), VOX * 1.4, ROYAL, 0.75, 0, 0.04, ("mesh", "pants")), ("Shoes", shoes, (-0.17, -0.17, -0.01), (0.17, 0.10, 0.11), VOX, WHITE, 0.5, 0, 0.05, ("mesh", "shoes")), ("Eyes", eyes, FACE_LO, FACE_HI, 0.0008, DARK, 0.35, 0, 0.15, ("side", "Eye_")), ("Brows", brows, FACE_LO, FACE_HI, 0.0008, DARK, 0.35, 0, 0.15, ("side", "Brow_")), ] for k, fn in MOUTHS.items(): col = MOUTH_IN if k in ("talk", "surprised") else LIP parts.append(("Mouth_" + k, fn, MOUTH_LO, MOUTH_HI, 0.0006, col, 0.5, 0, 0.3, ("whole", "Head"))) rig = build_armature() tris = {} for name, fn, lo, hi, vox, col, rough, sss, ratio, how in parts: v, f = mesh_of(fn, lo, hi, vox) ob = to_blender("Nurse_" + name, v, f, col, rough, sss, ratio) if how[0] == "mesh": weight_mesh(ob, how[1]) elif how[0] == "sleeves": weight_sleeves(ob) elif how[0] == "whole": weight_whole(ob, how[1]) else: weight_by_side(ob, how[1]) ob.parent = rig mod = ob.modifiers.new("Armature", "ARMATURE") mod.object = rig if name.startswith("Mouth_") and name != "Mouth_smile": ob.hide_render = True # 既定の顔は笑顔。ほかの口は隠しておく ob.hide_viewport = True tris[ob.name] = sum(len(p.vertices) - 2 for p in ob.data.polygons) tris["_total_triangles"] = sum(tris.values()) bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT, "nurse_clay.blend")) for ob in bpy.data.objects: # FBX には隠した口も入れる(Unity で切り替える) ob.hide_viewport = False bpy.ops.export_scene.fbx( filepath=os.path.join(OUT, "nurse_oshiro.fbx"), use_selection=False, object_types={"ARMATURE", "MESH"}, apply_unit_scale=True, apply_scale_options="FBX_SCALE_ALL", axis_forward="-Z", axis_up="Y", bake_space_transform=False, mesh_smooth_type="FACE", use_mesh_modifiers=False, use_triangles=True, add_leaf_bones=False, primary_bone_axis="Y", secondary_bone_axis="X", armature_nodetype="NULL", bake_anim=False, path_mode="STRIP", embed_textures=False, ) import json # noqa: E402 json.dump({"height_m": 1.65, "triangles": tris, "bones": list(bone_layout())}, open(os.path.join(OUT, "nurse_report.json"), "w"), ensure_ascii=False, indent=1) print(json.dumps(tris, ensure_ascii=False))