tailor(mako): kilt v2 on Mako via Lena donor-swap -- MD rejects all Mako meshes

First male garment through the MD lane. MD's body analyzer rejects every
Tripo-Mako-derived mesh (0 arrangement points, 'avatar must be in T-pose or
A-pose' despite a verified 0.4-deg-perfect skeleton; same wall as ariki-game
e244e8a). Chimera test pins it on the MESH -- Lena's mesh on Mako's armature
gets 109 points. Ruled out: finger weights, decimation, head/hand chops, leg
spread, rename, OBJ import, export settings.

Workaround that ships: arrange+sew+elastic-lock the garment on a 1.07x-scaled
LENA donor, ImportFBX Mako with bAdd, DeleteAvatar(Lena), re-settle. A
seamless single-wrap slides off Mako's legs during the swap -- only SEWN
garments survive it, so the flat pattern's wrap reads are faked: overlap flap
seamed to a split front-waist segment, comb-teeth fringe (piupiu technique)
carved into the hem, rope belt as rigid Blender geometry (build_rope_belt.py).

Other findings baked into work/mako_kilt_v1/ scripts + config:
- fabric texture anchor: pattern y=0 maps to texture v=0.5 and WRAPS
  (striped-probe verified; make_kilt_cloth.py mm_from_row)
- waist elastic never reaches TotalLength at default strength; strength 60
  moved placement from 1.019 to 1.043
- SetAvatarActivation returns False (unusable); GetArrangementList dies on
  any scene avatar mutation

QC PASS (turntable render, alpha mask, height 1.818): top 1.063 vs 1.054
target (tol .03), fringe tips 0.666 vs 0.665 (tol .04).

