diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index c473a67af9c..ba35f18431c 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 as _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/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/core/src/shaderlib/index.ts b/modules/core/src/shaderlib/index.ts index 1eda4ee9557..04eb1c3e589 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,17 @@ 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 447bac3359f..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 {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'; @@ -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,22 +118,24 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US }, // eslint-disable-next-line complexity getUniforms: (opts: Partial = {}) => { - if ('dummyHeightMap' in opts) { + if (!dummyTexture.texture) { + 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; + 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 - let sampler: Texture | undefined = dummyHeightMap as Texture; + 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) { @@ -149,19 +150,17 @@ 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; - } + } else if (opts.isPicking && !terrainSkipRender) { + // terrain+draw layer without cover FBO: render own picking colors + mode = TERRAIN_MODE.NONE; } } @@ -180,7 +179,13 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US : [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: dummyTexture.texture, + bounds: [0, 0, 0, 0] + }; }, uniformTypes: { mode: 'f32', diff --git a/modules/extensions/src/terrain/terrain-effect.ts b/modules/extensions/src/terrain/terrain-effect.ts index 4567abfeb5b..e983c7e45c8 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,11 +30,6 @@ export class TerrainEffect implements Effect { private terrainCovers: Map = new Map(); setup({device, deck}: EffectContext) { - this.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'}); @@ -83,7 +75,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( @@ -104,7 +103,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') @@ -113,11 +111,6 @@ export class TerrainEffect implements Effect { } cleanup({deck}: EffectContext): void { - if (this.dummyHeightMap) { - this.dummyHeightMap.delete(); - this.dummyHeightMap = undefined; - } - if (this.heightMap) { this.heightMap.delete(); this.heightMap = undefined; @@ -148,7 +141,6 @@ export class TerrainEffect implements Effect { shaderModuleProps: { terrain: { heightMapBounds: this.heightMap.bounds, - dummyHeightMap: this.dummyHeightMap, drawToTerrainHeightMap: true }, project: { @@ -209,7 +201,6 @@ export class TerrainEffect implements Effect { layers: drapeLayers, shaderModuleProps: { terrain: { - dummyHeightMap: this.dummyHeightMap, terrainSkipRender: false }, project: { 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`