# 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
import numpy as np
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)

# ---------- テクスチャ（numpyで描いて画像に詰める） ----------
def srgb(h):
    h = h.lstrip("#")
    return np.array([int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)])

def sstep(e0, e1, x):
    t = np.clip((x - e0) / (e1 - e0), 0, 1)
    return t * t * (3 - 2 * t)

def make_image(name, w, h, fn):
    """fn(u, v) が返す (h, w, 4) の sRGB 配列を画像にする。u: 右向き, v: 上向き"""
    u, v = np.meshgrid((np.arange(w) + 0.5) / w, (np.arange(h) + 0.5) / h)
    rgba = np.clip(fn(u, v), 0, 1).astype(np.float32)
    img = bpy.data.images.new(name, w, h, alpha=True)
    img.pixels.foreach_set(rgba.ravel())
    img.pack()
    return img

def mix(base, col, a):
    return base * (1 - a[..., None]) + col * a[..., None]

def iris_tex(u, v):
    # 瞳: 上が濃く下が明るいグラデーション、外周の暗い縁、瞳孔、下側の明るい弧、ハイライト
    x, y = u * 2 - 1, v * 2 - 1
    r = np.sqrt(x * x + y * y)
    c = mix(np.broadcast_to(srgb("#3a2490"), u.shape + (3,)), srgb("#b496ff"), sstep(0.55, -0.60, y))
    c = mix(c, srgb("#22145a"), sstep(0.55, 0.98, y) * 0.8)                # 上まぶたの影
    c = mix(c, srgb("#c8b4ff"), sstep(-0.15, -0.65, y) * sstep(0.95, 0.70, r) * 0.7)  # 下側の明るい弧
    pr = np.sqrt((x / 0.42) ** 2 + ((y + 0.02) / 0.48) ** 2)
    c = mix(c, srgb("#140a2e"), sstep(1.05, 0.90, pr))                     # 瞳孔
    c = mix(c, srgb("#1a1040"), sstep(0.80, 0.97, r))                      # 外周の縁
    for hx, hy, hr in ((-0.30, 0.36, 0.30), (0.38, -0.44, 0.13), (0.14, 0.58, 0.08)):
        hd = np.sqrt((x - hx) ** 2 + (y - hy) ** 2)
        c = mix(c, np.ones(3), sstep(hr, hr * 0.8, hd))                     # ハイライト
    return np.dstack([c, np.ones(u.shape)])

def mouth_tex(u, v):
    # 口の中: 上が暗い赤、下に舌のピンク
    c = mix(np.broadcast_to(srgb("#5a1626"), u.shape + (3,)), srgb("#e9768a"),
            sstep(0.48, 0.22, v) * sstep(0.95, 0.55, np.abs(u * 2 - 1)))
    return np.dstack([c, np.ones(u.shape)])

def blush_tex(u, v):
    # 頬の赤み: 中心が濃く外へぼかす楕円と、斜線3本
    x, y = u * 2 - 1, v * 2 - 1
    r = np.sqrt(x * x + y * y)
    a = sstep(1.0, 0.2, r) * 0.55
    c = np.broadcast_to(srgb("#ff8fa3"), u.shape + (3,)).copy()
    for k in (-0.45, 0.0, 0.45):
        d = np.abs((x - k) - 0.5 * y)          # 右上がりの斜線
        line = sstep(0.09, 0.04, d) * sstep(0.75, 0.45, np.abs(y))
        c = mix(c, srgb("#e8506e"), line)
        a = np.maximum(a, line * 0.85)
    return np.dstack([c, a])

def hair_matcap(u, v):
    # 髪の「天使の輪」: 視線に対して上向きの面に、ギザギザの明るい帯を出すマットキャップ
    x, y = u * 2 - 1, v * 2 - 1
    band = sstep(0.36, 0.44, y) * sstep(0.60, 0.52, y + 0.05 * np.abs(np.sin(x * 22)))
    band *= sstep(0.95, 0.6, np.abs(x))
    c = np.dstack([band, band, band])
    return np.dstack([c, np.ones(u.shape)])

TEX = {
    "iris": make_image("IrisTex", 256, 256, iris_tex),
    "mouth": make_image("MouthTex", 64, 64, mouth_tex),
    "blush": make_image("BlushTex", 128, 64, blush_tex),
    "matcap": make_image("HairMatcap", 256, 256, hair_matcap),
}

