Files
animation/tools/tailor/textures/make_voyager_bandeau.py
T

182 lines
6.4 KiB
Python
Raw Normal View History

#!/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()