verify-instanced-city.mjs 5.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. // Decode actual source/final primitives, expand instances, verify exact indexed
  2. // geometry/material/TRS, normal invariance, engine command bounds and budgets.
  3. import assert from 'node:assert/strict'
  4. import {readFileSync,writeFileSync} from 'node:fs'
  5. import {resolve,join} from 'node:path'
  6. import {createHash} from 'node:crypto'
  7. import {parseGlb,accessorValues} from './instance-repeated-meshes.mjs'
  8. import {decodePrimitive,rotate,safeNormalDot} from './instance-safe-primitives.mjs'
  9. import {unpackInstanceBuffers} from './instance-buffer-codec.mjs'
  10. import {verifyMaterialClosure} from './verify-material-closure.mjs'
  11. const out=resolve(process.argv[2]),manifest=JSON.parse(readFileSync(join(out,'manifest.json'))),plan=JSON.parse(readFileSync('tools/city-massing/city/cad-bim/instanced-active-input.json'))
  12. const bm=new Map(plan.buildings.map(b=>[b.id,b])),seen=new Set()
  13. let triangles=0,nodes=0,instances=0,normalDot=1,maxTriangles=0,maxVertices=0,maxError=0
  14. function hash(g,material,count=g.positions.length/3){return createHash('sha256').update(JSON.stringify({positions:g.positions.slice(0,count*3),normals:g.normals.slice(0,count*3),indices:g.indices,material})).digest('hex')}
  15. function transform(p,t,r,s){return rotate(p.map((v,a)=>v*s[a]),r).map((v,a)=>v+t[a])}
  16. for(const c of manifest.cells){
  17. c.buildingIds.forEach(id=>{assert.ok(bm.has(id)&&!seen.has(id));seen.add(id)})
  18. const source=parseGlb(readFileSync(join(manifest.source,c.lod0.uri))),fin=unpackInstanceBuffers(readFileSync(join(out,c.lod0.uri)))
  19. verifyMaterialClosure(fin.json,c.id)
  20. const refs=new Map(),sourceGeometry=new Map(),remaining=new Map()
  21. for(const n of source.json.nodes){const ps=source.json.meshes[n.mesh].primitives,expected=new Map()
  22. const keys=ps.map(p=>{const i=JSON.stringify([n.mesh,p.indices]);if(!sourceGeometry.has(i))sourceGeometry.set(i,decodePrimitive(source.json,source.bin,p));const g=sourceGeometry.get(i);return hash(g,source.json.materials[p.material])})
  23. keys.forEach(k=>expected.set(k,(expected.get(k)||0)+1));remaining.set(n.name,expected);refs.set(n.name,n)
  24. }
  25. let cellTriangles=0,cellVertices=0
  26. for(const n of fin.json.nodes){const ext=n.extensions?.EXT_mesh_gpu_instancing,attrs=ext?.attributes,ns=ext?n.extras.instanceNames:[n.name],ts=ext?accessorValues(fin.json,fin.bin,attrs.TRANSLATION):[n.translation||[0,0,0]],rs=ext?accessorValues(fin.json,fin.bin,attrs.ROTATION):[n.rotation||[0,0,0,1]],ss=ext?accessorValues(fin.json,fin.bin,attrs.SCALE):[n.scale||[1,1,1]]
  27. assert.equal(ns.length,ts.length);assert.equal(ns.length,rs.length);assert.equal(ns.length,ss.length)
  28. for(const p of fin.json.meshes[n.mesh].primitives){const g=decodePrimitive(fin.json,fin.bin,p),vertexCount=g.positions.length/3,originalCount=ext?n.extras.sourceVertexCount:vertexCount
  29. if(ext)assert.ok(Math.max(...g.indices)<originalCount,'bound padding must remain unused')
  30. const key=hash(g,fin.json.materials[p.material],originalCount)
  31. const bounds=fin.json.accessors[p.attributes.POSITION]
  32. let center,radius
  33. if(ext){const mi=bounds.min.map((v,a)=>v+Math.min(...ts.map(t=>t[a]))),ma=bounds.max.map((v,a)=>v+Math.max(...ts.map(t=>t[a])));center=mi.map((v,a)=>(v+ma[a])/2);radius=Math.hypot(...mi.map((v,a)=>(ma[a]-v)/2))}
  34. for(let i=0;i<ns.length;i++){
  35. const name=ns[i],ref=refs.get(name),expected=remaining.get(name),t=ts[i],r=rs[i],s=ss[i]
  36. assert.ok(ref&&expected.get(key)>0,name+' changed geometry/material');expected.set(key,expected.get(key)-1);nodes++
  37. for(const [values,target]of [[t,ref.translation||[0,0,0]],[r,ref.rotation||[0,0,0,1]],[s,ref.scale||[1,1,1]]])values.forEach((v,a)=>{maxError=Math.max(maxError,Math.abs(v-target[a]));assert.ok(Math.abs(v-target[a])<0.002)})
  38. if(bm.has(name)){const b=bm.get(name),target=[b.positionENU[0],b.positionENU[2],-b.positionENU[1]];t.forEach((v,a)=>assert.ok(Math.abs(v-target[a])<0.002));assert.ok(Math.abs(r[1]-Math.sin(b.heading/2))<1e-5&&Math.abs(r[3]-Math.cos(b.heading/2))<1e-5)}
  39. if(ext){instances++;const dot=safeNormalDot(g.normals.slice(0,originalCount*3),s);normalDot=Math.min(normalDot,dot);assert.ok(dot>=0.999999)}
  40. assert.ok(s.every(v=>v>0)&&Math.abs(Math.hypot(...r)-1)<1e-5)
  41. for(const index of new Set(g.indices)){const q=transform(g.positions.slice(index*3,index*3+3),t,r,s),enu=[q[0],-q[2],q[1]]
  42. enu.forEach((v,a)=>assert.ok(Number.isFinite(v)&&v>=c.boundsENU.min[a]-0.06&&v<=c.boundsENU.max[a]+0.06,c.id+' transformed bounds'))
  43. if(ext)assert.ok(Math.hypot(...q.map((v,a)=>v-center[a]))<=radius+0.01,'engine command bound excludes indexed vertex')
  44. }
  45. cellTriangles+=g.indices.length/3;cellVertices+=vertexCount
  46. }
  47. }
  48. }
  49. for(const [name,counts]of remaining)assert.ok([...counts.values()].every(v=>v===0),name+' missing primitive')
  50. assert.equal(cellTriangles,c.lod0.triangles);assert.ok(cellTriangles<=20000);assert.ok(cellVertices<=60000)
  51. triangles+=cellTriangles;maxTriangles=Math.max(maxTriangles,cellTriangles);maxVertices=Math.max(maxVertices,cellVertices)
  52. }
  53. for(const c of manifest.fallbackCells)c.buildingIds.forEach(id=>{assert.ok(!seen.has(id));seen.add(id)})
  54. assert.equal(seen.size,plan.buildings.length)
  55. const result={passed:true,sourceCoverage:seen.size,convertedPairs:manifest.cells.length,fallbackCells:manifest.fallbackCells.length,primitivePlacementsChecked:nodes,instancedPrimitivePlacements:instances,
  56. triangles,maxRenderedTriangles:maxTriangles,maxExpandedVertices:maxVertices,maxTrsError:maxError,minShaderNormalDot:normalDot,indexedGeometryAndMaterialsUnchanged:true,
  57. engineCommandBoundsChecked:true,finalTreeFallbackReachabilityValidatedSeparately:true,renderingValidated:false,fpsValidated:false}
  58. if(process.argv[3])writeFileSync(process.argv[3],JSON.stringify(result,null,2));console.log(JSON.stringify(result))