feat(lena): v02 lane recipes + the archival full-res master
The v02 chain, which exists because bisecting the shard-eye defect proved it lives in
the hires MASTER rather than in any v02 step: the early heal chain ran whole-mesh welds
and reset custom split normals, and the eye/lash shells only read as an eye through
Tripo's authored normals. Every descendant inherits it, including the shipped body.
48_decimate_headsafe body/hands only, head bit-identical (feathered ramp)
49_seams_v02 24_seams with a density-aware hip landmark — the v01 rule fired
at u=0.610, mid-belly, on the head-protected vert count
50_seams_headuv body-only unwrap; the head KEEPS its original Tripo charts.
SLIM collapsed the undecimated lash/brow slivers to points and
ANGLE_BASED packed at half v01's texel density; the face was
the best-mapped region of the source atlas, so it is reused
51_reatlas_v02 31_reatlas + centroid splats for sub-texel triangles — the
skipped set IS the lashes, which rendered as grey glass
52_head_transplant the PRISTINE ORIGINAL head onto the decimated nude body
53_rig_transfer 54_crotch_refill
55_fullres_v02.blend is pinned in .lanekeep as THE archival master: full-res nude body
+ pristine original head, crotch refill and texture despeckle applied, 883,404 v /
1,761,640 f. It supersedes 34_v04 as the lane root (34_v04's head has the shard eyes).
Also: tools/graft_hands.py, the Marvelous Designer hunter-skirt configs v1-v8, the
hunter cloth texture generator, and Mako's measurement card.
Per .agents/rules/working-files.md the per-attempt .blend files under work/lena/v02
are SCRATCH ("never committed") — the .py recipes here are the history and regenerate
any of them from the pinned master. See the ignore rule landing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"source": "C:\\Users\\Jeremy\\tinqs\\ariki-game\\assets\\quaternius\\derived-bodies\\Mako_Fullhead_QuatSkin_candidate.glb",
|
||||
"units": "meters (glTF)",
|
||||
"height_total": 1.818,
|
||||
"chest_circ": 1.3592,
|
||||
"chest_z": 1.2992,
|
||||
"waist_circ": 0.8868,
|
||||
"waist_z": 1.065,
|
||||
"hip_circ": 1.0143,
|
||||
"hip_z": 0.9321,
|
||||
"thigh_circ": 0.6726,
|
||||
"thigh_z": 0.813,
|
||||
"neck_circ": 0.6052,
|
||||
"neck_z": 1.5661,
|
||||
"bicep_circ": 0.5922,
|
||||
"shoulder_width": 0.3838,
|
||||
"arm_len_shoulder_to_wrist": 0.5471,
|
||||
"nape_to_pelvis": 0.5709,
|
||||
"crotch_height": 0.9321,
|
||||
"pelvis_height": 0.9167,
|
||||
"knee_height": 0.5318,
|
||||
"ankle_height": 0.1037,
|
||||
"shoulder_z": 1.4579,
|
||||
"notes": {
|
||||
"verified": "Torso circumference profile re-run independently 2026-08-12; chest/waist/hip/thigh are real body geometry (dominant groups all spine_*/pelvis/thigh_*, no arm intrusion at T-pose).",
|
||||
"neck_circ_unreliable": "neck_circ/neck_z measure the GRAFTED FULL-RES HEAD, not the neck. The scan's min landed at z=1.566 where the slice is 340+ verts of the Head group (hair/jaw), reading 60.6 cm. Do not draft a collar to this. The body mesh's own neck stump narrows to 51 cm at z=1.525 and the body geometry ENDS at z=1.545 - above that is all Head group. Treat z=1.52-1.55 as the collar ceiling.",
|
||||
"body_mesh_top_z": 1.545,
|
||||
"shape_vs_lena": "Inverted triangle where Lena is a pear. chest +27 cm, waist +21 cm, hip -8 cm, thigh -17 cm, shoulder_width +6 cm vs Ariki_Female_QuatSkin. Lena garment configs will bind at the chest and hang loose at the hips - redraft, do not re-drape.",
|
||||
"vert_budget": "122,067 verts total: Head 81,552 (full-res, includes hair), hand_l/r 22,472 (rigid mitts), torso+limbs the remainder. Heavy avatar for MD - simulate with patience.",
|
||||
"md_avatar_fbx": "tools/tailor/avatar/Mako_Fullhead_QuatSkin_Avatar.fbx (this GLB, Icosphere stray dropped, leaf bones stripped). Blender-exported so import with op.scale = 10.0."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
make_hunter_cloth.py -- coarse woven brown cloth for the hunter wrap skirt.
|
||||
|
||||
python tools/tailor/textures/make_hunter_cloth.py
|
||||
|
||||
Derived from the reference renders
|
||||
`male-clothing-hunter-gpt-v2{,-side,-back}.png`: a dark warm-brown coarse plain
|
||||
weave, roughly burlap/harakeke-sack in character, with visible thread grain and
|
||||
wear mottling.
|
||||
|
||||
WHY THE COLOUR IS BAKED IN, NOT GREY
|
||||
The clothing pipeline has NO alpha and `apply_fabric_texture()` wires the PNG
|
||||
straight into Base Color, *replacing* the part's flat `color` rather than
|
||||
multiplying it. So a grayscale weave renders grey in-game, not brown. Every
|
||||
value below is a final albedo, not a mask.
|
||||
|
||||
WHY THESE RGB NUMBERS
|
||||
Sampled from the reference renders: garment mean (56,35,26) with highlights to
|
||||
about (92,61,45). Those are *lit* pixels from a dim studio setup, so the albedo
|
||||
sits above them -- BASE is set brighter so that in-game lighting lands the
|
||||
garment back on the reference's apparent tone instead of crushing it to near
|
||||
black. Warm ramp throughout: r > g > b, r-b about 45.
|
||||
|
||||
WHY DPI AND NOT IMAGE SCALE
|
||||
In MD the PNG's DPI sets the cloth's physical size, so tiling is controlled by
|
||||
dpi, never by resizing the image. This tile represents CLOTH_MM of fabric:
|
||||
dpi = SIZE / (CLOTH_MM / 25.4). At 100 mm it repeats about 7x across the skirt's
|
||||
687 mm hem, which is what keeps the weave reading as thread rather than pattern.
|
||||
|
||||
DETERMINISM
|
||||
No `random` and no time source -- a fixed LCG plus a fixed hash, so the file is
|
||||
byte-identical run to run. This PNG is a build input; a texture that changes
|
||||
under you turns a placement regression into a wild goose chase.
|
||||
"""
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(HERE, "hunter_cloth.png")
|
||||
|
||||
SIZE = 512 # px, square
|
||||
CLOTH_MM = 100.0 # physical span this tile represents
|
||||
DPI = SIZE / (CLOTH_MM / 25.4)
|
||||
|
||||
BASE = (108, 80, 60) # mid warm brown albedo
|
||||
THREAD_PITCH = 16 # px per thread; 512px/100mm -> ~3.1 mm threads (coarse)
|
||||
OVER_LIFT = 16 # threads on top of the weave are lighter
|
||||
UNDER_DROP = 20 # threads passing under are shaded
|
||||
ROUND_SHADE = 14 # cross-thread rounding falloff
|
||||
SLUB_RANGE = 10 # per-thread thickness/tone irregularity
|
||||
MOTTLE = 16 # large-scale wear variation
|
||||
MOTTLE_CELL = 64 # px per mottle cell
|
||||
|
||||
|
||||
def lcg(seed):
|
||||
"""Deterministic 0..1 sequence. Fixed constants (glibc), fixed seed."""
|
||||
state = seed
|
||||
while True:
|
||||
state = (1103515245 * state + 12345) % (2 ** 31)
|
||||
yield state / float(2 ** 31)
|
||||
|
||||
|
||||
def thread_tones(n, seed):
|
||||
"""One slub value per thread, so a thread's irregularity runs its length --
|
||||
per-pixel noise would read as sand, not as spun fibre."""
|
||||
g = lcg(seed)
|
||||
return [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(n)]
|
||||
|
||||
|
||||
def value_noise(cells, seed):
|
||||
"""Coarse lattice of values, bilinearly interpolated -> smooth wear blotches."""
|
||||
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
|
||||
# smoothstep so cell borders don't show as creases
|
||||
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 main():
|
||||
n_threads = SIZE // THREAD_PITCH
|
||||
warp = thread_tones(n_threads, seed=20260812)
|
||||
weft = thread_tones(n_threads, seed=90210)
|
||||
mottle = value_noise(SIZE // MOTTLE_CELL, seed=5150)
|
||||
|
||||
img = Image.new("RGB", (SIZE, SIZE))
|
||||
px = img.load()
|
||||
|
||||
for y in range(SIZE):
|
||||
j = (y // THREAD_PITCH) % n_threads
|
||||
# position across the weft thread, -1..1, for rounding
|
||||
vy = ((y % THREAD_PITCH) / (THREAD_PITCH - 1.0)) * 2.0 - 1.0
|
||||
for x in range(SIZE):
|
||||
i = (x // THREAD_PITCH) % n_threads
|
||||
vx = ((x % THREAD_PITCH) / (THREAD_PITCH - 1.0)) * 2.0 - 1.0
|
||||
|
||||
# plain weave: alternate which thread sits on top
|
||||
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
|
||||
|
||||
# warm ramp: brown shifts warmer as it lightens, cooler in shadow
|
||||
r = BASE[0] + d
|
||||
g = BASE[1] + int(d * 0.78)
|
||||
b = BASE[2] + int(d * 0.62)
|
||||
px[x, y] = (max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b)))
|
||||
|
||||
img.save(OUT, dpi=(DPI, DPI))
|
||||
|
||||
vals = [px[x, y] for y in range(0, SIZE, 8) for x in range(0, SIZE, 8)]
|
||||
n = len(vals)
|
||||
mean = tuple(sum(v[c] for v in vals) // n for c in range(3))
|
||||
print("wrote %s (%dx%d, dpi %.1f -> %.0f mm of cloth)"
|
||||
% (OUT, SIZE, SIZE, DPI, CLOTH_MM))
|
||||
print("mean albedo %s (reference lit mean was (56,35,26))" % (mean,))
|
||||
print("range r %d-%d g %d-%d b %d-%d"
|
||||
% (min(v[0] for v in vals), max(v[0] for v in vals),
|
||||
min(v[1] for v in vals), max(v[1] for v in vals),
|
||||
min(v[2] for v in vals), max(v[2] for v in vals)))
|
||||
warm = mean[0] - mean[2]
|
||||
print("warmth r-b = %d (reference %d)" % (warm, 56 - 26))
|
||||
if warm < 30:
|
||||
print("WARNING: not warm enough -- will read as grey cloth")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user