From 3810cb13904c9689985b00553269488d91155a4f Mon Sep 17 00:00:00 2001 From: Felix Palmer Date: Tue, 7 Apr 2026 15:16:10 +0200 Subject: [PATCH 1/7] fix(extension): Mask & Terrain compatibility --- modules/core/src/passes/layers-pass.ts | 9 +++++++++ modules/extensions/src/terrain/shader-module.ts | 17 +++++++++++++++-- .../extensions/src/terrain/terrain-effect.ts | 14 +++++++++++++- modules/extensions/src/terrain/terrain-pass.ts | 1 - .../src/terrain/terrain-picking-pass.ts | 1 - 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/modules/core/src/passes/layers-pass.ts b/modules/core/src/passes/layers-pass.ts index 7c2adfac061..9941b81abb6 100644 --- a/modules/core/src/passes/layers-pass.ts +++ b/modules/core/src/passes/layers-pass.ts @@ -453,6 +453,15 @@ export default class LayersPass extends Pass { } } + // Ensure all default shader modules have an entry so their getUniforms is called. + // Without this, default modules added by effects (e.g. terrain) may not get their + // bindings set when rendered in passes that don't include those effects (e.g. mask pass). + for (const module of layer.context.defaultShaderModules) { + if (!(module.name in shaderModuleProps)) { + shaderModuleProps[module.name] = {}; + } + } + return mergeModuleParameters( shaderModuleProps, this.getShaderModuleProps(layer, effects, shaderModuleProps), diff --git a/modules/extensions/src/terrain/shader-module.ts b/modules/extensions/src/terrain/shader-module.ts index 447bac3359f..04300b577ac 100644 --- a/modules/extensions/src/terrain/shader-module.ts +++ b/modules/extensions/src/terrain/shader-module.ts @@ -129,6 +129,7 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US useTerrainHeightMap, terrainSkipRender } = opts; + const projectUniforms = project.getUniforms(opts.project) as ProjectUniforms; const {commonOrigin} = projectUniforms; @@ -180,10 +181,22 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US : [0, 0, 0, 0] }; } + // When terrain props are not provided (e.g. mask pass), provide the dummy + // texture to satisfy the terrain_map binding and prevent draw abort. + // dummyHeightMap is stored on the module by TerrainEffect.setup. + if (terrainModule.dummyHeightMap) { + return { + mode: TERRAIN_MODE.NONE, + terrain_map: terrainModule.dummyHeightMap, + bounds: [0, 0, 0, 0] + }; + } return {}; }, uniformTypes: { mode: 'f32', bounds: 'vec4' - } -} as const satisfies ShaderModule; + }, + /** Dummy texture stored by TerrainEffect.setup, used as fallback terrain_map binding */ + dummyHeightMap: null as Texture | null +} as ShaderModule & {dummyHeightMap: Texture | null}; diff --git a/modules/extensions/src/terrain/terrain-effect.ts b/modules/extensions/src/terrain/terrain-effect.ts index 4567abfeb5b..5f6576ce11a 100644 --- a/modules/extensions/src/terrain/terrain-effect.ts +++ b/modules/extensions/src/terrain/terrain-effect.ts @@ -38,6 +38,10 @@ export class TerrainEffect implements Effect { height: 1, data: new Uint8Array([0, 0, 0, 0]) }); + // Store on the module so it can provide a fallback terrain_map binding + // when rendered in passes that don't provide terrain effect props (e.g. mask pass) + terrainModule.dummyHeightMap = this.dummyHeightMap; + this.terrainPass = new TerrainPass(device, {id: 'terrain'}); this.terrainPickingPass = new TerrainPickingPass(device, {id: 'terrain-picking'}); @@ -83,7 +87,14 @@ export class TerrainEffect implements Effect { } const drapeLayers = layers.filter(l => l.state.terrainDrawMode === 'drape'); - this._updateTerrainCovers(terrainLayers, drapeLayers, viewport, opts); + // Filter out the terrain effect itself to avoid feedback loops when rendering terrain covers + // (the terrain cover FBO would be both read and written to). Other effects like MaskEffect + // need to be passed through so they can apply to draped layers. + const nonTerrainEffects = opts.effects?.filter(e => e !== this); + this._updateTerrainCovers(terrainLayers, drapeLayers, viewport, { + ...opts, + effects: nonTerrainEffects + }); } getShaderModuleProps( @@ -116,6 +127,7 @@ export class TerrainEffect implements Effect { if (this.dummyHeightMap) { this.dummyHeightMap.delete(); this.dummyHeightMap = undefined; + terrainModule.dummyHeightMap = null; } if (this.heightMap) { diff --git a/modules/extensions/src/terrain/terrain-pass.ts b/modules/extensions/src/terrain/terrain-pass.ts index 37ccd5fa936..209d2fe1491 100644 --- a/modules/extensions/src/terrain/terrain-pass.ts +++ b/modules/extensions/src/terrain/terrain-pass.ts @@ -73,7 +73,6 @@ export class TerrainPass extends LayersPass { target, pass: `terrain-cover-${terrainCover.id}`, layers, - effects: [], viewports: [viewport], clearColor: [0, 0, 0, 0] }); diff --git a/modules/extensions/src/terrain/terrain-picking-pass.ts b/modules/extensions/src/terrain/terrain-picking-pass.ts index da3a14abba6..1bcd5194711 100644 --- a/modules/extensions/src/terrain/terrain-picking-pass.ts +++ b/modules/extensions/src/terrain/terrain-picking-pass.ts @@ -69,7 +69,6 @@ export class TerrainPickingPass extends PickLayersPass { pickingFBO: target, pass: `terrain-cover-picking-${terrainCover.id}`, layers, - effects: [], viewports: [viewport], // Disable the default culling because TileLayer would cull sublayers based on the screen viewport, // not the viewport of the terrain cover. Culling is already done by `terrainCover.filterLayers` From 030be264a4b186e3cc3fe9717b7c0e0cdb08e0a4 Mon Sep 17 00:00:00 2001 From: Felix Palmer Date: Tue, 7 Apr 2026 15:32:37 +0200 Subject: [PATCH 2/7] Centralize dummy map --- .../extensions/src/terrain/shader-module.ts | 43 ++++++++++--------- .../extensions/src/terrain/terrain-effect.ts | 16 ++----- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/modules/extensions/src/terrain/shader-module.ts b/modules/extensions/src/terrain/shader-module.ts index 04300b577ac..c3507aa021f 100644 --- a/modules/extensions/src/terrain/shader-module.ts +++ b/modules/extensions/src/terrain/shader-module.ts @@ -17,7 +17,6 @@ export type TerrainModuleProps = { isPicking: boolean; heightMap: Texture | null; heightMapBounds?: Bounds | null; - dummyHeightMap: Texture; terrainCover?: TerrainCover | null; drawToTerrainHeightMap?: boolean; useTerrainHeightMap?: boolean; @@ -119,23 +118,27 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US }, // eslint-disable-next-line complexity getUniforms: (opts: Partial = {}) => { - if ('dummyHeightMap' in opts) { + const dummyHeightMap = terrainModule.dummyHeightMap; + if (!dummyHeightMap) { + // TerrainEffect has not been set up yet + return {}; + } + + if ('terrainSkipRender' in opts || 'drawToTerrainHeightMap' in opts) { const { drawToTerrainHeightMap, heightMap, heightMapBounds, - dummyHeightMap, terrainCover, useTerrainHeightMap, terrainSkipRender } = opts; - const projectUniforms = project.getUniforms(opts.project) as ProjectUniforms; const {commonOrigin} = projectUniforms; let mode: number = terrainSkipRender ? TERRAIN_MODE.SKIP : TERRAIN_MODE.NONE; // height map if case USE_HEIGHT_MAP, terrain cover if USE_COVER, otherwise empty - let sampler: Texture | undefined = dummyHeightMap as Texture; + let sampler: Texture = dummyHeightMap; // height map bounds if case USE_HEIGHT_MAP, terrain cover bounds if USE_COVER, otherwise null let bounds: number[] | null = null; if (drawToTerrainHeightMap) { @@ -150,15 +153,15 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US const fbo = opts.isPicking ? terrainCover.getPickingFramebuffer() : terrainCover.getRenderFramebuffer(); - sampler = fbo?.colorAttachments[0].texture; + const coverTexture = fbo?.colorAttachments[0].texture; if (opts.isPicking) { mode = TERRAIN_MODE.SKIP; } - if (sampler) { + if (coverTexture) { + sampler = coverTexture; mode = mode === TERRAIN_MODE.SKIP ? TERRAIN_MODE.USE_COVER_ONLY : TERRAIN_MODE.USE_COVER; bounds = terrainCover.bounds; } else { - sampler = dummyHeightMap!; if (opts.isPicking && !terrainSkipRender) { // terrain+draw layer without cover FBO: render own picking colors mode = TERRAIN_MODE.NONE; @@ -181,22 +184,20 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US : [0, 0, 0, 0] }; } - // When terrain props are not provided (e.g. mask pass), provide the dummy - // texture to satisfy the terrain_map binding and prevent draw abort. - // dummyHeightMap is stored on the module by TerrainEffect.setup. - if (terrainModule.dummyHeightMap) { - return { - mode: TERRAIN_MODE.NONE, - terrain_map: terrainModule.dummyHeightMap, - bounds: [0, 0, 0, 0] - }; - } - return {}; + // No terrain-specific props provided (e.g. mask pass or other non-terrain render pass). + // Provide the dummy texture to satisfy the terrain_map binding. + return { + mode: TERRAIN_MODE.NONE, + terrain_map: dummyHeightMap, + bounds: [0, 0, 0, 0] + }; }, uniformTypes: { mode: 'f32', bounds: 'vec4' }, - /** Dummy texture stored by TerrainEffect.setup, used as fallback terrain_map binding */ + /** Dummy texture created by TerrainEffect.setup, used as default terrain_map binding */ dummyHeightMap: null as Texture | null -} as ShaderModule & {dummyHeightMap: Texture | null}; +} as ShaderModule & { + dummyHeightMap: Texture | null; +}; diff --git a/modules/extensions/src/terrain/terrain-effect.ts b/modules/extensions/src/terrain/terrain-effect.ts index 5f6576ce11a..7a3828570d7 100644 --- a/modules/extensions/src/terrain/terrain-effect.ts +++ b/modules/extensions/src/terrain/terrain-effect.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Texture} from '@luma.gl/core'; import {log} from '@deck.gl/core'; import {terrainModule, TerrainModuleProps} from './shader-module'; @@ -23,8 +22,6 @@ export class TerrainEffect implements Effect { private isPicking: boolean = false; /** true if should use in the current pass */ private isDrapingEnabled: boolean = false; - /** An empty texture as placeholder */ - private dummyHeightMap?: Texture; /** A texture encoding the ground elevation, updated once per redraw. Used by layers with offset mode */ private heightMap?: HeightMapBuilder; private terrainPass!: TerrainPass; @@ -33,14 +30,11 @@ export class TerrainEffect implements Effect { private terrainCovers: Map = new Map(); setup({device, deck}: EffectContext) { - this.dummyHeightMap = device.createTexture({ + terrainModule.dummyHeightMap = device.createTexture({ width: 1, height: 1, data: new Uint8Array([0, 0, 0, 0]) }); - // Store on the module so it can provide a fallback terrain_map binding - // when rendered in passes that don't provide terrain effect props (e.g. mask pass) - terrainModule.dummyHeightMap = this.dummyHeightMap; this.terrainPass = new TerrainPass(device, {id: 'terrain'}); this.terrainPickingPass = new TerrainPickingPass(device, {id: 'terrain-picking'}); @@ -115,7 +109,6 @@ export class TerrainEffect implements Effect { isPicking: this.isPicking, heightMap: this.heightMap?.getRenderFramebuffer()?.colorAttachments[0].texture || null, heightMapBounds: this.heightMap?.bounds, - dummyHeightMap: this.dummyHeightMap!, terrainCover, useTerrainHeightMap: terrainDrawMode === 'offset', terrainSkipRender: terrainDrawMode === 'drape' || !layer.props.operation.includes('draw') @@ -124,9 +117,8 @@ export class TerrainEffect implements Effect { } cleanup({deck}: EffectContext): void { - if (this.dummyHeightMap) { - this.dummyHeightMap.delete(); - this.dummyHeightMap = undefined; + if (terrainModule.dummyHeightMap) { + terrainModule.dummyHeightMap.delete(); terrainModule.dummyHeightMap = null; } @@ -160,7 +152,6 @@ export class TerrainEffect implements Effect { shaderModuleProps: { terrain: { heightMapBounds: this.heightMap.bounds, - dummyHeightMap: this.dummyHeightMap, drawToTerrainHeightMap: true }, project: { @@ -221,7 +212,6 @@ export class TerrainEffect implements Effect { layers: drapeLayers, shaderModuleProps: { terrain: { - dummyHeightMap: this.dummyHeightMap, terrainSkipRender: false }, project: { From 1260e7f037f112de4178448b1851e340c90a8319 Mon Sep 17 00:00:00 2001 From: Felix Palmer Date: Tue, 7 Apr 2026 15:47:39 +0200 Subject: [PATCH 3/7] shared dummyTexture --- modules/core/src/index.ts | 1 + modules/core/src/lib/layer-manager.ts | 2 ++ modules/core/src/shaderlib/index.ts | 3 +- .../core/src/shaderlib/misc/dummy-texture.ts | 34 +++++++++++++++++++ .../extensions/src/terrain/shader-module.ts | 18 ++++------ .../extensions/src/terrain/terrain-effect.ts | 11 ------ 6 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 modules/core/src/shaderlib/misc/dummy-texture.ts diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index c473a67af9c..d05a246406b 100644 --- a/modules/core/src/index.ts +++ b/modules/core/src/index.ts @@ -47,6 +47,7 @@ export {default as FirstPersonViewport} from './viewports/first-person-viewport' // Shader modules export { color, + dummyTexture, picking, project, project32, diff --git a/modules/core/src/lib/layer-manager.ts b/modules/core/src/lib/layer-manager.ts index 33ea731d2c9..f937ba2d4db 100644 --- a/modules/core/src/lib/layer-manager.ts +++ b/modules/core/src/lib/layer-manager.ts @@ -6,6 +6,7 @@ import type {Device, RenderPass} from '@luma.gl/core'; import {Timeline} from '@luma.gl/engine'; import type {ShaderAssembler, ShaderModule} from '@luma.gl/shadertools'; import {getShaderAssembler, layerUniforms} from '../shaderlib/index'; +import {dummyTexture} from '../shaderlib/misc/dummy-texture'; import {LIFECYCLE} from '../lifecycle/constants'; import log from '../utils/log'; import debug from '../debug/index'; @@ -80,6 +81,7 @@ export default class LayerManager { // will be skipped. this.layers = []; this.resourceManager = new ResourceManager({device, protocol: 'deck://'}); + dummyTexture.setup(device); this.context = { mousePosition: null, diff --git a/modules/core/src/shaderlib/index.ts b/modules/core/src/shaderlib/index.ts index 1eda4ee9557..f4076d56ba7 100644 --- a/modules/core/src/shaderlib/index.ts +++ b/modules/core/src/shaderlib/index.ts @@ -4,6 +4,7 @@ import {ShaderAssembler, gouraudMaterial, phongMaterial} from '@luma.gl/shadertools'; import {layerUniforms} from './misc/layer-uniforms'; +import {dummyTexture} from './misc/dummy-texture'; import color from './color/color'; import geometry from './misc/geometry'; import project from './project/project'; @@ -46,7 +47,7 @@ export function getShaderAssembler(language: 'glsl' | 'wgsl'): ShaderAssembler { return shaderAssembler; } -export {layerUniforms, color, picking, project, project32, gouraudMaterial, phongMaterial, shadow}; +export {dummyTexture, layerUniforms, color, picking, project, project32, gouraudMaterial, phongMaterial, shadow}; // Useful for custom shader modules export type {ProjectProps, ProjectUniforms} from './project/viewport-uniforms'; diff --git a/modules/core/src/shaderlib/misc/dummy-texture.ts b/modules/core/src/shaderlib/misc/dummy-texture.ts new file mode 100644 index 00000000000..70f4346ff6b --- /dev/null +++ b/modules/core/src/shaderlib/misc/dummy-texture.ts @@ -0,0 +1,34 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {Device, Texture} from '@luma.gl/core'; + +/** + * A global 1x1 dummy texture, available for shader modules that declare texture + * bindings but may not always receive a real texture (e.g. the terrain module + * when rendered in a non-terrain pass like the mask pass). + * + * Created during LayerManager initialization, before any layers or effects set up. + */ +export const dummyTexture = { + /** The dummy texture. Available after `setup()` has been called. */ + texture: null as Texture | null, + + setup(device: Device): void { + if (!this.texture) { + this.texture = device.createTexture({ + width: 1, + height: 1, + data: new Uint8Array([0, 0, 0, 0]) + }); + } + }, + + cleanup(): void { + if (this.texture) { + this.texture.delete(); + this.texture = null; + } + } +}; diff --git a/modules/extensions/src/terrain/shader-module.ts b/modules/extensions/src/terrain/shader-module.ts index c3507aa021f..00e19e9f162 100644 --- a/modules/extensions/src/terrain/shader-module.ts +++ b/modules/extensions/src/terrain/shader-module.ts @@ -5,7 +5,7 @@ /* eslint-disable camelcase */ import type {ShaderModule} from '@luma.gl/shadertools'; -import {project, ProjectProps, ProjectUniforms} from '@deck.gl/core'; +import {dummyTexture, project, ProjectProps, ProjectUniforms} from '@deck.gl/core'; import type {Texture} from '@luma.gl/core'; import type {Bounds} from '../utils/projection-utils'; @@ -118,9 +118,7 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US }, // eslint-disable-next-line complexity getUniforms: (opts: Partial = {}) => { - const dummyHeightMap = terrainModule.dummyHeightMap; - if (!dummyHeightMap) { - // TerrainEffect has not been set up yet + if (!dummyTexture.texture) { return {}; } @@ -138,7 +136,7 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US let mode: number = terrainSkipRender ? TERRAIN_MODE.SKIP : TERRAIN_MODE.NONE; // height map if case USE_HEIGHT_MAP, terrain cover if USE_COVER, otherwise empty - let sampler: Texture = dummyHeightMap; + let sampler: Texture = dummyTexture.texture; // height map bounds if case USE_HEIGHT_MAP, terrain cover bounds if USE_COVER, otherwise null let bounds: number[] | null = null; if (drawToTerrainHeightMap) { @@ -188,16 +186,12 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US // Provide the dummy texture to satisfy the terrain_map binding. return { mode: TERRAIN_MODE.NONE, - terrain_map: dummyHeightMap, + terrain_map: dummyTexture.texture, bounds: [0, 0, 0, 0] }; }, uniformTypes: { mode: 'f32', bounds: 'vec4' - }, - /** Dummy texture created by TerrainEffect.setup, used as default terrain_map binding */ - dummyHeightMap: null as Texture | null -} as ShaderModule & { - dummyHeightMap: Texture | null; -}; + } +} as const satisfies ShaderModule; diff --git a/modules/extensions/src/terrain/terrain-effect.ts b/modules/extensions/src/terrain/terrain-effect.ts index 7a3828570d7..e983c7e45c8 100644 --- a/modules/extensions/src/terrain/terrain-effect.ts +++ b/modules/extensions/src/terrain/terrain-effect.ts @@ -30,12 +30,6 @@ export class TerrainEffect implements Effect { private terrainCovers: Map = new Map(); setup({device, deck}: EffectContext) { - terrainModule.dummyHeightMap = device.createTexture({ - width: 1, - height: 1, - data: new Uint8Array([0, 0, 0, 0]) - }); - this.terrainPass = new TerrainPass(device, {id: 'terrain'}); this.terrainPickingPass = new TerrainPickingPass(device, {id: 'terrain-picking'}); @@ -117,11 +111,6 @@ export class TerrainEffect implements Effect { } cleanup({deck}: EffectContext): void { - if (terrainModule.dummyHeightMap) { - terrainModule.dummyHeightMap.delete(); - terrainModule.dummyHeightMap = null; - } - if (this.heightMap) { this.heightMap.delete(); this.heightMap = undefined; From 046d50115d1f08da19604add87101cfd9eed533b Mon Sep 17 00:00:00 2001 From: Felix Palmer Date: Tue, 7 Apr 2026 15:59:54 +0200 Subject: [PATCH 4/7] WIP use dummy in other places --- .../src/effects/lighting/lighting-effect.ts | 15 ++------------- modules/core/src/shaderlib/shadow/shadow.ts | 12 ++++++------ .../collision-filter/collision-filter-effect.ts | 17 ++++------------- .../src/collision-filter/shader-module.ts | 12 ++++++------ modules/extensions/src/mask/mask-effect.ts | 16 +++------------- 5 files changed, 21 insertions(+), 51 deletions(-) diff --git a/modules/core/src/effects/lighting/lighting-effect.ts b/modules/core/src/effects/lighting/lighting-effect.ts index 87f87b7f2ff..4ffc77c0200 100644 --- a/modules/core/src/effects/lighting/lighting-effect.ts +++ b/modules/core/src/effects/lighting/lighting-effect.ts @@ -3,7 +3,6 @@ // Copyright (c) vis.gl contributors import type {Device} from '@luma.gl/core'; -import {Texture} from '@luma.gl/core'; import {AmbientLight} from './ambient-light'; import {DirectionalLight} from './directional-light'; import {PointLight} from './point-light'; @@ -48,7 +47,6 @@ export default class LightingEffect implements Effect { private directionalLights: DirectionalLight[] = []; private pointLights: PointLight[] = []; private shadowPasses: ShadowPass[] = []; - private dummyShadowMap: Texture | null = null; private shadowMatrices?: Matrix4[]; constructor(props: LightingEffectProps = {}) { @@ -59,15 +57,10 @@ export default class LightingEffect implements Effect { this.context = context; const {device, deck} = context; - if (this.shadow && !this.dummyShadowMap) { + if (this.shadow && this.shadowPasses.length === 0) { this._createShadowPasses(device); deck._addDefaultShaderModule(shadow); - - this.dummyShadowMap = device.createTexture({ - width: 1, - height: 1 - }); } } @@ -121,7 +114,6 @@ export default class LightingEffect implements Effect { shaderModuleProps: { shadow: { shadowLightId: i, - dummyShadowMap: this.dummyShadowMap, shadowMatrices: this.shadowMatrices } } @@ -134,7 +126,6 @@ export default class LightingEffect implements Effect { ? ({ project: otherShaderModuleProps.project, shadowMaps: this.shadowPasses.map(shadowPass => shadowPass.getShadowMap()), - dummyShadowMap: this.dummyShadowMap!, shadowColor: this.shadowColor, shadowMatrices: this.shadowMatrices } satisfies ShadowModuleProps) @@ -161,9 +152,7 @@ export default class LightingEffect implements Effect { } this.shadowPasses.length = 0; - if (this.dummyShadowMap) { - this.dummyShadowMap.destroy(); - this.dummyShadowMap = null; + if (this.shadow) { context.deck._removeDefaultShaderModule(shadow); } } diff --git a/modules/core/src/shaderlib/shadow/shadow.ts b/modules/core/src/shaderlib/shadow/shadow.ts index e1c68e83774..9e24fb76a32 100644 --- a/modules/core/src/shaderlib/shadow/shadow.ts +++ b/modules/core/src/shaderlib/shadow/shadow.ts @@ -4,6 +4,7 @@ import {COORDINATE_SYSTEM, PROJECTION_MODE} from '../../lib/constants'; import project from '../project/project'; +import {dummyTexture} from '../misc/dummy-texture'; import {Vector3, Matrix4} from '@math.gl/core'; import type {NumericArray} from '@math.gl/core'; import memoize from '../../utils/memoize'; @@ -123,7 +124,6 @@ export type ShadowModuleProps = { shadowEnabled?: boolean; drawToShadowMap?: boolean; shadowMaps?: Texture[]; - dummyShadowMap: Texture; shadowColor?: NumberArray4; shadowMatrices?: Matrix4[]; shadowLightId?: number; @@ -217,8 +217,8 @@ function createShadowUniforms( return { drawShadowMap: false, useShadowMap: false, - shadow_uShadowMap0: opts.dummyShadowMap!, - shadow_uShadowMap1: opts.dummyShadowMap! + shadow_uShadowMap0: dummyTexture.texture!, + shadow_uShadowMap1: dummyTexture.texture! }; } const projectUniforms = project.getUniforms(projectProps) as ProjectUniforms; @@ -259,8 +259,8 @@ function createShadowUniforms( color: opts.shadowColor || DEFAULT_SHADOW_COLOR, lightId: opts.shadowLightId || 0, lightCount: opts.shadowMatrices.length, - shadow_uShadowMap0: opts.dummyShadowMap!, - shadow_uShadowMap1: opts.dummyShadowMap! + shadow_uShadowMap0: dummyTexture.texture!, + shadow_uShadowMap1: dummyTexture.texture! }; for (let i = 0; i < viewProjectionMatrices.length; i++) { @@ -270,7 +270,7 @@ function createShadowUniforms( for (let i = 0; i < 2; i++) { uniforms[`shadow_uShadowMap${i}`] = - (opts.shadowMaps && opts.shadowMaps[i]) || opts.dummyShadowMap; + (opts.shadowMaps && opts.shadowMaps[i]) || dummyTexture.texture!; } return uniforms; } diff --git a/modules/extensions/src/collision-filter/collision-filter-effect.ts b/modules/extensions/src/collision-filter/collision-filter-effect.ts index ef058cae3a5..22b30e4c67e 100644 --- a/modules/extensions/src/collision-filter/collision-filter-effect.ts +++ b/modules/extensions/src/collision-filter/collision-filter-effect.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Device, Framebuffer, Texture} from '@luma.gl/core'; +import {Device, Framebuffer} from '@luma.gl/core'; import {equals} from '@math.gl/core'; import {_deepEqual as deepEqual} from '@deck.gl/core'; import type {Effect, EffectContext, Layer, PreRenderOptions, Viewport} from '@deck.gl/core'; @@ -33,13 +33,11 @@ export default class CollisionFilterEffect implements Effect { private channels: Record = {}; private collisionFilterPass?: CollisionFilterPass; private collisionFBOs: Record = {}; - private dummyCollisionMap?: Texture; private lastViewport?: Viewport; setup(context: EffectContext) { this.context = context; const {device} = context; - this.dummyCollisionMap = device.createTexture({width: 1, height: 1}); this.collisionFilterPass = new CollisionFilterPass(device, {id: 'default-collision-filter'}); } @@ -159,9 +157,7 @@ export default class CollisionFilterEffect implements Effect { views, shaderModuleProps: { collision: { - enabled: true, - // To avoid feedback loop forming between Framebuffer and active Texture. - dummyCollisionMap: this.dummyCollisionMap + enabled: true }, project: { // @ts-expect-error TODO - assuming WebGL context @@ -218,23 +214,18 @@ export default class CollisionFilterEffect implements Effect { } { const {collisionGroup, collisionEnabled} = (layer as Layer) .props; - const {collisionFBOs, dummyCollisionMap} = this; + const {collisionFBOs} = this; const collisionFBO = collisionFBOs[collisionGroup!]; const enabled = collisionEnabled && Boolean(collisionFBO); return { collision: { enabled, - collisionFBO, - dummyCollisionMap: dummyCollisionMap! + collisionFBO } }; } cleanup(): void { - if (this.dummyCollisionMap) { - this.dummyCollisionMap.delete(); - this.dummyCollisionMap = undefined; - } this.channels = {}; for (const collisionGroup of Object.keys(this.collisionFBOs)) { this.destroyFBO(collisionGroup); diff --git a/modules/extensions/src/collision-filter/shader-module.ts b/modules/extensions/src/collision-filter/shader-module.ts index 9e93a7a1aa2..abbabd59655 100644 --- a/modules/extensions/src/collision-filter/shader-module.ts +++ b/modules/extensions/src/collision-filter/shader-module.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Framebuffer, Texture, TextureView} from '@luma.gl/core'; +import {Framebuffer, TextureView} from '@luma.gl/core'; +import type {Texture} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; -import {project} from '@deck.gl/core'; +import {project, dummyTexture} from '@deck.gl/core'; const vs = /* glsl */ ` in float collisionPriorities; @@ -85,7 +86,6 @@ export type CollisionModuleProps = { enabled: boolean; collisionFBO?: Framebuffer; drawToCollisionMap?: boolean; - dummyCollisionMap?: Texture; }; /* eslint-disable camelcase */ @@ -101,15 +101,15 @@ type CollisionBindings = { const getCollisionUniforms = ( opts: CollisionModuleProps | {} ): CollisionBindings & CollisionUniforms => { - if (!opts || !('dummyCollisionMap' in opts)) { + if (!opts || !('enabled' in opts)) { return {}; } - const {enabled, collisionFBO, drawToCollisionMap, dummyCollisionMap} = opts; + const {enabled, collisionFBO, drawToCollisionMap} = opts; return { enabled: enabled && !drawToCollisionMap, sort: Boolean(drawToCollisionMap), collision_texture: - !drawToCollisionMap && collisionFBO ? collisionFBO.colorAttachments[0] : dummyCollisionMap + !drawToCollisionMap && collisionFBO ? collisionFBO.colorAttachments[0] : dummyTexture.texture! }; }; diff --git a/modules/extensions/src/mask/mask-effect.ts b/modules/extensions/src/mask/mask-effect.ts index 6680fd2ce4a..083aaa8472c 100644 --- a/modules/extensions/src/mask/mask-effect.ts +++ b/modules/extensions/src/mask/mask-effect.ts @@ -9,7 +9,8 @@ import { EffectContext, PreRenderOptions, CoordinateSystem, - log + log, + dummyTexture } from '@deck.gl/core'; import type {Texture} from '@luma.gl/core'; import {equals} from '@math.gl/core'; @@ -47,7 +48,6 @@ export default class MaskEffect implements Effect { useInPicking = true; order = 0; - private dummyMaskMap?: Texture; private channels: (Channel | null)[] = []; private masks: Record | null = null; private maskPass?: MaskPass; @@ -55,11 +55,6 @@ export default class MaskEffect implements Effect { private lastViewport?: Viewport; setup({device}: EffectContext) { - this.dummyMaskMap = device.createTexture({ - width: 1, - height: 1 - }); - this.maskPass = new MaskPass(device, {id: 'default-mask'}); this.maskMap = this.maskPass.maskMap; } @@ -271,18 +266,13 @@ export default class MaskEffect implements Effect { } { return { mask: { - maskMap: this.masks ? this.maskMap! : this.dummyMaskMap!, + maskMap: this.masks ? this.maskMap! : dummyTexture.texture!, maskChannels: this.masks } }; } cleanup(): void { - if (this.dummyMaskMap) { - this.dummyMaskMap.delete(); - this.dummyMaskMap = undefined; - } - if (this.maskPass) { this.maskPass.delete(); this.maskPass = undefined; From 191f6bb230686434b90f65a0addebea55113948b Mon Sep 17 00:00:00 2001 From: Felix Palmer Date: Tue, 7 Apr 2026 16:00:36 +0200 Subject: [PATCH 5/7] Revert "WIP use dummy in other places" This reverts commit 046d50115d1f08da19604add87101cfd9eed533b. --- .../src/effects/lighting/lighting-effect.ts | 15 +++++++++++++-- modules/core/src/shaderlib/shadow/shadow.ts | 12 ++++++------ .../collision-filter/collision-filter-effect.ts | 17 +++++++++++++---- .../src/collision-filter/shader-module.ts | 12 ++++++------ modules/extensions/src/mask/mask-effect.ts | 16 +++++++++++++--- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/modules/core/src/effects/lighting/lighting-effect.ts b/modules/core/src/effects/lighting/lighting-effect.ts index 4ffc77c0200..87f87b7f2ff 100644 --- a/modules/core/src/effects/lighting/lighting-effect.ts +++ b/modules/core/src/effects/lighting/lighting-effect.ts @@ -3,6 +3,7 @@ // Copyright (c) vis.gl contributors import type {Device} from '@luma.gl/core'; +import {Texture} from '@luma.gl/core'; import {AmbientLight} from './ambient-light'; import {DirectionalLight} from './directional-light'; import {PointLight} from './point-light'; @@ -47,6 +48,7 @@ export default class LightingEffect implements Effect { private directionalLights: DirectionalLight[] = []; private pointLights: PointLight[] = []; private shadowPasses: ShadowPass[] = []; + private dummyShadowMap: Texture | null = null; private shadowMatrices?: Matrix4[]; constructor(props: LightingEffectProps = {}) { @@ -57,10 +59,15 @@ export default class LightingEffect implements Effect { this.context = context; const {device, deck} = context; - if (this.shadow && this.shadowPasses.length === 0) { + if (this.shadow && !this.dummyShadowMap) { this._createShadowPasses(device); deck._addDefaultShaderModule(shadow); + + this.dummyShadowMap = device.createTexture({ + width: 1, + height: 1 + }); } } @@ -114,6 +121,7 @@ export default class LightingEffect implements Effect { shaderModuleProps: { shadow: { shadowLightId: i, + dummyShadowMap: this.dummyShadowMap, shadowMatrices: this.shadowMatrices } } @@ -126,6 +134,7 @@ export default class LightingEffect implements Effect { ? ({ project: otherShaderModuleProps.project, shadowMaps: this.shadowPasses.map(shadowPass => shadowPass.getShadowMap()), + dummyShadowMap: this.dummyShadowMap!, shadowColor: this.shadowColor, shadowMatrices: this.shadowMatrices } satisfies ShadowModuleProps) @@ -152,7 +161,9 @@ export default class LightingEffect implements Effect { } this.shadowPasses.length = 0; - if (this.shadow) { + if (this.dummyShadowMap) { + this.dummyShadowMap.destroy(); + this.dummyShadowMap = null; context.deck._removeDefaultShaderModule(shadow); } } diff --git a/modules/core/src/shaderlib/shadow/shadow.ts b/modules/core/src/shaderlib/shadow/shadow.ts index 9e24fb76a32..e1c68e83774 100644 --- a/modules/core/src/shaderlib/shadow/shadow.ts +++ b/modules/core/src/shaderlib/shadow/shadow.ts @@ -4,7 +4,6 @@ import {COORDINATE_SYSTEM, PROJECTION_MODE} from '../../lib/constants'; import project from '../project/project'; -import {dummyTexture} from '../misc/dummy-texture'; import {Vector3, Matrix4} from '@math.gl/core'; import type {NumericArray} from '@math.gl/core'; import memoize from '../../utils/memoize'; @@ -124,6 +123,7 @@ export type ShadowModuleProps = { shadowEnabled?: boolean; drawToShadowMap?: boolean; shadowMaps?: Texture[]; + dummyShadowMap: Texture; shadowColor?: NumberArray4; shadowMatrices?: Matrix4[]; shadowLightId?: number; @@ -217,8 +217,8 @@ function createShadowUniforms( return { drawShadowMap: false, useShadowMap: false, - shadow_uShadowMap0: dummyTexture.texture!, - shadow_uShadowMap1: dummyTexture.texture! + shadow_uShadowMap0: opts.dummyShadowMap!, + shadow_uShadowMap1: opts.dummyShadowMap! }; } const projectUniforms = project.getUniforms(projectProps) as ProjectUniforms; @@ -259,8 +259,8 @@ function createShadowUniforms( color: opts.shadowColor || DEFAULT_SHADOW_COLOR, lightId: opts.shadowLightId || 0, lightCount: opts.shadowMatrices.length, - shadow_uShadowMap0: dummyTexture.texture!, - shadow_uShadowMap1: dummyTexture.texture! + shadow_uShadowMap0: opts.dummyShadowMap!, + shadow_uShadowMap1: opts.dummyShadowMap! }; for (let i = 0; i < viewProjectionMatrices.length; i++) { @@ -270,7 +270,7 @@ function createShadowUniforms( for (let i = 0; i < 2; i++) { uniforms[`shadow_uShadowMap${i}`] = - (opts.shadowMaps && opts.shadowMaps[i]) || dummyTexture.texture!; + (opts.shadowMaps && opts.shadowMaps[i]) || opts.dummyShadowMap; } return uniforms; } diff --git a/modules/extensions/src/collision-filter/collision-filter-effect.ts b/modules/extensions/src/collision-filter/collision-filter-effect.ts index 22b30e4c67e..ef058cae3a5 100644 --- a/modules/extensions/src/collision-filter/collision-filter-effect.ts +++ b/modules/extensions/src/collision-filter/collision-filter-effect.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Device, Framebuffer} from '@luma.gl/core'; +import {Device, Framebuffer, Texture} from '@luma.gl/core'; import {equals} from '@math.gl/core'; import {_deepEqual as deepEqual} from '@deck.gl/core'; import type {Effect, EffectContext, Layer, PreRenderOptions, Viewport} from '@deck.gl/core'; @@ -33,11 +33,13 @@ export default class CollisionFilterEffect implements Effect { private channels: Record = {}; private collisionFilterPass?: CollisionFilterPass; private collisionFBOs: Record = {}; + private dummyCollisionMap?: Texture; private lastViewport?: Viewport; setup(context: EffectContext) { this.context = context; const {device} = context; + this.dummyCollisionMap = device.createTexture({width: 1, height: 1}); this.collisionFilterPass = new CollisionFilterPass(device, {id: 'default-collision-filter'}); } @@ -157,7 +159,9 @@ export default class CollisionFilterEffect implements Effect { views, shaderModuleProps: { collision: { - enabled: true + enabled: true, + // To avoid feedback loop forming between Framebuffer and active Texture. + dummyCollisionMap: this.dummyCollisionMap }, project: { // @ts-expect-error TODO - assuming WebGL context @@ -214,18 +218,23 @@ export default class CollisionFilterEffect implements Effect { } { const {collisionGroup, collisionEnabled} = (layer as Layer) .props; - const {collisionFBOs} = this; + const {collisionFBOs, dummyCollisionMap} = this; const collisionFBO = collisionFBOs[collisionGroup!]; const enabled = collisionEnabled && Boolean(collisionFBO); return { collision: { enabled, - collisionFBO + collisionFBO, + dummyCollisionMap: dummyCollisionMap! } }; } cleanup(): void { + if (this.dummyCollisionMap) { + this.dummyCollisionMap.delete(); + this.dummyCollisionMap = undefined; + } this.channels = {}; for (const collisionGroup of Object.keys(this.collisionFBOs)) { this.destroyFBO(collisionGroup); diff --git a/modules/extensions/src/collision-filter/shader-module.ts b/modules/extensions/src/collision-filter/shader-module.ts index abbabd59655..9e93a7a1aa2 100644 --- a/modules/extensions/src/collision-filter/shader-module.ts +++ b/modules/extensions/src/collision-filter/shader-module.ts @@ -2,10 +2,9 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Framebuffer, TextureView} from '@luma.gl/core'; -import type {Texture} from '@luma.gl/core'; +import {Framebuffer, Texture, TextureView} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; -import {project, dummyTexture} from '@deck.gl/core'; +import {project} from '@deck.gl/core'; const vs = /* glsl */ ` in float collisionPriorities; @@ -86,6 +85,7 @@ export type CollisionModuleProps = { enabled: boolean; collisionFBO?: Framebuffer; drawToCollisionMap?: boolean; + dummyCollisionMap?: Texture; }; /* eslint-disable camelcase */ @@ -101,15 +101,15 @@ type CollisionBindings = { const getCollisionUniforms = ( opts: CollisionModuleProps | {} ): CollisionBindings & CollisionUniforms => { - if (!opts || !('enabled' in opts)) { + if (!opts || !('dummyCollisionMap' in opts)) { return {}; } - const {enabled, collisionFBO, drawToCollisionMap} = opts; + const {enabled, collisionFBO, drawToCollisionMap, dummyCollisionMap} = opts; return { enabled: enabled && !drawToCollisionMap, sort: Boolean(drawToCollisionMap), collision_texture: - !drawToCollisionMap && collisionFBO ? collisionFBO.colorAttachments[0] : dummyTexture.texture! + !drawToCollisionMap && collisionFBO ? collisionFBO.colorAttachments[0] : dummyCollisionMap }; }; diff --git a/modules/extensions/src/mask/mask-effect.ts b/modules/extensions/src/mask/mask-effect.ts index 083aaa8472c..6680fd2ce4a 100644 --- a/modules/extensions/src/mask/mask-effect.ts +++ b/modules/extensions/src/mask/mask-effect.ts @@ -9,8 +9,7 @@ import { EffectContext, PreRenderOptions, CoordinateSystem, - log, - dummyTexture + log } from '@deck.gl/core'; import type {Texture} from '@luma.gl/core'; import {equals} from '@math.gl/core'; @@ -48,6 +47,7 @@ export default class MaskEffect implements Effect { useInPicking = true; order = 0; + private dummyMaskMap?: Texture; private channels: (Channel | null)[] = []; private masks: Record | null = null; private maskPass?: MaskPass; @@ -55,6 +55,11 @@ export default class MaskEffect implements Effect { private lastViewport?: Viewport; setup({device}: EffectContext) { + this.dummyMaskMap = device.createTexture({ + width: 1, + height: 1 + }); + this.maskPass = new MaskPass(device, {id: 'default-mask'}); this.maskMap = this.maskPass.maskMap; } @@ -266,13 +271,18 @@ export default class MaskEffect implements Effect { } { return { mask: { - maskMap: this.masks ? this.maskMap! : dummyTexture.texture!, + maskMap: this.masks ? this.maskMap! : this.dummyMaskMap!, maskChannels: this.masks } }; } cleanup(): void { + if (this.dummyMaskMap) { + this.dummyMaskMap.delete(); + this.dummyMaskMap = undefined; + } + if (this.maskPass) { this.maskPass.delete(); this.maskPass = undefined; From 31bca0924c157db270832792a7f4d26399d9f527 Mon Sep 17 00:00:00 2001 From: Felix Palmer Date: Tue, 7 Apr 2026 16:34:13 +0200 Subject: [PATCH 6/7] Lint --- modules/core/src/shaderlib/index.ts | 12 +++++++++++- modules/extensions/src/terrain/shader-module.ts | 11 ++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/modules/core/src/shaderlib/index.ts b/modules/core/src/shaderlib/index.ts index f4076d56ba7..04eb1c3e589 100644 --- a/modules/core/src/shaderlib/index.ts +++ b/modules/core/src/shaderlib/index.ts @@ -47,7 +47,17 @@ export function getShaderAssembler(language: 'glsl' | 'wgsl'): ShaderAssembler { return shaderAssembler; } -export {dummyTexture, layerUniforms, color, picking, project, project32, gouraudMaterial, phongMaterial, shadow}; +export { + dummyTexture, + layerUniforms, + color, + picking, + project, + project32, + gouraudMaterial, + phongMaterial, + shadow +}; // Useful for custom shader modules export type {ProjectProps, ProjectUniforms} from './project/viewport-uniforms'; diff --git a/modules/extensions/src/terrain/shader-module.ts b/modules/extensions/src/terrain/shader-module.ts index 00e19e9f162..9ed58f8a480 100644 --- a/modules/extensions/src/terrain/shader-module.ts +++ b/modules/extensions/src/terrain/shader-module.ts @@ -131,8 +131,7 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US useTerrainHeightMap, terrainSkipRender } = opts; - const projectUniforms = project.getUniforms(opts.project) as ProjectUniforms; - const {commonOrigin} = projectUniforms; + const {commonOrigin} = project.getUniforms(opts.project) as ProjectUniforms; let mode: number = terrainSkipRender ? TERRAIN_MODE.SKIP : TERRAIN_MODE.NONE; // height map if case USE_HEIGHT_MAP, terrain cover if USE_COVER, otherwise empty @@ -159,11 +158,9 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US sampler = coverTexture; mode = mode === TERRAIN_MODE.SKIP ? TERRAIN_MODE.USE_COVER_ONLY : TERRAIN_MODE.USE_COVER; bounds = terrainCover.bounds; - } else { - if (opts.isPicking && !terrainSkipRender) { - // terrain+draw layer without cover FBO: render own picking colors - mode = TERRAIN_MODE.NONE; - } + } else if (opts.isPicking && !terrainSkipRender) { + // terrain+draw layer without cover FBO: render own picking colors + mode = TERRAIN_MODE.NONE; } } From 41d7dfb5ef75f7725d2a994d9885e787f12e9e58 Mon Sep 17 00:00:00 2001 From: Felix Palmer Date: Wed, 8 Apr 2026 13:38:40 +0200 Subject: [PATCH 7/7] underscore export --- modules/core/src/index.ts | 2 +- modules/extensions/src/terrain/shader-module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index d05a246406b..ba35f18431c 100644 --- a/modules/core/src/index.ts +++ b/modules/core/src/index.ts @@ -47,7 +47,7 @@ export {default as FirstPersonViewport} from './viewports/first-person-viewport' // Shader modules export { color, - dummyTexture, + dummyTexture as _dummyTexture, picking, project, project32, diff --git a/modules/extensions/src/terrain/shader-module.ts b/modules/extensions/src/terrain/shader-module.ts index 9ed58f8a480..8fe2771f658 100644 --- a/modules/extensions/src/terrain/shader-module.ts +++ b/modules/extensions/src/terrain/shader-module.ts @@ -5,7 +5,7 @@ /* eslint-disable camelcase */ import type {ShaderModule} from '@luma.gl/shadertools'; -import {dummyTexture, project, ProjectProps, ProjectUniforms} from '@deck.gl/core'; +import {_dummyTexture as dummyTexture, project, ProjectProps, ProjectUniforms} from '@deck.gl/core'; import type {Texture} from '@luma.gl/core'; import type {Bounds} from '../utils/projection-utils';