code wiki / (root) / nx_gltf_export_multipart_candidate_t345.nx

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}