code wiki / _hdl_build / nx_gsplatbind.nx

nx_gsplatbind.nx source

↩ module page · 328 lines · 15444 B

1// nx_gsplatbind.nx -- ★THE HYBRID: OUTSIDE SPLAT, INSIDE PARAMETRIC MESH (operator 2026-07-27, 2// "do the outside splat the inside the same as gnm google"). 3// 4// This is the mid-2026 industry architecture and it resolves the 3DGS-vs-poly-mesh question without 5// choosing: a low-resolution parametric mesh acts as the DRIVING CAGE, and millions of Gaussians are 6// anchored to its triangles. When the rig deforms the cage, the splat cloud is carried with it. You get 7// the animation, physics, collision and EDITABILITY of a mesh with the photorealism of splats -- and, 8// decisively for this program, the mesh keeps the INTERIOR that a splat cloud cannot have at all. 9// 10// ★WHY THE MESH MUST STAY THE SOURCE OF TRUTH HERE: every load-bearing claim of the twin program -- 11// layered bone/muscle/fascia, surgical prediction, joint limits, self-penetration, centre-of-mass -- 12// needs structured solid geometry. Splats are an APPEARANCE representation with no inside. So the 13// binding is deliberately one-directional: the mesh drives, the splats follow, never the reverse. 14// 15// ★THE BINDING (GaussianAvatars-class): each Gaussian is stored in the LOCAL FRAME of its triangle -- 16// barycentric position within the face, a signed offset along the face normal, and a scale expressed as 17// a RATIO of the face's own size. Rebuilding from a deformed triangle then reproduces the splat in the 18// deformed configuration for free, at any pose, with no re-fit. 19// 20// ⚠DECLARED LIMIT: nx_gsplat v0 Gaussians are ISOTROPIC {x,y,z,scale,r,g,b,opacity} -- there is no 21// orientation term, so this binding carries POSITION and SIZE but has no rotation to carry. When 22// anisotropic covariance lands (nx_gsplat's own named next rung), the binding MUST also rotate each 23// Gaussian into the triangle's frame, and a splat that translates without rotating is the classic bug 24// -- highlights that slide across a turning surface. T4 is written now so that rung has a tooth waiting. 25// 26// nx_gsplatbind selftest 27// license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26). 28import "nx_gate_verdict.nx" 29 30const GB_FX: i64 = 1024 // fixed point for barycentric weights and scale ratios 31const GB_GS: i64 = 8 // nx_gsplat gaussian stride {x,y,z,scale,r,g,b,opacity} 32const GB_TS: i64 = 9 // triangle stride: 3 verts * xyz 33const GB_BS: i64 = 5 // bind record: tri, u, v, h, scale_ratio 34 35func gb_isqrt(v: i64) -> i64 { 36 if v <= 0 { return 0 } 37 var x: i64 = v 38 var y: i64 = (x+1)/2 39 while y < x { x = y; y = (x + v/x)/2 } 40 return x 41} 42func gb_iabs(v: i64) -> i64 { if v < 0 { return 0-v } return v } 43 44// face normal (unnormalised) and its length; also the face "size" = the normal's magnitude, which is 45// twice the triangle area -- a scale-invariant handle on how big the face currently is. 46func gb_face(tri: *i64, t: i64, n3: *i64) -> i64 { 47 let o: i64 = t*GB_TS 48 let ux: i64 = tri[o+3] - tri[o] 49 let uy: i64 = tri[o+4] - tri[o+1] 50 let uz: i64 = tri[o+5] - tri[o+2] 51 let vx: i64 = tri[o+6] - tri[o] 52 let vy: i64 = tri[o+7] - tri[o+1] 53 let vz: i64 = tri[o+8] - tri[o+2] 54 n3[0] = uy*vz - uz*vy 55 n3[1] = uz*vx - ux*vz 56 n3[2] = ux*vy - uy*vx 57 return gb_isqrt(n3[0]*n3[0] + n3[1]*n3[1] + n3[2]*n3[2]) 58} 59 60// ★BIND. For each gaussian find the triangle whose centroid is nearest, then express it in that face's 61// local frame. Nearest-centroid is declared: it is exact for the fixtures here and for a dense cage, 62// and a closest-point-on-triangle search is the refinement when cages get coarse. 63func gb_bind(g: *i64, ng: i64, tri: *i64, nt: i64, bind: *i64) -> i64 { 64 let n3: *i64 = sys_mmap(64) as *i64 65 var i: i64 = 0 66 while i < ng { 67 let px: i64 = g[i*GB_GS] 68 let py: i64 = g[i*GB_GS+1] 69 let pz: i64 = g[i*GB_GS+2] 70 var best: i64 = 0 71 var bestd: i64 = 0-1 72 var t: i64 = 0 73 while t < nt { 74 let o: i64 = t*GB_TS 75 let cx: i64 = (tri[o] + tri[o+3] + tri[o+6])/3 76 let cy: i64 = (tri[o+1] + tri[o+4] + tri[o+7])/3 77 let cz: i64 = (tri[o+2] + tri[o+5] + tri[o+8])/3 78 let dx: i64 = px-cx 79 let dy: i64 = py-cy 80 let dz: i64 = pz-cz 81 let d: i64 = dx*dx + dy*dy + dz*dz 82 if bestd < 0 { bestd = d; best = t } else { if d < bestd { bestd = d; best = t } } 83 t = t + 1 84 } 85 let len: i64 = gb_face(tri, best, n3) 86 let o2: i64 = best*GB_TS 87 // barycentric by the area method, in the face plane 88 let ax: i64 = tri[o2]; let ay: i64 = tri[o2+1]; let az: i64 = tri[o2+2] 89 let bx: i64 = tri[o2+3]; let by: i64 = tri[o2+4]; let bz: i64 = tri[o2+5] 90 let cx2: i64 = tri[o2+6]; let cy2: i64 = tri[o2+7]; let cz2: i64 = tri[o2+8] 91 // n . ((B-P) x (C-P)) gives 2*area of sub-triangle PBC -> weight of A 92 let e1x: i64 = bx-px; let e1y: i64 = by-py; let e1z: i64 = bz-pz 93 let e2x: i64 = cx2-px; let e2y: i64 = cy2-py; let e2z: i64 = cz2-pz 94 let f1x: i64 = e1y*e2z - e1z*e2y 95 let f1y: i64 = e1z*e2x - e1x*e2z 96 let f1z: i64 = e1x*e2y - e1y*e2x 97 let e3x: i64 = cx2-px; let e3y: i64 = cy2-py; let e3z: i64 = cz2-pz 98 let e4x: i64 = ax-px; let e4y: i64 = ay-py; let e4z: i64 = az-pz 99 let f2x: i64 = e3y*e4z - e3z*e4y 100 let f2y: i64 = e3z*e4x - e3x*e4z 101 let f2z: i64 = e3x*e4y - e3y*e4x 102 var den: i64 = n3[0]*n3[0] + n3[1]*n3[1] + n3[2]*n3[2] 103 if den == 0 { den = 1 } 104 let wa: i64 = (f1x*n3[0] + f1y*n3[1] + f1z*n3[2]) * GB_FX / den 105 let wb: i64 = (f2x*n3[0] + f2y*n3[1] + f2z*n3[2]) * GB_FX / den 106 // signed height above the face plane, along the unit normal 107 var h: i64 = 0 108 if len > 0 { 109 h = ((px-ax)*n3[0] + (py-ay)*n3[1] + (pz-az)*n3[2]) / len 110 } 111 // ★store the REST FACE LENGTH, not a ratio. The first version stored scale*GB_FX/len, and for a 112 // 1000-unit face len is ~1e6, so 40*1024/1e6 TRUNCATED TO ZERO and every splat drove to size 0. 113 // Keeping the rest length and forming scale*len/rest_len at drive time is exact at both ends. 114 let sr: i64 = len 115 bind[i*GB_BS] = best 116 bind[i*GB_BS+1] = wa 117 bind[i*GB_BS+2] = wb 118 bind[i*GB_BS+3] = h 119 bind[i*GB_BS+4] = sr 120 i = i + 1 121 } 122 return 0 123} 124 125// ★DRIVE. Rebuild world-space gaussians from a (possibly deformed) cage. Colour and opacity are copied 126// through untouched -- appearance belongs to the splat, geometry belongs to the mesh. 127func gb_drive(bind: *i64, g: *i64, ng: i64, tri: *i64, out: *i64) -> i64 { 128 let n3: *i64 = sys_mmap(64) as *i64 129 var i: i64 = 0 130 while i < ng { 131 let t: i64 = bind[i*GB_BS] 132 let wa: i64 = bind[i*GB_BS+1] 133 let wb: i64 = bind[i*GB_BS+2] 134 let h: i64 = bind[i*GB_BS+3] 135 let sr: i64 = bind[i*GB_BS+4] 136 let wc: i64 = GB_FX - wa - wb 137 let len: i64 = gb_face(tri, t, n3) 138 let o: i64 = t*GB_TS 139 var x: i64 = (tri[o]*wa + tri[o+3]*wb + tri[o+6]*wc) / GB_FX 140 var y: i64 = (tri[o+1]*wa + tri[o+4]*wb + tri[o+7]*wc) / GB_FX 141 var z: i64 = (tri[o+2]*wa + tri[o+5]*wb + tri[o+8]*wc) / GB_FX 142 if len > 0 { 143 x = x + h*n3[0]/len 144 y = y + h*n3[1]/len 145 z = z + h*n3[2]/len 146 } 147 out[i*GB_GS] = x 148 out[i*GB_GS+1] = y 149 out[i*GB_GS+2] = z 150 // ★★A SPLAT RADIUS IS A LENGTH, AND `len` IS AN AREA. The face normal's magnitude is TWICE THE 151 // TRIANGLE AREA, so it grows as the square of the cage's linear scale -- driving scale by it 152 // directly made a doubled cage produce 4x splats (measured 160 where 80 was right). Take the 153 // square root on both sides so the radius follows the LINEAR dimension. 154 // ★Same class as this lane's nx_fascia bug: a quantity applied in the wrong dimension still 155 // produces a plausible number, and only a known-answer test says which. 156 var rlin: i64 = gb_isqrt(sr) 157 if rlin <= 0 { rlin = 1 } 158 out[i*GB_GS+3] = g[i*GB_GS+3] * gb_isqrt(len) / rlin 159 out[i*GB_GS+4] = g[i*GB_GS+4] 160 out[i*GB_GS+5] = g[i*GB_GS+5] 161 out[i*GB_GS+6] = g[i*GB_GS+6] 162 out[i*GB_GS+7] = g[i*GB_GS+7] 163 i = i + 1 164 } 165 return 0 166} 167 168// ---- fixtures + gate ------------------------------------------------------------------------- 169const GB_NT: i64 = 2 170const GB_NG: i64 = 6 171 172// two triangles forming a quad in the z=0 plane, 1000 units across 173func gb_cage(tri: *i64, sc: i64, offx: i64, rot90: i64) -> i64 { 174 let vx: *i64 = sys_mmap(64) as *i64 175 let vy: *i64 = sys_mmap(64) as *i64 176 vx[0] = 0; vy[0] = 0 177 vx[1] = 1000; vy[1] = 0 178 vx[2] = 1000; vy[2] = 1000 179 vx[3] = 0; vy[3] = 1000 180 var k: i64 = 0 181 while k < 4 { 182 var X: i64 = vx[k]*sc/GB_FX 183 var Y: i64 = vy[k]*sc/GB_FX 184 if rot90 == 1 { let tq: i64 = X; X = 0-Y; Y = tq } // rotate the CAGE about z 185 vx[k] = X + offx 186 vy[k] = Y 187 k = k + 1 188 } 189 tri[0]=vx[0]; tri[1]=vy[0]; tri[2]=0 190 tri[3]=vx[1]; tri[4]=vy[1]; tri[5]=0 191 tri[6]=vx[2]; tri[7]=vy[2]; tri[8]=0 192 tri[9]=vx[0]; tri[10]=vy[0]; tri[11]=0 193 tri[12]=vx[2]; tri[13]=vy[2]; tri[14]=0 194 tri[15]=vx[3]; tri[16]=vy[3]; tri[17]=0 195 return 0 196} 197func gb_cloud(g: *i64) -> i64 { 198 var i: i64 = 0 199 while i < GB_NG { 200 g[i*GB_GS] = 200 + i*120 201 g[i*GB_GS+1] = 250 + i*90 202 g[i*GB_GS+2] = 0 203 g[i*GB_GS+3] = 40 204 g[i*GB_GS+4] = 200; g[i*GB_GS+5] = 150; g[i*GB_GS+6] = 120 205 g[i*GB_GS+7] = 256 206 i = i + 1 207 } 208 return 0 209} 210 211func main(argc: i64, argv: *i64) -> i64 { 212 let ctr: *i64 = gv_ctr() 213 gv_head("nx_gsplatbind selftest -- mesh cage drives the splat cloud (outside splat, inside mesh)" as *u8) 214 let tri: *i64 = sys_mmap(GB_NT*GB_TS*8) as *i64 215 let g: *i64 = sys_mmap(GB_NG*GB_GS*8) as *i64 216 let bind: *i64 = sys_mmap(GB_NG*GB_BS*8) as *i64 217 let out: *i64 = sys_mmap(GB_NG*GB_GS*8) as *i64 218 gb_cage(tri, GB_FX, 0, 0) 219 gb_cloud(g) 220 gb_bind(g, GB_NG, tri, GB_NT, bind) 221 222 // ★T1 IDENTITY ROUND-TRIP: driving with the UNDEFORMED cage must reproduce the cloud it was bound 223 // from. If this drifts, every later pose is measured against a cloud that already moved. 224 gb_drive(bind, g, GB_NG, tri, out) 225 var t1: i64 = 1 226 var i: i64 = 0 227 while i < GB_NG { 228 if gb_iabs(out[i*GB_GS] - g[i*GB_GS]) > 2 { t1 = 0 } 229 if gb_iabs(out[i*GB_GS+1] - g[i*GB_GS+1]) > 2 { t1 = 0 } 230 if gb_iabs(out[i*GB_GS+2] - g[i*GB_GS+2]) > 2 { t1 = 0 } 231 i = i + 1 232 } 233 gv_check("T1 IDENTITY: the undeformed cage reproduces the bound cloud" as *u8, t1, ctr) 234 235 // T2 RIGID TRANSLATION: shift the cage, the cloud shifts by exactly the same amount 236 let tri2: *i64 = sys_mmap(GB_NT*GB_TS*8) as *i64 237 gb_cage(tri2, GB_FX, 500, 0) 238 let out2: *i64 = sys_mmap(GB_NG*GB_GS*8) as *i64 239 gb_drive(bind, g, GB_NG, tri2, out2) 240 var t2: i64 = 1 241 i = 0 242 while i < GB_NG { 243 if gb_iabs((out2[i*GB_GS] - g[i*GB_GS]) - 500) > 2 { t2 = 0 } 244 if gb_iabs(out2[i*GB_GS+1] - g[i*GB_GS+1]) > 2 { t2 = 0 } 245 i = i + 1 246 } 247 gv_check("T2 the cage TRANSLATES the cloud, exactly" as *u8, t2, ctr) 248 249 // ★★T3 ROTATION: rotate the cage 90 about z; every splat must land at its rotated position, which a 250 // world-space (unbound) cloud would NOT do. This is the tooth that proves the cloud is really riding 251 // the cage rather than sitting in world space next to it. 252 let tri3: *i64 = sys_mmap(GB_NT*GB_TS*8) as *i64 253 gb_cage(tri3, GB_FX, 0, 1) 254 let out3: *i64 = sys_mmap(GB_NG*GB_GS*8) as *i64 255 gb_drive(bind, g, GB_NG, tri3, out3) 256 var t3: i64 = 1 257 i = 0 258 while i < GB_NG { 259 let ex: i64 = 0 - g[i*GB_GS+1] 260 let ey: i64 = g[i*GB_GS] 261 if gb_iabs(out3[i*GB_GS] - ex) > 3 { t3 = 0 } 262 if gb_iabs(out3[i*GB_GS+1] - ey) > 3 { t3 = 0 } 263 i = i + 1 264 } 265 gv_check("T3 the cage ROTATES the cloud -- splats ride the surface, not world space" as *u8, t3, ctr) 266 267 // ★★★T4 SCALE COUPLING: double the cage and the splat SIZE must double too. Without this a grown 268 // surface renders as pinpricks -- the cloud would translate correctly and still look wrong. 269 let tri4: *i64 = sys_mmap(GB_NT*GB_TS*8) as *i64 270 gb_cage(tri4, GB_FX*2, 0, 0) 271 let out4: *i64 = sys_mmap(GB_NG*GB_GS*8) as *i64 272 gb_drive(bind, g, GB_NG, tri4, out4) 273 gv_puts(" scale: rest=" as *u8); gv_num(g[3]) 274 gv_puts(" doubled-cage=" as *u8); gv_num(out4[3]); gv_puts("\n" as *u8) 275 var t4: i64 = 0 276 if gb_iabs(out4[3] - g[3]*2) <= 4 { t4 = 1 } 277 gv_check("T4 SCALE rides the surface: a cage twice as big carries splats twice as big" as *u8, t4, ctr) 278 279 // ★★T5 ANTI-VACUITY + LOCALITY: move ONE triangle's free vertex. Splats bound to that face must 280 // move; splats bound to the other face must be EXACTLY unchanged. A binding that moves everything 281 // is a global transform wearing a cage costume; one that moves nothing is cosmetic. 282 let tri5: *i64 = sys_mmap(GB_NT*GB_TS*8) as *i64 283 var c: i64 = 0 284 while c < GB_NT*GB_TS { tri5[c] = tri[c]; c = c + 1 } 285 tri5[4] = tri5[4] + 400 // pull triangle 0's vertex B in y 286 let out5: *i64 = sys_mmap(GB_NG*GB_GS*8) as *i64 287 gb_drive(bind, g, GB_NG, tri5, out5) 288 var moved: i64 = 0 289 var still: i64 = 0 290 i = 0 291 while i < GB_NG { 292 // ⚠"unchanged" must mean WITHIN THE ROUND-TRIP TOLERANCE, not exactly zero. T1 already showed the 293 // identity round-trip carries +-2 of integer rounding, so demanding d==0 here asserted something 294 // T1 had already disproved -- a bug in the TEST, the same exact-equality-over-integers mistake 295 // nx_skelgen's own gate documents twice. "Moved" is held well clear of that band. 296 var d: i64 = gb_iabs(out5[i*GB_GS] - g[i*GB_GS]) + gb_iabs(out5[i*GB_GS+1] - g[i*GB_GS+1]) 297 if bind[i*GB_BS] == 0 { if d > 20 { moved = moved + 1 } } 298 if bind[i*GB_BS] == 1 { if d <= 2 { still = still + 1 } } 299 i = i + 1 300 } 301 gv_puts(" local deform: moved-on-face0=" as *u8); gv_num(moved) 302 gv_puts(" unchanged-on-face1=" as *u8); gv_num(still); gv_puts("\n" as *u8) 303 var t5: i64 = 0 304 if moved > 0 { if still > 0 { t5 = 1 } } 305 gv_check("T5 LOCALITY: deforming one face moves only ITS splats, the rest are untouched" as *u8, t5, ctr) 306 307 // T6 determinism 308 let out6: *i64 = sys_mmap(GB_NG*GB_GS*8) as *i64 309 gb_drive(bind, g, GB_NG, tri3, out6) 310 var t6: i64 = 1 311 i = 0 312 while i < GB_NG*GB_GS { if out6[i] != out3[i] { t6 = 0 } i = i + 1 } 313 gv_check("T6 deterministic" as *u8, t6, ctr) 314 315 // ★T7 colour and opacity are CARRIED THROUGH, never recomputed -- appearance belongs to the splat, 316 // geometry belongs to the mesh, and the binding must not blend the two responsibilities. 317 var t7: i64 = 1 318 i = 0 319 while i < GB_NG { 320 if out3[i*GB_GS+4] != g[i*GB_GS+4] { t7 = 0 } 321 if out3[i*GB_GS+7] != g[i*GB_GS+7] { t7 = 0 } 322 i = i + 1 323 } 324 gv_check("T7 appearance passes through untouched -- the cage moves geometry only" as *u8, t7, ctr) 325 326 return gv_verdict("GSPLATBIND-GATE" as *u8, ctr, 327 "mesh cage drives splats: identity, translate, rotate, scale-couple, local deform, determinism" as *u8) 328}