| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 |
- // Decode actual source/final primitives, expand instances, verify exact indexed
- // geometry/material/TRS, normal invariance, engine command bounds and budgets.
- import assert from 'node:assert/strict'
- import {readFileSync,writeFileSync} from 'node:fs'
- import {resolve,join} from 'node:path'
- import {createHash} from 'node:crypto'
- import {parseGlb,accessorValues} from './instance-repeated-meshes.mjs'
- import {decodePrimitive,rotate,safeNormalDot} from './instance-safe-primitives.mjs'
- import {unpackInstanceBuffers} from './instance-buffer-codec.mjs'
- import {verifyMaterialClosure} from './verify-material-closure.mjs'
- 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'))
- const bm=new Map(plan.buildings.map(b=>[b.id,b])),seen=new Set()
- let triangles=0,nodes=0,instances=0,normalDot=1,maxTriangles=0,maxVertices=0,maxError=0
- 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')}
- function transform(p,t,r,s){return rotate(p.map((v,a)=>v*s[a]),r).map((v,a)=>v+t[a])}
- for(const c of manifest.cells){
- c.buildingIds.forEach(id=>{assert.ok(bm.has(id)&&!seen.has(id));seen.add(id)})
- const source=parseGlb(readFileSync(join(manifest.source,c.lod0.uri))),fin=unpackInstanceBuffers(readFileSync(join(out,c.lod0.uri)))
- verifyMaterialClosure(fin.json,c.id)
- const refs=new Map(),sourceGeometry=new Map(),remaining=new Map()
- for(const n of source.json.nodes){const ps=source.json.meshes[n.mesh].primitives,expected=new Map()
- 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])})
- keys.forEach(k=>expected.set(k,(expected.get(k)||0)+1));remaining.set(n.name,expected);refs.set(n.name,n)
- }
- let cellTriangles=0,cellVertices=0
- 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]]
- assert.equal(ns.length,ts.length);assert.equal(ns.length,rs.length);assert.equal(ns.length,ss.length)
- 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
- if(ext)assert.ok(Math.max(...g.indices)<originalCount,'bound padding must remain unused')
- const key=hash(g,fin.json.materials[p.material],originalCount)
- const bounds=fin.json.accessors[p.attributes.POSITION]
- let center,radius
- 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))}
- for(let i=0;i<ns.length;i++){
- const name=ns[i],ref=refs.get(name),expected=remaining.get(name),t=ts[i],r=rs[i],s=ss[i]
- assert.ok(ref&&expected.get(key)>0,name+' changed geometry/material');expected.set(key,expected.get(key)-1);nodes++
- 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)})
- 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)}
- if(ext){instances++;const dot=safeNormalDot(g.normals.slice(0,originalCount*3),s);normalDot=Math.min(normalDot,dot);assert.ok(dot>=0.999999)}
- assert.ok(s.every(v=>v>0)&&Math.abs(Math.hypot(...r)-1)<1e-5)
- 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]]
- 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'))
- if(ext)assert.ok(Math.hypot(...q.map((v,a)=>v-center[a]))<=radius+0.01,'engine command bound excludes indexed vertex')
- }
- cellTriangles+=g.indices.length/3;cellVertices+=vertexCount
- }
- }
- }
- for(const [name,counts]of remaining)assert.ok([...counts.values()].every(v=>v===0),name+' missing primitive')
- assert.equal(cellTriangles,c.lod0.triangles);assert.ok(cellTriangles<=20000);assert.ok(cellVertices<=60000)
- triangles+=cellTriangles;maxTriangles=Math.max(maxTriangles,cellTriangles);maxVertices=Math.max(maxVertices,cellVertices)
- }
- for(const c of manifest.fallbackCells)c.buildingIds.forEach(id=>{assert.ok(!seen.has(id));seen.add(id)})
- assert.equal(seen.size,plan.buildings.length)
- const result={passed:true,sourceCoverage:seen.size,convertedPairs:manifest.cells.length,fallbackCells:manifest.fallbackCells.length,primitivePlacementsChecked:nodes,instancedPrimitivePlacements:instances,
- triangles,maxRenderedTriangles:maxTriangles,maxExpandedVertices:maxVertices,maxTrsError:maxError,minShaderNormalDot:normalDot,indexedGeometryAndMaterialsUnchanged:true,
- engineCommandBoundsChecked:true,finalTreeFallbackReachabilityValidatedSeparately:true,renderingValidated:false,fpsValidated:false}
- if(process.argv[3])writeFileSync(process.argv[3],JSON.stringify(result,null,2));console.log(JSON.stringify(result))
|