Repository: playcanvas/engine
Pull request #7362 (2025-02-20 15:56:46)

Examples disable user selection
### Overview
- Disables user selection for all html elements in each example iframe (conflicts with mobile interactions)



Patch:
diff --git a/examples/iframe/main.css b/examples/iframe/main.css
index 253e75709..1e476ef29 100644
--- a/examples/iframe/main.css
+++ b/examples/iframe/main.css
@@ -1,3 +1,11 @@
+* {
+    -webkit-user-select: none;      /* iOS Safari */
+    user-select: none;              /* Other browsers */
+    -webkit-touch-callout: none;    /* Prevents text selection popups on iOS */
+    -webkit-user-drag: none;        /* Prevents dragging images on iOS */
+    touch-action: none;             /* Prevent touch interactions */
+}
+
 body {
     margin: 0;
     overflow: hidden;
diff --git a/src/core/preprocessor.js b/src/core/preprocessor.js
index e0b811f31..db524cabe 100644
--- a/src/core/preprocessor.js
+++ b/src/core/preprocessor.js
@@ -105,10 +105,10 @@ class Preprocessor {
             }
         });
 
-        // extract defines with name starting with __INJECT_
+        // extract defines with name starting with _INJECT_
         const injectDefines = new Map();
         defines.forEach((value, key) => {
-            if (key.startsWith('__INJECT_')) {
+            if (key.startsWith('_INJECT_')) {
                 injectDefines.set(key, value);
             }
         });
diff --git a/src/extras/renderers/outline-renderer.js b/src/extras/renderers/outline-renderer.js
index 66d52f6fa..4cd7d1653 100644
--- a/src/extras/renderers/outline-renderer.js
+++ b/src/extras/renderers/outline-renderer.js
@@ -97,11 +97,12 @@ class OutlineRenderer {
         this.outlineShaderPass = this.outlineCameraEntity.camera.setShaderPass('OutlineShaderPass');
 
         // function called after the camera has rendered the outline objects to the texture
-        app.scene.on('postrender', (cameraComponent) => {
+        this.postRender = (cameraComponent) => {
             if (this.outlineCameraEntity.camera === cameraComponent) {
                 this.onPostRender();
             }
-        });
+        };
+        app.scene.on('postrender', this.postRender);
 
         // add the camera to the scene
         this.app.root.addChild(this.outlineCameraEntity);
@@ -148,6 +149,8 @@ class OutlineRenderer {
         this.tempRt.destroy();
         this.tempRt = null;
 
+        this.app.scene.off('postrender', this.postRender);
+
         this.quadRenderer?.destroy();
         this.quadRenderer = null;
     }
diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js
index b50c03b34..53236e533 100644
--- a/src/platform/graphics/graphics-device.js
+++ b/src/platform/graphics/graphics-device.js
@@ -400,6 +400,14 @@ class GraphicsDevice extends EventHandler {
      */
     _shadersDirty = false;
 
+    /**
+     * A list of shader defines based on the capabilities of the device.
+     *
+     * @type {Map<string, string>}
+     * @ignore
+     */
+    capsDefines = new Map();
+
     static EVENT_RESIZE = 'resizecanvas';
 
     constructor(canvas, options) {
@@ -477,6 +485,17 @@ class GraphicsDevice extends EventHandler {
         });
     }
 
+    /**
+     * Initialize the map of device capabilities, which are supplied to shaders as defines.
+     *
+     * @ignore
+     */
+    initCapsDefines() {
+        const { capsDefines } = this;
+        capsDefines.clear();
+        if (this.textureFloatFilterable) capsDefines.set('CAPS_TEXTURE_FLOAT_FILTERABLE', '');
+    }
+
     /**
      * Destroy the graphics device.
      */
diff --git a/src/platform/graphics/shader-utils.js b/src/platform/graphics/shader-utils.js
index c2f57a37c..3fc44a5f0 100644
--- a/src/platform/graphics/shader-utils.js
+++ b/src/platform/graphics/shader-utils.js
@@ -109,8 +109,8 @@ class ShaderUtils {
         let vertCode;
         let fragCode;
 
-        const vertexDefinesCode = ShaderUtils.getDefinesCode(options.vertexDefines);
-        const fragmentDefinesCode = ShaderUtils.getDefinesCode(options.fragmentDefines);
+        const vertexDefinesCode = ShaderUtils.getDefinesCode(device, options.vertexDefines);
+        const fragmentDefinesCode = ShaderUtils.getDefinesCode(device, options.fragmentDefines);
         const wgsl = options.shaderLanguage === SHADERLANGUAGE_WGSL;
 
         if (wgsl) {
@@ -160,15 +160,24 @@ class ShaderUtils {
     }
 
     /**
+     * @param {GraphicsDevice} device - The graphics device.
      * @param {Map<string, string>} [defines] - A map containing key-value pairs.
      * @returns {string} The shader code for the defines.
      * @private
      */
-    static getDefinesCode(defines) {
+    static getDefinesCode(device, defines) {
         let code = '';
+
+        device.capsDefines.forEach((value, key) => {
+            code += `#define ${key} ${value}\n`;
+        });
+        code += '\n';
+
         defines?.forEach((value, key) => {
             code += `#define ${key} ${value}\n`;
         });
+        code += '\n';
+
         return code;
     }
 
diff --git a/src/platform/graphics/webgl/webgl-graphics-device.js b/src/platform/graphics/webgl/webgl-graphics-device.js
index fb997b679..530ff9a1f 100644
--- a/src/platform/graphics/webgl/webgl-graphics-device.js
+++ b/src/platform/graphics/webgl/webgl-graphics-device.js
@@ -866,6 +866,8 @@ class WebglGraphicsDevice extends GraphicsDevice {
         if (this.maxTextures <= 8) {
             this.supportsAreaLights = false;
         }
+
+        this.initCapsDefines();
     }
 
     /**
diff --git a/src/platform/graphics/webgl/webgl-render-target.js b/src/platform/graphics/webgl/webgl-render-target.js
index f9fb0f20b..320bc73c5 100644
--- a/src/platform/graphics/webgl/webgl-render-target.js
+++ b/src/platform/graphics/webgl/webgl-render-target.js
@@ -150,6 +150,12 @@ class WebglRenderTarget {
 
         } else {
 
+            Debug.call(() => {
+                if (target.width <= 0 || target.height <= 0) {
+                    Debug.warnOnce(`Invalid render target size: ${target.width} x ${target.height}`, target);
+                }
+            });
+
             // ##### Create main FBO #####
             this._glFrameBuffer = gl.createFramebuffer();
             device.setFramebuffer(this._glFrameBuffer);
@@ -224,12 +230,6 @@ class WebglRenderTarget {
         // ##### Create MSAA FBO #####
         if (target._samples > 1) {
 
-            Debug.call(() => {
-                if (target.width <= 0 || target.height <= 0) {
-                    Debug.warnOnce(`Invalid render target size: ${target.width} x ${target.height}`, target);
-                }
-            });
-
             // Use previous FBO for resolves
             this._glResolveFrameBuffer = this._glFrameBuffer;
 
diff --git a/src/platform/graphics/webgl/webgl-texture.js b/src/platform/graphics/webgl/webgl-texture.js
index 13e408041..f68ba16b4 100644
--- a/src/platform/graphics/webgl/webgl-texture.js
+++ b/src/platform/graphics/webgl/webgl-texture.js
@@ -632,7 +632,7 @@ class WebglTexture {
                                 Math.max(Math.floor(texture._width * resMult), 1),
                                 Math.max(Math.floor(texture._height * resMult), 1),
                                 1,
-                                this._glFormat,
+                                this._glInternalFormat,
                                 mipObject[index]
                             );
                         }
diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js
index ceec28780..bbe77bca2 100644
--- a/src/platform/graphics/webgpu/webgpu-graphics-device.js
+++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js
@@ -172,6 +172,8 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
         // WGSL features
         const wgslFeatures = navigator.gpu.wgslLanguageFeatures;
         this.supportsStorageTextureRead = wgslFeatures?.has('readonly_and_readwrite_storage_textures');
+
+        this.initCapsDefines();
     }
 
     async initWebGpu(glslangUrl, twgslUrl) {
diff --git a/src/scene/constants.js b/src/scene/constants.js
index 9761018d0..ad9e9027f 100644
--- a/src/scene/constants.js
+++ b/src/scene/constants.js
@@ -226,6 +226,12 @@ export const LIGHTTYPE_SPOT = 2;
 // private - the number of light types
 export const LIGHTTYPE_COUNT = 3;
 
+export const lightTypeNames = {
+    [LIGHTTYPE_DIRECTIONAL]: 'DIRECTIONAL',
+    [LIGHTTYPE_OMNI]: 'OMNI',
+    [LIGHTTYPE_SPOT]: 'SPOT'
+};
+
 /**
  * Infinitesimally small point light source shape.
  *
@@ -258,6 +264,13 @@ export const LIGHTSHAPE_DISK = 2;
  */
 export const LIGHTSHAPE_SPHERE = 3;
 
+export const lightShapeNames = {
+    [LIGHTSHAPE_PUNCTUAL]: 'PUNCTUAL',
+    [LIGHTSHAPE_RECT]: 'RECT',
+    [LIGHTSHAPE_DISK]: 'DISK',
+    [LIGHTSHAPE_SPHERE]: 'SPHERE'
+};
+
 /**
  * Linear distance falloff model for light attenuation.
  *
@@ -384,15 +397,15 @@ export const SHADOW_PCF5_16F = 9;
  * @ignore
  */
 export const shadowTypeInfo = new Map([
-    [SHADOW_PCF1_32F,    { name: 'PCF1_32F', format: PIXELFORMAT_DEPTH, pcf: true }],
-    [SHADOW_PCF3_32F,    { name: 'PCF3_32F', format: PIXELFORMAT_DEPTH, pcf: true }],
-    [SHADOW_PCF5_32F,    { name: 'PCF5_32F', format: PIXELFORMAT_DEPTH, pcf: true }],
-    [SHADOW_PCF1_16F,    { name: 'PCF1_16F', format: PIXELFORMAT_DEPTH16, pcf: true }],
-    [SHADOW_PCF3_16F,    { name: 'PCF3_16F', format: PIXELFORMAT_DEPTH16, pcf: true }],
-    [SHADOW_PCF5_16F,    { name: 'PCF5_16F', format: PIXELFORMAT_DEPTH16, pcf: true }],
-    [SHADOW_VSM_16F,     { name: 'VSM_16F', format: PIXELFORMAT_RGBA16F, vsm: true }],
-    [SHADOW_VSM_32F,     { name: 'VSM_32F', format: PIXELFORMAT_RGBA32F, vsm: true }],
-    [SHADOW_PCSS_32F,    { name: 'PCSS_32F', format: PIXELFORMAT_R32F, pcss: true }]
+    [SHADOW_PCF1_32F,    { name: 'PCF1_32F', kind: 'PCF1', format: PIXELFORMAT_DEPTH, pcf: true }],
+    [SHADOW_PCF3_32F,    { name: 'PCF3_32F', kind: 'PCF3', format: PIXELFORMAT_DEPTH, pcf: true }],
+    [SHADOW_PCF5_32F,    { name: 'PCF5_32F', kind: 'PCF5', format: PIXELFORMAT_DEPTH, pcf: true }],
+    [SHADOW_PCF1_16F,    { name: 'PCF1_16F', kind: 'PCF1', format: PIXELFORMAT_DEPTH16, pcf: true }],
+    [SHADOW_PCF3_16F,    { name: 'PCF3_16F', kind: 'PCF3', format: PIXELFORMAT_DEPTH16, pcf: true }],
+    [SHADOW_PCF5_16F,    { name: 'PCF5_16F', kind: 'PCF5', format: PIXELFORMAT_DEPTH16, pcf: true }],
+    [SHADOW_VSM_16F,     { name: 'VSM_16F', kind: 'VSM', format: PIXELFORMAT_RGBA16F, vsm: true }],
+    [SHADOW_VSM_32F,     { name: 'VSM_32F', kind: 'VSM', format: PIXELFORMAT_RGBA32F, vsm: true }],
+    [SHADOW_PCSS_32F,    { name: 'PCSS_32F', kind: 'PCSS', format: PIXELFORMAT_R32F, pcss: true }]
 ]);
 
 /**
diff --git a/src/scene/shader-lib/chunks-wgsl/chunks-wgsl.js b/src/scene/shader-lib/chunks-wgsl/chunks-wgsl.js
index 79c4507fc..f3c3b61f2 100644
--- a/src/scene/shader-lib/chunks-wgsl/chunks-wgsl.js
+++ b/src/scene/shader-lib/chunks-wgsl/chunks-wgsl.js
@@ -71,8 +71,10 @@ import immediateLineVS from './internal/vert/immediateLine.js';
 // import iridescencePS from './standard/frag/iridescence.js';
 // import iridescenceThicknessPS from './standard/frag/iridescenceThickness.js';
 // import iorPS from './standard/frag/ior.js';
+// import lightDeclarationPS from './lit/frag/lightDeclaration.js';
 // import lightDiffuseLambertPS from './lit/frag/lightDiffuseLambert.js';
 // import lightDirPointPS from './lit/frag/lightDirPoint.js';
+// import lightingPS from './lit/frag/lighting/lighting.js';
 // import lightmapAddPS from './lit/frag/lightmapAdd.js';
 // import lightmapDirAddPS from './lit/frag/lightmapDirAdd.js';
 // import lightmapDirPS from './standard/frag/lightmapDir.js';
@@ -159,13 +161,12 @@ import immediateLineVS from './internal/vert/immediateLine.js';
 // import screenDepthPS from './common/frag/screenDepth.js';
 // import shadowCascadesPS from './lit/frag/shadowCascades.js';
 // import shadowEVSMPS from './lit/frag/shadowEVSM.js';
-// import shadowEVSMnPS from './lit/frag/shadowEVSMn.js';
+// import shadowPCF1PS from './lit/frag/shadowPCF1.js';
+// import shadowPCF3PS from './lit/frag/shadowPCF3.js';
+// import shadowPCF5PS from './lit/frag/shadowPCF5.js';
 // import shadowPCSSPS from './lit/frag/shadowPCSS.js';
 // import shadowSampleCoordPS from './lit/frag/shadowSampleCoord.js';
 // import shadowSoftPS from './lit/frag/shadowSoft.js';
-// import shadowStandardPS from './lit/frag/shadowStandard.js';
-// import shadowStandardGL2PS from './lit/frag/shadowStandardGL2.js';
-// import shadowVSM_commonPS from './lit/frag/shadowVSM_common.js';
 // import skinBatchVS from './common/vert/skinBatch.js';
 // import skinVS from './common/vert/skin.js';
 import skyboxPS from './skybox/frag/skybox.js';
@@ -283,8 +284,10 @@ const shaderChunksWGSL = {
     // iridescencePS,
     // iridescenceThicknessPS,
     // iorPS,
+    // lightDeclarationPS,
     // lightDiffuseLambertPS,
     // lightDirPointPS,
+    // lightingPS,
     // lightmapAddPS,
     // lightmapDirAddPS,
     // lightmapDirPS,
@@ -371,13 +374,12 @@ const shaderChunksWGSL = {
     // screenDepthPS,
     // shadowCascadesPS,
     // shadowEVSMPS,
-    // shadowEVSMnPS,
+    // shadowPCF1PS,
+    // shadowPCF3PS
+    // shadowPCF5PS,
     // shadowPCSSPS,
     // shadowSampleCoordPS,
     // shadowSoftPS,
-    // shadowStandardPS,
-    // shadowStandardGL2PS,
-    // shadowVSM_commonPS,
     // skinBatchVS,
     // skinVS,
     skyboxPS,
diff --git a/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js b/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js
index 0b17df670..b966cd25e 100644
--- a/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js
+++ b/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js
@@ -47,13 +47,13 @@ export default /* wgsl */`
             #endif
 
             dir.x *= -1.0;
-            linear = __INJECT_SKYBOX_DECODE_FNC(textureSample(texture_cubeMap, texture_cubeMap_sampler, dir));
+            linear = _INJECT_SKYBOX_DECODE_FNC(textureSample(texture_cubeMap, texture_cubeMap_sampler, dir));
 
         #else // env-atlas
 
             dir = input.vViewDir * vec3f(-1.0, 1.0, 1.0);
             let uv : vec2f = toSphericalUv(normalize(dir));
-            linear = __INJECT_SKYBOX_DECODE_FNC(textureSample(texture_envAtlas, texture_envAtlas_sampler, mapRoughnessUv(uv, uniform.mipLevel)));
+            linear = _INJECT_SKYBOX_DECODE_FNC(textureSample(texture_envAtlas, texture_envAtlas_sampler, mapRoughnessUv(uv, uniform.mipLevel)));
 
         #endif
 
diff --git a/src/scene/shader-lib/chunks/chunk-validation.js b/src/scene/shader-lib/chunks/chunk-validation.js
index 689f8add3..26dfca71d 100644
--- a/src/scene/shader-lib/chunks/chunk-validation.js
+++ b/src/scene/shader-lib/chunks/chunk-validation.js
@@ -63,9 +63,6 @@ const chunkVersions = {
     shadowCoordPS: CHUNKAPI_1_62,
     shadowCoordPerspZBufferPS: CHUNKAPI_1_62,
     shadowEVSMPS: CHUNKAPI_1_62,
-    shadowEVSMnPS: CHUNKAPI_1_62,
-    shadowStandardPS: CHUNKAPI_1_62,
-    shadowStandardGL2PS: CHUNKAPI_1_62,
     spotPS: CHUNKAPI_1_62,
     TBNPS: CHUNKAPI_1_62,
     TBNObjectSpacePS: CHUNKAPI_1_62,
@@ -114,7 +111,11 @@ const removedChunks = {
     fogNonePS: CHUNKAPI_2_5,
     gamma1_0PS: CHUNKAPI_2_5,
     gamma2_2PS: CHUNKAPI_2_5,
-    storeEVSMPS: CHUNKAPI_2_6
+    storeEVSMPS: CHUNKAPI_2_6,
+    shadowEVSMnPS: CHUNKAPI_2_6,
+    shadowVSM_commonPS: CHUNKAPI_2_6,
+    shadowStandardPS: CHUNKAPI_2_6,
+    shadowStandardGL2PS: CHUNKAPI_2_6
 };
 
 // compare two "major.minor" semantic version strings and return true if a is a smaller version than b.
diff --git a/src/scene/shader-lib/chunks/chunks.js b/src/scene/shader-lib/chunks/chunks.js
index ab3a409b1..e41ff3e79 100644
--- a/src/scene/shader-lib/chunks/chunks.js
+++ b/src/scene/shader-lib/chunks/chunks.js
@@ -71,8 +71,10 @@ import iridescenceDiffractionPS from './lit/frag/iridescenceDiffraction.js';
 import iridescencePS from './standard/frag/iridescence.js';
 import iridescenceThicknessPS from './standard/frag/iridescenceThickness.js';
 import iorPS from './standard/frag/ior.js';
+import lightDeclarationPS from './lit/frag/lighting/lightDeclaration.js';
 import lightDiffuseLambertPS from './lit/frag/lightDiffuseLambert.js';
 import lightDirPointPS from './lit/frag/lightDirPoint.js';
+import lightingPS from './lit/frag/lighting/lighting.js';
 import lightmapAddPS from './lit/frag/lightmapAdd.js';
 import lightmapDirAddPS from './lit/frag/lightmapDirAdd.js';
 import lightmapDirPS from './standard/frag/lightmapDir.js';
@@ -157,15 +159,14 @@ import refractionDynamicPS from './lit/frag/refractionDynamic.js';
 import reprojectPS from './common/frag/reproject.js';
 import sampleCatmullRomPS from './common/frag/sampleCatmullRom.js';
 import screenDepthPS from './common/frag/screenDepth.js';
-import shadowCascadesPS from './lit/frag/shadowCascades.js';
-import shadowEVSMPS from './lit/frag/shadowEVSM.js';
-import shadowEVSMnPS from './lit/frag/shadowEVSMn.js';
-import shadowPCSSPS from './lit/frag/shadowPCSS.js';
-import shadowSampleCoordPS from './lit/frag/shadowSampleCoord.js';
-import shadowSoftPS from './lit/frag/shadowSoft.js';
-import shadowStandardPS from './lit/frag/shadowStandard.js';
-import shadowStandardGL2PS from './lit/frag/shadowStandardGL2.js';
-import shadowVSM_commonPS from './lit/frag/shadowVSM_common.js';
+import shadowCascadesPS from './lit/frag/lighting/shadowCascades.js';
+import shadowEVSMPS from './lit/frag/lighting/shadowEVSM.js';
+import shadowPCF1PS from './lit/frag/lighting/shadowPCF1.js';
+import shadowPCF3PS from './lit/frag/lighting/shadowPCF3.js';
+import shadowPCF5PS from './lit/frag/lighting/shadowPCF5.js';
+import shadowPCSSPS from './lit/frag/lighting/shadowPCSS.js';
+import shadowSampleCoordPS from './lit/frag/lighting/shadowSampleCoord.js';
+import shadowSoftPS from './lit/frag/lighting/shadowSoft.js';
 import skinBatchVS from './common/vert/skinBatch.js';
 import skinVS from './common/vert/skin.js';
 import skyboxPS from './skybox/frag/skybox.js';
@@ -283,8 +284,10 @@ const shaderChunks = {
     iridescencePS,
     iridescenceThicknessPS,
     iorPS,
+    lightDeclarationPS,
     lightDiffuseLambertPS,
     lightDirPointPS,
+    lightingPS,
     lightmapAddPS,
     lightmapDirAddPS,
     lightmapDirPS,
@@ -371,13 +374,12 @@ const shaderChunks = {
     screenDepthPS,
     shadowCascadesPS,
     shadowEVSMPS,
-    shadowEVSMnPS,
+    shadowPCF1PS,
+    shadowPCF3PS,
+    shadowPCF5PS,
     shadowPCSSPS,
     shadowSampleCoordPS,
     shadowSoftPS,
-    shadowStandardPS,
-    shadowStandardGL2PS,
-    shadowVSM_commonPS,
     skinBatchVS,
     skinVS,
     skyboxPS,
diff --git a/src/scene/shader-lib/chunks/common/frag/spherical.js b/src/scene/shader-lib/chunks/common/frag/spherical.js
index 845f4ea0f..a966e275c 100644
--- a/src/scene/shader-lib/chunks/common/frag/spherical.js
+++ b/src/scene/shader-lib/chunks/common/frag/spherical.js
@@ -1,12 +1,11 @@
 export default /* glsl */`
 // equirectangular helper functions
-const float PI = 3.141592653589793;
-
 vec2 toSpherical(vec3 dir) {
     return vec2(dir.xz == vec2(0.0) ? 0.0 : atan(dir.x, dir.z), asin(dir.y));
 }
 
 vec2 toSphericalUv(vec3 dir) {
+    const float PI = 3.141592653589793;
     vec2 uv = toSpherical(dir) / vec2(PI * 2.0, PI) + 0.5;
     return vec2(uv.x, 1.0 - uv.y);
 }
diff --git a/src/scene/shader-lib/chunks/lit/frag/lightSheen.js b/src/scene/shader-lib/chunks/lit/frag/lightSheen.js
index a5f6e6ac5..09550abea 100644
--- a/src/scene/shader-lib/chunks/lit/frag/lightSheen.js
+++ b/src/scene/shader-lib/chunks/lit/frag/lightSheen.js
@@ -1,6 +1,7 @@
 export default /* glsl */`
 
 float sheenD(vec3 normal, vec3 h, float roughness) {
+    const float PI = 3.141592653589793;
     float invR = 1.0 / (roughness * roughness);
     float cos2h = max(dot(normal, h), 0.0);
     cos2h *= cos2h;
diff --git a/src/scene/shader-lib/chunks/lit/frag/lighting/lightDeclaration.js b/src/scene/shader-lib/chunks/lit/frag/lighting/lightDeclaration.js
new file mode 100644
index 000000000..ad718586b
--- /dev/null
+++ b/src/scene/shader-lib/chunks/lit/frag/lighting/lightDeclaration.js
@@ -0,0 +1,93 @@
+// uniforms for a light with index {i}, driven by defines
+export default /* glsl */`
+#if defined(LIGHT{i})
+
+    uniform vec3 light{i}_color;
+
+    #if LIGHT{i}TYPE == DIRECTIONAL
+        uniform vec3 light{i}_direction;
+    #else
+        uniform vec3 light{i}_position;
+        uniform float light{i}_radius;
+
+        #if LIGHT{i}TYPE == SPOT
+            uniform vec3 light{i}_direction;
+            uniform float light{i}_innerConeAngle;
+            uniform float light{i}_outerConeAngle;
+        #endif
+    #endif
+
+    // area lights
+    #if LIGHT{i}SHAPE != PUNCTUAL
+        #if LIGHT{i}TYPE == DIRECTIONAL
+            uniform vec3 light{i}_position;
+        #endif
+        uniform vec3 light{i}_halfWidth;
+        uniform vec3 light{i}_halfHeight;
+    #endif
+
+    // shadow casting
+    #if defined(LIGHT{i}CASTSHADOW)
+
+        uniform mat4 light{i}_shadowMatrix;
+        uniform float light{i}_shadowIntensity;
+        uniform vec4 light{i}_shadowParams; // width, height, bias, radius
+
+        #if LIGHT{i}SHADOWTYPE == PCSS_32F
+            uniform float light{i}_shadowSearchArea;
+            uniform vec4 light{i}_cameraParams;
+            #if LIGHT{i}TYPE == DIRECTIONAL
+                uniform vec4 light{i}_softShadowParams;
+            #endif
+        #endif
+
+        // directional (cascaded) shadows
+        #if LIGHT{i}TYPE == DIRECTIONAL
+            uniform mat4 light{i}_shadowMatrixPalette[4];
+            uniform vec4 light{i}_shadowCascadeDistances;
+            uniform int light{i}_shadowCascadeCount;
+            uniform float light{i}_shadowCascadeBlend;
+        #endif
+
+        #if LIGHT{i}TYPE == OMNI
+            #if defined(LIGHT{i}SHADOW_PCF)
+                uniform samplerCubeShadow light{i}_shadowMap;
+            #else
+                uniform samplerCube light{i}_shadowMap;
+            #endif
+        #else
+            #if defined(LIGHT{i}SHADOW_PCF)
+                uniform sampler2DShadow light{i}_shadowMap;
+            #else
+                uniform sampler2D light{i}_shadowMap;
+            #endif
+        #endif
+
+    #endif
+
+    // cookie
+    #if defined(LIGHT{i}COOKIE)
+        #if defined(LIGHT{i}COOKIE_CUBEMAP)
+            #if LIGHT{i}TYPE == OMNI
+                uniform samplerCube light{i}_cookie;
+                uniform float light{i}_cookieIntensity;
+                #if !defined(LIGHT{i}CASTSHADOW)
+                    uniform mat4 light{i}_shadowMatrix;
+                #endif
+            #endif
+        #else
+            #if LIGHT{i}TYPE == SPOT
+                uniform sampler2D light{i}_cookie;
+                uniform float light{i}_cookieIntensity;
+                #if !defined(LIGHT{i}CASTSHADOW)
+                    uniform mat4 light{i}_shadowMatrix;
+                #endif
+                #if defined(LIGHT{i}COOKIE_TRANSFORM)
+                    uniform vec4 light{i}_cookieMatrix;
+                    uniform vec2 light{i}_cookieOffset;
+                #endif
+            #endif
+        #endif
+    #endif
+#endif
+`;
diff --git a/src/scene/shader-lib/chunks/lit/frag/lighting/lighting.js b/src/scene/shader-lib/chunks/lit/frag/lighting/lighting.js
new file mode 100644
index 000000000..6556a1ff1
--- /dev/null
+++ b/src/scene/shader-lib/chunks/lit/frag/lighting/lighting.js
@@ -0,0 +1,43 @@
+// Core include for lighting / shadowing code.
+export default /* glsl */`
+
+// clustered lighting
+#ifdef CLUSTERED_LIGHTS
+    // include this before shadow / cookie code
+    #include "clusteredLightUtilsPS"
+    #ifdef CLUSTER_COOKIES
+        #include "clusteredLightCookiesPS"
+    #endif
+#endif
+
+#ifdef AREA_LIGHTS
+    uniform highp sampler2D areaLightsLutTex1;
+    uniform highp sampler2D areaLightsLutTex2;
+#endif
+
+#ifdef SHADOW_DIRECTIONAL
+    #include "shadowCascadesPS"
+#endif
+
+#if defined(SHADOW_KIND_PCF1)
+    #include "shadowPCF1PS"
+#endif
+
+#if defined(SHADOW_KIND_PCF3)
+    #include "shadowPCF3PS"
+#endif
+
+#if defined(SHADOW_KIND_PCF5)
+    #include "shadowPCF5PS"
+#endif
+
+#if defined(SHADOW_KIND_PCSS)
+    #include "linearizeDepthPS"
+    #include "shadowPCSSPS"
+    #include "shadowSoftPS"
+#endif
+
+#if defined(SHADOW_KIND_VSM)
+    #include "shadowEVSMPS"
+#endif
+`;
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowCascades.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowCascades.js
similarity index 100%
rename from src/scene/shader-lib/chunks/lit/frag/shadowCascades.js
rename to src/scene/shader-lib/chunks/lit/frag/lighting/shadowCascades.js
diff --git a/src/scene/shader-lib/chunks/lit/frag/lighting/shadowEVSM.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowEVSM.js
new file mode 100644
index 000000000..ac1175e8b
--- /dev/null
+++ b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowEVSM.js
@@ -0,0 +1,85 @@
+export default /* glsl */`
+
+// ------ VSM Shared ------
+
+float linstep(float a, float b, float v) {
+    return saturate((v - a) / (b - a));
+}
+
+float reduceLightBleeding(float pMax, float amount) {
+   // Remove the [0, amount] tail and linearly rescale (amount, 1].
+   return linstep(amount, 1.0, pMax);
+}
+
+float chebyshevUpperBound(vec2 moments, float mean, float minVariance, float lightBleedingReduction) {
+    // Compute variance
+    float variance = moments.y - (moments.x * moments.x);
+    variance = max(variance, minVariance);
+
+    // Compute probabilistic upper bound
+    float d = mean - moments.x;
+    float pMax = variance / (variance + (d * d));
+
+    pMax = reduceLightBleeding(pMax, lightBleedingReduction);
+
+    // One-tailed Chebyshev
+    return (mean <= moments.x ? 1.0 : pMax);
+}
+
+float calculateEVSM(vec3 moments, float Z, float vsmBias, float exponent) {
+    Z = 2.0 * Z - 1.0;
+    float warpedDepth = exp(exponent * Z);
+
+    moments.xy += vec2(warpedDepth, warpedDepth*warpedDepth) * (1.0 - moments.z);
+
+    float VSMBias = vsmBias;//0.01 * 0.25;
+    float depthScale = VSMBias * exponent * warpedDepth;
+    float minVariance1 = depthScale * depthScale;
+    return chebyshevUpperBound(moments.xy, warpedDepth, minVariance1, 0.1);
+}
+
+// ------ VSM 16 ------
+
+float VSM16(TEXTURE_ACCEPT(tex), vec2 texCoords, float resolution, float Z, float vsmBias, float exponent) {
+    vec3 moments = texture2D(tex, texCoords).xyz;
+    return calculateEVSM(moments, Z, vsmBias, exponent);
+}
+
+float getShadowVSM16(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
+    return VSM16(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent);
+}
+
+float getShadowSpotVSM16(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
+    return VSM16(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, length(lightDir) * shadowParams.w + shadowParams.z, shadowParams.y, exponent);
+}
+
+// ------ VSM 32 ------
+
+float VSM32(TEXTURE_ACCEPT(tex), vec2 texCoords, float resolution, float Z, float vsmBias, float exponent) {
+
+    #ifdef CAPS_TEXTURE_FLOAT_FILTERABLE
+        vec3 moments = texture2D(tex, texCoords).xyz;
+    #else
+        // manual bilinear filtering
+        float pixelSize = 1.0 / resolution;
+        texCoords -= vec2(pixelSize);
+        vec3 s00 = texture2D(tex, texCoords).xyz;
+        vec3 s10 = texture2D(tex, texCoords + vec2(pixelSize, 0)).xyz;
+        vec3 s01 = texture2D(tex, texCoords + vec2(0, pixelSize)).xyz;
+        vec3 s11 = texture2D(tex, texCoords + vec2(pixelSize)).xyz;
+        vec2 fr = fract(texCoords * resolution);
+        vec3 h0 = mix(s00, s10, fr.x);
+        vec3 h1 = mix(s01, s11, fr.x);
+    #endif
+
+    return calculateEVSM(moments, Z, vsmBias, exponent);
+}
+
+float getShadowVSM32(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
+    return VSM32(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent);
+}
+
+float getShadowSpotVSM32(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
+    return VSM32(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, length(lightDir) * shadowParams.w + shadowParams.z, shadowParams.y, exponent);
+}
+`;
diff --git a/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF1.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF1.js
new file mode 100644
index 000000000..2e89721ac
--- /dev/null
+++ b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF1.js
@@ -0,0 +1,22 @@
+export default /* glsl */`
+// ----- Directional/Spot Sampling -----
+
+float getShadowPCF1x1(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) {
+    return textureShadow(shadowMap, shadowCoord);
+}
+
+float getShadowSpotPCF1x1(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) {
+    return textureShadow(shadowMap, shadowCoord);
+}
+
+// ----- Omni Sampling -----
+
+#ifndef WEBGPU
+
+float getShadowPointPCF1x1(samplerCubeShadow shadowMap, vec3 shadowCoord, vec4 shadowParams, vec3 lightDir) {
+    float shadowZ = length(lightDir) * shadowParams.w + shadowParams.z;
+    return texture(shadowMap, vec4(lightDir, shadowZ));
+}
+
+#endif
+`;
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowStandard.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF3.js
similarity index 83%
rename from src/scene/shader-lib/chunks/lit/frag/shadowStandard.js
rename to src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF3.js
index 24fd188bf..2b3c6a7af 100644
--- a/src/scene/shader-lib/chunks/lit/frag/shadowStandard.js
+++ b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF3.js
@@ -48,14 +48,6 @@ float getShadowSpotPCF3x3(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 sh
     return _getShadowPCF3x3(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams.xyz);
 }
 
-float getShadowPCF1x1(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) {
-    return textureShadow(shadowMap, shadowCoord);
-}
-
-float getShadowSpotPCF1x1(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) {
-    return textureShadow(shadowMap, shadowCoord);
-}
-
 // ----- Omni Sampling -----
 
 #ifndef WEBGPU
@@ -79,11 +71,6 @@ float getShadowPointPCF3x3(samplerCubeShadow shadowMap, vec4 shadowParams, vec3
     return dot(shadows, vec4(0.25));
 }
 
-float getShadowPointPCF1x1(samplerCubeShadow shadowMap, vec3 shadowCoord, vec4 shadowParams, vec3 lightDir) {
-    float shadowZ = length(lightDir) * shadowParams.w + shadowParams.z;
-    return texture(shadowMap, vec4(lightDir, shadowZ));
-}
-
 float getShadowPointPCF3x3(samplerCubeShadow shadowMap, vec3 shadowCoord, vec4 shadowParams, vec3 lightDir) {
     return getShadowPointPCF3x3(shadowMap, shadowParams, lightDir);
 }
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowStandardGL2.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF5.js
similarity index 100%
rename from src/scene/shader-lib/chunks/lit/frag/shadowStandardGL2.js
rename to src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCF5.js
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowPCSS.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCSS.js
similarity index 98%
rename from src/scene/shader-lib/chunks/lit/frag/shadowPCSS.js
rename to src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCSS.js
index 2b93a1c4f..23b4bbe4d 100644
--- a/src/scene/shader-lib/chunks/lit/frag/shadowPCSS.js
+++ b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowPCSS.js
@@ -64,6 +64,7 @@ float PCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoords, vec4 cameraParams, vec2
     float receiverDepth = linearizeDepth(shadowCoords.z, cameraParams);
 
     vec2 samplePoints[PCSS_SAMPLE_COUNT];
+    const float PI = 3.141592653589793;
     float noise = noise( gl_FragCoord.xy ) * 2.0 * PI;
     for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) {
         float pcssPresample = pcssDiskSamples[i];
@@ -114,6 +115,7 @@ float PCSSCubeBlockerDistance(samplerCube shadowMap, vec3 lightDirNorm, vec3 sam
 float PCSSCube(samplerCube shadowMap, vec4 shadowParams, vec3 shadowCoords, vec4 cameraParams, float shadowSearchArea, vec3 lightDir) {
     
     vec3 samplePoints[PCSS_SAMPLE_COUNT];
+    const float PI = 3.141592653589793;
     float noise = noise( gl_FragCoord.xy ) * 2.0 * PI;
     for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) {
         float r = pcssSphereSamples[i];
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowSampleCoord.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowSampleCoord.js
similarity index 100%
rename from src/scene/shader-lib/chunks/lit/frag/shadowSampleCoord.js
rename to src/scene/shader-lib/chunks/lit/frag/lighting/shadowSampleCoord.js
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowSoft.js b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowSoft.js
similarity index 99%
rename from src/scene/shader-lib/chunks/lit/frag/shadowSoft.js
rename to src/scene/shader-lib/chunks/lit/frag/lighting/shadowSoft.js
index 2997bb213..01781b30e 100644
--- a/src/scene/shader-lib/chunks/lit/frag/shadowSoft.js
+++ b/src/scene/shader-lib/chunks/lit/frag/lighting/shadowSoft.js
@@ -5,6 +5,7 @@ export default /* glsl */`
  */
 
 highp float fractSinRand( const in vec2 uv ) {
+    const float PI = 3.141592653589793;
     const highp float a = 12.9898, b = 78.233, c = 43758.5453;
     highp float dt = dot(uv.xy, vec2(a, b)), sn = mod(dt, PI);
     return fract(sin(sn) * c);
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowEVSM.js b/src/scene/shader-lib/chunks/lit/frag/shadowEVSM.js
deleted file mode 100644
index dbcfab45a..000000000
--- a/src/scene/shader-lib/chunks/lit/frag/shadowEVSM.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default /* glsl */`
-float VSM$(TEXTURE_ACCEPT(tex), vec2 texCoords, float resolution, float Z, float vsmBias, float exponent) {
-    vec3 moments = texture2D(tex, texCoords).xyz;
-    return calculateEVSM(moments, Z, vsmBias, exponent);
-}
-
-float getShadowVSM$(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
-    return VSM$(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent);
-}
-
-float getShadowSpotVSM$(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
-    return VSM$(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, length(lightDir) * shadowParams.w + shadowParams.z, shadowParams.y, exponent);
-}
-`;
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowEVSMn.js b/src/scene/shader-lib/chunks/lit/frag/shadowEVSMn.js
deleted file mode 100644
index d67c20a36..000000000
--- a/src/scene/shader-lib/chunks/lit/frag/shadowEVSMn.js
+++ /dev/null
@@ -1,23 +0,0 @@
-export default /* glsl */`
-float VSM$(TEXTURE_ACCEPT(tex), vec2 texCoords, float resolution, float Z, float vsmBias, float exponent) {
-    float pixelSize = 1.0 / resolution;
-    texCoords -= vec2(pixelSize);
-    vec3 s00 = texture2D(tex, texCoords).xyz;
-    vec3 s10 = texture2D(tex, texCoords + vec2(pixelSize, 0)).xyz;
-    vec3 s01 = texture2D(tex, texCoords + vec2(0, pixelSize)).xyz;
-    vec3 s11 = texture2D(tex, texCoords + vec2(pixelSize)).xyz;
-    vec2 fr = fract(texCoords * resolution);
-    vec3 h0 = mix(s00, s10, fr.x);
-    vec3 h1 = mix(s01, s11, fr.x);
-    vec3 moments = mix(h0, h1, fr.y);
-    return calculateEVSM(moments, Z, vsmBias, exponent);
-}
-
-float getShadowVSM$(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
-    return VSM$(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent);
-}
-
-float getShadowSpotVSM$(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) {
-    return VSM$(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, length(lightDir) * shadowParams.w + shadowParams.z, shadowParams.y, exponent);
-}
-`;
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowVSM_common.js b/src/scene/shader-lib/chunks/lit/frag/shadowVSM_common.js
deleted file mode 100644
index d168f108f..000000000
--- a/src/scene/shader-lib/chunks/lit/frag/shadowVSM_common.js
+++ /dev/null
@@ -1,37 +0,0 @@
-export default /* glsl */`
-float linstep(float a, float b, float v) {
-    return saturate((v - a) / (b - a));
-}
-
-float reduceLightBleeding(float pMax, float amount) {
-   // Remove the [0, amount] tail and linearly rescale (amount, 1].
-   return linstep(amount, 1.0, pMax);
-}
-
-float chebyshevUpperBound(vec2 moments, float mean, float minVariance, float lightBleedingReduction) {
-    // Compute variance
-    float variance = moments.y - (moments.x * moments.x);
-    variance = max(variance, minVariance);
-
-    // Compute probabilistic upper bound
-    float d = mean - moments.x;
-    float pMax = variance / (variance + (d * d));
-
-    pMax = reduceLightBleeding(pMax, lightBleedingReduction);
-
-    // One-tailed Chebyshev
-    return (mean <= moments.x ? 1.0 : pMax);
-}
-
-float calculateEVSM(vec3 moments, float Z, float vsmBias, float exponent) {
-    Z = 2.0 * Z - 1.0;
-    float warpedDepth = exp(exponent * Z);
-
-    moments.xy += vec2(warpedDepth, warpedDepth*warpedDepth) * (1.0 - moments.z);
-
-    float VSMBias = vsmBias;//0.01 * 0.25;
-    float depthScale = VSMBias * exponent * warpedDepth;
-    float minVariance1 = depthScale * depthScale;
-    return chebyshevUpperBound(moments.xy, warpedDepth, minVariance1, 0.1);
-}
-`;
diff --git a/src/scene/shader-lib/chunks/skybox/frag/skybox.js b/src/scene/shader-lib/chunks/skybox/frag/skybox.js
index 167f92bc6..a84bebcd5 100644
--- a/src/scene/shader-lib/chunks/skybox/frag/skybox.js
+++ b/src/scene/shader-lib/chunks/skybox/frag/skybox.js
@@ -43,14 +43,14 @@ export default /* glsl */`
             #endif
 
             dir.x *= -1.0;
-            vec3 linear = __INJECT_SKYBOX_DECODE_FNC(textureCube(texture_cubeMap, dir));
+            vec3 linear = _INJECT_SKYBOX_DECODE_FNC(textureCube(texture_cubeMap, dir));
 
         #else // env-atlas
 
             vec3 dir = vViewDir * vec3(-1.0, 1.0, 1.0);
             vec2 uv = toSphericalUv(normalize(dir));
 
-            vec3 linear = __INJECT_SKYBOX_DECODE_FNC(texture2D(texture_envAtlas, mapRoughnessUv(uv, mipLevel)));
+            vec3 linear = _INJECT_SKYBOX_DECODE_FNC(texture2D(texture_envAtlas, mapRoughnessUv(uv, mipLevel)));
 
         #endif
 
diff --git a/src/scene/shader-lib/programs/lit-shader.js b/src/scene/shader-lib/programs/lit-shader.js
index 2f87a4191..0fd320d56 100644
--- a/src/scene/shader-lib/programs/lit-shader.js
+++ b/src/scene/shader-lib/programs/lit-shader.js
@@ -13,7 +13,9 @@ import {
     SHADOW_PCF1_32F, SHADOW_PCF3_32F, SHADOW_PCF5_32F, SHADOW_VSM_16F, SHADOW_VSM_32F, SHADOW_PCSS_32F,
     SPECOCC_AO, SPECOCC_GLOSSDEPENDENT,
     SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, shadowTypeInfo, SHADER_PREPASS,
-    SHADOW_PCF1_16F, SHADOW_PCF5_16F, SHADOW_PCF3_16F
+    SHADOW_PCF1_16F, SHADOW_PCF5_16F, SHADOW_PCF3_16F,
+    lightTypeNames,
+    lightShapeNames
 } from '../../constants.js';
 import { shaderChunks } from '../chunks/chunks.js';
 import { ChunkUtils } from '../chunk-utils.js';
@@ -127,6 +129,9 @@ class LitShader {
         // defines set by the shader generation
         this.defines = [];
 
+        this.vDefines = new Map();
+        this.fDefines = new Map();
+
         // generated by fshader
         this.fshader = null;
     }
@@ -455,6 +460,73 @@ class LitShader {
         return code;
     }
 
+    /**
+     * Generate defines for lighting environment as well as individual lights.
+     *
+     * @param {boolean} hasAreaLights - Whether any of the lights are area lights.
+     * @param {boolean} clusteredLightingEnabled - Whether clustered lighting is enabled.
+     */
+    _setupLightingDefines(hasAreaLights, clusteredLightingEnabled) {
+
+        const fDefines = this.fDefines;
+        const options = this.options;
+
+        this.fDefines.set('LIGHT_COUNT', options.lights.length);
+        if (hasAreaLights) fDefines.set('AREA_LIGHTS', true);
+
+        // clustered lights defines
+        if (clusteredLightingEnabled && this.lighting) {
+            fDefines.set('CLUSTERED_LIGHTS', true);
+            if (options.clusteredLightingCookiesEnabled) fDefines.set('CLUSTER_COOKIES', true);
+            if (options.clusteredLightingAreaLightsEnabled) fDefines.set('CLUSTER_AREALIGHTS', true);
+            if (options.lightMaskDynamic) fDefines.set('CLUSTER_MESH_DYNAMIC_LIGHTS', true);
+
+            // shadows
+            if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
+                const clusteredShadowInfo = shadowTypeInfo.get(options.clusteredLightingShadowType);
+                fDefines.set('CLUSTER_SHADOWS', true);
+                fDefines.set(`SHADOW_KIND_${clusteredShadowInfo.kind}`, true);
+                fDefines.set(`CLUSTER_SHADOW_TYPE_${clusteredShadowInfo.kind}`, true);
+            }
+        }
+
+        // generate defines for all non-clustered lights
+        for (let i = 0; i < options.lights.length; i++) {
+            const light = options.lights[i];
+            const lightType = light._type;
+
+            // when clustered lighting is enabled, skip non-directional lights
+            if (clusteredLightingEnabled && lightType !== LIGHTTYPE_DIRECTIONAL) {
+                continue;
+            }
+
+            const lightShape = (hasAreaLights && light._shape) ? light._shape : LIGHTSHAPE_PUNCTUAL;
+            const shadowType = light._shadowType;
+            const castShadow = light.castShadows && !options.noShadow;
+            const shadowInfo = shadowTypeInfo.get(shadowType);
+
+            // per light defines
+            fDefines.set(`LIGHT${i}`, true);
+            fDefines.set(`LIGHT${i}TYPE`, `${lightTypeNames[lightType]}`);
+            fDefines.set(`LIGHT${i}SHADOWTYPE`, `${shadowInfo.name}`);
+            fDefines.set(`LIGHT${i}SHAPE`, `${lightShapeNames[lightShape]}`);
+            if (light._cookie) fDefines.set(`LIGHT${i}COOKIE`, true);
+            if (light._cookie?._cubemap) fDefines.set(`LIGHT${i}COOKIE_CUBEMAP`, true);
+            if (light._cookieTransform) fDefines.set(`LIGHT${i}COOKIE_TRANSFORM`, true);
+
+            if (castShadow) {
+                fDefines.set(`LIGHT${i}CASTSHADOW`, true);
+                if (shadowInfo.pcf) fDefines.set(`LIGHT${i}SHADOW_PCF`, true);
+            }
+
+            // global lighting defines
+            if (castShadow) {
+                fDefines.set(`SHADOW_KIND_${shadowInfo.kind}`, true);
+                if (lightType === LIGHTTYPE_DIRECTIONAL) fDefines.set('SHADOW_DIRECTIONAL', true);
+            }
+        }
+    }
+
     // the big one
     _fsGetLitPassCode() {
         const device = this.device;
@@ -495,11 +567,6 @@ class LitShader {
         }
 
         // FRAGMENT SHADER INPUTS: UNIFORMS
-        const shadowTypeUsed = [];
-        let numShadowLights = 0;
-        let shadowedDirectionalLightUsed = false;
-        let useVsm = false;
-        let usePcss = false;
 
         let hasAreaLights = options.lights.some((light) => {
             return light._shape && light._shape !== LIGHTSHAPE_PUNCTUAL;
@@ -511,101 +578,21 @@ class LitShader {
             hasAreaLights = true;
         }
 
-        if (hasAreaLights || options.clusteredLightingEnabled) {
-            decl.append('#define AREA_LIGHTS');
-            decl.append('uniform highp sampler2D areaLightsLutTex1;');
-            decl.append('uniform highp sampler2D areaLightsLutTex2;');
-        }
-
-        for (let i = 0; i < options.lights.length; i++) {
-            const light = options.lights[i];
-            const lightType = light._type;
-
-            // skip uniform generation for local lights if clustered lighting is enabled
-            if (options.clusteredLightingEnabled && lightType !== LIGHTTYPE_DIRECTIONAL) {
-                continue;
-            }
-
-            const lightShape = (hasAreaLights && light._shape) ? light._shape : LIGHTSHAPE_PUNCTUAL;
-
-            decl.append(`uniform vec3 light${i}_color;`);
+        // generate lighting defines
+        this._setupLightingDefines(hasAreaLights, options.clusteredLightingEnabled);
 
-            if (light._shadowType === SHADOW_PCSS_32F && light.castShadows && !options.noShadow) {
-                decl.append(`uniform float light${i}_shadowSearchArea;`);
-                decl.append(`uniform vec4 light${i}_cameraParams;`);
-
-                if (lightType === LIGHTTYPE_DIRECTIONAL) {
-                    decl.append(`uniform vec4 light${i}_softShadowParams;`);
-                }
-            }
+        // lighting and shadowing code
+        decl.append(`
+            // LOOP - uniform declarations for all non-clustered lights
+            #include "lightDeclarationPS, LIGHT_COUNT"
 
-            if (lightType === LIGHTTYPE_DIRECTIONAL) {
-                decl.append(`uniform vec3 light${i}_direction;`);
-            } else {
-                decl.append(`uniform vec3 light${i}_position;`);
-                decl.append(`uniform float light${i}_radius;`);
-                if (lightType === LIGHTTYPE_SPOT) {
-                    decl.append(`uniform vec3 light${i}_direction;`);
-                    decl.append(`uniform float light${i}_innerConeAngle;`);
-                    decl.append(`uniform float light${i}_outerConeAngle;`);
-                }
-            }
-            if (lightShape !== LIGHTSHAPE_PUNCTUAL) {
-                if (lightType === LIGHTTYPE_DIRECTIONAL) {
-                    decl.append(`uniform vec3 light${i}_position;`);
-                }
-                decl.append(`uniform vec3 light${i}_halfWidth;`);
-                decl.append(`uniform vec3 light${i}_halfHeight;`);
-            }
-            if (light.castShadows && !options.noShadow) {
-                decl.append(`uniform mat4 light${i}_shadowMatrix;`);
-                decl.append(`uniform float light${i}_shadowIntensity;`);
+            #if defined(CLUSTERED_LIGHTS)
+                ${LightsBuffer.getShaderDefines()}
+            #endif
 
-                // directional (cascaded) shadows
-                if (lightType === LIGHTTYPE_DIRECTIONAL) {
-                    decl.append(`uniform mat4 light${i}_shadowMatrixPalette[4];`);
-                    decl.append(`uniform vec4 light${i}_shadowCascadeDistances;`);
-                    decl.append(`uniform int light${i}_shadowCascadeCount;`);
-                    decl.append(`uniform float light${i}_shadowCascadeBlend;`);
-                }
-                decl.append(`uniform vec4 light${i}_shadowParams;`); // Width, height, bias, radius
-                if (lightType === LIGHTTYPE_DIRECTIONAL) {
-                    shadowedDirectionalLightUsed = true;
-                }
-                if (lightType === LIGHTTYPE_OMNI) {
-                    decl.append(`uniform ${light._isPcf ? 'samplerCubeShadow' : 'samplerCube'} light${i}_shadowMap;`);
-                } else {
-                    decl.append(`uniform ${light._isPcf ? 'sampler2DShadow' : 'sampler2D'} light${i}_shadowMap;`);
-                }
-                numShadowLights++;
-                shadowTypeUsed[light._shadowType] = true;
-                if (light._isVsm) useVsm = true;
-                if (light._shadowType === SHADOW_PCSS_32F) usePcss = true;
-            }
-            if (light._cookie) {
-                if (light._cookie._cubemap) {
-                    if (lightType === LIGHTTYPE_OMNI) {
-                        decl.append(`uniform samplerCube light${i}_cookie;`);
-                        decl.append(`uniform float light${i}_cookieIntensity;`);
-                        if (!light.castShadows || options.noShadow) {
-                            decl.append(`uniform mat4 light${i}_shadowMatrix;`);
-                        }
-                    }
-                } else {
-                    if (lightType === LIGHTTYPE_SPOT) {
-                        decl.append(`uniform sampler2D light${i}_cookie;`);
-                        decl.append(`uniform float light${i}_cookieIntensity;`);
-                        if (!light.castShadows || options.noShadow) {
-                            decl.append(`uniform mat4 light${i}_shadowMatrix;`);
-                        }
-                        if (light._cookieTransform) {
-                            decl.append(`uniform vec4 light${i}_cookieMatrix;`);
-                            decl.append(`uniform vec2 light${i}_cookieOffset;`);
-                        }
-                    }
-                }
-            }
-        }
+            // required lighting functionality
+            #include "lightingPS"
+        `);
 
         // TBN
         const hasTBN = this.needsNormal && (options.useNormals || options.useClearCoatNormals || (options.enableGGXSpecular && !options.useHeights));
@@ -711,49 +698,6 @@ class LitShader {
             func.append(chunks.lightSheenPS);
         }
 
-        // clustered lighting
-        if (options.clusteredLightingEnabled) {
-
-            // include this before shadow / cookie code
-            func.append(chunks.clusteredLightUtilsPS);
-            if (options.clusteredLightingCookiesEnabled) {
-                func.append(chunks.clusteredLightCookiesPS);
-            }
-
-            // include shadow chunks clustered lights support
-            if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
-                shadowTypeUsed[SHADOW_PCF3_32F] = true;
-                shadowTypeUsed[SHADOW_PCF5_32F] = true;
-                shadowTypeUsed[SHADOW_PCSS_32F] = true;
-            }
-        }
-
-        if (numShadowLights > 0 || options.clusteredLightingEnabled) {
-            if (shadowedDirectionalLightUsed) {
-                func.append(chunks.shadowCascadesPS);
-            }
-            if (shadowTypeUsed[SHADOW_PCF1_32F] || shadowTypeUsed[SHADOW_PCF3_32F] || shadowTypeUsed[SHADOW_PCF1_16F] || shadowTypeUsed[SHADOW_PCF3_16F]) {
-                func.append(chunks.shadowStandardPS);
-            }
-            if (shadowTypeUsed[SHADOW_PCF5_32F] || shadowTypeUsed[SHADOW_PCF5_16F]) {
-                func.append(chunks.shadowStandardGL2PS);
-            }
-            if (useVsm) {
-                func.append(chunks.shadowVSM_commonPS);
-                if (shadowTypeUsed[SHADOW_VSM_16F]) {
-                    func.append(chunks.shadowEVSMPS.replace(/\$/g, '16'));
-                }
-                if (shadowTypeUsed[SHADOW_VSM_32F]) {
-                    func.append(device.extTextureFloatLinear ? chunks.shadowEVSMPS.replace(/\$/g, '32') : chunks.shadowEVSMnPS.replace(/\$/g, '32'));
-                }
-            }
-            if (usePcss) {
-                func.append(chunks.linearizeDepthPS);
-                func.append(chunks.shadowPCSSPS);
-                func.append(chunks.shadowSoftPS);
-            }
-        }
-
         if (options.enableGGXSpecular) func.append('uniform float material_anisotropy;');
 
         if (this.lighting) {
@@ -831,34 +775,16 @@ class LitShader {
             hasPointLights = true;
             usesLinearFalloff = true;
             usesCookie = true;
+        }
 
-            func.append(chunks.floatUnpackingPS);
-
-            if (options.lightMaskDynamic) {
-                decl.append('#define CLUSTER_MESH_DYNAMIC_LIGHTS');
-            }
-
-            if (options.clusteredLightingCookiesEnabled) {
-                decl.append('#define CLUSTER_COOKIES');
-            }
-            if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
-                const shadowTypeName = shadowTypeInfo.get(options.clusteredLightingShadowType)?.name;
-                Debug.assert(shadowTypeName);
-                const clusteredSampleType = shadowTypeName.substring(0, 4);     // PCF1 from PCF1_FLOAT16
-                decl.append('#define CLUSTER_SHADOWS');
-                decl.append(`#define CLUSTER_SHADOW_TYPE_${clusteredSampleType}`);
-            }
-
-            if (options.clusteredLightingAreaLightsEnabled) {
-                decl.append('#define CLUSTER_AREALIGHTS');
-            }
-
-            decl.append(LightsBuffer.getShaderDefines());
+        // TODO - move this section to lighting when possible, for now it has dependencies on the above code
+        if (options.clusteredLightingEnabled && this.lighting) {
 
             if (options.clusteredLightingShadowsEnabled && !options.noShadow) {
                 func.append(chunks.clusteredLightShadowsPS);
             }
 
+            func.append(chunks.floatUnpackingPS);
             func.append(chunks.clusteredLightPS);
         }
 
@@ -1164,14 +1090,14 @@ class LitShader {
                         let shadowMatrix = `light${i}_shadowMatrix`;
                         if (lightType === LIGHTTYPE_DIRECTIONAL && light.numCascades > 1) {
                             // select shadow cascade matrix
-                            backend.append(`int cascadeIndex = getShadowCascadeIndex(light${i}_shadowCascadeDistances, light${i}_shadowCascadeCount);`);
+                            backend.append(`int cascadeIndex${i} = getShadowCascadeIndex(light${i}_shadowCascadeDistances, light${i}_shadowCascadeCount);`);
 
                             if (light.cascadeBlend > 0) {
-                                backend.append(`cascadeIndex = ditherShadowCascadeIndex(cascadeIndex, light${i}_shadowCascadeDistances, light${i}_shadowCascadeCount, light${i}_shadowCascadeBlend);`);
+                                backend.append(`cascadeIndex${i} = ditherShadowCascadeIndex(cascadeIndex${i}, light${i}_shadowCascadeDistances, light${i}_shadowCascadeCount, light${i}_shadowCascadeBlend);`);
                             }
 
-                            backend.append(`mat4 cascadeShadowMat = light${i}_shadowMatrixPalette[cascadeIndex];`);
-                            shadowMatrix = 'cascadeShadowMat';
+                            backend.append(`mat4 cascadeShadowMat${i} = light${i}_shadowMatrixPalette[cascadeIndex${i}];`);
+                            shadowMatrix = `cascadeShadowMat${i}`;
                         }
 
                         backend.append(`    dShadowCoord = getShadowSampleCoord${i}(${shadowMatrix}, light${i}_shadowParams, vPositionW, dLightPosW, dLightDirW, dLightDirNormW, dVertexNormalW);`);
diff --git a/src/scene/shader-lib/programs/lit.js b/src/scene/shader-lib/programs/lit.js
index 5a6fdd79e..d015b2ba1 100644
--- a/src/scene/shader-lib/programs/lit.js
+++ b/src/scene/shader-lib/programs/lit.js
@@ -60,15 +60,19 @@ class ShaderGeneratorLit extends ShaderGenerator {
             ...options.litOptions.chunks
         }));
 
-        const defines = new Map(options.defines);
+        const vDefines = litShader.vDefines;
+        options.defines.forEach((value, key) => vDefines.set(key, value));
+
+        const fDefines = litShader.fDefines;
+        options.defines.forEach((value, key) => fDefines.set(key, value));
 
         definitionOptions.attributes = litShader.attributes;
         definitionOptions.vertexCode = litShader.vshader;
         definitionOptions.vertexIncludes = includes;
-        definitionOptions.vertexDefines = defines;
+        definitionOptions.vertexDefines = vDefines;
         definitionOptions.fragmentCode = litShader.fshader;
         definitionOptions.fragmentIncludes = includes;
-        definitionOptions.fragmentDefines = defines;
+        definitionOptions.fragmentDefines = fDefines;
 
         return ShaderUtils.createDefinition(device, definitionOptions);
     }
diff --git a/src/scene/shader-lib/programs/standard.js b/src/scene/shader-lib/programs/standard.js
index 7827d35e0..210047a0a 100644
--- a/src/scene/shader-lib/programs/standard.js
+++ b/src/scene/shader-lib/programs/standard.js
@@ -532,7 +532,11 @@ class ShaderGeneratorStandard extends ShaderGenerator {
             ...options.litOptions.chunks
         }));
 
-        const defines = new Map(options.defines);
+        const vDefines = litShader.vDefines;
+        options.defines.forEach((value, key) => vDefines.set(key, value));
+
+        const fDefines = litShader.fDefines;
+        options.defines.forEach((value, key) => fDefines.set(key, value));
 
         const definition = ShaderUtils.createDefinition(device, {
             name: 'StandardShader',
@@ -541,8 +545,8 @@ class ShaderGeneratorStandard extends ShaderGenerator {
             fragmentCode: litShader.fshader,
             vertexIncludes: includes,
             fragmentIncludes: includes,
-            fragmentDefines: defines,
-            vertexDefines: defines
+            fragmentDefines: fDefines,
+            vertexDefines: vDefines
         });
 
         if (litShader.shaderPassInfo.isForward) {
diff --git a/src/scene/skybox/sky-mesh.js b/src/scene/skybox/sky-mesh.js
index e47824569..f7cc44c5f 100644
--- a/src/scene/skybox/sky-mesh.js
+++ b/src/scene/skybox/sky-mesh.js
@@ -46,7 +46,7 @@ class SkyMesh {
         });
 
         // defines
-        material.setDefine('__INJECT_SKYBOX_DECODE_FNC', ChunkUtils.decodeFunc(texture.encoding));
+        material.setDefine('_INJECT_SKYBOX_DECODE_FNC', ChunkUtils.decodeFunc(texture.encoding));
         if (type !== SKYTYPE_INFINITE) material.setDefine('SKYMESH', '');
         if (texture.cubemap) material.setDefine('SKY_CUBEMAP', '');
 


Test patch:
diff --git a/test/core/preprocessor.test.mjs b/test/core/preprocessor.test.mjs
index 2d0dc994d..a70232948 100644
--- a/test/core/preprocessor.test.mjs
+++ b/test/core/preprocessor.test.mjs
@@ -18,8 +18,8 @@ describe('Preprocessor', function () {
     const srcData = `
         
         #define LOOP_COUNT 3
-        #define __INJECT_COUNT 2
-        #define __INJECT_STRING hello
+        #define _INJECT_COUNT 2
+        #define _INJECT_STRING hello
         #define FEATURE1
         #define FEATURE2
         #define AND1
@@ -126,8 +126,8 @@ describe('Preprocessor', function () {
             CMP5
         #endif
 
-        TESTINJECTION __INJECT_COUNT
-        INJECTSTRING __INJECT_STRING(x)
+        TESTINJECTION _INJECT_COUNT
+        INJECTSTRING _INJECT_STRING(x)
     `;
 
     it('returns false for MORPH_A', function () {