Skip to content

Commit d1efd40

Browse files
committed
Add multi-band RGB pass-through for true-color COGs (pulls multi-band follow-up forward)
Band-count branch in the tile loader: single-band -> r32float + colormap (as before); multi-band uint -> rgba8unorm displayed directly via CreateTexture, no colormap/rescale. Adds FilterBlack to discard black nodata edges (Sentinel UTM tile corners) -> transparent. Verified live: Sentinel-2 TCI renders in true color client-side with correct zoom refinement across its 5-level overview pyramid. Assumes 8-bit multi-band samples.
1 parent 29dd8d3 commit d1efd40

1 file changed

Lines changed: 80 additions & 33 deletions

File tree

src/essence/Basics/MapEngines/Adapters/DeckCOGLayer.ts

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*/
2020

2121
import type { Layer, UpdateParameters } from '@deck.gl/core'
22-
import { COGLayer } from '@developmentseed/deck.gl-geotiff'
22+
import { COGLayer, addAlphaChannel } from '@developmentseed/deck.gl-geotiff'
2323
// `moduleResolution: "node"` in tsconfig.json does not read package exports
2424
// maps, so tsc cannot resolve the `./gpu-modules` subpath export. The import
2525
// below works correctly at runtime (Node.js, Playwright, Webpack all honour
@@ -107,57 +107,78 @@ function lutToImageData(lut: Uint8ClampedArray): ImageData {
107107
return new ImageData(lut, 256, 1)
108108
}
109109

