diff --git a/examples/website/text/app.tsx b/examples/website/text/app.tsx index 6cafd96f17c..ef74ed600ca 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; @@ -97,11 +100,13 @@ 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 ( extends getShaders() { const shaders = super.getShaders(); - return {...shaders, modules: [...shaders.modules, textUniforms, sdfUniforms], vs, fs}; + const textShaders = { + ...shaders, + modules: [...shaders.modules, textUniforms, sdfUniforms], + vs, + fs + }; + return this.context.device.type === 'webgpu' ? {...textShaders, source} : textShaders; } 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/website/src/examples/text-layer.js b/website/src/examples/text-layer.js index df25d88718b..5d417a2a08b 100644 --- a/website/src/examples/text-layer.js +++ b/website/src/examples/text-layer.js @@ -10,6 +10,8 @@ import App from 'website-examples/text/app'; import {makeExample} from '../components'; class TextDemo extends Component { + static hasDeviceTabs = true; + static title = 'World Cities by Population'; static data = { @@ -52,6 +54,8 @@ class TextDemo extends Component { return (