code wiki / (root) / nx_arousal_body.nx

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}