diff --git a/examples/cog-basic/src/App.tsx b/examples/cog-basic/src/App.tsx index 3f88539..f6d1ba7 100644 --- a/examples/cog-basic/src/App.tsx +++ b/examples/cog-basic/src/App.tsx @@ -1,6 +1,9 @@ import type { DeckProps } from "@deck.gl/core"; import { MapboxOverlay } from "@deck.gl/mapbox"; -import { COGLayer, proj } from "@developmentseed/deck.gl-geotiff"; +import { COGLayer, loadRgbImage, proj } from "@developmentseed/deck.gl-geotiff"; +import { CreateTexture } from "@developmentseed/deck.gl-raster"; +import { Device, Texture } from "@luma.gl/core"; +import type { GeoTIFFImage } from "geotiff"; import { Pool } from "geotiff"; import { toProj4 } from "geotiff-geokeys-to-proj4"; import "maplibre-gl/dist/maplibre-gl.css"; @@ -35,6 +38,54 @@ async function geoKeysParser( const COG_URL = "https://ds-wheels.s3.us-east-1.amazonaws.com/m_4007307_sw_18_060_20220803.tif"; +type DataT = { + texture: Texture; + height: number; + width: number; +}; + +async function getTileData( + image: GeoTIFFImage, + options: { + device: Device; + window: [number, number, number, number]; + signal?: AbortSignal; + pool: Pool; + }, +): Promise { + const { device } = options; + const { texture: data, height, width } = await loadRgbImage(image, options); + + // Note: if we set this format to r8unorm it'll only fill the red channel of + // the texture, making it red. + const texture = device.createTexture({ + format: "rgba8unorm", + dimension: "2d", + width, + height, + data, + }); + + return { + texture, + height, + width, + }; +} + +function renderTile(data: DataT) { + const { texture } = data; + + return [ + { + module: CreateTexture, + props: { + textureName: texture, + }, + }, + ]; +} + export default function App() { const mapRef = useRef(null); const [debug, setDebug] = useState(false); @@ -51,6 +102,8 @@ export default function App() { debugOpacity, geoKeysParser, pool, + getTileData, + renderTile, onGeoTIFFLoad: (_tiff, options) => { const { west, south, east, north } = options.geographicBounds; mapRef.current?.fitBounds( diff --git a/examples/land-cover/src/App.tsx b/examples/land-cover/src/App.tsx index a64c986..2f206d9 100644 --- a/examples/land-cover/src/App.tsx +++ b/examples/land-cover/src/App.tsx @@ -1,7 +1,6 @@ import type { DeckProps } from "@deck.gl/core"; import { MapboxOverlay } from "@deck.gl/mapbox"; import type { Device, Texture } from "@luma.gl/core"; -import { ShaderModule } from "@luma.gl/shadertools"; import { COGLayer, parseColormap, @@ -12,7 +11,12 @@ import type { GeoTIFFImage, TypedArrayArrayWithDimensions, } from "geotiff"; -import { RasterLayerProps } from "@developmentseed/deck.gl-raster"; +import { + Colormap, + CreateTexture, + FilterNoDataVal, + RasterModule, +} from "@developmentseed/deck.gl-raster"; import { fromUrl, Pool } from "geotiff"; import { toProj4 } from "geotiff-geokeys-to-proj4"; import "maplibre-gl/dist/maplibre-gl.css"; @@ -88,14 +92,6 @@ async function getCogBounds( const COG_URL = "https://ds-wheels.s3.us-east-1.amazonaws.com/Annual_NLCD_LndCov_2023_CU_C1V0.tif"; -export type LandCoverProps = { - colormap_texture: Texture; -}; - -const landCoverModule = { - name: "landCover", -} as const satisfies ShaderModule; - async function getTileData( image: GeoTIFFImage, options: { @@ -142,10 +138,7 @@ type TileDataT = { function renderTile( tileData: TileDataT, colormapTexture: Texture, -): { - texture: RasterLayerProps["texture"]; - shaders?: RasterLayerProps["shaders"]; -} { +): RasterModule[] { const { texture } = tileData; // Hard coded NoData value but this ideally would be fetched from COG metadata @@ -153,33 +146,26 @@ function renderTile( // Since values are 0-1 for unorm textures, const noDataScaled = nodataVal / 255.0; - return { - texture, - // For colormap rendering: - shaders: { - inject: { - "fs:#decl": ` - uniform sampler2D colormap_texture; - `, - "fs:DECKGL_FILTER_COLOR": ` - float value = color.r; - vec3 pickingval = vec3(value, 0., 0.); - if (value == ${noDataScaled}) { - discard; - } else { - vec4 color_val = texture(colormap_texture, vec2(value, 0.)); - color = color_val; - } - `, + return [ + { + module: CreateTexture, + props: { + textureName: texture, }, - modules: [landCoverModule], - shaderProps: { - landCover: { - colormap_texture: colormapTexture, - }, + }, + { + module: FilterNoDataVal, + props: { + value: noDataScaled, }, }, - }; + { + module: Colormap, + props: { + colormapTexture: colormapTexture, + }, + }, + ]; } export default function App() { diff --git a/packages/deck.gl-geotiff/src/cog-layer.ts b/packages/deck.gl-geotiff/src/cog-layer.ts index 0980b51..ea31df4 100644 --- a/packages/deck.gl-geotiff/src/cog-layer.ts +++ b/packages/deck.gl-geotiff/src/cog-layer.ts @@ -13,13 +13,13 @@ import { } from "@deck.gl/geo-layers"; import { PathLayer } from "@deck.gl/layers"; import type { - RasterLayerProps, + RasterModule, TileMatrix, TileMatrixSet, } from "@developmentseed/deck.gl-raster"; import { RasterLayer, RasterTileset2D } from "@developmentseed/deck.gl-raster"; import type { ReprojectionFns } from "@developmentseed/raster-reproject"; -import type { Device, Texture } from "@luma.gl/core"; +import type { Device } from "@luma.gl/core"; import type { BaseClient, GeoTIFF, GeoTIFFImage, Pool } from "geotiff"; import proj4 from "proj4"; import { parseCOGTileMatrixSet } from "./cog-tile-matrix-set.js"; @@ -48,7 +48,7 @@ export type MinimalDataT = { }; export type DefaultDataT = MinimalDataT & { - texture: ImageData | Texture; + texture: ImageData; }; export type GetTileTextureOptions = { @@ -126,10 +126,7 @@ export interface COGLayerProps< * The default implementation returns an object with a `texture` property, * assuming that this texture is already renderable. */ - renderTile: (data: DataT) => { - texture: RasterLayerProps["texture"]; - shaders?: RasterLayerProps["shaders"]; - }; + renderTile: (data: DataT) => ImageData | RasterModule[]; /** * Enable debug visualization showing the triangulation mesh @@ -172,7 +169,7 @@ const defaultProps: Partial = { geoKeysParser: epsgIoGeoKeyParser, getTileData: loadRgbImage, renderTile: (data) => { - return { texture: data.texture }; + return data.texture; }, }; @@ -314,15 +311,13 @@ export class COGLayer< if (data) { const { height, width } = data; - const { texture, shaders } = this.props.renderTile(data); layers.push( new RasterLayer({ id: `${props.id}-raster`, width, height, - texture, - shaders, + renderPipeline: this.props.renderTile(data), maxError, reprojectionFns: { forwardTransform, diff --git a/packages/deck.gl-raster/src/index.ts b/packages/deck.gl-raster/src/index.ts index 2de410b..bc78434 100644 --- a/packages/deck.gl-raster/src/index.ts +++ b/packages/deck.gl-raster/src/index.ts @@ -6,6 +6,8 @@ export type { TileMatrixSet, TileMatrixSetBoundingBox, } from "./raster-tileset/types.js"; +export { Colormap, CreateTexture, FilterNoDataVal } from "./webgl/index.js"; +export type { RasterModule } from "./webgl/types.js"; import { __TEST_EXPORTS as traversalTestExports } from "./raster-tileset/raster-tile-traversal.js"; diff --git a/packages/deck.gl-raster/src/mesh-layer/mesh-layer-fragment.glsl.ts b/packages/deck.gl-raster/src/mesh-layer/mesh-layer-fragment.glsl.ts new file mode 100644 index 0000000..d79060c --- /dev/null +++ b/packages/deck.gl-raster/src/mesh-layer/mesh-layer-fragment.glsl.ts @@ -0,0 +1,41 @@ +/** + * This is a vendored copy of the SimpleMeshLayer's fragment shader: + * https://github.com/visgl/deck.gl/blob/a15c8cea047993c8a861bf542835c1988f30165c/modules/mesh-layers/src/simple-mesh-layer/simple-mesh-layer-fragment.glsl.ts + * under the MIT license. + * + * We edited this to remove the hard-coded texture uniform because we want to + * support integer and signed integer textures, not only normalized unsigned + * textures. + */ +export default /* glsl */ `#version 300 es +#define SHADER_NAME simple-mesh-layer-fs + +precision highp float; + +in vec2 vTexCoord; +in vec3 cameraPosition; +in vec3 normals_commonspace; +in vec4 position_commonspace; +in vec4 vColor; + +out vec4 fragColor; + +void main(void) { + geometry.uv = vTexCoord; + + vec3 normal; + if (simpleMesh.flatShading) { + + normal = normalize(cross(dFdx(position_commonspace.xyz), dFdy(position_commonspace.xyz))); + } else { + normal = normals_commonspace; + } + + // We initialize color here before passing into DECKGL_FILTER_COLOR + vec4 color; + DECKGL_FILTER_COLOR(color, geometry); + + vec3 lightColor = lighting_getLightColor(color.rgb, cameraPosition, position_commonspace.xyz, normal); + fragColor = vec4(lightColor, color.a * layer.opacity); +} +`; diff --git a/packages/deck.gl-raster/src/mesh-layer.ts b/packages/deck.gl-raster/src/mesh-layer/mesh-layer.ts similarity index 55% rename from packages/deck.gl-raster/src/mesh-layer.ts rename to packages/deck.gl-raster/src/mesh-layer/mesh-layer.ts index 0467ffb..aa5ad85 100644 --- a/packages/deck.gl-raster/src/mesh-layer.ts +++ b/packages/deck.gl-raster/src/mesh-layer/mesh-layer.ts @@ -1,17 +1,12 @@ import type { SimpleMeshLayerProps } from "@deck.gl/mesh-layers"; import { SimpleMeshLayer } from "@deck.gl/mesh-layers"; +import fs from "./mesh-layer-fragment.glsl.js"; import type { ShaderModule } from "@luma.gl/shadertools"; +import type { RasterModule } from "../webgl/types.js"; export interface MeshTextureLayerProps extends SimpleMeshLayerProps { - shaders?: { - inject?: { - "fs:#decl"?: string; - "fs:DECKGL_FILTER_COLOR"?: string; - }; - modules?: ShaderModule[]; - shaderProps?: { [x: string]: Partial | undefined> }; - }; + renderPipeline: RasterModule[]; } /** @@ -31,24 +26,30 @@ export class MeshTextureLayer extends SimpleMeshLayer< override getShaders() { const upstreamShaders = super.getShaders(); + const modules: ShaderModule[] = upstreamShaders.modules; + for (const m of this.props.renderPipeline) { + modules.push(m.module); + } + return { ...upstreamShaders, - inject: { - ...upstreamShaders.inject, - ...this.props.shaders?.inject, - }, - modules: [ - ...upstreamShaders.modules, - ...(this.props.shaders?.modules || []), - ], + // Override upstream's fragment shader with our copy with modified + // injection points + fs, + modules, }; } override draw(opts: any): void { - if (this.props.shaders?.shaderProps) - for (const m of super.getModels()) { - m.shaderInputs.setProps(this.props.shaders.shaderProps); - } + const shaderProps: { [x: string]: Partial> } = {}; + for (const m of this.props.renderPipeline) { + // Props should be keyed by module name + shaderProps[m.module.name] = m.props; + } + + for (const m of super.getModels()) { + m.shaderInputs.setProps(shaderProps); + } super.draw(opts); } diff --git a/packages/deck.gl-raster/src/raster-layer.ts b/packages/deck.gl-raster/src/raster-layer.ts index 841963c..dafc89d 100644 --- a/packages/deck.gl-raster/src/raster-layer.ts +++ b/packages/deck.gl-raster/src/raster-layer.ts @@ -5,10 +5,11 @@ import type { } from "@deck.gl/core"; import { CompositeLayer } from "@deck.gl/core"; import { PolygonLayer } from "@deck.gl/layers"; -import type { SimpleMeshLayerProps } from "@deck.gl/mesh-layers"; import type { ReprojectionFns } from "@developmentseed/raster-reproject"; import { RasterReprojector } from "@developmentseed/raster-reproject"; -import { MeshTextureLayer, MeshTextureLayerProps } from "./mesh-layer"; +import { MeshTextureLayer } from "./mesh-layer/mesh-layer"; +import { RasterModule } from "./webgl/types"; +import { CreateTexture } from "./webgl/create-texture"; const DEFAULT_MAX_ERROR = 0.125; @@ -59,18 +60,14 @@ export interface RasterLayerProps extends CompositeLayerProps { reprojectionFns: ReprojectionFns; /** - * Texture to apply to the mesh. Can be: - * - URL or Data URL string - * - WebGL2-compatible pixel source (Image, ImageData, Canvas, etc.) - * - Luma.gl Texture instance - * - Plain object: {width, height, data} + * Render pipeline for visualizing textures. + * + * Can be: + * + * - ImageData representing RGBA pixel data + * - Sequence of shader modules to be composed into a shader program */ - texture?: SimpleMeshLayerProps["texture"]; - - /** - * Optional shader injection. - */ - shaders?: MeshTextureLayerProps["shaders"]; + renderPipeline: ImageData | RasterModule[]; /** * Maximum reprojection error in pixels for mesh refinement. @@ -224,9 +221,32 @@ export class RasterLayer extends CompositeLayer { ); } + /** Create assembled render pipeline from the renderPipeline prop input. */ + _createRenderPipeline(): RasterModule[] { + if (this.props.renderPipeline instanceof ImageData) { + const imageData = this.props.renderPipeline; + const texture = this.context.device.createTexture({ + format: "rgba8unorm", + dimension: "2d", + width: imageData.width, + height: imageData.height, + data: imageData.data, + }); + const wrapper: RasterModule = { + module: CreateTexture, + props: { + textureName: texture, + }, + }; + return [wrapper]; + } else { + return this.props.renderPipeline; + } + } + renderLayers() { const { mesh } = this.state; - const { texture, debug, shaders } = this.props; + const { debug } = this.props; if (!mesh) { return null; @@ -237,8 +257,7 @@ export class RasterLayer extends CompositeLayer { const meshLayer = new MeshTextureLayer( this.getSubLayerProps({ id: "raster", - texture, - shaders, + renderPipeline: this._createRenderPipeline(), // Dummy data because we're only rendering _one_ instance of this mesh // https://github.com/visgl/deck.gl/blob/93111b667b919148da06ff1918410cf66381904f/modules/geo-layers/src/terrain-layer/terrain-layer.ts#L241 data: [1], diff --git a/packages/deck.gl-raster/src/webgl/colormap.ts b/packages/deck.gl-raster/src/webgl/colormap.ts new file mode 100644 index 0000000..67a12fd --- /dev/null +++ b/packages/deck.gl-raster/src/webgl/colormap.ts @@ -0,0 +1,26 @@ +import type { Texture } from "@luma.gl/core"; +import type { ShaderModule } from "@luma.gl/shadertools"; + +// Props expected by the Colormap shader module +export type ColormapProps = { + colormapTexture: Texture; +}; + +/** + * A shader module that injects a unorm texture and uses a sampler2D to assign + * to a color. + */ +export const Colormap = { + name: "colormap", + inject: { + "fs:#decl": `uniform sampler2D colormapTexture;`, + "fs:DECKGL_FILTER_COLOR": /* glsl */ ` + color = texture(colormapTexture, vec2(color.r, 0.)); + `, + }, + getUniforms: (props: Partial) => { + return { + colormapTexture: props.colormapTexture, + }; + }, +} as const satisfies ShaderModule; diff --git a/packages/deck.gl-raster/src/webgl/create-texture.ts b/packages/deck.gl-raster/src/webgl/create-texture.ts new file mode 100644 index 0000000..1a20d4a --- /dev/null +++ b/packages/deck.gl-raster/src/webgl/create-texture.ts @@ -0,0 +1,26 @@ +import type { Texture } from "@luma.gl/core"; +import type { ShaderModule } from "@luma.gl/shadertools"; + +// Props expected by the CreateTexture shader module +export type CreateTextureProps = { + textureName: Texture; +}; + +/** + * A shader module that injects a unorm texture and uses a sampler2D to assign + * to a color. + */ +export const CreateTexture = { + name: "create-texture-unorm", + inject: { + "fs:#decl": `uniform sampler2D textureName;`, + "fs:DECKGL_FILTER_COLOR": /* glsl */ ` + color = texture(textureName, geometry.uv); + `, + }, + getUniforms: (props: Partial) => { + return { + textureName: props.textureName, + }; + }, +} as const satisfies ShaderModule; diff --git a/packages/deck.gl-raster/src/webgl/filter-nodata.ts b/packages/deck.gl-raster/src/webgl/filter-nodata.ts new file mode 100644 index 0000000..0b90e03 --- /dev/null +++ b/packages/deck.gl-raster/src/webgl/filter-nodata.ts @@ -0,0 +1,38 @@ +import type { ShaderModule } from "@luma.gl/shadertools"; + +export type FilterNoDataValProps = { + value: number; +}; + +/** This module name must be consistent */ +const MODULE_NAME = "nodata"; + +const uniformBlock = `\ +uniform ${MODULE_NAME}Uniforms { + float value; +} ${MODULE_NAME}; +`; + +/** + * A shader module that filters out (discards) pixels whose value matches the + * provided nodata value. + */ +export const FilterNoDataVal = { + name: MODULE_NAME, + fs: uniformBlock, + inject: { + "fs:DECKGL_FILTER_COLOR": /* glsl */ ` + if (color.r == nodata.value) { + discard; + } + `, + }, + uniformTypes: { + value: "f32", + }, + getUniforms: (props: Partial) => { + return { + value: props.value, + }; + }, +} as const satisfies ShaderModule; diff --git a/packages/deck.gl-raster/src/webgl/index.ts b/packages/deck.gl-raster/src/webgl/index.ts new file mode 100644 index 0000000..3f85177 --- /dev/null +++ b/packages/deck.gl-raster/src/webgl/index.ts @@ -0,0 +1,3 @@ +export { Colormap } from "./colormap"; +export { CreateTexture } from "./create-texture"; +export { FilterNoDataVal } from "./filter-nodata"; diff --git a/packages/deck.gl-raster/src/webgl/types.ts b/packages/deck.gl-raster/src/webgl/types.ts new file mode 100644 index 0000000..4ce991e --- /dev/null +++ b/packages/deck.gl-raster/src/webgl/types.ts @@ -0,0 +1,8 @@ +import type { ShaderModule } from "@luma.gl/shadertools"; + +export type RasterModule< + PropsT extends Record = Record, +> = { + module: ShaderModule; + props: Partial; +};