# 2.5頭身のアニメ調デフォルメキャラを、Blenderのスクリプトだけで組み立ててVRM 1.0で書き出す
# 使い方: VRM Add-on for Blender を入れた Blender 5.2 で  blender -b --python build_chibi.py -- out.vrm
import bpy
import bmesh
import math
import sys
from mathutils import Vector, Matrix
from mathutils.bvhtree import BVHTree

OUT = sys.argv[sys.argv.index("--") + 1] if "--" in sys.argv else "chibi.vrm"

# ---------- シーン初期化 ----------
bpy.ops.wm.read_homefile(use_empty=True)
scene = bpy.context.scene

def hexcol(h, a=1.0):
    h = h.lstrip("#")
    srgb = [int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)]
    lin = [c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in srgb]
    return (*lin, a)

# ---------- マテリアル（MToon） ----------
def mtoon(name, base, shade=None, outline=0.0, toony=0.95, shift=-0.05, rim=None):
    m = bpy.data.materials.new(name)
    ext = m.vrm_addon_extension.mtoon1
    ext.enabled = True
    ext.pbr_metallic_roughness.base_color_factor = hexcol(base)
    mt = ext.extensions.vrmc_materials_mtoon
    # 陰色を基本色と同じにすると、光の向きに関係なく塗りつぶしで描かれる（目や口などのデカール用）
    mt.shade_color_factor = hexcol(shade or base)[:3]
    mt.shading_toony_factor = toony
    mt.shading_shift_factor = shift
    if rim:
        mt.parametric_rim_color_factor = hexcol(rim)[:3]
        mt.parametric_rim_fresnel_power_factor = 4.0
        mt.parametric_rim_lift_factor = 0.0
    if outline > 0:
        mt.outline_width_mode = "worldCoordinates"
        mt.outline_width_factor = outline
        mt.outline_color_factor = hexcol("#3b2630")[:3]
        mt.outline_lighting_mix_factor = 0.0
    return m

OL = 0.0022  # アウトラインの太さ（メートル）
MAT = {
    "skin": mtoon("Skin", "#ffe7da", "#f2b5a5", outline=OL, shift=-0.1),
    "hair": mtoon("Hair", "#7d6ae0", "#4a3a9e", outline=OL, shift=0.0, rim="#b8a8ff"),
    "shirt": mtoon("Shirt", "#fbfbff", "#c3c6e0", outline=OL),
    "skirt": mtoon("Skirt", "#2c3566", "#1a1f40", outline=OL),
    "ribbon": mtoon("Ribbon", "#e8475f", "#a82640", outline=OL),
    "sock": mtoon("Sock", "#2a2a3a", "#16161f", outline=OL),
    "shoe": mtoon("Shoe", "#6b4232", "#40261c", outline=OL),
    # 以下は顔に貼るデカール。陰を出さず、輪郭線も付けない
    "iris": mtoon("Iris", "#3d2a78"),
    "iris_light": mtoon("IrisLight", "#9b7cf0"),
    "pupil": mtoon("Pupil", "#1c1238"),
    "highlight": mtoon("Highlight", "#ffffff"),
    "lash": mtoon("Lash", "#2b1a24"),
    "brow": mtoon("Brow", "#4a3a8e"),
    "mouth": mtoon("Mouth", "#b8384e"),
    "blush": mtoon("Blush", "#ffb8c2"),
}

def assign(obj, mat):
    obj.data.materials.clear()
    obj.data.materials.append(mat)

def finish(obj, mat, bone, smooth=True):
    assign(obj, mat)
    if smooth:
        for p in obj.data.polygons:
            p.use_smooth = True
    vg = obj.vertex_groups.new(name=bone)
    vg.add(range(len(obj.data.vertices)), 1.0, "REPLACE")
    return obj

def apply_mods(obj):
    bpy.context.view_layer.objects.active = obj
    for m in list(obj.modifiers):
        bpy.ops.object.modifier_apply(modifier=m.name)

def link(name, bm):
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    obj = bpy.data.objects.new(name, me)
    scene.collection.objects.link(obj)
    return obj

