Skip to content

Commit dd403d4

Browse files
igorDykhtaIhor Dykhta
andauthored
fix: fixes related to deck.gl upgrade (#3380)
* fix: polygon tool regression after deck.gl upgrade Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * fix tile 3d layer crash; don't render polygon tool Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * revert fragile injection Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * fix stuck tiles Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * fix polygon tool again Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * pbr lighting to simple phong - eliminate radial specular/diffuse Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * more fixes Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * fix tests Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> * nit comments Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> --------- Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local> Co-authored-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
1 parent eadf5ae commit dd403d4

6 files changed

Lines changed: 146 additions & 19 deletions

File tree

src/components/src/modals/export-video-modal.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ describe('ExportVideoModal', () => {
158158
parameters: {blend: true},
159159
controller: true,
160160
views: {},
161+
_isExport: true,
161162
effects: []
162163
});
163164
});

src/components/src/modals/export-video-modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ const ExportVideoModalFactory = () => {
328328
const deckPropsWithEffects = useMemo(
329329
() => ({
330330
...hubbleDeckGlProps,
331+
_isExport: true,
331332
effects: deckEffects
332333
}),
333334
[hubbleDeckGlProps, deckEffects]

src/components/src/plot-container.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ export default function PlotContainerFactory(
360360
isExport: true,
361361
deckGlProps: {
362362
...mapFields.deckGlProps,
363+
_isExport: true,
363364
useDevicePixels: false,
364365
deviceProps: {
365366
webgl: {

src/constants/src/default-settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,8 +1412,8 @@ export const LIGHT_AND_SHADOW_EFFECT: EffectDescription = {
14121412
parameters: [
14131413
{name: 'timestamp', min: 0, max: Number.MAX_SAFE_INTEGER},
14141414
{name: 'shadowIntensity', min: 0, max: 1, defaultValue: DEFAULT_SHADOW_INTENSITY},
1415-
{name: 'sunLightIntensity', min: 0, max: 10, defaultValue: DEFAULT_LIGHT_INTENSITY},
1416-
{name: 'ambientLightIntensity', min: 0, max: 10, defaultValue: DEFAULT_LIGHT_INTENSITY},
1415+
{name: 'sunLightIntensity', min: 0, max: 5, defaultValue: DEFAULT_LIGHT_INTENSITY},
1416+
{name: 'ambientLightIntensity', min: 0, max: 5, defaultValue: DEFAULT_LIGHT_INTENSITY},
14171417
{name: 'shadowColor', type: 'color', min: 0, max: 255, defaultValue: DEFAULT_SHADOW_COLOR},
14181418
{name: 'sunLightColor', type: 'color', min: 0, max: 255, defaultValue: DEFAULT_LIGHT_COLOR},
14191419
{name: 'ambientLightColor', type: 'color', min: 0, max: 255, defaultValue: DEFAULT_LIGHT_COLOR}

src/effects/src/custom-deck-lighting-effect.ts

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44
import {LightingEffect, shadow} from '@deck.gl/core';
55
import type {Texture} from '@luma.gl/core';
66
import type {ShaderModule} from '@luma.gl/shadertools';
7+
import {EDITOR_LAYER_ID} from '@kepler.gl/constants';
78
import {patchTileViewportIds} from './tile-viewport-fix';
89

10+
// A plain LightingEffect() — its constructor calls _applyDefaultLights()
11+
// which populates the same default lights that deck.gl's EffectManager
12+
// uses internally. This is used to restore the original shading.
13+
const DEFAULT_LIGHTING_EFFECT = new LightingEffect();
14+
915
/**
1016
* Exposes private members of LightingEffect that we need to access.
1117
* These are runtime-accessible but TypeScript marks them as private.
@@ -20,6 +26,7 @@ interface LightingEffectPrivate {
2026
/** Extended shadow module props with our custom field. */
2127
interface CustomShadowProps {
2228
outputUniformShadow?: boolean;
29+
useSimplePhong?: boolean;
2330
dummyShadowMap?: Texture | null;
2431
[key: string]: unknown;
2532
}
@@ -34,29 +41,78 @@ function insertBefore(source: string, target: string, textToInsert: string): str
3441
}
3542

3643
/**
37-
* Create a patched shadow module that adds `outputUniformShadow` to the
38-
* shadow UBO. When true, `shadow_getShadowWeight` returns 1.0 (full
39-
* uniform shadow) instead of sampling the shadow map. Used for nighttime
40-
* rendering to avoid partial shadows from below.
44+
* Create a patched shadow module that adds:
45+
* - `outputUniformShadow`: when true, shadow_getShadowWeight returns 1.0
46+
* (full uniform shadow) instead of sampling the shadow map.
47+
* - `useSimplePhong`: when true, replaces PBR lighting with simple
48+
* Lambertian diffuse (ambient + NdotL) for 3D tile layers, avoiding
49+
* the PBR microfacet specular artifacts on flat surfaces.
50+
*
51+
* Also fixes the vertex injection to be a no-op when shadows are disabled,
52+
* preventing position corruption in billboard layers (e.g. the editor layer).
4153
*/
4254
function createCustomShadowModule(): ShaderModule | null {
4355
if (!shadow) return null;
4456

4557
const mod = {...shadow} as Record<string, any>;
4658

47-
const uboField = ' float outputUniformShadow;\n';
59+
const uboField = ' float outputUniformShadow;\n float useSimplePhong;\n';
4860
mod.vs = insertBefore(mod.vs, '} shadow;', uboField);
4961
mod.fs = insertBefore(mod.fs, '} shadow;', uboField);
5062

63+
// Add a varying to carry the common-space position for simple phong normals.
64+
mod.vs = 'out vec3 custom_vWorldPos;\n' + mod.vs;
65+
mod.fs = 'in vec3 custom_vWorldPos;\n' + mod.fs;
66+
5167
mod.fs = insertBefore(
5268
mod.fs,
5369
'vec4 rgbaDepth = texture(shadowMap, position.xy);',
5470
'if (shadow.outputUniformShadow > 0.5) return 1.0;\n '
5571
);
5672

73+
// Patch the VS so the fallback returns the caller's position instead of
74+
// gl_Position (which is uninitialised when the hook fires in billboard
75+
// layers like PathLayer / the editor polygon tool).
76+
mod.vs = mod.vs
77+
.replace(
78+
'vec4 shadow_setVertexPosition(vec4 position_commonspace)',
79+
'vec4 shadow_setVertexPosition(vec4 position_commonspace, vec4 currentPosition)'
80+
)
81+
.replace(
82+
/return gl_Position;\s*\}/,
83+
'return currentPosition;\n}'
84+
);
85+
86+
mod.inject = {
87+
...shadow.inject,
88+
'vs:DECKGL_FILTER_GL_POSITION': `
89+
position = shadow_setVertexPosition(geometry.position, position);
90+
custom_vWorldPos = geometry.position.xyz;
91+
`,
92+
'fs:DECKGL_FILTER_COLOR': `
93+
#ifdef LIGHTING_FRAGMENT
94+
if (shadow.useSimplePhong > 0.5) {
95+
vec3 spDx = dFdx(custom_vWorldPos);
96+
vec3 spDy = dFdy(custom_vWorldPos);
97+
vec3 spN = normalize(cross(spDx, spDy));
98+
vec3 spLit = lighting.ambientColor;
99+
for (int i = 0; i < 3; i++) {
100+
if (i >= lighting.directionalLightCount) break;
101+
vec3 spDir = lighting_getDirectionalLight(i).direction;
102+
float spNdotL = max(dot(spN, -normalize(spDir)), 0.0);
103+
spLit += lighting_getDirectionalLight(i).color * spNdotL;
104+
}
105+
color.rgb *= spLit;
106+
}
107+
#endif
108+
color = shadow_filterShadowColor(color);
109+
`
110+
};
111+
57112
mod.uniformTypes = {
58113
...shadow.uniformTypes,
59-
outputUniformShadow: 'f32'
114+
outputUniformShadow: 'f32',
115+
useSimplePhong: 'f32'
60116
};
61117

62118
const originalGetUniforms = shadow.getUniforms as (
@@ -66,6 +122,7 @@ function createCustomShadowModule(): ShaderModule | null {
66122
mod.getUniforms = (opts: CustomShadowProps = {}, context = {}) => {
67123
const u = originalGetUniforms(opts, context);
68124
u.outputUniformShadow = opts.outputUniformShadow ? 1.0 : 0.0;
125+
u.useSimplePhong = opts.useSimplePhong ? 1.0 : 0.0;
69126
return u;
70127
};
71128

@@ -74,12 +131,21 @@ function createCustomShadowModule(): ShaderModule | null {
74131

75132
const CustomShadowModule = createCustomShadowModule();
76133

134+
/**
135+
* Detect layers that use the PBR shader module (3D tile sublayers:
136+
* ScenegraphLayer sets `_lighting: 'pbr'`, SimpleMeshLayer sets `pbrMaterial`).
137+
*/
138+
function isPbrLayer(layer: any): boolean {
139+
return layer.props?._lighting === 'pbr' || layer.props?.pbrMaterial !== undefined;
140+
}
141+
77142
/**
78143
* Custom LightingEffect for kepler.gl.
79144
*
80145
* Extends deck.gl's LightingEffect with:
81146
* - A patched shadow module with `outputUniformShadow` for uniform shadow
82147
* during nighttime (avoids partial shadows from below).
148+
* - Simple phong replacement for PBR layers (avoids microfacet specular).
83149
* - getShaderModuleProps override that always provides dummyShadowMap
84150
* to prevent "Bad texture binding" errors when shadows are disabled.
85151
*/
@@ -110,6 +176,10 @@ class CustomDeckLightingEffect extends LightingEffect {
110176
preRender(opts) {
111177
if (!this._private.shadow) return;
112178

179+
// Filter editor layers out of the shadow pass so they don't cast shadows.
180+
const originalLayers = opts.layers;
181+
opts.layers = originalLayers.filter(l => !l.id.startsWith(EDITOR_LAYER_ID));
182+
113183
let unpatch: (() => void) | undefined;
114184
if (this.isExportMode) {
115185
unpatch = patchTileViewportIds(opts);
@@ -118,6 +188,8 @@ class CustomDeckLightingEffect extends LightingEffect {
118188
super.preRender(opts);
119189

120190
unpatch?.();
191+
192+
opts.layers = originalLayers;
121193
}
122194

123195
cleanup(context) {
@@ -133,6 +205,34 @@ class CustomDeckLightingEffect extends LightingEffect {
133205
}
134206

135207
getShaderModuleProps(layer, otherShaderModuleProps) {
208+
// Skip lighting/shadow for the editor layer and its sublayers.
209+
if (layer.id.startsWith(EDITOR_LAYER_ID)) {
210+
return {
211+
shadow: {
212+
shadowEnabled: false,
213+
dummyShadowMap: this._private.dummyShadowMap
214+
},
215+
lighting: {enabled: false},
216+
phongMaterial: null,
217+
gouraudMaterial: null
218+
} as unknown as ReturnType<LightingEffect['getShaderModuleProps']>;
219+
}
220+
221+
// When the effect is disabled (ghost kept alive to preserve the shadow
222+
// shader module), delegate to a default LightingEffect so phong/gouraud
223+
// layers get the same shading they had before the effect was ever added.
224+
if (!this._private.shadow) {
225+
const defaults = DEFAULT_LIGHTING_EFFECT.getShaderModuleProps(layer, otherShaderModuleProps);
226+
return {
227+
...defaults,
228+
shadow: {
229+
shadowEnabled: false,
230+
dummyShadowMap: this._private.dummyShadowMap
231+
},
232+
pbrMaterial: {unlit: 1}
233+
} as unknown as ReturnType<LightingEffect['getShaderModuleProps']>;
234+
}
235+
136236
const props = super.getShaderModuleProps(layer, otherShaderModuleProps);
137237

138238
if (
@@ -147,6 +247,21 @@ class CustomDeckLightingEffect extends LightingEffect {
147247
(props.shadow as CustomShadowProps).outputUniformShadow = this.outputUniformShadow;
148248
}
149249

250+
if (isPbrLayer(layer)) {
251+
// PBR layers: disable PBR lighting (unlit=1 → outputs base texture color),
252+
// keep directional lights in the lighting uniforms, and enable our simple
253+
// phong replacement in the DECKGL_FILTER_COLOR hook. This gives us
254+
// ambient + NdotL diffuse without PBR microfacet specular artifacts.
255+
// Light direction is sky→surface (deck.gl convention), negated in GLSL.
256+
(props as any).pbrMaterial = {unlit: 1};
257+
if (props.shadow) {
258+
(props.shadow as CustomShadowProps).useSimplePhong = true;
259+
}
260+
return props;
261+
}
262+
263+
(props as any).pbrMaterial = {unlit: 0};
264+
150265
return props;
151266
}
152267
}

src/layers/src/tile3d-layer/tile3d-layer.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ function _checkLightingActive(context: any): boolean {
5555
*/
5656
// @ts-expect-error Types have separate declarations of a private property '_loadTileset'.
5757
class KeplerTile3DLayer extends DeckTile3DLayer {
58+
shouldUpdateState(params: any): boolean {
59+
if (super.shouldUpdateState(params)) return true;
60+
const lightingActive = _checkLightingActive(this.context);
61+
return lightingActive !== ((this.state as any)?._lightingWasActive ?? false);
62+
}
63+
5864
// deck.gl Tile3DLayer.updateState only sets `needsUpdate` on cached tile
5965
// sub-layers when `propsChanged` fires, but NOT when `extensionsChanged`
6066
// fires (e.g. the shadow module being added/removed). The LayerManager sets
@@ -83,10 +89,7 @@ class KeplerTile3DLayer extends DeckTile3DLayer {
8389
const {layerMap} = this.state as any;
8490
if (layerMap) {
8591
for (const key in layerMap) {
86-
const entry = layerMap[key];
87-
if (entry?.layer) {
88-
entry.layer.needsUpdate = true;
89-
}
92+
layerMap[key].needsUpdate = true;
9093
}
9194
}
9295
}
@@ -120,6 +123,13 @@ class KeplerTile3DLayer extends DeckTile3DLayer {
120123
const viewportsNumber = Object.keys(viewports).length;
121124
if (!timeline || !viewportsNumber || !tileset3d) return;
122125

126+
// We want higher detail for video/image export.
127+
const isExporting = (this.context as any)?.deck?.props?._isExport;
128+
if (isExporting) {
129+
const baseSSE: number = tileset3d.options?.maximumScreenSpaceError ?? 8;
130+
tileset3d.memoryAdjustedScreenSpaceError = baseSSE / 2;
131+
}
132+
123133
tileset3d
124134
.selectTiles(Object.values(viewports))
125135
.then((frameNumber: number) => {
@@ -275,20 +285,19 @@ class KeplerTile3DLayer extends DeckTile3DLayer {
275285
}
276286

277287
/**
278-
* During video export (preserveDrawingBuffer), report the layer as not loaded
279-
* until every selected tile has a renderable sublayer. Hubble.gl's
280-
* DeckAdapter.onAfterRender checks layer.isLoaded on every top-level layer
281-
* before capturing a frame, so returning false here makes it wait until the
282-
* LOD transition is complete — no frame is captured with missing tiles.
288+
* During video/image export, report the layer as not loaded until every
289+
* selected tile has a renderable sublayer. Hubble.gl's DeckAdapter and
290+
* PlotContainer check layer.isLoaded before capturing a frame, so
291+
* returning false here makes them wait until the LOD transition is
292+
* complete — no frame is captured with missing tiles.
283293
*/
284294
get isLoaded(): boolean {
285295
const baseLoaded = super.isLoaded;
286296
if (!baseLoaded) {
287297
return false;
288298
}
289299

290-
const gl = this.context?.gl;
291-
const isExporting = gl?.getContextAttributes?.()?.preserveDrawingBuffer;
300+
const isExporting = (this.context as any)?.deck?.props?._isExport;
292301
if (!isExporting) {
293302
return true;
294303
}

0 commit comments

Comments
 (0)