44import { LightingEffect , shadow } from '@deck.gl/core' ;
55import type { Texture } from '@luma.gl/core' ;
66import type { ShaderModule } from '@luma.gl/shadertools' ;
7+ import { EDITOR_LAYER_ID } from '@kepler.gl/constants' ;
78import { 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. */
2127interface 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 */
4254function 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+ / r e t u r n g l _ P o s i t i o n ; \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
75132const 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}
0 commit comments