instance-safe-primitives.test.mjs 3.1 KB

123456789101112131415161718192021222324
  1. import assert from 'node:assert/strict'
  2. import {instanceSafePrimitives,decodePrimitive,rotate,safeNormalDot} from './instance-safe-primitives.mjs'
  3. import {packGlb,parseGlb,accessorValues} from './instance-repeated-meshes.mjs'
  4. import {compressInstanceBuffers,unpackInstanceBuffers} from './instance-buffer-codec.mjs'
  5. const bin=Buffer.alloc(84),positions=[-1,-1,0,1,-1,0,-1,1,0],normals=[0,0,1,0,0,1,0,0,1]
  6. positions.concat(normals).forEach((v,i)=>bin.writeFloatLE(v,i*4));[0,1,2].forEach((v,i)=>bin.writeUInt32LE(v,72+i*4))
  7. const json={asset:{version:'2.0'},scene:0,scenes:[{nodes:[0,1]}],nodes:[{name:'a',mesh:0,translation:[2,3,4],scale:[20,4,3],rotation:[0,0.6,0,0.8]},
  8. {name:'b',mesh:0,translation:[7,8,9],scale:[10,2,7]}],meshes:[{primitives:[{attributes:{POSITION:0,NORMAL:1},indices:2,material:0}]}],materials:[{name:'wall'}],buffers:[{byteLength:84}],
  9. bufferViews:[{buffer:0,byteOffset:0,byteLength:36},{buffer:0,byteOffset:36,byteLength:36},{buffer:0,byteOffset:72,byteLength:12}],
  10. accessors:[{bufferView:0,componentType:5126,count:3,type:'VEC3'},{bufferView:1,componentType:5126,count:3,type:'VEC3'},{bufferView:2,componentType:5125,count:3,type:'SCALAR'}]}
  11. const result=instanceSafePrimitives(packGlb(json,bin));assert.equal(result.report.instanceGroups,1);assert.equal(result.report.outputPrimitives,1)
  12. const compressed=compressInstanceBuffers(result.bytes),out=unpackInstanceBuffers(compressed),node=out.json.nodes[0],p=out.json.meshes[0].primitives[0],g=decodePrimitive(out.json,out.bin,p)
  13. assert.deepEqual(g.positions.slice(0,9),positions);assert.deepEqual(g.normals.slice(0,9),normals);assert.deepEqual(g.indices,[0,1,2]);assert.ok(Math.max(...g.indices)<node.extras.sourceVertexCount)
  14. const ext=node.extensions.EXT_mesh_gpu_instancing.attributes
  15. assert.deepEqual(accessorValues(out.json,out.bin,ext.TRANSLATION),[[2,3,4],[7,8,9]])
  16. const a=out.json.accessors[p.attributes.POSITION],ts=accessorValues(out.json,out.bin,ext.TRANSLATION),rs=accessorValues(out.json,out.bin,ext.ROTATION),ss=accessorValues(out.json,out.bin,ext.SCALE)
  17. const min=a.min.map((v,k)=>v+Math.min(...ts.map(t=>t[k]))),max=a.max.map((v,k)=>v+Math.max(...ts.map(t=>t[k]))),center=min.map((v,k)=>(v+max[k])/2),radius=Math.hypot(...min.map((v,k)=>(max[k]-v)/2))
  18. for(let i=0;i<ts.length;i++)for(let j=0;j<positions.length;j+=3){const q=rotate(positions.slice(j,j+3).map((v,k)=>v*ss[i][k]),rs[i]).map((v,k)=>v+ts[i][k]);assert.ok(Math.hypot(...q.map((v,k)=>v-center[k]))<=radius)}
  19. const sloped=Buffer.from(bin),n=[Math.SQRT1_2,Math.SQRT1_2,0];for(let i=0;i<3;i++)n.forEach((v,j)=>sloped.writeFloatLE(v,36+(i*3+j)*4))
  20. assert.ok(safeNormalDot(n,[1.3,1,.7])<0.999999)
  21. assert.equal(instanceSafePrimitives(packGlb(json,sloped)).report.instanceGroups,0,'sloped normals must retain ordinary transforms')
  22. const mutated=structuredClone(json);mutated.meshes[0].primitives[0].targets=[{POSITION:0}];assert.throws(()=>instanceSafePrimitives(packGlb(mutated,bin)))
  23. const raw=parseGlb(result.bytes);assert.deepEqual(out.bin,raw.bin,'meshopt compression must be bit-lossless')
  24. console.log(JSON.stringify({passed:true,scope:'safe normal invariance, exact TRS, conservative command bounds, unused vertices, lossless meshopt, slope/morph rejection'}))