From 1939b6a217537a63a7a4dc92c3019dc55b76c50d Mon Sep 17 00:00:00 2001 From: Ib Green Date: Sat, 18 Jul 2026 09:41:55 -0400 Subject: [PATCH] feat(layers): extend WebGPU layer support --- .../mesh-layers/scenegraph-layer.md | 1 + docs/developer-guide/webgpu.md | 43 +-- examples/website/3d-heatmap/app.tsx | 6 +- examples/website/arc/app.tsx | 6 +- examples/website/brushing/app.tsx | 6 +- examples/website/collision-filter/app.tsx | 7 +- examples/website/contour/app.tsx | 6 +- examples/website/data-filter/app.tsx | 6 +- examples/website/geojson/app.tsx | 6 +- examples/website/highway/app.tsx | 4 + examples/website/map-tile/app.tsx | 6 +- examples/website/scenegraph/app.tsx | 4 + examples/website/screen-grid/app.tsx | 7 +- examples/website/text/app.tsx | 5 +- examples/website/treemap/app.tsx | 8 +- examples/website/wms/app.tsx | 12 +- .../src/hexagon-layer/hexagon-cell-layer.ts | 3 +- .../hexagon-layer/hexagon-cell-layer.wgsl.ts | 101 ++++++ .../hexagon-layer/hexagon-layer-uniforms.ts | 14 + .../screen-grid-cell-layer.ts | 8 +- .../screen-grid-cell-layer.wgsl.ts | 57 +++ .../screen-grid-layer-uniforms.ts | 17 +- .../lib/attribute/attribute-buffer-groups.ts | 3 + .../src/lib/attribute/attribute-manager.ts | 6 +- modules/core/src/lib/layer.ts | 22 +- .../src/bitmap-layer/bitmap-layer-uniforms.ts | 21 +- .../layers/src/bitmap-layer/bitmap-layer.ts | 16 +- .../src/bitmap-layer/bitmap-layer.wgsl.ts | 121 +++++++ .../src/path-layer/path-layer-uniforms.ts | 23 +- modules/layers/src/path-layer/path-layer.ts | 223 +++++++++--- .../layers/src/path-layer/path-layer.wgsl.ts | 258 ++++++++++++++ .../layers/src/path-layer/path-tesselator.ts | 4 +- .../solid-polygon-layer-uniforms.ts | 1 + .../solid-polygon-layer.ts | 138 ++++++- .../solid-polygon-layer.wgsl.ts | 233 ++++++++++++ .../multi-icon-layer/multi-icon-layer.ts | 11 +- .../multi-icon-layer/multi-icon-layer.wgsl.ts | 336 ++++++++++++++++++ .../text-background-layer.ts | 10 + .../text-background-layer.wgsl.ts | 202 +++++++++++ .../scenegraph-layer-uniforms.ts | 24 +- .../scenegraph-layer-vertex.glsl.ts | 5 +- .../src/scenegraph-layer/scenegraph-layer.ts | 43 ++- .../scenegraph-layer/scenegraph-layer.wgsl.ts | 118 ++++++ .../scenegraph-pbr-material.ts | 36 ++ .../attribute-buffer-groups.node.spec.ts | 16 +- website/src/examples/arc-layer.js | 4 + website/src/examples/brushing-extension.js | 4 + .../examples/collision-filter-extension.js | 4 + website/src/examples/contour-layer.js | 4 + website/src/examples/data-filter-extension.js | 4 +- website/src/examples/geojson-layer-paths.js | 4 + .../src/examples/geojson-layer-polygons.js | 4 +- website/src/examples/hexagon-layer.js | 4 + website/src/examples/scenegraph-layer.js | 4 + website/src/examples/screen-grid-layer.js | 4 + website/src/examples/text-layer-clipping.js | 4 + website/src/examples/text-layer.js | 4 + website/src/examples/tile-layer.js | 4 + website/src/examples/wms-layer.js | 4 + 59 files changed, 2107 insertions(+), 152 deletions(-) create mode 100644 modules/aggregation-layers/src/hexagon-layer/hexagon-cell-layer.wgsl.ts create mode 100644 modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.wgsl.ts create mode 100644 modules/layers/src/bitmap-layer/bitmap-layer.wgsl.ts create mode 100644 modules/layers/src/path-layer/path-layer.wgsl.ts create mode 100644 modules/layers/src/solid-polygon-layer/solid-polygon-layer.wgsl.ts create mode 100644 modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.wgsl.ts create mode 100644 modules/layers/src/text-layer/text-background-layer/text-background-layer.wgsl.ts create mode 100644 modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.wgsl.ts create mode 100644 modules/mesh-layers/src/scenegraph-layer/scenegraph-pbr-material.ts diff --git a/docs/api-reference/mesh-layers/scenegraph-layer.md b/docs/api-reference/mesh-layers/scenegraph-layer.md index 5cb253a4cb4..2792ea93f31 100644 --- a/docs/api-reference/mesh-layers/scenegraph-layer.md +++ b/docs/api-reference/mesh-layers/scenegraph-layer.md @@ -1,4 +1,5 @@ # ScenegraphLayer +![webgpu](https://img.shields.io/badge/webgpu-supported-blue.svg?style=flat-square) import {ScenegraphLayerDemo} from '@site/src/doc-demos/mesh-layers'; diff --git a/docs/developer-guide/webgpu.md b/docs/developer-guide/webgpu.md index 8e242f51612..4130921ae4f 100644 --- a/docs/developer-guide/webgpu.md +++ b/docs/developer-guide/webgpu.md @@ -36,7 +36,7 @@ The table below covers the public layer exports from the layer packages. It is d | Module | Layer | WebGL | WebGPU | | --- | --- | --- | --- | | `@deck.gl/layers` | `ArcLayer` | ✅ | ✅ | -| `@deck.gl/layers` | `BitmapLayer` | ✅ | ❌ | +| `@deck.gl/layers` | `BitmapLayer` | ✅ | 🚧 | | `@deck.gl/layers` | `IconLayer` | ✅ | ✅ | | `@deck.gl/layers` | `LineLayer` | ✅ | ✅ | | `@deck.gl/layers` | `PointCloudLayer` | ✅ | ✅ | @@ -44,22 +44,23 @@ The table below covers the public layer exports from the layer packages. It is d | `@deck.gl/layers` | `ColumnLayer` | ✅ | ✅ | | `@deck.gl/layers` | `GridCellLayer` | ✅ | ✅ | | `@deck.gl/layers` | `PathLayer` | ✅ | ✅ | -| `@deck.gl/layers` | `PolygonLayer` | ✅ | ❌ | -| `@deck.gl/layers` | `GeoJsonLayer` | ✅ | ❌ | -| `@deck.gl/layers` | `TextLayer` | ✅ | ❌ | -| `@deck.gl/layers` | `SolidPolygonLayer` | ✅ | ❌ | -| `@deck.gl/aggregation-layers` | `ScreenGridLayer` | ✅ | ❌ | -| `@deck.gl/aggregation-layers` | `HexagonLayer` | ✅ | ❌ | -| `@deck.gl/aggregation-layers` | `ContourLayer` | ✅ | ❌ | +| `@deck.gl/layers` | `PolygonLayer` | ✅ | ✅ | +| `@deck.gl/layers` | `GeoJsonLayer` | ✅ | ✅ | +| `@deck.gl/layers` | `TextLayer` | ✅ | ✅ | +| `@deck.gl/layers` | `SolidPolygonLayer` | ✅ | ✅ | +| `@deck.gl/aggregation-layers` | `ScreenGridLayer` | ✅ | ✅ | +| `@deck.gl/aggregation-layers` | `HexagonLayer` | ✅ | ✅ | +| `@deck.gl/aggregation-layers` | `ContourLayer` | ✅ | ✅ | | `@deck.gl/aggregation-layers` | `GridLayer` | ✅ | ❌ | | `@deck.gl/aggregation-layers` | `HeatmapLayer` | ✅ | ❌ | | `@deck.gl/mesh-layers` | `SimpleMeshLayer` | ✅ | ❌ | -| `@deck.gl/mesh-layers` | `ScenegraphLayer` | ✅ | ❌ | +| `@deck.gl/mesh-layers` | `ScenegraphLayer` | ✅ | ✅ | | `@deck.gl/geo-layers` | `A5Layer` | ✅ | ❌ | | `@deck.gl/geo-layers` | `GreatCircleLayer` | ✅ | ❌ | | `@deck.gl/geo-layers` | `S2Layer` | ✅ | ❌ | | `@deck.gl/geo-layers` | `QuadkeyLayer` | ✅ | ❌ | -| `@deck.gl/geo-layers` | `TileLayer` | ✅ | ❌ | +| `@deck.gl/geo-layers` | `TileLayer` | ✅ | ✅ | +| `@deck.gl/geo-layers` | `_WMSLayer` | ✅ | ✅ | | `@deck.gl/geo-layers` | `TripsLayer` | ✅ | ❌ | | `@deck.gl/geo-layers` | `H3ClusterLayer` | ✅ | ❌ | | `@deck.gl/geo-layers` | `H3HexagonLayer` | ✅ | ❌ | @@ -75,20 +76,22 @@ The table below covers the public layer exports from the layer packages. It is d | `@deck.gl/carto` | `RasterTileLayer` | ✅ | ❌ | | `@deck.gl/carto` | `VectorTileLayer` | ✅ | ❌ | +On WebGPU, the supported aggregation layers currently use their CPU aggregation fallback before rendering their WGSL-backed sublayers. Their WebGL GPU aggregation paths are unchanged. + ## Extensions -The table below covers the public extensions in `@deck.gl/extensions`. They all remain WebGL-only today because they rely on GLSL shader injections, GLSL-only shader modules, or extra render/picking passes that have not been ported to WebGPU. +The table below covers the public extensions in `@deck.gl/extensions`. They remain WebGL-only because they rely on GLSL shader injections, GLSL-only shader modules, or extra render/picking passes that have not been ported to WebGPU. On WebGPU, deck.gl ignores extensions instead of invoking their lifecycle hooks or adding their shaders and attributes. Layers that otherwise support WebGPU can still render, but the extension behavior is absent. | Module | Extension | WebGL | WebGPU | | --- | --- | --- | --- | -| `@deck.gl/extensions` | `BrushingExtension` | ✅ | ❌ | -| `@deck.gl/extensions` | `DataFilterExtension` | ✅ | ❌ | -| `@deck.gl/extensions` | `Fp64Extension` | ✅ | ❌ | -| `@deck.gl/extensions` | `PathStyleExtension` | ✅ | ❌ | -| `@deck.gl/extensions` | `FillStyleExtension` | ✅ | ❌ | -| `@deck.gl/extensions` | `ClipExtension` | ✅ | ❌ | -| `@deck.gl/extensions` | `CollisionFilterExtension` | ✅ | ❌ | -| `@deck.gl/extensions` | `MaskExtension` | ✅ | ❌ | +| `@deck.gl/extensions` | `BrushingExtension` | ✅ | ignored | +| `@deck.gl/extensions` | `DataFilterExtension` | ✅ | ignored | +| `@deck.gl/extensions` | `Fp64Extension` | ✅ | ignored | +| `@deck.gl/extensions` | `PathStyleExtension` | ✅ | ignored | +| `@deck.gl/extensions` | `FillStyleExtension` | ✅ | ignored | +| `@deck.gl/extensions` | `ClipExtension` | ✅ | ignored | +| `@deck.gl/extensions` | `CollisionFilterExtension` | ✅ | ignored | +| `@deck.gl/extensions` | `MaskExtension` | ✅ | ignored | ## Effects @@ -105,7 +108,7 @@ The table below covers the public effect classes exported by `@deck.gl/core`. | --- | --- | --- | | Views | 🚧 | The core `project` and `project32` shader modules have WGSL ports, so standard view/projection paths should work. | | Picking | ❌ | `Deck` currently skips picking on WebGPU, including hover and click picking paths. | -| Shader hooks / layer extensions | ❌ | deck.gl's WGSL shader hook list is currently empty, so injection-based extensions are not yet portable. | +| Shader hooks / layer extensions | ❌ | deck.gl's WGSL shader hook list is currently empty, so WebGPU ignores layer extensions. | | GPU transforms | 🚧 | Underlying GPU transform APIs are evolving, but deck.gl still has transform-gated tests and no documented WebGPU support for transform-based workflows. | | Constant attributes | 🚧 | `AttributeManager` now materializes constant attributes into full buffers on WebGPU as a compatibility path for layers that rely on constant accessors. | | Attribute transitions | 🚧 | Some layers disable transitions on WebGPU, and transition utilities still contain WebGL-specific buffer read paths. | diff --git a/examples/website/3d-heatmap/app.tsx b/examples/website/3d-heatmap/app.tsx index f4787c1204f..c40d56b7e2b 100644 --- a/examples/website/3d-heatmap/app.tsx +++ b/examples/website/3d-heatmap/app.tsx @@ -12,6 +12,7 @@ import {CSVLoader} from '@loaders.gl/csv'; import {load} from '@loaders.gl/core'; import type {Color, PickingInfo, MapViewState} from '@deck.gl/core'; +import type {Device} from '@luma.gl/core'; // Source data CSV const DATA_URL = @@ -78,13 +79,15 @@ export default function App({ mapStyle = MAP_STYLE, radius = 1000, upperPercentile = 100, - coverage = 1 + coverage = 1, + device }: { data?: DataPoint[] | null; mapStyle?: string; radius?: number; upperPercentile?: number; coverage?: number; + device?: Device; }) { const layers = [ new HexagonLayer({ @@ -115,6 +118,7 @@ export default function App({ return ( ) { export default function App({ data, strokeWidth = 1, - mapStyle = MAP_STYLE + mapStyle = MAP_STYLE, + device }: { data?: County[]; strokeWidth?: number; mapStyle?: string; + device?: Device; }) { const [selectedCounty, selectCounty] = useState(); @@ -138,6 +141,7 @@ export default function App({ return ( getLayerData(data), [data]); const radiusScale = useMemo(() => { @@ -195,6 +198,7 @@ export default function App({ return ( >(); @@ -99,7 +102,7 @@ export default function App({ ]; return ( - + ); diff --git a/examples/website/contour/app.tsx b/examples/website/contour/app.tsx index 2f7bc39f87f..b5c986e6550 100644 --- a/examples/website/contour/app.tsx +++ b/examples/website/contour/app.tsx @@ -10,6 +10,7 @@ import {ContourLayer} from '@deck.gl/aggregation-layers'; import type {ContourLayerProps} from '@deck.gl/aggregation-layers'; import type {PickingInfo, MapViewState} from '@deck.gl/core'; +import type {Device} from '@luma.gl/core'; const DATA_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/contour/covid-by-county.json'; // eslint-disable-line @@ -55,13 +56,15 @@ export default function App({ week = 35, contours = BANDS, cellSize = 60000, - mapStyle = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json' + mapStyle = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json', + device }: { data?: string | CaseReport[]; week?: number; contours?: ContourLayerProps['contours']; cellSize?: number; mapStyle?: string; + device?: Device; }) { const layers = [ new ContourLayer({ @@ -101,6 +104,7 @@ export default function App({ return ( ) { export default function App({ data, - mapStyle = MAP_STYLE + mapStyle = MAP_STYLE, + device }: { data?: Earthquake[]; mapStyle?: string; + device?: Device; }) { const [filter, setFilter] = useState<[start: number, end: number] | null>(null); @@ -132,6 +135,7 @@ export default function App({ return ( <> >) { export default function App({ data = DATA_URL, - mapStyle = MAP_STYLE + mapStyle = MAP_STYLE, + device }: { data?: string | Feature[]; mapStyle?: string; + device?: Device; }) { const [effects] = useState(() => { const lightingEffect = new LightingEffect({ambientLight, dirLight}); @@ -127,6 +130,7 @@ export default function App({ return ( void; @@ -69,6 +71,7 @@ export default function App({ visibleMaxZoom?: number; zoomOffset?: number; useExtent?: boolean; + device?: Device; }) { const [zoom, setZoom] = useState(INITIAL_VIEW_STATE.zoom); const onViewStateChange = useCallback( @@ -130,6 +133,7 @@ export default function App({ return ( unknown, delay: number) { } export default function App({ + device, sizeScale = 25, onDataLoad, mapStyle = MAP_STYLE }: { + device?: Device; sizeScale?: number; onDataLoad?: (count: number) => void; mapStyle?: string; @@ -178,6 +181,7 @@ export default function App({ return ( ({ @@ -64,7 +67,7 @@ export default function App({ ]; return ( - + ); diff --git a/examples/website/text/app.tsx b/examples/website/text/app.tsx index 6cafd96f17c..d87c94d0c14 100644 --- a/examples/website/text/app.tsx +++ b/examples/website/text/app.tsx @@ -17,6 +17,7 @@ import {load} from '@loaders.gl/core'; import type {Color, MapViewState} from '@deck.gl/core'; import type {CollisionFilterExtensionProps} from '@deck.gl/extensions'; +import type {Device} from '@luma.gl/core'; // Sample data const DATA_URL = @@ -51,11 +52,13 @@ const colorScale = scaleLog() ]); export default function App({ + device, data, noOverlap = true, fontSize = 32, mapStyle = MAP_STYLE }: { + device?: Device; data?: City[]; noOverlap?: boolean; fontSize?: number; @@ -70,7 +73,6 @@ export default function App({ const scale = 2 ** zoom; const sizeMaxPixels = (scale / 3) * fontSize; const sizeMinPixels = Math.min(scale / 1000, 0.5) * fontSize; - const textLayer = new TextLayer>({ id: 'world-cities', data, @@ -102,6 +104,7 @@ export default function App({ return ( ; width?: number; height?: number; @@ -162,8 +167,9 @@ export default function App({ return ( void; onMetadataLoadError?: (error: Error) => void; + device?: Device; }) { const [selection, setSelection] = useState<{ x: number; @@ -79,7 +82,12 @@ export default function App({ return ( <> - + {selection && (
extends Colum getShaders() { const shaders = super.getShaders(); shaders.modules.push(hexagonUniforms); - return {...shaders, vs}; + return {...shaders, source, vs}; } initializeState() { diff --git a/modules/aggregation-layers/src/hexagon-layer/hexagon-cell-layer.wgsl.ts b/modules/aggregation-layers/src/hexagon-layer/hexagon-cell-layer.wgsl.ts new file mode 100644 index 00000000000..fb25fb55dfb --- /dev/null +++ b/modules/aggregation-layers/src/hexagon-layer/hexagon-cell-layer.wgsl.ts @@ -0,0 +1,101 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export default /* wgsl */ `\ +const HEXBIN_DISTANCE: vec2 = vec2(1.7320508, 1.5); + +struct Attributes { + @builtin(instance_index) instanceIndex: u32, + @location(0) positions: vec3, + @location(1) normals: vec3, + @location(2) instancePositions: vec2, + @location(3) instanceColorValues: f32, + @location(4) instanceElevationValues: f32, +}; + +struct Varyings { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) pickingColor: vec3, +}; + +fn hexbinCentroid(binId: vec2, radius: f32) -> vec2 { + var adjustedBinId = binId; + adjustedBinId.x += fract(adjustedBinId.y * 0.5); + return adjustedBinId * HEXBIN_DISTANCE * radius; +} + +fn interpolate(value: f32, domain: vec2, range: vec2) -> f32 { + let ratio = clamp((value - domain.x) / (domain.y - domain.x), 0.0, 1.0); + return mix(range.x, range.y, ratio); +} + +fn sampleColorRange(value: f32, domain: vec2) -> vec4 { + let ratio = (value - domain.x) / (domain.y - domain.x); + return textureSampleLevel(colorRange, colorRangeSampler, vec2(ratio, 0.5), 0.0); +} + +@vertex +fn vertexMain(attributes: Attributes) -> Varyings { + var output: Varyings; + geometry.pickingColor = picking_getPickingColorFromIndex(attributes.instanceIndex); + output.pickingColor = geometry.pickingColor; + + if ( + attributes.instanceColorValues != attributes.instanceColorValues || + attributes.instanceColorValues < hexagon.colorDomain.z || + attributes.instanceColorValues > hexagon.colorDomain.w || + attributes.instanceElevationValues < hexagon.elevationDomain.z || + attributes.instanceElevationValues > hexagon.elevationDomain.w + ) { + output.position = vec4(0.0); + output.color = vec4(0.0); + return output; + } + + var commonPosition = + hexbinCentroid(attributes.instancePositions, column.radius) + + (hexagon.originCommon - project.commonOrigin.xy); + commonPosition += attributes.positions.xy * column.radius * column.coverage; + geometry.position = vec4(commonPosition, 0.0, 1.0); + geometry.normal = project_normal(attributes.normals); + + if (column.extruded > 0.5) { + var elevation = interpolate( + attributes.instanceElevationValues, + hexagon.elevationDomain.xy, + hexagon.elevationRange + ); + elevation = project_size_float(elevation); + geometry.position.z = (attributes.positions.z + 1.0) / 2.0 * elevation; + } + + output.position = project_common_position_to_clipspace(geometry.position); + var colorValue = sampleColorRange(attributes.instanceColorValues, hexagon.colorDomain.xy); + if (column.extruded > 0.5) { + colorValue = vec4( + lighting_getLightColor2( + colorValue.rgb, + project.cameraPosition, + geometry.position.xyz, + geometry.normal + ), + colorValue.a + ); + } + output.color = vec4(colorValue.rgb, colorValue.a * layer.opacity); + return output; +} + +@fragment +fn fragmentMain(varyings: Varyings) -> @location(0) vec4 { + if (picking.isActive > 0.5) { + if (!picking_isColorValid(varyings.pickingColor)) { + discard; + } + return vec4(varyings.pickingColor, 1.0); + } + return deckgl_premultiplied_alpha(varyings.color); +} +`; diff --git a/modules/aggregation-layers/src/hexagon-layer/hexagon-layer-uniforms.ts b/modules/aggregation-layers/src/hexagon-layer/hexagon-layer-uniforms.ts index d0a86f6343a..3f1085dd8b2 100644 --- a/modules/aggregation-layers/src/hexagon-layer/hexagon-layer-uniforms.ts +++ b/modules/aggregation-layers/src/hexagon-layer/hexagon-layer-uniforms.ts @@ -5,6 +5,19 @@ import {Texture} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; +const uniformBlockWGSL = /* wgsl */ `\ +struct HexagonUniforms { + colorDomain: vec4, + elevationDomain: vec4, + elevationRange: vec2, + originCommon: vec2, +}; + +@group(0) @binding(auto) var hexagon: HexagonUniforms; +@group(0) @binding(auto) var colorRange: texture_2d; +@group(0) @binding(auto) var colorRangeSampler: sampler; +`; + const uniformBlock = /* glsl */ `\ layout(std140) uniform hexagonUniforms { vec4 colorDomain; @@ -24,6 +37,7 @@ export type HexagonProps = { export const hexagonUniforms = { name: 'hexagon', + source: uniformBlockWGSL, vs: uniformBlock, uniformTypes: { colorDomain: 'vec4', diff --git a/modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.ts b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.ts index 52f6a6ba006..37bcea25e82 100644 --- a/modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.ts +++ b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.ts @@ -4,12 +4,12 @@ import {Texture} from '@luma.gl/core'; import {Model, Geometry} from '@luma.gl/engine'; -import {Layer, picking, UpdateParameters, Color} from '@deck.gl/core'; +import {Layer, color, picking, UpdateParameters, Color} from '@deck.gl/core'; import {createColorRangeTexture, updateColorRangeTexture} from '../common/utils/color-utils'; +import source from './screen-grid-cell-layer.wgsl'; import vs from './screen-grid-layer-vertex.glsl'; import fs from './screen-grid-layer-fragment.glsl'; import {ScreenGridProps, screenGridUniforms} from './screen-grid-layer-uniforms'; -import {ShaderModule} from '@luma.gl/shadertools'; import type {ScaleType} from '../common/types'; /** Proprties added by ScreenGridCellLayer. */ @@ -31,8 +31,8 @@ export default class ScreenGridCellLayer extends La colorTexture: Texture; }; - getShaders(): {vs: string; fs: string; modules: ShaderModule[]} { - return super.getShaders({vs, fs, modules: [picking, screenGridUniforms]}); + getShaders() { + return super.getShaders({source, vs, fs, modules: [color, picking, screenGridUniforms]}); } initializeState() { diff --git a/modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.wgsl.ts b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.wgsl.ts new file mode 100644 index 00000000000..68bde2cc74f --- /dev/null +++ b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-cell-layer.wgsl.ts @@ -0,0 +1,57 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export default /* wgsl */ ` +struct Attributes { + @builtin(instance_index) instanceIndex: u32, + @location(0) positions: vec2, + @location(1) instancePositions: vec2, + @location(2) instanceWeights: f32, +}; + +struct Varyings { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) pickingColor: vec3, +}; + +fn sampleColorRange(value: f32, domain: vec2) -> vec4 { + let ratio = (value - domain.x) / (domain.y - domain.x); + return textureSampleLevel(colorRange, colorRangeSampler, vec2(ratio, 0.5), 0.0); +} + +@vertex +fn vertexMain(attributes: Attributes) -> Varyings { + var output: Varyings; + output.pickingColor = picking_getPickingColorFromIndex(attributes.instanceIndex); + + if (attributes.instanceWeights != attributes.instanceWeights) { + output.position = vec4(0.0); + output.color = vec4(0.0); + return output; + } + + var position = + attributes.instancePositions * screenGrid.gridSizeClipspace + + attributes.positions * screenGrid.cellSizeClipspace; + position.x -= 1.0; + position.y = 1.0 - position.y; + + output.position = vec4(position, 0.0, 1.0); + let colorValue = sampleColorRange(attributes.instanceWeights, screenGrid.colorDomain); + output.color = vec4(colorValue.rgb, colorValue.a * layer.opacity); + return output; +} + +@fragment +fn fragmentMain(varyings: Varyings) -> @location(0) vec4 { + if (picking.isActive > 0.5) { + if (!picking_isColorValid(varyings.pickingColor)) { + discard; + } + return vec4(varyings.pickingColor, 1.0); + } + return deckgl_premultiplied_alpha(varyings.color); +} +`; diff --git a/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer-uniforms.ts b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer-uniforms.ts index 87aefd62e2f..ea35ce5d572 100644 --- a/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer-uniforms.ts +++ b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer-uniforms.ts @@ -5,7 +5,19 @@ import {Texture} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; -const uniformBlock = /* glsl */ `\ +const uniformBlockWGSL = /* wgsl */ ` +struct ScreenGridUniforms { + cellSizeClipspace: vec2, + gridSizeClipspace: vec2, + colorDomain: vec2, +}; + +@group(0) @binding(auto) var screenGrid: ScreenGridUniforms; +@group(0) @binding(auto) var colorRange: texture_2d; +@group(0) @binding(auto) var colorRangeSampler: sampler; +`; + +const uniformBlockGLSL = /* glsl */ `\ layout(std140) uniform screenGridUniforms { vec2 cellSizeClipspace; vec2 gridSizeClipspace; @@ -22,7 +34,8 @@ export type ScreenGridProps = { export const screenGridUniforms = { name: 'screenGrid', - vs: uniformBlock, + source: uniformBlockWGSL, + vs: uniformBlockGLSL, uniformTypes: { cellSizeClipspace: 'vec2', gridSizeClipspace: 'vec2', diff --git a/modules/core/src/lib/attribute/attribute-buffer-groups.ts b/modules/core/src/lib/attribute/attribute-buffer-groups.ts index 3ade67b0ef9..eb0b6035e94 100644 --- a/modules/core/src/lib/attribute/attribute-buffer-groups.ts +++ b/modules/core/src/lib/attribute/attribute-buffer-groups.ts @@ -251,6 +251,9 @@ export default class AttributeBufferGroups { } for (const attribute of Object.values(attributes)) { + if (attribute.settings.isIndexed) { + continue; + } const groupId = attribute.settings.bufferGroup; const group = groupId && groups.get(groupId); if (group && groupedAttributeIds.has(attribute.id)) { diff --git a/modules/core/src/lib/attribute/attribute-manager.ts b/modules/core/src/lib/attribute/attribute-manager.ts index 326e4a77bc3..9a1a522aab3 100644 --- a/modules/core/src/lib/attribute/attribute-manager.ts +++ b/modules/core/src/lib/attribute/attribute-manager.ts @@ -315,9 +315,9 @@ export default class AttributeManager { if (this.hasBufferGroups()) { return this.attributeBufferGroups!.getBufferLayouts(this.getAttributes(), modelInfo); } - return Object.values(this.getAttributes()).map(attribute => - attribute.getBufferLayout(modelInfo) - ); + return Object.values(this.getAttributes()) + .filter(attribute => this.device.type !== 'webgpu' || !attribute.settings.isIndexed) + .map(attribute => attribute.getBufferLayout(modelInfo)); } /** @internal Returns whether this WebGPU manager has explicitly grouped attributes. */ diff --git a/modules/core/src/lib/layer.ts b/modules/core/src/lib/layer.ts index 2e7b15b4179..de6fd2e41f6 100644 --- a/modules/core/src/lib/layer.ts +++ b/modules/core/src/lib/layer.ts @@ -471,7 +471,7 @@ export default abstract class Layer extends Component< disableWarnings: true, modules: this.context.defaultShaderModules }); - for (const extension of this.props.extensions) { + for (const extension of this._getExtensions()) { shaders = mergeShaders(shaders, extension.getShaders.call(this, extension)); } return shaders; @@ -505,7 +505,9 @@ export default abstract class Layer extends Component< const needsPickingBuffer = Number.isInteger(props.highlightedObjectIndex) || Boolean(props.pickable) || - props.extensions.some(extension => extension.getNeedsPickingBuffer.call(this, extension)); + this._getExtensions().some(extension => + extension.getNeedsPickingBuffer.call(this, extension) + ); // Only generate picking buffer if needed if (hasPickingBuffer !== needsPickingBuffer) { @@ -1003,7 +1005,7 @@ export default abstract class Layer extends Component< this.initializeState(this.context); // Initialize extensions - for (const extension of this.props.extensions) { + for (const extension of this._getExtensions()) { extension.initializeState.call(this, this.context, extension); } // End subclass lifecycle methods @@ -1084,7 +1086,7 @@ export default abstract class Layer extends Component< } } // Execute extension updates - for (const extension of this.props.extensions) { + for (const extension of this._getExtensions()) { extension.updateState.call(this, updateParams, extension); } @@ -1114,7 +1116,7 @@ export default abstract class Layer extends Component< // Call subclass lifecycle method this.finalizeState(this.context); // Finalize extensions - for (const extension of this.props.extensions) { + for (const extension of this._getExtensions()) { extension.finalizeState.call(this, this.context, extension); } } @@ -1166,7 +1168,7 @@ export default abstract class Layer extends Component< const opts: DrawOptions = {renderPass, shaderModuleProps, uniforms, parameters, context}; // extensions - for (const extension of this.props.extensions) { + for (const extension of this._getExtensions()) { extension.draw.call(this, opts, extension); } @@ -1179,7 +1181,7 @@ export default abstract class Layer extends Component< const opts: DrawOptions = {renderPass, shaderModuleProps, uniforms, parameters, context}; // extensions - for (const extension of this.props.extensions) { + for (const extension of this._getExtensions()) { extension.draw.call(this, opts, extension); } @@ -1411,7 +1413,7 @@ export default abstract class Layer extends Component< redraw = redraw || attributeManagerNeedsRedraw; if (redraw) { - for (const extension of this.props.extensions) { + for (const extension of this._getExtensions()) { extension.onNeedsRedraw.call(this, extension); } } @@ -1426,6 +1428,10 @@ export default abstract class Layer extends Component< this._diffProps(this.props, this.internalState.getOldProps()); this.setNeedsUpdate(); } + + private _getExtensions() { + return this.context.device.type === 'webgpu' ? EMPTY_ARRAY : this.props.extensions; + } } function splitWebGPUDrawParameters(parameters: LumaParameters): { diff --git a/modules/layers/src/bitmap-layer/bitmap-layer-uniforms.ts b/modules/layers/src/bitmap-layer/bitmap-layer-uniforms.ts index 94ac79bc2fd..9d2e3db83c4 100644 --- a/modules/layers/src/bitmap-layer/bitmap-layer-uniforms.ts +++ b/modules/layers/src/bitmap-layer/bitmap-layer-uniforms.ts @@ -5,7 +5,21 @@ import type {Texture} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; -const uniformBlock = `\ +const uniformBlockWGSL = /* wgsl */ ` +struct BitmapUniforms { + bounds: vec4, + coordinateConversion: f32, + desaturate: f32, + tintColor: vec3, + transparentColor: vec4, +}; + +@group(0) @binding(auto) var bitmap: BitmapUniforms; +@group(0) @binding(auto) var bitmapTexture: texture_2d; +@group(0) @binding(auto) var bitmapTextureSampler: sampler; +`; + +const uniformBlockGLSL = `\ layout(std140) uniform bitmapUniforms { vec4 bounds; float coordinateConversion; @@ -26,8 +40,9 @@ export type BitmapProps = { export const bitmapUniforms = { name: 'bitmap', - vs: uniformBlock, - fs: uniformBlock, + source: uniformBlockWGSL, + vs: uniformBlockGLSL, + fs: uniformBlockGLSL, uniformTypes: { bounds: 'vec4', coordinateConversion: 'f32', diff --git a/modules/layers/src/bitmap-layer/bitmap-layer.ts b/modules/layers/src/bitmap-layer/bitmap-layer.ts index 3f5c7be75a6..72a19a8bcb7 100644 --- a/modules/layers/src/bitmap-layer/bitmap-layer.ts +++ b/modules/layers/src/bitmap-layer/bitmap-layer.ts @@ -4,6 +4,7 @@ import { Layer, + color as colorModule, project32, picking, CoordinateSystem, @@ -23,6 +24,7 @@ import {lngLatToWorld} from '@math.gl/web-mercator'; import createMesh from './create-mesh'; import {bitmapUniforms, BitmapProps} from './bitmap-layer-uniforms'; +import source from './bitmap-layer.wgsl'; import vs from './bitmap-layer-vertex'; import fs from './bitmap-layer-fragment'; @@ -129,7 +131,12 @@ export default class BitmapLayer extends Layer< }; getShaders() { - return super.getShaders({vs, fs, modules: [project32, picking, bitmapUniforms]}); + return super.getShaders({ + source, + vs, + fs, + modules: [colorModule, project32, picking, bitmapUniforms] + }); } initializeState() { @@ -256,10 +263,15 @@ export default class BitmapLayer extends Layer< | | 0,1 --- 1,1 */ + const bufferLayout = + this.context.device.type === 'webgpu' + ? this.getAttributeManager()!.getBufferLayouts({isInstanced: false}) + : this.getAttributeManager()!.getBufferLayouts(); + return new Model(this.context.device, { ...this.getShaders(), id: this.props.id, - bufferLayout: this.getAttributeManager()!.getBufferLayouts(), + bufferLayout, topology: 'triangle-list', isInstanced: false }); diff --git a/modules/layers/src/bitmap-layer/bitmap-layer.wgsl.ts b/modules/layers/src/bitmap-layer/bitmap-layer.wgsl.ts new file mode 100644 index 00000000000..7d99b077b31 --- /dev/null +++ b/modules/layers/src/bitmap-layer/bitmap-layer.wgsl.ts @@ -0,0 +1,121 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export default /* wgsl */ `\ +struct Attributes { + @location(0) positions: vec3, + @location(1) positions64Low: vec3, + @location(2) texCoords: vec2, +}; + +struct Varyings { + @builtin(position) position: vec4, + @location(0) vTexCoord: vec2, + @location(1) vTexPos: vec2, +}; + +// from degrees to Web Mercator +fn lnglat_to_mercator(lnglat: vec2) -> vec2 { + let x = lnglat.x; + let y = clamp(lnglat.y, -89.9, 89.9); + return vec2( + radians(x) + PI, + PI + log(tan(PI * 0.25 + radians(y) * 0.5)) + ) * WORLD_SCALE; +} + +// from Web Mercator to degrees +fn mercator_to_lnglat(xy: vec2) -> vec2 { + let position = xy / WORLD_SCALE; + return degrees(vec2( + position.x - PI, + atan(exp(position.y - PI)) * 2.0 - PI * 0.5 + )); +} + +fn color_desaturate(colorValue: vec3) -> vec3 { + let luminance = (colorValue.r + colorValue.g + colorValue.b) * 0.333333333; + return mix(colorValue, vec3(luminance), bitmap.desaturate); +} + +fn color_tint(colorValue: vec3) -> vec3 { + return colorValue * bitmap.tintColor; +} + +fn apply_opacity(colorValue: vec3, alpha: f32) -> vec4 { + if (bitmap.transparentColor.a == 0.0) { + return vec4(colorValue, alpha); + } + let blendedAlpha = alpha + bitmap.transparentColor.a * (1.0 - alpha); + let highLightRatio = alpha / blendedAlpha; + let blendedRGB = mix(bitmap.transparentColor.rgb, colorValue, highLightRatio); + return vec4(blendedRGB, blendedAlpha); +} + +fn getUV(position: vec2) -> vec2 { + return vec2( + (position.x - bitmap.bounds[0]) / (bitmap.bounds[2] - bitmap.bounds[0]), + (position.y - bitmap.bounds[3]) / (bitmap.bounds[1] - bitmap.bounds[3]) + ); +} + +// Pack the top 12 bits of two normalized floats into three 8-bit values. +fn packUVsIntoRGB(uv: vec2) -> vec3 { + let uv8bit = floor(uv * 256.0); + let uvFraction = fract(uv * 256.0); + let uvFraction4bit = floor(uvFraction * 16.0); + let fractions = uvFraction4bit.x + uvFraction4bit.y * 16.0; + return vec3(uv8bit, fractions) / 255.0; +} + +@vertex +fn vertexMain(attributes: Attributes) -> Varyings { + var output: Varyings; + geometry.worldPosition = attributes.positions; + geometry.uv = attributes.texCoords; + geometry.pickingColor = vec3(1.0, 0.0, 0.0); + + let projectedPosition = project_position_to_clipspace_and_commonspace( + attributes.positions, + attributes.positions64Low, + vec3(0.0) + ); + geometry.position = projectedPosition.commonPosition; + output.position = projectedPosition.clipPosition; + output.vTexCoord = attributes.texCoords; + output.vTexPos = vec2(0.0); + + if (bitmap.coordinateConversion < -0.5) { + output.vTexPos = geometry.position.xy + project.commonOrigin.xy; + } else if (bitmap.coordinateConversion > 0.5) { + output.vTexPos = geometry.worldPosition.xy; + } + + return output; +} + +@fragment +fn fragmentMain(input: Varyings) -> @location(0) vec4 { + var uv = input.vTexCoord; + if (bitmap.coordinateConversion < -0.5) { + uv = getUV(mercator_to_lnglat(input.vTexPos)); + } else if (bitmap.coordinateConversion > 0.5) { + uv = getUV(lnglat_to_mercator(input.vTexPos)); + } + + let bitmapColor = textureSample(bitmapTexture, bitmapTextureSampler, uv); + let colorValue = apply_opacity( + color_tint(color_desaturate(bitmapColor.rgb)), + bitmapColor.a * layer.opacity + ); + + geometry.uv = uv; + + if (picking.isActive > 0.5 && picking.isAttribute < 0.5) { + return vec4(packUVsIntoRGB(uv), 1.0); + } + + return deckgl_premultiplied_alpha(colorValue); +} +`; diff --git a/modules/layers/src/path-layer/path-layer-uniforms.ts b/modules/layers/src/path-layer/path-layer-uniforms.ts index d04292ef991..6cf1a8a5153 100644 --- a/modules/layers/src/path-layer/path-layer-uniforms.ts +++ b/modules/layers/src/path-layer/path-layer-uniforms.ts @@ -4,7 +4,23 @@ import type {ShaderModule} from '@luma.gl/shadertools'; -const uniformBlock = `\ +const uniformBlockWGSL = /* wgsl */ `\ +struct PathUniforms { + widthScale: f32, + widthMinPixels: f32, + widthMaxPixels: f32, + jointType: f32, + capType: f32, + miterLimit: f32, + billboard: f32, + widthUnits: i32, +}; + +@group(0) @binding(auto) +var path: PathUniforms; +`; + +const uniformBlockGLSL = `\ layout(std140) uniform pathUniforms { float widthScale; float widthMinPixels; @@ -30,8 +46,9 @@ export type PathProps = { export const pathUniforms = { name: 'path', - vs: uniformBlock, - fs: uniformBlock, + source: uniformBlockWGSL, + vs: uniformBlockGLSL, + fs: uniformBlockGLSL, uniformTypes: { widthScale: 'f32', widthMinPixels: 'f32', diff --git a/modules/layers/src/path-layer/path-layer.ts b/modules/layers/src/path-layer/path-layer.ts index 1f39ae33767..7296153f8a9 100644 --- a/modules/layers/src/path-layer/path-layer.ts +++ b/modules/layers/src/path-layer/path-layer.ts @@ -2,12 +2,14 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Layer, project32, picking, UNIT} from '@deck.gl/core'; +import {Layer, project32, color, picking, UNIT} from '@deck.gl/core'; +import {Parameters} from '@luma.gl/core'; import {Geometry} from '@luma.gl/engine'; import {Model} from '@luma.gl/engine'; import PathTesselator from './path-tesselator'; import {pathUniforms, PathProps} from './path-layer-uniforms'; +import source from './path-layer.wgsl'; import vs from './path-layer-vertex.glsl'; import fs from './path-layer-fragment.glsl'; @@ -135,7 +137,7 @@ export default class PathLayer extends }; getShaders() { - return super.getShaders({vs, fs, modules: [project32, picking, pathUniforms]}); // 'project' module added by default. + return super.getShaders({vs, fs, source, modules: [project32, color, picking, pathUniforms]}); // 'project' module added by default. } get wrapLongitude(): boolean { @@ -143,72 +145,140 @@ export default class PathLayer extends } getBounds(): [number[], number[]] | null { + if (this.context.device.type === 'webgpu') { + return null; + } return this.getAttributeManager()?.getBounds(['vertexPositions']); } initializeState() { const noAlloc = true; const attributeManager = this.getAttributeManager(); + const enableTransitions = this.context.device.type !== 'webgpu'; /* eslint-disable max-len */ - attributeManager!.addInstanced({ - vertexPositions: { - size: 3, - // Start filling buffer from 1 vertex in - vertexOffset: 1, - type: 'float64', - fp64: this.use64bitPositions(), - transition: ATTRIBUTE_TRANSITION, - accessor: 'getPath', - // eslint-disable-next-line @typescript-eslint/unbound-method - update: this.calculatePositions, - noAlloc, - shaderAttributes: { - instanceLeftPositions: { - vertexOffset: 0 + if (this.context.device.type === 'webgpu') { + attributeManager!.addInstanced({ + instancePositions: { + size: 12, + type: 'float32', + // WebGPU keeps the fp64 path by uploading float32 high parts and reconstructing + // with the matching `instancePositions64Low` residuals in WGSL. + transition: false, + accessor: 'getPath', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateInstancePositions, + shaderAttributes: { + instanceLeftPositions: {size: 3, elementOffset: 0}, + instanceStartPositions: {size: 3, elementOffset: 3}, + instanceEndPositions: {size: 3, elementOffset: 6}, + instanceRightPositions: {size: 3, elementOffset: 9} }, - instanceStartPositions: { - vertexOffset: 1 + noAlloc + }, + instancePositions64Low: { + size: 12, + type: 'float32', + // This is the low-part companion to `instancePositions`, not a plain-fp32 downgrade. + transition: false, + accessor: 'getPath', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateInstancePositions64Low, + shaderAttributes: { + instanceLeftPositions64Low: {size: 3, elementOffset: 0}, + instanceStartPositions64Low: {size: 3, elementOffset: 3}, + instanceEndPositions64Low: {size: 3, elementOffset: 6}, + instanceRightPositions64Low: {size: 3, elementOffset: 9} }, - instanceEndPositions: { - vertexOffset: 2 - }, - instanceRightPositions: { - vertexOffset: 3 + noAlloc + }, + instanceTypes: { + size: 1, + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateSegmentTypes, + noAlloc + }, + instanceStrokeWidths: { + size: 1, + accessor: 'getWidth', + transition: false, + defaultValue: 1 + }, + instanceColors: { + size: this.props.colorFormat.length, + type: 'unorm8', + accessor: 'getColor', + transition: false, + defaultValue: DEFAULT_COLOR + }, + /** Source path row for each generated segment/joint instance. */ + rowIndexes: { + size: 1, + type: 'uint32', + accessor: (object, {index}) => (object && object.__source ? object.__source.index : index) + } + }); + } else { + attributeManager!.addInstanced({ + vertexPositions: { + size: 3, + // Start filling buffer from 1 vertex in + vertexOffset: 1, + type: 'float64', + fp64: this.use64bitPositions(), + transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, + accessor: 'getPath', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculatePositions, + noAlloc, + shaderAttributes: { + instanceLeftPositions: { + vertexOffset: 0 + }, + instanceStartPositions: { + vertexOffset: 1 + }, + instanceEndPositions: { + vertexOffset: 2 + }, + instanceRightPositions: { + vertexOffset: 3 + } } + }, + instanceTypes: { + size: 1, + type: 'uint8', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateSegmentTypes, + noAlloc + }, + instanceStrokeWidths: { + size: 1, + accessor: 'getWidth', + transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, + defaultValue: 1 + }, + instanceColors: { + size: this.props.colorFormat.length, + type: 'unorm8', + accessor: 'getColor', + transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, + defaultValue: DEFAULT_COLOR + }, + /** Source path row for each generated segment/joint instance. */ + rowIndexes: { + size: 1, + type: 'uint32', + accessor: (object, {index}) => (object && object.__source ? object.__source.index : index) } - }, - instanceTypes: { - size: 1, - type: 'uint8', - // eslint-disable-next-line @typescript-eslint/unbound-method - update: this.calculateSegmentTypes, - noAlloc - }, - instanceStrokeWidths: { - size: 1, - accessor: 'getWidth', - transition: ATTRIBUTE_TRANSITION, - defaultValue: 1 - }, - instanceColors: { - size: this.props.colorFormat.length, - type: 'unorm8', - accessor: 'getColor', - transition: ATTRIBUTE_TRANSITION, - defaultValue: DEFAULT_COLOR - }, - /** Source path row for each generated segment/joint instance. */ - rowIndexes: { - size: 1, - type: 'uint32', - accessor: (object, {index}) => (object && object.__source ? object.__source.index : index) - } - }); + }); + } /* eslint-enable max-len */ this.setState({ pathTesselator: new PathTesselator({ - fp64: this.use64bitPositions() + fp64: this.use64bitPositions(), + webgpu: this.context.device.type === 'webgpu' }) }); } @@ -317,6 +387,14 @@ export default class PathLayer extends } protected _getModel(): Model { + const parameters = + this.context.device.type === 'webgpu' + ? ({ + depthWriteEnabled: true, + depthCompare: 'less-equal' + } satisfies Parameters) + : undefined; + /* * _ * "-_ 1 3 5 @@ -372,6 +450,7 @@ export default class PathLayer extends positions: {value: new Float32Array(SEGMENT_POSITIONS), size: 2} } }), + parameters, isInstanced: true }); } @@ -383,10 +462,48 @@ export default class PathLayer extends attribute.value = pathTesselator.get('positions'); } + protected calculateInstancePositions(attribute) { + this._calculateInterleavedInstancePositions(attribute, false); + } + + protected calculateInstancePositions64Low(attribute) { + this._calculateInterleavedInstancePositions(attribute, true); + } + protected calculateSegmentTypes(attribute) { const {pathTesselator} = this.state; attribute.startIndices = pathTesselator.vertexStarts; attribute.value = pathTesselator.get('segmentTypes'); } + + protected _calculateInterleavedInstancePositions(attribute, lowPart: boolean) { + const {pathTesselator} = this.state; + const value = pathTesselator.get('positions'); + + if (!value) { + attribute.value = null; + return; + } + + const numInstances = pathTesselator.instanceCount; + const result = new Float32Array(numInstances * 12); + const neighborOffsets = [-1, 0, 1, 2]; + + for (let i = 0; i < numInstances; i++) { + const targetIndex = i * 12; + for (let vertexOffset = 0; vertexOffset < 4; vertexOffset++) { + const sourceVertex = i + neighborOffsets[vertexOffset]; + const targetOffset = targetIndex + vertexOffset * 3; + for (let j = 0; j < 3; j++) { + const position = + sourceVertex >= 0 && sourceVertex < numInstances ? value[sourceVertex * 3 + j] : 0; + result[targetOffset + j] = lowPart ? position - Math.fround(position) : position; + } + } + } + + attribute.startIndices = pathTesselator.vertexStarts; + attribute.value = result; + } } diff --git a/modules/layers/src/path-layer/path-layer.wgsl.ts b/modules/layers/src/path-layer/path-layer.wgsl.ts new file mode 100644 index 00000000000..66a5f02ce96 --- /dev/null +++ b/modules/layers/src/path-layer/path-layer.wgsl.ts @@ -0,0 +1,258 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export default /* wgsl */ `\ +const EPSILON: f32 = 0.001; +const ZERO_OFFSET: vec3 = vec3(0.0, 0.0, 0.0); + +struct JoinResult { + offset: vec3, + cornerOffset: vec2, + miterLength: f32, + pathPosition: vec2, + pathLength: f32, + jointType: f32, +}; + +struct Attributes { + @location(0) positions: vec2, + @location(1) instanceTypes: f32, + @location(2) instanceLeftPositions: vec3, + @location(3) instanceStartPositions: vec3, + @location(4) instanceEndPositions: vec3, + @location(5) instanceRightPositions: vec3, + @location(6) instanceLeftPositions64Low: vec3, + @location(7) instanceStartPositions64Low: vec3, + @location(8) instanceEndPositions64Low: vec3, + @location(9) instanceRightPositions64Low: vec3, + @location(10) instanceStrokeWidths: f32, + @location(11) instanceColors: vec4, + @location(12) rowIndexes: u32, +}; + +struct Varyings { + @builtin(position) position: vec4, + @location(0) vColor: vec4, + @location(1) vCornerOffset: vec2, + @location(2) vMiterLength: f32, + @location(3) vPathPosition: vec2, + @location(4) vPathLength: f32, + @location(5) vJointType: f32, +}; + +fn flipIfTrue(flag: bool) -> f32 { + return select(1.0, -1.0, flag); +} + +fn clipLine(position: vec4, refPosition: vec4) -> vec4 { + if (position.w < EPSILON) { + let r = (EPSILON - refPosition.w) / (position.w - refPosition.w); + return refPosition + (position - refPosition) * r; + } + return position; +} + +fn getLineJoinOffset( + prevPoint: vec3, + currPoint: vec3, + nextPoint: vec3, + width: vec2, + positions: vec2, + instanceTypes: f32 +) -> JoinResult { + let isEnd = positions.x > 0.0; + let sideOfPath = positions.y; + let isJoint = select(0.0, 1.0, sideOfPath == 0.0); + + var deltaA3 = currPoint - prevPoint; + var deltaB3 = nextPoint - currPoint; + + let rotationResult = project_needs_rotation(currPoint); + if (path.billboard == 0.0 && rotationResult.needsRotation) { + deltaA3 = rotationResult.transform * deltaA3; + deltaB3 = rotationResult.transform * deltaB3; + } + + let deltaA = deltaA3.xy / width; + let deltaB = deltaB3.xy / width; + + let lenA = length(deltaA); + let lenB = length(deltaB); + + let dirA = select(vec2(0.0, 0.0), normalize(deltaA), lenA > 0.0); + let dirB = select(vec2(0.0, 0.0), normalize(deltaB), lenB > 0.0); + + let perpA = vec2(-dirA.y, dirA.x); + let perpB = vec2(-dirB.y, dirB.x); + + var tangent = dirA + dirB; + tangent = select(perpA, normalize(tangent), length(tangent) > 0.0); + let miterVec = vec2(-tangent.y, tangent.x); + let dir = select(dirB, dirA, isEnd); + let perp = select(perpB, perpA, isEnd); + let pathLength = select(lenB, lenA, isEnd); + + let sinHalfA = abs(dot(miterVec, perp)); + let cosHalfA = abs(dot(dirA, miterVec)); + let turnDirection = flipIfTrue(dirA.x * dirB.y >= dirA.y * dirB.x); + let cornerPosition = sideOfPath * turnDirection; + + var miterSize = 1.0 / max(sinHalfA, EPSILON); + miterSize = mix( + min(miterSize, max(lenA, lenB) / max(cosHalfA, EPSILON)), + miterSize, + step(0.0, cornerPosition) + ); + + var offsetVec = + mix(miterVec * miterSize, perp, step(0.5, cornerPosition)) * + (sideOfPath + isJoint * turnDirection); + + let isStartCap = lenA == 0.0 || (!isEnd && (instanceTypes == 1.0 || instanceTypes == 3.0)); + let isEndCap = lenB == 0.0 || (isEnd && (instanceTypes == 2.0 || instanceTypes == 3.0)); + let isCap = isStartCap || isEndCap; + + var jointType = path.jointType; + if (isCap) { + offsetVec = mix( + perp * sideOfPath, + dir * path.capType * 4.0 * flipIfTrue(isStartCap), + isJoint + ); + jointType = path.capType; + } + + var miterLength = dot(offsetVec, miterVec * turnDirection); + miterLength = select(miterLength, isJoint, isCap); + + let offsetFromStartOfPath = offsetVec + deltaA * select(0.0, 1.0, isEnd); + let pathPosition = vec2( + dot(offsetFromStartOfPath, perp), + dot(offsetFromStartOfPath, dir) + ); + let isValid = step(f32(instanceTypes), 3.5); + var offset = vec3(offsetVec * width * isValid, 0.0); + + if (path.billboard == 0.0 && rotationResult.needsRotation) { + offset = rotationResult.transform * offset; + } + + return JoinResult(offset, offsetVec, miterLength, pathPosition, pathLength, jointType); +} + +@vertex +fn vertexMain(attributes: Attributes) -> Varyings { + var varyings: Varyings; + + geometry.pickingColor = picking_getPickingColorFromIndex(attributes.rowIndexes); + + let isEnd = attributes.positions.x; + + let prevPosition = mix(attributes.instanceLeftPositions, attributes.instanceStartPositions, isEnd); + let prevPosition64Low = mix( + attributes.instanceLeftPositions64Low, + attributes.instanceStartPositions64Low, + isEnd + ); + let currPosition = mix(attributes.instanceStartPositions, attributes.instanceEndPositions, isEnd); + let currPosition64Low = mix( + attributes.instanceStartPositions64Low, + attributes.instanceEndPositions64Low, + isEnd + ); + let nextPosition = mix(attributes.instanceEndPositions, attributes.instanceRightPositions, isEnd); + let nextPosition64Low = mix( + attributes.instanceEndPositions64Low, + attributes.instanceRightPositions64Low, + isEnd + ); + + geometry.worldPosition = currPosition; + + let widthPixels = + clamp( + project_unit_size_to_pixel(attributes.instanceStrokeWidths * path.widthScale, path.widthUnits), + path.widthMinPixels, + path.widthMaxPixels + ) / 2.0; + + if (path.billboard != 0.0) { + var prevPositionScreen = project_position_to_clipspace(prevPosition, prevPosition64Low, ZERO_OFFSET); + var currPositionScreen = project_position_to_clipspace(currPosition, currPosition64Low, ZERO_OFFSET); + var nextPositionScreen = project_position_to_clipspace(nextPosition, nextPosition64Low, ZERO_OFFSET); + + prevPositionScreen = clipLine(prevPositionScreen, currPositionScreen); + nextPositionScreen = clipLine(nextPositionScreen, currPositionScreen); + currPositionScreen = clipLine(currPositionScreen, mix(nextPositionScreen, prevPositionScreen, isEnd)); + + let join = getLineJoinOffset( + prevPositionScreen.xyz / prevPositionScreen.w, + currPositionScreen.xyz / currPositionScreen.w, + nextPositionScreen.xyz / nextPositionScreen.w, + project_pixel_size_to_clipspace(vec2(widthPixels, widthPixels)), + attributes.positions, + attributes.instanceTypes + ); + + geometry.uv = join.pathPosition; + varyings.position = vec4( + currPositionScreen.xyz + join.offset * currPositionScreen.w, + currPositionScreen.w + ); + varyings.vCornerOffset = join.cornerOffset; + varyings.vMiterLength = join.miterLength; + varyings.vPathPosition = join.pathPosition; + varyings.vPathLength = join.pathLength; + varyings.vJointType = join.jointType; + } else { + let prevPositionCommon = project_position_vec3_f64(prevPosition, prevPosition64Low); + let currPositionCommon = project_position_vec3_f64(currPosition, currPosition64Low); + let nextPositionCommon = project_position_vec3_f64(nextPosition, nextPosition64Low); + + let width = vec2( + project_pixel_size_float(widthPixels), + project_pixel_size_float(widthPixels) + ); + let join = getLineJoinOffset( + prevPositionCommon, + currPositionCommon, + nextPositionCommon, + width, + attributes.positions, + attributes.instanceTypes + ); + + geometry.position = vec4(currPositionCommon + join.offset, 1.0); + geometry.uv = join.pathPosition; + varyings.position = project_common_position_to_clipspace(geometry.position); + varyings.vCornerOffset = join.cornerOffset; + varyings.vMiterLength = join.miterLength; + varyings.vPathPosition = join.pathPosition; + varyings.vPathLength = join.pathLength; + varyings.vJointType = join.jointType; + } + + varyings.vColor = vec4( + attributes.instanceColors.rgb, + attributes.instanceColors.a * layer.opacity + ); + return varyings; +} + +@fragment +fn fragmentMain(varyings: Varyings) -> @location(0) vec4 { + geometry.uv = varyings.vPathPosition; + + if (varyings.vPathPosition.y < 0.0 || varyings.vPathPosition.y > varyings.vPathLength) { + if (varyings.vJointType > 0.5 && length(varyings.vCornerOffset) > 1.0) { + discard; + } + if (varyings.vJointType < 0.5 && varyings.vMiterLength > path.miterLimit + 1.0) { + discard; + } + } + + return deckgl_premultiplied_alpha(varyings.vColor); +} +`; diff --git a/modules/layers/src/path-layer/path-tesselator.ts b/modules/layers/src/path-layer/path-tesselator.ts index f1ad0fb5818..898425bf6f6 100644 --- a/modules/layers/src/path-layer/path-tesselator.ts +++ b/modules/layers/src/path-layer/path-tesselator.ts @@ -22,6 +22,7 @@ export default class PathTesselator extends Tesselator< resolution?: number; wrapLongitude?: boolean; loop?: boolean; + webgpu?: boolean; } > { constructor(opts) { @@ -36,7 +37,8 @@ export default class PathTesselator extends Tesselator< initialize: true, type: opts.fp64 ? Float64Array : Float32Array }, - segmentTypes: {size: 1, type: Uint8ClampedArray} + // WebGPU consumes `instanceTypes` as f32; the WebGL path keeps its compact uint8 buffer. + segmentTypes: {size: 1, type: opts.webgpu ? Float32Array : Uint8ClampedArray} } }); } diff --git a/modules/layers/src/solid-polygon-layer/solid-polygon-layer-uniforms.ts b/modules/layers/src/solid-polygon-layer/solid-polygon-layer-uniforms.ts index 25f512bb9c3..fe666981bf2 100644 --- a/modules/layers/src/solid-polygon-layer/solid-polygon-layer-uniforms.ts +++ b/modules/layers/src/solid-polygon-layer/solid-polygon-layer-uniforms.ts @@ -20,6 +20,7 @@ export type SolidPolygonProps = { export const solidPolygonUniforms = { name: 'solidPolygon', + source: '', vs: uniformBlock, fs: uniformBlock, uniformTypes: { diff --git a/modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts b/modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts index 3289ef01076..03ca47ff518 100644 --- a/modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts +++ b/modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts @@ -2,8 +2,9 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Layer, project32, picking, gouraudMaterial} from '@deck.gl/core'; +import {Layer, color, project32, picking, gouraudMaterial} from '@deck.gl/core'; import {Model, Geometry} from '@luma.gl/engine'; +import type {BufferLayout} from '@luma.gl/core'; // Polygon geometry generation is managed by the polygon tesselator import PolygonTesselator from './polygon-tesselator'; @@ -12,6 +13,7 @@ import {solidPolygonUniforms, SolidPolygonProps} from './solid-polygon-layer-uni import vsTop from './solid-polygon-layer-vertex-top.glsl'; import vsSide from './solid-polygon-layer-vertex-side.glsl'; import fs from './solid-polygon-layer-fragment.glsl'; +import {getSolidPolygonShaderWGSL} from './solid-polygon-layer.wgsl'; import type { LayerProps, @@ -133,13 +135,21 @@ export default class SolidPolygonLayer }; getShaders(type) { + const ringWindingOrderCW = !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1; + const isWebGPU = this.context.device.type === 'webgpu'; + return super.getShaders({ vs: type === 'top' ? vsTop : vsSide, fs, - defines: { - RING_WINDING_ORDER_CW: !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1 - }, - modules: [project32, gouraudMaterial, picking, solidPolygonUniforms] + source: isWebGPU ? getSolidPolygonShaderWGSL(type, Boolean(ringWindingOrderCW)) : undefined, + ...(isWebGPU + ? {} + : { + defines: { + RING_WINDING_ORDER_CW: ringWindingOrderCW + } + }), + modules: [project32, color, gouraudMaterial, picking, solidPolygonUniforms] }); } @@ -182,6 +192,7 @@ export default class SolidPolygonLayer const attributeManager = this.getAttributeManager()!; const noAlloc = true; + const isWebGPU = this.context.device.type === 'webgpu'; /* eslint-disable max-len */ attributeManager.add({ @@ -202,15 +213,34 @@ export default class SolidPolygonLayer // eslint-disable-next-line @typescript-eslint/unbound-method update: this.calculatePositions, noAlloc, - shaderAttributes: { - nextVertexPositions: { - vertexOffset: 1 - } - } + ...(isWebGPU + ? {} + : { + shaderAttributes: { + nextVertexPositions: { + vertexOffset: 1 + } + } + }) }, + ...(isWebGPU + ? { + nextVertexPositions: { + size: 3, + type: 'float64', + stepMode: 'dynamic', + fp64: this.use64bitPositions(), + transition: ATTRIBUTE_TRANSITION, + accessor: 'getPolygon', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateNextPositions, + noAlloc + } + } + : {}), instanceVertexValid: { size: 1, - type: 'uint16', + ...(isWebGPU ? {} : {type: 'uint16'}), stepMode: 'instance', // eslint-disable-next-line @typescript-eslint/unbound-method update: this.calculateVertexValid, @@ -377,8 +407,21 @@ export default class SolidPolygonLayer if (filled) { const shaders = this.getShaders('top'); - shaders.defines.NON_INSTANCED_MODEL = 1; - const bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: false}); + shaders.defines = {...shaders.defines, NON_INSTANCED_MODEL: 1}; + let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: false}); + if (this.context.device.type === 'webgpu') { + bufferLayout = filterBufferLayout( + bufferLayout, + new Set([ + 'vertexPositions', + 'vertexPositions64Low', + 'elevations', + 'fillColors', + 'lineColors', + 'rowIndexes' + ]) + ); + } topModel = new Model(this.context.device, { ...shaders, @@ -392,7 +435,23 @@ export default class SolidPolygonLayer }); } if (extruded) { - const bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true}); + let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true}); + if (this.context.device.type === 'webgpu') { + bufferLayout = filterBufferLayout( + bufferLayout, + new Set([ + 'vertexPositions', + 'vertexPositions64Low', + 'nextVertexPositions', + 'nextVertexPositions64Low', + 'instanceVertexValid', + 'elevations', + 'fillColors', + 'lineColors', + 'rowIndexes' + ]) + ); + } sideModel = new Model(this.context.device, { ...this.getShaders('side'), @@ -456,6 +515,55 @@ export default class SolidPolygonLayer } protected calculateVertexValid(attribute) { - attribute.value = this.state.polygonTesselator.get('vertexValid'); + const vertexValid = this.state.polygonTesselator.get('vertexValid'); + attribute.value = + this.context.device.type === 'webgpu' && vertexValid + ? Float32Array.from(vertexValid) + : vertexValid; } + + protected calculateNextPositions(attribute) { + const {polygonTesselator} = this.state; + const positions = polygonTesselator.get('positions'); + attribute.startIndices = polygonTesselator.vertexStarts; + + if (!positions) { + attribute.value = positions; + return; + } + + const ArrayType = positions.constructor as typeof Float32Array; + const nextPositions = new ArrayType(positions.length); + + for (let i = 0; i < positions.length; i += 3) { + const nextIndex = i + 3 < positions.length ? i + 3 : i; + nextPositions[i] = positions[nextIndex]; + nextPositions[i + 1] = positions[nextIndex + 1]; + nextPositions[i + 2] = positions[nextIndex + 2]; + } + + attribute.value = nextPositions; + } +} + +function filterBufferLayout( + bufferLayout: BufferLayout[], + allowedAttributes: Set +): BufferLayout[] { + const filteredLayouts: BufferLayout[] = []; + + for (const layout of bufferLayout) { + if (layout.attributes) { + const attributes = layout.attributes.filter(attribute => + allowedAttributes.has(attribute.attribute) + ); + if (attributes.length) { + filteredLayouts.push({...layout, attributes}); + } + } else if (allowedAttributes.has(layout.name)) { + filteredLayouts.push(layout); + } + } + + return filteredLayouts; } diff --git a/modules/layers/src/solid-polygon-layer/solid-polygon-layer.wgsl.ts b/modules/layers/src/solid-polygon-layer/solid-polygon-layer.wgsl.ts new file mode 100644 index 00000000000..6c3d3e51257 --- /dev/null +++ b/modules/layers/src/solid-polygon-layer/solid-polygon-layer.wgsl.ts @@ -0,0 +1,233 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +type SolidPolygonShaderType = 'top' | 'side'; + +function getSolidPolygonVertexHelpers() { + return /* wgsl */ `\ +struct SolidPolygonUniforms { + extruded: f32, + isWireframe: f32, + elevationScale: f32, +}; + +@group(0) @binding(0) var solidPolygon: SolidPolygonUniforms; + +fn project_offset_normal(vector: vec3) -> vec3 { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || + project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT_OFFSETS) { + return normalize(vector * project.commonUnitsPerWorldUnit); + } + return project_normal(vector); +} + +fn apply_polygon_color( + colors: vec4, + normal: vec3, + position: vec4 +) -> vec4 { + if (solidPolygon.extruded > 0.5) { + let lightColor = lighting_getLightColor2( + colors.rgb, + project.cameraPosition, + position.xyz, + normal + ); + return vec4(lightColor, colors.a * layer.opacity); + } + return vec4(colors.rgb, colors.a * layer.opacity); +} +`; +} + +function getSolidPolygonFragmentMain() { + return /* wgsl */ `\ +@fragment +fn fragmentMain(inp: Varyings) -> @location(0) vec4 { + geometry.uv = vec2(0.0, 0.0); + + if (picking.isActive > 0.5) { + if (!picking_isColorValid(inp.pickingColor)) { + discard; + } + return vec4(inp.pickingColor, 1.0); + } + + var fragColor = inp.vColor; + + if (picking.isHighlightActive > 0.5) { + let highlightedObjectColor = picking_normalizeColor(picking.highlightedObjectColor); + if (picking_isColorZero(abs(inp.pickingColor - highlightedObjectColor))) { + let highLightAlpha = picking.highlightColor.a; + let blendedAlpha = highLightAlpha + fragColor.a * (1.0 - highLightAlpha); + if (blendedAlpha > 0.0) { + let highLightRatio = highLightAlpha / blendedAlpha; + fragColor = vec4( + mix(fragColor.rgb, picking.highlightColor.rgb, highLightRatio), + blendedAlpha + ); + } else { + fragColor = vec4(fragColor.rgb, 0.0); + } + } + } + + return deckgl_premultiplied_alpha(fragColor); +} +`; +} + +function getTopShaderWGSL() { + return /* wgsl */ `\ +${getSolidPolygonVertexHelpers()} + +struct Attributes { + @location(0) vertexPositions: vec3, + @location(1) vertexPositions64Low: vec3, + @location(2) elevations: f32, + @location(3) fillColors: vec4, + @location(4) lineColors: vec4, + @location(5) rowIndexes: u32, +}; + +struct Varyings { + @builtin(position) position: vec4, + @location(0) vColor: vec4, + @location(1) pickingColor: vec3, +}; + +@vertex +fn vertexMain(attributes: Attributes) -> Varyings { + var outp: Varyings; + + var pos = attributes.vertexPositions; + if (solidPolygon.extruded > 0.5) { + pos.z += attributes.elevations * solidPolygon.elevationScale; + } + + geometry.worldPosition = attributes.vertexPositions; + geometry.pickingColor = picking_getPickingColorFromIndex(attributes.rowIndexes); + + let projectedPosition = project_position_to_clipspace_and_commonspace( + pos, + attributes.vertexPositions64Low, + vec3(0.0) + ); + geometry.position = projectedPosition.commonPosition; + outp.position = projectedPosition.clipPosition; + + let normal = project_normal(vec3(0.0, 0.0, 1.0)); + geometry.normal = normal; + + let colors = select( + attributes.fillColors, + attributes.lineColors, + solidPolygon.isWireframe > 0.5 + ); + outp.vColor = apply_polygon_color(colors, normal, geometry.position); + outp.pickingColor = geometry.pickingColor; + + return outp; +} + +${getSolidPolygonFragmentMain()} +`; +} + +function getSideShaderWGSL(ringWindingOrderCW: boolean) { + return /* wgsl */ `\ +const RING_WINDING_ORDER_CW: bool = ${ringWindingOrderCW ? 'true' : 'false'}; + +${getSolidPolygonVertexHelpers()} + +struct Attributes { + @location(0) positions: vec2, + @location(1) vertexPositions: vec3, + @location(2) vertexPositions64Low: vec3, + @location(3) nextVertexPositions: vec3, + @location(4) nextVertexPositions64Low: vec3, + @location(5) instanceVertexValid: f32, + @location(6) elevations: f32, + @location(7) fillColors: vec4, + @location(8) lineColors: vec4, + @location(9) rowIndexes: u32, +}; + +struct Varyings { + @builtin(position) position: vec4, + @location(0) vColor: vec4, + @location(1) pickingColor: vec3, +}; + +@vertex +fn vertexMain(attributes: Attributes) -> Varyings { + var outp: Varyings; + outp.position = vec4(0.0); + outp.vColor = vec4(0.0); + outp.pickingColor = picking_getPickingColorFromIndex(attributes.rowIndexes); + + if (attributes.instanceVertexValid < 0.5) { + return outp; + } + + let pos = select(attributes.nextVertexPositions, attributes.vertexPositions, RING_WINDING_ORDER_CW); + let pos64Low = select( + attributes.nextVertexPositions64Low, + attributes.vertexPositions64Low, + RING_WINDING_ORDER_CW + ); + let nextPos = select(attributes.vertexPositions, attributes.nextVertexPositions, RING_WINDING_ORDER_CW); + let nextPos64Low = select( + attributes.vertexPositions64Low, + attributes.nextVertexPositions64Low, + RING_WINDING_ORDER_CW + ); + + let position = mix(pos, nextPos, attributes.positions.x); + let position64Low = mix(pos64Low, nextPos64Low, attributes.positions.x); + + var worldPosition = position; + if (solidPolygon.extruded > 0.5) { + worldPosition.z += attributes.elevations * attributes.positions.y * solidPolygon.elevationScale; + } + + geometry.worldPosition = position; + geometry.pickingColor = picking_getPickingColorFromIndex(attributes.rowIndexes); + + let projectedPosition = project_position_to_clipspace_and_commonspace( + worldPosition, + position64Low, + vec3(0.0) + ); + geometry.position = projectedPosition.commonPosition; + outp.position = projectedPosition.clipPosition; + + let normal = project_offset_normal(vec3( + pos.y - nextPos.y + (pos64Low.y - nextPos64Low.y), + nextPos.x - pos.x + (nextPos64Low.x - pos64Low.x), + 0.0 + )); + geometry.normal = normal; + + let colors = select( + attributes.fillColors, + attributes.lineColors, + solidPolygon.isWireframe > 0.5 + ); + outp.vColor = apply_polygon_color(colors, normal, geometry.position); + outp.pickingColor = geometry.pickingColor; + + return outp; +} + +${getSolidPolygonFragmentMain()} +`; +} + +export function getSolidPolygonShaderWGSL( + type: SolidPolygonShaderType, + ringWindingOrderCW: boolean +): string { + return type === 'top' ? getTopShaderWGSL() : getSideShaderWGSL(ringWindingOrderCW); +} diff --git a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts index 8762f175172..427e826dc12 100644 --- a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts +++ b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts @@ -10,6 +10,7 @@ import {TextModuleProps, textUniforms, ContentAlignModes} from '../text-uniforms import vs from './multi-icon-layer-vertex.glsl'; import fs from './multi-icon-layer-fragment.glsl'; +import {shaderWGSL as source} from './multi-icon-layer.wgsl'; import type {IconLayerProps} from '../../icon-layer/icon-layer'; import type { @@ -69,7 +70,13 @@ export default class MultiIconLayer extends getShaders() { const shaders = super.getShaders(); - return {...shaders, modules: [...shaders.modules, textUniforms, sdfUniforms], vs, fs}; + return { + ...shaders, + modules: [...shaders.modules, textUniforms, sdfUniforms], + vs, + fs, + source + }; } initializeState() { @@ -84,10 +91,12 @@ export default class MultiIconLayer extends rowIndexes: { type: 'uint32', size: 1, + bufferGroup: 'icon-instance-data', accessor: (object, {index}) => index }, instanceClipRect: { size: 4, + bufferGroup: 'icon-instance-data', accessor: 'getContentBox', defaultValue: [0, 0, -1, -1] } diff --git a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.wgsl.ts b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.wgsl.ts new file mode 100644 index 00000000000..ef591a7d1ff --- /dev/null +++ b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.wgsl.ts @@ -0,0 +1,336 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export function getShaderWGSL({collision = false}: {collision?: boolean} = {}): string { + return /* wgsl */ `\ +struct IconUniforms { + sizeScale: f32, + iconsTextureDim: vec2, + sizeBasis: f32, + sizeMinPixels: f32, + sizeMaxPixels: f32, + billboard: i32, + sizeUnits: i32, + alphaCutoff: f32 +}; + +struct TextUniforms { + cutoffPixels: vec2, + align: vec2, + fontSize: f32, + flipY: f32 +}; + +struct SdfUniforms { + gamma: f32, + enabled: f32, + buffer: f32, + outlineBuffer: f32, + outlineColor: vec4 +}; + +${ + collision + ? `\ +struct CollisionUniforms { + sort: i32, + enabled: i32 +}; +` + : '' +} + +const ALIGN_MODE_START: i32 = 1; +const ALIGN_MODE_CENTER: i32 = 2; +const ALIGN_MODE_END: i32 = 3; + +@group(0) @binding(auto) var icon: IconUniforms; +@group(0) @binding(auto) var text: TextUniforms; +@group(0) @binding(auto) var sdf: SdfUniforms; +${collision ? '@group(0) @binding(auto) var collision: CollisionUniforms;' : ''} +@group(0) @binding(auto) var iconsTexture : texture_2d; +@group(0) @binding(auto) var iconsTextureSampler : sampler; +${ + collision + ? `\ +@group(0) @binding(auto) var collision_texture : texture_2d; +` + : '' +} + +fn rotate_by_angle(vertex: vec2, angle_deg: f32) -> vec2 { + let angle_radian = angle_deg * PI / 180.0; + let c = cos(angle_radian); + let s = sin(angle_radian); + let rotation = mat2x2(vec2(c, -s), vec2(s, c)); + return rotation * vertex; +} + +fn get_pixel_offset_from_alignment( + anchor: f32, + extent: f32, + clipStart: f32, + clipEnd: f32, + mode: i32 +) -> f32 { + if (clipEnd < clipStart) { + return 0.0; + } + if (mode == ALIGN_MODE_START) { + return max(-(anchor + clipStart), 0.0); + } + if (mode == ALIGN_MODE_CENTER) { + let minValue = max(0.0, anchor + clipStart); + let maxValue = min(extent, anchor + clipEnd); + if (minValue < maxValue) { + return (minValue + maxValue) / 2.0 - anchor; + } + return 0.0; + } + if (mode == ALIGN_MODE_END) { + return min(extent - (anchor + clipEnd), 0.0); + } + return 0.0; +} + +${ + collision + ? `\ +fn collision_match(texCoords: vec2, pickingColor: vec3) -> f32 { + let textureSize = vec2(textureDimensions(collision_texture)); + let pixelCoords = clamp( + vec2(texCoords * vec2(textureSize)), + vec2(0), + textureSize - vec2(1) + ); + let collisionPickingColor = textureLoad(collision_texture, pixelCoords, 0); + let delta = dot(abs(collisionPickingColor.rgb - pickingColor), vec3(1.0)); + return step(delta, 0.001); +} + +fn collision_is_visible(texCoords: vec2, pickingColor: vec3) -> f32 { + if (collision.enabled == 0) { + return 1.0; + } + + var accumulator = 0.0; + let stepSize = vec2(1.0) / project.viewportSize; + + for (var i: i32 = -2; i <= 2; i = i + 1) { + for (var j: i32 = -2; j <= 2; j = j + 1) { + let delta = vec2(f32(j), f32(i)) * stepSize; + accumulator = accumulator + collision_match(texCoords + delta, pickingColor); + } + } + + return pow(accumulator / 25.0, 2.2); +} +` + : '' +} + +struct Attributes { + @location(0) positions: vec2, + + @location(1) instancePositions: vec3, + @location(2) instancePositions64Low: vec3, + @location(3) instanceSizes: f32, + @location(4) instanceAngles: f32, + @location(5) instanceColors: vec4, + @location(6) instanceIconFrames: vec4, + @location(7) instanceColorModes: f32, + @location(8) instanceOffsets: vec2, + @location(9) instancePixelOffset: vec2, + @location(10) rowIndexes: u32, + @location(11) instanceClipRect: vec4, + ${collision ? '@location(12) collisionPriorities: f32,' : ''} +}; + +struct Varyings { + @builtin(position) position: vec4, + + @location(0) vColorMode: f32, + @location(1) vColor: vec4, + @location(2) vTextureCoords: vec2, + @location(3) uv: vec2, + @location(4) pickingColor: vec3, +}; + +@vertex +fn vertexMain(inp: Attributes) -> Varyings { + geometry.worldPosition = inp.instancePositions; + geometry.uv = inp.positions; + geometry.pickingColor = picking_getPickingColorFromIndex(inp.rowIndexes); + + var outp: Varyings; + outp.uv = inp.positions; + + let iconSize = inp.instanceIconFrames.zw; + + let sizePixels = clamp( + project_unit_size_to_pixel(inp.instanceSizes * icon.sizeScale, icon.sizeUnits), + icon.sizeMinPixels, icon.sizeMaxPixels + ); + let instanceScale = sizePixels / text.fontSize; + + var pixelOffset = inp.positions / 2.0 * iconSize + inp.instanceOffsets; + pixelOffset = rotate_by_angle(pixelOffset, inp.instanceAngles) * instanceScale; + pixelOffset = pixelOffset + inp.instancePixelOffset; + pixelOffset.y = pixelOffset.y * -1.0; + + var pos: vec4; + var anchorPosScreen: vec2; + if (icon.billboard != 0) { + pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, vec3(0.0)); + anchorPosScreen = pos.xy / pos.w; + + let clipOffset = project_pixel_size_to_clipspace(pixelOffset); + pos = vec4(pos.x + clipOffset.x, pos.y + clipOffset.y, pos.z, pos.w); + } else { + var offsetCommon = vec3(project_pixel_size_vec2(pixelOffset), 0.0); + if (text.flipY > 0.5) { + offsetCommon.y = offsetCommon.y * -1.0; + } + let anchorPos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, vec3(0.0)); + anchorPosScreen = anchorPos.xy / anchorPos.w; + pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, offsetCommon); + } + + anchorPosScreen = vec2(anchorPosScreen.x + 1.0, 1.0 - anchorPosScreen.y) / 2.0 * + project.viewportSize / project.devicePixelRatio; + var xy = project_size_vec2(inp.instanceClipRect.xy) * project.scale; + var wh = project_size_vec2(inp.instanceClipRect.zw) * project.scale; + + if (text.flipY > 0.5) { + xy.y = -xy.y - wh.y; + } + if (text.align.x > 0 || text.align.y > 0) { + let viewportPixels = project.viewportSize / project.devicePixelRatio; + let scrollPixels = vec2( + get_pixel_offset_from_alignment(anchorPosScreen.x, viewportPixels.x, xy.x, xy.x + wh.x, text.align.x), + -get_pixel_offset_from_alignment(anchorPosScreen.y, viewportPixels.y, -xy.y - wh.y, -xy.y, text.align.y) + ); + pixelOffset = pixelOffset + scrollPixels; + let scrollClipOffset = project_pixel_size_to_clipspace(scrollPixels); + pos.x = pos.x + scrollClipOffset.x; + pos.y = pos.y + scrollClipOffset.y; + } + + if (inp.instanceClipRect.z >= 0.0) { + if (pixelOffset.x < xy.x || pixelOffset.x > xy.x + wh.x) { + pos = vec4(0.0); + } else if (text.cutoffPixels.x > 0.0) { + let viewportWidth = project.viewportSize.x / project.devicePixelRatio; + let left = max(anchorPosScreen.x + xy.x, 0.0); + let right = min(anchorPosScreen.x + xy.x + wh.x, viewportWidth); + if (right - left < text.cutoffPixels.x) { + pos = vec4(0.0); + } + } + } + if (inp.instanceClipRect.w >= 0.0) { + if (pixelOffset.y < xy.y || pixelOffset.y > xy.y + wh.y) { + pos = vec4(0.0); + } else if (text.cutoffPixels.y > 0.0) { + let viewportHeight = project.viewportSize.y / project.devicePixelRatio; + let top = max(anchorPosScreen.y - xy.y - wh.y, 0.0); + let bottom = min(anchorPosScreen.y - xy.y, viewportHeight); + if (bottom - top < text.cutoffPixels.y) { + pos = vec4(0.0); + } + } + } + + ${ + collision + ? `\ + if (collision.sort != 0) { + pos.z = -0.001 * inp.collisionPriorities * pos.w; + } + ` + : '' + } + + let uvMix = (inp.positions.xy + vec2(1.0, 1.0)) * 0.5; + outp.vTextureCoords = mix(inp.instanceIconFrames.xy, inp.instanceIconFrames.xy + iconSize, uvMix) / icon.iconsTextureDim; + + outp.position = pos; + outp.vColor = inp.instanceColors; + outp.vColorMode = inp.instanceColorModes; + outp.pickingColor = picking_getPickingColorFromIndex(inp.rowIndexes); + + return outp; +} + +@fragment +fn fragmentMain(inp: Varyings) -> @location(0) vec4 { + geometry.uv = inp.uv; + + let texColor = textureSample(iconsTexture, iconsTextureSampler, inp.vTextureCoords); + var alpha = texColor.a; + var color = inp.vColor; + + if (sdf.enabled > 0.5) { + let distance = alpha; + alpha = smoothstep(sdf.buffer - sdf.gamma, sdf.buffer + sdf.gamma, distance); + + if (sdf.outlineBuffer > 0.0) { + let inFill = alpha; + let inBorder = smoothstep(sdf.outlineBuffer - sdf.gamma, sdf.outlineBuffer + sdf.gamma, distance); + color = mix(sdf.outlineColor, inp.vColor, inFill); + alpha = inBorder; + } + } else if (inp.vColorMode == 0.0) { + color = texColor; + } + + var a = alpha * color.a * layer.opacity; + if (a < icon.alphaCutoff) { + discard; + } + + if (picking.isActive > 0.5) { + if (!picking_isColorValid(inp.pickingColor)) { + discard; + } + return vec4(inp.pickingColor, 1.0); + } + + ${ + collision + ? `\ + let collisionFade = collision_is_visible(inp.position.xy / project.viewportSize, inp.pickingColor); + a = a * collisionFade; + if (a <= 0.0001) { + discard; + } + ` + : '' + } + + var fragColor = deckgl_premultiplied_alpha(vec4(color.rgb, a)); + + if (picking.isHighlightActive > 0.5) { + let highlightedObjectColor = picking_normalizeColor(picking.highlightedObjectColor); + if (picking_isColorZero(abs(inp.pickingColor - highlightedObjectColor))) { + let highLightAlpha = picking.highlightColor.a; + let blendedAlpha = highLightAlpha + fragColor.a * (1.0 - highLightAlpha); + if (blendedAlpha > 0.0) { + let highLightRatio = highLightAlpha / blendedAlpha; + fragColor = vec4( + mix(fragColor.rgb, picking.highlightColor.rgb, highLightRatio), + blendedAlpha + ); + } else { + fragColor = vec4(fragColor.rgb, 0.0); + } + } + } + + return fragColor; +} +`; +} + +export const shaderWGSL = getShaderWGSL(); diff --git a/modules/layers/src/text-layer/text-background-layer/text-background-layer.ts b/modules/layers/src/text-layer/text-background-layer/text-background-layer.ts index 0b694a7797f..45ce093edca 100644 --- a/modules/layers/src/text-layer/text-background-layer/text-background-layer.ts +++ b/modules/layers/src/text-layer/text-background-layer/text-background-layer.ts @@ -10,6 +10,7 @@ import {TextBackgroundProps, textBackgroundUniforms} from './text-background-lay import {TextModuleProps, textUniforms} from '../text-uniforms'; import vs from './text-background-layer-vertex.glsl'; import fs from './text-background-layer-fragment.glsl'; +import {shaderWGSL as source} from './text-background-layer.wgsl'; import type { LayerProps, @@ -84,6 +85,7 @@ export default class TextBackgroundLayer, + padding: vec4, + sizeUnits: i32, + stroked: f32 +}; + +struct TextUniforms { + cutoffPixels: vec2, + align: vec2, + fontSize: f32, + flipY: f32 +}; + +@group(0) @binding(auto) var textBackground: TextBackgroundUniforms; +@group(0) @binding(auto) var text: TextUniforms; + +fn rotate_by_angle(vertex: vec2, angle_deg: f32) -> vec2 { + let angle_radian = angle_deg * PI / 180.0; + let c = cos(angle_radian); + let s = sin(angle_radian); + let rotation = mat2x2(vec2(c, -s), vec2(s, c)); + return rotation * vertex; +} + +fn round_rect(p: vec2, size: vec2, radii: vec4) -> f32 { + let pixelPositionCB = (p - vec2(0.5)) * size; + let sizeCB = size * 0.5; + + let maxBorderRadius = min(size.x, size.y) * 0.5; + let borderRadius = min(radii, vec4(maxBorderRadius)); + let xRadii = select(borderRadius.zw, borderRadius.xy, pixelPositionCB.x > 0.0); + let radius = select(xRadii.y, xRadii.x, pixelPositionCB.y > 0.0); + let q = abs(pixelPositionCB) - sizeCB + radius; + return -(min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - radius); +} + +fn rect(p: vec2, size: vec2) -> f32 { + let pixelPosition = p * size; + return min( + min(pixelPosition.x, size.x - pixelPosition.x), + min(pixelPosition.y, size.y - pixelPosition.y) + ); +} + +fn premultiplied_alpha(color: vec4) -> vec4 { + return vec4(color.rgb * color.a, color.a); +} + +struct Attributes { + @builtin(instance_index) instanceIndex: u32, + @location(0) positions: vec2, + + @location(1) instancePositions: vec3, + @location(2) instancePositions64Low: vec3, + @location(3) instanceSizes: f32, + @location(4) instanceAngles: f32, + @location(5) instanceRects: vec4, + @location(6) instanceClipRect: vec4, + @location(7) instancePixelOffsets: vec2, + @location(8) instanceFillColors: vec4, + @location(9) instanceLineColors: vec4, + @location(10) instanceLineWidths: f32, +}; + +struct Varyings { + @builtin(position) position: vec4, + + @location(0) vFillColor: vec4, + @location(1) vLineColor: vec4, + @location(2) vLineWidth: f32, + @location(3) uv: vec2, + @location(4) dimensions: vec2, + @location(5) pickingColor: vec3, +}; + +@vertex +fn vertexMain(inp: Attributes) -> Varyings { + geometry.worldPosition = inp.instancePositions; + geometry.uv = inp.positions; + geometry.pickingColor = picking_getPickingColorFromIndex(inp.instanceIndex); + + var outp: Varyings; + outp.uv = inp.positions; + outp.vLineWidth = inp.instanceLineWidths; + + let sizePixels = clamp( + project_unit_size_to_pixel(inp.instanceSizes * textBackground.sizeScale, textBackground.sizeUnits), + textBackground.sizeMinPixels, + textBackground.sizeMaxPixels + ); + let instanceScale = sizePixels / text.fontSize; + + outp.dimensions = inp.instanceRects.zw * instanceScale + textBackground.padding.xy + textBackground.padding.zw; + + var pixelOffset = + (inp.positions * inp.instanceRects.zw + inp.instanceRects.xy) * instanceScale + + mix(-textBackground.padding.xy, textBackground.padding.zw, inp.positions); + pixelOffset = rotate_by_angle(pixelOffset, inp.instanceAngles); + pixelOffset = pixelOffset + inp.instancePixelOffsets; + pixelOffset.y = pixelOffset.y * -1.0; + + var xy = project_size_vec2(inp.instanceClipRect.xy) * project.scale; + let wh = project_size_vec2(inp.instanceClipRect.zw) * project.scale; + if (text.flipY > 0.5) { + xy.y = -xy.y - wh.y; + } + if (inp.instanceClipRect.z >= 0.0) { + outp.dimensions.x = wh.x; + pixelOffset.x = + xy.x + outp.uv.x * wh.x + mix(-textBackground.padding.x, textBackground.padding.z, outp.uv.x); + } + if (inp.instanceClipRect.w >= 0.0) { + outp.dimensions.y = wh.y; + pixelOffset.y = + xy.y + outp.uv.y * wh.y + mix(-textBackground.padding.y, textBackground.padding.w, outp.uv.y); + } + + if (textBackground.billboard > 0.5) { + var pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, vec3(0.0)); + let clipOffset = project_pixel_size_to_clipspace(pixelOffset); + pos = vec4(pos.x + clipOffset.x, pos.y + clipOffset.y, pos.z, pos.w); + outp.position = pos; + } else { + var offsetCommon = vec3(project_pixel_size_vec2(pixelOffset), 0.0); + if (text.flipY > 0.5) { + offsetCommon.y = offsetCommon.y * -1.0; + } + outp.position = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, offsetCommon); + } + + outp.vFillColor = vec4(inp.instanceFillColors.rgb, inp.instanceFillColors.a * layer.opacity); + outp.vLineColor = vec4(inp.instanceLineColors.rgb, inp.instanceLineColors.a * layer.opacity); + outp.pickingColor = picking_getPickingColorFromIndex(inp.instanceIndex); + + return outp; +} + +fn get_stroked_frag_color(dist: f32, lineWidth: f32, fillColor: vec4, lineColor: vec4) -> vec4 { + let isBorder = smoothedge(dist, lineWidth); + return mix(fillColor, lineColor, isBorder); +} + +@fragment +fn fragmentMain(inp: Varyings) -> @location(0) vec4 { + geometry.uv = inp.uv; + + var fragColor: vec4; + if (any(textBackground.borderRadius != vec4(0.0))) { + let distToEdge = round_rect(inp.uv, inp.dimensions, textBackground.borderRadius); + if (textBackground.stroked > 0.5) { + fragColor = get_stroked_frag_color(distToEdge, inp.vLineWidth, inp.vFillColor, inp.vLineColor); + } else { + fragColor = inp.vFillColor; + } + let shapeAlpha = smoothedge(-distToEdge, 0.0); + fragColor.a = fragColor.a * shapeAlpha; + } else { + if (textBackground.stroked > 0.5) { + let distToEdge = rect(inp.uv, inp.dimensions); + fragColor = get_stroked_frag_color(distToEdge, inp.vLineWidth, inp.vFillColor, inp.vLineColor); + } else { + fragColor = inp.vFillColor; + } + } + + if (picking.isActive > 0.5) { + if (!picking_isColorValid(inp.pickingColor)) { + discard; + } + return vec4(inp.pickingColor, 1.0); + } + + if (picking.isHighlightActive > 0.5) { + let highlightedObjectColor = picking_normalizeColor(picking.highlightedObjectColor); + if (picking_isColorZero(abs(inp.pickingColor - highlightedObjectColor))) { + let highLightAlpha = picking.highlightColor.a; + let blendedAlpha = highLightAlpha + fragColor.a * (1.0 - highLightAlpha); + if (blendedAlpha > 0.0) { + let highLightRatio = highLightAlpha / blendedAlpha; + fragColor = vec4( + mix(fragColor.rgb, picking.highlightColor.rgb, highLightRatio), + blendedAlpha + ); + } else { + fragColor = vec4(fragColor.rgb, 0.0); + } + } + } + + return premultiplied_alpha(fragColor); +} +`; diff --git a/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-uniforms.ts b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-uniforms.ts index c449f3c9c0c..8d4a855bfe9 100644 --- a/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-uniforms.ts +++ b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-uniforms.ts @@ -5,13 +5,26 @@ import type {Matrix4} from '@math.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; -const uniformBlock = `\ +const uniformBlockWGSL = /* wgsl */ `\ +struct ScenegraphUniforms { + sizeScale: f32, + sizeMinPixels: f32, + sizeMaxPixels: f32, + sceneModelMatrix: mat4x4, + composeModelMatrix: f32, +}; + +@group(0) @binding(20) +var scenegraph: ScenegraphUniforms; +`; + +const uniformBlockGLSL = /* glsl */ `\ layout(std140) uniform scenegraphUniforms { float sizeScale; float sizeMinPixels; float sizeMaxPixels; mat4 sceneModelMatrix; - bool composeModelMatrix; + float composeModelMatrix; } scenegraph; `; @@ -20,13 +33,14 @@ export type ScenegraphProps = { sizeMinPixels: number; sizeMaxPixels: number; sceneModelMatrix: Matrix4; - composeModelMatrix: boolean; + composeModelMatrix: number; }; export const scenegraphUniforms = { name: 'scenegraph', - vs: uniformBlock, - fs: uniformBlock, + source: uniformBlockWGSL, + vs: uniformBlockGLSL, + fs: uniformBlockGLSL, uniformTypes: { sizeScale: 'f32', sizeMinPixels: 'f32', diff --git a/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-vertex.glsl.ts b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-vertex.glsl.ts index fa94b0f60d1..f781c7caff7 100644 --- a/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-vertex.glsl.ts +++ b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer-vertex.glsl.ts @@ -58,9 +58,10 @@ void main(void) { float originalSize = project_size_to_pixel(scenegraph.sizeScale); float clampedSize = clamp(originalSize, scenegraph.sizeMinPixels, scenegraph.sizeMaxPixels); + float sizeRatio = originalSize == 0.0 ? 0.0 : clampedSize / originalSize; - vec3 pos = (instanceModelMatrix * (scenegraph.sceneModelMatrix * vec4(positions, 1.0)).xyz) * scenegraph.sizeScale * (clampedSize / originalSize) + instanceTranslation; - if(scenegraph.composeModelMatrix) { + vec3 pos = (instanceModelMatrix * (scenegraph.sceneModelMatrix * vec4(positions, 1.0)).xyz) * scenegraph.sizeScale * sizeRatio + instanceTranslation; + if(scenegraph.composeModelMatrix > 0.5) { DECKGL_FILTER_SIZE(pos, geometry); // using instancePositions as world coordinates // when using globe mode, this branch does not re-orient the model to align with the surface of the earth diff --git a/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.ts b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.ts index 555163c25b6..6ec6c8fa87f 100644 --- a/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.ts +++ b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.ts @@ -2,8 +2,8 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Layer, project32, picking, log} from '@deck.gl/core'; -import type {Device} from '@luma.gl/core'; +import {Layer, project32, color, picking, log} from '@deck.gl/core'; +import type {Device, RenderPipelineParameters} from '@luma.gl/core'; import {pbrMaterial} from '@luma.gl/shadertools'; import {ScenegraphNode, GroupNode, ModelNode, Model} from '@luma.gl/engine'; import {GLTFAnimator, PBREnvironment, createScenegraphsFromGLTF} from '@luma.gl/gltf'; @@ -15,6 +15,8 @@ import {MATRIX_ATTRIBUTES, shouldComposeModelMatrix} from '../utils/matrix'; import {scenegraphUniforms, ScenegraphProps} from './scenegraph-layer-uniforms'; import vs from './scenegraph-layer-vertex.glsl'; import fs from './scenegraph-layer-fragment.glsl'; +import source from './scenegraph-layer.wgsl'; +import {scenegraphPbrMaterial} from './scenegraph-pbr-material'; import { UpdateParameters, @@ -77,7 +79,11 @@ type _ScenegraphLayerProps = { */ _imageBasedLightingEnvironment?: | PBREnvironment - | ((context: {gl: WebGL2RenderingContext; layer: ScenegraphLayer}) => PBREnvironment); + | ((context: { + device?: Device; + gl?: WebGL2RenderingContext; + layer: ScenegraphLayer; + }) => PBREnvironment); /** Anchor position accessor. */ getPosition?: Accessor; @@ -183,22 +189,26 @@ export default class ScenegraphLayer e getShaders() { const defines: {LIGHTING_PBR?: 1} = {}; let pbr; + const isWebGPU = this.context.device?.type === 'webgpu'; if (this.props._lighting === 'pbr') { - pbr = pbrMaterial; + pbr = isWebGPU ? scenegraphPbrMaterial : pbrMaterial; defines.LIGHTING_PBR = 1; + } else if (isWebGPU) { + pbr = scenegraphPbrMaterial; } else { // Dummy shader module needed to handle // pbrMaterial.pbr_baseColorSampler binding pbr = {name: 'pbrMaterial'}; } - const modules = [project32, picking, scenegraphUniforms, pbr]; - return super.getShaders({defines, vs, fs, modules}); + const modules = [project32, color, picking, scenegraphUniforms, pbr]; + return super.getShaders({defines, vs, fs, source, modules}); } initializeState() { const attributeManager = this.getAttributeManager(); + const supportsTransitions = this.context.device.type !== 'webgpu'; // attributeManager is always defined for primitive layers attributeManager!.addInstanced({ instancePositions: { @@ -206,14 +216,14 @@ export default class ScenegraphLayer e type: 'float64', fp64: this.use64bitPositions(), accessor: 'getPosition', - transition: true + transition: supportsTransitions }, instanceColors: { type: 'unorm8', size: this.props.colorFormat.length, accessor: 'getColor', defaultValue: DEFAULT_COLOR, - transition: true + transition: supportsTransitions }, instanceModelMatrix: MATRIX_ATTRIBUTES }); @@ -346,18 +356,31 @@ export default class ScenegraphLayer e let env: PBREnvironment | undefined; if (_imageBasedLightingEnvironment) { if (typeof _imageBasedLightingEnvironment === 'function') { - env = _imageBasedLightingEnvironment({gl: this.context.gl, layer: this}); + env = _imageBasedLightingEnvironment({ + device: this.context.device, + gl: this.context.gl, + layer: this + }); } else { env = _imageBasedLightingEnvironment; } } + const parameters = + this.context.device.type === 'webgpu' + ? ({ + depthWriteEnabled: true, + depthCompare: 'less-equal' + } satisfies RenderPipelineParameters) + : undefined; + return { imageBasedLightingEnvironment: env, modelOptions: { id: this.props.id, isInstanced: true, bufferLayout: this.getAttributeManager()!.getBufferLayouts(), + parameters, ...this.getShaders() }, // tangents are not supported @@ -389,7 +412,7 @@ export default class ScenegraphLayer e sizeScale, sizeMinPixels, sizeMaxPixels, - composeModelMatrix: shouldComposeModelMatrix(viewport, coordinateSystem), + composeModelMatrix: shouldComposeModelMatrix(viewport, coordinateSystem) ? 1 : 0, sceneModelMatrix: worldMatrix }; diff --git a/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.wgsl.ts b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.wgsl.ts new file mode 100644 index 00000000000..aa38dfefdf7 --- /dev/null +++ b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.wgsl.ts @@ -0,0 +1,118 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export default /* wgsl */ `\ +struct VertexInputs { + @builtin(instance_index) instanceIndex: u32, + @location(0) positions: vec3, +#ifdef HAS_NORMALS + @location(1) normals: vec3, +#endif +#ifdef HAS_UV + @location(3) texCoords: vec2, +#endif + @location(6) instancePositions: vec3, + @location(7) instancePositions64Low: vec3, + @location(8) instanceColors: vec4, + @location(10) instanceModelMatrixCol0: vec3, + @location(11) instanceModelMatrixCol1: vec3, + @location(12) instanceModelMatrixCol2: vec3, + @location(13) instanceTranslation: vec3, +}; + +struct FragmentInputs { + @builtin(position) position: vec4, + @location(0) vColor: vec4, + @location(1) vTexCoord: vec2, + @location(2) pbrPosition: vec3, + @location(3) pbrUV: vec2, + @location(4) pbrNormal: vec3, +}; + +@vertex +fn vertexMain(inputs: VertexInputs) -> FragmentInputs { + var outputs: FragmentInputs; + + geometry.worldPosition = inputs.instancePositions; + geometry.pickingColor = picking_getPickingColorFromIndex(inputs.instanceIndex); + + var vertexPosition = inputs.positions; + var texCoord = vec2(0.0, 0.0); + var normal = vec3(0.0, 0.0, 1.0); + +#ifdef HAS_UV + texCoord = inputs.texCoords; +#endif +#ifdef HAS_NORMALS + normal = inputs.normals; +#endif + + geometry.uv = texCoord; + + let instanceModelMatrix = mat3x3( + inputs.instanceModelMatrixCol0, + inputs.instanceModelMatrixCol1, + inputs.instanceModelMatrixCol2 + ); + + let scenePosition = (scenegraph.sceneModelMatrix * vec4(vertexPosition, 1.0)).xyz; + let worldNormal = instanceModelMatrix * (scenegraph.sceneModelMatrix * vec4(normal, 0.0)).xyz; + + let originalSize = project_meter_size_to_pixel(scenegraph.sizeScale); + let clampedSize = clamp(originalSize, scenegraph.sizeMinPixels, scenegraph.sizeMaxPixels); + let sizeRatio = select(0.0, clampedSize / originalSize, originalSize > 0.0); + + let pos = + (instanceModelMatrix * scenePosition) * scenegraph.sizeScale * sizeRatio + + inputs.instanceTranslation; + + if (scenegraph.composeModelMatrix > 0.5) { + geometry.normal = project_normal(worldNormal); + geometry.worldPosition = inputs.instancePositions + pos; + geometry.position = vec4( + project_position_vec3_f64(inputs.instancePositions + pos, inputs.instancePositions64Low), + 1.0 + ); + } else { + let sizeAdjustedPos = project_size_vec3(pos); + geometry.position = vec4( + project_position_vec3_f64(inputs.instancePositions, inputs.instancePositions64Low) + + sizeAdjustedPos, + 1.0 + ); + geometry.normal = project_normal(worldNormal); + } + + outputs.position = project_common_position_to_clipspace(geometry.position); + outputs.vColor = inputs.instanceColors; + outputs.vTexCoord = texCoord; + outputs.pbrPosition = geometry.position.xyz; + outputs.pbrUV = texCoord; + outputs.pbrNormal = geometry.normal; + return outputs; +} + +@fragment +fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4 { + fragmentGeometry.uv = inputs.vTexCoord; + + var fragColor = inputs.vColor; + +#ifdef LIGHTING_PBR + fragmentInputs.pbr_vPosition = inputs.pbrPosition; + fragmentInputs.pbr_vUV = inputs.pbrUV; + fragmentInputs.pbr_vNormal = inputs.pbrNormal; + fragColor = fragColor * pbr_filterColor(vec4(0.0)); +#else +#ifdef HAS_BASECOLORMAP + fragColor = + fragColor * + textureSample(pbr_baseColorSampler, pbr_baseColorSamplerSampler, inputs.vTexCoord); +#endif +#endif + + fragColor.a *= color.opacity; + return deckgl_premultiplied_alpha(fragColor); +} +`; diff --git a/modules/mesh-layers/src/scenegraph-layer/scenegraph-pbr-material.ts b/modules/mesh-layers/src/scenegraph-layer/scenegraph-pbr-material.ts new file mode 100644 index 00000000000..6daeec4ad56 --- /dev/null +++ b/modules/mesh-layers/src/scenegraph-layer/scenegraph-pbr-material.ts @@ -0,0 +1,36 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {lighting, pbrMaterial} from '@luma.gl/shadertools'; + +import type { + PBRMaterialBindings, + PBRMaterialProps, + PBRMaterialUniforms, + ShaderModule +} from '@luma.gl/shadertools'; + +const source = (pbrMaterial.source as string) + .replace( + /fn pbr_setPositionNormalTangentUV\([\s\S]*?\n}\n/, + `fn pbr_setPositionNormalTangentUV(position: vec4f, normal: vec4f, tangent: vec4f, uv: vec2f) +{ + fragmentInputs.pbr_vPosition = position.xyz; + fragmentInputs.pbr_vNormal = normal.xyz; + fragmentInputs.pbr_vTBN = mat3x3f( + vec3f(1.0, 0.0, 0.0), + vec3f(0.0, 1.0, 0.0), + vec3f(0.0, 0.0, 1.0) + ); + fragmentInputs.pbr_vUV = uv; +} +` + ) + .replace(/pbrProjection\.camera/g, 'project.cameraPosition'); + +export const scenegraphPbrMaterial = { + ...pbrMaterial, + dependencies: [lighting], + source +} as const satisfies ShaderModule; diff --git a/test/modules/core/lib/attribute/attribute-buffer-groups.node.spec.ts b/test/modules/core/lib/attribute/attribute-buffer-groups.node.spec.ts index 8c3fc8ca798..97485c93204 100644 --- a/test/modules/core/lib/attribute/attribute-buffer-groups.node.spec.ts +++ b/test/modules/core/lib/attribute/attribute-buffer-groups.node.spec.ts @@ -74,6 +74,20 @@ test('AttributeManager without groups keeps WebGPU layouts unchanged', () => { attributeManager.finalize(); }); +test('AttributeManager excludes index buffers from WebGPU vertex layouts', () => { + const attributeManager = new AttributeManager(createWebGPUDevice()); + attributeManager.add({ + indices: {size: 1, isIndexed: true, accessor: 'getIndex'}, + positions: {size: 3, accessor: 'getPosition'} + }); + + expect( + attributeManager.getBufferLayouts({isInstanced: false}).map(layout => layout.name) + ).toEqual(['positions']); + + attributeManager.finalize(); +}); + test('AttributeManager buffer groups pack IconLayer-style shader attributes', async () => { const attributeManager = new AttributeManager(createWebGPUDevice()); attributeManager.addInstanced({ @@ -285,7 +299,6 @@ test('Layer grouped bindings preserve legacy fallback and index binding', () => (layer as any)._setModelAttributes(model, attributeManager.getAttributes()); expect(model.setBufferLayout.mock.calls[0][0].map(layout => layout.name)).toEqual([ - 'indices', 'constant', 'group-a' ]); @@ -304,7 +317,6 @@ test('Layer grouped bindings preserve legacy fallback and index binding', () => (layer as any)._setModelAttributes(excludedModel, attributeManager.getAttributes()); expect(excludedModel.setBufferLayout.mock.calls[0][0].map(layout => layout.name)).toEqual([ - 'indices', 'constant', 'a', 'b' diff --git a/website/src/examples/arc-layer.js b/website/src/examples/arc-layer.js index 96a6fb26313..3633041f392 100644 --- a/website/src/examples/arc-layer.js +++ b/website/src/examples/arc-layer.js @@ -18,6 +18,8 @@ const colorRamp = inFlowColors class ArcDemo extends Component { static title = 'United States County-to-county Migration'; + static hasDeviceTabs = true; + static data = { url: `${DATA_URI}/arc-data.txt`, worker: '/workers/arc-data-decoder.js' @@ -78,6 +80,8 @@ class ArcDemo extends Component { return ( diff --git a/website/src/examples/brushing-extension.js b/website/src/examples/brushing-extension.js index 6c6fdd0e05a..3035f1c3ce0 100644 --- a/website/src/examples/brushing-extension.js +++ b/website/src/examples/brushing-extension.js @@ -15,6 +15,8 @@ const colorRamp = [inFlowColor, outFlowColor] class BrushingDemo extends Component { static title = 'United States County-to-county Migration'; + static hasDeviceTabs = true; + static data = { url: `${DATA_URI}/arc-data.txt`, worker: '/workers/arc-data-decoder.js' @@ -81,6 +83,8 @@ class BrushingDemo extends Component { return ( diff --git a/website/src/examples/contour-layer.js b/website/src/examples/contour-layer.js index c0cbe564d4e..5e92494c5b0 100644 --- a/website/src/examples/contour-layer.js +++ b/website/src/examples/contour-layer.js @@ -13,6 +13,8 @@ const MS_PER_WEEK = 1000 * 60 * 60 * 24 * 7; class ContourDemo extends Component { static title = 'COVID-19 Cases in the United States'; + static hasDeviceTabs = true; + static data = { url: `${DATA_URI}/covid-by-county.txt`, worker: '/workers/contour-data-decoder.js' @@ -126,6 +128,8 @@ class ContourDemo extends Component { return ( ; + return ; } } diff --git a/website/src/examples/geojson-layer-paths.js b/website/src/examples/geojson-layer-paths.js index 4cf3b71b160..d34cc6a76bc 100644 --- a/website/src/examples/geojson-layer-paths.js +++ b/website/src/examples/geojson-layer-paths.js @@ -25,6 +25,8 @@ class HighwayDemo extends Component { static code = `${GITHUB_TREE}/examples/website/highway`; + static hasDeviceTabs = true; + static parameters = { year: {displayName: 'Year', type: 'range', value: 1990, step: 5, min: 1990, max: 2015} }; @@ -84,6 +86,8 @@ class HighwayDemo extends Component { return ( ; + return ; } } diff --git a/website/src/examples/hexagon-layer.js b/website/src/examples/hexagon-layer.js index 43c6a9c5e9a..0f54124577f 100644 --- a/website/src/examples/hexagon-layer.js +++ b/website/src/examples/hexagon-layer.js @@ -13,6 +13,8 @@ import {makeExample} from '../components'; class HexagonDemo extends Component { static title = 'United Kingdom Road Safety'; + static hasDeviceTabs = true; + static data = { url: `${DATA_URI}/heatmap-data.txt`, worker: '/workers/heatmap-data-decoder.js' @@ -77,6 +79,8 @@ class HexagonDemo extends Component { return ( this.props.onStateChange({count})} /> diff --git a/website/src/examples/screen-grid-layer.js b/website/src/examples/screen-grid-layer.js index 2b235b01fb5..58191946f6a 100644 --- a/website/src/examples/screen-grid-layer.js +++ b/website/src/examples/screen-grid-layer.js @@ -12,6 +12,8 @@ import {makeExample} from '../components'; class ScreenGridDemo extends Component { static title = 'Uber Pickup Locations In NewYork City'; + static hasDeviceTabs = true; + static code = `${GITHUB_TREE}/examples/website/screen-grid`; static data = { @@ -95,6 +97,8 @@ class ScreenGridDemo extends Component { return (