Skip to content

Commit efdd0cc

Browse files
committed
Add @cf-wasm/png dependency and implement texture processing in GLB generation
1 parent fb62365 commit efdd0cc

File tree

4 files changed

+88
-356
lines changed

4 files changed

+88
-356
lines changed

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
"dependencies": {
1414
"@aws-sdk/client-s3": "^3.830.0",
1515
"@cesium/engine": "^15.0.0",
16+
"@cf-wasm/png": "^0.1.24",
1617
"@gltf-transform/core": "^4.1.2",
1718
"@gltf-transform/extensions": "^4.1.2",
1819
"@gltf-transform/functions": "^4.1.2",
1920
"@mapbox/martini": "^0.2.0",
2021
"@types/proj4": "^2.5.6",
2122
"geotiff": "2.1.4-beta.0",
2223
"hono": "^4.6.20",
23-
"pngjs": "^7.0.0",
2424
"proj4": "^2.15.0"
2525
},
2626
"devDependencies": {

apps/api/src/glb.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GeoTIFF, GeoTIFFImage, ReadRasterResult, fromUrl } from 'geotiff';
44
// @ts-ignore - @mapbox/martini doesn't have TypeScript declarations
55
import Martini from '@mapbox/martini';
66
import { Document, NodeIO } from '@gltf-transform/core';
7+
import { encode } from '@cf-wasm/png';
78

89
// Define environment variable types
910
type Bindings = {
@@ -24,6 +25,7 @@ glb.get('/:z/:x/:y.glb', async (c) => {
2425
const y = yWithExt ? yWithExt.replace('.glb', '') : '0';
2526

2627
let elevation = 'swissalti3d/swissalti3d_web_mercator.tif';
28+
let texture = 'swissimage-dop10/swissimage_web_mercator.tif';
2729

2830
const levelNum = parseInt(z);
2931
const xNum = parseInt(x);
@@ -135,6 +137,58 @@ glb.get('/:z/:x/:y.glb', async (c) => {
135137
return c.json({ error: 'No valid elevation data in tile' }, 404);
136138
}
137139

140+
// Load texture GeoTIFF URL
141+
const textureUrl = `${c.env.R2_PUBLIC_ARPENTRY_ENDPOINT}/${texture}`;
142+
143+
// Load and resample texture GeoTIFF with same parameters as elevation
144+
let texturePngBuffer: Uint8Array | null = null;
145+
try {
146+
const textureTiff: GeoTIFF = await fromUrl(textureUrl);
147+
148+
const textureRaster: ReadRasterResult = await textureTiff.readRasters({
149+
bbox: tileBbox,
150+
width: tileSize,
151+
height: tileSize,
152+
fillValue: 0
153+
});
154+
155+
// Convert raster data to PNG using UPNG
156+
if (textureRaster && Array.isArray(textureRaster)) {
157+
const imageData = new Uint8Array(tileSize * tileSize * 4);
158+
const numBands = textureRaster.length;
159+
160+
for (let i = 0; i < tileSize * tileSize; i++) {
161+
const pixelIndex = i * 4;
162+
163+
if (numBands >= 3) {
164+
// RGB or RGBA
165+
const r = textureRaster[0] as TypedArray;
166+
const g = textureRaster[1] as TypedArray;
167+
const b = textureRaster[2] as TypedArray;
168+
169+
imageData[pixelIndex] = Number(r[i]) || 0; // R
170+
imageData[pixelIndex + 1] = Number(g[i]) || 0; // G
171+
imageData[pixelIndex + 2] = Number(b[i]) || 0; // B
172+
imageData[pixelIndex + 3] = 255; // A
173+
} else if (numBands === 1) {
174+
// Grayscale
175+
const gray = textureRaster[0] as TypedArray;
176+
const value = Number(gray[i]) || 0;
177+
178+
imageData[pixelIndex] = value; // R
179+
imageData[pixelIndex + 1] = value; // G
180+
imageData[pixelIndex + 2] = value; // B
181+
imageData[pixelIndex + 3] = 255; // A
182+
}
183+
}
184+
185+
// Create PNG buffer using @cf-wasm/png
186+
texturePngBuffer = encode(imageData, tileSize, tileSize);
187+
}
188+
} catch (error) {
189+
console.warn('Failed to load texture GeoTIFF:', error);
190+
}
191+
138192
const document = new Document();
139193
const buffer = document.createBuffer();
140194

@@ -145,6 +199,7 @@ glb.get('/:z/:x/:y.glb', async (c) => {
145199
const scaleY = tileHeight / tileSize;
146200

147201
const positionArray = [];
202+
const uvArray = [];
148203
for (let i = 0; i < validVertices.length; i += 2) {
149204
const x = validVertices[i];
150205
const y = validVertices[i + 1];
@@ -156,6 +211,9 @@ glb.get('/:z/:x/:y.glb', async (c) => {
156211
const worldY = minY + y * scaleY;
157212

158213
positionArray.push(worldX, z, worldY);
214+
215+
// Create UV coordinates (normalized to 0-1 range)
216+
uvArray.push(x / tileSize, y / tileSize);
159217
}
160218

161219
const positionBuffer = new Float32Array(positionArray);
@@ -164,6 +222,12 @@ glb.get('/:z/:x/:y.glb', async (c) => {
164222
.setArray(positionBuffer)
165223
.setBuffer(buffer);
166224

225+
const uvBuffer = new Float32Array(uvArray);
226+
const uvAccessor = document.createAccessor()
227+
.setType('VEC2')
228+
.setArray(uvBuffer)
229+
.setBuffer(buffer);
230+
167231
const indexBuffer = new Uint16Array(validTriangles);
168232
const indexAccessor = document.createAccessor()
169233
.setType('SCALAR')
@@ -172,8 +236,23 @@ glb.get('/:z/:x/:y.glb', async (c) => {
172236

173237
const primitive = document.createPrimitive()
174238
.setAttribute('POSITION', positionAccessor)
239+
.setAttribute('TEXCOORD_0', uvAccessor)
175240
.setIndices(indexAccessor);
176241

242+
// Create material with resampled texture if available
243+
if (texturePngBuffer) {
244+
const baseColorTexture = document.createTexture("BaseColorTexture")
245+
.setImage(texturePngBuffer)
246+
.setMimeType('image/png');
247+
248+
const material = document.createMaterial()
249+
.setBaseColorTexture(baseColorTexture)
250+
.setBaseColorFactor([1.0, 1.0, 1.0, 1.0])
251+
.setDoubleSided(true);
252+
253+
primitive.setMaterial(material);
254+
}
255+
177256
const mesh = document.createMesh('terrainMesh')
178257
.addPrimitive(primitive);
179258

0 commit comments

Comments
 (0)