texture-coarse-roofs.mjs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. // Bounded satellite atlas for existing coarse roof primitives; geometry preserved bitwise.
  2. import assert from 'node:assert/strict'
  3. import {readFileSync,existsSync} from 'node:fs'
  4. import {createHash} from 'node:crypto'
  5. import {PNG} from 'pngjs'
  6. import jpeg from 'jpeg-js'
  7. import {decodePrimitive} from './instance-safe-primitives.mjs'
  8. import {parseGlb,packGlb} from './instance-repeated-meshes.mjs'
  9. import {roofRasterSize,sharpenRoofRgba} from './roof-raster-quality.mjs'
  10. import {enuToGeodetic} from './roof-geodesy.mjs'
  11. const images=new Map(),sources=new Map(),origin=JSON.parse(readFileSync('tools/city-massing/city/cad-bim/instanced-active-input.json')).transform
  12. const mercator=(lon,lat,z)=>[(lon+180)/360*2**z,(1-Math.asinh(Math.tan(lat*Math.PI/180))/Math.PI)/2*2**z]
  13. function getTile(x,y,z){const path=`D:/GIS/GIS/yuanlingPic3/${z}/${x}/${2**z-1-y}.png`;if(images.has(path))return images.get(path)
  14. assert.ok(existsSync(path),'missing roof imagery '+path);const bytes=readFileSync(path),image=bytes[0]===0xff?jpeg.decode(bytes):PNG.sync.read(bytes)
  15. assert.equal(image.width,256);assert.equal(image.height,256)
  16. images.set(path,image);sources.set(path,{path,sha256:createHash('sha256').update(bytes).digest('hex')});if(images.size>128)images.delete(images.keys().next().value);return image}
  17. export function textureCoarseRoofs(bytes,{maxDimension=256,zoom=19,metresPerPixel=null,sharpenAmount=0,jpegQuality=85}={}){
  18. assert.ok(Number.isInteger(jpegQuality)&&jpegQuality>=1&&jpegQuality<=100)
  19. assert.ok(Number.isFinite(sharpenAmount)&&sharpenAmount>=0&&sharpenAmount<=0.5)
  20. const {json,bin}=parseGlb(bytes),t=json.nodes[0].translation||[0,0,0]
  21. assert.equal(json.nodes.length,1);assert.ok(!json.nodes[0].rotation&&!json.nodes[0].scale&&!json.nodes[0].matrix)
  22. const roofs=json.meshes[0].primitives.filter(p=>/^(Far_Roof|Roof_|SatelliteRoof_)/.test(json.materials[p.material].name))
  23. if(!roofs.length){
  24. const ids=[...new Set(json.meshes.flatMap(m=>m.primitives.map(p=>p.material)))],map=new Map(ids.map((v,i)=>[v,i]))
  25. json.materials=ids.map(i=>json.materials[i]);for(const mesh of json.meshes)for(const p of mesh.primitives)p.material=map.get(p.material)
  26. return{bytes:packGlb(json,bin),report:{textured:false}}
  27. }
  28. const geometry=roofs.map(p=>({p,g:decodePrimitive(json,bin,p)})),low=[Infinity,Infinity],high=[-Infinity,-Infinity];let z=0,n=0
  29. for(const {g}of geometry)for(let i=0;i<g.positions.length;i+=3){const p=[g.positions[i]+t[0],-(g.positions[i+2]+t[2])];p.forEach((v,a)=>{low[a]=Math.min(low[a],v);high[a]=Math.max(high[a],v)});z+=g.positions[i+1]+t[1];n++}
  30. z/=n;const span=high.map((v,a)=>v-low[a]),[width,height]=roofRasterSize(span,{maxDimension,metresPerPixel})
  31. assert.ok(span.every(v=>v>0));const atlas=Buffer.alloc(width*height*4),used=new Set()
  32. for(let y=0;y<height;y++)for(let x=0;x<width;x++){
  33. const enu=enuToGeodetic(origin,low[0]+(x+.5)/width*span[0],high[1]-(y+.5)/height*span[1],z)
  34. const pixel=mercator(enu.longitude*180/Math.PI,enu.latitude*180/Math.PI,zoom).map(v=>v*256),tx=Math.floor(pixel[0]/256),ty=Math.floor(pixel[1]/256),image=getTile(tx,ty,zoom)
  35. used.add(`D:/GIS/GIS/yuanlingPic3/${zoom}/${tx}/${2**zoom-1-ty}.png`)
  36. const from=(Math.floor(pixel[1]-ty*256)*256+Math.floor(pixel[0]-tx*256))*4,to=(y*width+x)*4;image.data.copy(atlas,to,from,from+4)
  37. }
  38. const processed=sharpenRoofRgba(atlas,width,height,sharpenAmount)
  39. const imageBytes=jpeg.encode({data:processed,width,height},jpegQuality).data,chunks=[bin];let offset=bin.length
  40. function append(data){const padding=(-offset)&3;if(padding){chunks.push(Buffer.alloc(padding));offset+=padding}const i=json.bufferViews.length;json.bufferViews.push({buffer:0,byteOffset:offset,byteLength:data.length});chunks.push(data);offset+=data.length;return i}
  41. const image=json.images?.length||0;json.images||=[];json.images.push({mimeType:'image/jpeg',bufferView:append(imageBytes)})
  42. const sampler=json.samplers?.length||0;json.samplers||=[];json.samplers.push({wrapS:33071,wrapT:33071,minFilter:9987,magFilter:9729})
  43. const texture=json.textures?.length||0;json.textures||=[];json.textures.push({source:image,sampler})
  44. const material=json.materials.length;json.materials.push({name:'SatelliteRoofAtlas',doubleSided:false,pbrMetallicRoughness:{baseColorFactor:[1,1,1,1],baseColorTexture:{index:texture},metallicFactor:0,roughnessFactor:.9}})
  45. let uvBytes=0
  46. for(const {p,g} of geometry){const values=Buffer.alloc(g.positions.length/3*8);uvBytes+=values.length
  47. for(let i=0;i<g.positions.length/3;i++){values.writeFloatLE((g.positions[i*3]+t[0]-low[0])/span[0],i*8);values.writeFloatLE((high[1]-(-g.positions[i*3+2]-t[2]))/span[1],i*8+4)}
  48. const accessor=json.accessors.length;json.accessors.push({bufferView:append(values),componentType:5126,type:'VEC2',count:g.positions.length/3});p.attributes.TEXCOORD_0=accessor;p.material=material
  49. }
  50. // Prune unused roof-tone materials without changing any primitive geometry.
  51. const ids=[...new Set(json.meshes.flatMap(m=>m.primitives.map(p=>p.material)))],map=new Map(ids.map((v,i)=>[v,i]));json.materials=ids.map(i=>json.materials[i]);for(const mesh of json.meshes)for(const p of mesh.primitives)p.material=map.get(p.material)
  52. json.buffers=[{byteLength:offset}]
  53. let decoded=0,w=width,h=height;while(true){decoded+=w*h*4;if(w===1&&h===1)break;w=Math.max(1,w>>1);h=Math.max(1,h>>1)}
  54. return{bytes:packGlb(json,Buffer.concat(chunks)),report:{textured:true,width,height,boundsENU:[low,high],planeHeight:z,zoom,rasterBytes:imageBytes.length,uvBytes,decodedTextureEstimate:decoded,
  55. processing:{requestedMetresPerPixel:metresPerPixel,actualMetresPerPixel:[span[0]/width,span[1]/height],maxDimension,sharpenAmount,sharpenDeltaLimit:8,jpegQuality,rawRgbaSha256:createHash('sha256').update(atlas).digest('hex'),processedRgbaSha256:createHash('sha256').update(processed).digest('hex')},
  56. sourceTiles:[...used].map(path=>sources.get(path)),rasterSha256:createHash('sha256').update(imageBytes).digest('hex')}}
  57. }