code wiki / _hdl_build / nx_vnsprite.nx

nx_vnsprite.nx source

↩ module page · 150 lines · 6570 B

1// nx_vnsprite.nx -- certified composable part: the VN SPRITE COMPOSITOR (gamebench bit 13). 2// REFERENCE BAR RESEARCHED 2026-07-29 (never invent a ruler, applied to the build itself): the field 3// standard is Ren'Py's layeredimage -- layers organized into ATTRIBUTE GROUPS (expression / outfit / 4// pose) where selecting one attribute auto-excludes the rest of its group, so a 4-outfit x 6-emotion 5// character is 10 layers, not 24 images -- plus dissolve transitions as the core scene grammar 6// (renpy.org/doc/html/layeredimage.html). This part is those semantics, sovereign and integer: 7// z-ordered clipped alpha blits + GROUP-EXCLUSIVE attribute selection + integer dissolve, with the 8// layer STATE a pure i64 vector so a scene round-trips through nx_gamesave (parts doctrine). 9// HONEST DELTA declared: commercial 2026 SOTA adds Live2D-class mesh-deformed "living" sprites -- 10// that is the animation-skeletal / soft-tissue axis (separate bits), not flat compositing. 11// LIB, no main. KEY color, alpha, groups are PARAMETERS (rule 11); art quality is the critic's axis, 12// composition correctness is THIS axis. 13// license_tier: ORIGINAL expect_exit: 0 14import "nx_syscalls.nx" 15const VSP_65536: i64 = 65536 // packed-RGB blue-channel multiplier (ecosystem fb convention) 16const VSP_A1: i64 = 256 // alpha denominator (256 = fully opaque) 17const VSP_LROW: i64 = 7 // layer row: [spr-index, x, y, alpha, z, visible, group] 18 19func vsp_clear(fb: *i64, W: i64, H: i64, c: i64) -> i64 { 20 var i: i64 = 0 21 let n: i64 = W*H 22 while i < n { fb[i] = c; i = i + 1 } 23 return 0 24} 25// clipped alpha blit: skip pixels equal to the KEY color; blend s over d by alpha/256 per channel. 26// Out-of-bounds target pixels are clipped BY CONSTRUCTION -- the loop bounds are intersected with the 27// framebuffer rectangle before any write, so a sprite can hang off any edge safely. 28func vsp_blit(fb: *i64, W: i64, H: i64, spr: *i64, sw: i64, sh: i64, x: i64, y: i64, key: i64, alpha: i64) -> i64 { 29 var sy: i64 = 0 30 if y < 0 { sy = 0 - y } 31 var ey: i64 = sh 32 if y + sh > H { ey = H - y } 33 while sy < ey { 34 var sx: i64 = 0 35 if x < 0 { sx = 0 - x } 36 var ex: i64 = sw 37 if x + sw > W { ex = W - x } 38 while sx < ex { 39 let s: i64 = spr[sy*sw + sx] 40 if s != key { 41 let di: i64 = (y + sy)*W + (x + sx) 42 if alpha >= VSP_A1 { 43 fb[di] = s 44 } else { 45 let d: i64 = fb[di] 46 let sr: i64 = s % 256 47 let sg: i64 = (s/256) % 256 48 let sb: i64 = (s/VSP_65536) % 256 49 let dr: i64 = d % 256 50 let dg: i64 = (d/256) % 256 51 let db: i64 = (d/VSP_65536) % 256 52 let orr: i64 = (sr*alpha + dr*(VSP_A1 - alpha))/VSP_A1 53 let og: i64 = (sg*alpha + dg*(VSP_A1 - alpha))/VSP_A1 54 let ob: i64 = (sb*alpha + db*(VSP_A1 - alpha))/VSP_A1 55 fb[di] = orr + og*256 + ob*VSP_65536 56 } 57 } 58 sx = sx + 1 59 } 60 sy = sy + 1 61 } 62 return 0 63} 64// compose n layers in ascending z (stable: equal z keeps table order). layers = flat VSP_LROW rows; 65// sprites/sprw/sprh are parallel tables indexed by the layer's spr-index. Invisible layers skipped. 66func vsp_compose(fb: *i64, W: i64, H: i64, sprites: *i64, sprw: *i64, sprh: *i64, layers: *i64, n: i64, key: i64) -> i64 { 67 let done: *i64 = sys_mmap(n*8) as *i64 68 var i: i64 = 0 69 while i < n { done[i] = 0; i = i + 1 } 70 var emitted: i64 = 0 71 var rounds: i64 = 0 72 while rounds < n { 73 var best: i64 = 0 - 1 74 var bz: i64 = 0 75 i = 0 76 while i < n { 77 if done[i] == 0 { 78 let z: i64 = layers[i*VSP_LROW + 4] 79 var take: i64 = 0 80 if best < 0 { take = 1 } 81 if best >= 0 { if z < bz { take = 1 } } 82 if take == 1 { best = i; bz = z } 83 } 84 i = i + 1 85 } 86 if best >= 0 { 87 done[best] = 1 88 if layers[best*VSP_LROW + 5] == 1 { 89 let si: i64 = layers[best*VSP_LROW] 90 vsp_blit(fb, W, H, sprites[si] as *i64, sprw[si], sprh[si], 91 layers[best*VSP_LROW + 1], layers[best*VSP_LROW + 2], 92 key, layers[best*VSP_LROW + 3]) 93 emitted = emitted + 1 94 } 95 } 96 rounds = rounds + 1 97 } 98 return emitted 99} 100// ---- ATTRIBUTE GROUPS (the Ren'Py layeredimage signature): show EXACTLY ONE layer of a group. 101// group 0 = ungrouped (visibility managed by the caller); group > 0 = exclusive attribute group. 102// vsp_setg(layers, n, grp, want) makes row `want` the group's visible attribute and hides the rest -- 103// ONE call swaps an expression/outfit with no manual layer bookkeeping (the 96-combos-no-96-files law). 104func vsp_setg(layers: *i64, n: i64, grp: i64, want: i64) -> i64 { 105 var i: i64 = 0 106 var shown: i64 = 0 107 while i < n { 108 if layers[i*VSP_LROW + 6] == grp { 109 if i == want { layers[i*VSP_LROW + 5] = 1; shown = shown + 1 } 110 if i != want { layers[i*VSP_LROW + 5] = 0 } 111 } 112 i = i + 1 113 } 114 return shown 115} 116// ---- DISSOLVE (the core VN transition): out = A*(256-t)/256 + B*t/256 per channel, integer-exact. 117// t=0 -> exactly A, t=256 -> exactly B; a scene change is a t-ramp over frames. 118func vsp_dissolve(fa: *i64, fbb: *i64, out: *i64, n: i64, t: i64) -> i64 { 119 var i: i64 = 0 120 while i < n { 121 let a: i64 = fa[i] 122 let b: i64 = fbb[i] 123 let ar: i64 = a % 256 124 let ag: i64 = (a/256) % 256 125 let ab2: i64 = (a/VSP_65536) % 256 126 let br: i64 = b % 256 127 let bg: i64 = (b/256) % 256 128 let bb2: i64 = (b/VSP_65536) % 256 129 let orr: i64 = (ar*(VSP_A1 - t) + br*t)/VSP_A1 130 let og: i64 = (ag*(VSP_A1 - t) + bg*t)/VSP_A1 131 let ob: i64 = (ab2*(VSP_A1 - t) + bb2*t)/VSP_A1 132 out[i] = orr + og*256 + ob*VSP_65536 133 i = i + 1 134 } 135 return 0 136} 137// bounded positive rolling checksum over a frame (independent-walker shape, cf. nx_collide2d) 138const VSP_CKMOD: i64 = 1000000007 139func vsp_fbck(fb: *i64, W: i64, H: i64) -> i64 { 140 var ck: i64 = 0 141 var i: i64 = 0 142 let n: i64 = W*H 143 while i < n { 144 var v: i64 = fb[i] % VSP_CKMOD 145 if v < 0 { v = v + VSP_CKMOD } 146 ck = (ck*31 + v) % VSP_CKMOD 147 i = i + 1 148 } 149 return ck 150}