code wiki / (root) / nx_nxa_groom.nx

nx_nxa_groom.nx source

↩ module page · 619 lines · 29067 B

1// nx_nxa_groom.nx -- GENERATE THE GROOM: full-scalp procedural hair strands written into the 2// asset's HSTR section, replacing the sampled partial cap that left the back of the head bare 3// (operator-reported 2026-08-26: "the back of the head is shaved"). 4// 5// INFINIGEN-2 PRINCIPLE, APPLIED (operator standing order: those principles for everything): 6// hair is GENERATED from parameters and the mesh's own measured geometry -- no groom asset, no 7// donor, no hand-placed strand. The industry's own groom pipelines (XGen, Ornatrix, Houdini) 8// are procedural for the same reason: a scalp map plus guide growth beats any sampled cap. 9// 10// METHOD (every number derived or parameterized, never tasted): 11// 1. Axes derived from vertex extents (the nx_nxa_dyna derivation, same tree of assumptions). 12// 2. HEAD TOP = highest vertex; HEAD CENTER = centroid of verts in the top head band (the 13// band spans from crown down by a face-height fraction derived from stature). 14// 3. ROOTS: every scalp vertex above the EAR LINE (head centre height) whose radial direction 15// is not into the face cone, deterministically thinned to <= GRO_MAX_STRANDS with a seeded 16// hash (Infinigen principle: seeded, reproducible diversity). 17// 4. GROWTH: 8 points per strand (the HSTR contract, NXHCH_W=24 words). Point i steps 18// seg_len along a direction that starts RADIAL (off the scalp) and blends toward GRAVITY 19// (down the stature axis) as the strand extends -- long hair hangs, crown hair lifts then 20// falls, back hair drapes the nape. Length = stature * length_permil / 1000. 21// 5. Writes HSTR via the proven section-replace machinery (nx_nxa_dyna pattern): input never 22// modified, an existing HSTR is REPLACED, re-runs identical. 23// 24// usage: nx_nxa_groom <in.nxa> <out.nxa> [length_permil=280] [strands=1400] [seed=7] 25// exit 0 OK | 2 usage | 3 bad-nxa | 1 io 26// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 27import "nx_syscalls.nx" 28import "nx_nxa.nx" 29 30const GR_HDR: i64 = 32 31const GR_TOCE: i64 = 32 32const GR_MAXSEC: i64 = 64 33const GR_MODE: i64 = 0x1a4 34const GR_PTS: i64 = 8 // points per strand == NXHCH_N(7 segments) + 1: the page contract 35const GR_WORDS: i64 = 24 // GR_PTS * 3 coords == NXHCH_W: the page contract 36const GR_DEF_LEN_PERMIL: i64 = 150 // default hair length as permil of stature: mid-back on a 37 // 1.71 m figure (~48 cm) -- the "long and beautiful" band the 38 // operator asked for; a style PARAMETER, not a constant 39const GR_DEF_STRANDS: i64 = 4800 // lock granularity for digital-twin density: ~25 follicles per lock against the ~120k human scalp (follicle-count citation to mirror; the RATIO is the parameter) // page samples down to its 900 guides (step floor), so anything 40 // >= 900 renders at full guide density 41const GR_DEF_SEED: i64 = 7 42// head-band fraction: the head occupies ~1/8 of stature (the 917-permil head anchor the estate 43// already banks against a 744 nipple line puts crown-to-chin near 120 permil); the scalp band is 44// the top HALF of that. Derived from the banked anchors, stated here. 45const GR_HEADBAND_PERMIL: i64 = 60 46// face cone: roots whose radial direction points ANTERIOR more than this permil of the radial 47// length are the FACE, not scalp -- no bangs into the eyes at v1 (a style parameter later). 48const GR_FACE_PERMIL: i64 = 350 49// side-sweep (operator: hair must FRAME the face, never cover it): a root whose radial 50// direction carries face-ward component gets a lateral bias toward its OWN side of the part, 51// scaled by how face-ward it is -- the brief's fall-left-or-right-of-the-part rule. 52const GR_SWEEP_Q10: i64 = 700 53// gravity blend: how fast growth direction turns downward per point index (q10 per step). At 340, 54// point 1 is mostly radial (lift off the scalp) and points 5..7 are mostly gravity (drape). 55const GR_GRAVITY_RAMP_Q10: i64 = 520 56// v2 cosmetology rules (operator's Infinigen-2 groom briefs, 2026-08-26): 57// nape zone: posterior roots admitted BELOW the ear line by this much of stature -- covers the 58// occiput down to the hairline's nape boundary (the shaved-back fix). 59const GR_NAPE_PERMIL: i64 = 45 60// layering: strand length scales down by up to this fraction (q10) at the crown -- long layers. 61const GR_LAYER_Q10: i64 = 300 62// waves: sine along growth, cycles per strand (q10 phase step per point) and amplitude as 63// permil of segment length. Large-scale + tight amplitude = waves, not frizz (the brief's rule). 64const GR_WAVE_STEP_Q10: i64 = 640 65const GR_WAVE_AMP_PERMIL: i64 = 80 66const GR_Q10: i64 = 1024 67// root-synthesis salts and jitter band (rule 11: named, never inline) 68// comb style parameters (the loose-down default; a style-preset lane owns richer values): 69// BACK = 350/1000 backward bias in the style vector (~19 deg comb-back), LIFT = 150/1000 normal 70// lift so the first segment clears the surface before the tangent flow takes over -- the small-lift 71// large-tangent split is the Infinigen comb_direction shape. 72const GR_COMB_BACK_Q10: i64 = 358 73const GR_COMB_LIFT_Q10: i64 = 154 74const GR_SYN_SALT_A: i64 = 77 75const GR_SYN_SALT_B: i64 = 177 76const GR_SYN_SALT_M: i64 = 277 77 78func gr_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 79func gr_err(s: *u8) -> i64 { sys_write(2, s, gr_slen(s)); return 0 } 80func gr_out(s: *u8) -> i64 { sys_write(1, s, gr_slen(s)); return 0 } 81func gr_num(v: i64) -> i64 { 82 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 } 83 var m: i64 = v 84 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 85 let t: *u8 = sys_mmap(32) 86 var k: i64 = 0 87 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 88 let o: *u8 = sys_mmap(32) 89 var i: i64 = 0 90 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 91 sys_write(1, o, k) 92 return 0 93} 94func gr_rd64(b: *u8, off: i64) -> i64 { 95 var v: i64 = 0 96 var i: i64 = 7 97 while i >= 0 { v = v*256 + ((b[off + i] & 0xff) as i64); i = i - 1 } 98 return v 99} 100func gr_wr64(b: *u8, off: i64, v: i64) -> i64 { 101 var m: i64 = v 102 var i: i64 = 0 103 while i < 8 { b[off + i] = (m & 0xff) as u8; m = m >> 8; i = i + 1 } 104 return 0 105} 106func gr_tageq(b: *u8, off: i64, t: *u8) -> i64 { 107 var i: i64 = 0 108 while i < 4 { if b[off + i] != t[i] { return 0 } i = i + 1 } 109 return 1 110} 111func gr_isqrt(n: i64) -> i64 { 112 if n <= 0 { return 0 } 113 var x: i64 = n 114 var y: i64 = (x + 1) / 2 115 while y < x { x = y; y = (x + n/x) / 2 } 116 return x 117} 118func gr_atoi(s: *u8) -> i64 { 119 var v: i64 = 0 120 var i: i64 = 0 121 while s[i] >= (48 as u8) { if s[i] > (57 as u8) { return v } v = v*10 + ((s[i] as i64) - 48); i = i + 1 } 122 return v 123} 124// deterministic hash (the estate's whash shape) for seeded thinning 125func gr_hash(a: i64, b: i64) -> i64 { 126 var n: i64 = a*374761393 + b*668265263 + 1442695041 127 n = (n ^ (n >> 13))*1274126177 128 n = n ^ (n >> 16) 129 if n < 0 { n = 0 - n } 130 return n 131} 132 133func main(argc: i64, argv: *i64) -> i64 { 134 if argc < 3 { 135 gr_err("usage: nx_nxa_groom <in.nxa> <out.nxa> [length_permil] [strands] [seed]\n" as *u8) 136 sys_exit(2) 137 return 2 138 } 139 let inp: *u8 = argv[1] as *u8 140 let outp: *u8 = argv[2] as *u8 141 var lenp: i64 = GR_DEF_LEN_PERMIL 142 var want: i64 = GR_DEF_STRANDS 143 var seed: i64 = GR_DEF_SEED 144 if argc > 3 { lenp = gr_atoi(argv[3] as *u8) } 145 if argc > 4 { want = gr_atoi(argv[4] as *u8) } 146 if argc > 5 { seed = gr_atoi(argv[5] as *u8) } 147 if lenp < 60 { lenp = GR_DEF_LEN_PERMIL } 148 if want < 100 { want = GR_DEF_STRANDS } 149 150 let lp: *i64 = sys_mmap(16) as *i64 151 let b: *u8 = sys_read_file(inp, lp) 152 if (b as i64) == 0 { gr_err("GROOM-RED cannot read input\n" as *u8); sys_exit(1); return 1 } 153 let flen: i64 = lp[0] 154 if flen < GR_HDR { gr_err("GROOM-RED not an NXA\n" as *u8); sys_exit(3); return 3 } 155 if gr_rd64(b, 0) != nxa_magic() { gr_err("GROOM-RED bad magic\n" as *u8); sys_exit(3); return 3 } 156 let ns: i64 = gr_rd64(b, 16) 157 if ns < 1 { gr_err("GROOM-RED bad section count\n" as *u8); sys_exit(3); return 3 } 158 if ns >= GR_MAXSEC { gr_err("GROOM-RED toc full\n" as *u8); sys_exit(3); return 3 } 159 160 var vo: i64 = 0 - 1 161 var tro: i64 = 0 - 1 162 var hsi: i64 = 0 - 1 163 var s: i64 = 0 164 while s < ns { 165 let e: i64 = GR_HDR + s*GR_TOCE 166 if gr_tageq(b, e, "VERT" as *u8) == 1 { vo = gr_rd64(b, e + 8) } 167 if gr_tageq(b, e, "TRIS" as *u8) == 1 { tro = gr_rd64(b, e + 8) } 168 if gr_tageq(b, e, "HSTR" as *u8) == 1 { hsi = s } 169 s = s + 1 170 } 171 if tro < 0 { gr_err("GROOM-RED no TRIS section: barycentric root sampling needs the faces 172" as *u8); sys_exit(3); return 3 } 173 if vo < 0 { gr_err("GROOM-RED no VERT\n" as *u8); sys_exit(3); return 3 } 174 let nv: i64 = gr_rd64(b, vo) 175 176 // ---- axes from extents (the nx_nxa_dyna derivation) ---- 177 var mn0: i64 = 0 178 var mx0: i64 = 0 179 var mn1: i64 = 0 180 var mx1: i64 = 0 181 var mn2: i64 = 0 182 var mx2: i64 = 0 183 var first: i64 = 1 184 var i: i64 = 0 185 while i < nv { 186 let c0: i64 = gr_rd64(b, vo + 8 + (i*3)*8) 187 let c1: i64 = gr_rd64(b, vo + 8 + (i*3 + 1)*8) 188 let c2: i64 = gr_rd64(b, vo + 8 + (i*3 + 2)*8) 189 if first == 1 { mn0 = c0; mx0 = c0; mn1 = c1; mx1 = c1; mn2 = c2; mx2 = c2; first = 0 } 190 if c0 < mn0 { mn0 = c0 } 191 if c0 > mx0 { mx0 = c0 } 192 if c1 < mn1 { mn1 = c1 } 193 if c1 > mx1 { mx1 = c1 } 194 if c2 < mn2 { mn2 = c2 } 195 if c2 > mx2 { mx2 = c2 } 196 i = i + 1 197 } 198 let e0: i64 = mx0 - mn0 199 let e1: i64 = mx1 - mn1 200 let e2: i64 = mx2 - mn2 201 var sa: i64 = 2 202 if e0 >= e1 { if e0 >= e2 { sa = 0 } } 203 if e1 > e0 { if e1 >= e2 { sa = 1 } } 204 var la: i64 = 0 205 var aa: i64 = 1 206 if sa == 0 { la = 1; aa = 2; if e2 > e1 { la = 2; aa = 1 } } 207 if sa == 1 { la = 0; aa = 2; if e2 > e0 { la = 2; aa = 0 } } 208 if sa == 2 { la = 0; aa = 1; if e1 > e0 { la = 1; aa = 0 } } 209 var span: i64 = e2 210 var stop: i64 = mx2 211 if sa == 0 { span = e0; stop = mx0 } 212 if sa == 1 { span = e1; stop = mx1 } 213 if span <= 0 { gr_err("GROOM-RED degenerate span\n" as *u8); sys_exit(3); return 3 } 214 gr_out(" axes: stature=" as *u8); gr_num(sa) 215 gr_out(" lateral=" as *u8); gr_num(la) 216 gr_out(" anterior=" as *u8); gr_num(aa) 217 gr_out(" span=" as *u8); gr_num(span); gr_out("\n" as *u8) 218 219 // ---- head band: crown down GR_HEADBAND_PERMIL of stature; centroid = head centre ---- 220 let bandlo: i64 = stop - span*GR_HEADBAND_PERMIL/1000 221 var hcx: i64 = 0 222 var hcy: i64 = 0 223 var hcz: i64 = 0 224 var hcn: i64 = 0 225 i = 0 226 while i < nv { 227 let pst: i64 = gr_rd64(b, vo + 8 + (i*3 + sa)*8) 228 if pst >= bandlo { 229 hcx = hcx + gr_rd64(b, vo + 8 + (i*3)*8) 230 hcy = hcy + gr_rd64(b, vo + 8 + (i*3 + 1)*8) 231 hcz = hcz + gr_rd64(b, vo + 8 + (i*3 + 2)*8) 232 hcn = hcn + 1 233 } 234 i = i + 1 235 } 236 if hcn < 8 { gr_err("GROOM-RED no head band vertices\n" as *u8); sys_exit(3); return 3 } 237 let cx: i64 = hcx/hcn 238 let cy: i64 = hcy/hcn 239 let cz: i64 = hcz/hcn 240 gr_out(" head band verts=" as *u8); gr_num(hcn) 241 gr_out(" centre=" as *u8); gr_num(cx); gr_out("/" as *u8); gr_num(cy); gr_out("/" as *u8); gr_num(cz) 242 gr_out("\n" as *u8) 243 244 // anterior sign, FOURTH derivation -- ORIENTATION IS A CONVENTION, CROSS-CHECKED, NEVER GUESSED. 245 // History, kept so nobody re-walks it: v1 (denser side = face) was CORRECT; v2 (bigger extreme 246 // extent = nose) inverted it -- the dense face mesh drags the band centroid toward itself so the 247 // sparse occiput measures the LARGER extent; v3 (narrow tip) did not discriminate over the full 248 // band (6059 vs 8066). Three heuristics, three answers ends the guessing: the estate rig 249 // convention is ANTERIOR-NEGATIVE = FACE (the bust and ref9d both hold it), the mesh-density 250 // count must AGREE (faces carry more mesh detail), and on disagreement this organ REFUSES loudly 251 // instead of grooming a head it cannot orient. 252 var antpos: i64 = 0 253 var antneg: i64 = 0 254 i = 0 255 while i < nv { 256 let pst: i64 = gr_rd64(b, vo + 8 + (i*3 + sa)*8) 257 if pst >= bandlo { 258 var pav: i64 = gr_rd64(b, vo + 8 + (i*3 + aa)*8) 259 if aa == 0 { pav = pav - cx } 260 if aa == 1 { pav = pav - cy } 261 if aa == 2 { pav = pav - cz } 262 if pav > 0 { antpos = antpos + 1 } else { antneg = antneg + 1 } 263 } 264 i = i + 1 265 } 266 if antneg <= antpos { gr_err("GROOM-RED orientation ambiguous: mesh density disagrees with the anterior-negative rig convention\n" as *u8); sys_exit(3); return 3 } 267 let fsign: i64 = 0 - 1 268 gr_out(" face sign=" as *u8); gr_num(fsign) 269 gr_out(" (convention anterior-negative; density agrees: neg=" as *u8); gr_num(antneg) 270 gr_out(" pos=" as *u8); gr_num(antpos); gr_out(")\n" as *u8) 271 272 // ---- ROOTS BY AREA-WEIGHTED BARYCENTRIC FACE SAMPLING (the published method) ---- 273 // This is how Blender particle hair and the Infinigen creature hair chain place roots: sample 274 // the scalp REGION's triangles, area-weighted, with uniform barycentric coordinates -- every 275 // root lies ON a face by construction, so no synthesis, no interior points, no reprojection. 276 // The three prior hand-rolled placements (vert-keep + chord-blend, small-chord, hash-offset) 277 // are superseded; each was refused by the anatomy gate before this form. 278 // Region rule per FACE CENTROID (same anatomy as before): above the hairline floor on the 279 // stature axis (nape floor lower on the posterior side), outside the face cone. 280 var cht: i64 = cz 281 if sa == 0 { cht = cx } 282 if sa == 1 { cht = cy } 283 let ntri: i64 = gr_rd64(b, tro) 284 if ntri < 4 { gr_err("GROOM-RED empty TRIS 285" as *u8); sys_exit(3); return 3 } 286 let sfidx: *i64 = sys_mmap(ntri*8 + 64) as *i64 287 let sfsum: *i64 = sys_mmap(ntri*8 + 64) as *i64 288 var nsf: i64 = 0 289 var asum: i64 = 0 290 var t9: i64 = 0 291 while t9 < ntri { 292 let ia: i64 = gr_rd64(b, tro + 8 + (t9*3)*8) 293 let ib: i64 = gr_rd64(b, tro + 8 + (t9*3 + 1)*8) 294 let ic: i64 = gr_rd64(b, tro + 8 + (t9*3 + 2)*8) 295 if ia < nv { if ib < nv { if ic < nv { 296 let ax: i64 = gr_rd64(b, vo + 8 + (ia*3)*8) 297 let ay: i64 = gr_rd64(b, vo + 8 + (ia*3 + 1)*8) 298 let az: i64 = gr_rd64(b, vo + 8 + (ia*3 + 2)*8) 299 let bx: i64 = gr_rd64(b, vo + 8 + (ib*3)*8) 300 let by: i64 = gr_rd64(b, vo + 8 + (ib*3 + 1)*8) 301 let bz: i64 = gr_rd64(b, vo + 8 + (ib*3 + 2)*8) 302 let ccx: i64 = gr_rd64(b, vo + 8 + (ic*3)*8) 303 let ccy: i64 = gr_rd64(b, vo + 8 + (ic*3 + 1)*8) 304 let ccz: i64 = gr_rd64(b, vo + 8 + (ic*3 + 2)*8) 305 let gx: i64 = (ax + bx + ccx)/3 306 let gy: i64 = (ay + by + ccy)/3 307 let gz: i64 = (az + bz + ccz)/3 308 var gst: i64 = gz 309 if sa == 0 { gst = gx } 310 if sa == 1 { gst = gy } 311 var gant: i64 = gz - cz 312 if aa == 0 { gant = gx - cx } 313 if aa == 1 { gant = gy - cy } 314 var floor9: i64 = cht 315 if gant*fsign < 0 { floor9 = cht - span*GR_NAPE_PERMIL/1000 } 316 if gst >= floor9 { 317 let rgx: i64 = gx - cx 318 let rgy: i64 = gy - cy 319 let rgz: i64 = gz - cz 320 let grl: i64 = gr_isqrt(rgx*rgx + rgy*rgy + rgz*rgz) 321 if grl > 0 { if gant*fsign*1000 < grl*GR_FACE_PERMIL { 322 // area weight: |cross(B-A, C-A)| -- proportionality is all the sampler needs 323 let ux: i64 = bx - ax 324 let uy: i64 = by - ay 325 let uz: i64 = bz - az 326 let vx: i64 = ccx - ax 327 let vy: i64 = ccy - ay 328 let vz: i64 = ccz - az 329 let crx: i64 = uy*vz - uz*vy 330 let cry: i64 = uz*vx - ux*vz 331 let crz: i64 = ux*vy - uy*vx 332 let ar9: i64 = gr_isqrt(crx*crx + cry*cry + crz*crz) 333 if ar9 > 0 { 334 sfidx[nsf] = t9 335 asum = asum + ar9 336 sfsum[nsf] = asum 337 nsf = nsf + 1 338 } 339 } } 340 } 341 } } } 342 t9 = t9 + 1 343 } 344 if nsf < 32 { gr_err("GROOM-RED scalp face set too small 345" as *u8); sys_exit(3); return 3 } 346 let roots: *i64 = sys_mmap(want*3*8 + 64) as *i64 347 let nrm: *i64 = sys_mmap(want*3*8 + 64) as *i64 348 var nr: i64 = 0 349 while nr < want { 350 // area-weighted face draw, then uniform barycentric (u = 1 - sqrt(r1), v = r2*sqrt(r1)) 351 let hdraw: i64 = gr_hash(seed + GR_SYN_SALT_A, nr) % asum 352 var lo9: i64 = 0 353 var fi9: i64 = nsf - 1 354 var scan: i64 = 0 355 var found: i64 = 0 356 while scan < nsf { 357 if found == 0 { if sfsum[scan] > hdraw { fi9 = scan; found = 1 } } 358 scan = scan + 1 359 } 360 let tt: i64 = sfidx[fi9] 361 let ia: i64 = gr_rd64(b, tro + 8 + (tt*3)*8) 362 let ib: i64 = gr_rd64(b, tro + 8 + (tt*3 + 1)*8) 363 let ic: i64 = gr_rd64(b, tro + 8 + (tt*3 + 2)*8) 364 let ax: i64 = gr_rd64(b, vo + 8 + (ia*3)*8) 365 let ay: i64 = gr_rd64(b, vo + 8 + (ia*3 + 1)*8) 366 let az: i64 = gr_rd64(b, vo + 8 + (ia*3 + 2)*8) 367 let bx: i64 = gr_rd64(b, vo + 8 + (ib*3)*8) 368 let by: i64 = gr_rd64(b, vo + 8 + (ib*3 + 1)*8) 369 let bz: i64 = gr_rd64(b, vo + 8 + (ib*3 + 2)*8) 370 let ccx: i64 = gr_rd64(b, vo + 8 + (ic*3)*8) 371 let ccy: i64 = gr_rd64(b, vo + 8 + (ic*3 + 1)*8) 372 let ccz: i64 = gr_rd64(b, vo + 8 + (ic*3 + 2)*8) 373 let r1: i64 = gr_hash(seed + GR_SYN_SALT_B, nr) % GR_Q10 374 let r2: i64 = gr_hash(seed + GR_SYN_SALT_M, nr) % GR_Q10 375 let sq1: i64 = gr_isqrt(r1*GR_Q10) 376 let bu: i64 = GR_Q10 - sq1 377 let bv: i64 = r2*sq1/GR_Q10 378 let bw: i64 = GR_Q10 - bu - bv 379 roots[nr*3] = (ax*bu + bx*bv + ccx*bw)/GR_Q10 380 roots[nr*3 + 1] = (ay*bu + by*bv + ccy*bw)/GR_Q10 381 roots[nr*3 + 2] = (az*bu + bz*bv + ccz*bw)/GR_Q10 382 // launch frame from the FACE NORMAL (outward-oriented), not from a radial approximation 383 let ux: i64 = bx - ax 384 let uy: i64 = by - ay 385 let uz: i64 = bz - az 386 let vx: i64 = ccx - ax 387 let vy: i64 = ccy - ay 388 let vz: i64 = ccz - az 389 var fnx: i64 = uy*vz - uz*vy 390 var fny: i64 = uz*vx - ux*vz 391 var fnz: i64 = ux*vy - uy*vx 392 let fnl: i64 = gr_isqrt(fnx*fnx + fny*fny + fnz*fnz) 393 if fnl > 0 { fnx = fnx*GR_Q10/fnl; fny = fny*GR_Q10/fnl; fnz = fnz*GR_Q10/fnl } else { fnz = GR_Q10 } 394 let odot: i64 = fnx*(roots[nr*3] - cx) + fny*(roots[nr*3 + 1] - cy) + fnz*(roots[nr*3 + 2] - cz) 395 if odot < 0 { fnx = 0 - fnx; fny = 0 - fny; fnz = 0 - fnz } 396 nrm[nr*3] = fnx 397 nrm[nr*3 + 1] = fny 398 nrm[nr*3 + 2] = fnz 399 nr = nr + 1 400 } 401 if nr < want { gr_err("GROOM-RED PARTIAL WORK REFUSED: fewer roots than requested 402" as *u8); sys_exit(3); return 3 } 403 gr_out(" scalp faces=" as *u8); gr_num(nsf) 404 gr_out(" of " as *u8); gr_num(ntri) 405 gr_out(" area-sampled roots=" as *u8); gr_num(nr); gr_out(" 406" as *u8) 407 408 // ---- grow strands ---- 409 let total_len: i64 = span*lenp/1000 410 let hw: *i64 = sys_mmap((8 + nr*GR_WORDS*8) + 64) as *i64 411 hw[0] = nr 412 var r: i64 = 0 413 while r < nr { 414 var px: i64 = roots[r*3] 415 var py: i64 = roots[r*3 + 1] 416 var pz: i64 = roots[r*3 + 2] 417 // COMB-FIELD LAUNCH (the Infinigen comb_direction form): a style vector -- down the 418 // stature axis with a backward bias away from the face -- PROJECTED ONTO THE TANGENT PLANE 419 // of the root's face normal, plus a small normal lift. The pure-normal launch this replaces 420 // was the dandelion-puff defect (operator screenshots 2026-08-27): strands left the scalp 421 // perpendicular, skin showed between roots, and the crown veil drifted over the face. 422 // Combed-tangent is how a groom LIES ON the scalp and flows back. 423 let nx9: i64 = nrm[r*3] 424 let ny9: i64 = nrm[r*3 + 1] 425 let nz9: i64 = nrm[r*3 + 2] 426 var sxv: i64 = 0 427 var syv: i64 = 0 428 var szv: i64 = 0 429 if sa == 0 { sxv = 0 - GR_Q10 } 430 if sa == 1 { syv = 0 - GR_Q10 } 431 if sa == 2 { szv = 0 - GR_Q10 } 432 let backq: i64 = (0 - fsign)*GR_COMB_BACK_Q10 433 if aa == 0 { sxv = sxv + backq } 434 if aa == 1 { syv = syv + backq } 435 if aa == 2 { szv = szv + backq } 436 let sdn: i64 = (sxv*nx9 + syv*ny9 + szv*nz9)/GR_Q10 437 var dx: i64 = sxv - nx9*sdn/GR_Q10 + nx9*GR_COMB_LIFT_Q10/GR_Q10 438 var dy: i64 = syv - ny9*sdn/GR_Q10 + ny9*GR_COMB_LIFT_Q10/GR_Q10 439 var dz: i64 = szv - nz9*sdn/GR_Q10 + nz9*GR_COMB_LIFT_Q10/GR_Q10 440 let dln: i64 = gr_isqrt(dx*dx + dy*dy + dz*dz) 441 if dln > 0 { dx = dx*GR_Q10/dln; dy = dy*GR_Q10/dln; dz = dz*GR_Q10/dln } else { dz = 0 - GR_Q10 } 442 // LAYERING: spawn height above the nape line scales length down (crown shortest) 443 var rst9: i64 = roots[r*3 + sa] 444 if sa == 0 { rst9 = roots[r*3] } 445 if sa == 1 { rst9 = roots[r*3 + 1] } 446 if sa == 2 { rst9 = roots[r*3 + 2] } 447 let napebase9: i64 = cht - span*GR_NAPE_PERMIL/1000 448 var crown_q10: i64 = 0 449 let headspan9: i64 = stop - napebase9 450 if headspan9 > 0 { crown_q10 = (rst9 - napebase9)*GR_Q10/headspan9 } 451 if crown_q10 < 0 { crown_q10 = 0 } 452 if crown_q10 > GR_Q10 { crown_q10 = GR_Q10 } 453 let slen9: i64 = total_len*(GR_Q10 - crown_q10*GR_LAYER_Q10/GR_Q10)/GR_Q10 454 // scalp-hugging spacing: seg_i = slen*(i+2)/35 (sum over 7 segments = slen) 455 let segbase9: i64 = slen9/35 456 let r0x: i64 = px - cx 457 let r0y: i64 = py - cy 458 let r0z: i64 = pz - cz 459 var r0len: i64 = gr_isqrt(r0x*r0x + r0y*r0y + r0z*r0z) 460 // face-wardness (Q10, 0 when back-facing) and the root's lateral side sign 461 var rant0: i64 = r0z 462 if aa == 0 { rant0 = r0x } 463 if aa == 1 { rant0 = r0y } 464 var rlat0: i64 = r0y 465 if la == 0 { rlat0 = r0x } 466 if la == 2 { rlat0 = r0z } 467 var facew: i64 = 0 468 if r0len > 0 { facew = rant0*fsign*GR_Q10/r0len } 469 if facew < 0 { facew = 0 } 470 var lsgn: i64 = 1 471 if rlat0 < 0 { lsgn = 0 - 1 } 472 if r0len < 1 { r0len = 1 } 473 // wave phase seeded per strand so locks differ without frizz 474 let wph9: i64 = gr_hash(seed, r) % GR_Q10 475 var p: i64 = 0 476 while p < GR_PTS { 477 let base: i64 = 1 + r*GR_WORDS + p*3 478 hw[base] = px 479 hw[base + 1] = py 480 hw[base + 2] = pz 481 // blend toward gravity (down the stature axis) as the strand extends 482 var g: i64 = p*GR_GRAVITY_RAMP_Q10 483 if g > GR_Q10 { g = GR_Q10 } 484 var gx: i64 = 0 485 var gy: i64 = 0 486 var gz: i64 = 0 487 if sa == 0 { gx = 0 - GR_Q10 } 488 if sa == 1 { gy = 0 - GR_Q10 } 489 if sa == 2 { gz = 0 - GR_Q10 } 490 var sx: i64 = (dx*(GR_Q10 - g) + gx*g)/GR_Q10 491 var sy: i64 = (dy*(GR_Q10 - g) + gy*g)/GR_Q10 492 var sz: i64 = (dz*(GR_Q10 - g) + gz*g)/GR_Q10 493 // COSMETOLOGY RULE (operator brief): hair never falls TOWARD the face -- clamp the 494 // growth direction's anterior component at zero once it points face-ward, so front 495 // roots sweep back and sideways instead of curtaining the eyes. 496 // SIGN CORRECTED (operator: "back entirely empty, like reversed"): a face-ward 497 // component has the SAME sign as fsign (front lies at fsign-signed anterior), so the 498 // clamp fires on product > 0. The inverted form clamped BACK-ward flow -- every 499 // strand could only drape forward: face curtained, occiput bare. 500 if aa == 0 { if sx*fsign > 0 { sx = 0 } } 501 if aa == 1 { if sy*fsign > 0 { sy = 0 } } 502 if aa == 2 { if sz*fsign > 0 { sz = 0 } } 503 // side-sweep: face-adjacent strands flow toward their own side 504 let sw9: i64 = facew*GR_SWEEP_Q10/GR_Q10*lsgn 505 if la == 0 { sx = sx + sw9 } 506 if la == 1 { sy = sy + sw9 } 507 if la == 2 { sz = sz + sw9 } 508 let sl: i64 = gr_isqrt(sx*sx + sy*sy + sz*sz) 509 if sl > 0 { sx = sx*GR_Q10/sl; sy = sy*GR_Q10/sl; sz = sz*GR_Q10/sl } 510 // WAVES: clean sine on the LATERAL axis (integer sine via triangle approx of the 511 // phase -- smooth at 8 points), amplitude a small fraction of segment length 512 let seg: i64 = segbase9*(p + 2) 513 let ph9: i64 = (wph9 + p*GR_WAVE_STEP_Q10) % (2*GR_Q10) 514 var tri9: i64 = ph9 515 if tri9 > GR_Q10 { tri9 = 2*GR_Q10 - tri9 } 516 let wav9: i64 = (tri9*2 - GR_Q10)*GR_WAVE_AMP_PERMIL/1000 517 var wx9: i64 = 0 518 var wy9: i64 = 0 519 var wz9: i64 = 0 520 if la == 0 { wx9 = wav9 } 521 if la == 1 { wy9 = wav9 } 522 if la == 2 { wz9 = wav9 } 523 px = px + (sx + wx9)*seg/GR_Q10 524 py = py + (sy + wy9)*seg/GR_Q10 525 pz = pz + (sz + wz9)*seg/GR_Q10 526 // SCALP CONFORMANCE (data-space plot 2026-08-26 showed the groom as a detached 527 // curtain floating behind the head): while above head-centre height, clamp the 528 // point to the skull shell -- distance from head centre <= the ROOT's own radius 529 // (+15 percent breathing room, a named tolerance). Strands wrap the skull and only 530 // hang free below the centre line, like combed hair on a real head. 531 if pz > cht { 532 let qx: i64 = px - cx 533 let qy: i64 = py - cy 534 let qz: i64 = pz - cz 535 let qd: i64 = gr_isqrt(qx*qx + qy*qy + qz*qz) 536 let rmax: i64 = r0len*104/100 // 104%: the 115% slack read as hair FLOATING off the scalp (operator 2026-08-27); one mesh-cell of clearance is all a lying groom needs 537 if qd > rmax { if qd > 0 { 538 px = cx + qx*rmax/qd 539 py = cy + qy*rmax/qd 540 pz = cz + qz*rmax/qd 541 } } 542 } 543 p = p + 1 544 } 545 r = r + 1 546 } 547 let dwords: i64 = 1 + nr*GR_WORDS 548 gr_out(" strands=" as *u8); gr_num(nr) 549 gr_out(" length=" as *u8); gr_num(total_len) 550 gr_out(" seg=" as *u8); gr_num(seg); gr_out("\n" as *u8) 551 552 // ---- rebuild the file with HSTR replaced (nx_nxa_dyna machinery) ---- 553 var nsec: i64 = ns 554 if hsi < 0 { nsec = ns + 1 } 555 let toclen: i64 = GR_HDR + nsec*GR_TOCE 556 var total: i64 = toclen 557 var s2: i64 = 0 558 while s2 < ns { 559 if s2 != hsi { 560 let e2s: i64 = GR_HDR + s2*GR_TOCE 561 total = total + gr_rd64(b, e2s + 16)*8 562 } 563 s2 = s2 + 1 564 } 565 total = total + dwords*8 566 let nb: *u8 = sys_mmap(total + 4096) 567 if (nb as i64) == 0 { gr_err("GROOM-RED alloc\n" as *u8); sys_exit(1); return 1 } 568 gr_wr64(nb, 0, nxa_magic()) 569 gr_wr64(nb, 8, NXA_VER) 570 gr_wr64(nb, 16, nsec) 571 var wo: i64 = toclen 572 var ti: i64 = 0 573 var s3: i64 = 0 574 while s3 < ns { 575 if s3 != hsi { 576 let e3s: i64 = GR_HDR + s3*GR_TOCE 577 let oldoff: i64 = gr_rd64(b, e3s + 8) 578 let wl: i64 = gr_rd64(b, e3s + 16) 579 let te: i64 = GR_HDR + ti*GR_TOCE 580 gr_wr64(nb, te, gr_rd64(b, e3s)) 581 gr_wr64(nb, te + 8, wo) 582 gr_wr64(nb, te + 16, wl) 583 var k2: i64 = 0 584 while k2 < wl*8 { nb[wo + k2] = b[oldoff + k2]; k2 = k2 + 1 } 585 let pw: *i64 = ((nb as i64) + wo) as *i64 586 gr_wr64(nb, te + 24, nxa_check2(1, pw, wl)) 587 wo = wo + wl*8 588 ti = ti + 1 589 } 590 s3 = s3 + 1 591 } 592 let dstart: i64 = wo 593 var w3: i64 = 0 594 while w3 < dwords { gr_wr64(nb, wo, hw[w3]); wo = wo + 8; w3 = w3 + 1 } 595 let dte: i64 = GR_HDR + ti*GR_TOCE 596 gr_wr64(nb, dte, nxa_tag4("HSTR" as *u8)) 597 gr_wr64(nb, dte + 8, dstart) 598 gr_wr64(nb, dte + 16, dwords) 599 let dpw: *i64 = ((nb as i64) + dstart) as *i64 600 gr_wr64(nb, dte + 24, nxa_check2(1, dpw, dwords)) 601 let tbp: *i64 = ((nb as i64) + GR_HDR) as *i64 602 gr_wr64(nb, 24, nxa_check2(1, tbp, nsec*4)) 603 604 let fd: i64 = sys_openat_wr(outp, GR_MODE) 605 if fd < 0 { gr_err("GROOM-RED cannot open output\n" as *u8); sys_exit(1); return 1 } 606 var w2: i64 = 0 607 while w2 < wo { 608 let kk: i64 = sys_write(fd, ((nb as i64) + w2) as *u8, wo - w2) 609 if kk <= 0 { sys_close(fd); gr_err("GROOM-RED short write\n" as *u8); sys_exit(1); return 1 } 610 w2 = w2 + kk 611 } 612 sys_close(fd) 613 gr_out("GROOM-GREEN strands=" as *u8); gr_num(nr) 614 gr_out(" length_permil=" as *u8); gr_num(lenp) 615 gr_out(" bytes=" as *u8); gr_num(wo) 616 gr_out("\n <- full-scalp procedural groom: generated, seeded, parameterized -- no asset.\n" as *u8) 617 sys_exit(0) 618 return 0 619}