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}