nx_gltf_mesh.nx source
↩ module page · 154 lines · 9593 B
1// nx_gltf_mesh.nx -- ★F6 ASSET IO (graphics foundation ladder): export the current nx_trimesh mesh as a standard binary
2// glTF (.glb) -- POSITION (float32) + NORMAL (float32 VEC3, UNIT) + COLOR_0 (normalized uint8 VEC4) + indices (uint32),
3// one PBR material, doubleSided. This makes our sovereign geometry a REAL, universally-readable 3D asset (Blender, web
4// viewers, engines). 100% integer: includes an integer IEEE-754 float32 encoder. license_tier: ORIGINAL
5import "nx_syscalls.nx"
6import "nx_trimesh.nx"
7import "nx_glbnorm_lib.nx"
8const K_MAGIC_8388607: i64 = 8388607
9const K_MAGIC_2000000000: i64 = 2000000000
10const K_MAGIC_32767: i64 = 32767
11const K_MAGIC_65535: i64 = 65535
12const K_MAGIC_8192: i64 = 8192
13
14// integer -> IEEE-754 float32 bit pattern (exact for |v| < 2^24, which covers our coordinates)
15func gl_f32bits(v: i64) -> i64 {
16 if v == 0 { return 0 }
17 var sign: i64 = 0; var a: i64 = v
18 if v < 0 { sign = 1; a = 0-v }
19 var e: i64 = 0; var t: i64 = a
20 while t > 1 { t = t>>1; e = e+1 } // e = floor(log2 a)
21 var mant: i64 = 0
22 if e <= 23 { mant = (a - (1<<e)) << (23-e) } else { mant = (a - (1<<e)) >> (e-23) }
23 let exp: i64 = e + 127
24 return (sign<<31) + (exp<<23) + (mant & K_MAGIC_8388607)
25}
26func gl_u8(out: *u8, at: i64, v: i64) -> i64 { out[at] = (v&255) as u8; return at+1 }
27func gl_u16(out: *u8, at: i64, v: i64) -> i64 { out[at]=(v&255) as u8; out[at+1]=((v>>8)&255) as u8; return at+2 }
28func gl_u32(out: *u8, at: i64, v: i64) -> i64 { out[at]=(v&255) as u8; out[at+1]=((v>>8)&255) as u8; out[at+2]=((v>>16)&255) as u8; out[at+3]=((v>>24)&255) as u8; return at+4 }
29func gl_f32(out: *u8, at: i64, v: i64) -> i64 { return gl_u32(out, at, gl_f32bits(v)) }
30func gl_str(out: *u8, at: i64, s: *u8) -> i64 { var i: i64=0; var a: i64=at; while s[i]!=(0 as u8){ out[a]=s[i]; a=a+1; i=i+1 } return a }
31func gl_align4(x: i64) -> i64 { return (x+3)/4*4 }
32
33const GL_V3: i64 = 3
34const GL_VEC3_F32_BYTES: i64 = 12
35
36// ONE OWNER for the NORMAL block of the trimesh-based exporters. nx_gltf_anim carried a character-for-
37// character copy of this loop and now calls this instead, so the two cannot drift apart again.
38//
39// SPEC FIX 2026-08-25, MEASURED BY THE KHRONOS VALIDATOR, NOT ARGUED. This block used to emit
40// {VEC3, SHORT normalized} -- n*8 clamped to 32767. glTF 2.0 permits exactly one format for NORMAL,
41// {VEC3, FLOAT}, so every file this wrote carried MESH_PRIMITIVE_ATTRIBUTES_ACCESSOR_INVALID_FORMAT; and
42// a tightly packed VEC3 of SHORT is a 6-byte element, which the spec's 4-byte vertex-attribute alignment
43// rule forbids, so each file ALSO carried MESH_PRIMITIVE_ACCESSOR_UNALIGNED. Six published files, twelve
44// errors, one loop. Emitting float32 fixes both AT ONCE -- 12 bytes is 4-aligned by construction -- and
45// routing through gn_unit3 makes the vectors unit as well, so a zero normal from tm_vnorm becomes the
46// named +Y fallback rather than a third error.
47// COST, STATED: the NORMAL block doubles from nv*6 to nv*12 bytes. Every caller of the exporters was
48// checked before this landed -- all three allocate 8,388,608 bytes against a 447,244-byte worst case.
49func gl_emit_normals(bin: *u8, at0: i64, nv: i64) -> i64 {
50 let vp: *i64 = sys_mmap(GL_V3 * 8) as *i64
51 let un: *i64 = sys_mmap(GL_V3 * 8) as *i64
52 var at: i64 = at0
53 var a: i64 = 0
54 while a < nv {
55 tm_vnorm(a, vp)
56 gn_unit3(vp[0], vp[1], vp[2], un)
57 at = gl_u32(bin, at, gn_f32(un[0]))
58 at = gl_u32(bin, at, gn_f32(un[1]))
59 at = gl_u32(bin, at, gn_f32(un[2]))
60 a = a + 1
61 }
62 return at
63}
64// json helpers
65func jcat(o: *u8, at: i64, s: *u8) -> i64 { var i: i64=0; var a: i64=at; while s[i]!=(0 as u8){o[a]=s[i]; a=a+1; i=i+1} return a }
66func jnum(o: *u8, at: i64, v: i64) -> i64 {
67 let b: *u8 = sys_mmap(32) as *u8
68 var x: i64 = v; var ng: i64 = 0
69 if x < 0 { ng = 1; x = 0-x }
70 var i: i64 = 31
71 if x == 0 { b[i]=48 as u8; i=i-1 }
72 while x > 0 { b[i]=(48+x%10) as u8; x=x/10; i=i-1 }
73 if ng == 1 { b[i]=45 as u8; i=i-1 }
74 var a: i64 = at; var j: i64 = i+1
75 while j < 32 { o[a]=b[j]; a=a+1; j=j+1 }
76 return a
77}
78
79// export current trimesh -> GLB bytes in `out` (*u8, must be >= ~NV*22 + NT*12 + 4096). Returns total length.
80func gltf_export(out: *u8) -> i64 {
81 let NV: i64 = tm_nv(); let NT: i64 = tm_nt(); let NI: i64 = NT*3
82 // ---- BIN layout (each bufferView aligned for its component type) ----
83 let posOff: i64 = 0; let posLen: i64 = NV*12
84 let nrmOff: i64 = posLen; let nrmLen: i64 = NV*GL_VEC3_F32_BYTES
85 let colOff: i64 = gl_align4(nrmOff+nrmLen); let colLen: i64 = NV*4
86 let idxOff: i64 = colOff+colLen; let idxLen: i64 = NI*4
87 let binLen: i64 = idxOff+idxLen
88 let bin: *u8 = sys_mmap(binLen + 64)
89 let vp: *i64 = sys_mmap(24) as *i64
90 let tt: *i64 = sys_mmap(24) as *i64
91 var minx: i64 = K_MAGIC_2000000000; var miny: i64 = K_MAGIC_2000000000; var minz: i64 = K_MAGIC_2000000000
92 var maxx: i64 = 0-K_MAGIC_2000000000; var maxy: i64 = 0-K_MAGIC_2000000000; var maxz: i64 = 0-K_MAGIC_2000000000
93 // positions (float32) + bbox
94 var a: i64 = 0; var at: i64 = posOff
95 while a < NV {
96 tm_vpos(a, vp)
97 at = gl_f32(bin, at, vp[0]); at = gl_f32(bin, at, vp[1]); at = gl_f32(bin, at, vp[2])
98 if vp[0]<minx {minx=vp[0]} if vp[0]>maxx {maxx=vp[0]}
99 if vp[1]<miny {miny=vp[1]} if vp[1]>maxy {maxy=vp[1]}
100 if vp[2]<minz {minz=vp[2]} if vp[2]>maxz {maxz=vp[2]}
101 a = a + 1
102 }
103 // normals: {VEC3, FLOAT}, unit. See gl_emit_normals for why this is no longer normalized int16.
104 at = gl_emit_normals(bin, nrmOff, NV)
105 // colours (uint8 normalized VEC4 r,g,b,255)
106 a = 0; at = colOff
107 while a < NV {
108 let col: i64 = tm_getvcol(a)
109 at = gl_u8(bin, at, col&255); at = gl_u8(bin, at, (col>>8)&255); at = gl_u8(bin, at, (col>>16)&255); at = gl_u8(bin, at, 255)
110 a = a + 1
111 }
112 // indices (uint32)
113 var tix: i64 = 0; at = idxOff
114 while tix < NT {
115 tm_tget(tix, tt)
116 at = gl_u32(bin, at, tt[0]); at = gl_u32(bin, at, tt[1]); at = gl_u32(bin, at, tt[2])
117 tix = tix + 1
118 }
119 // ---- JSON ----
120 let json: *u8 = sys_mmap(K_MAGIC_8192)
121 var j: i64 = 0
122 j = jcat(json, j, "{\"asset\":{\"version\":\"2.0\",\"generator\":\"nishi nx_gltf_mesh\"},\"scene\":0,\"scenes\":[{\"nodes\":[0]}],\"nodes\":[{\"mesh\":0}]," as *u8)
123 j = jcat(json, j, "\"meshes\":[{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"NORMAL\":1,\"COLOR_0\":2},\"indices\":3,\"material\":0}]}]," as *u8)
124 j = jcat(json, j, "\"materials\":[{\"pbrMetallicRoughness\":{\"metallicFactor\":0,\"roughnessFactor\":1},\"doubleSided\":true}]," as *u8)
125 j = jcat(json, j, "\"accessors\":[{\"bufferView\":0,\"componentType\":5126,\"count\":" as *u8); j = jnum(json, j, NV)
126 j = jcat(json, j, ",\"type\":\"VEC3\",\"min\":[" as *u8); j = jnum(json,j,minx); j=jcat(json,j,","as *u8); j=jnum(json,j,miny); j=jcat(json,j,","as *u8); j=jnum(json,j,minz)
127 j = jcat(json, j, "],\"max\":[" as *u8); j=jnum(json,j,maxx); j=jcat(json,j,","as *u8); j=jnum(json,j,maxy); j=jcat(json,j,","as *u8); j=jnum(json,j,maxz); j=jcat(json,j,"]}," as *u8)
128 j = jcat(json, j, "{\"bufferView\":1,\"componentType\":5126,\"count\":" as *u8); j = jnum(json, j, NV); j = jcat(json, j, ",\"type\":\"VEC3\"}," as *u8)
129 j = jcat(json, j, "{\"bufferView\":2,\"componentType\":5121,\"normalized\":true,\"count\":" as *u8); j = jnum(json, j, NV); j = jcat(json, j, ",\"type\":\"VEC4\"}," as *u8)
130 j = jcat(json, j, "{\"bufferView\":3,\"componentType\":5125,\"count\":" as *u8); j = jnum(json, j, NI); j = jcat(json, j, ",\"type\":\"SCALAR\"}]," as *u8)
131 j = jcat(json, j, "\"bufferViews\":[{\"buffer\":0,\"byteOffset\":0,\"byteLength\":" as *u8); j = jnum(json, j, posLen); j = jcat(json, j, ",\"target\":34962}," as *u8)
132 j = jcat(json, j, "{\"buffer\":0,\"byteOffset\":" as *u8); j=jnum(json,j,nrmOff); j=jcat(json,j,",\"byteLength\":" as *u8); j=jnum(json,j,nrmLen); j=jcat(json,j,",\"target\":34962}," as *u8)
133 j = jcat(json, j, "{\"buffer\":0,\"byteOffset\":" as *u8); j=jnum(json,j,colOff); j=jcat(json,j,",\"byteLength\":" as *u8); j=jnum(json,j,colLen); j=jcat(json,j,",\"target\":34962}," as *u8)
134 j = jcat(json, j, "{\"buffer\":0,\"byteOffset\":" as *u8); j=jnum(json,j,idxOff); j=jcat(json,j,",\"byteLength\":" as *u8); j=jnum(json,j,idxLen); j=jcat(json,j,",\"target\":34963}]," as *u8)
135 j = jcat(json, j, "\"buffers\":[{\"byteLength\":" as *u8); j = jnum(json, j, binLen); j = jcat(json, j, "}]}" as *u8)
136 let jsonLen: i64 = j
137 let jsonPad: i64 = gl_align4(jsonLen)
138 // ---- assemble GLB ----
139 var o: i64 = 0
140 o = gl_str(out, o, "glTF" as *u8) // magic
141 o = gl_u32(out, o, 2) // version
142 let total: i64 = 12 + 8 + jsonPad + 8 + binLen
143 o = gl_u32(out, o, total) // total length
144 o = gl_u32(out, o, jsonPad) // JSON chunk length
145 o = gl_str(out, o, "JSON" as *u8) // JSON chunk type
146 var q: i64 = 0
147 while q < jsonLen { out[o] = json[q]; o = o + 1; q = q + 1 }
148 while q < jsonPad { out[o] = 32 as u8; o = o + 1; q = q + 1 } // pad JSON with spaces
149 o = gl_u32(out, o, binLen) // BIN chunk length
150 o = gl_u8(out, o, 66); o = gl_u8(out, o, 73); o = gl_u8(out, o, 78); o = gl_u8(out, o, 0) // "BIN\0"
151 q = 0
152 while q < binLen { out[o] = bin[q]; o = o + 1; q = q + 1 }
153 return o
154}