110+
type TileData = {
111+
texture: any
112+
/** 'single' → float value in color.r (colormap path); 'rgb' → RGBA displayed directly. */
113+
mode: 'single' | 'rgb'
114+
width: number
115+
height: number
116+
byteLength: number
117+
} | null
118+
110119
/**
111-
* Custom tile loader for single-band COGs. Uploads the band as a single-channel
112-
* `r32float` GPU texture so the RAW pixel value lands in `color.r` for the
113-
* downstream LinearRescale + Colormap modules.
120+
* Custom tile loader for COGs. Two paths, chosen by band count:
114121
*
115-
* This deliberately replaces `@developmentseed/deck.gl-geotiff`'s default
116-
* `inferRenderPipeline`, whose auto-inference currently only supports unsigned
117-
* integer COGs (it throws `non-unsigned integers not yet supported` for float,
118-
* SampleFormat 3 — the common case for scientific single-band rasters). By
119-
* supplying our own `getTileData` + `renderTile`, `COGLayer._parseGeoTIFF`
120-
* skips that inference entirely.
122+
* - **Single-band** (the primary case): upload the band as a single-channel
123+
* `r32float` texture so the RAW value lands in `color.r` for the downstream
124+
* FilterNaN + LinearRescale + Colormap modules. Nearest sampling (float
125+
* linear filtering is not guaranteed in WebGL2).
126+
* - **Multi-band uint (RGB/RGBA)**: upload as `rgba8unorm` and display the
127+
* colour directly via CreateTexture (no colormap/rescale). This is the
128+
* multi-band case #158 defers — supported here as a passthrough so true-colour
129+
* COGs (e.g. Sentinel-2 TCI) render client-side. Assumes 8-bit samples.
121130
*
122-
* Values of any numeric type are converted to Float32 and uploaded as
123-
* `r32float`, so a single rescale (in raw data units) is correct for float and
124-
* integer COGs alike. Sampled with nearest filtering (float-linear filtering is
125-
* not guaranteed in WebGL2).
131+
* Supplying this `getTileData` (+ a `renderTile`) makes `COGLayer._parseGeoTIFF`
132+
* skip its default `inferRenderPipeline`, whose auto-inference throws for float
133+
* COGs (`non-unsigned integers not yet supported`, SampleFormat 3).
126134
*
127135
* @param image A `GeoTIFF` (full-res) or `Overview`, selected per-zoom by
128-
* COGLayer's `_getTileDataCallback` wrapper.
136+
* COGLayer's `_getTileDataCallback` wrapper — this is what makes
137+
* higher zooms load finer overviews.
129138
* @param options `{ device, x, y, signal, pool }` supplied by the wrapper.
130139
*/
131140
async function cogGetTileData(
132141
image: any,
133142
options: { device: any; x: number; y: number; signal?: AbortSignal; pool?: any }
134-
): Promise<{ texture: any; width: number; height: number; byteLength: number } | null> {
143+
): Promise<TileData> {
135144
const { device, x, y, signal, pool } = options
136-
const tile = await image.fetchTile(x, y, { boundless: false, pool, signal })
137-
const array = tile?.array
145+
let array = (await image.fetchTile(x, y, { boundless: false, pool, signal }))?.array
138146
if (!array) return null
139147
const width = array.width
140148
const height = array.height
141-
// Single-band: prefer flat `data`; fall back to first band for band-separate.
142-
let src = array.data
143-
if (array.layout === 'band-separate' && array.bands && array.bands.length) {
144-
src = array.bands[0]
149+
const samples =
150+
(array.bands && array.bands.length) ||
151+
Math.max(1, Math.round(array.data.length / (width * height)))
152+
153+
if (samples === 1) {
154+
const src =
155+
array.layout === 'band-separate' && array.bands && array.bands.length
156+
? array.bands[0]
157+
: array.data
158+
const f32 = src instanceof Float32Array ? src : Float32Array.from(src)
159+
const texture = device.createTexture({
160+
data: f32,
161+
format: 'r32float',
162+
width,
163+
height,
164+
mipmaps: false,
165+
sampler: { minFilter: 'nearest', magFilter: 'nearest', addressModeU: 'clamp-to-edge', addressModeV: 'clamp-to-edge' },
166+
})
167+
return { texture, mode: 'single', width, height, byteLength: f32.byteLength }
145168
}
146-
const f32 = src instanceof Float32Array ? src : Float32Array.from(src)
169+
170+
// Multi-band: WebGL2 has no RGB-only format, so pad RGB → RGBA.
171+
if (samples === 3) array = addAlphaChannel(array)
172+
const u8 = array.data instanceof Uint8Array ? array.data : Uint8Array.from(array.data)
147173
const texture = device.createTexture({
148-
data: f32,
149-
format: 'r32float',
174+
data: u8,
175+
format: 'rgba8unorm',
150176
width,
151177
height,
152178
mipmaps: false,
153-
sampler: {
154-
minFilter: 'nearest',
155-
magFilter: 'nearest',
156-
addressModeU: 'clamp-to-edge',
157-
addressModeV: 'clamp-to-edge',
158-
},
179+
sampler: { minFilter: 'linear', magFilter: 'linear', addressModeU: 'clamp-to-edge', addressModeV: 'clamp-to-edge' },
159180
})
160-
return { texture, width, height, byteLength: f32.byteLength }
181+
return { texture, mode: 'rgb', width, height, byteLength: u8.byteLength }
161182
}
162183

163184
/** Truthy placeholder so COGLayer._parseGeoTIFF skips the (float-unsupported)
@@ -182,6 +203,22 @@ const FilterNaN = {
182203
},
183204
}
184205

206+
/**
207+
* GPU module for the multi-band RGB path: discards fully-black pixels. Satellite
208+
* true-colour COGs (e.g. Sentinel-2 TCI) encode nodata as exact black (0,0,0) —
209+
* most visibly the rotated UTM tile edges — so this leaves them transparent
210+
* rather than painting black over the basemap. Heuristic: also discards any
211+
* legitimately pure-black pixel (rare in true-colour imagery).
212+
*/
213+
const FilterBlack = {
214+
name: 'filter-black',
215+
inject: {
216+
'fs:DECKGL_FILTER_COLOR': `
217+
if (color.r == 0.0 && color.g == 0.0 && color.b == 0.0) { discard; }
218+
`,
219+
},
220+
}
221+
185222
// ---------------------------------------------------------------------------
186223
// ColormappedCOGLayer
187224
// ---------------------------------------------------------------------------
@@ -264,11 +301,21 @@ export class ColormappedCOGLayer extends COGLayer<any> {
264301

265302
return (data: any) => {
266303
if (!data || !data.texture) return null
304+
// Multi-band RGB: display the colour directly (no colormap/rescale);
305+
// discard black nodata edges → transparent.
306+
if (data.mode === 'rgb') {
307+
return {
308+
renderPipeline: [
309+
{ module: CreateTexture, props: { textureName: data.texture } },
310+
{ module: FilterBlack },
311+
],
312+
}
313+
}
314+
// Single-band: raw value in color.r → discard NaN → rescale → colormap.
267315
return composeColormapPipeline(
268316
{
269317
renderPipeline: [
270318
{ module: CreateTexture, props: { textureName: data.texture } },
271-
// Discard NaN nodata (oceans/fill) → transparent.
272319
{ module: FilterNaN },
273320
],
274321
},

0 commit comments

Comments
 (0)