def uv_sphere(name, center, scale, seg=48, ring=24):
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=ring, radius=1.0)
    bmesh.ops.scale(bm, vec=scale, verts=bm.verts)
    bmesh.ops.translate(bm, vec=center, verts=bm.verts)
    return link(name, bm)

def tube(name, a, b, r1, r2, seg=24, caps=True):
    """点aから点bへ伸びる、半径r1→r2の筒"""
    a, b = Vector(a), Vector(b)
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=caps, segments=seg, radius1=r1, radius2=r2, depth=(b - a).length)
    rot = (b - a).to_track_quat("Z", "Y").to_matrix().to_4x4()
    bmesh.ops.transform(bm, matrix=Matrix.Translation((a + b) / 2) @ rot, verts=bm.verts)
    return link(name, bm)

# ---------- 頭 ----------
HC = Vector((0, 0, 0.86))          # 頭の中心
HR = 0.25                          # 頭の半径
HS = Vector((1.0, 0.95, 0.92))     # 頭の縦横比

head = uv_sphere("Head", HC, HR * HS, seg=64, ring=32)
# あごを細くする: 中心より下の頂点ほど横幅を絞る
for v in head.data.vertices:
    t = max(0.0, (HC.z - v.co.z) / (HR * HS.z))
    v.co.x *= 1 - 0.28 * t ** 1.3
    if v.co.y < 0:  # 顔の前面の下側を少し前へ出し、あごの先を作る
        v.co.y -= 0.02 * t ** 2

# 法線の転写: あごを削った後も、陰は「ただの球」のように丸く出したい
proxy = uv_sphere("HeadNormalProxy", HC, HR * HS, seg=64, ring=32)
for p in proxy.data.polygons:
    p.use_smooth = True
dt = head.modifiers.new("NormalTransfer", "DATA_TRANSFER")
dt.object = proxy
dt.use_loop_data = True
dt.data_types_loops = {"CUSTOM_NORMAL"}
dt.loop_mapping = "POLYINTERP_NEAREST"
finish(head, MAT["skin"], "head")
apply_mods(head)
bpy.data.objects.remove(proxy)

# 顔に貼るデカールの投影用（正面から光線を飛ばして頭の表面を探す）
dg = bpy.context.evaluated_depsgraph_get()
head_bvh = BVHTree.FromObject(head, dg)

def project(x, z, offset):
    hit, normal, _, _ = head_bvh.ray_cast(Vector((x, -1.0, z)), Vector((0, 1, 0)))
    if hit is None:
        return Vector((x, -HR, z))
    return hit + normal * offset

def ellipse_pts(cx, cz, a, b, n=32):
    return [(cx + a * math.cos(2 * math.pi * i / n), cz + b * math.sin(2 * math.pi * i / n)) for i in range(n)]

def decal(name, pts2d, offset, mat, bone="head"):
    """2D多角形(x,z)を顔の表面に投影して貼る。中心に頂点を置き扇状に面を張る"""
    cx = sum(p[0] for p in pts2d) / len(pts2d)
    cz = sum(p[1] for p in pts2d) / len(pts2d)
    bm = bmesh.new()
    c = bm.verts.new(project(cx, cz, offset))
    ring = [bm.verts.new(project(x, z, offset)) for x, z in pts2d]
    for i in range(len(ring)):
        bm.faces.new((c, ring[i], ring[(i + 1) % len(ring)]))
    obj = link(name, bm)
    obj["pts2d"] = [list(p) for p in [(cx, cz)] + list(pts2d)]
    obj["offset"] = offset
    return finish(obj, mat, bone)

def lash_pts(cx, cz, a, b, side):
    """目の上側を縁取るまつ毛の帯。外側（目尻）を太く、少し跳ね上げる"""
    outer, inner = [], []
    n = 16
    for i in range(n + 1):
        th = math.radians(10 + 160 * i / n)
        x, z = math.cos(th), math.sin(th)
        # 目尻側（side方向）ほど太く
        w = 0.010 + 0.008 * max(0.0, x * side)
        outer.append((cx + (a + 0.006) * x, cz + (b + 0.004) * z + w * 0.6))
        inner.append((cx + (a - 0.002) * x, cz + (b - 0.006) * z))
    wing = (cx + side * (a + 0.014), cz + b * 0.25)
    pts = outer + ([wing] if side > 0 else []) + inner[::-1]
    if side < 0:
        pts = [wing] + pts
    return pts

EYE_Z, EYE_X, EA, EB = 0.80, 0.088, 0.037, 0.050
EYES = []  # (obj, cx, cz, side)
for side, tag in ((1, "L"), (-1, "R")):
    cx = EYE_X * side
    parts = [
        decal(f"Iris{tag}", ellipse_pts(cx, EYE_Z, EA, EB), 0.0020, MAT["iris"]),
        decal(f"IrisLight{tag}", ellipse_pts(cx, EYE_Z - 0.016, EA * 0.78, EB * 0.55), 0.0026, MAT["iris_light"]),
        decal(f"Pupil{tag}", ellipse_pts(cx, EYE_Z - 0.004, EA * 0.40, EB * 0.45), 0.0032, MAT["pupil"]),
        # ハイライトは左右の目で同じ向き（光源が片側にあるように見せる）
        decal(f"HiBig{tag}", ellipse_pts(cx - 0.012, EYE_Z + 0.018, 0.011, 0.013, 20), 0.0038, MAT["highlight"]),
        decal(f"HiSmall{tag}", ellipse_pts(cx + 0.013, EYE_Z - 0.022, 0.005, 0.005, 12), 0.0038, MAT["highlight"]),
        decal(f"Lash{tag}", lash_pts(cx, EYE_Z, EA, EB, side), 0.0044, MAT["lash"]),
    ]
    for p in parts:
        EYES.append((p, cx, EYE_Z, side))
    brow = decal(f"Brow{tag}", [
        (cx - 0.026, 0.869), (cx, 0.877), (cx + 0.026, 0.869),
        (cx + 0.026, 0.8635), (cx, 0.871), (cx - 0.026, 0.8635)], 0.0030, MAT["brow"])
    EYES.append((brow, cx, 0.870, side))
    decal(f"Blush{tag}", ellipse_pts(side * 0.118, 0.735, 0.026, 0.012, 20), 0.0012, MAT["blush"])

MOUTH_Z = 0.695
mouth = decal("Mouth", ellipse_pts(0, MOUTH_Z, 0.016, 0.0045, 24), 0.0020, MAT["mouth"])

# ---------- 髪 ----------
HAIR_SCALE = 1.05

def back_cut(ux, uy):
    return -0.55 - 0.22 * abs(math.sin(math.atan2(ux, uy) * 4.5))

def hair_delete(u):
    ux, uy, uz = u
    if uz < -0.80:
        return True  # 首の穴
    if uy > 0 and uz < back_cut(ux, uy):
        return True  # 後ろ髪の裾をギザギザに
    if uy < -0.05 and uz < 0.45 and abs(ux) < 0.62:
        return True  # 顔の前（前髪の毛束で覆う）
    if uy < 0.20 and uz < -0.45:
        return True  # 頬より下の横（もみあげの毛束で覆う）
    return False

# 後ろ髪と頭頂部は球を切り抜いた殻で作る
bm = bmesh.new()
bmesh.ops.create_uvsphere(bm, u_segments=96, v_segments=48, radius=1.0)
bmesh.ops.delete(bm, geom=[v for v in bm.verts if hair_delete(v.co)], context="VERTS")
def back_cut(ux, uy):
    return -0.55 - 0.22 * abs(math.sin(math.atan2(ux, uy) * 4.5))

for v in bm.verts:
    if v.is_boundary and v.co.y > 0 and v.co.z < -0.4:
        a = math.atan2(v.co.x, v.co.y)
        uz = back_cut(v.co.x, v.co.y)
        h = math.sqrt(1 - uz * uz)
        v.co = Vector((h * math.sin(a), h * math.cos(a), uz))
bmesh.ops.scale(bm, vec=HR * HS * HAIR_SCALE, verts=bm.verts)
bmesh.ops.translate(bm, vec=HC, verts=bm.verts)
hair = link("HairShell", bm)
sol = hair.modifiers.new("Thickness", "SOLIDIFY")
sol.thickness = 0.010
sol.offset = -1
finish(hair, MAT["hair"], "head")
apply_mods(hair)

def curve_strand(name, pts, radii, mat, bone, res=6):
    cu = bpy.data.curves.new(name, "CURVE")
    cu.dimensions = "3D"
    cu.bevel_depth = 1.0
    cu.bevel_resolution = 2
    cu.resolution_u = res
    cu.use_fill_caps = True
    sp = cu.splines.new("NURBS")
    sp.points.add(len(pts) - 1)
    for p, co, r in zip(sp.points, pts, radii):
        p.co = (*co, 1.0)
        p.radius = r
    sp.use_endpoint_u = True
    sp.order_u = 3
    ob = bpy.data.objects.new(name, cu)
    scene.collection.objects.link(ob)
    bpy.context.view_layer.objects.active = ob
    for o in bpy.context.selected_objects:
        o.select_set(False)
    ob.select_set(True)
    bpy.ops.object.convert(target="MESH")
    ob = bpy.context.view_layer.objects.active
    return finish(ob, mat, bone)

def on_head(ux, uz, k=1.06):
    """単位球上の(ux, uz)を頭の前面の点に変換する"""
    uy = -math.sqrt(max(0.0, 1 - ux * ux - uz * uz))
    return tuple(HC + Vector((ux, uy, uz)) * k * Vector(HR * HS))

def hug_head(obj, lo=1.035, hi=1.075):
    """毛束を頭の殻の厚みに押し込んで平たくする（丸い筒のままだと額から浮く）"""
    sc = Vector(HR * HS)
    for v in obj.data.vertices:
        u = (v.co - HC)
        u = Vector((u.x / sc.x, u.y / sc.y, u.z / sc.z))
        r = u.length
        if r < 1e-6:
            continue
        nr = lo + (hi - lo) * max(0.0, min(1.0, (r - 0.98) / 0.18))
        u = u * (nr / r)
        v.co = HC + Vector((u.x * sc.x, u.y * sc.y, u.z * sc.z))

# 前髪: 先の尖った毛束を額に並べる (中心ux, 毛先uz)
BANGS = [(-0.80, -0.25), (0.80, -0.22), (-0.72, -0.12), (0.72, -0.10), (-0.66, -0.05), (-0.50, 0.20), (-0.34, 0.12), (-0.17, 0.22), (0.0, 0.14),
         (0.17, 0.24), (0.33, 0.13), (0.49, 0.21), (0.65, -0.02)]
for i, (cx, tip) in enumerate(BANGS):
    path = []
    for t in (0.0, 0.3, 0.6, 0.85, 1.0):
        ux = cx * (0.25 + 0.75 * t)
        uz = 0.93 + (tip - 0.93) * t
        path.append(on_head(ux, uz, 1.06))
    r0 = 0.030 if abs(cx) < 0.6 else 0.026
    ob = curve_strand(f"Bang{i}", path, [r0 * 0.35, r0, r0 * 0.9, r0 * 0.5, 0.0015], MAT["hair"], "head", res=6)
    hug_head(ob, 1.068, 1.10)

# もみあげ: 耳の前から頬の横へ垂らす
for side, tag in ((1, "L"), (-1, "R")):
    for j, (ux0, ux1, tip, r0) in enumerate(((0.80, 0.78, -0.85, 0.030), (0.88, 0.90, -0.55, 0.026))):
        path = [on_head(side * (ux0 - 0.06 + 0.06 * t), 0.35 + (tip - 0.35) * t, 1.06) for t in (0.0, 0.3, 0.6, 0.85, 1.0)]
        ob = curve_strand(f"SideLock{tag}{j}", path, [r0 * 0.8, r0, r0 * 0.9, r0 * 0.55, 0.0015], MAT["hair"], "head", res=8)
        hug_head(ob, 1.03, 1.08)

def seg_dist(p, a, b):
    ab = b - a
    t = max(0.0, min(1.0, (p - a).dot(ab) / ab.length_squared))
    return (p - (a + ab * t)).length, t

def weight_tail(obj, tag, pts):
    """ツインテールの頂点に、骨の並び（pts[1]→pts[5]の4本）に沿った重みを付ける。根元は頭に残す"""
    pts = [Vector(p) for p in pts]
    vg_head = obj.vertex_groups["head"]
    groups = [obj.vertex_groups.new(name=f"tail{tag}{i}") for i in range(4)]
    for v in obj.data.vertices:
        best = min(range(4), key=lambda i: seg_dist(v.co, pts[i + 1], pts[i + 2])[0])
        d, t = seg_dist(v.co, pts[best + 1], pts[best + 2])
        if best == 0 and t < 0.05:
            continue  # 根元（結び目付近）は頭に固定
        vg_head.remove([v.index])
        if best < 3 and t > 0.5:
            w = (t - 0.5)  # 骨の後半は次の骨と混ぜて、折れ目を滑らかにする
            groups[best].add([v.index], 1.0 - w, "REPLACE")
            groups[best + 1].add([v.index], w, "REPLACE")
        else:
            groups[best].add([v.index], 1.0, "REPLACE")

# ツインテール（揺れもの用に骨を4本ずつ入れる）
TAILS = {}
for side, tag in ((1, "L"), (-1, "R")):
    s = side
    pts = [(s * 0.205, 0.06, 0.99), (s * 0.29, 0.10, 0.95), (s * 0.345, 0.12, 0.80),
           (s * 0.345, 0.11, 0.62), (s * 0.31, 0.08, 0.46), (s * 0.27, 0.05, 0.36)]
    radii = [0.035, 0.060, 0.070, 0.058, 0.035, 0.004]
    tail = curve_strand(f"Tail{tag}", pts, radii, MAT["hair"], "head")
    TAILS[tag] = (tail, pts)
    weight_tail(tail, tag, pts)
    # 結び目のリボン
    for k, dx in enumerate((-1, 1)):
        rb = uv_sphere(f"TieBow{tag}{k}", Vector((s * 0.215 + 0.0, 0.03 + dx * 0.035, 1.01)), Vector((0.016, 0.032, 0.026)), 24, 12)
        finish(rb, MAT["ribbon"], "head")
    finish(uv_sphere(f"TieKnot{tag}", Vector((s * 0.212, 0.03, 1.01)), Vector((0.016, 0.016, 0.016)), 16, 8), MAT["ribbon"], "head")

# アホ毛
curve_strand("Ahoge", [(0.0, -0.02, 1.08), (0.0, -0.05, 1.17), (0.035, -0.09, 1.19), (0.06, -0.08, 1.15)],
             [0.012, 0.010, 0.006, 0.001], MAT["hair"], "head")

# ---------- 体 ----------
finish(tube("Neck", (0, 0, 0.56), (0, 0, 0.67), 0.034, 0.032), MAT["skin"], "neck")
torso = uv_sphere("Torso", Vector((0, 0, 0.485)), Vector((0.105, 0.078, 0.115)))
finish(torso, MAT["shirt"], "chest")
# 胸元の大きなリボン
for k, sx in enumerate((-1, 1)):
    finish(uv_sphere(f"ChestBow{k}", Vector((sx * 0.030, -0.074, 0.555)), Vector((0.030, 0.012, 0.020)), 24, 12), MAT["ribbon"], "chest")
finish(uv_sphere("ChestKnot", Vector((0, -0.083, 0.555)), Vector((0.011, 0.010, 0.012)), 16, 8), MAT["ribbon"], "chest")
for k, sx in enumerate((-1, 1)):
    t = tube(f"ChestTail{k}", (sx * 0.006, -0.082, 0.548), (sx * 0.022, -0.085, 0.505), 0.008, 0.010, 12)
    finish(t, MAT["ribbon"], "chest")

# スカート（裾の頂点を交互に出し入れしてプリーツにする）
bm = bmesh.new()
bmesh.ops.create_cone(bm, cap_ends=False, segments=32, radius1=0.215, radius2=0.098, depth=0.19)
bmesh.ops.translate(bm, vec=(0, 0, 0.335), verts=bm.verts)
for v in bm.verts:
    if v.co.z < 0.3:
        ang = math.atan2(v.co.y, v.co.x)
        k = round(ang / (2 * math.pi / 32))
        f = 1.0 + (0.06 if k % 2 == 0 else -0.02)
        v.co.x *= f
        v.co.y *= f
skirt = link("Skirt", bm)
sol = skirt.modifiers.new("Thickness", "SOLIDIFY")
sol.thickness = 0.006
finish(skirt, MAT["skirt"], "hips")
apply_mods(skirt)
finish(uv_sphere("Pelvis", Vector((0, 0, 0.385)), Vector((0.098, 0.075, 0.06))), MAT["skirt"], "hips")

# 脚（太もも=肌、すね=ハイソックス）
for side, tag in ((1, "L"), (-1, "R")):
    x = 0.055 * side
    finish(tube(f"Thigh{tag}", (x, 0, 0.34), (x, 0, 0.19), 0.042, 0.036), MAT["skin"], f"upperLeg{tag}")
    finish(tube(f"Shin{tag}", (x, 0, 0.20), (x, 0, 0.05), 0.037, 0.032), MAT["sock"], f"lowerLeg{tag}")
    finish(uv_sphere(f"Knee{tag}", Vector((x, 0, 0.195)), Vector((0.037, 0.037, 0.037)), 24, 12), MAT["sock"], f"lowerLeg{tag}")
    finish(uv_sphere(f"Shoe{tag}", Vector((x, -0.018, 0.035)), Vector((0.043, 0.068, 0.035)), 32, 16), MAT["shoe"], f"foot{tag}")

# 腕（VRM 1.0の基準姿勢はTポーズなので水平に伸ばす。腕を下ろすのは表示側で行う）
ARM = {}
for side, tag in ((1, "L"), (-1, "R")):
    sh = Vector((0.098 * side, 0, 0.56))
    d = Vector((side, 0, 0))
    el = sh + d * 0.12
    wr = el + d * 0.105
    hand = wr + d * 0.03
    ARM[tag] = (sh, el, wr, hand)
    finish(uv_sphere(f"Sleeve{tag}", (sh + el) / 2, Vector((0.045, 0.045, 0.072)), 32, 16), MAT["shirt"], f"upperArm{tag}")
    obj = bpy.data.objects[f"Sleeve{tag}"]
    obj.data.transform(Matrix.Translation((sh + el) / 2) @ (d.to_track_quat("Z", "Y").to_matrix().to_4x4()) @ Matrix.Translation(-(sh + el) / 2))
    finish(tube(f"Forearm{tag}", el, wr, 0.026, 0.022), MAT["skin"], f"lowerArm{tag}")
    finish(uv_sphere(f"Hand{tag}", hand, Vector((0.030, 0.022, 0.034)), 24, 12), MAT["skin"], f"hand{tag}")

# ---------- 表情（シェイプキー） ----------
def reproject(obj, fn):
    """各頂点の2D座標(x,z)にfnを適用して投影し直した位置を返す"""
    pts = obj["pts2d"]
    off = obj["offset"]
    out = []
    for (x, z) in pts:
        nx, nz = fn(x, z)
        out.append(project(nx, nz, off))
    return out

def add_key(obj, name, coords):
    if obj.data.shape_keys is None:
        obj.shape_key_add(name="Basis")
    k = obj.shape_key_add(name=name)
    for v, co in zip(k.data, coords):
        v.co = co

def eye_fn(kind, cx, cz, side, is_brow):
    def f(x, z):
        dx, dz = x - cx, z - cz
        if is_brow:
            if kind == "angry":  # 眉間側を下げる
                return x, z - 0.010 * (1 - dx * side / 0.03) / 2 - 0.004
            if kind == "sad":    # 目尻側を下げ、眉間側を上げる
                return x, z + 0.008 * (1 - dx * side / 0.03) / 2 - 0.006 * (1 + dx * side / 0.03) / 2
            if kind == "surprised":
                return x, z + 0.014
            if kind == "happy":
                return x, z + 0.006
            return x, z
        if kind in ("blink", "blinkL", "blinkR"):
            if kind == "blinkL" and side < 0 or kind == "blinkR" and side > 0:
                return x, z
            return x, (cz - 0.022) + dz * 0.06
        if kind == "happy":  # 「^ ^」の目: 細くつぶして上に反らす
            t = dx / (EA + 0.015)
            return x, (cz - 0.008) + dz * 0.12 + 0.016 * (1 - min(1.0, t * t))
        if kind == "relaxed":
            return x, (cz - 0.02) + dz * 0.45
        if kind == "angry":  # 目の上側を、眉間側が下がるように傾ける
            top = max(0.0, dz) / EB
            return x, cz + dz * 0.80 - 0.004 + top * 0.022 * (dx * side / EA)
        if kind == "sad":    # 目の上側を、目尻側が下がるように傾ける
            top = max(0.0, dz) / EB
            return x, cz + dz * 0.85 - 0.006 - top * 0.018 * (dx * side / EA)
        if kind == "surprised":
            return cx + dx * 0.92, cz + dz * 1.08 + 0.004
        return x, z
    return f

