#!/usr/bin/env python3 """ make_bottom_test1.py — flax-strand skirt texture for bottomTest1. python tools/tailor/textures/make_bottom_test1.py WHY THIS EXISTS The first bottom_test1.png (kept as bottom_test1_prestrand.png) was authored for a SOLID skirt panel and has two properties that fight the strand geometry: 1. It paints its own VERTICAL STRAND LINES. The garment is now 32 discrete geometric strands, so those painted lines double up — strands-within-strands. 2. Its band region VARIES ALONG U (measured horizontal stdev 68-83 per row, vs 0-13 on the clean rows). Every geometric strand samples a different slice of that region, so the horizontal bands do not line up strand-to-strand and the skirt reads as broken noise instead of woven rows. THE RULE, and it is the whole trick: for a strand garment the texture must be a function of V ONLY. Constant along U means every strand samples the same colour at the same height, so bands align across all 32 strands automatically — regardless of each strand's UV position, its width, or how the drape stretched it. No UV surgery needed, and it stays true if the strand count changes. METHOD Collapse the reference to V-only by taking each row's MEDIAN colour. That keeps the original vertical rhythm (waistband, band groups, plain flax field) — the design intent — and throws away only the horizontal variation that was causing the noise. Then add back a very low-contrast vertical striation for fibre feel, small enough (+/- STRIATION levels) that it cannot resurrect the misalignment. Re-sampling happens through the EXISTING UVs, so this needs no MD session: rebuild the PNG, copy it next to the exported glTF, reimport the texture. """ import os import statistics from PIL import Image HERE = os.path.dirname(os.path.abspath(__file__)) REF = os.path.join(HERE, "bottom_test1_prestrand.png") # the pre-strand original OUT = os.path.join(HERE, "bottom_test1.png") SIZE = 1024 STRIATION = 6 # +/- levels of vertical fibre variation. Keep SMALL. STRIATION_PERIOD = 7 # px between fibre lines DPI = 43.3 # matches the original: 1024 px at 43.3 dpi = 600 mm of cloth def main(): ref = Image.open(REF).convert("RGB").resize((SIZE, SIZE), Image.LANCZOS) px = ref.load() out = Image.new("RGB", (SIZE, SIZE)) op = out.load() row_var_before, row_var_after = [], [] for y in range(SIZE): row = [px[x, y] for x in range(SIZE)] med = tuple(int(statistics.median(c[i] for c in row)) for i in range(3)) row_var_before.append(statistics.pstdev([sum(c) / 3 for c in row])) for x in range(SIZE): # deterministic, seed-free striation: a fixed comb, not noise, so the # result is byte-identical run to run (Date/random are avoided on # purpose — this file is a build input). d = STRIATION if (x % STRIATION_PERIOD) < STRIATION_PERIOD // 2 else -STRIATION op[x, y] = tuple(max(0, min(255, med[i] + d)) for i in range(3)) row_var_after.append( statistics.pstdev([sum(op[x, y]) / 3 for x in range(0, SIZE, 4)])) out.save(OUT, dpi=(DPI, DPI)) print("wrote %s (%dx%d, dpi %.1f)" % (OUT, SIZE, SIZE, DPI)) print("mean horizontal stdev per row: %.1f -> %.1f (lower = bands align)" % (sum(row_var_before) / SIZE, sum(row_var_after) / SIZE)) print("worst row stdev: %.1f -> %.1f" % (max(row_var_before), max(row_var_after))) if __name__ == "__main__": main()