187 lines
6.7 KiB
Python
187 lines
6.7 KiB
Python
|
|
#!/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()
|