Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 67 additions & 18 deletions packages/deck.gl-geotiff/src/geotiff/render-pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { Photometric, SampleFormat } from "@cogeotiff/core";
import type { RasterModule } from "@developmentseed/deck.gl-raster/gpu-modules";
import {
BlackIsZero,
CMYKToRGB,
Colormap,
CreateTexture,
cieLabToRGB,
FilterNoDataVal,
MaskTexture,
WhiteIsZero,
} from "@developmentseed/deck.gl-raster/gpu-modules";
import type { GeoTIFF, Overview } from "@developmentseed/geotiff";

import type {
GeoTIFF,
Overview,
RasterTypedArray,
} from "@developmentseed/geotiff";
import { parseColormap } from "@developmentseed/geotiff";
import type { Device, SamplerProps, Texture } from "@luma.gl/core";
import type { GetTileDataOptions } from "../cog-layer";
Expand Down Expand Up @@ -63,7 +70,7 @@ export function inferRenderPipeline(
}

throw new Error(
`Inferring render pipeline for non-unsigned integers not yet supported. Found SampleFormat: ${SampleFormat}`,
`Inferring render pipeline for non-unsigned integers not yet supported. Found SampleFormat: ${sampleFormat}`,
);
}

Expand Down Expand Up @@ -99,8 +106,9 @@ function createUnormPipeline(
];

