Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export {default as FirstPersonViewport} from './viewports/first-person-viewport'
// Shader modules
export {
color,
dummyTexture as _dummyTexture,
picking,
project,
project32,
Expand Down
2 changes: 2 additions & 0 deletions modules/core/src/lib/layer-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions modules/core/src/passes/layers-pass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
13 changes: 12 additions & 1 deletion modules/core/src/shaderlib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
34 changes: 34 additions & 0 deletions modules/core/src/shaderlib/misc/dummy-texture.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would execute extreme caution when using any singleton involving GPU resources. There could be multiple Deck instances on the same page, each using their own device.

/** 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;
}
}
};
37 changes: 21 additions & 16 deletions modules/extensions/src/terrain/shader-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,7 +17,6 @@ export type TerrainModuleProps = {
isPicking: boolean;
heightMap: Texture | null;
heightMapBounds?: Bounds | null;
dummyHeightMap: Texture;
terrainCover?: TerrainCover | null;
drawToTerrainHeightMap?: boolean;
useTerrainHeightMap?: boolean;
Expand Down Expand Up @@ -119,22 +118,24 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US
},
// eslint-disable-next-line complexity
getUniforms: (opts: Partial<TerrainModuleProps> = {}) => {
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) {
Expand All @@ -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;
}
}

Expand All @@ -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',
Expand Down
25 changes: 8 additions & 17 deletions modules/extensions/src/terrain/terrain-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -33,11 +30,6 @@ export class TerrainEffect implements Effect {
private terrainCovers: Map<string, TerrainCover> = 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'});

Expand Down Expand Up @@ -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(
Expand All @@ -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')
Expand All @@ -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;
Expand Down Expand Up @@ -148,7 +141,6 @@ export class TerrainEffect implements Effect {
shaderModuleProps: {
terrain: {
heightMapBounds: this.heightMap.bounds,
dummyHeightMap: this.dummyHeightMap,
drawToTerrainHeightMap: true
},
project: {
Expand Down Expand Up @@ -209,7 +201,6 @@ export class TerrainEffect implements Effect {
layers: drapeLayers,
shaderModuleProps: {
terrain: {
dummyHeightMap: this.dummyHeightMap,
terrainSkipRender: false
},
project: {
Expand Down
1 change: 0 additions & 1 deletion modules/extensions/src/terrain/terrain-pass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export class TerrainPass extends LayersPass {
target,
pass: `terrain-cover-${terrainCover.id}`,
layers,
effects: [],
viewports: [viewport],
clearColor: [0, 0, 0, 0]
});
Expand Down
1 change: 0 additions & 1 deletion modules/extensions/src/terrain/terrain-picking-pass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading