Repository: playcanvas/engine
Pull request #5421 (2023-06-21 11:09:16)

Change behaviour of lightSize
This PR changes the way lightSize modifies the softness of PCSS shadows. Instead of using lightSize directly, it's now a factor of how many pixels per meter to fetch, making the parameter more intuitive to use.

Also threw in a bunch of small optimisations. 


Patch:
diff --git a/examples/src/examples/graphics/contact-hardening-shadows.tsx b/examples/src/examples/graphics/contact-hardening-shadows.tsx
index 481bbe21c..4c864b3a5 100644
--- a/examples/src/examples/graphics/contact-hardening-shadows.tsx
+++ b/examples/src/examples/graphics/contact-hardening-shadows.tsx
@@ -197,7 +197,7 @@ class ContactHardeningShadowsExample {
                         range: 150,
                         shadowResolution: 2048,
                         shadowDistance: 100,
-                        lightSize: data.get('script.area.size'),
+                        penumbraSize: data.get('script.area.size'),
                         shadowType: data.get('script.area.shadowType'),
                         intensity: data.get('script.area.intensity'),
                         falloffMode: pc.LIGHTFALLOFF_INVERSESQUARED,
@@ -233,7 +233,7 @@ class ContactHardeningShadowsExample {
                         color: new pc.Color(1, 1, 1),
                         castShadows: true,
                         numCascades: 1,
-                        lightSize: data.get('script.directional.size'),
+                        penumbraSize: data.get('script.directional.size'),
                         shadowType: data.get('script.directional.shadowType'),
                         intensity: data.get('script.directional.intensity'),
                         shadowBias: 0.5,
@@ -249,7 +249,7 @@ class ContactHardeningShadowsExample {
                         type: "omni",
                         color: new pc.Color(1, 0.25, 0.25),
                         range: 25,
-                        lightSize: data.get('script.point.size'),
+                        penumbraSize: data.get('script.point.size'),
                         shadowType: data.get('script.point.shadowType'),
                         intensity: data.get('script.point.intensity'),
                         castShadows: true,
@@ -308,7 +308,7 @@ class ContactHardeningShadowsExample {
                                 brightMaterial.update();
                                 break;
                             case 'script.area.size':
-                                areaLight.light.lightSize = value;
+                                areaLight.light.penumbraSize = value;
                                 break;
                             case 'script.area.shadowType':
                                 areaLight.light.shadowType = parseInt(value);
@@ -320,7 +320,7 @@ class ContactHardeningShadowsExample {
                                 directionalLight.light.intensity = value;
                                 break;
                             case 'script.directional.size':
-                                directionalLight.light.lightSize = value;
+                                directionalLight.light.penumbraSize = value;
                                 break;
                             case 'script.directional.shadowType':
                                 directionalLight.light.shadowType = parseInt(value);
@@ -332,7 +332,7 @@ class ContactHardeningShadowsExample {
                                 lightOmni.light.intensity = value;
                                 break;
                             case 'script.point.size':
-                                lightOmni.light.lightSize = value;
+                                lightOmni.light.penumbraSize = value;
                                 break;
                             case 'script.point.shadowType':
                                 lightOmni.light.shadowType = parseInt(value);
diff --git a/src/framework/anim/evaluator/anim-clip.js b/src/framework/anim/evaluator/anim-clip.js
index a58e7db80..5edba2d97 100644
--- a/src/framework/anim/evaluator/anim-clip.js
+++ b/src/framework/anim/evaluator/anim-clip.js
@@ -9,6 +9,12 @@ import { AnimSnapshot } from './anim-snapshot.js';
  * @ignore
  */
 class AnimClip {
+    static eventFrame = {
+        start: 0,
+        end: 0,
+        residual: 0
+    };
+
     /**
      * Create a new animation clip.
      *
@@ -65,7 +71,11 @@ class AnimClip {
     }
 
     set speed(speed) {
+        const signChanged = Math.sign(speed) !== Math.sign(this._speed);
         this._speed = speed;
+        if (signChanged) {
+            this.alignCursorToCurrentTime();
+        }
     }
 
     get speed() {
@@ -104,35 +114,100 @@ class AnimClip {
         return this._eventCursor;
     }
 
-    alignCursorToCurrentTime() {
-        this._eventCursor = 0;
-        // move the event cursor to the event that should fire after the current time
-        while (this._track.events[this._eventCursor] && this._track.events[this._eventCursor].time < this.time) {
-            this._eventCursor++;
+    get eventCursorEnd() {
+        return this.isReverse ? 0 : this._track.events.length - 1;
+    }
+
+    get nextEvent() {
+        return this._track.events[this._eventCursor];
+    }
+
+    get isReverse() {
+        return this._speed < 0;
+    }
+
+    nextEventAheadOfTime(time) {
+        if (!this.nextEvent) return false;
+        return this.isReverse ? this.nextEvent.time <= time : this.nextEvent.time >= time;
+    }
+
+    nextEventBehindTime(time) {
+        if (!this.nextEvent) return false;
+        if (time === this.track.duration) {
+            return this.isReverse ? this.nextEvent.time >= time : this.nextEvent.time <= time;
         }
+        return this.isReverse ? this.nextEvent.time > time : this.nextEvent.time < time;
     }
 
-    activeEventsForFrame(frameStartTime, frameEndTime) {
-        if (frameStartTime === 0) {
-            this.eventCursor = 0;
+    resetEventCursor() {
+        this._eventCursor = this.isReverse ? this._track.events.length - 1 : 0;
+    }
+
+    moveEventCursor() {
+        this._eventCursor += (this.isReverse ? -1 : 1);
+        if (this._eventCursor >= this.track.events.length) {
+            this._eventCursor = 0;
+        } else if (this._eventCursor < 0) {
+            this._eventCursor = this.track.events.length - 1;
         }
-        let clippedFrameDuration;
+    }
+
+    clipFrameTime(frameEndTime) {
+        const eventFrame = AnimClip.eventFrame;
+        eventFrame.start = 0;
+        eventFrame.end = frameEndTime;
+        eventFrame.residual = 0;
+
         // if this frame overlaps with the end of the track, we should clip off the end of the frame time then check that clipped time later
-        if (frameEndTime > this.track.duration) {
-            clippedFrameDuration = frameEndTime - this.track.duration;
-            frameEndTime = this.track.duration;
+        if (this.isReverse) {
+            if (frameEndTime < 0) {
+                eventFrame.start = this.track.duration;
+                eventFrame.end = 0;
+                eventFrame.residual = frameEndTime + this.track.duration;
+            }
+        } else {
+            if (frameEndTime > this.track.duration) {
+                eventFrame.start = 0;
+                eventFrame.end = this.track.duration;
+                eventFrame.residual = frameEndTime - this.track.duration;
+            }
         }
+    }
 
-        // check whether the next event occurs during the current frame. If the frame end time is at the end of the track then test that inclusively too
-        while (this.track.events[this.eventCursor] && this.track.events[this.eventCursor].time >= frameStartTime && (frameEndTime === this.track.duration ? this.track.events[this.eventCursor].time <= frameEndTime : this.track.events[this.eventCursor].time < frameEndTime)) {
-            const event = this.track.events[this.eventCursor];
-            this._eventHandler.fire(event.name, { track: this.track, ...event });
-            this.eventCursor++;
+    alignCursorToCurrentTime() {
+        this.resetEventCursor();
+        while (this.nextEventBehindTime(this._time) && this._eventCursor !== this.eventCursorEnd) {
+            this.moveEventCursor();
         }
+    }
 
-        // if we had to clip the current frame, then we should check the start of the track for events during that clipped duration
-        if (Number.isFinite(clippedFrameDuration)) {
-            this.activeEventsForFrame(0, clippedFrameDuration);
+    fireNextEvent() {
+        this._eventHandler.fire(this.nextEvent.name, { track: this.track, ...this.nextEvent });
+        this.moveEventCursor();
+    }
+
+    fireNextEventInFrame(frameStartTime, frameEndTime) {
+        if (this.nextEventAheadOfTime(frameStartTime) && this.nextEventBehindTime(frameEndTime)) {
+            this.fireNextEvent();
+            return true;
+        }
+        return false;
+    }
+
+    activeEventsForFrame(frameStartTime, frameEndTime) {
+        // get frame start and end times clipped to the track duration with the residual duration stored
+        const eventFrame = AnimClip.eventFrame;
+        this.clipFrameTime(frameEndTime);
+        // fire all events that should fire during this clipped frame
+        const initialCursor = this.eventCursor;
+        while (this.fireNextEventInFrame(frameStartTime, eventFrame.end)) {
+            if (initialCursor === this.eventCursor) {
+                break;
+            }
+        }
+        // recurse the process until the residual duration is 0
+        if (this.loop && Math.abs(eventFrame.residual) > 0) {
+            this.activeEventsForFrame(eventFrame.start, eventFrame.residual);
         }
     }
 
diff --git a/src/framework/components/light/component.js b/src/framework/components/light/component.js
index 52b1f9cff..143af18d5 100644
--- a/src/framework/components/light/component.js
+++ b/src/framework/components/light/component.js
@@ -89,9 +89,9 @@ const _lightPropsDefault = [];
  * angle is specified in degrees. Affects spot lights only. Defaults to 40.
  * @property {number} outerConeAngle The angle at which the spotlight cone has faded to nothing.
  * The angle is specified in degrees. Affects spot lights only. Defaults to 45.
- * @property {number} lightSize Size of the light used for contact hardening shadows. For area lights
- * acts as a size multiplier with the area light dimensions. For punctual and directional lights
- * acts as the actual size of the light. Defaults to 1.0.
+ * @property {number} penumbraSize Size of penumbra for contact hardening shadows. For area lights
+ * acts as a multiplier with the dimensions of the area light. For punctual and directional lights
+ * it's the area size of the light. Defaults to 1.0.
  * @property {number} falloffMode Controls the rate at which a light attenuates from its position.
  * Can be:
  *
@@ -337,12 +337,12 @@ class LightComponent extends Component {
         return this.light.shadowUpdateOverrides;
     }
 
-    set lightSize(value) {
-        this.light.lightSize = value;
+    set penumbraSize(value) {
+        this.light.penumbraSize = value;
     }
 
-    get lightSize() {
-        return this.light.lightSize;
+    get penumbraSize() {
+        return this.light.penumbraSize;
     }
 }
 
@@ -567,7 +567,7 @@ function _defineProps() {
         }
     });
 
-    _lightProps.push("lightSize");
+    _lightProps.push("penumbraSize");
     _lightPropsDefault.push(1);
 }
 
diff --git a/src/scene/light.js b/src/scene/light.js
index 3af5247be..6e79d0559 100644
--- a/src/scene/light.js
+++ b/src/scene/light.js
@@ -187,7 +187,7 @@ class Light {
         this._normalOffsetBias = 0.0;
         this.shadowUpdateMode = SHADOWUPDATE_REALTIME;
         this.shadowUpdateOverrides = null;
-        this._lightSize = 1.0;
+        this._penumbraSize = 1.0;
         this._isVsm = false;
         this._isPcf = true;
 
@@ -457,12 +457,12 @@ class Light {
         return this._outerConeAngle;
     }
 
-    set lightSize(value) {
-        this._lightSize = value;
+    set penumbraSize(value) {
+        this._penumbraSize = value;
     }
 
-    get lightSize() {
-        return this._lightSize;
+    get penumbraSize() {
+        return this._penumbraSize;
     }
 
     _updateOuterAngle(angle) {
@@ -675,7 +675,7 @@ class Light {
         clone.vsmBlurSize = this._vsmBlurSize;
         clone.vsmBlurMode = this.vsmBlurMode;
         clone.vsmBias = this.vsmBias;
-        clone.lightSize = this.lightSize;
+        clone.penumbraSize = this.penumbraSize;
         clone.shadowUpdateMode = this.shadowUpdateMode;
         clone.mask = this.mask;
 
diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js
index 0527d12a7..8b8a77394 100644
--- a/src/scene/renderer/forward-renderer.js
+++ b/src/scene/renderer/forward-renderer.js
@@ -111,7 +111,7 @@ class ForwardRenderer extends Renderer {
         this.lightCookieIntId = [];
         this.lightCookieMatrixId = [];
         this.lightCookieOffsetId = [];
-        this.lightSizeId = [];
+        this.lightShadowSearchAreaId = [];
         this.lightCameraParamsId = [];
 
         // shadow cascades
@@ -174,6 +174,7 @@ class ForwardRenderer extends Renderer {
         this.lightShadowMatrixId[i] = scope.resolve(light + '_shadowMatrix');
         this.lightShadowParamsId[i] = scope.resolve(light + '_shadowParams');
         this.lightShadowIntensity[i] = scope.resolve(light + '_shadowIntensity');
+        this.lightShadowSearchAreaId[i] = scope.resolve(light + '_shadowSearchArea');
         this.lightRadiusId[i] = scope.resolve(light + '_radius');
         this.lightPos[i] = new Float32Array(3);
         this.lightPosId[i] = scope.resolve(light + '_position');
@@ -187,7 +188,6 @@ class ForwardRenderer extends Renderer {
         this.lightCookieIntId[i] = scope.resolve(light + '_cookieIntensity');
         this.lightCookieMatrixId[i] = scope.resolve(light + '_cookieMatrix');
         this.lightCookieOffsetId[i] = scope.resolve(light + '_cookieOffset');
-        this.lightSizeId[i] = scope.resolve(light + '_size');
         this.lightCameraParamsId[i] = scope.resolve(light + '_cameraParams');
 
         // shadow cascades
@@ -257,11 +257,13 @@ class ForwardRenderer extends Renderer {
                 this.shadowCascadeDistancesId[cnt].setValue(directional._shadowCascadeDistances);
                 this.shadowCascadeCountId[cnt].setValue(directional.numCascades);
                 this.lightShadowIntensity[cnt].setValue(directional.shadowIntensity);
-                this.lightSizeId[cnt].setValue(directional.lightSize);
+
+                const pixelsPerMeter = 1.0 / (lightRenderData.shadowCamera.renderTarget.width / directional.penumbraSize);
+                this.lightShadowSearchAreaId[cnt].setValue(pixelsPerMeter);
 
                 const cameraParams = directional._shadowCameraParams;
                 cameraParams.length = 4;
-                cameraParams[0] = 2.0;
+                cameraParams[0] = 0;
                 cameraParams[1] = lightRenderData.shadowCamera._farClip;
                 cameraParams[2] = lightRenderData.shadowCamera._nearClip;
                 cameraParams[3] = 1;
@@ -329,12 +331,13 @@ class ForwardRenderer extends Renderer {
             params[3] = 1.0 / omni.attenuationEnd;
             this.lightShadowParamsId[cnt].setValue(params);
             this.lightShadowIntensity[cnt].setValue(omni.shadowIntensity);
-            this.lightSizeId[cnt].setValue(omni.lightSize);
 
-            const fov = lightRenderData.shadowCamera._fov * Math.PI / 180.0;
+            const pixelsPerMeter = 1.0 / (lightRenderData.shadowCamera.renderTarget.width / omni.penumbraSize);
+            this.lightShadowSearchAreaId[cnt].setValue(pixelsPerMeter);
             const cameraParams = omni._shadowCameraParams;
+
             cameraParams.length = 4;
-            cameraParams[0] = 1.0 / Math.tan(fov / 2.0);
+            cameraParams[0] = 0;
             cameraParams[1] = lightRenderData.shadowCamera._farClip;
             cameraParams[2] = lightRenderData.shadowCamera._nearClip;
             cameraParams[3] = 0;
@@ -394,12 +397,15 @@ class ForwardRenderer extends Renderer {
             params[3] = 1.0 / spot.attenuationEnd;
             this.lightShadowParamsId[cnt].setValue(params);
             this.lightShadowIntensity[cnt].setValue(spot.shadowIntensity);
-            this.lightSizeId[cnt].setValue(spot.lightSize);
 
+            const pixelsPerMeter = 1.0 / (lightRenderData.shadowCamera.renderTarget.width / spot.penumbraSize);
             const fov = lightRenderData.shadowCamera._fov * Math.PI / 180.0;
+            const fovRatio = 1.0 / Math.tan(fov / 2.0);
+            this.lightShadowSearchAreaId[cnt].setValue(pixelsPerMeter * fovRatio);
+
             const cameraParams = spot._shadowCameraParams;
             cameraParams.length = 4;
-            cameraParams[0] = 1.0 / Math.tan(fov / 2.0);
+            cameraParams[0] = 0;
             cameraParams[1] = lightRenderData.shadowCamera._farClip;
             cameraParams[2] = lightRenderData.shadowCamera._nearClip;
             cameraParams[3] = 0;
diff --git a/src/scene/shader-lib/chunks/lit/frag/shadowPCSS.js b/src/scene/shader-lib/chunks/lit/frag/shadowPCSS.js
index 1e38c87d9..336ac068e 100644
--- a/src/scene/shader-lib/chunks/lit/frag/shadowPCSS.js
+++ b/src/scene/shader-lib/chunks/lit/frag/shadowPCSS.js
@@ -73,7 +73,7 @@ float PCSSBlockerDistance(TEXTURE_ACCEPT(shadowMap), vec2 sampleCoords[PCSS_SAMP
     return -1.0;
 }
 
-float PCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoords, vec4 cameraParams, float oneOverShadowMapSize, vec2 lightSize) {
+float PCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoords, vec4 cameraParams, vec2 shadowSearchArea) {
     float receiverDepth = linearizeDepth(shadowCoords.z, cameraParams);
 #ifndef GL2
     // If using packed depth on GL1, we need to normalize to get the correct receiver depth
@@ -88,13 +88,12 @@ float PCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoords, vec4 cameraParams, floa
     }
 
     // Calculate the ratio of FOV between 45.0 degrees (tan(45) == 1) and the FOV of the camera    
-    float fovRatioAtDepth = cameraParams.x;
-    float averageBlocker = PCSSBlockerDistance(TEXTURE_PASS(shadowMap), samplePoints, shadowCoords.xy, oneOverShadowMapSize * lightSize * fovRatioAtDepth, receiverDepth);
+    float averageBlocker = PCSSBlockerDistance(TEXTURE_PASS(shadowMap), samplePoints, shadowCoords.xy, shadowSearchArea, receiverDepth);
     if (averageBlocker == -1.0) {
         return 1.0;
     } else {
 
-        vec2 filterRadius = ((receiverDepth - averageBlocker) / averageBlocker) * lightSize * oneOverShadowMapSize * fovRatioAtDepth;
+        vec2 filterRadius = ((receiverDepth - averageBlocker) / averageBlocker) * shadowSearchArea;
 
         float shadow = 0.0;
 
@@ -114,11 +113,11 @@ float PCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoords, vec4 cameraParams, floa
     } 
 }
 
-float PCSSCubeBlockerDistance(samplerCube shadowMap, vec3 lightDirNorm, vec3 samplePoints[PCSS_SAMPLE_COUNT], float z, float lightSize) {
+float PCSSCubeBlockerDistance(samplerCube shadowMap, vec3 lightDirNorm, vec3 samplePoints[PCSS_SAMPLE_COUNT], float z, float shadowSearchArea) {
     float blockers = 0.0;
     float averageBlocker = 0.0;
     for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) {
-        vec3 sampleDir = lightDirNorm + samplePoints[i] * lightSize;
+        vec3 sampleDir = lightDirNorm + samplePoints[i] * shadowSearchArea;
         sampleDir = normalize(sampleDir);
 
     #ifdef GL2
@@ -136,7 +135,7 @@ float PCSSCubeBlockerDistance(samplerCube shadowMap, vec3 lightDirNorm, vec3 sam
     return -1.0;
 }
 
-float PCSSCube(samplerCube shadowMap, vec4 shadowParams, vec3 shadowCoords, vec4 cameraParams, float oneOverShadowMapSize, float lightSize, vec3 lightDir) {
+float PCSSCube(samplerCube shadowMap, vec4 shadowParams, vec3 shadowCoords, vec4 cameraParams, float shadowSearchArea, vec3 lightDir) {
     
     vec3 samplePoints[PCSS_SAMPLE_COUNT];
     float noise = gradientNoise( gl_FragCoord.xy ) * 2.0 * PI;
@@ -148,12 +147,12 @@ float PCSSCube(samplerCube shadowMap, vec4 shadowParams, vec3 shadowCoords, vec4
     float receiverDepth = length(lightDir) * shadowParams.w + shadowParams.z;
     vec3 lightDirNorm = normalize(lightDir);
     
-    float averageBlocker = PCSSCubeBlockerDistance(shadowMap, lightDirNorm, samplePoints, receiverDepth, lightSize * oneOverShadowMapSize * 2.0);
+    float averageBlocker = PCSSCubeBlockerDistance(shadowMap, lightDirNorm, samplePoints, receiverDepth, shadowSearchArea);
     if (averageBlocker == -1.0) {
         return 1.0;
     } else {
 
-        float filterRadius = ((receiverDepth - averageBlocker) / averageBlocker) * lightSize * oneOverShadowMapSize * 2.0;
+        float filterRadius = ((receiverDepth - averageBlocker) / averageBlocker) * shadowSearchArea;
 
         float shadow = 0.0;
         for (int i = 0; i < PCSS_SAMPLE_COUNT; i++)
@@ -173,16 +172,16 @@ float PCSSCube(samplerCube shadowMap, vec4 shadowParams, vec3 shadowCoords, vec4
     }
 }
 
-float getShadowPointPCSS(samplerCube shadowMap, vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 lightSize, vec3 lightDir) {
-    return PCSSCube(shadowMap, shadowParams, shadowCoord, cameraParams, (1.0 / shadowParams.x), lightSize.x, lightDir);
+float getShadowPointPCSS(samplerCube shadowMap, vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 shadowSearchArea, vec3 lightDir) {
+    return PCSSCube(shadowMap, shadowParams, shadowCoord, cameraParams, shadowSearchArea.x, lightDir);
 }
 
-float getShadowSpotPCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 lightSize, vec3 lightDir) {
-    return PCSS(TEXTURE_PASS(shadowMap), shadowCoord, cameraParams, (1.0 / shadowParams.x), lightSize);
+float getShadowSpotPCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 shadowSearchArea, vec3 lightDir) {
+    return PCSS(TEXTURE_PASS(shadowMap), shadowCoord, cameraParams, shadowSearchArea);
 }
 
-float getShadowPCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 lightSize, vec3 lightDir) {
-    return PCSS(TEXTURE_PASS(shadowMap), shadowCoord, cameraParams, (1.0 / shadowParams.x), lightSize);
+float getShadowPCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 shadowSearchArea, vec3 lightDir) {
+    return PCSS(TEXTURE_PASS(shadowMap), shadowCoord, cameraParams, shadowSearchArea);
 }
 
 `;
diff --git a/src/scene/shader-lib/chunks/standard/frag/litShaderArgs.js b/src/scene/shader-lib/chunks/standard/frag/litShaderArgs.js
index 43d4f97e2..c12429686 100644
--- a/src/scene/shader-lib/chunks/standard/frag/litShaderArgs.js
+++ b/src/scene/shader-lib/chunks/standard/frag/litShaderArgs.js
@@ -11,71 +11,71 @@ struct IridescenceArgs
 
 struct ClearcoatArgs
 {
+    // The normal used for the clearcoat layer
+    vec3 worldNormal;
+
     // Intensity of the clearcoat layer, range [0..1]
     float specularity;
 
     // Glossiness of clearcoat layer, range [0..1]
     float gloss;
-
-    // The normal used for the clearcoat layer
-    vec3 worldNormal;
 };
 
 struct SheenArgs
 {
-    // Glossiness of the sheen layer, range [0..1]
-    float gloss;
-
     // The color of the f0 specularity factor for the sheen layer
     vec3 specularity;
+
+    // Glossiness of the sheen layer, range [0..1]
+    float gloss;
 };
 
 struct LitShaderArguments {
-    // Transparency
-    float opacity;
-
     // Normal direction in world space
     vec3 worldNormal;
 
+    // Transparency
+    float opacity;
+
     // Surface albedo absorbance
     vec3 albedo;
 
     // Transmission factor (refraction), range [0..1]
     float transmission;
 
-    // Uniform thickness of medium, used by transmission, range [0..inf]
-    float thickness;
-
     // The f0 specularity factor
     vec3 specularity;
 
-    // The microfacet glossiness factor, range [0..1]
-    float gloss;
-
-    // Surface metalness factor, range [0..1]
-    float metalness;
+    // Uniform thickness of medium, used by transmission, range [0..inf]
+    float thickness;
 
-    // Specularity intensity factor, range [0..1]
-    float specularityFactor;
+    // Emission color
+    vec3 emission;
 
     // Ambient occlusion amount, range [0..1]
     float ao;
 
-    // Emission color
-    vec3 emission;
-
     // Light map color
     vec3 lightmap;
 
+    // Specularity intensity factor, range [0..1]
+    float specularityFactor;
+
     // Light map direction
     vec3 lightmapDir;
 
+    // The microfacet glossiness factor, range [0..1]
+    float gloss;
+
     // Iridescence extension arguments
     IridescenceArgs iridescence;
 
     // Clearcoat extension arguments
     ClearcoatArgs clearcoat;
 
+    // Surface metalness factor, range [0..1]
+    float metalness;
+
     // Sheen extension arguments
     SheenArgs sheen;
 };
diff --git a/src/scene/shader-lib/programs/lit-shader.js b/src/scene/shader-lib/programs/lit-shader.js
index fd79424d1..5e838ee69 100644
--- a/src/scene/shader-lib/programs/lit-shader.js
+++ b/src/scene/shader-lib/programs/lit-shader.js
@@ -543,7 +543,7 @@ class LitShader {
             code += "    gl_FragColor = vec4(1.0);\n"; // just the simplest code, color is not written anyway
 
             // clustered omni light is using shadow sampler and needs to write custom depth
-            if (shadowType === SHADOW_PCSS || (lightType === LIGHTTYPE_OMNI && !options.clusteeredLightingEnabled)) {
+            if (shadowType === SHADOW_PCSS || (lightType === LIGHTTYPE_OMNI && !options.clusteredLightingEnabled)) {
                 code += "   gl_FragColor.r = depth;\n";
             } else if (options.clusteredLightingEnabled && lightType === LIGHTTYPE_OMNI && device.supportsDepthShadow) {
                 code += "    gl_FragDepth = depth;\n";
@@ -646,7 +646,7 @@ class LitShader {
             decl.append("uniform vec3 light" + i + "_color;");
 
             if (light._shadowType === SHADOW_PCSS && light.castShadows && !options.noShadow) {
-                decl.append(`uniform float light${i}_size;`);
+                decl.append(`uniform float light${i}_shadowSearchArea;`);
                 decl.append(`uniform vec4 light${i}_cameraParams;`);
             }
 
@@ -1298,11 +1298,11 @@ class LitShader {
                             // VSM
                             shadowCoordArgs = `${shadowCoordArgs}, ${evsmExp}, dLightDirW`;
                         } else if (pcssShadows) {
-                            let lightSizeArg = `vec2(light${i}_size)`;
+                            let penumbraSizeArg = `vec2(light${i}_shadowSearchArea)`;
                             if (lightShape !== LIGHTSHAPE_PUNCTUAL) {
-                                lightSizeArg = `vec2(length(light${i}_halfWidth), length(light${i}_halfHeight)) * light${i}_size`;
+                                penumbraSizeArg = `vec2(length(light${i}_halfWidth), length(light${i}_halfHeight)) * light${i}_shadowSearchArea`;
                             }
-                            shadowCoordArgs = `${shadowCoordArgs}, light${i}_cameraParams, ${lightSizeArg}, dLightDirW`;
+                            shadowCoordArgs = `${shadowCoordArgs}, light${i}_cameraParams, ${penumbraSizeArg}, dLightDirW`;
                         }
 
                         if (lightType === LIGHTTYPE_OMNI) {


Test patch:
diff --git a/test/framework/anim/evaluator/anim-clip.test.mjs b/test/framework/anim/evaluator/anim-clip.test.mjs
index 075c239a5..a70458bb1 100644
--- a/test/framework/anim/evaluator/anim-clip.test.mjs
+++ b/test/framework/anim/evaluator/anim-clip.test.mjs
@@ -19,7 +19,9 @@ describe('AnimClip', function () {
             { name: 'event3', time: 1.5 }
         ]);
         const animTrack = new AnimTrack('track', 2, inputs, outputs, curves, animEvents);
-        animClip = new AnimClip(animTrack, 0, 1, true, false);
+        animClip = new AnimClip(animTrack, 0, 1, true, true, {
+            fire: () => {}
+        });
     });
 
     describe('#constructor', function () {
@@ -30,7 +32,7 @@ describe('AnimClip', function () {
             expect(animClip.track.name).to.equal('track');
             expect(animClip.snapshot._name).to.equal('trackSnapshot');
             expect(animClip.time).to.equal(0);
-            expect(animClip.loop).to.equal(false);
+            expect(animClip.loop).to.equal(true);
             expect(animClip.eventCursor).to.equal(0);
         });
 
@@ -102,7 +104,7 @@ describe('AnimClip', function () {
 
     describe('#time', function () {
 
-        it('updates the clips eventCursor property', function () {
+        it('aligns the clips eventCursor property when setting the time', function () {
             expect(animClip.eventCursor).to.equal(0);
             animClip.time = 1.1;
             expect(animClip.eventCursor).to.equal(2);
@@ -112,6 +114,42 @@ describe('AnimClip', function () {
             expect(animClip.eventCursor).to.equal(0);
         });
 
+        it('updates the clips eventCursor property as the clip updates forwards', function () {
+            expect(animClip.time).to.equal(0);
+            expect(animClip.eventCursor).to.equal(0);
+            animClip._update(0.55);
+            expect(animClip.time).to.equal(0.55);
+            expect(animClip.eventCursor).to.equal(1);
+            animClip._update(0.5);
+            expect(animClip.time).to.equal(1.05);
+            expect(animClip.eventCursor).to.equal(2);
+            animClip._update(0.5);
+            expect(animClip.time).to.equal(1.55);
+            expect(animClip.eventCursor).to.equal(0);
+            animClip._update(0.45);
+            expect(animClip.time).to.equal(2);
+            expect(animClip.eventCursor).to.equal(0);
+        });
+
+        it('updates the clips eventCursor property as the clip updates backwards', function () {
+            animClip.speed = -1;
+            animClip.time = 2;
+            expect(animClip.time).to.equal(2);
+            expect(animClip.eventCursor).to.equal(2);
+            animClip._update(0.45);
+            expect(animClip.time).to.equal(1.55);
+            expect(animClip.eventCursor).to.equal(2);
+            animClip._update(0.5);
+            expect(animClip.time).to.equal(1.05);
+            expect(animClip.eventCursor).to.equal(1);
+            animClip._update(0.5);
+            expect(animClip.time).to.equal(0.55);
+            expect(animClip.eventCursor).to.equal(0);
+            animClip._update(0.55);
+            expect(animClip.time).to.equal(0);
+            expect(animClip.eventCursor).to.equal(2);
+        });
+
     });
 
 });