# ---------- マテリアル（MToon） ----------
def mtoon(name, base, shade=None, outline=0.0, toony=0.95, shift=-0.05, rim=None, tex=None, alpha=False, matcap=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 tex is not None:
        # テクスチャで描いた部分は、陰色にも同じテクスチャを掛けて陰を出さない
        ext.pbr_metallic_roughness.base_color_texture.index.source = tex
        mt.shade_multiply_texture.index.source = tex
    if alpha:
        ext.alpha_mode = "BLEND"
    if matcap is not None:
        mt.matcap_texture.index.source = matcap[0]
        mt.matcap_factor = matcap[1]
    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",
                  matcap=(TEX["matcap"], (0.30, 0.28, 0.42))),
    # ツインテールは太い筒なので、天使の輪を付けるとプラスチックのように光る。リムも弱める
    "hair_tail": mtoon("HairTail", "#7d6ae0", "#4a3a9e", outline=OL, shift=0.0, rim="#6e5fc0"),
    "collar": mtoon("Collar", "#2c3566", "#1a1f40", outline=OL),
    "sclera": mtoon("Sclera", "#ffffff"),
    "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", "#ffffff", "#ffffff", tex=TEX["iris"]),
    "lash": mtoon("Lash", "#2b1a24"),
    "brow": mtoon("Brow", "#4a3a8e"),
    "mouth": mtoon("Mouth", "#ffffff", "#ffffff", tex=TEX["mouth"]),
    "nose": mtoon("Nose", "#efb3a3"),
    "blush": mtoon("Blush", "#ffffff", "#ffffff", tex=TEX["blush"], alpha=True),
}

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 * Vector((1.0, 0.45, 1.0)), 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", uv_box=None):
    """2D多角形(x,z)を顔の表面に投影して貼る。中心に頂点を置き扇状に面を張る。
    uv_box=(x0, z0, x1, z1) を渡すと、その範囲を0〜1にしたUVを付ける（テクスチャ用）"""
    cx = sum(p[0] for p in pts2d) / len(pts2d)
    cz = sum(p[1] for p in pts2d) / len(pts2d)
    bm = bmesh.new()
    uvl = bm.loops.layers.uv.new("UVMap")
    c = bm.verts.new(project(cx, cz, offset))
    ring = [bm.verts.new(project(x, z, offset)) for x, z in pts2d]
    flat = [(cx, cz)] + list(pts2d)
    box = uv_box or (min(p[0] for p in pts2d), min(p[1] for p in pts2d), max(p[0] for p in pts2d), max(p[1] for p in pts2d))
    def uv_of(i):
        x, z = flat[i]
        return ((x - box[0]) / (box[2] - box[0]), (z - box[1]) / (box[3] - box[1]))
    for i in range(len(ring)):
        f = bm.faces.new((c, ring[i], ring[(i + 1) % len(ring)]))
        for loop, idx in zip(f.loops, (0, i + 1, (i + 1) % len(ring) + 1)):
            loop[uvl].uv = uv_of(idx)
    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

def lower_lash_pts(cx, cz, a, b, side):
    """目尻側の下まつ毛（細い弧）"""
    outer, inner = [], []
    for i in range(9):
        th = math.radians(-12 - 58 * i / 8)
        x, z = math.cos(th) * side, math.sin(th)
        w = 0.0032 * (1 - i / 10)
        outer.append((cx + (a * 1.22 + 0.002) * x, cz + (b * 0.93) * z - w))
        inner.append((cx + (a * 1.22) * x, cz + (b * 0.93) * z))
    return outer + inner[::-1]

EYE_Z, EYE_X, EA, EB = 0.795, 0.088, 0.039, 0.053
EYES = []  # (obj, cx, cz, side)
for side, tag in ((1, "L"), (-1, "R")):
    cx = EYE_X * side
    parts = [
        # 白目は瞳より横に広く、上下は少し短い
        decal(f"Sclera{tag}", ellipse_pts(cx + side * 0.004, EYE_Z - 0.002, EA * 1.30, EB * 0.92, 48), 0.0016, MAT["sclera"]),
        # 瞳は1枚のテクスチャ（グラデーション・瞳孔・ハイライト）。ハイライトの向きは左右で同じ
        decal(f"Iris{tag}", ellipse_pts(cx, EYE_Z, EA, EB, 48), 0.0024, MAT["iris"],
              uv_box=(cx - EA, EYE_Z - EB, cx + EA, EYE_Z + EB)),
        decal(f"Lash{tag}", lash_pts(cx, EYE_Z, EA * 1.18, EB, side), 0.0040, MAT["lash"]),
        decal(f"LowerLash{tag}", lower_lash_pts(cx, EYE_Z, EA, EB, side), 0.0040, 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))
    bx, bz, ba, bb = side * 0.118, 0.738, 0.036, 0.017
    decal(f"Blush{tag}", ellipse_pts(bx, bz, ba, bb, 24), 0.0012, MAT["blush"], uv_box=(bx - ba, bz - bb, bx + ba, bz + bb))

# 鼻: 肌の陰色の小さな点だけで表す
decal("Nose", ellipse_pts(0.004, 0.742, 0.0045, 0.0028, 12), 0.0010, MAT["nose"])

MOUTH_Z = 0.695
# 口: 閉じた状態は細い線。開くとテクスチャの口の中（上が暗い赤、下が舌）が見える
mouth = decal("Mouth", ellipse_pts(0, MOUTH_Z, 0.016, 0.0040, 32), 0.0020, MAT["mouth"],
              uv_box=(-0.016, MOUTH_Z - 0.0040, 0.016, MOUTH_Z + 0.0040))

