build-region-city.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. // Split verified coarse ownership into bounded independent detail regions.
  2. import assert from 'node:assert/strict'
  3. import {readFileSync,writeFileSync,mkdirSync,existsSync} from 'node:fs'
  4. import {resolve,join,relative} from 'node:path'
  5. import {addRegionMetadata} from './region-metadata.mjs'
  6. import {compressInstanceBuffers} from './instance-buffer-codec.mjs'
  7. import {parseGlb} from './instance-repeated-meshes.mjs'
  8. import {textureCoarseRoofs} from './texture-coarse-roofs.mjs'
  9. import {tileRasterBudget} from './raster-budget.mjs'
  10. const base=resolve('tools/city-massing/city/cad-bim/city-far-shell-v4'),out=resolve(process.argv[2]||'tools/city-massing/city/cad-bim/city-regions-v6')
  11. const withRoofs=process.argv.includes('--satellite-roofs')
  12. assert.ok(!existsSync(out),'use immutable generation')
  13. const tree=JSON.parse(readFileSync(join(base,'tileset.json'))),manifest=JSON.parse(readFileSync(join(base,'manifest.json'))),regions=[],coarse=[],coarseRecords=[],seen=new Set()
  14. mkdirSync(out,{recursive:true})
  15. function fineLeaves(t,list=[]){if(t.content?.uri?.endsWith('-lod0.glb'))list.push(t);for(const c of t.children||[])fineLeaves(c,list);return list}
  16. for(let id=0;id<tree.root.children.length;id++){
  17. const original=tree.root.children[id],leaves=fineLeaves(original)
  18. assert.ok(leaves.length>0&&leaves.length<=128,'region content budget')
  19. let decodedGeometryBytes=0
  20. for(const leaf of leaves){const p=resolve(base,leaf.content.uri);assert.ok(!seen.has(p),'fine repeated across regions');seen.add(p)
  21. const {json,bin}=parseGlb(readFileSync(p))
  22. if(json.images?.length){const images=json.images.map(image=>{const v=json.bufferViews[image.bufferView];return{bytes:bin.subarray(v.byteOffset||0,(v.byteOffset||0)+v.byteLength),mimeType:image.mimeType}});decodedGeometryBytes+=tileRasterBudget(images,json.textures||[]).decodedTextureBytes}
  23. for(const mesh of json.meshes)for(const primitive of mesh.primitives){
  24. for(const i of Object.values(primitive.attributes)){const a=json.accessors[i];decodedGeometryBytes+=a.count*({VEC2:2,VEC3:3,VEC4:4,SCALAR:1}[a.type])*4}
  25. decodedGeometryBytes+=json.accessors[primitive.indices].count*4
  26. }
  27. }
  28. const regionDir=join(out,'region-'+id);mkdirSync(regionDir)
  29. const fineRecords=[]
  30. const fine=leaves.map((t,index)=>{
  31. const path=resolve(base,t.content.uri),raw=readFileSync(path),{json}=parseGlb(raw)
  32. let uri=relative(regionDir,path).replaceAll('\\','/')
  33. if(withRoofs&&!json.images?.length){const result=textureCoarseRoofs(raw,{maxDimension:64}),name=`fine-${index}.glb`;writeFileSync(join(regionDir,name),compressInstanceBuffers(result.bytes));uri=name;decodedGeometryBytes+=(result.report.uvBytes||0)+(result.report.decodedTextureEstimate||0);fineRecords.push({uri:name,sourceUri:t.content.uri,...result.report})}
  34. else fineRecords.push({uri,sourceUri:t.content.uri,textured:false,preservedOriginal:true})
  35. return{...structuredClone(t),content:{uri},geometricError:0}
  36. })
  37. fine.forEach(t=>{delete t.children;delete t.viewerRequestVolume})
  38. const fineMap=new Map(leaves.map((t,i)=>[resolve(base,t.content.uri),fine[i]]))
  39. const previewRecords=[],previewTiles=[]
  40. if(withRoofs){
  41. const branches=original.content?.uri?.startsWith('shell-')?original.children:original.content?[original]:original.children
  42. for(let p=0;p<branches.length;p++){
  43. const branch=branches[p];assert.ok(branch.content)
  44. const raw=readFileSync(resolve(base,branch.content.uri)),{json}=parseGlb(raw)
  45. for(const mesh of json.meshes)for(const primitive of mesh.primitives){for(const i of Object.values(primitive.attributes)){const a=json.accessors[i];decodedGeometryBytes+=a.count*({VEC2:2,VEC3:3,VEC4:4,SCALAR:1}[a.type])*4}decodedGeometryBytes+=json.accessors[primitive.indices].count*4}
  46. const result=textureCoarseRoofs(raw,{maxDimension:128}),name=`preview-${p}.glb`
  47. decodedGeometryBytes+=(result.report.uvBytes||0)+(result.report.decodedTextureEstimate||0)
  48. writeFileSync(join(regionDir,name),compressInstanceBuffers(result.bytes))
  49. previewRecords.push({uri:name,sourceUri:branch.content.uri,...result.report})
  50. const childLeaves=fineLeaves(branch).map(t=>structuredClone(fineMap.get(resolve(base,t.content.uri))))
  51. childLeaves.forEach(t=>{delete t.children;delete t.viewerRequestVolume})
  52. previewTiles.push({boundingVolume:branch.boundingVolume,geometricError:48,refine:'REPLACE',content:{uri:name},children:childLeaves})
  53. }
  54. }
  55. const root={transform:tree.root.transform,boundingVolume:original.boundingVolume,geometricError:1e6,refine:'REPLACE',children:withRoofs?previewTiles:fine}
  56. writeFileSync(join(regionDir,'tileset.json'),JSON.stringify({asset:{version:'1.1'},geometricError:1e6,root}))
  57. const contents=original.content?[original]:original.children
  58. for(let i=0;i<contents.length;i++){
  59. const t=contents[i],uri=`coarse-${id}-${i}.glb`,raw=readFileSync(resolve(base,t.content.uri)),textured=withRoofs?textureCoarseRoofs(raw):{bytes:raw,report:{textured:false}},bytes=compressInstanceBuffers(addRegionMetadata(textured.bytes,id))
  60. writeFileSync(join(out,uri),bytes)
  61. coarse.push({boundingVolume:t.boundingVolume,geometricError:0,content:{uri}})
  62. coarseRecords.push({uri,region:id,sourceUri:t.content.uri,bytes:bytes.length,...textured.report})
  63. }
  64. const info={id,url:`region-${id}/tileset.json`,bounds:original.boundingVolume,contentCount:fine.length+previewRecords.length,decodedGeometryBytes,
  65. previewCount:previewRecords.length,previewRecords,previewContentUrls:previewRecords.map(p=>p.uri),
  66. contentUrls:[...fine.map(t=>t.content.uri),...previewRecords.map(p=>p.uri)],fineRecords,sourceCells:original.content?.uri?.startsWith('shell-')?manifest.records.find(r=>r.uri===original.content.uri).sourceCells:[original.extras.fallbackSourceCell]}
  67. regions.push(info)
  68. }
  69. assert.equal(seen.size,3023)
  70. writeFileSync(join(out,'coarse-tileset.json'),JSON.stringify({asset:{version:'1.1'},geometricError:1e6,root:{...tree.root,children:coarse}}))
  71. const report={transform:tree.root.transform,buildings:manifest.buildings,regions,coarseRecords,regionCount:regions.length,fineFiles:seen.size,source:'city-far-shell-v4',maxRegionContents:Math.max(...regions.map(r=>r.contentCount)),policy:'Coarse always active; bounded independent region fine loads. Metadata.region masks coarse only after all region contents loaded and visible. Pipeline loads zero detail regions.',runtimeValidated:false}
  72. writeFileSync(join(out,'manifest.json'),JSON.stringify(report,null,2));console.log(JSON.stringify({out,regions:regions.length,maxRegionContents:report.maxRegionContents,fineFiles:seen.size,coarseFiles:coarse.length}))