nx_gltf_export_multipart_candidate_t345.nx source
↩ module page · 349 lines · 23902 B
1// nx_gltf_export.nx -- export a bestiary mesh as binary glTF (.glb), roadmap R7 / the VRM foundation. glTF 2.0
2// is THE modern interchange format (web three.js/babylon, Unity/Godot/Blender, and the container VRM extends for
3// riggable avatars). Extends the STL/OBJ export (nx_meshgen verts+tris) to a real scene-graph format: a JSON
4// chunk (scene/node/mesh/accessors) + a BIN chunk (float32 positions + uint32 indices). Positions are the fx1024
5// integer verts encoded as float32, with a node scale of 1/1024 so the imported mesh is correctly sized. v0 =
6// static mesh; the skeleton + skin + VRM humanoid extension are the next layer. license_tier: ORIGINAL
7import "nx_syscalls.nx"
8import "nx_atomic_rewrite.nx"
9import "nx_gltf_writer_candidate_t345.nx"
10import "nx_vecmath.nx"
11import "nx_vnormals_lib.nx"
12import "nx_glbnorm_lib.nx"
13const K_MAGIC_8192: i64 = 8192
14
15// signed integer -> IEEE-754 float32 bits (low 32 of the return). Truncating mantissa (fine for geometry).
16func gl_i2f32(v: i64) -> i64 {
17 if v == 0 { return 0 }
18 var sign: i64 = 0
19 var a: i64 = v
20 if a < 0 { sign = 1; a = 0 - a }
21 var e: i64 = 0
22 var t: i64 = a
23 while t > 1 { t = t >> 1; e = e + 1 } // e = floor(log2(a))
24 var mant: i64 = 0
25 if e <= 23 { mant = (a << (23 - e)) & 0x7fffff }
26 else { mant = (a >> (e - 23)) & 0x7fffff }
27 let exp: i64 = 127 + e
28 return (sign << 31) | (exp << 23) | mant
29}
30func gl_wr32(buf: *u8, o: i64, v: i64) -> i64 {
31 buf[o] = (v & 0xff) as u8; buf[o+1] = ((v>>8)&0xff) as u8; buf[o+2] = ((v>>16)&0xff) as u8; buf[o+3] = ((v>>24)&0xff) as u8
32 return o + 4
33}
34func gl_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){d[o+i]=s[i]; i=i+1} return o+i }
35func gl_num(d: *u8, o: i64, v: i64) -> i64 {
36 var x: i64 = v; var oo: i64 = o
37 if x < 0 { d[oo]=45 as u8; oo=oo+1; x=0-x }
38 if x == 0 { d[oo]=48 as u8; return oo+1 }
39 let t: *u8 = sys_mmap(24); var k: i64 = 0
40 while x > 0 { t[k]=(48+x%10) as u8; x=x/10; k=k+1 }
41 var i: i64 = 0
42 while i < k { d[oo+i]=t[k-1-i]; i=i+1 }
43 return oo + k
44}
45// (gl_isqrt removed 2026-08-25: a one-line wrapper over vm_isqrt whose ONLY caller was gl_vnormals's
46// former body, now retired onto nx_vnormals_lib. Proven callerless first -- nx_absent over the whole of
47// buildroot/runtime, corpus_complete=1: the only other hit is a SEPARATE gl_isqrt that
48// nx_procgen_gtlabels_gate defines for itself.)
49// smooth per-vertex normals into nrm[nv*3], at vn_smooth_area's VN_SCALE of ~1000.
50// CORRECTED 2026-08-25: this line used to end "renderers re-normalise after the node's inverse-transpose",
51// which was a COMMENT, NOT A MEASUREMENT, and it was false. glTF 2.0 requires the NORMAL accessor itself to
52// carry unit vectors, and the Khronos validator ranks a non-unit one an Error -- 33,656 of them across the
53// 12 published .glb files this function wrote. The ~1000 scale is still correct HERE, because nx_obj_export
54// consumes the same buffer and OBJ importers do rescale; the unit conversion now happens at the glTF write
55// site only, in nx_glbnorm_lib.gn_unit3.
56// RETIRED ONTO nx_vnormals_lib.vn_smooth_area, 2026-08-25. This function's body WAS the area-weighted
57// accumulate-and-normalise, character for character the same computation as the block that was inline inside
58// nx_obj_export.write_obj. One owner now; nx_vnormals_gate carries a verbatim transcription of this former
59// body as a reference oracle and asserts the owner reproduces it EXACTLY on every fixture, so the retirement
60// is proven rather than assumed.
61// The one difference, named: the owner REFUSES nv <= 0 (VN_E_ARGS) where this returned 0. Both call sites
62// below discard the return value and neither passes nv <= 0, so no caller observes it; for nv > 0 the return
63// is 0 exactly as before.
64func gl_vnormals(vbuf: *i64, fbuf: *i64, nv: i64, nf: i64, nrm: *i64) -> i64 {
65 return vn_smooth_area(vbuf, fbuf, nv, nf, nrm)
66}
67
68// write a .glb WITH per-vertex COLOR_0 (the texture-bake path: photo colors baked onto the mesh, VR-loadable
69// with no UV unwrap). cbuf = nv*3 i64 (RGB 0..255). COLOR_0 = VEC4 normalized UNSIGNED_BYTE (RGBA, A=255) --
70// the standard cleanly-4-aligned vertex-color format every glTF/VRM viewer accepts. Returns byte length or -1.
71func write_glb_colored(vbuf: *i64, fbuf: *i64, cbuf: *i64, nv: i64, nf: i64, path: *u8) -> i64 {
72 let posbytes: i64 = nv * 12
73 let normbytes: i64 = nv * 12
74 let colbytes: i64 = nv * 4
75 let idxbytes: i64 = nf * 3 * 4
76 let binlen: i64 = posbytes + normbytes + colbytes + idxbytes
77 let bin: *u8 = sys_mmap(binlen + 64)
78 let nrm: *i64 = sys_mmap(nv * 3 * 8) as *i64
79 gl_vnormals(vbuf, fbuf, nv, nf, nrm)
80 var bo: i64 = 0
81 var i: i64 = 0
82 while i < nv {
83 bo = gl_wr32(bin, bo, gl_i2f32(vbuf[i*3])); bo = gl_wr32(bin, bo, gl_i2f32(vbuf[i*3+1])); bo = gl_wr32(bin, bo, gl_i2f32(vbuf[i*3+2]))
84 i = i + 1
85 }
86 let normoff: i64 = bo
87 // SPEC FIX 2026-08-25 (glTF 2.0 requires unit NORMALs; the validator ranks a non-unit one an Error).
88 // Allocated ONCE, outside the loop -- never allocate in a hot loop.
89 let un: *i64 = sys_mmap(GN_V3 * 8) as *i64
90 i = 0
91 while i < nv {
92 gn_unit3(nrm[i*3], nrm[i*3+1], nrm[i*3+2], un)
93 bo = gl_wr32(bin, bo, gn_f32(un[0])); bo = gl_wr32(bin, bo, gn_f32(un[1])); bo = gl_wr32(bin, bo, gn_f32(un[2]))
94 i = i + 1
95 }
96 let coloff: i64 = bo
97 i = 0
98 while i < nv {
99 var r: i64 = cbuf[i*3]; var g: i64 = cbuf[i*3+1]; var b: i64 = cbuf[i*3+2]
100 if r < 0 { r = 0 } if r > 255 { r = 255 }
101 if g < 0 { g = 0 } if g > 255 { g = 255 }
102 if b < 0 { b = 0 } if b > 255 { b = 255 }
103 bin[bo] = r as u8; bin[bo+1] = g as u8; bin[bo+2] = b as u8; bin[bo+3] = 255 as u8; bo = bo + 4
104 i = i + 1
105 }
106 let idxoff: i64 = bo
107 var j: i64 = 0
108 while j < nf * 3 { bo = gl_wr32(bin, bo, fbuf[j]); j = j + 1 }
109 var minx: i64 = vbuf[0]; var miny: i64 = vbuf[1]; var minz: i64 = vbuf[2]
110 var maxx: i64 = vbuf[0]; var maxy: i64 = vbuf[1]; var maxz: i64 = vbuf[2]
111 i = 1
112 while i < nv {
113 if vbuf[i*3]<minx{minx=vbuf[i*3]} if vbuf[i*3]>maxx{maxx=vbuf[i*3]}
114 if vbuf[i*3+1]<miny{miny=vbuf[i*3+1]} if vbuf[i*3+1]>maxy{maxy=vbuf[i*3+1]}
115 if vbuf[i*3+2]<minz{minz=vbuf[i*3+2]} if vbuf[i*3+2]>maxz{maxz=vbuf[i*3+2]}
116 i = i + 1
117 }
118 let js: *u8 = sys_mmap(K_MAGIC_8192)
119 var o: i64 = gl_cat(js, 0, "{\"asset\":{\"version\":\"2.0\",\"generator\":\"nishi-nx_gltf\"},\"scene\":0,\"scenes\":[{\"nodes\":[0]}]," as *u8)
120 o = gl_cat(js, o, "\"nodes\":[{\"mesh\":0,\"scale\":[0.0009765625,0.0009765625,0.0009765625]}]," as *u8)
121 o = gl_cat(js, o, "\"meshes\":[{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"NORMAL\":1,\"COLOR_0\":2},\"indices\":3}]}]," as *u8)
122 o = gl_cat(js, o, "\"buffers\":[{\"byteLength\":" as *u8); o = gl_num(js, o, binlen); o = gl_cat(js, o, "}]," as *u8)
123 o = gl_cat(js, o, "\"bufferViews\":[{\"buffer\":0,\"byteOffset\":0,\"byteLength\":" as *u8); o = gl_num(js, o, posbytes); o = gl_cat(js, o, ",\"target\":34962}," as *u8)
124 o = gl_cat(js, o, "{\"buffer\":0,\"byteOffset\":" as *u8); o = gl_num(js, o, normoff); o = gl_cat(js, o, ",\"byteLength\":" as *u8); o = gl_num(js, o, normbytes); o = gl_cat(js, o, ",\"target\":34962}," as *u8)
125 o = gl_cat(js, o, "{\"buffer\":0,\"byteOffset\":" as *u8); o = gl_num(js, o, coloff); o = gl_cat(js, o, ",\"byteLength\":" as *u8); o = gl_num(js, o, colbytes); o = gl_cat(js, o, ",\"target\":34962}," as *u8)
126 o = gl_cat(js, o, "{\"buffer\":0,\"byteOffset\":" as *u8); o = gl_num(js, o, idxoff); o = gl_cat(js, o, ",\"byteLength\":" as *u8); o = gl_num(js, o, idxbytes); o = gl_cat(js, o, ",\"target\":34963}]," as *u8)
127 o = gl_cat(js, o, "\"accessors\":[{\"bufferView\":0,\"componentType\":5126,\"count\":" as *u8); o = gl_num(js, o, nv)
128 o = gl_cat(js, o, ",\"type\":\"VEC3\",\"min\":[" as *u8); o = gl_num(js, o, minx); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, miny); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, minz)
129 o = gl_cat(js, o, "],\"max\":[" as *u8); o = gl_num(js, o, maxx); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, maxy); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, maxz)
130 o = gl_cat(js, o, "]},{\"bufferView\":1,\"componentType\":5126,\"count\":" as *u8); o = gl_num(js, o, nv); o = gl_cat(js, o, ",\"type\":\"VEC3\"}," as *u8)
131 o = gl_cat(js, o, "{\"bufferView\":2,\"componentType\":5121,\"normalized\":true,\"count\":" as *u8); o = gl_num(js, o, nv); o = gl_cat(js, o, ",\"type\":\"VEC4\"}," as *u8)
132 o = gl_cat(js, o, "{\"bufferView\":3,\"componentType\":5125,\"count\":" as *u8); o = gl_num(js, o, nf*3); o = gl_cat(js, o, ",\"type\":\"SCALAR\"}]}" as *u8)
133 let jsonlen: i64 = o
134 var jpad: i64 = (4 - (jsonlen & 3)) & 3
135 var bpad: i64 = (4 - (binlen & 3)) & 3
136 let total: i64 = 12 + 8 + jsonlen + jpad + 8 + binlen + bpad
137 let out: *u8 = sys_mmap(total + 64)
138 gl_wr32(out, 0, 0x46546C67); gl_wr32(out, 4, 2); gl_wr32(out, 8, total)
139 gl_wr32(out, 12, jsonlen + jpad); gl_wr32(out, 16, 0x4E4F534A)
140 var p: i64 = 20
141 var k: i64 = 0
142 while k < jsonlen { out[p] = js[k]; p = p + 1; k = k + 1 }
143 k = 0; while k < jpad { out[p] = 32 as u8; p = p + 1; k = k + 1 }
144 gl_wr32(out, p, binlen + bpad); p = p + 4
145 gl_wr32(out, p, 0x004E4942); p = p + 4
146 k = 0; while k < binlen { out[p] = bin[k]; p = p + 1; k = k + 1 }
147 k = 0; while k < bpad { out[p] = 0 as u8; p = p + 1; k = k + 1 }
148 let fd: i64 = sys_openat_wr(path, 0x1a4)
149 if fd < 0 { return 0 - 1 }
150 sys_write(fd, out, p)
151 sys_close(fd)
152 return p
153}
154
155// write a .glb. vbuf = nv*3 fx1024 ints; fbuf = nf*3 tri vertex indices. Returns byte length, or -1.
156
157// Scalar glTF material ABI. RGB is linear, never encoded sRGB. Ratios use explicit fixed-point scales.
158const GL_MAT_R:i64=0
159const GL_MAT_G:i64=1
160const GL_MAT_B:i64=2
161const GL_MAT_METALLIC:i64=3
162const GL_MAT_ROUGHNESS:i64=4
163const GL_MAT_WORDS:i64=5
164const GL_MAT_IO_RC_BASE:i64=-100
165const GL_MAT_LINEAR_Q:i64=100000
166const GL_MAT_RATIO_Q:i64=1000
167func gl_material_ok(m:*i64,words:i64)->i64{
168 if (m as i64)<=0||words!=GL_MAT_WORDS{return 0}
169 var i:i64=GL_MAT_R;while i<=GL_MAT_B{if m[i]<0||m[i]>GL_MAT_LINEAR_Q{return 0};i=i+1}
170 if m[GL_MAT_METALLIC]<0||m[GL_MAT_METALLIC]>GL_MAT_RATIO_Q||m[GL_MAT_ROUGHNESS]<0||m[GL_MAT_ROUGHNESS]>GL_MAT_RATIO_Q{return 0}
171 return 1
172}
173func gl_material_decimal(out:*u8,pos:i64,value:i64,q:i64)->i64{
174 var p:i64=gl_num(out,pos,value/q);p=gl_cat(out,p,".");var d:i64=q/10
175 while d>0{out[p]=(48+(value/d)%10) as u8;p=p+1;d=d/10};return p
176}
177func gl_material_json(out:*u8,pos:i64,m:*i64)->i64{
178 var p:i64=gl_cat(out,pos,"\"materials\":[{\"pbrMetallicRoughness\":{\"baseColorFactor\":[")
179 var i:i64=GL_MAT_R;while i<=GL_MAT_B{if i>GL_MAT_R{p=gl_cat(out,p,",")};p=gl_material_decimal(out,p,m[i],GL_MAT_LINEAR_Q);i=i+1}
180 p=gl_cat(out,p,",1],\"metallicFactor\":");p=gl_material_decimal(out,p,m[GL_MAT_METALLIC],GL_MAT_RATIO_Q)
181 p=gl_cat(out,p,",\"roughnessFactor\":");p=gl_material_decimal(out,p,m[GL_MAT_ROUGHNESS],GL_MAT_RATIO_Q)
182 return gl_cat(out,p,"}}],")
183}
184
185func gl_write_glb_scalar(vbuf: *i64, fbuf: *i64, nv: i64, nf: i64, path: *u8, material:*i64) -> i64 {
186 // ---- BIN: float32 positions then uint32 indices (both 4-aligned; 12 & 4 are multiples of 4) ----
187 let posbytes: i64 = nv * 12
188 let normbytes: i64 = nv * 12
189 let idxbytes: i64 = nf * 3 * 4
190 let binlen: i64 = posbytes + normbytes + idxbytes
191 let bin: *u8 = sys_mmap(binlen + 64)
192 let nrm: *i64 = sys_mmap(nv * 3 * 8) as *i64
193 gl_vnormals(vbuf, fbuf, nv, nf, nrm)
194 var bo: i64 = 0
195 var i: i64 = 0
196 while i < nv {
197 bo = gl_wr32(bin, bo, gl_i2f32(vbuf[i*3]))
198 bo = gl_wr32(bin, bo, gl_i2f32(vbuf[i*3+1]))
199 bo = gl_wr32(bin, bo, gl_i2f32(vbuf[i*3+2]))
200 i = i + 1
201 }
202 let normoff: i64 = bo
203 // SPEC FIX 2026-08-25: the NORMAL accessor must carry UNIT vectors (glTF 2.0). This was the whole of
204 // ACCESSOR_VECTOR3_NON_UNIT x 33,656 across 12 published files. Positions and indices are untouched.
205 // Allocated ONCE, outside the loop -- never allocate in a hot loop.
206 let un: *i64 = sys_mmap(GN_V3 * 8) as *i64
207 i = 0
208 while i < nv {
209 gn_unit3(nrm[i*3], nrm[i*3+1], nrm[i*3+2], un)
210 bo = gl_wr32(bin, bo, gn_f32(un[0]))
211 bo = gl_wr32(bin, bo, gn_f32(un[1]))
212 bo = gl_wr32(bin, bo, gn_f32(un[2]))
213 i = i + 1
214 }
215 let idxoff: i64 = bo
216 var j: i64 = 0
217 while j < nf * 3 { bo = gl_wr32(bin, bo, fbuf[j]); j = j + 1 }
218 // bounds (raw ints -> JSON numbers, matching the float positions)
219 var minx: i64 = vbuf[0]; var miny: i64 = vbuf[1]; var minz: i64 = vbuf[2]
220 var maxx: i64 = vbuf[0]; var maxy: i64 = vbuf[1]; var maxz: i64 = vbuf[2]
221 i = 1
222 while i < nv {
223 if vbuf[i*3]<minx{minx=vbuf[i*3]} if vbuf[i*3]>maxx{maxx=vbuf[i*3]}
224 if vbuf[i*3+1]<miny{miny=vbuf[i*3+1]} if vbuf[i*3+1]>maxy{maxy=vbuf[i*3+1]}
225 if vbuf[i*3+2]<minz{minz=vbuf[i*3+2]} if vbuf[i*3+2]>maxz{maxz=vbuf[i*3+2]}
226 i = i + 1
227 }
228 // ---- JSON chunk ----
229 let js: *u8 = sys_mmap(K_MAGIC_8192)
230 var o: i64 = gl_cat(js, 0, "{\"asset\":{\"version\":\"2.0\",\"generator\":\"nishi-nx_gltf\"},\"scene\":0,\"scenes\":[{\"nodes\":[0]}]," as *u8)
231 o = gl_cat(js, o, "\"nodes\":[{\"mesh\":0,\"scale\":[0.0009765625,0.0009765625,0.0009765625]}]," as *u8)
232 if (material as i64)>0{o=gl_material_json(js,o,material)}
233 o = gl_cat(js, o, "\"meshes\":[{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"NORMAL\":1},\"indices\":2" as *u8)
234 if (material as i64)>0{o=gl_cat(js,o,",\"material\":0")}
235 o=gl_cat(js,o,"}]}],")
236 o = gl_cat(js, o, "\"buffers\":[{\"byteLength\":" as *u8); o = gl_num(js, o, binlen); o = gl_cat(js, o, "}]," as *u8)
237 o = gl_cat(js, o, "\"bufferViews\":[{\"buffer\":0,\"byteOffset\":0,\"byteLength\":" as *u8); o = gl_num(js, o, posbytes); o = gl_cat(js, o, ",\"target\":34962}," as *u8)
238 o = gl_cat(js, o, "{\"buffer\":0,\"byteOffset\":" as *u8); o = gl_num(js, o, normoff); o = gl_cat(js, o, ",\"byteLength\":" as *u8); o = gl_num(js, o, normbytes); o = gl_cat(js, o, ",\"target\":34962}," as *u8)
239 o = gl_cat(js, o, "{\"buffer\":0,\"byteOffset\":" as *u8); o = gl_num(js, o, idxoff); o = gl_cat(js, o, ",\"byteLength\":" as *u8); o = gl_num(js, o, idxbytes); o = gl_cat(js, o, ",\"target\":34963}]," as *u8)
240 o = gl_cat(js, o, "\"accessors\":[{\"bufferView\":0,\"componentType\":5126,\"count\":" as *u8); o = gl_num(js, o, nv)
241 o = gl_cat(js, o, ",\"type\":\"VEC3\",\"min\":[" as *u8); o = gl_num(js, o, minx); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, miny); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, minz)
242 o = gl_cat(js, o, "],\"max\":[" as *u8); o = gl_num(js, o, maxx); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, maxy); o = gl_cat(js, o, "," as *u8); o = gl_num(js, o, maxz)
243 o = gl_cat(js, o, "]},{\"bufferView\":1,\"componentType\":5126,\"count\":" as *u8); o = gl_num(js, o, nv); o = gl_cat(js, o, ",\"type\":\"VEC3\"}," as *u8)
244 o = gl_cat(js, o, "{\"bufferView\":2,\"componentType\":5125,\"count\":" as *u8); o = gl_num(js, o, nf*3); o = gl_cat(js, o, ",\"type\":\"SCALAR\"}]}" as *u8)
245 let jsonlen: i64 = o
246 var jpad: i64 = (4 - (jsonlen & 3)) & 3
247 var bpad: i64 = (4 - (binlen & 3)) & 3
248 // ---- GLB container ----
249 let total: i64 = 12 + 8 + jsonlen + jpad + 8 + binlen + bpad
250 let out: *u8 = sys_mmap(total + 64)
251 gl_wr32(out, 0, 0x46546C67) // magic "glTF"
252 gl_wr32(out, 4, 2) // version 2
253 gl_wr32(out, 8, total)
254 gl_wr32(out, 12, jsonlen + jpad) // JSON chunk length
255 gl_wr32(out, 16, 0x4E4F534A) // "JSON"
256 var p: i64 = 20
257 var k: i64 = 0
258 while k < jsonlen { out[p] = js[k]; p = p + 1; k = k + 1 }
259 k = 0; while k < jpad { out[p] = 32 as u8; p = p + 1; k = k + 1 } // pad JSON with spaces
260 gl_wr32(out, p, binlen + bpad); p = p + 4 // BIN chunk length
261 gl_wr32(out, p, 0x004E4942); p = p + 4 // "BIN\0"
262 k = 0; while k < binlen { out[p] = bin[k]; p = p + 1; k = k + 1 }
263 k = 0; while k < bpad { out[p] = 0 as u8; p = p + 1; k = k + 1 }
264 if (material as i64)>0{
265 // Existing checked writer owns commit semantics. -107 means rename may be visible: do not retry blindly.
266 let rc:i64=atomic_rewrite_checked(path,out,p)
267 sys_munmap(bin,binlen+64);sys_munmap(nrm as *u8,nv*3*8);sys_munmap(un as *u8,GN_V3*8);sys_munmap(js,K_MAGIC_8192);sys_munmap(out,total+64)
268 if rc!=0{return GL_MAT_IO_RC_BASE+rc};return p
269 }
270 let fd: i64 = sys_openat_wr(path, 0x1a4)
271 if fd < 0 { return 0 - 1 }
272 sys_write(fd, out, p)
273 sys_close(fd)
274 return p
275}
276
277// Legacy output remains material-free and byte-identical. Geometry buffers retain the existing trusted internal contract.
278func write_glb(vbuf:*i64,fbuf:*i64,nv:i64,nf:i64,path:*u8)->i64{return gl_write_glb_scalar(vbuf,fbuf,nv,nf,path,0 as *i64)}
279// Validate the complete material boundary before any allocation or file operation.
280func write_glb_material(vbuf:*i64,fbuf:*i64,nv:i64,nf:i64,material:*i64,words:i64,path:*u8)->i64{
281 if gl_material_ok(material,words)!=1{return -2}
282 return gl_write_glb_scalar(vbuf,fbuf,nv,nf,path,material)
283}
284
285// Caller-owned face attribution and material table; fixed word count is the scalar material ABI.
286struct GlMaterialParts { face_material:*i64, face_count:i64, materials:*i64, material_count:i64, material_words:i64 }
287const GL_PART_U32_MAX:i64=4294967295
288func gl_parts_decimal(w:*i64,value:i64,q:i64)->i64{
289 glw_n(w,value/q);glw_s(w,".");var d:i64=q/10;while d>0{let digits:*u8="0123456789";glw_put(w,(digits as i64+(value/d)%10) as *u8,1);d=d/10};return w[3]
290}
291func gl_parts_material(w:*i64,m:*i64)->i64{
292 glw_s(w,"{\"pbrMetallicRoughness\":{\"baseColorFactor\":[");var i:i64=0
293 while i<3{if i>0{glw_s(w,",")};gl_parts_decimal(w,m[i],GL_MAT_LINEAR_Q);i=i+1}
294 glw_s(w,",1],\"metallicFactor\":");gl_parts_decimal(w,m[GL_MAT_METALLIC],GL_MAT_RATIO_Q)
295 glw_s(w,",\"roughnessFactor\":");gl_parts_decimal(w,m[GL_MAT_ROUGHNESS],GL_MAT_RATIO_Q);glw_s(w,"}}");return w[3]
296}
297// Exact same function performs sizing and writing: no material-count capacity policy or guessed JSON allowance.
298func gl_parts_json(w:*i64,nv:i64,nf:i64,recipe:*GlMaterialParts,counts:*i64,bounds:*i64)->i64{
299 let binlen:i64=nv*24+nf*12
300 glw_s(w,"{\"asset\":{\"version\":\"2.0\",\"generator\":\"nishi-nx_gltf\"},\"scene\":0,\"scenes\":[{\"nodes\":[0]}],\"nodes\":[{\"mesh\":0,\"scale\":[0.0009765625,0.0009765625,0.0009765625]}],\"materials\":[")
301 var m:i64=0;while m<recipe.material_count{if m>0{glw_s(w,",")};gl_parts_material(w,(recipe.materials as i64+m*GL_MAT_WORDS*8) as *i64);m=m+1}
302 glw_s(w,"],\"meshes\":[{\"primitives\":[");var used:i64=0;m=0
303 while m<recipe.material_count{if counts[m]>0{if used>0{glw_s(w,",")};glw_s(w,"{\"attributes\":{\"POSITION\":0,\"NORMAL\":1},\"indices\":");glw_n(w,used+2);glw_s(w,",\"material\":");glw_n(w,m);glw_s(w,"}");used=used+1};m=m+1}
304 glw_s(w,"]}],\"buffers\":[{\"byteLength\":");glw_n(w,binlen);glw_s(w,"}],\"bufferViews\":[{\"buffer\":0,\"byteOffset\":0,\"byteLength\":");glw_n(w,nv*12);glw_s(w,",\"target\":34962},{\"buffer\":0,\"byteOffset\":");glw_n(w,nv*12);glw_s(w,",\"byteLength\":");glw_n(w,nv*12);glw_s(w,",\"target\":34962}")
305 var off:i64=nv*24;m=0;while m<recipe.material_count{if counts[m]>0{glw_s(w,",{\"buffer\":0,\"byteOffset\":");glw_n(w,off);glw_s(w,",\"byteLength\":");glw_n(w,counts[m]*12);glw_s(w,",\"target\":34963}");off=off+counts[m]*12};m=m+1}
306 glw_s(w,"],\"accessors\":[{\"bufferView\":0,\"componentType\":5126,\"count\":");glw_n(w,nv);glw_s(w,",\"type\":\"VEC3\",\"min\":[")
307 var k:i64=0;while k<3{if k>0{glw_s(w,",")};glw_i(w,bounds[k]);k=k+1};glw_s(w,"],\"max\":[");k=3;while k<6{if k>3{glw_s(w,",")};glw_i(w,bounds[k]);k=k+1}
308 glw_s(w,"]},{\"bufferView\":1,\"componentType\":5126,\"count\":");glw_n(w,nv);glw_s(w,",\"type\":\"VEC3\"}");used=0;m=0
309 while m<recipe.material_count{if counts[m]>0{glw_s(w,",{\"bufferView\":");glw_n(w,used+2);glw_s(w,",\"componentType\":5125,\"count\":");glw_n(w,counts[m]*3);glw_s(w,",\"type\":\"SCALAR\"}");used=used+1};m=m+1}
310 glw_s(w,"]}");return w[3]
311}
312// All externally supplied material/count/index ranges are validated before any output operation.
313// Geometry coordinates and normal arithmetic retain vn_smooth_area's trusted mesh contract.
314func gl_parts_admit(vbuf:*i64,fbuf:*i64,nv:i64,nf:i64,r:*GlMaterialParts)->i64{
315 if (vbuf as i64)<=0||(fbuf as i64)<=0||(r as i64)<=0||nv<=0||nf<=0{return -2}
316 if nv>GL_PART_U32_MAX/24||nf>(GL_PART_U32_MAX-nv*24)/12{return -2}
317 if (r.face_material as i64)<=0||r.face_count!=nf||(r.materials as i64)<=0||r.material_count<=0||r.material_count>GL_PART_U32_MAX/GL_MAT_WORDS/8||r.material_words!=r.material_count*GL_MAT_WORDS{return -2}
318 var m:i64=0;while m<r.material_count{if gl_material_ok((r.materials as i64+m*GL_MAT_WORDS*8) as *i64,GL_MAT_WORDS)!=1{return -2};m=m+1}
319 var i:i64=0;while i<nf{if r.face_material[i]<0||r.face_material[i]>=r.material_count{return -2};var k:i64=0;while k<3{if fbuf[i*3+k]<0||fbuf[i*3+k]>=nv{return -2};k=k+1};i=i+1}
320 i=0;while i<nv*3{if vbuf[i]==0-GLW_I64_MAX-1{return -2};i=i+1};return 0
321}
322func write_glb_material_parts(vbuf:*i64,fbuf:*i64,nv:i64,nf:i64,r:*GlMaterialParts,path:*u8)->i64{
323 if gl_parts_admit(vbuf,fbuf,nv,nf,r)!=0||(path as i64)<=0{return -2}
324 let scratchBytes:i64=r.material_count*16+nv*24+(GN_V3+6+4)*8
325 let scratch:*u8=sys_mmap_try(scratchBytes);if (scratch as i64)==0{return -3}
326 let counts:*i64=scratch as *i64;let cursor:*i64=(scratch as i64+r.material_count*8) as *i64
327 let nrm:*i64=(cursor as i64+r.material_count*8) as *i64;let un:*i64=(nrm as i64+nv*24) as *i64
328 let bounds:*i64=(un as i64+GN_V3*8) as *i64;let w:*i64=(bounds as i64+6*8) as *i64
329 var m:i64=0;while m<r.material_count{counts[m]=0;m=m+1};var i:i64=0;while i<nf{counts[r.face_material[i]]=counts[r.face_material[i]]+1;i=i+1}
330 var k:i64=0;while k<3{bounds[k]=vbuf[k];bounds[k+3]=vbuf[k];k=k+1};i=1;while i<nv{k=0;while k<3{let value:i64=vbuf[i*3+k];if value<bounds[k]{bounds[k]=value};if value>bounds[k+3]{bounds[k+3]=value};k=k+1};i=i+1}
331 w[0]=0;w[1]=0;w[2]=0;w[3]=0;let sized:i64=gl_parts_json(w,nv,nf,r,counts,bounds);let jsonlen:i64=w[2]
332 let binlen:i64=nv*24+nf*12;let jpad:i64=(4-(jsonlen&3))&3
333 if sized!=0||jsonlen>GL_PART_U32_MAX-28-binlen-jpad{sys_munmap_direct(scratch,scratchBytes);return -3}
334 let total:i64=28+jsonlen+jpad+binlen;let output:*u8=sys_mmap_try(total)
335 if (output as i64)==0{sys_munmap_direct(scratch,scratchBytes);return -3}
336 gl_wr32(output,0,0x46546C67);gl_wr32(output,4,2);gl_wr32(output,8,total);gl_wr32(output,12,jsonlen+jpad);gl_wr32(output,16,0x4E4F534A)
337 w[0]=(output as i64)+20;w[1]=jsonlen;w[2]=0;w[3]=0;let wrote:i64=gl_parts_json(w,nv,nf,r,counts,bounds)
338 if wrote!=0||w[2]!=jsonlen{sys_munmap_direct(output,total);sys_munmap_direct(scratch,scratchBytes);return -3}
339 i=0;while i<jpad{output[20+jsonlen+i]=32 as u8;i=i+1}
340 let chunk:i64=20+jsonlen+jpad;gl_wr32(output,chunk,binlen);gl_wr32(output,chunk+4,0x004E4942)
341 let bin:*u8=(output as i64+chunk+8) as *u8;var bo:i64=0;i=0
342 while i<nv*3{bo=gl_wr32(bin,bo,gl_i2f32(vbuf[i]));i=i+1}
343 if gl_vnormals(vbuf,fbuf,nv,nf,nrm)!=0{sys_munmap_direct(output,total);sys_munmap_direct(scratch,scratchBytes);return -2}
344 i=0;while i<nv{gn_unit3(nrm[i*3],nrm[i*3+1],nrm[i*3+2],un);k=0;while k<3{bo=gl_wr32(bin,bo,gn_f32(un[k]));k=k+1};i=i+1}
345 m=0;var start:i64=bo;while m<r.material_count{cursor[m]=start;start=start+counts[m]*12;m=m+1}
346 i=0;while i<nf{m=r.face_material[i];k=0;while k<3{cursor[m]=gl_wr32(bin,cursor[m],fbuf[i*3+k]);k=k+1};i=i+1}
347 let rc:i64=atomic_rewrite_checked(path,output,total);sys_munmap_direct(output,total);sys_munmap_direct(scratch,scratchBytes)
348 if rc!=0{return GL_MAT_IO_RC_BASE+rc};return total
349}