| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657 |
- // Bounded satellite atlas for existing coarse roof primitives; geometry preserved bitwise.
- import assert from 'node:assert/strict'
- import {readFileSync,existsSync} from 'node:fs'
- import {createHash} from 'node:crypto'
- import {PNG} from 'pngjs'
- import jpeg from 'jpeg-js'
- import {decodePrimitive} from './instance-safe-primitives.mjs'
- import {parseGlb,packGlb} from './instance-repeated-meshes.mjs'
- import {roofRasterSize,sharpenRoofRgba} from './roof-raster-quality.mjs'
- import {enuToGeodetic} from './roof-geodesy.mjs'
- const images=new Map(),sources=new Map(),origin=JSON.parse(readFileSync('tools/city-massing/city/cad-bim/instanced-active-input.json')).transform
- const mercator=(lon,lat,z)=>[(lon+180)/360*2**z,(1-Math.asinh(Math.tan(lat*Math.PI/180))/Math.PI)/2*2**z]
- 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)
- assert.ok(existsSync(path),'missing roof imagery '+path);const bytes=readFileSync(path),image=bytes[0]===0xff?jpeg.decode(bytes):PNG.sync.read(bytes)
- assert.equal(image.width,256);assert.equal(image.height,256)
- 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}
- export function textureCoarseRoofs(bytes,{maxDimension=256,zoom=19,metresPerPixel=null,sharpenAmount=0,jpegQuality=85}={}){
- assert.ok(Number.isInteger(jpegQuality)&&jpegQuality>=1&&jpegQuality<=100)
- assert.ok(Number.isFinite(sharpenAmount)&&sharpenAmount>=0&&sharpenAmount<=0.5)
- const {json,bin}=parseGlb(bytes),t=json.nodes[0].translation||[0,0,0]
- assert.equal(json.nodes.length,1);assert.ok(!json.nodes[0].rotation&&!json.nodes[0].scale&&!json.nodes[0].matrix)
- const roofs=json.meshes[0].primitives.filter(p=>/^(Far_Roof|Roof_|SatelliteRoof_)/.test(json.materials[p.material].name))
- if(!roofs.length){
- 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)
- return{bytes:packGlb(json,bin),report:{textured:false}}
- }
- const geometry=roofs.map(p=>({p,g:decodePrimitive(json,bin,p)})),low=[Infinity,Infinity],high=[-Infinity,-Infinity];let z=0,n=0
- 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++}
- z/=n;const span=high.map((v,a)=>v-low[a]),[width,height]=roofRasterSize(span,{maxDimension,metresPerPixel})
- assert.ok(span.every(v=>v>0));const atlas=Buffer.alloc(width*height*4),used=new Set()
- for(let y=0;y<height;y++)for(let x=0;x<width;x++){
- const enu=enuToGeodetic(origin,low[0]+(x+.5)/width*span[0],high[1]-(y+.5)/height*span[1],z)
- 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)
- used.add(`D:/GIS/GIS/yuanlingPic3/${zoom}/${tx}/${2**zoom-1-ty}.png`)
- 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)
- }
- const processed=sharpenRoofRgba(atlas,width,height,sharpenAmount)
- const imageBytes=jpeg.encode({data:processed,width,height},jpegQuality).data,chunks=[bin];let offset=bin.length
- 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}
- const image=json.images?.length||0;json.images||=[];json.images.push({mimeType:'image/jpeg',bufferView:append(imageBytes)})
- const sampler=json.samplers?.length||0;json.samplers||=[];json.samplers.push({wrapS:33071,wrapT:33071,minFilter:9987,magFilter:9729})
- const texture=json.textures?.length||0;json.textures||=[];json.textures.push({source:image,sampler})
- 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}})
- let uvBytes=0
- for(const {p,g} of geometry){const values=Buffer.alloc(g.positions.length/3*8);uvBytes+=values.length
- 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)}
- 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
- }
- // Prune unused roof-tone materials without changing any primitive geometry.
- 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)
- json.buffers=[{byteLength:offset}]
- 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)}
- return{bytes:packGlb(json,Buffer.concat(chunks)),report:{textured:true,width,height,boundsENU:[low,high],planeHeight:z,zoom,rasterBytes:imageBytes.length,uvBytes,decodedTextureEstimate:decoded,
- 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')},
- sourceTiles:[...used].map(path=>sources.get(path)),rasterSha256:createHash('sha256').update(imageBytes).digest('hex')}}
- }
|