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 dfc432a125c..47be86c775b 100644 --- a/docs/developer-guide/webgpu.md +++ b/docs/developer-guide/webgpu.md @@ -44,17 +44,17 @@ 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/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` | `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` | ✅ | ❌ | @@ -75,6 +75,8 @@ 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. diff --git a/examples/website/text/app.tsx b/examples/website/text/app.tsx index 3ce529caf7f..c82b1af30e6 100644 --- a/examples/website/text/app.tsx +++ b/examples/website/text/app.tsx @@ -100,7 +100,8 @@ export default function App({ sizeMaxPixels: sizeMaxPixels * 2, sizeMinPixels: sizeMinPixels * 2 }, - extensions: [new CollisionFilterExtension()] + // CollisionFilterExtension has not been ported to WebGPU yet. + extensions: device?.type === 'webgpu' ? [] : [new CollisionFilterExtension()] }); return ( 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..a7e905e0233 100644 --- a/modules/layers/src/path-layer/path-layer.ts +++ b/modules/layers/src/path-layer/path-layer.ts @@ -2,12 +2,13 @@ // 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 {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 +136,12 @@ 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,43 +149,73 @@ 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 isWebGPU = this.context.device.type === 'webgpu'; const attributeManager = this.getAttributeManager(); /* 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 - }, - instanceStartPositions: { - vertexOffset: 1 - }, - instanceEndPositions: { - vertexOffset: 2 - }, - instanceRightPositions: { - vertexOffset: 3 + ...(isWebGPU + ? { + // WebGPU cannot express WebGL's vertexOffset window in one vertex buffer layout. + // Pack each segment's [left, start, end, right] high and low position parts instead. + instancePositions: { + size: 24, + type: 'float32', + transition: false, + accessor: 'getPath', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateWebGPUPositions, + shaderAttributes: { + instanceLeftPositions: {size: 3, elementOffset: 0}, + instanceStartPositions: {size: 3, elementOffset: 3}, + instanceEndPositions: {size: 3, elementOffset: 6}, + instanceRightPositions: {size: 3, elementOffset: 9}, + instanceLeftPositions64Low: {size: 3, elementOffset: 12}, + instanceStartPositions64Low: {size: 3, elementOffset: 15}, + instanceEndPositions64Low: {size: 3, elementOffset: 18}, + instanceRightPositions64Low: {size: 3, elementOffset: 21} + }, + noAlloc + } } - } - }, + : { + 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 + }, + instanceStartPositions: { + vertexOffset: 1 + }, + instanceEndPositions: { + vertexOffset: 2 + }, + instanceRightPositions: { + vertexOffset: 3 + } + } + } + }), instanceTypes: { size: 1, - type: 'uint8', + type: isWebGPU ? 'float32' : 'uint8', // eslint-disable-next-line @typescript-eslint/unbound-method update: this.calculateSegmentTypes, noAlloc @@ -187,28 +223,33 @@ export default class PathLayer extends instanceStrokeWidths: { size: 1, accessor: 'getWidth', - transition: ATTRIBUTE_TRANSITION, - defaultValue: 1 + transition: isWebGPU ? false : ATTRIBUTE_TRANSITION, + defaultValue: 1, + bufferGroup: 'path-instance-data' }, instanceColors: { size: this.props.colorFormat.length, type: 'unorm8', accessor: 'getColor', - transition: ATTRIBUTE_TRANSITION, - defaultValue: DEFAULT_COLOR + transition: isWebGPU ? false : ATTRIBUTE_TRANSITION, + defaultValue: DEFAULT_COLOR, + bufferGroup: 'path-instance-data' }, /** Source path row for each generated segment/joint instance. */ rowIndexes: { size: 1, type: 'uint32', - accessor: (object, {index}) => (object && object.__source ? object.__source.index : index) + accessor: (object, {index}) => (object && object.__source ? object.__source.index : index), + // AttributeManager only materializes buffer groups on WebGPU, so WebGL keeps its layout. + bufferGroup: 'path-instance-data' } }); /* eslint-enable max-len */ this.setState({ pathTesselator: new PathTesselator({ - fp64: this.use64bitPositions() + fp64: this.use64bitPositions(), + isWebGPU }) }); } @@ -389,4 +430,38 @@ export default class PathLayer extends attribute.startIndices = pathTesselator.vertexStarts; attribute.value = pathTesselator.get('segmentTypes'); } + + protected calculateWebGPUPositions(attribute) { + const {pathTesselator} = this.state; + const value = pathTesselator.get('positions'); + + if (!value) { + attribute.value = null; + return; + } + + const numInstances = pathTesselator.instanceCount; + const result = new Float32Array(numInstances * 24); + // WebGL reads a padded neighbor window using `vertexOffset: 1`; this materializes + // the same [-1, 0, 1, 2] access pattern explicitly for the WebGPU layout. + const neighborOffsets = [-1, 0, 1, 2]; + + for (let i = 0; i < numInstances; i++) { + const targetIndex = i * 24; + 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; + const highPart = Math.fround(position); + result[targetOffset + j] = highPart; + result[targetOffset + j + 12] = position - highPart; + } + } + } + + 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..64dc68836cf 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; + isWebGPU?: 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 vertex inputs use a 4-byte scalar; keep WebGL's compact uint8 buffer unchanged. + segmentTypes: {size: 1, type: opts.isWebGPU ? 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..c58dba65d33 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 @@ -4,6 +4,16 @@ import type {ShaderModule} from '@luma.gl/shadertools'; +const uniformBlockWGSL = /* wgsl */ `\ +struct SolidPolygonUniforms { + extruded: f32, + isWireframe: f32, + elevationScale: f32, +}; + +@group(0) @binding(auto) var solidPolygon: SolidPolygonUniforms; +`; + const uniformBlock = `\ layout(std140) uniform solidPolygonUniforms { bool extruded; @@ -20,6 +30,7 @@ export type SolidPolygonProps = { export const solidPolygonUniforms = { name: 'solidPolygon', + source: uniformBlockWGSL, 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..bb4ca538973 100644 --- a/modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts +++ b/modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts @@ -2,7 +2,7 @@ // 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'; // Polygon geometry generation is managed by the polygon tesselator @@ -12,6 +12,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 +134,16 @@ export default class SolidPolygonLayer }; getShaders(type) { + const ringWindingOrderCW = !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1; + return super.getShaders({ vs: type === 'top' ? vsTop : vsSide, fs, + source: getSolidPolygonShaderWGSL(type, Boolean(ringWindingOrderCW)), defines: { - RING_WINDING_ORDER_CW: !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1 + RING_WINDING_ORDER_CW: ringWindingOrderCW }, - modules: [project32, gouraudMaterial, picking, solidPolygonUniforms] + modules: [project32, color, gouraudMaterial, picking, solidPolygonUniforms] }); } @@ -182,6 +186,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 +207,35 @@ 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 + ? { + // WebGPU cannot express WebGL's one-vertex offset view in a buffer layout. + nextVertexPositions: { + size: 3, + type: 'float64', + stepMode: 'dynamic', + fp64: this.use64bitPositions(), + transition: false, + accessor: 'getPolygon', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateNextPositions, + noAlloc + } + } + : {}), instanceVertexValid: { size: 1, - type: 'uint16', + type: isWebGPU ? 'float32' : 'uint16', stepMode: 'instance', // eslint-disable-next-line @typescript-eslint/unbound-method update: this.calculateVertexValid, @@ -220,7 +245,8 @@ export default class SolidPolygonLayer size: 1, stepMode: 'dynamic', transition: ATTRIBUTE_TRANSITION, - accessor: 'getElevation' + accessor: 'getElevation', + bufferGroup: 'solid-polygon-instance-data' }, fillColors: { size: this.props.colorFormat.length, @@ -228,7 +254,8 @@ export default class SolidPolygonLayer stepMode: 'dynamic', transition: ATTRIBUTE_TRANSITION, accessor: 'getFillColor', - defaultValue: DEFAULT_COLOR + defaultValue: DEFAULT_COLOR, + bufferGroup: 'solid-polygon-instance-data' }, lineColors: { size: this.props.colorFormat.length, @@ -236,14 +263,16 @@ export default class SolidPolygonLayer stepMode: 'dynamic', transition: ATTRIBUTE_TRANSITION, accessor: 'getLineColor', - defaultValue: DEFAULT_COLOR + defaultValue: DEFAULT_COLOR, + bufferGroup: 'solid-polygon-instance-data' }, /** Source polygon row, including __source.index for composite data. */ rowIndexes: { size: 1, type: 'uint32', stepMode: 'dynamic', - accessor: (object, {index}) => (object && object.__source ? object.__source.index : index) + accessor: (object, {index}) => (object && object.__source ? object.__source.index : index), + bufferGroup: 'solid-polygon-instance-data' } }); /* eslint-enable max-len */ @@ -377,8 +406,17 @@ 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') { + // Indices are bound separately, and the top model does not bind side-only attributes. + bufferLayout = bufferLayout.filter( + layout => + layout.name !== 'indices' && + layout.name !== 'instanceVertexValid' && + layout.name !== 'nextVertexPositions' + ); + } topModel = new Model(this.context.device, { ...shaders, @@ -387,12 +425,16 @@ export default class SolidPolygonLayer bufferLayout, isIndexed: true, userData: { - excludeAttributes: {instanceVertexValid: true} + excludeAttributes: {instanceVertexValid: true, nextVertexPositions: true} } }); } if (extruded) { - const bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true}); + let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true}); + if (this.context.device.type === 'webgpu') { + // Indices are owned by the top model; WebGPU vertex layouts cannot include index buffers. + bufferLayout = bufferLayout.filter(layout => layout.name !== 'indices'); + } sideModel = new Model(this.context.device, { ...this.getShaders('side'), @@ -456,6 +498,35 @@ 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'); + const vertexValid = polygonTesselator.get('vertexValid'); + attribute.startIndices = polygonTesselator.vertexStarts; + + if (!positions) { + attribute.value = positions; + return; + } + + const vertexCount = positions.length / 3; + const nextPositions = new (positions.constructor as typeof Float32Array)(positions.length); + for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) { + const sourceIndex = vertexIndex * 3; + const nextSourceIndex = + vertexValid?.[vertexIndex] && vertexIndex + 1 < vertexCount ? sourceIndex + 3 : sourceIndex; + for (let componentIndex = 0; componentIndex < 3; componentIndex++) { + nextPositions[sourceIndex + componentIndex] = positions[nextSourceIndex + componentIndex]; + } + } + + attribute.value = nextPositions; } } 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..6f9df065d48 --- /dev/null +++ b/modules/layers/src/solid-polygon-layer/solid-polygon-layer.wgsl.ts @@ -0,0 +1,225 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +type SolidPolygonShaderType = 'top' | 'side'; + +function getSolidPolygonVertexHelpers() { + return /* wgsl */ `\ +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..a3e7b3f910c 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,12 @@ type _ScenegraphLayerProps = { */ _imageBasedLightingEnvironment?: | PBREnvironment - | ((context: {gl: WebGL2RenderingContext; layer: ScenegraphLayer}) => PBREnvironment); + | ((context: { + device?: Device; + /** @deprecated Use `device.handle`. */ + gl?: WebGL2RenderingContext; + layer: ScenegraphLayer; + }) => PBREnvironment); /** Anchor position accessor. */ getPosition?: Accessor; @@ -183,22 +190,34 @@ 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; + // The stock pbrMaterial module supplies the existing GLSL PBR path. + // WebGPU uses a Scenegraph-specific wrapper because its WGSL source must + // adapt the PBR position/normal inputs and camera binding to deck.gl's + // project module. + pbr = isWebGPU ? scenegraphPbrMaterial : pbrMaterial; defines.LIGHTING_PBR = 1; + } else if (isWebGPU) { + // The flat WGSL path can still sample a glTF base-color texture, so it + // needs the Scenegraph PBR module's sampler bindings even though + // LIGHTING_PBR is not enabled. + pbr = scenegraphPbrMaterial; } else { - // Dummy shader module needed to handle - // pbrMaterial.pbr_baseColorSampler binding + // The flat GLSL shader declares pbr_baseColorSampler itself. Register a + // name-only module so the glTF material binding is still routed to that + // sampler without injecting the full PBR lighting module. 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 +225,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 +365,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 +421,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..b035b163830 --- /dev/null +++ b/modules/mesh-layers/src/scenegraph-layer/scenegraph-layer.wgsl.ts @@ -0,0 +1,125 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export default /* wgsl */ `\ +struct VertexInputs { + @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, + @builtin(instance_index) instanceIndex: u32 +) -> FragmentInputs { + var outputs: FragmentInputs; + + geometry.worldPosition = inputs.instancePositions; + geometry.pickingColor = picking_getPickingColorFromIndex(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); + // Scenegraph offsets are east/north/up in globe mode. Use project32's helper so it can + // rotate the offset onto the local tangent plane before producing the common position. + let projectResult = project_position_to_clipspace_and_commonspace( + inputs.instancePositions, + inputs.instancePositions64Low, + sizeAdjustedPos + ); + geometry.position = projectResult.commonPosition; + 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; + // scenegraphPbrMaterial uses the indexed UV fields from the current PBR module. + fragmentInputs.pbr_vUV0 = inputs.pbrUV; + fragmentInputs.pbr_vUV1 = vec2(0.0); + 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 *= layer.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..4ef4baa6e39 --- /dev/null +++ b/modules/mesh-layers/src/scenegraph-layer/scenegraph-pbr-material.ts @@ -0,0 +1,37 @@ +// 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_vUV0 = uv; + fragmentInputs.pbr_vUV1 = uv; +} +` + ) + .replace(/pbrProjection\.camera/g, 'project.cameraPosition'); + +export const scenegraphPbrMaterial = { + ...pbrMaterial, + dependencies: [lighting], + source +} as const satisfies ShaderModule; diff --git a/website/src/examples/arc-layer.js b/website/src/examples/arc-layer.js index 96a6fb26313..385b1b221e7 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' diff --git a/website/src/examples/geojson-layer-paths.js b/website/src/examples/geojson-layer-paths.js index 4cf3b71b160..663c1af2e4a 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} }; diff --git a/website/src/examples/geojson-layer-polygons.js b/website/src/examples/geojson-layer-polygons.js index c8c45de7cef..cd976a84ff2 100644 --- a/website/src/examples/geojson-layer-polygons.js +++ b/website/src/examples/geojson-layer-polygons.js @@ -12,6 +12,8 @@ import {makeExample} from '../components'; class GeoJsonDemo extends Component { static title = 'Vancouver Property Value'; + static hasDeviceTabs = true; + static data = { url: `${DATA_URI}/vancouver-blocks.txt`, worker: '/workers/geojson-data-decoder.js' diff --git a/website/src/examples/scenegraph-layer.js b/website/src/examples/scenegraph-layer.js index 13fb084f8c2..5830ea2627e 100644 --- a/website/src/examples/scenegraph-layer.js +++ b/website/src/examples/scenegraph-layer.js @@ -14,6 +14,8 @@ class ScenegraphDemo extends Component { static code = `${GITHUB_TREE}/examples/website/scenegraph`; + static hasDeviceTabs = true; + static parameters = { sizeScale: {displayName: 'Size', type: 'range', value: 25, step: 5, min: 5, max: 500} }; diff --git a/website/src/examples/text-layer-clipping.js b/website/src/examples/text-layer-clipping.js index 3dd7b900b5b..552222fbbe6 100644 --- a/website/src/examples/text-layer-clipping.js +++ b/website/src/examples/text-layer-clipping.js @@ -19,6 +19,8 @@ class TextDemo extends Component { static code = `${GITHUB_TREE}/examples/website/treemap`; + static hasDeviceTabs = true; + static parameters = { gzip: {displayName: 'Gzipped size', type: 'checkbox', value: true}, }; diff --git a/website/src/examples/text-layer.js b/website/src/examples/text-layer.js index df25d88718b..ffd7d8dc84c 100644 --- a/website/src/examples/text-layer.js +++ b/website/src/examples/text-layer.js @@ -19,6 +19,8 @@ class TextDemo extends Component { static code = `${GITHUB_TREE}/examples/website/text`; + static hasDeviceTabs = true; + static parameters = { noOverlap: {displayName: 'Prevent overlap', type: 'checkbox', value: true}, fontSize: {displayName: 'Font Size', type: 'range', value: 32, step: 1, min: 20, max: 80}