code wiki / (root) / nx_gltf_mesh.nx

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}