Avatars: _fingers = full finger weights restored via UV-space transfer from
rig-work/mako_fullhead_rigged.fbx (build_mako_avatar_fingers.py); shipped-body
avatar landed for future diagnosis. v1 (pre-flap/teeth) kept as provenance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Can Narin
2026-08-18 16:21:16 +03:00
parent 9ac741302e
commit fea96bf582
47 changed files with 1450 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""
make_kilt_cloth.py -- beige woven cloth with black triangle border band for the
male feather-cape kilt (mako_kilt_v1).
python tools/tailor/textures/make_kilt_cloth.py
Derived from the reference renders
`male-clothing-feather-cape-gpt-piece-kilt{,-front,-back}.png`: a light beige
coarse plain weave (harakeke/tapa in character) with a black triangle border
band above a short fringe at the hem.
UNLIKE hunter_cloth THIS TILE IS POSITIONED, NOT GENERIC. It spans the kilt
panel's full 390 mm height exactly once (dpi = SIZE / (390 / 25.4)), so the
band and fringe land at fixed garment heights:
garment y (mm, hem=0) zone
0-30 fringe strands (drawn, not geometry -- v1)
30-90 black band, beige triangles (6 cm, per flat pattern)
90-390 plain beige weave
Horizontally the same 390 mm tiles ~1.7x across the 650 mm panel; the band is
horizontal and the triangles repeat 8/tile, so the wrap is seamless.
IF THE BAND RENDERS AT THE WAIST INSTEAD OF THE HEM, MD anchored the texture
the other way up -- regenerate with FLIP=True rather than touching the pattern.
Same doctrine as make_hunter_cloth.py: albedo baked in (pipeline has no alpha,
texture replaces base color), warm ramp r>g>b, deterministic LCG noise.
"""
import os
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "kilt_cloth.png")
SIZE = 1024 # px, square
CLOTH_MM = 390.0 # spans the kilt panel height exactly once
DPI = SIZE / (CLOTH_MM / 25.4)
FLIP = False # set True if MD anchors the texture upside-down
BASE = (206, 188, 152) # light beige albedo (reference lit mean ~(196,180,148))
BAND = (44, 38, 30) # near-black band, warm
THREAD_PITCH = 8 # px per thread; 1024px/390mm -> ~3 mm threads
OVER_LIFT = 12
UNDER_DROP = 16
ROUND_SHADE = 10
SLUB_RANGE = 8
MOTTLE = 12
MOTTLE_CELL = 64
# zone boundaries in garment mm (hem = 0)
# v2: fringe is CUT GEOMETRY now (comb teeth, 0-30 mm) -- the texture paints the
# teeth as strand-beige instead of drawing fake strands. Band triangles refined
# 8 -> 12 per tile (32.5 mm) to match the reference's tighter zigzag.
FRINGE_TOP_MM = 30.0
BAND_TOP_MM = 90.0
TRIANGLES_PER_TILE = 12
def lcg(seed):
state = seed
while True:
state = (1103515245 * state + 12345) % (2 ** 31)
yield state / float(2 ** 31)
def thread_tones(n, seed):
g = lcg(seed)
return [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(n)]
def value_noise(cells, seed):
g = lcg(seed)
grid = [[(next(g) * 2.0 - 1.0) for _ in range(cells + 1)] for _ in range(cells + 1)]
def sample(x, y):
fx, fy = x * cells / SIZE, y * cells / SIZE
x0, y0 = int(fx), int(fy)
tx, ty = fx - x0, fy - y0
tx, ty = tx * tx * (3 - 2 * tx), ty * ty * (3 - 2 * ty)
a = grid[y0][x0] * (1 - tx) + grid[y0][x0 + 1] * tx
b = grid[y0 + 1][x0] * (1 - tx) + grid[y0 + 1][x0 + 1] * tx
return a * (1 - ty) + b * ty
return sample
def mm_from_row(y):
"""Garment height in mm for an image row.
ANCHOR DISCOVERED BY PROBE (2026-08-18, striped texture on the kilt panels,
scratchpad probe_front/back.png): MD maps pattern y=0 (hem) to image v=0.5
and WRAPS -- v = (0.5 + (390 - y)/390) mod 1. Both panels map identically.
Inverted here: row -> garment mm."""
v = (y + 0.5) / SIZE
return CLOTH_MM - CLOTH_MM * ((v - 0.5) % 1.0)
def in_triangle(x, y):
"""Beige zigzag triangles inside the band: alternating up/down, classic
taniko-style row. Returns True where the pixel is beige (triangle), False
where it stays black band."""
band_lo = FRINGE_TOP_MM + 4.0 # thinner black margins (v2, per reference)
band_hi = BAND_TOP_MM - 4.0
mm = mm_from_row(y)
if not (band_lo <= mm <= band_hi):
return False
v = (mm - band_lo) / (band_hi - band_lo) # 0 bottom .. 1 top
tri_w = SIZE / float(TRIANGLES_PER_TILE)
u = (x % tri_w) / tri_w # 0..1 across one triangle
k = int(x / tri_w) % 2
if k == 0: # upward triangle: wide at bottom
half = (1.0 - v) * 0.5
else: # downward: wide at top
half = v * 0.5
return abs(u - 0.5) < half * 0.88 # slim gutters (v2)
def fringe_strand(x, y, gap_noise):
"""Fringe zone: vertical beige strands over shadow, ragged tips."""
mm = mm_from_row(y)
strand_w = 6
u = x % strand_w
# per-strand ragged length: strands end 0..10 mm above the tile bottom
tip = gap_noise[(x // strand_w) % len(gap_noise)] * 10.0
if mm < tip:
return None # past the tip: shadow
return u >= 1 # 1px gap between strands
def main():
n_threads = SIZE // THREAD_PITCH
warp = thread_tones(n_threads, seed=20260818)
weft = thread_tones(n_threads, seed=31337)
mottle = value_noise(SIZE // MOTTLE_CELL, seed=777)
g = lcg(424242)
gap_noise = [next(g) for _ in range(64)]
img = Image.new("RGB", (SIZE, SIZE))
px = img.load()
for y in range(SIZE):
j = (y // THREAD_PITCH) % n_threads
vy = ((y % THREAD_PITCH) / (THREAD_PITCH - 1.0)) * 2.0 - 1.0
mm = mm_from_row(y)
for x in range(SIZE):
i = (x // THREAD_PITCH) % n_threads
vx = ((x % THREAD_PITCH) / (THREAD_PITCH - 1.0)) * 2.0 - 1.0
warp_on_top = ((i + j) % 2) == 0
if warp_on_top:
lift = OVER_LIFT - int(ROUND_SHADE * vx * vx)
slub = warp[i]
else:
lift = -UNDER_DROP + int(ROUND_SHADE * (1.0 - vy * vy))
slub = weft[j]
wear = int(mottle(x, y) * MOTTLE)
d = lift + slub + wear
if mm < FRINGE_TOP_MM:
# v2: teeth are cut geometry; paint them as strand fibre --
# duller beige with vertical grain, no fake shadow gaps.
u = x % 6
base = (192, 174, 140) if u >= 1 else (160, 143, 112)
elif mm < BAND_TOP_MM:
base = BASE if in_triangle(x, y) else BAND
else:
base = BASE
r = base[0] + d
gg = base[1] + int(d * 0.82)
b = base[2] + int(d * 0.66)
px[x, y] = (max(0, min(255, r)), max(0, min(255, gg)),
max(0, min(255, b)))
if FLIP:
img = img.transpose(Image.FLIP_TOP_BOTTOM)
img.save(OUT, dpi=(DPI, DPI))
print("wrote %s (%dx%d, dpi %.2f -> %.0f mm of cloth, band %d-%d mm, fringe 0-%d mm)"
% (OUT, SIZE, SIZE, DPI, CLOTH_MM, FRINGE_TOP_MM, BAND_TOP_MM, FRINGE_TOP_MM))
if __name__ == "__main__":
main()