Repository: playcanvas/engine
Pull request #5425 (2023-06-21 13:07:21)

Simplify the normal offset shadow coordinate method for clustered point lights
The `normalOffsetPointShadow` method previously changed the dLightDirW parameter to only use it on the line below. This was left in the clustered light shader for backwards compatibility, but this can now be changed to return the offsetted direction, making the code much cleaner. 


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/clusteredLight.js b/src/scene/shader-lib/chunks/lit/frag/clusteredLight.js
index f2a466264..597af12d4 100644
--- a/src/scene/shader-lib/chunks/lit/frag/clusteredLight.js
+++ b/src/scene/shader-lib/chunks/lit/frag/clusteredLight.js
@@ -464,14 +464,14 @@ void evaluateLight(
                     } else {
 
                         // omni shadow
-                        normalOffsetPointShadow(shadowParams, dLightPosW, dLightDirW, dLightDirNormW, geometricNormal);  // normalBias adjusted for distance
+                        vec3 dir = normalOffsetPointShadow(shadowParams, dLightPosW, dLightDirW, dLightDirNormW, geometricNormal);  // normalBias adjusted for distance
 
                         #if defined(CLUSTER_SHADOW_TYPE_PCF1)
-                            float shadow = getShadowOmniClusteredPCF1(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dLightDirW);
+                            float shadow = getShadowOmniClusteredPCF1(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir);
                         #elif defined(CLUSTER_SHADOW_TYPE_PCF3)
-                            float shadow = getShadowOmniClusteredPCF3(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dLightDirW);
+                            float shadow = getShadowOmniClusteredPCF3(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir);
                         #elif defined(CLUSTER_SHADOW_TYPE_PCF5)
-                            float shadow = getShadowOmniClusteredPCF5(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dLightDirW);
+                            float shadow = getShadowOmniClusteredPCF5(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir);
                         #endif
                         falloffAttenuation *= mix(1.0, shadow, light.shadowIntensity);
                     }
diff --git a/src/scene/shader-lib/chunks/lit/frag/clusteredLightShadows.js b/src/scene/shader-lib/chunks/lit/frag/clusteredLightShadows.js
index b2ba651a3..f8810e3b9 100644
--- a/src/scene/shader-lib/chunks/lit/frag/clusteredLightShadows.js
+++ b/src/scene/shader-lib/chunks/lit/frag/clusteredLightShadows.js
@@ -14,11 +14,11 @@ void getShadowCoordPerspZbufferNormalOffset(mat4 shadowMatrix, vec4 shadowParams
     _getShadowCoordPerspZbuffer(shadowMatrix, shadowParams, wPos);
 }
 
-void normalOffsetPointShadow(vec4 shadowParams, vec3 lightPos, inout vec3 lightDir, vec3 lightDirNorm, vec3 normal) {
+vec3 normalOffsetPointShadow(vec4 shadowParams, vec3 lightPos, inout vec3 lightDir, vec3 lightDirNorm, vec3 normal) {
     float distScale = length(lightDir);
     vec3 wPos = vPositionW + normal * shadowParams.y * clamp(1.0 - dot(normal, -lightDirNorm), 0.0, 1.0) * distScale; //0.02
     vec3 dir = wPos - lightPos;
-    lightDir = dir;
+    return dir;
 }
 
 #ifdef GL2
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 4c490d572..5e838ee69 100644
--- a/src/scene/shader-lib/programs/lit-shader.js
+++ b/src/scene/shader-lib/programs/lit-shader.js
@@ -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);
+        });
+
     });
 
 });