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}