if (nodata !== null) {
// Since values are 0-1 for unorm textures,
const noDataScaled = nodata / 255.0;
// Since values are 0-1 for unorm textures, scale nodata to [0, 1]
const maxVal = 2 ** bitsPerSample[0]! - 1;
const noDataScaled = nodata / maxVal;

renderPipeline.push({
module: FilterNoDataVal,
Expand Down Expand Up @@ -171,8 +179,13 @@ function createUnormPipeline(
sampleFormat,
);
const bytesPerPixel = (bitsPerSample[0]! / 8) * numSamples;
const textureData = enforceAlignment(array.data, {
width,
height,
bytesPerPixel,
});
const texture = device.createTexture({
data: padToAlignment(array.data, width, height, bytesPerPixel),
data: textureData,
format: textureFormat,
width,
height,
Expand Down Expand Up @@ -216,6 +229,16 @@ function photometricInterpretationToRGB(
ColorMap?: Uint16Array,
): RasterModule | null {
switch (photometric) {
case Photometric.MinIsWhite: {
return {
module: WhiteIsZero,
};
}
case Photometric.MinIsBlack: {
return {
module: BlackIsZero,
};
}
case Photometric.Rgb:
return null;
case Photometric.Palette: {
Expand Down Expand Up @@ -259,6 +282,7 @@ function photometricInterpretationToRGB(
return {
module: cieLabToRGB,
};

default:
throw new Error(`Unsupported PhotometricInterpretation ${photometric}`);
}
Expand Down Expand Up @@ -290,30 +314,55 @@ function resolveModule<T>(m: UnresolvedRasterModule<T>, data: T): RasterModule {
* WebGL's default `UNPACK_ALIGNMENT` is 4, meaning each row of pixel data must
* start on a 4-byte boundary.
*
* For textures with widths not divisible by 4, we need to pad each row to the
* next multiple of 4 bytes so WebGL doesn't reject the buffer as "too small".
* For all array types, we must match our typed array type to what WebGL
* expects, so this must return the same array type as what was passed in.
*/
function enforceAlignment<T extends RasterTypedArray>(
data: T,
{
width,
height,
bytesPerPixel,
}: { width: number; height: number; bytesPerPixel: number },
): T {
return data instanceof Uint8Array ||
data instanceof Int8Array ||
data instanceof Uint16Array ||
data instanceof Int16Array
? padToAlignment(data, width, height, bytesPerPixel)
: data;
}

/**
* WebGL's default `UNPACK_ALIGNMENT` is 4, meaning each row of pixel data must
* start on a 4-byte boundary.
*
* For 8-bit and 16-bit data, rows may not be 4-byte aligned. For 32-bit+ data,
* each element is already 4 bytes so rows are always aligned.
*
* Returns the original array unchanged when no padding is needed.
*/
function padToAlignment(
data: ArrayBufferView,
width: number,
height: number,
bytesPerPixel: number,
): Uint8Array {
const src = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
function padToAlignment<
T extends Uint8Array | Int8Array | Uint16Array | Int16Array,
>(data: T, width: number, height: number, bytesPerPixel: number): T {
const rowBytes = width * bytesPerPixel;
const alignedRowBytes = Math.ceil(rowBytes / 4) * 4;
if (alignedRowBytes === rowBytes) {
return src;
return data;
}

const dst = new Uint8Array(alignedRowBytes * height);
const src = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
const dstBytes = new Uint8Array(alignedRowBytes * height);
for (let r = 0; r < height; r++) {
dst.set(
dstBytes.set(
src.subarray(r * rowBytes, (r + 1) * rowBytes),
r * alignedRowBytes,
);
}
return dst;

// Return the same typed array type as the input
if (data instanceof Int8Array) return new Int8Array(dstBytes.buffer) as T;
if (data instanceof Uint16Array) return new Uint16Array(dstBytes.buffer) as T;
if (data instanceof Int16Array) return new Int16Array(dstBytes.buffer) as T;
return dstBytes as T;
}
21 changes: 21 additions & 0 deletions packages/deck.gl-raster/src/gpu-modules/color/black-is-zero.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ShaderModule } from "@luma.gl/shadertools";

const shader = /* glsl */ `
vec3 black_zero_to_rgb(float value) {
return vec3(value, value, value);
}
`;

/**
* A shader module that injects a unorm texture and uses a sampler2D to assign
* to a color.
*/
export const BlackIsZero = {
name: "black-is-zero",
inject: {
"fs:#decl": shader,
"fs:DECKGL_FILTER_COLOR": /* glsl */ `
color.rgb = black_zero_to_rgb(color.r);
`,
},
} as const satisfies ShaderModule;
2 changes: 2 additions & 0 deletions packages/deck.gl-raster/src/gpu-modules/color/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { BlackIsZero } from "./black-is-zero";
export { cieLabToRGB } from "./cielab";
export { CMYKToRGB } from "./cmyk";
export { WhiteIsZero } from "./white-is-zero";
export { YCbCrToRGB } from "./ycbcr";
21 changes: 21 additions & 0 deletions packages/deck.gl-raster/src/gpu-modules/color/white-is-zero.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ShaderModule } from "@luma.gl/shadertools";

const shader = /* glsl */ `
vec3 white_zero_to_rgb(float value) {
return vec3(1.0 - value, 1.0 - value, 1.0 - value);
}
`;

/**
* A shader module that injects a unorm texture and uses a sampler2D to assign
* to a color.
*/
export const WhiteIsZero = {
name: "white-is-zero",
inject: {
"fs:#decl": shader,
"fs:DECKGL_FILTER_COLOR": /* glsl */ `
color.rgb = white_zero_to_rgb(color.r);
`,
},
} as const satisfies ShaderModule;
8 changes: 7 additions & 1 deletion packages/deck.gl-raster/src/gpu-modules/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { CMYKToRGB, cieLabToRGB, YCbCrToRGB } from "./color";
export {
BlackIsZero,
CMYKToRGB,
cieLabToRGB,
WhiteIsZero,
YCbCrToRGB,
} from "./color";
export { Colormap } from "./colormap";
export { CreateTexture } from "./create-texture";
export { FilterNoDataVal } from "./filter-nodata";
Expand Down
2 changes: 1 addition & 1 deletion packages/geotiff/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { RasterArray } from "./array.js";
export type { RasterArray, RasterTypedArray } from "./array.js";
export { parseColormap } from "./colormap.js";
export type { ProjJson } from "./crs.js";
export type { DecodedPixels, Decoder, DecoderMetadata } from "./decode.js";
Expand Down
Loading