nx_arousal_body.nx source
↩ module page · 441 lines · 21113 B
1// nx_arousal_body.nx -- arousal progression ON THE BODY, not on a chart.
2//
3// Sphere-traces the sovereign SDF humanoid (nx_sdfrender's 18-ellipsoid form, feminine morph from
4// nx_body_figure) as an orthographic FRONT view, maps every surface hit to an anatomical site by its
5// world position, and shades it with the real Beer-Lambert skin optics at that site's perfusion.
6// Emits a FILMSTRIP: the same body at N times, so flush PROPAGATION is visible as it spreads.
7//
8// Bands and ticks are an abstraction of the physiology. This is the physiology on a body.
9//
10// ★Front is -z in this rig (breasts and face both sit at -z), so the camera looks along +z.
11// ★SITE MAP is by world (x,y) against the rig's own part centres: head/neck y>980, arms |x|>380,
12// legs y<-150, breast y>700, sub-mammary y>560, torso y>200, else pelvic/genital.
13//
14// usage: nx_arousal_body <out.png> <sex0m1f> <drive_permil> <melanin_permil> [propensity] [track]
15// license_tier: ORIGINAL expect_exit: 0
16import "nx_arousal_skin_lib.nx"
17import "nx_body_figure.nx"
18import "nx_jpeg_decoder.nx"
19import "nx_doc_layout.nx"
20import "nx_png_write.nx"
21
22const AB_FRAMES: i64 = 6
23const AB_FW: i64 = 190
24const AB_FH: i64 = 330
25const AB_HEAD: i64 = 30
26const AB_FOOT: i64 = 22
27const AB_SPAN_MS: i64 = 900000
28const AB_TAU_NIP: i64 = 6000
29const AB_MAXPT: i64 = 512
30const AB_W: i64 = 1140
31
32func ab_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
33func ab_px(img: *u8, x: i64, y: i64, r: i64, g: i64, b: i64) -> i64 {
34 if x < 0 { return 0 }
35 if x >= AB_W { return 0 }
36 let o: i64 = (y * AB_W + x) * 3
37 img[o] = ((r * 255) / 1000) as u8
38 img[o+1] = ((g * 255) / 1000) as u8
39 img[o+2] = ((b * 255) / 1000) as u8
40 return 0
41}
42func ab_char(img: *u8, tbl: *u8, x: i64, y: i64, ch: i64, cr: i64, cg: i64, cb: i64) -> i64 {
43 let gl: *u8 = nx_doc_glyph(tbl, ch)
44 var r: i64 = 0
45 while r < 8 {
46 let bits: i64 = gl[r] as i64
47 var c: i64 = 0
48 while c < 8 {
49 if ((bits >> c) & 1) == 1 { ab_px(img, x + c, y + r, cr, cg, cb) }
50 c = c + 1
51 }
52 r = r + 1
53 }
54 return 0
55}
56func ab_text(img: *u8, tbl: *u8, x: i64, y: i64, s: *u8, cr: i64, cg: i64, cb: i64) -> i64 {
57 var i: i64 = 0
58 while s[i] != (0 as u8) { ab_char(img, tbl, x + i * 8, y, s[i] as i64, cr, cg, cb); i = i + 1 }
59 return 0
60}
61func ab_num(img: *u8, tbl: *u8, x: i64, y: i64, v: i64, cr: i64, cg: i64, cb: i64) -> i64 {
62 var m: i64 = v
63 var nd: i64 = 1
64 while m >= 10 { m = m / 10; nd = nd + 1 }
65 var i: i64 = 0
66 while i < nd {
67 var dv: i64 = v
68 var k: i64 = 0
69 while k < nd - 1 - i { dv = dv / 10; k = k + 1 }
70 ab_char(img, tbl, x + i * 8, y, 48 + (dv % 10), cr, cg, cb)
71 i = i + 1
72 }
73 return 0
74}
75// ---- SKIN SHADING: the three axes nx_mannequin_index actually scores ---------------------------
76// A flat reflectance term with a fake depth gradient scores OBVIOUS_PLASTIC on that ruler, because
77// it has ZERO of: subsurface scatter (weight 3), specular falloff (2), micro-texture (2).
78//
79// integer sqrt + normalise helpers
80func ab_isqrt(v: i64) -> i64 { if v <= 0 { return 0 } var x: i64 = v; var y: i64 = (x + 1) / 2; while y < x { x = y; y = (x + v / x) / 2 } return x }
81// deterministic value noise in [-1000,1000] from an integer lattice -- MICRO-TEXTURE axis.
82// Skin without pore-scale variation reads as moulded plastic no matter how good the lighting is.
83func ab_hash(x: i64, y: i64, z: i64) -> i64 {
84 var h: i64 = x * 374761393 + y * 668265263 + z * 2147483647
85 h = (h ^ (h >> 13)) * 1274126177
86 h = h ^ (h >> 16)
87 return ((h & 2047) - 1024) * 1000 / 1024
88}
89// SUBSURFACE: wrap-diffuse spreads light PAST the terminator (light enters, scatters, exits nearby),
90// and the deep component is RED-shifted because long path lengths in dermis attenuate G/B far more
91// than R -- the same Beer-Lambert asymmetry that makes flush read red, now acting on transport depth.
92func ab_wrap(ndl: i64, wrapw: i64) -> i64 {
93 let v: i64 = ((ndl + wrapw) * 1000) / (1000 + wrapw)
94 if v < 0 { return 0 }
95 if v > 1000 { return 1000 }
96 return v
97}
98// SPECULAR: Blinn-Phong lobe with a Fresnel-ish grazing boost. Skin has a thin oily layer; with no
99// specular at all a surface cannot read as wet, and every arousal cue involving sheen is invisible.
100func ab_spec(ndh: i64, ndv: i64, shin: i64) -> i64 {
101 if ndh <= 0 { return 0 }
102 var p: i64 = ndh
103 var i: i64 = 1
104 while i < shin { p = (p * ndh) / 1000; i = i + 1 }
105 let fres: i64 = 1000 - ndv
106 let f: i64 = 200 + (fres * fres) / 1250
107 return (p * f) / 1000
108}
109// AMBIENT OCCLUSION by SDF cone-stepping along the normal. Flat constant ambient is a major reason
110// a render reads as unlit plastic: where two forms meet (armpit, under-breast, between thighs, neck)
111// real light is BLOCKED, and without that the geometry never feels solid or in contact with itself.
112// Standard SDF AO: march out along N; wherever the field is CLOSER than the step distance, something
113// is nearby, and the deficit is occlusion.
114func ab_ao(base: i64, px: i64, py: i64, pz: i64, nx1: i64, ny1: i64, nz1: i64) -> i64 {
115 var occ: i64 = 0
116 var w: i64 = 1000
117 var k: i64 = 1
118 while k <= 5 {
119 let d: i64 = k * 55
120 let sx: i64 = px + (nx1 * d) / 1000
121 let sy: i64 = py + (ny1 * d) / 1000
122 let sz: i64 = pz + (nz1 * d) / 1000
123 var f: i64 = sdf_eval(base, sx, sy, sz)
124 if f < 0 { f = 0 }
125 var diff: i64 = d - f
126 if diff < 0 { diff = 0 }
127 occ = occ + (diff * w) / d
128 w = (w * 62) / 100
129 k = k + 1
130 }
131 var ao: i64 = 1000 - (occ * 1000) / 2600
132 if ao < 130 { ao = 130 }
133 if ao > 1000 { ao = 1000 }
134 return ao
135}
136// ---- REAL SKIN ALBEDO ---------------------------------------------------------------------------
137// ★A DIFFUSE MAP IS THE RESTING STATE, NOT A BASE COLOUR TO OVERWRITE. It already encodes resting
138// melanin AND resting haemoglobin, plus every pore, freckle and tonal variation the procedural
139// hash-noise was imitating. So arousal must ADD absorption on top of it, not replace it:
140// aroused = texture * exp(-(hb_now - hb_rest) * ext_channel)
141// Only the DELTA is physiological. Measured mean R:G of this asset is 1336 permil against the
142// model's independently-derived resting prediction of 1270 -- within 5%, from opposite directions.
143//
144// ⚠PLACEHOLDER PROJECTION: the SDF rig carries no UVs, so this maps world (x,y) planar onto the
145// atlas. Real UVs arrive with the FBX; this is deliberately the crude step that proves the shading
146// path, not the final mapping. Labelled in the render so it cannot be mistaken for finished.
147func ab_tex(tex: *u8, tw: i64, th: i64, wx: i64, wy: i64, ch: i64) -> i64 {
148 if (tex as i64) == 0 { return 0 - 1 }
149 var u: i64 = ((wx + 900) * tw) / 1800
150 var v: i64 = ((1650 - wy) * th) / 3150
151 if u < 0 { u = 0 }
152 if v < 0 { v = 0 }
153 if u > tw - 1 { u = tw - 1 }
154 if v > th - 1 { v = th - 1 }
155 let o: i64 = (v * tw + u) * 3
156 return tex[o + ch] as i64
157}
158// anatomical site from world position, against the rig's own part centres
159func ab_site(wx: i64, wy: i64) -> i64 {
160 let ax: i64 = ab_abs(wx)
161 if wy > 980 { return 3 }
162 if ax > 380 { return 4 }
163 if wy < 0 - 150 { return 4 }
164 if wy > 700 { return 1 }
165 if wy > 560 { return 0 }
166 if wy > 200 { return 2 }
167 return 5
168}
169
170func main(argc: i64, argv: *i64) -> i64 {
171 if argc < 5 { a_puts("REFUSED reason=body_needs_4_args\n" as *u8); return 2 }
172 let path: *u8 = argv[1] as *u8
173 let sex: i64 = a_atoi(argv[2] as *u8)
174 let drive0: i64 = a_atoi(argv[3] as *u8)
175 let mel: i64 = a_atoi(argv[4] as *u8)
176 var prop: i64 = 1000
177 if argc >= 6 { prop = a_atoi(argv[5] as *u8) }
178 if drive0 <= 0 { a_puts("REFUSED reason=nonpositive_drive\n" as *u8); return 3 }
179 if drive0 > 1000 { a_puts("REFUSED reason=drive_over_permil\n" as *u8); return 3 }
180 if mel < 0 { a_puts("REFUSED reason=melanin_out_of_range\n" as *u8); return 3 }
181 if mel > 1000 { a_puts("REFUSED reason=melanin_out_of_range\n" as *u8); return 3 }
182
183 let cl: *i64 = sys_mmap(16) as *i64
184 cl[0] = 0
185 let cbuf: *u8 = sys_read_file("arousal_params.conf" as *u8, cl)
186 let cn: i64 = cl[0]
187 let ext: *i64 = sys_mmap(SK_EXTN * 8) as *i64
188 ext[0] = a_conf(cbuf, cn, "mel_ext_r" as *u8, 300)
189 ext[1] = a_conf(cbuf, cn, "mel_ext_g" as *u8, 500)
190 ext[2] = a_conf(cbuf, cn, "mel_ext_b" as *u8, 700)
191 ext[3] = a_conf(cbuf, cn, "hb_ext_r" as *u8, 100)
192 ext[4] = a_conf(cbuf, cn, "hb_ext_g" as *u8, 700)
193 ext[5] = a_conf(cbuf, cn, "hb_ext_b" as *u8, 900)
194 ext[6] = a_conf(cbuf, cn, "hb_base" as *u8, 150)
195 ext[7] = a_conf(cbuf, cn, "hb_span" as *u8, 600)
196 ext[8] = a_conf(cbuf, cn, "flush_span" as *u8, 300)
197 var tau: i64 = a_conf(cbuf, cn, "tau_slow_m_ms" as *u8, 185000)
198 if sex == 1 { tau = a_conf(cbuf, cn, "tau_slow_f_ms" as *u8, 210000) }
199
200 // optional drive track
201 let ta: *i64 = sys_mmap(AB_MAXPT * 8) as *i64
202 let da: *i64 = sys_mmap(AB_MAXPT * 8) as *i64
203 var nt: i64 = 0
204 if argc >= 7 {
205 let tl: *i64 = sys_mmap(16) as *i64
206 tl[0] = 0
207 let tb: *u8 = sys_read_file(argv[6] as *u8, tl)
208 let tn: i64 = tl[0]
209 if (tb as i64) == 0 { a_puts("REFUSED reason=track_unreadable\n" as *u8); return 4 }
210 let pp: *i64 = sys_mmap(16) as *i64
211 pp[0] = 0
212 var dn: i64 = 0
213 while dn == 0 {
214 a_skipnd(tb, tn, pp)
215 if pp[0] >= tn { dn = 1 } else {
216 let tv: i64 = a_pint(tb, tn, pp)
217 a_skipnd(tb, tn, pp)
218 let dv: i64 = a_pint(tb, tn, pp)
219 if tv < 0 { dn = 1 } else { if dv < 0 { dn = 1 } else {
220 if nt < AB_MAXPT { ta[nt] = tv; da[nt] = dv; nt = nt + 1 } else { dn = 1 }
221 } }
222 }
223 }
224 }
225
226 // integrate perfusion once, sampling at each frame time
227 let fp: *i64 = sys_mmap(AB_FRAMES * 8) as *i64
228 let fd: *i64 = sys_mmap(AB_FRAMES * 8) as *i64
229 var cur: i64 = 0
230 var t: i64 = 0
231 var ti: i64 = 0
232 var fi: i64 = 0
233 while fi < AB_FRAMES {
234 let ftarget: i64 = (fi * AB_SPAN_MS) / (AB_FRAMES - 1)
235 var dr: i64 = drive0
236 var stepping: i64 = 1
237 while stepping == 1 {
238 if t >= ftarget { stepping = 0 } else {
239 dr = drive0
240 if nt > 0 {
241 var adv: i64 = 1
242 while adv == 1 {
243 if ti + 1 < nt { if ta[ti+1] <= t { ti = ti + 1 } else { adv = 0 } } else { adv = 0 }
244 }
245 dr = da[ti]
246 }
247 cur = a_lag(cur, dr * 1000, tau)
248 t = t + 50
249 }
250 }
251 if nt > 0 { dr = da[ti] }
252 fp[fi] = cur / 1000
253 fd[fi] = dr
254 fi = fi + 1
255 }
256
257 // Build the SDF person: 17 body parts + the 27-part SCULPTED FACE (skull/brow/nose/cheeks/lips/
258 // eyes/ears/jaw/chin/temples), not the bare 18-part body whose head is a featureless blob.
259 // sdf_person2 replaces body part 0 with the skull and appends the features after part 17.
260 // figure_apply still targets parts 2/3/4 (chest/waist/hips), which person2 leaves in place.
261 // 17 body parts + the 27-part SCULPTED FACE, registered for a full-figure shot.
262 // ⚠Do NOT use sdf_person2 here: its face transform is tuned for a head CLOSE-UP and lands the
263 // skull ~117 units too high, which reads as a mask. sdf_person_full carries the correct fit.
264 // ---- real skin albedo (Std_Skin_Body_Diffuse). Absent file => procedural fallback. ----------
265 var tex: *u8 = 0 as *u8
266 var tw: i64 = 0
267 var th: i64 = 0
268 let tl: *i64 = sys_mmap(16) as *i64
269 tl[0] = 0
270 let tj: *u8 = sys_read_file("_scratch/tex/Std_Skin_Body_Diffuse.jpeg" as *u8, tl)
271 if (tj as i64) != 0 {
272 if tl[0] > 0 {
273 let tr: *NxJpgResult = nx_jpeg_decode(tj, tl[0])
274 if tr.error_code == 0 {
275 if tr.width > 0 { tex = tr.rgb; tw = tr.width; th = tr.height }
276 }
277 }
278 }
279 let base: i64 = sys_mmap(sdf_bytes()) as i64
280 let scratch: i64 = sys_mmap(sdf_bytes()) as i64
281 sdf_person_full(base, scratch, 0)
282 if sex == 1 { figure_apply(base, FIG_FEM) } else { figure_apply(base, FIG_MASC) }
283
284 let h: i64 = AB_HEAD + AB_FH + AB_FOOT
285 let img: *u8 = sys_mmap(AB_W * h * 3)
286 var byy: i64 = 0
287 while byy < h {
288 var bxx: i64 = 0
289 while bxx < AB_W { ab_px(img, bxx, byy, 45, 49, 58); bxx = bxx + 1 }
290 byy = byy + 1
291 }
292
293 let out: *i64 = sys_mmap(SK_OUTN * 8) as *i64
294 fi = 0
295 while fi < AB_FRAMES {
296 let ox: i64 = fi * AB_FW
297 var v: i64 = 0
298 while v < AB_FH {
299 // world y: +1650 (above head) down to -1500 (below feet)
300 let wy: i64 = 1650 - (v * 3150) / AB_FH
301 var u: i64 = 0
302 while u < AB_FW {
303 let wx: i64 = ((u - AB_FW / 2) * 2000) / AB_FW
304 // SPHERE-TRACE from the camera side (-z) along +z
305 var wz: i64 = 0 - 1500
306 var hit: i64 = 0
307 var it: i64 = 0
308 while it < 48 {
309 let d: i64 = sdf_eval(base, wx, wy, wz)
310 if d < 6 { hit = 1; it = 48 } else {
311 wz = wz + d
312 if wz > 1500 { it = 48 } else { it = it + 1 }
313 }
314 }
315 if hit == 1 {
316 let st: i64 = ab_site(wx, wy)
317 sk_eval_at(wx, wy, st, fp[fi], mel, ext, out)
318 // REAL surface normal from the SDF gradient (central differences)
319 var gx: i64 = sdf_eval(base, wx + 20, wy, wz) - sdf_eval(base, wx - 20, wy, wz)
320 var gy: i64 = sdf_eval(base, wx, wy + 20, wz) - sdf_eval(base, wx, wy - 20, wz)
321 var gz: i64 = sdf_eval(base, wx, wy, wz + 20) - sdf_eval(base, wx, wy, wz - 20)
322 // MICRO-TEXTURE: perturb the normal at pore scale before normalising
323 gx = gx * 100 + ab_hash(wx / 12, wy / 12, wz / 12) / 22
324 gy = gy * 100 + ab_hash(wx / 12 + 77, wy / 12, wz / 12) / 22
325 gz = gz * 100 + ab_hash(wx / 12, wy / 12 + 31, wz / 12) / 22
326 var gl: i64 = ab_isqrt(gx*gx + gy*gy + gz*gz)
327 if gl < 1 { gl = 1 }
328 let nxv: i64 = (gx * 1000) / gl
329 let nyv: i64 = (gy * 1000) / gl
330 let nzv: i64 = (gz * 1000) / gl
331 // key light from upper-left-front, view along -z (camera side)
332 let ndl: i64 = (nxv * (0-500) + nyv * 600 + nzv * (0-624)) / 1000
333 let ndv: i64 = (0 - nzv)
334 // half-vector between L and V, both unit-ish
335 var hx: i64 = 0 - 500
336 var hy: i64 = 600
337 var hz: i64 = (0 - 624) - 1000
338 var hl: i64 = ab_isqrt(hx*hx + hy*hy + hz*hz)
339 if hl < 1 { hl = 1 }
340 let ndh: i64 = (nxv*(hx*1000/hl) + nyv*(hy*1000/hl) + nzv*(hz*1000/hl)) / 1000000
341 // SUBSURFACE: wrapped diffuse, plus a red-shifted deep term near the terminator
342 let wrapd: i64 = ab_wrap(ndl, 550)
343 var deep: i64 = 550 - ab_abs(ndl - 100)
344 if deep < 0 { deep = 0 }
345 let amb: i64 = 260
346 var lr: i64 = amb + (wrapd * 740) / 1000 + (deep * 300) / 1000
347 var lg: i64 = amb + (wrapd * 740) / 1000 + (deep * 90) / 1000
348 var lb: i64 = amb + (wrapd * 740) / 1000 + (deep * 40) / 1000
349 // AMBIENT OCCLUSION: darkens where forms meet, which is what makes a surface read
350 // as solid rather than as a flat cutout. Applied to ambient + wrap, NOT specular.
351 let ao: i64 = ab_ao(base, wx, wy, wz, nxv, nyv, nzv)
352 lr = (lr * ao) / 1000
353 lg = (lg * ao) / 1000
354 lb = (lb * ao) / 1000
355 // ALBEDO VARIATION at three scales. One uniform colour per site is a headline
356 // "plastic" cue: real skin is mottled by vascular density (coarse), melanin
357 // clustering (mid) and pores (fine). Coarse octave is warm-biased because the
358 // mottling that dominates human skin is VASCULAR, i.e. haemoglobin-red.
359 // ⚠⚠KNOWN REGRESSION, REMOVED. nx_graphics_board records this exact defect:
360 // "coarse tone-variation + micro-bump read as DISEASED BLOTCHY MOTTLING at body
361 // scale (fine in a closeup, awful full-body) -- removed the coarse blotch, kept
362 // only a whisper of fine grain + even SSS. REAL SKIN IS EVEN, NOT MOTTLED."
363 // I rebuilt the known-bad because I never opened the board. Coarse and mid
364 // octaves are gone; only the whisper of fine grain remains.
365 let m_coarse: i64 = 0
366 let av: i64 = 1000 + ab_hash(wx / 9 + 5, wy / 9, wz / 9) / 60
367 // coarse vascular mottle: warm-biased, so it shifts R up while pulling G/B down
368 let vasc: i64 = m_coarse / 30
369 // ★REAL ALBEDO PATH: the diffuse map is the RESTING skin -- it already carries
370 // resting melanin, resting haemoglobin, pores, freckles and tonal variation.
371 // So apply only the AROUSAL DELTA of haemoglobin absorption on top of it.
372 // Overwriting the texture with computed colour would throw away every bit of
373 // real detail and put us straight back to painted plastic.
374 var b2: i64 = out[2]
375 var b3: i64 = out[3]
376 var b4: i64 = out[4]
377 if (tex as i64) != 0 {
378 let tr8: i64 = ab_tex(tex, tw, th, wx, wy, 0)
379 let tg8: i64 = ab_tex(tex, tw, th, wx, wy, 1)
380 let tb8: i64 = ab_tex(tex, tw, th, wx, wy, 2)
381 if tr8 >= 0 {
382 var dhb: i64 = out[1] - ext[6]
383 if dhb < 0 { dhb = 0 }
384 b2 = ((tr8 * 1000) / 255 * a_exp_neg((dhb * ext[3]) / 1000)) / 1000
385 b3 = ((tg8 * 1000) / 255 * a_exp_neg((dhb * ext[4]) / 1000)) / 1000
386 b4 = ((tb8 * 1000) / 255 * a_exp_neg((dhb * ext[5]) / 1000)) / 1000
387 }
388 }
389 var cr: i64 = (((b2 * lr) / 1000) * (av + vasc)) / 1000
390 var cg: i64 = (((b3 * lg) / 1000) * (av - vasc / 2)) / 1000
391 var cb: i64 = (((b4 * lb) / 1000) * (av - vasc / 2)) / 1000
392 // SPECULAR: narrow oily lobe on top, white, Fresnel-weighted
393 let sp: i64 = (ab_spec(ndh, ndv, 12) * 620) / 1000
394 cr = cr + sp
395 cg = cg + sp
396 cb = cb + sp
397 if cr > 1000 { cr = 1000 }
398 if cg > 1000 { cg = 1000 }
399 if cb > 1000 { cb = 1000 }
400 ab_px(img, ox + u, AB_HEAD + v, cr, cg, cb)
401 }
402 u = u + 1
403 }
404 v = v + 1
405 }
406 fi = fi + 1
407 }
408
409 let tbl: *u8 = font8x8_table()
410 ab_text(img, tbl, 8, 11, "AROUSAL PROGRESSION ON THE BODY" as *u8, 1000, 1000, 1000)
411 if sex == 0 { ab_text(img, tbl, 268, 11, "male" as *u8, 700, 760, 850) } else { ab_text(img, tbl, 268, 11, "female" as *u8, 700, 760, 850) }
412 if nt > 0 { ab_text(img, tbl, 340, 11, "with inhibition event" as *u8, 950, 700, 420) }
413 ab_text(img, tbl, 940, 11, "flush spreads" as *u8, 620, 670, 750)
414 fi = 0
415 while fi < AB_FRAMES {
416 let ox2: i64 = fi * AB_FW
417 let tsec: i64 = (fi * AB_SPAN_MS) / (AB_FRAMES - 1) / 1000
418 ab_text(img, tbl, ox2 + 8, AB_HEAD + AB_FH + 7, "t=" as *u8, 750, 790, 850)
419 ab_num(img, tbl, ox2 + 24, AB_HEAD + AB_FH + 7, tsec, 750, 790, 850)
420 ab_text(img, tbl, ox2 + 24 + 32, AB_HEAD + AB_FH + 7, "s" as *u8, 750, 790, 850)
421 ab_text(img, tbl, ox2 + 82, AB_HEAD + AB_FH + 7, "P=" as *u8, 620, 670, 750)
422 ab_num(img, tbl, ox2 + 98, AB_HEAD + AB_FH + 7, fp[fi], 620, 670, 750)
423 fi = fi + 1
424 }
425
426 let rc: i64 = nx_png_write_rgb(path, img, AB_W, h)
427 if rc < 0 { a_puts("REFUSED reason=png_write_failed\n" as *u8); return 4 }
428 a_puts("ok wrote=" as *u8); a_puts(path)
429 a_puts(" w=" as *u8); a_putn(AB_W)
430 a_puts(" h=" as *u8); a_putn(h)
431 a_puts(" frames=" as *u8); a_putn(AB_FRAMES)
432 a_puts(" track_pts=" as *u8); a_putn(nt)
433 fi = 0
434 while fi < AB_FRAMES {
435 a_puts(" p" as *u8); a_putn(fi)
436 a_puts("=" as *u8); a_putn(fp[fi])
437 fi = fi + 1
438 }
439 a_puts("\n" as *u8)
440 return 0
441}