0fae2b514c
Female voyager bandeau top from the fc-voyager-kilt-gpt refs. Winning construction (md_final_v16/v23): seamless-loop doctrine -- NO edge elastic (gathering is what notched the armpit junctions), body-snug cut, zero-g wrap (SetSimulationGravity(0) BEFORE ResetClothArrangement -- one full-gravity materialization tick drops the bistable tube into the low basin), stiff 40f positioning then soft 110f conform, self-measuring retry loop. Hard-won facts logged in the scripts: strapless garments never survive full gravity (frictionless skin + stretchy elastic); arrangement x IS azimuth (Body_Back_Center_1 native x=0, forcing 50/50 sews both panels into a front pouch); SetArrangementOrientation flips panel facing but mirrors it L/R; frozen cloth renders cyan; back panel textured face points at the skin -- fix is manual Flip Normal (no API). Texture: positioned plait/rope/fringe tile spanning the 170mm panel once (hem anchors at v=0.5 and wraps). manual_fit_backup.zprj preserves Can's hand-fitted drape. Exports still pending final drape approval. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
6.4 KiB
Python
182 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
make_voyager_bandeau.py -- golden plaited-weave band with black rope trim and
|
|
fringe for the female voyager bandeau top (lena_voyager_bandeau_v1).
|
|
|
|
python tools/tailor/textures/make_voyager_bandeau.py
|
|
|
|
Derived from the reference renders
|
|
`fc-voyager-kilt-gpt-piece-top{,-front,-back}.png` (docs repo): a diagonal
|
|
basket plait in warm golden tan, a black braided-rope band near each edge, and
|
|
a short strand fringe extending to both the top and bottom edges.
|
|
|
|
POSITIONED TILE, same doctrine as make_kilt_cloth.py: spans the bandeau
|
|
panel's full 170 mm height exactly once (dpi = SIZE / (170 / 25.4)), so every
|
|
zone lands at a fixed garment height:
|
|
|
|
garment y (mm, hem=0) zone
|
|
0-15 bottom fringe strands (painted, not geometry -- v1)
|
|
15-27 black rope band (braided chevrons)
|
|
27-143 diagonal basket plait
|
|
143-155 black rope band
|
|
155-170 top fringe strands (tips ragged toward y=170)
|
|
|
|
Horizontally the 170 mm tile repeats ~3.2x across the 550 mm panel; the plait
|
|
pitch (17 mm) and rope chevron pitch (32 px) both divide the tile exactly, so
|
|
the wrap is seamless.
|
|
|
|
IF THE ROPE BANDS RENDER SWAPPED TOP-FOR-BOTTOM, MD anchored the texture the
|
|
other way up -- regenerate with FLIP=True rather than touching the pattern.
|
|
(Anchor per probe 2026-08-18: pattern y=0 (hem) maps to image v=0.5 and wraps.)
|
|
|
|
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, "voyager_bandeau.png")
|
|
|
|
SIZE = 1024 # px, square
|
|
CLOTH_MM = 170.0 # spans the bandeau panel height exactly once
|
|
DPI = SIZE / (CLOTH_MM / 25.4)
|
|
FLIP = False # set True if MD anchors the texture upside-down
|
|
PX_PER_MM = SIZE / CLOTH_MM
|
|
|
|
# zone boundaries in garment mm (hem = 0)
|
|
BOT_FRINGE_TOP = 15.0
|
|
BOT_ROPE_TOP = 27.0
|
|
TOP_ROPE_BOT = 143.0
|
|
TOP_FRINGE_BOT = 155.0
|
|
|
|
PLAIT_PITCH_MM = 17.0 # strand pitch; 170/17 = 10 cells -> seamless wrap
|
|
ROPE_CHEVRON_PX = 32 # divides 1024 -> seamless wrap
|
|
|
|
BASE = (203, 165, 110) # golden tan plait (reference lit mean ~(200,165,115))
|
|
ROPE = (46, 40, 32) # near-black rope, warm
|
|
ROPE_HI = (92, 80, 62) # braid ridge highlight
|
|
FRINGE = (191, 152, 100)
|
|
FRINGE_GAP = (140, 108, 70)
|
|
SHADOW = (74, 60, 44) # past a fringe tip: painted shadow (no alpha in lane)
|
|
|
|
OVER_LIFT = 14
|
|
UNDER_DROP = 18
|
|
ROUND_SHADE = 12
|
|
SLUB_RANGE = 8
|
|
MOTTLE = 10
|
|
MOTTLE_CELL = 64
|
|
|
|
|
|
def lcg(seed):
|
|
state = seed
|
|
while True:
|
|
state = (1103515245 * state + 12345) % (2 ** 31)
|
|
yield state / float(2 ** 31)
|
|
|
|
|
|
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: hem -> v=0.5, wraps)."""
|
|
v = (y + 0.5) / SIZE
|
|
return CLOTH_MM - CLOTH_MM * ((v - 0.5) % 1.0)
|
|
|
|
|
|
def plait(xm, ym, slub_a, slub_b):
|
|
"""Diagonal basket plait: two +/-45-degree strand families, over-under.
|
|
Returns a brightness delta to apply on BASE."""
|
|
u = (xm + ym) / PLAIT_PITCH_MM
|
|
w = (xm - ym) / PLAIT_PITCH_MM
|
|
a, b = int(u // 1), int(w // 1)
|
|
fu, fw = u - a, w - b
|
|
over_a = ((a + b) % 2) == 0
|
|
if over_a:
|
|
# family A on top: rounded across its width (fw), slub along it
|
|
c = abs(fw - 0.5) * 2.0
|
|
d = OVER_LIFT - int(ROUND_SHADE * c * c)
|
|
d += slub_a[a % len(slub_a)]
|
|
if fw < 0.06 or fw > 0.94: # crevice against the under strand
|
|
d -= 26
|
|
else:
|
|
c = abs(fu - 0.5) * 2.0
|
|
d = OVER_LIFT - int(ROUND_SHADE * c * c) - UNDER_DROP // 2
|
|
d += slub_b[b % len(slub_b)]
|
|
if fu < 0.06 or fu > 0.94:
|
|
d -= 26
|
|
return d
|
|
|
|
|
|
def main():
|
|
g = lcg(20260818)
|
|
slub_a = [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(64)]
|
|
slub_b = [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(64)]
|
|
tip_noise = [next(g) for _ in range(64)]
|
|
mottle = value_noise(SIZE // MOTTLE_CELL, seed=777)
|
|
|
|
img = Image.new("RGB", (SIZE, SIZE))
|
|
px = img.load()
|
|
|
|
strand_w = 16 # fringe strand width px (~2.7 mm)
|
|
for y in range(SIZE):
|
|
mm = mm_from_row(y)
|
|
ym = mm
|
|
for x in range(SIZE):
|
|
xm = x / PX_PER_MM
|
|
wear = int(mottle(x, y) * MOTTLE)
|
|
|
|
if mm < BOT_FRINGE_TOP or mm > TOP_FRINGE_BOT:
|
|
# fringe: vertical strands, ragged tips toward the edge
|
|
sid = (x // strand_w) % len(tip_noise)
|
|
ragged = tip_noise[sid] * 8.0
|
|
past_tip = (mm < ragged if mm < BOT_FRINGE_TOP
|
|
else mm > CLOTH_MM - ragged)
|
|
if past_tip:
|
|
base, d = SHADOW, wear // 2
|
|
else:
|
|
u = x % strand_w
|
|
base = FRINGE_GAP if u < 2 else FRINGE
|
|
d = slub_a[sid] + wear
|
|
elif mm < BOT_ROPE_TOP or mm > TOP_ROPE_BOT:
|
|
# braided rope: diagonal chevron ridges
|
|
phase = ((x + int(mm * PX_PER_MM) * 2) % ROPE_CHEVRON_PX) \
|
|
/ float(ROPE_CHEVRON_PX)
|
|
base = ROPE_HI if phase < 0.30 else ROPE
|
|
d = wear // 2
|
|
else:
|
|
base = BASE
|
|
d = plait(xm, ym, slub_a, slub_b) + wear
|
|
|
|
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; ropes %d-%d & %d-%d mm, "
|
|
"fringe 0-%d & %d-%d mm)"
|
|
% (OUT, SIZE, SIZE, DPI, CLOTH_MM, BOT_FRINGE_TOP, BOT_ROPE_TOP,
|
|
TOP_ROPE_BOT, TOP_FRINGE_BOT, BOT_FRINGE_TOP, TOP_FRINGE_BOT, CLOTH_MM))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|