MOUTH = {  # (横の倍率, 縦の倍率, 上下のずらし)
    "aa": (1.25, 4.2, -0.006), "ih": (1.45, 2.0, -0.002), "ou": (0.65, 2.6, -0.003),
    "ee": (1.35, 2.6, -0.003), "oh": (0.95, 3.6, -0.006),
    "happy": (1.6, 2.6, -0.002), "angry": (0.9, 1.4, 0.0), "sad": (0.8, 1.2, -0.002),
    "relaxed": (1.3, 1.2, 0.0), "surprised": (0.75, 3.4, -0.006),
}

EXPR = ["blink", "blinkL", "blinkR", "happy", "angry", "sad", "relaxed", "surprised", "aa", "ih", "ou", "ee", "oh"]
for name in EXPR:
    for obj, cx, cz, side in EYES:
        is_brow = obj.name.startswith("Brow")
        add_key(obj, name, reproject(obj, eye_fn(name, cx, cz, side, is_brow)))
    if name in MOUTH:
        sx, sz, oz = MOUTH[name]
        def mf(x, z, sx=sx, sz=sz, oz=oz, name=name):
            nz = MOUTH_Z + (z - MOUTH_Z) * sz + oz
            if name == "happy":  # 口角を上げた笑顔: 下側だけ開く
                nz = MOUTH_Z + min(0.0, z - MOUTH_Z) * 4.0 + max(0.0, z - MOUTH_Z) * 0.5 + 0.0015 * (x / 0.016) ** 2
            if name == "sad":
                nz -= 0.002 * (x / 0.016) ** 2
            return x * sx, nz
        add_key(mouth, name, reproject(mouth, mf))
    else:
        add_key(mouth, name, reproject(mouth, lambda x, z: (x, z)))

# ---------- 統合 ----------
# 表情のシェイプキーは顔のメッシュだけに持たせる（全身に持たせるとファイルが数倍に膨らむ）
FACE_PREFIX = ("Head", "Iris", "Pupil", "Hi", "Lash", "Brow", "Blush", "Mouth")

def join(objs, name):
    for o in scene.objects:
        o.select_set(False)
    for o in objs:
        o.select_set(True)
    bpy.context.view_layer.objects.active = objs[0]
    bpy.ops.object.join()
    ob = bpy.context.view_layer.objects.active
    ob.name = name
    ob.data.name = name
    return ob

meshes = [o for o in scene.objects if o.type == "MESH"]
face_objs = [bpy.data.objects["Head"]] + [o for o in meshes if o.name.startswith(FACE_PREFIX) and o.name != "Head"]
body_objs = [o for o in meshes if not o.name.startswith(FACE_PREFIX)]
face = join(face_objs, "Face")
body = join(body_objs, "Body")

# ---------- 骨 ----------
arm_data = bpy.data.armatures.new("Armature")
rig = bpy.data.objects.new("Armature", arm_data)
scene.collection.objects.link(rig)
bpy.context.view_layer.objects.active = rig
bpy.ops.object.mode_set(mode="EDIT")
eb = arm_data.edit_bones

def bone(name, head_, tail, parent=None):
    b = eb.new(name)
    b.head, b.tail = Vector(head_), Vector(tail)
    if parent:
        b.parent = eb[parent]
        b.use_connect = False
    return b

bone("hips", (0, 0, 0.36), (0, 0, 0.42))
bone("spine", (0, 0, 0.42), (0, 0, 0.48), "hips")
bone("chest", (0, 0, 0.48), (0, 0, 0.56), "spine")
bone("neck", (0, 0, 0.56), (0, 0, 0.64), "chest")
bone("head", (0, 0, 0.64), (0, 0, 1.0), "neck")
for side, tag in ((1, "L"), (-1, "R")):
    x = 0.055 * side
    bone(f"upperLeg{tag}", (x, 0, 0.34), (x, 0, 0.195), "hips")
    bone(f"lowerLeg{tag}", (x, 0, 0.195), (x, 0, 0.06), f"upperLeg{tag}")
    bone(f"foot{tag}", (x, 0, 0.06), (x, -0.06, 0.02), f"lowerLeg{tag}")
    sh, el, wr, hand = ARM[tag]
    bone(f"upperArm{tag}", sh, el, "chest")
    bone(f"lowerArm{tag}", el, wr, f"upperArm{tag}")
    bone(f"hand{tag}", wr, hand + (hand - wr), f"lowerArm{tag}")
    pts = TAILS[tag][1]
    chain = [Vector(pts[1]), Vector(pts[2]), Vector(pts[3]), Vector(pts[4]), Vector(pts[5])]
    prev = "head"
    for i in range(4):
        bone(f"tail{tag}{i}", chain[i], chain[i + 1], prev)
        prev = f"tail{tag}{i}"
bpy.ops.object.mode_set(mode="OBJECT")

for ob in (face, body):
    ob.parent = rig
    ob.modifiers.new("Armature", "ARMATURE").object = rig

# ---------- VRMの設定 ----------
ext = arm_data.vrm_addon_extension
ext.spec_version = "1.0"
v1 = ext.vrm1
hb = v1.humanoid.human_bones
MAP = {
    "hips": "hips", "spine": "spine", "chest": "chest", "neck": "neck", "head": "head",
    "left_upper_leg": "upperLegL", "left_lower_leg": "lowerLegL", "left_foot": "footL",
    "right_upper_leg": "upperLegR", "right_lower_leg": "lowerLegR", "right_foot": "footR",
    "left_upper_arm": "upperArmL", "left_lower_arm": "lowerArmL", "left_hand": "handL",
    "right_upper_arm": "upperArmR", "right_lower_arm": "lowerArmR", "right_hand": "handR",
}
for k, b in MAP.items():
    getattr(hb, k).node.bone_name = b

meta = v1.meta
meta.vrm_name = "Chibi Twintail"
a = meta.authors.add()
a.value = "saka2jp (built with Claude Code)"
meta.avatar_permission = "everyone"
meta.commercial_usage = "personalNonProfit"

PRESET = {"blink": "blink", "blinkL": "blink_left", "blinkR": "blink_right", "happy": "happy",
          "angry": "angry", "sad": "sad", "relaxed": "relaxed", "surprised": "surprised",
          "aa": "aa", "ih": "ih", "ou": "ou", "ee": "ee", "oh": "oh"}
for key, preset in PRESET.items():
    ex = getattr(v1.expressions.preset, preset)
    bind = ex.morph_target_binds.add()
    bind.node.mesh_object_name = "Face"
    bind.index = key
    bind.weight = 1.0
    if preset in ("happy", "angry", "sad", "relaxed", "surprised"):
        ex.override_blink = "block"   # 笑顔の最中にまばたきが重なって目が崩れるのを防ぐ
v1.look_at.type = "expression"

# 揺れもの（ツインテール）
sb = ext.spring_bone1
for tag in ("L", "R"):
    spring = sb.springs.add()
    spring.vrm_name = f"Twintail{tag}"
    for i in range(4):
        j = spring.joints.add()
        j.node.bone_name = f"tail{tag}{i}"
        j.stiffness = 0.6
        j.drag_force = 0.35
        j.gravity_power = 0.3
        j.hit_radius = 0.03

bpy.ops.wm.save_as_mainfile(filepath=OUT.replace(".vrm", ".blend"))
res = bpy.ops.export_scene.vrm(filepath=OUT, armature_object_name="Armature", ignore_warning=False, export_try_sparse_sk=True)
print("EXPORT", res, OUT)
for n in ("Face", "Body"):
    me = bpy.data.objects[n].data
    print("STATS", n, "verts", len(me.vertices), "tris", sum(len(p.vertices) - 2 for p in me.polygons))
