diff --git a/modules/core/src/lib/attribute/data-column.ts b/modules/core/src/lib/attribute/data-column.ts index 2b66519acd9..8e17cd641c4 100644 --- a/modules/core/src/lib/attribute/data-column.ts +++ b/modules/core/src/lib/attribute/data-column.ts @@ -268,11 +268,10 @@ export default class DataColumn { options: Partial | null = null ): BufferLayout { const accessor = this.getAccessor(); - const attributes: BufferAttributeLayout[] = []; + const attributes: (BufferAttributeLayout | null)[] = []; const result: BufferLayout = { name: this.id, - byteStride: getStride(accessor), - attributes + byteStride: getStride(accessor) }; if (this.doublePrecision) { @@ -307,6 +306,7 @@ export default class DataColumn { } else { attributes.push(getBufferAttributeLayout(attributeName, accessor, this.device.type)); } + result.attributes = attributes.filter(Boolean) as BufferAttributeLayout[]; return result; } diff --git a/modules/core/src/lib/attribute/gl-utils.ts b/modules/core/src/lib/attribute/gl-utils.ts index 286a441768f..f18f86eac0c 100644 --- a/modules/core/src/lib/attribute/gl-utils.ts +++ b/modules/core/src/lib/attribute/gl-utils.ts @@ -26,7 +26,11 @@ export function getBufferAttributeLayout( name: string, accessor: BufferAccessor, deviceType: 'webgpu' | 'wegbgl' | string -): BufferAttributeLayout { +): BufferAttributeLayout | null { + if ((accessor.size as number) > 4) { + // Definitely not valid. TODO - stricter validation? + return null; + } // TODO(ibgreen): WebGPU change. Currently we always use normalized 8 bit integers const type = deviceType === 'webgpu' && accessor.type === 'uint8' ? 'unorm8' : accessor.type; return { diff --git a/modules/layers/src/icon-layer/icon-layer.ts b/modules/layers/src/icon-layer/icon-layer.ts index 8bb5b33e15d..983c390ae33 100644 --- a/modules/layers/src/icon-layer/icon-layer.ts +++ b/modules/layers/src/icon-layer/icon-layer.ts @@ -2,13 +2,14 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Layer, project32, picking, log, UNIT} from '@deck.gl/core'; +import {Layer, color, project32, picking, log, UNIT} from '@deck.gl/core'; import {SamplerProps, Texture} from '@luma.gl/core'; import {Model, Geometry} from '@luma.gl/engine'; import {iconUniforms, IconProps} from './icon-layer-uniforms'; import vs from './icon-layer-vertex.glsl'; import fs from './icon-layer-fragment.glsl'; +import {shaderWGSL as source} from './icon-layer.wgsl'; import IconManager from './icon-manager'; import type { @@ -25,6 +26,7 @@ import type { } from '@deck.gl/core'; import type {UnpackedIcon, IconMapping, LoadIconErrorContext} from './icon-manager'; +import {Parameters} from '@luma.gl/core'; type _IconLayerProps = { data: LayerDataSource; @@ -139,7 +141,7 @@ export default class IconLayer extends }; getShaders() { - return super.getShaders({vs, fs, modules: [project32, picking, iconUniforms]}); + return super.getShaders({vs, fs, source, modules: [project32, color, picking, iconUniforms]}); } initializeState() { @@ -166,24 +168,25 @@ export default class IconLayer extends accessor: 'getSize', defaultValue: 1 }, - instanceOffsets: { - size: 2, - accessor: 'getIcon', - // eslint-disable-next-line @typescript-eslint/unbound-method - transform: this.getInstanceOffset - }, - instanceIconFrames: { - size: 4, - accessor: 'getIcon', - // eslint-disable-next-line @typescript-eslint/unbound-method - transform: this.getInstanceIconFrame - }, - instanceColorModes: { - size: 1, - type: 'uint8', + instanceIconDefs: { + size: 7, accessor: 'getIcon', // eslint-disable-next-line @typescript-eslint/unbound-method - transform: this.getInstanceColorMode + transform: this.getInstanceIconDef, + shaderAttributes: { + instanceOffsets: { + size: 2, + elementOffset: 0 + }, + instanceIconFrames: { + size: 4, + elementOffset: 2 + }, + instanceColorModes: { + size: 1, + elementOffset: 6 + } + } }, instanceColors: { size: this.props.colorFormat.length, @@ -286,6 +289,13 @@ export default class IconLayer extends } protected _getModel(): Model { + const parameters = + this.context.device.type === 'webgpu' + ? ({ + depthWriteEnabled: true, + depthCompare: 'less-equal' + } satisfies Parameters) + : undefined; // The icon-layer vertex shader uses 2d positions // specifed via: in vec2 positions; const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; @@ -305,7 +315,8 @@ export default class IconLayer extends } } }), - isInstanced: true + isInstanced: true, + parameters }); } @@ -322,23 +333,17 @@ export default class IconLayer extends } } - protected getInstanceOffset(icon: string): number[] { + protected getInstanceIconDef(icon: string): number[] { const { + x, + y, width, height, + mask, anchorX = width / 2, anchorY = height / 2 } = this.state.iconManager.getIconMapping(icon); - return [width / 2 - anchorX, height / 2 - anchorY]; - } - - protected getInstanceColorMode(icon: string): number { - const mapping = this.state.iconManager.getIconMapping(icon); - return mapping.mask ? 1 : 0; - } - protected getInstanceIconFrame(icon: string): number[] { - const {x, y, width, height} = this.state.iconManager.getIconMapping(icon); - return [x, y, width, height]; + return [width / 2 - anchorX, height / 2 - anchorY, x, y, width, height, mask ? 1 : 0]; } } diff --git a/modules/layers/src/icon-layer/icon-layer.wgsl.ts b/modules/layers/src/icon-layer/icon-layer.wgsl.ts new file mode 100644 index 00000000000..1ba09cbd0fb --- /dev/null +++ b/modules/layers/src/icon-layer/icon-layer.wgsl.ts @@ -0,0 +1,129 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export const shaderWGSL = /* wgsl */ `\ +struct IconUniforms { + sizeScale: f32, + iconsTextureDim: vec2, + sizeBasis: f32, + sizeMinPixels: f32, + sizeMaxPixels: f32, + billboard: i32, + sizeUnits: i32, + alphaCutoff: f32 +}; + +@group(0) @binding(2) var icon: IconUniforms; +@group(0) @binding(3) var iconsTexture : texture_2d; +@group(0) @binding(4) var iconsTextureSampler : sampler; + +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; +} + +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) instancePickingColors: vec3, + @location(7) instanceIconFrames: vec4, + @location(8) instanceColorModes: f32, + @location(9) instanceOffsets: vec2, + @location(10) instancePixelOffset: vec2, +}; + +struct Varyings { + @builtin(position) position: vec4, + + @location(0) vColorMode: f32, + @location(1) vColor: vec4, + @location(2) vTextureCoords: vec2, + @location(3) uv: vec2, +}; + +@vertex +fn vertexMain(inp: Attributes) -> Varyings { + // write geometry fields used by filters + FS + geometry.worldPosition = inp.instancePositions; + geometry.uv = inp.positions; + geometry.pickingColor = inp.instancePickingColors; + + var outp: Varyings; + outp.uv = inp.positions; + + let iconSize = inp.instanceIconFrames.zw; + + // convert size in meters to pixels, then clamp + let sizePixels = clamp( + project_unit_size_to_pixel(inp.instanceSizes * icon.sizeScale, icon.sizeUnits), + icon.sizeMinPixels, icon.sizeMaxPixels + ); + + // scale icon height to match instanceSize + let iconConstraint = select(iconSize.y, iconSize.x, icon.sizeBasis == 0.0); + let instanceScale = select(sizePixels / iconConstraint, 0.0, iconConstraint == 0.0); + + // scale and rotate vertex in "pixel" units; then add per-instance pixel offset + 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; + + if (icon.billboard != 0) { + var pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, vec3(0.0)); // TODO, &geometry.position); + // DECKGL_FILTER_GL_POSITION(pos, geometry); + + var offset = vec3(pixelOffset, 0.0); + // DECKGL_FILTER_SIZE(offset, geometry); + let clipOffset = project_pixel_size_to_clipspace(offset.xy); + pos = vec4(pos.x + clipOffset.x, pos.y + clipOffset.y, pos.z, pos.w); + outp.position = pos; + } else { + var offset_common = vec3(project_pixel_size_vec2(pixelOffset), 0.0); + // DECKGL_FILTER_SIZE(offset_common, geometry); + var pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, offset_common); // TODO, &geometry.position); + // DECKGL_FILTER_GL_POSITION(pos, geometry); + outp.position = pos; + } + + 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.vColor = inp.instanceColors; + // DECKGL_FILTER_COLOR(outp.vColor, geometry); + + outp.vColorMode = inp.instanceColorModes; + + return outp; +} + +@fragment +fn fragmentMain(inp: Varyings) -> @location(0) vec4 { + // expose to deck.gl filter hooks + geometry.uv = inp.uv; + + let texColor = textureSample(iconsTexture, iconsTextureSampler, inp.vTextureCoords); + + // if colorMode == 0, use pixel color from the texture + // if colorMode == 1 (or picking), use texture as transparency mask + let rgb = mix(texColor.rgb, inp.vColor.rgb, inp.vColorMode); + let a = texColor.a * color.opacity * inp.vColor.a; + + if (a < icon.alphaCutoff) { + discard; + } + + var fragColor = deckgl_premultiplied_alpha(vec4(rgb, a)); + // DECKGL_FILTER_COLOR(fragColor, geometry); + return fragColor; +} +`; 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 aa20d536902..5f2bc35ad02 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 @@ -115,17 +115,13 @@ export default class MultiIconLayer extends } } - protected getInstanceOffset(icons: string): number[] { - return icons ? Array.from(icons).flatMap(icon => super.getInstanceOffset(icon)) : EMPTY_ARRAY; - } - - getInstanceColorMode(icons: string): number { - return 1; // mask - } - - getInstanceIconFrame(icons: string): number[] { + protected getInstanceIconDef(icons: string): number[] { return icons - ? Array.from(icons).flatMap(icon => super.getInstanceIconFrame(icon)) + ? Array.from(icons).flatMap(icon => { + const def = super.getInstanceIconDef(icon); + def[6] = 1; // mask + return def; + }) : EMPTY_ARRAY; } }