# ---------- 髪 ----------
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_tail"], "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")
def collar(name, scale, zmin, vdepth, mat):
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=48, v_segments=24, radius=1.0)
    # 肩にかかる上側だけを残し、前は胸元に向けてV字に開ける
    kill = [v for v in bm.verts if v.co.z < zmin or v.co.z > 0.93 or (v.co.y < 0 and abs(v.co.x) < (v.co.z - zmin) * vdepth)]
    bmesh.ops.delete(bm, geom=kill, context="VERTS")
    bmesh.ops.scale(bm, vec=scale, verts=bm.verts)
    bmesh.ops.translate(bm, vec=(0, 0, 0.485), verts=bm.verts)
    ob = link(name, bm)
    sol = ob.modifiers.new("Thickness", "SOLIDIFY")
    sol.thickness = 0.003
    finish(ob, mat, "chest")
    apply_mods(ob)

collar("CollarLine", Vector((0.111, 0.084, 0.121)), 0.30, 1.05, MAT["shirt"])
collar("Collar", Vector((0.114, 0.087, 0.124)), 0.38, 1.20, MAT["collar"])

# 胸元の大きなリボン
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.205, 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.09 if k % 2 == 0 else -0.04)
        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")

def skin_limb(name, nodes, edges, mats_fn, weight_fn, level=2):
    """点と半径の並びから、Skinモディファイア＋細分化で、継ぎ目のない手足を作る
    nodes: [(位置, (半径x, 半径y))], mats_fn(co)->マテリアル番号, weight_fn(co)->[(骨名, 重み)]"""
    me = bpy.data.meshes.new(name)
    me.from_pydata([n[0] for n in nodes], edges, [])
    ob = bpy.data.objects.new(name, me)
    scene.collection.objects.link(ob)
    ob.modifiers.new("Skin", "SKIN")
    for i, (_, r) in enumerate(nodes):
        me.skin_vertices[0].data[i].radius = r
    me.skin_vertices[0].data[0].use_root = True
    sub = ob.modifiers.new("Smooth", "SUBSURF")
    sub.levels = level
    apply_mods(ob)
    return ob

def limb_finish(ob, mats, mats_fn, weight_fn):
    ob.data.materials.clear()
    for m in mats:
        ob.data.materials.append(m)
    for p in ob.data.polygons:
        p.use_smooth = True
        p.material_index = mats_fn(p.center)
    groups = {}
    for v in ob.data.vertices:
        for bone, w in weight_fn(v.co):
            if w <= 0:
                continue
            if bone not in groups:
                groups[bone] = ob.vertex_groups.new(name=bone)
            groups[bone].add([v.index], w, "REPLACE")
    return ob

def blend2(t, a, b, width):
    """位置tが境界0の前後width以内なら、骨aとbの重みを線形に混ぜる"""
    w = max(0.0, min(1.0, 0.5 + t / (2 * width)))
    return [(a, 1 - w), (b, w)]

# 脚（太もも=肌、ひざ下=ハイソックス）。付け根から足首まで1本のメッシュ
for side, tag in ((1, "L"), (-1, "R")):
    x = 0.055 * side
    leg = skin_limb(f"Leg{tag}", [((x, 0, 0.37), (0.044, 0.044)), ((x, 0, 0.30), (0.043, 0.043)),
                                  ((x, 0, 0.195), (0.034, 0.035)), ((x, 0, 0.11), (0.031, 0.032)),
                                  ((x, 0, 0.055), (0.026, 0.027))],
                    [(0, 1), (1, 2), (2, 3), (3, 4)], None, None)
    def leg_w(co, tag=tag):
        if co.z > 0.195:
            return blend2(0.195 + 0.02 - co.z, f"upperLeg{tag}", f"lowerLeg{tag}", 0.02)
        return blend2(0.065 - co.z, f"lowerLeg{tag}", f"foot{tag}", 0.01)
    limb_finish(leg, [MAT["skin"], MAT["sock"]], lambda c: 1 if c.z < 0.235 else 0, leg_w)
    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))
    # 二の腕から指先まで1本のメッシュ。手はミトン形で、親指を前に出す
    s = side
    arm = skin_limb(f"Arm{tag}", [
        ((sh.x + s * 0.02, 0, 0.56), (0.026, 0.026)), (tuple(el), (0.023, 0.023)),
        (tuple(wr), (0.019, 0.017)), (tuple(hand), (0.026, 0.012)),
        ((hand.x + s * 0.030, 0, hand.z), (0.020, 0.010)),
        ((wr.x + s * 0.016, -0.018, wr.z - 0.004), (0.009, 0.009))],
        [(0, 1), (1, 2), (2, 3), (3, 4), (2, 5)], None, None)
    def arm_w(co, s=s, el=el, wr=wr, tag=tag):
        t = (co.x - el.x) * s
        if t < 0.02:
            return blend2(t, f"upperArm{tag}", f"lowerArm{tag}", 0.02)
        return blend2((co.x - wr.x) * s, f"lowerArm{tag}", f"hand{tag}", 0.012)
    limb_finish(arm, [MAT["skin"]], lambda c: 0, arm_w)

# ---------- 表情（シェイプキー） ----------
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", "Sclera", "Iris", "Lash", "LowerLash", "Brow", "Blush", "Mouth", "Nose")

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