+
three.js webgl - sunlight
+ WASD to move, mouse to look around
+
+
+
+
+
+
+
+
diff --git a/examples/webgl_loader_3mf_materials.html b/examples/webgl_loader_3mf_materials.html
index af6c74e591b947..a0dac5fc4d70ea 100644
--- a/examples/webgl_loader_3mf_materials.html
+++ b/examples/webgl_loader_3mf_materials.html
@@ -57,19 +57,11 @@
hemiLight.position.set( 0, 100, 0 );
scene.add( hemiLight );
- const dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
- dirLight.position.set( - 0, 40, 50 );
- dirLight.castShadow = true;
- dirLight.shadow.camera.top = 50;
- dirLight.shadow.camera.bottom = - 25;
- dirLight.shadow.camera.left = - 25;
- dirLight.shadow.camera.right = 25;
- dirLight.shadow.camera.near = 0.1;
- dirLight.shadow.camera.far = 200;
- dirLight.shadow.mapSize.set( 1024, 1024 );
- scene.add( dirLight );
-
- // scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) );
+ const sunLight = new THREE.SunLight( 0xffffff, 3 );
+ sunLight.position.set( 0, 40, 50 );
+ sunLight.castShadow = true;
+ sunLight.shadow.camera.far = 100;
+ scene.add( sunLight );
//
diff --git a/examples/webgl_loader_fbx.html b/examples/webgl_loader_fbx.html
index 685e354ea60efb..1f612eca33f573 100644
--- a/examples/webgl_loader_fbx.html
+++ b/examples/webgl_loader_fbx.html
@@ -90,16 +90,10 @@
hemiLight.position.set( 0, 200, 0 );
scene.add( hemiLight );
- const dirLight = new THREE.DirectionalLight( 0xffffff, 5 );
- dirLight.position.set( 0, 200, 100 );
- dirLight.castShadow = true;
- dirLight.shadow.camera.top = 180;
- dirLight.shadow.camera.bottom = - 100;
- dirLight.shadow.camera.left = - 120;
- dirLight.shadow.camera.right = 120;
- scene.add( dirLight );
-
- // scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) );
+ const sunLight = new THREE.SunLight( 0xffffff, 5 );
+ sunLight.position.set( 0, 200, 100 );
+ sunLight.castShadow = true;
+ scene.add( sunLight );
// ground
const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 2000, 2000 ), new THREE.MeshPhongMaterial( { color: 0x999999, depthWrite: false } ) );
diff --git a/examples/webgl_loader_md2_control.html b/examples/webgl_loader_md2_control.html
index 70b2623677fd73..edff9fec2cbb0b 100644
--- a/examples/webgl_loader_md2_control.html
+++ b/examples/webgl_loader_md2_control.html
@@ -92,24 +92,11 @@
scene.add( new THREE.AmbientLight( 0x666666, 3 ) );
- const light = new THREE.DirectionalLight( 0xffffff, 7 );
- light.position.set( 200, 450, 500 );
-
- light.castShadow = true;
-
- light.shadow.mapSize.width = 1024;
- light.shadow.mapSize.height = 512;
-
- light.shadow.camera.near = 100;
- light.shadow.camera.far = 1200;
-
- light.shadow.camera.left = - 1000;
- light.shadow.camera.right = 1000;
- light.shadow.camera.top = 350;
- light.shadow.camera.bottom = - 350;
-
- scene.add( light );
- // scene.add( new THREE.CameraHelper( light.shadow.camera ) );
+ const sunLight = new THREE.SunLight( 0xffffff, 7 );
+ sunLight.position.set( 200, 450, 500 );
+ sunLight.castShadow = true;
+ sunLight.shadow.camera.far = 2000;
+ scene.add( sunLight );
// GROUND
@@ -233,7 +220,6 @@
const gyro = new Gyroscope();
gyro.add( camera );
- gyro.add( light, light.target );
characters[ Math.floor( nSkins / 2 ) ].root.add( gyro );
diff --git a/examples/webgl_postprocessing_outline.html b/examples/webgl_postprocessing_outline.html
index f9293721aeefc5..54b9973b6634bb 100644
--- a/examples/webgl_postprocessing_outline.html
+++ b/examples/webgl_postprocessing_outline.html
@@ -151,20 +151,11 @@
scene.add( new THREE.AmbientLight( 0xaaaaaa, 0.6 ) );
- const light = new THREE.DirectionalLight( 0xddffdd, 2 );
+ const light = new THREE.SunLight( 0xddffdd, 2 );
light.position.set( 5, 5, 5 );
light.castShadow = true;
- light.shadow.mapSize.width = 1024;
- light.shadow.mapSize.height = 1024;
-
- const d = 10;
-
- light.shadow.camera.left = - d;
- light.shadow.camera.right = d;
- light.shadow.camera.top = d;
- light.shadow.camera.bottom = - d;
- light.shadow.camera.far = 25;
-
+ light.shadow.camera.far = 40;
+ light.shadow.mapSize.setScalar( 2048 );
scene.add( light );
// model
diff --git a/examples/webgl_shadowmap_performance.html b/examples/webgl_shadowmap_performance.html
index 30bb9d6f412533..6b96e126d2969a 100644
--- a/examples/webgl_shadowmap_performance.html
+++ b/examples/webgl_shadowmap_performance.html
@@ -38,8 +38,6 @@
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
- const SHADOW_MAP_WIDTH = 2048, SHADOW_MAP_HEIGHT = 1024;
-
let SCREEN_WIDTH = window.innerWidth;
let SCREEN_HEIGHT = window.innerHeight;
const FLOOR = - 250;
@@ -81,21 +79,12 @@
const ambient = new THREE.AmbientLight( 0xffffff );
scene.add( ambient );
- const light = new THREE.DirectionalLight( 0xffffff, 3 );
- light.position.set( 0, 1500, 1000 );
- light.castShadow = true;
- light.shadow.camera.top = 2000;
- light.shadow.camera.bottom = - 2000;
- light.shadow.camera.left = - 2000;
- light.shadow.camera.right = 2000;
- light.shadow.camera.near = 1200;
- light.shadow.camera.far = 2500;
- light.shadow.bias = 0.0001;
-
- light.shadow.mapSize.width = SHADOW_MAP_WIDTH;
- light.shadow.mapSize.height = SHADOW_MAP_HEIGHT;
-
- scene.add( light );
+ const sunLight = new THREE.SunLight( 0xffffff, 3 );
+ sunLight.position.set( 0, 1500, 1000 );
+ sunLight.castShadow = true;
+ sunLight.shadow.camera.far = FAR;
+ sunLight.shadow.normalBias = 1;
+ scene.add( sunLight );
createScene();
diff --git a/src/Three.Core.js b/src/Three.Core.js
index a67148a67ae2b9..3186280a6c5ffc 100644
--- a/src/Three.Core.js
+++ b/src/Three.Core.js
@@ -60,6 +60,8 @@ export { SpotLight } from './lights/SpotLight.js';
export { PointLight } from './lights/PointLight.js';
export { RectAreaLight } from './lights/RectAreaLight.js';
export { HemisphereLight } from './lights/HemisphereLight.js';
+export { SunLight } from './lights/SunLight.js';
+export { SunLightShadow } from './lights/SunLightShadow.js';
export { DirectionalLight } from './lights/DirectionalLight.js';
export { AmbientLight } from './lights/AmbientLight.js';
export { Light } from './lights/Light.js';
diff --git a/src/lights/LightShadow.js b/src/lights/LightShadow.js
index cf98b66cfc4c65..34d472fa58a81d 100644
--- a/src/lights/LightShadow.js
+++ b/src/lights/LightShadow.js
@@ -182,6 +182,18 @@ class LightShadow {
}
+ /**
+ * Used internally by the renderer to get the camera that renders the given viewport.
+ *
+ * @param {number} [viewportIndex=0] - The viewport index.
+ * @return {Camera} The shadow camera.
+ */
+ getCamera( /* viewportIndex */ ) {
+
+ return this.camera;
+
+ }
+
/**
* Gets the shadow cameras frustum. Used internally by the renderer to cull objects.
*
@@ -201,23 +213,41 @@ class LightShadow {
updateMatrices( light ) {
const shadowCamera = this.camera;
- const shadowMatrix = this.matrix;
-
_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
shadowCamera.position.copy( _lightPositionWorld );
_lookTarget.setFromMatrixPosition( light.target.matrixWorld );
shadowCamera.lookAt( _lookTarget );
shadowCamera.updateMatrixWorld();
+ this._updateMatrix( shadowCamera, this.matrix, this._frustum );
+
+ }
+
+ /**
+ * Updates a shadow projection matrix and its corresponding frustum.
+ *
+ * @private
+ * @param {Camera} shadowCamera - The shadow camera.
+ * @param {Matrix4} shadowMatrix - The target shadow matrix.
+ * @param {Frustum} frustum - The target frustum.
+ * @param {Vector4} [viewport] - The viewport within the shadow atlas.
+ */
+ _updateMatrix( shadowCamera, shadowMatrix, frustum, viewport ) {
_projScreenMatrix.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
- this._frustum.setFromProjectionMatrix( _projScreenMatrix, shadowCamera.coordinateSystem, shadowCamera.reversedDepth );
+ frustum.setFromProjectionMatrix( _projScreenMatrix, shadowCamera.coordinateSystem, shadowCamera.reversedDepth );
+
+ const frameExtents = this._frameExtents;
+ const scaleX = viewport ? viewport.z / frameExtents.x : 1;
+ const scaleY = viewport ? viewport.w / frameExtents.y : 1;
+ const offsetX = viewport ? viewport.x / frameExtents.x : 0;
+ const offsetY = viewport ? viewport.y / frameExtents.y : 0;
if ( shadowCamera.coordinateSystem === WebGPUCoordinateSystem || shadowCamera.reversedDepth ) {
shadowMatrix.set(
- 0.5, 0.0, 0.0, 0.5,
- 0.0, 0.5, 0.0, 0.5,
+ 0.5 * scaleX, 0.0, 0.0, 0.5 * scaleX + offsetX,
+ 0.0, 0.5 * scaleY, 0.0, 0.5 * scaleY + offsetY,
0.0, 0.0, 1.0, 0.0, // Identity Z (preserving the correct [0, 1] range from the projection matrix)
0.0, 0.0, 0.0, 1.0
);
@@ -225,8 +255,8 @@ class LightShadow {
} else {
shadowMatrix.set(
- 0.5, 0.0, 0.0, 0.5,
- 0.0, 0.5, 0.0, 0.5,
+ 0.5 * scaleX, 0.0, 0.0, 0.5 * scaleX + offsetX,
+ 0.0, 0.5 * scaleY, 0.0, 0.5 * scaleY + offsetY,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0
);
diff --git a/src/lights/SunLight.js b/src/lights/SunLight.js
new file mode 100644
index 00000000000000..dd38710367d32d
--- /dev/null
+++ b/src/lights/SunLight.js
@@ -0,0 +1,91 @@
+import { Light } from './Light.js';
+import { SunLightShadow } from './SunLightShadow.js';
+
+/**
+ * A sun-like light that gets emitted in a specific direction, with rays that
+ * are all parallel, and casts cascaded shadow maps via {@link SunLightShadow},
+ * suited for lighting large scenes.
+ *
+ * Unlike {@link DirectionalLight}, the light has no target: like
+ * {@link HemisphereLight}, its direction is defined by its position. The
+ * light shines from its position towards the origin and points straight
+ * down by default.
+ *
+ * ```js
+ * const sun = new SunLight( 0xfff2e3, 3 );
+ * sun.position.set( 1, 1, 1 );
+ * sun.castShadow = true;
+ * scene.add( sun );
+ * ```
+ *
+ * This light is only supported by `WebGLRenderer`. When using `WebGPURenderer`,
+ * use {@link DirectionalLight} with `CSMShadowNode` instead.
+ *
+ * @augments Light
+ */
+class SunLight extends Light {
+
+ /**
+ * Constructs a new sun light.
+ *
+ * @param {(number|Color|string)} [color=0xffffff] - The light's color.
+ * @param {number} [intensity=1] - The light's strength/intensity.
+ */
+ constructor( color, intensity ) {
+
+ super( color, intensity );
+
+ /**
+ * This flag can be used for type testing.
+ *
+ * @type {boolean}
+ * @readonly
+ * @default true
+ */
+ this.isSunLight = true;
+
+ this.type = 'SunLight';
+
+ /**
+ * The light's shadow configuration.
+ *
+ * @type {SunLightShadow}
+ */
+ this.shadow = new SunLightShadow();
+
+ this.position.set( 0, 1, 0 );
+ this.updateMatrix();
+
+ }
+
+ dispose() {
+
+ super.dispose();
+
+ this.shadow.dispose();
+
+ }
+
+ copy( source ) {
+
+ super.copy( source );
+
+ this.shadow = source.shadow.clone();
+
+ return this;
+
+ }
+
+ toJSON( meta ) {
+
+ const data = super.toJSON( meta );
+
+ data.object.shadow = this.shadow.toJSON();
+
+ return data;
+
+ }
+
+}
+
+export { SunLight };
diff --git a/src/lights/SunLightShadow.js b/src/lights/SunLightShadow.js
new file mode 100644
index 00000000000000..85a2db5c4fd39d
--- /dev/null
+++ b/src/lights/SunLightShadow.js
@@ -0,0 +1,295 @@
+import { LightShadow } from './LightShadow.js';
+import { OrthographicCamera } from '../cameras/OrthographicCamera.js';
+import { Frustum } from '../math/Frustum.js';
+import { Matrix4 } from '../math/Matrix4.js';
+import { Vector3 } from '../math/Vector3.js';
+import { Vector4 } from '../math/Vector4.js';
+
+const _lightOrientationMatrix = /*@__PURE__*/ new Matrix4();
+const _viewToLightMatrix = /*@__PURE__*/ new Matrix4();
+const _lightDirection = /*@__PURE__*/ new Vector3();
+const _up = /*@__PURE__*/ new Vector3();
+const _center = /*@__PURE__*/ new Vector3();
+const _corner = /*@__PURE__*/ new Vector3();
+const _nearCorners = [ new Vector3(), new Vector3(), new Vector3(), new Vector3() ];
+const _farCorners = [ new Vector3(), new Vector3(), new Vector3(), new Vector3() ];
+const _cascadeCorners = [ new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3() ];
+
+// must match the cascade count in the sun shadow shader chunks
+
+const _cascadeCount = 4;
+
+// fraction of each cascade's depth range that blends into the next cascade
+
+const _cascadeFade = 0.1;
+
+/**
+ * Represents the shadow configuration of {@link SunLight}, using four
+ * cascaded shadow maps (CSM).
+ *
+ * The shadow camera projection is fitted automatically to slices of the view
+ * frustum, up to a distance of `camera.far` (or the view camera's far plane,
+ * whichever is smaller), and adjacent cascades blend into each other over a
+ * small depth range. `camera.left/right/top/bottom` are ignored.
+ *
+ * The default `mapSize` is `1024x1024` per cascade.
+ *
+ * @augments LightShadow
+ */
+class SunLightShadow extends LightShadow {
+
+ /**
+ * Constructs a new sun light shadow.
+ */
+ constructor() {
+
+ super( new OrthographicCamera( - 5, 5, 5, - 5, 0.5, 500 ) );
+
+ /**
+ * This flag can be used for type testing.
+ *
+ * @type {boolean}
+ * @readonly
+ * @default true
+ */
+ this.isSunLightShadow = true;
+
+ this.mapSize.set( 1024, 1024 );
+
+ this._cameras = [];
+ this._matrices = [];
+ this._frustums = [];
+ this._cascadeSplits = new Array( _cascadeCount + 1 ).fill( 0 );
+
+ // per cascade ( begin, end, fade start ) view depths, consumed by the renderer
+
+ this._cascadeData = [];
+
+ this._viewportCount = _cascadeCount;
+ this._frameExtents.set( 2, 2 );
+
+ for ( let i = 0; i < _cascadeCount; i ++ ) {
+
+ this._cameras.push( new OrthographicCamera() );
+ this._matrices.push( new Matrix4() );
+ this._frustums.push( new Frustum() );
+ this._cascadeData.push( new Vector4() );
+
+ }
+
+ while ( this._viewports.length < _cascadeCount ) this._viewports.push( new Vector4() );
+
+ }
+
+ /**
+ * Returns the shadow camera of the given cascade.
+ *
+ * @param {number} [cascadeIndex=0] - The cascade index.
+ * @return {OrthographicCamera} The shadow camera.
+ */
+ getCamera( cascadeIndex = 0 ) {
+
+ return this._cameras[ cascadeIndex ];
+
+ }
+
+ /**
+ * Returns the shadow matrix of the given cascade.
+ *
+ * @param {number} [cascadeIndex=0] - The cascade index.
+ * @return {Matrix4} The shadow matrix.
+ */
+ getMatrix( cascadeIndex = 0 ) {
+
+ return this._matrices[ cascadeIndex ];
+
+ }
+
+ /**
+ * Returns the shadow camera frustum of the given cascade. Used internally by
+ * the renderer to cull objects.
+ *
+ * @param {number} [cascadeIndex=0] - The cascade index.
+ * @return {Frustum} The shadow camera frustum.
+ */
+ getFrustum( cascadeIndex = 0 ) {
+
+ return this._frustums[ cascadeIndex ];
+
+ }
+
+ /**
+ * Update the matrices for the cascade cameras and shadows, used internally
+ * by the renderer.
+ *
+ * @param {Light} light - The light for which the shadow is being rendered.
+ * @param {Camera} viewCamera - The camera the scene is rendered with.
+ */
+ updateMatrices( light, viewCamera ) {
+
+ if ( viewCamera === undefined ) return;
+
+ // inset the cascade viewports so shadow filtering cannot read across atlas tiles
+
+ const insetX = Math.min( 0.25, ( Math.ceil( this.radius ) + 1 ) / this.mapSize.x );
+ const insetY = Math.min( 0.25, ( Math.ceil( this.radius ) + 1 ) / this.mapSize.y );
+
+ for ( let i = 0; i < _cascadeCount; i ++ ) {
+
+ this._viewports[ i ].set( i % 2 + insetX, Math.floor( i / 2 ) + insetY, 1 - 2 * insetX, 1 - 2 * insetY );
+
+ }
+
+ const camera = this.camera;
+ const cameraNear = viewCamera.near;
+ const cameraFar = Math.max( cameraNear + 1e-6, Math.min( camera.far, viewCamera.far ) );
+
+ // practical split scheme: the average of uniform and logarithmic splits
+
+ const splits = this._cascadeSplits;
+ splits[ 0 ] = cameraNear;
+
+ for ( let i = 1; i < _cascadeCount; i ++ ) {
+
+ const amount = i / _cascadeCount;
+ const uniform = cameraNear + ( cameraFar - cameraNear ) * amount;
+ const logarithmic = cameraNear > 0 ? cameraNear * Math.pow( cameraFar / cameraNear, amount ) : uniform;
+ splits[ i ] = ( uniform + logarithmic ) * 0.5;
+
+ }
+
+ splits[ _cascadeCount ] = cameraFar;
+
+ _lightDirection.setFromMatrixPosition( light.matrixWorld ).negate().normalize();
+
+ _up.set( 0, 1, 0 );
+ if ( Math.abs( _up.dot( _lightDirection ) ) > 0.99 ) _up.set( 0, 0, 1 );
+
+ _lightOrientationMatrix.lookAt( _center.set( 0, 0, 0 ), _lightDirection, _up );
+ _viewToLightMatrix.copy( _lightOrientationMatrix ).transpose().multiply( viewCamera.matrixWorld );
+
+ // view frustum corners in light space; the rotation preserves distances,
+ // so the cascades can be fitted and snapped directly in this space
+
+ const zNear = viewCamera.reversedDepth ? 1 : - 1;
+ const inverseProjectionMatrix = viewCamera.projectionMatrixInverse;
+
+ let globalMaxZ = - Infinity;
+
+ for ( let i = 0; i < 4; i ++ ) {
+
+ const x = i === 0 || i === 1 ? 1 : - 1;
+ const y = i === 0 || i === 3 ? 1 : - 1;
+
+ const nearCorner = _nearCorners[ i ].set( x, y, zNear ).applyMatrix4( inverseProjectionMatrix );
+ const farCorner = _farCorners[ i ];
+
+ if ( viewCamera.isPerspectiveCamera === true ) {
+
+ farCorner.copy( nearCorner ).multiplyScalar( cameraFar / cameraNear );
+
+ } else {
+
+ farCorner.set( nearCorner.x, nearCorner.y, - cameraFar );
+
+ }
+
+ nearCorner.applyMatrix4( _viewToLightMatrix );
+ farCorner.applyMatrix4( _viewToLightMatrix );
+
+ globalMaxZ = Math.max( globalMaxZ, nearCorner.z, farCorner.z );
+
+ }
+
+ // raise the ceiling one shadow range towards the light so casters outside
+ // the view frustum still cast into it
+
+ globalMaxZ += cameraFar;
+
+ const shadowNear = camera.near;
+
+ for ( let i = 0; i < _cascadeCount; i ++ ) {
+
+ // each cascade covers the fade band of the previous one so both can be sampled while blending
+
+ const cascadeNear = i === 0 ? splits[ 0 ] : this._cascadeData[ i - 1 ].z;
+ const cascadeFar = splits[ i + 1 ];
+ const fadeStart = cascadeFar - _cascadeFade * ( cascadeFar - splits[ i ] );
+
+ this._cascadeData[ i ].set( i === 0 ? - 1e10 : cascadeNear, cascadeFar, fadeStart, 0 );
+
+ // bounding sphere of the cascade slice for a rotation-stable projection
+
+ const nearAlpha = ( cascadeNear - cameraNear ) / ( cameraFar - cameraNear );
+ const farAlpha = ( cascadeFar - cameraNear ) / ( cameraFar - cameraNear );
+
+ _center.set( 0, 0, 0 );
+
+ for ( let j = 0; j < 4; j ++ ) {
+
+ _cascadeCorners[ j * 2 ].lerpVectors( _nearCorners[ j ], _farCorners[ j ], nearAlpha );
+ _cascadeCorners[ j * 2 + 1 ].lerpVectors( _nearCorners[ j ], _farCorners[ j ], farAlpha );
+ _center.add( _cascadeCorners[ j * 2 ] ).add( _cascadeCorners[ j * 2 + 1 ] );
+
+ }
+
+ _center.multiplyScalar( 1 / 8 );
+
+ let radiusSq = 0;
+ let minZ = Infinity;
+
+ for ( let j = 0; j < 8; j ++ ) {
+
+ radiusSq = Math.max( radiusSq, _cascadeCorners[ j ].distanceToSquared( _center ) );
+ minZ = Math.min( minZ, _cascadeCorners[ j ].z );
+
+ }
+
+ let radius = Math.sqrt( radiusSq );
+
+ // snap to the texel grid to avoid shimmering when the view camera moves
+
+ const resolutionX = this.mapSize.width * this._viewports[ i ].z;
+ const resolutionY = this.mapSize.height * this._viewports[ i ].w;
+ const resolution = Math.min( resolutionX, resolutionY );
+
+ if ( resolution > 1 ) {
+
+ // pad by half a texel so snapping cannot clip a frustum corner
+ radius /= 1 - 1 / resolution;
+ const texelSizeX = 2 * radius / resolutionX;
+ const texelSizeY = 2 * radius / resolutionY;
+ _center.x = Math.round( _center.x / texelSizeX ) * texelSizeX;
+ _center.y = Math.round( _center.y / texelSizeY ) * texelSizeY;
+
+ }
+
+ // place the near plane at the caster ceiling
+
+ _center.z = globalMaxZ + shadowNear;
+ _center.applyMatrix4( _lightOrientationMatrix );
+
+ const cascadeCamera = this._cameras[ i ];
+ cascadeCamera.position.copy( _center );
+ cascadeCamera.up.copy( _up );
+ cascadeCamera.lookAt( _corner.copy( _center ).add( _lightDirection ) );
+ cascadeCamera.left = - radius;
+ cascadeCamera.right = radius;
+ cascadeCamera.top = radius;
+ cascadeCamera.bottom = - radius;
+ cascadeCamera.near = shadowNear;
+ cascadeCamera.far = globalMaxZ - minZ + 2 * shadowNear;
+ cascadeCamera.coordinateSystem = camera.coordinateSystem;
+ cascadeCamera._reversedDepth = camera.reversedDepth;
+ cascadeCamera.updateProjectionMatrix();
+ cascadeCamera.updateMatrixWorld();
+
+ this._updateMatrix( cascadeCamera, this._matrices[ i ], this._frustums[ i ], this._viewports[ i ] );
+
+ }
+
+ }
+
+}
+
+export { SunLightShadow };
diff --git a/src/loaders/ObjectLoader.js b/src/loaders/ObjectLoader.js
index 9be1220b963620..56a9acf5cea24f 100644
--- a/src/loaders/ObjectLoader.js
+++ b/src/loaders/ObjectLoader.js
@@ -18,6 +18,7 @@ import {
LinearMipmapLinearFilter
} from '../constants.js';
import { InstancedBufferAttribute } from '../core/InstancedBufferAttribute.js';
+import { SunLight } from '../lights/SunLight.js';
import { Color } from '../math/Color.js';
import { Vector3 } from '../math/Vector3.js';
import { Object3D } from '../core/Object3D.js';
@@ -921,6 +922,12 @@ class ObjectLoader extends Loader {
break;
+ case 'SunLight':
+
+ object = new SunLight( data.color, data.intensity );
+
+ break;
+
case 'DirectionalLight':
object = new DirectionalLight( data.color, data.intensity );
diff --git a/src/nodes/Nodes.js b/src/nodes/Nodes.js
index f1385ffb670638..5a27ea0e7e69b7 100644
--- a/src/nodes/Nodes.js
+++ b/src/nodes/Nodes.js
@@ -55,6 +55,7 @@ export { default as ModelNode } from './accessors/ModelNode.js';
export { default as Object3DNode } from './accessors/Object3DNode.js';
export { default as PointUVNode } from './accessors/PointUVNode.js';
export { default as ReferenceBaseNode } from './accessors/ReferenceBaseNode.js';
+export { default as ReferenceElementNode } from './accessors/ReferenceElementNode.js';
export { default as ReferenceNode } from './accessors/ReferenceNode.js';
export { default as RendererReferenceNode } from './accessors/RendererReferenceNode.js';
export { default as StorageBufferNode } from './accessors/StorageBufferNode.js';
diff --git a/src/nodes/accessors/ReferenceBaseNode.js b/src/nodes/accessors/ReferenceBaseNode.js
index 0cdaab53d31247..1e56ea002046ca 100644
--- a/src/nodes/accessors/ReferenceBaseNode.js
+++ b/src/nodes/accessors/ReferenceBaseNode.js
@@ -2,79 +2,10 @@ import Node from '../core/Node.js';
import { NodeUpdateType } from '../core/constants.js';
import { uniform } from '../core/UniformNode.js';
import { nodeObject } from '../tsl/TSLCore.js';
-import ArrayElementNode from '../utils/ArrayElementNode.js';
+import ReferenceElementNode from './ReferenceElementNode.js';
// TODO: Avoid duplicated code and use only ReferenceBaseNode or ReferenceNode
-/**
- * This class is only relevant if the referenced property is array-like.
- * In this case, `ReferenceElementNode` allows to refer to a specific
- * element inside the data structure via an index.
- *
- * @augments ArrayElementNode
- */
-class ReferenceElementNode extends ArrayElementNode {
-
- static get type() {
-
- return 'ReferenceElementNode';
-
- }
-
- /**
- * Constructs a new reference element node.
- *
- * @param {ReferenceBaseNode} referenceNode - The reference node.
- * @param {Node} indexNode - The index node that defines the element access.
- */
- constructor( referenceNode, indexNode ) {
-
- super( referenceNode, indexNode );
-
- /**
- * Similar to {@link ReferenceBaseNode#reference}, an additional
- * property references to the current node.
- *
- * @type {?ReferenceBaseNode}
- * @default null
- */
- this.referenceNode = referenceNode;
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isReferenceElementNode = true;
-
- }
-
- /**
- * This method is overwritten since the node type is inferred from
- * the uniform type of the reference node.
- *
- * @return {string} The node type.
- */
- generateNodeType() {
-
- return this.referenceNode.uniformType;
-
- }
-
- generate( builder ) {
-
- const snippet = super.generate( builder );
- const arrayType = this.referenceNode.getNodeType();
- const elementType = this.getNodeType();
-
- return builder.format( snippet, arrayType, elementType );
-
- }
-
-}
-
/**
* Base class for nodes which establishes a reference to a property of another object.
* In this way, the value of the node is automatically linked to the value of
diff --git a/src/nodes/accessors/ReferenceElementNode.js b/src/nodes/accessors/ReferenceElementNode.js
new file mode 100644
index 00000000000000..ce3d760d3cc14f
--- /dev/null
+++ b/src/nodes/accessors/ReferenceElementNode.js
@@ -0,0 +1,72 @@
+import ArrayElementNode from '../utils/ArrayElementNode.js';
+
+/**
+ * This class is only relevant if the referenced property is array-like.
+ * In this case, `ReferenceElementNode` allows to refer to a specific
+ * element inside the data structure via an index.
+ *
+ * @augments ArrayElementNode
+ */
+class ReferenceElementNode extends ArrayElementNode {
+
+ static get type() {
+
+ return 'ReferenceElementNode';
+
+ }
+
+ /**
+ * Constructs a new reference element node.
+ *
+ * @param {(ReferenceBaseNode|ReferenceNode)} referenceNode - The reference node.
+ * @param {Node} indexNode - The index node that defines the element access.
+ */
+ constructor( referenceNode, indexNode ) {
+
+ super( referenceNode, indexNode );
+
+ /**
+ * Similar to {@link ReferenceNode#reference}, an additional
+ * property references to the current node.
+ *
+ * @type {?(ReferenceBaseNode|ReferenceNode)}
+ * @default null
+ */
+ this.referenceNode = referenceNode;
+
+ /**
+ * This flag can be used for type testing.
+ *
+ * @type {boolean}
+ * @readonly
+ * @default true
+ */
+ this.isReferenceElementNode = true;
+
+ }
+
+ /**
+ * This method is overwritten since the node type is inferred from
+ * the uniform type of the reference node.
+ *
+ * @return {string} The node type.
+ */
+ generateNodeType() {
+
+ return this.referenceNode.uniformType;
+
+ }
+
+ generate( builder ) {
+
+ const snippet = super.generate( builder );
+ const arrayType = this.referenceNode.getNodeType( builder );
+ const elementType = this.getNodeType( builder );
+
+ return builder.format( snippet, arrayType, elementType );
+
+ }
+
+}
+
+export default ReferenceElementNode;
diff --git a/src/nodes/accessors/ReferenceNode.js b/src/nodes/accessors/ReferenceNode.js
index 856d39f84d0771..9ccc4350944a8d 100644
--- a/src/nodes/accessors/ReferenceNode.js
+++ b/src/nodes/accessors/ReferenceNode.js
@@ -6,80 +6,11 @@ import { cubeTexture } from './CubeTextureNode.js';
import { buffer } from './BufferNode.js';
import { nodeObject } from '../tsl/TSLBase.js';
import { uniformArray } from './UniformArrayNode.js';
-import ArrayElementNode from '../utils/ArrayElementNode.js';
+import ReferenceElementNode from './ReferenceElementNode.js';
import { warn } from '../../utils.js';
// TODO: Avoid duplicated code and use only ReferenceBaseNode or ReferenceNode
-/**
- * This class is only relevant if the referenced property is array-like.
- * In this case, `ReferenceElementNode` allows to refer to a specific
- * element inside the data structure via an index.
- *
- * @augments ArrayElementNode
- */
-class ReferenceElementNode extends ArrayElementNode {
-
- static get type() {
-
- return 'ReferenceElementNode';
-
- }
-
- /**
- * Constructs a new reference element node.
- *
- * @param {?ReferenceNode} referenceNode - The reference node.
- * @param {Node} indexNode - The index node that defines the element access.
- */
- constructor( referenceNode, indexNode ) {
-
- super( referenceNode, indexNode );
-
- /**
- * Similar to {@link ReferenceNode#reference}, an additional
- * property references to the current node.
- *
- * @type {?ReferenceNode}
- * @default null
- */
- this.referenceNode = referenceNode;
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isReferenceElementNode = true;
-
- }
-
- /**
- * This method is overwritten since the node type is inferred from
- * the uniform type of the reference node.
- *
- * @return {string} The node type.
- */
- generateNodeType() {
-
- return this.referenceNode.uniformType;
-
- }
-
- generate( builder ) {
-
- const snippet = super.generate( builder );
- const arrayType = this.referenceNode.getNodeType( builder );
- const elementType = this.getNodeType( builder );
-
- return builder.format( snippet, arrayType, elementType );
-
- }
-
-}
-
/**
* This type of node establishes a reference to a property of another object.
* In this way, the value of the node is automatically linked to the value of
diff --git a/src/nodes/functions/ToonLightingModel.js b/src/nodes/functions/ToonLightingModel.js
index 25df145f7551b6..8d43e46d11aba9 100644
--- a/src/nodes/functions/ToonLightingModel.js
+++ b/src/nodes/functions/ToonLightingModel.js
@@ -1,7 +1,7 @@
import LightingModel from '../core/LightingModel.js';
import BRDF_Lambert from './BSDF/BRDF_Lambert.js';
import { diffuseColor } from '../core/PropertyNode.js';
-import { normalGeometry } from '../accessors/Normal.js';
+import { normalView } from '../accessors/Normal.js';
import { Fn, float, vec2, vec3 } from '../tsl/TSLBase.js';
import { mix, smoothstep } from '../math/MathNode.js';
import { materialReference } from '../accessors/MaterialReferenceNode.js';
@@ -44,7 +44,7 @@ class ToonLightingModel extends LightingModel {
*/
direct( { lightDirection, lightColor, reflectedLight }, builder ) {
- const irradiance = getGradientIrradiance( { normal: normalGeometry, lightDirection, builder } ).mul( lightColor );
+ const irradiance = getGradientIrradiance( { normal: normalView, lightDirection, builder } ).mul( lightColor );
reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseColor.rgb } ) ) );
diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js
index 965959aa659483..c15b688a9b95f5 100644
--- a/src/renderers/WebGLRenderer.js
+++ b/src/renderers/WebGLRenderer.js
@@ -2264,6 +2264,8 @@ class WebGLRenderer {
uniforms.ambientLightColor.value = lights.state.ambient;
uniforms.lightProbe.value = lights.state.probe;
+ uniforms.sunLights.value = lights.state.sun;
+ uniforms.sunLightShadows.value = lights.state.sunShadow;
uniforms.directionalLights.value = lights.state.directional;
uniforms.directionalLightShadows.value = lights.state.directionalShadow;
uniforms.spotLights.value = lights.state.spot;
@@ -2275,6 +2277,8 @@ class WebGLRenderer {
uniforms.pointLightShadows.value = lights.state.pointShadow;
uniforms.hemisphereLights.value = lights.state.hemi;
+ uniforms.sunShadowMatrix.value = lights.state.sunShadowMatrix;
+ uniforms.sunShadowCascade.value = lights.state.sunShadowCascade;
uniforms.directionalShadowMatrix.value = lights.state.directionalShadowMatrix;
uniforms.spotLightMatrix.value = lights.state.spotLightMatrix;
uniforms.spotLightMap.value = lights.state.spotLightMap;
@@ -2639,6 +2643,12 @@ class WebGLRenderer {
if ( materialProperties.needsLights ) {
// Set shadow map uniforms first to ensure they get the first texture units
+ if ( lights.state.sunShadowMap.length > 0 ) {
+
+ p_uniforms.setValue( _gl, 'sunShadowMap', lights.state.sunShadowMap, textures );
+
+ }
+
if ( lights.state.directionalShadowMap.length > 0 ) {
p_uniforms.setValue( _gl, 'directionalShadowMap', lights.state.directionalShadowMap, textures );
@@ -2818,6 +2828,8 @@ class WebGLRenderer {
uniforms.ambientLightColor.needsUpdate = value;
uniforms.lightProbe.needsUpdate = value;
+ uniforms.sunLights.needsUpdate = value;
+ uniforms.sunLightShadows.needsUpdate = value;
uniforms.directionalLights.needsUpdate = value;
uniforms.directionalLightShadows.needsUpdate = value;
uniforms.pointLights.needsUpdate = value;
diff --git a/src/renderers/common/InspectorBase.js b/src/renderers/common/InspectorBase.js
index b1e61902c7df5a..71c13d63dc7fa0 100644
--- a/src/renderers/common/InspectorBase.js
+++ b/src/renderers/common/InspectorBase.js
@@ -3,7 +3,6 @@ import { EventDispatcher } from '../../core/EventDispatcher.js';
/**
* InspectorBase is the base class for all inspectors.
*
- * @class InspectorBase
* @augments EventDispatcher
*/
class InspectorBase extends EventDispatcher {
diff --git a/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js
index f9af80b4e6692a..0e223525c4838f 100644
--- a/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js
@@ -60,7 +60,7 @@ vec3 geometryClearcoatNormal = vec3( 0.0 );
material.dfg = texture2D( dfgLUT, vec2( material.roughness, dotNVms ) ).rg;
- #if ( NUM_DIR_LIGHTS > 0 || NUM_POINT_LIGHTS > 0 || NUM_SPOT_LIGHTS > 0 )
+ #if ( NUM_SUN_LIGHTS > 0 || NUM_DIR_LIGHTS > 0 || NUM_POINT_LIGHTS > 0 || NUM_SPOT_LIGHTS > 0 )
// Multi-scattering energy compensation for direct lighting
// Based on "Practical Multiple Scattering Compensation for Microfacet Models"
@@ -152,6 +152,32 @@ IncidentLight directLight;
#endif
+#if ( NUM_SUN_LIGHTS > 0 ) && defined( RE_Direct )
+
+ SunLight sunLight;
+ #if defined( USE_SHADOWMAP ) && NUM_SUN_LIGHT_SHADOWS > 0
+ SunLightShadow sunLightShadow;
+ #endif
+
+ #pragma unroll_loop_start
+ for ( int i = 0; i < NUM_SUN_LIGHTS; i ++ ) {
+
+ sunLight = sunLights[ i ];
+
+ getSunLightInfo( sunLight, directLight );
+
+ #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SUN_LIGHT_SHADOWS )
+ sunLightShadow = sunLightShadows[ i ];
+ directLight.color *= ( directLight.visible && receiveShadow ) ? getSunShadow( sunShadowMap[ i ], sunLightShadow, UNROLLED_LOOP_INDEX ) : 1.0;
+ #endif
+
+ RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
+
+ }
+ #pragma unroll_loop_end
+
+#endif
+
#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )
DirectionalLight directionalLight;
diff --git a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
index a335fc2809df13..943d78279cf0b7 100644
--- a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
@@ -76,6 +76,25 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi
}
+#if NUM_SUN_LIGHTS > 0
+
+ struct SunLight {
+ vec3 direction;
+ vec3 color;
+ };
+
+ uniform SunLight sunLights[ NUM_SUN_LIGHTS ];
+
+ void getSunLightInfo( const in SunLight sunLight, out IncidentLight light ) {
+
+ light.color = sunLight.color;
+ light.direction = sunLight.direction;
+ light.visible = true;
+
+ }
+
+#endif
+
#if NUM_DIR_LIGHTS > 0
struct DirectionalLight {
diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js
index dd9e98257cc6ca..7cc8fc00f79457 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js
@@ -13,6 +13,35 @@ export default /* glsl */`
#ifdef USE_SHADOWMAP
+ #if NUM_SUN_LIGHT_SHADOWS > 0
+
+ #if defined( SHADOWMAP_TYPE_PCF )
+
+ uniform sampler2DShadow sunShadowMap[ NUM_SUN_LIGHT_SHADOWS ];
+
+ #else
+
+ uniform sampler2D sunShadowMap[ NUM_SUN_LIGHT_SHADOWS ];
+
+ #endif
+
+ uniform mat4 sunShadowMatrix[ NUM_SUN_LIGHT_SHADOWS * 4 ];
+ uniform vec4 sunShadowCascade[ NUM_SUN_LIGHT_SHADOWS * 4 ];
+ varying vec4 vSunShadowWorldPosition;
+ varying vec3 vSunShadowWorldNormal;
+
+ struct SunLightShadow {
+ float shadowIntensity;
+ float shadowBias;
+ float shadowNormalBias;
+ float shadowRadius;
+ vec2 shadowMapSize;
+ };
+
+ uniform SunLightShadow sunLightShadows[ NUM_SUN_LIGHT_SHADOWS ];
+
+ #endif
+
#if NUM_DIR_LIGHT_SHADOWS > 0
#if defined( SHADOWMAP_TYPE_PCF )
@@ -258,6 +287,48 @@ export default /* glsl */`
#endif
+ #if NUM_SUN_LIGHT_SHADOWS > 0
+
+ float getSunShadow(
+ #if defined( SHADOWMAP_TYPE_PCF )
+ sampler2DShadow shadowMap,
+ #else
+ sampler2D shadowMap,
+ #endif
+ SunLightShadow sunLightShadow,
+ const in int shadowIndex
+ ) {
+
+ vec4 shadowWorldPosition = vec4( vSunShadowWorldPosition.xyz + vSunShadowWorldNormal * sunLightShadow.shadowNormalBias, 1.0 );
+ float viewDepth = vSunShadowWorldPosition.w;
+ int cascadeOffset = shadowIndex * 4;
+
+ float shadow = 1.0;
+
+ // walk the cascades back to front so each fade band can blend with the shadow behind it
+
+ for ( int i = 3; i >= 0; i -- ) {
+
+ // ( begin, end, fade start ) view depths of the cascade
+
+ vec4 cascade = sunShadowCascade[ cascadeOffset + i ];
+
+ if ( viewDepth >= cascade.x && viewDepth < cascade.y ) {
+
+ float cascadeShadow = getShadow( shadowMap, sunLightShadow.shadowMapSize, sunLightShadow.shadowIntensity, sunLightShadow.shadowBias, sunLightShadow.shadowRadius, sunShadowMatrix[ cascadeOffset + i ] * shadowWorldPosition );
+
+ shadow = viewDepth < cascade.z ? cascadeShadow : mix( cascadeShadow, shadow, smoothstep( cascade.z, cascade.y, viewDepth ) );
+
+ }
+
+ }
+
+ return shadow;
+
+ }
+
+ #endif
+
#if NUM_POINT_LIGHT_SHADOWS > 0
#if defined( SHADOWMAP_TYPE_PCF )
diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js
index 75911cdd018b82..4c3199255a3cad 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js
@@ -9,6 +9,15 @@ export default /* glsl */`
#ifdef USE_SHADOWMAP
+ #if NUM_SUN_LIGHT_SHADOWS > 0
+
+ // cascade selection and shadow coordinates are computed per fragment
+
+ varying vec4 vSunShadowWorldPosition;
+ varying vec3 vSunShadowWorldNormal;
+
+ #endif
+
#if NUM_DIR_LIGHT_SHADOWS > 0
uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];
diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js
index b0d1a1556d103d..2f9e04c92e8b02 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js
@@ -1,6 +1,6 @@
export default /* glsl */`
-#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 )
+#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_SUN_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 )
#ifdef HAS_NORMAL
@@ -20,6 +20,13 @@ export default /* glsl */`
#if defined( USE_SHADOWMAP )
+ #if NUM_SUN_LIGHT_SHADOWS > 0
+
+ vSunShadowWorldPosition = vec4( worldPosition.xyz, - mvPosition.z );
+ vSunShadowWorldNormal = shadowWorldNormal;
+
+ #endif
+
#if NUM_DIR_LIGHT_SHADOWS > 0
#pragma unroll_loop_start
diff --git a/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js
index 88f58c0db117bf..752c1a23b4bec6 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js
+++ b/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js
@@ -5,6 +5,21 @@ float getShadowMask() {
#ifdef USE_SHADOWMAP
+ #if NUM_SUN_LIGHT_SHADOWS > 0
+
+ SunLightShadow sunLight;
+
+ #pragma unroll_loop_start
+ for ( int i = 0; i < NUM_SUN_LIGHT_SHADOWS; i ++ ) {
+
+ sunLight = sunLightShadows[ i ];
+ shadow *= receiveShadow ? getSunShadow( sunShadowMap[ i ], sunLight, UNROLLED_LOOP_INDEX ) : 1.0;
+
+ }
+ #pragma unroll_loop_end
+
+ #endif
+
#if NUM_DIR_LIGHT_SHADOWS > 0
DirectionalLightShadow directionalLight;
diff --git a/src/renderers/shaders/UniformsLib.js b/src/renderers/shaders/UniformsLib.js
index 65a10b9641496a..66a52fd870401b 100644
--- a/src/renderers/shaders/UniformsLib.js
+++ b/src/renderers/shaders/UniformsLib.js
@@ -122,6 +122,22 @@ const UniformsLib = {
lightProbe: { value: [] },
+ sunLights: { value: [], properties: {
+ direction: {},
+ color: {}
+ } },
+
+ sunLightShadows: { value: [], properties: {
+ shadowIntensity: 1,
+ shadowBias: {},
+ shadowNormalBias: {},
+ shadowRadius: {},
+ shadowMapSize: {}
+ } },
+
+ sunShadowMatrix: { value: [] },
+ sunShadowCascade: { value: [] },
+
directionalLights: { value: [], properties: {
direction: {},
color: {}
diff --git a/src/renderers/webgl/WebGLLights.js b/src/renderers/webgl/WebGLLights.js
index e24f9266864354..52c5ce19766757 100644
--- a/src/renderers/webgl/WebGLLights.js
+++ b/src/renderers/webgl/WebGLLights.js
@@ -23,6 +23,7 @@ function UniformsCache() {
switch ( light.type ) {
+ case 'SunLight':
case 'DirectionalLight':
uniforms = {
direction: new Vector3(),
@@ -98,6 +99,7 @@ function ShadowUniformsCache() {
switch ( light.type ) {
+ case 'SunLight':
case 'DirectionalLight':
uniforms = {
shadowIntensity: 1,
@@ -165,12 +167,14 @@ function WebGLLights( extensions ) {
version: 0,
hash: {
+ sunLength: - 1,
directionalLength: - 1,
pointLength: - 1,
spotLength: - 1,
rectAreaLength: - 1,
hemiLength: - 1,
+ numSunShadows: - 1,
numDirectionalShadows: - 1,
numPointShadows: - 1,
numSpotShadows: - 1,
@@ -181,6 +185,11 @@ function WebGLLights( extensions ) {
ambient: [ 0, 0, 0 ],
probe: [],
+ sun: [],
+ sunShadow: [],
+ sunShadowMap: [],
+ sunShadowMatrix: [],
+ sunShadowCascade: [],
directional: [],
directionalShadow: [],
directionalShadowMap: [],
@@ -215,6 +224,8 @@ function WebGLLights( extensions ) {
for ( let i = 0; i < 9; i ++ ) state.probe[ i ].set( 0, 0, 0 );
+ let sunLength = 0;
+ let numSunShadows = 0;
let directionalLength = 0;
let pointLength = 0;
let spotLength = 0;
@@ -274,6 +285,44 @@ function WebGLLights( extensions ) {
numLightProbes ++;
+ } else if ( light.isSunLight ) {
+
+ const uniforms = cache.get( light );
+
+ uniforms.color.copy( light.color ).multiplyScalar( light.intensity );
+
+ if ( light.castShadow ) {
+
+ const shadow = light.shadow;
+
+ const shadowUniforms = shadowCache.get( light );
+
+ shadowUniforms.shadowIntensity = shadow.intensity;
+ shadowUniforms.shadowBias = shadow.bias;
+ shadowUniforms.shadowNormalBias = shadow.normalBias;
+ shadowUniforms.shadowRadius = shadow.radius;
+ shadowUniforms.shadowMapSize.copy( shadow.mapSize ).multiply( shadow.getFrameExtents() );
+
+ state.sunShadow[ numSunShadows ] = shadowUniforms;
+ state.sunShadowMap[ numSunShadows ] = shadowMap;
+
+ // four cascades per sun light, matching the sun shadow shader chunks
+
+ for ( let i = 0; i < 4; i ++ ) {
+
+ state.sunShadowMatrix[ numSunShadows * 4 + i ] = shadow.getMatrix( i );
+ state.sunShadowCascade[ numSunShadows * 4 + i ] = shadow._cascadeData[ i ];
+
+ }
+
+ numSunShadows ++;
+
+ }
+
+ state.sun[ sunLength ] = uniforms;
+
+ sunLength ++;
+
} else if ( light.isDirectionalLight ) {
const uniforms = cache.get( light );
@@ -439,42 +488,51 @@ function WebGLLights( extensions ) {
const hash = state.hash;
- if ( hash.directionalLength !== directionalLength ||
+ if ( hash.sunLength !== sunLength ||
+ hash.directionalLength !== directionalLength ||
hash.pointLength !== pointLength ||
hash.spotLength !== spotLength ||
hash.rectAreaLength !== rectAreaLength ||
hash.hemiLength !== hemiLength ||
+ hash.numSunShadows !== numSunShadows ||
hash.numDirectionalShadows !== numDirectionalShadows ||
hash.numPointShadows !== numPointShadows ||
hash.numSpotShadows !== numSpotShadows ||
hash.numSpotMaps !== numSpotMaps ||
hash.numLightProbes !== numLightProbes ) {
+ state.sun.length = sunLength;
state.directional.length = directionalLength;
state.spot.length = spotLength;
state.rectArea.length = rectAreaLength;
state.point.length = pointLength;
state.hemi.length = hemiLength;
+ state.sunShadow.length = numSunShadows;
+ state.sunShadowMap.length = numSunShadows;
+ state.sunShadowMatrix.length = numSunShadows * 4;
+ state.sunShadowCascade.length = numSunShadows * 4;
state.directionalShadow.length = numDirectionalShadows;
state.directionalShadowMap.length = numDirectionalShadows;
+ state.directionalShadowMatrix.length = numDirectionalShadows;
state.pointShadow.length = numPointShadows;
state.pointShadowMap.length = numPointShadows;
state.spotShadow.length = numSpotShadows;
state.spotShadowMap.length = numSpotShadows;
- state.directionalShadowMatrix.length = numDirectionalShadows;
state.pointShadowMatrix.length = numPointShadows;
state.spotLightMatrix.length = numSpotShadows + numSpotMaps - numSpotShadowsWithMaps;
state.spotLightMap.length = numSpotMaps;
state.numSpotLightShadowsWithMaps = numSpotShadowsWithMaps;
state.numLightProbes = numLightProbes;
+ hash.sunLength = sunLength;
hash.directionalLength = directionalLength;
hash.pointLength = pointLength;
hash.spotLength = spotLength;
hash.rectAreaLength = rectAreaLength;
hash.hemiLength = hemiLength;
+ hash.numSunShadows = numSunShadows;
hash.numDirectionalShadows = numDirectionalShadows;
hash.numPointShadows = numPointShadows;
hash.numSpotShadows = numSpotShadows;
@@ -490,6 +548,7 @@ function WebGLLights( extensions ) {
function setupView( lights, camera ) {
+ let sunLength = 0;
let directionalLength = 0;
let pointLength = 0;
let spotLength = 0;
@@ -502,7 +561,16 @@ function WebGLLights( extensions ) {
const light = lights[ i ];
- if ( light.isDirectionalLight ) {
+ if ( light.isSunLight ) {
+
+ const uniforms = state.sun[ sunLength ];
+
+ uniforms.direction.setFromMatrixPosition( light.matrixWorld );
+ uniforms.direction.transformDirection( viewMatrix );
+
+ sunLength ++;
+
+ } else if ( light.isDirectionalLight ) {
const uniforms = state.directional[ directionalLength ];
diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js
index 4d64b338ba5ee4..3bddcdcb9072ba 100644
--- a/src/renderers/webgl/WebGLProgram.js
+++ b/src/renderers/webgl/WebGLProgram.js
@@ -216,6 +216,7 @@ function replaceLightNums( string, parameters ) {
const numSpotLightCoords = parameters.numSpotLightShadows + parameters.numSpotLightMaps - parameters.numSpotLightShadowsWithMaps;
return string
+ .replace( /NUM_SUN_LIGHTS/g, parameters.numSunLights )
.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )
.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )
.replace( /NUM_SPOT_LIGHT_MAPS/g, parameters.numSpotLightMaps )
@@ -223,6 +224,7 @@ function replaceLightNums( string, parameters ) {
.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights )
+ .replace( /NUM_SUN_LIGHT_SHADOWS/g, parameters.numSunLightShadows )
.replace( /NUM_DIR_LIGHT_SHADOWS/g, parameters.numDirLightShadows )
.replace( /NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g, parameters.numSpotLightShadowsWithMaps )
.replace( /NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows )
diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js
index b6a0fceaf49724..1af6f63f716fb9 100644
--- a/src/renderers/webgl/WebGLPrograms.js
+++ b/src/renderers/webgl/WebGLPrograms.js
@@ -337,6 +337,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin
morphTargetsCount: morphTargetsCount,
morphTextureStride: morphTextureStride,
+ numSunLights: lights.sun.length,
numDirLights: lights.directional.length,
numPointLights: lights.point.length,
numSpotLights: lights.spot.length,
@@ -344,6 +345,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin
numRectAreaLights: lights.rectArea.length,
numHemiLights: lights.hemi.length,
+ numSunLightShadows: lights.sunShadowMap.length,
numDirLightShadows: lights.directionalShadowMap.length,
numPointLightShadows: lights.pointShadowMap.length,
numSpotLightShadows: lights.spotShadowMap.length,
@@ -471,12 +473,14 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin
array.push( parameters.sizeAttenuation );
array.push( parameters.morphTargetsCount );
array.push( parameters.morphAttributeCount );
+ array.push( parameters.numSunLights );
array.push( parameters.numDirLights );
array.push( parameters.numPointLights );
array.push( parameters.numSpotLights );
array.push( parameters.numSpotLightMaps );
array.push( parameters.numHemiLights );
array.push( parameters.numRectAreaLights );
+ array.push( parameters.numSunLightShadows );
array.push( parameters.numDirLightShadows );
array.push( parameters.numPointLightShadows );
array.push( parameters.numSpotLightShadows );
diff --git a/src/renderers/webgl/WebGLShadowMap.js b/src/renderers/webgl/WebGLShadowMap.js
index aabef88fadc2f6..dcc5b906355fdc 100644
--- a/src/renderers/webgl/WebGLShadowMap.js
+++ b/src/renderers/webgl/WebGLShadowMap.js
@@ -171,6 +171,7 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
_shadowMapSize.copy( shadow.mapSize );
+ const viewportCount = shadow.getViewportCount();
const shadowFrameExtents = shadow.getFrameExtents();
_shadowMapSize.multiply( shadowFrameExtents );
@@ -278,39 +279,21 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
}
- // For cube render targets (PointLights), render all 6 faces. Otherwise, render once.
- const faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : 1;
+ if ( shadow.map.isWebGLCubeRenderTarget !== true && ( shadow.map.width !== _shadowMapSize.x || shadow.map.height !== _shadowMapSize.y ) ) {
- for ( let face = 0; face < faceCount; face ++ ) {
-
- // For cube render targets, render to each face separately
- if ( shadow.map.isWebGLCubeRenderTarget ) {
-
- renderer.setRenderTarget( shadow.map, face );
- renderer.clear();
-
- } else {
-
- // For 2D render targets, use viewports
- if ( face === 0 ) {
-
- renderer.setRenderTarget( shadow.map );
- renderer.clear();
+ shadow.map.setSize( _shadowMapSize.x, _shadowMapSize.y );
- }
+ }
- const viewport = shadow.getViewport( face );
+ // For cube render targets (PointLights), render all 6 faces. Sun lights
+ // render one atlas viewport per cascade.
+ const faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : viewportCount;
- _viewport.set(
- _viewportSize.x * viewport.x,
- _viewportSize.y * viewport.y,
- _viewportSize.x * viewport.z,
- _viewportSize.y * viewport.w
- );
+ if ( light.isPointLight !== true ) shadow.updateMatrices( light, camera );
- _state.viewport( _viewport );
+ for ( let face = 0; face < faceCount; face ++ ) {
- }
+ const shadowCamera = shadow.getCamera( face );
if ( light.isPointLight ) {
@@ -340,15 +323,40 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
shadow._frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
+ }
+
+ // For cube render targets, render to each face separately
+ if ( shadow.map.isWebGLCubeRenderTarget ) {
+
+ renderer.setRenderTarget( shadow.map, face );
+ renderer.clear();
+
} else {
- shadow.updateMatrices( light );
+ // For 2D render targets, use viewports
+ if ( face === 0 ) {
+
+ renderer.setRenderTarget( shadow.map );
+ renderer.clear();
+
+ }
+
+ const viewport = shadow.getViewport( face );
+
+ _viewport.set(
+ _viewportSize.x * viewport.x,
+ _viewportSize.y * viewport.y,
+ _viewportSize.x * viewport.z,
+ _viewportSize.y * viewport.w
+ );
+
+ _state.viewport( _viewport );
}
- _frustum = shadow.getFrustum();
+ _frustum = shadow.getFrustum( face );
- renderObject( scene, camera, shadow.camera, light, this.type );
+ renderObject( scene, camera, shadowCamera, light, this.type );
}
@@ -393,12 +401,16 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
type: HalfFloatType
} );
+ } else if ( shadow.mapPass.width !== shadow.map.width || shadow.mapPass.height !== shadow.map.height ) {
+
+ shadow.mapPass.setSize( shadow.map.width, shadow.map.height );
+
}
// vertical pass - read from native depth texture
shadowMaterialVertical.uniforms.shadow_pass.value = shadow.map.depthTexture;
- shadowMaterialVertical.uniforms.resolution.value = shadow.mapSize;
+ shadowMaterialVertical.uniforms.resolution.value.set( shadow.map.width, shadow.map.height );
shadowMaterialVertical.uniforms.radius.value = shadow.radius;
renderer.setRenderTarget( shadow.mapPass );
renderer.clear();
@@ -407,7 +419,7 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
// horizontal pass
shadowMaterialHorizontal.uniforms.shadow_pass.value = shadow.mapPass.texture;
- shadowMaterialHorizontal.uniforms.resolution.value = shadow.mapSize;
+ shadowMaterialHorizontal.uniforms.resolution.value.set( shadow.map.width, shadow.map.height );
shadowMaterialHorizontal.uniforms.radius.value = shadow.radius;
renderer.setRenderTarget( shadow.map );
renderer.clear();
diff --git a/test/unit/src/lights/SunLight.tests.js b/test/unit/src/lights/SunLight.tests.js
new file mode 100644
index 00000000000000..4aa1c0eb023dad
--- /dev/null
+++ b/test/unit/src/lights/SunLight.tests.js
@@ -0,0 +1,80 @@
+import { SunLight } from '../../../../src/lights/SunLight.js';
+
+import { Light } from '../../../../src/lights/Light.js';
+import { ObjectLoader } from '../../../../src/loaders/ObjectLoader.js';
+import { Vector3 } from '../../../../src/math/Vector3.js';
+
+export default QUnit.module( 'Lights', () => {
+
+ QUnit.module( 'SunLight', () => {
+
+ // INHERITANCE
+ QUnit.test( 'Extending', ( assert ) => {
+
+ const object = new SunLight();
+ assert.strictEqual(
+ object instanceof Light, true,
+ 'SunLight extends from Light'
+ );
+
+ } );
+
+ // INSTANCING
+ QUnit.test( 'Instancing', ( assert ) => {
+
+ const object = new SunLight();
+ assert.ok( object, 'Can instantiate a SunLight.' );
+
+ } );
+
+ // PROPERTIES
+ QUnit.test( 'type', ( assert ) => {
+
+ const object = new SunLight();
+ assert.ok( object.type === 'SunLight', 'SunLight.type should be SunLight' );
+
+ } );
+
+ QUnit.test( 'shadow', ( assert ) => {
+
+ const object = new SunLight();
+ assert.ok( object.shadow.isSunLightShadow, 'SunLight.shadow is a SunLightShadow' );
+
+ } );
+
+ // PUBLIC
+ QUnit.test( 'isSunLight', ( assert ) => {
+
+ const object = new SunLight();
+ assert.ok( object.isSunLight, 'SunLight.isSunLight should be true' );
+
+ } );
+
+ QUnit.test( 'position', ( assert ) => {
+
+ const object = new SunLight();
+ assert.ok( object.position.distanceTo( new Vector3( 0, 1, 0 ) ) < 1e-12, 'Shines straight down by default' );
+
+ } );
+
+ // OTHERS
+ QUnit.test( 'toJSON', ( assert ) => {
+
+ const light = new SunLight( 0xffaa88, 2 );
+ light.shadow.bias = 10;
+ light.position.setFromSphericalCoords( 1, 0.5, 1.2 );
+ light.updateMatrix();
+
+ const json = light.toJSON();
+ const newLight = new ObjectLoader().parse( json );
+
+ assert.ok( newLight.isSunLight, 'Reloaded light is a SunLight' );
+ assert.ok( newLight.shadow.isSunLightShadow, 'Reloaded shadow is a SunLightShadow' );
+ assert.ok( newLight.position.distanceTo( light.position ) < 1e-6, 'Reloaded light keeps its direction' );
+ assert.smartEqual( newLight.shadow, light.shadow, 'Reloaded shadow is identical to the original one' );
+
+ } );
+
+ } );
+
+} );
diff --git a/test/unit/src/lights/SunLightShadow.tests.js b/test/unit/src/lights/SunLightShadow.tests.js
new file mode 100644
index 00000000000000..44cc1b616d6325
--- /dev/null
+++ b/test/unit/src/lights/SunLightShadow.tests.js
@@ -0,0 +1,271 @@
+import { SunLightShadow } from '../../../../src/lights/SunLightShadow.js';
+
+import { LightShadow } from '../../../../src/lights/LightShadow.js';
+import { ObjectLoader } from '../../../../src/loaders/ObjectLoader.js';
+import { SunLight } from '../../../../src/lights/SunLight.js';
+import { PerspectiveCamera } from '../../../../src/cameras/PerspectiveCamera.js';
+import { OrthographicCamera } from '../../../../src/cameras/OrthographicCamera.js';
+import { Vector3 } from '../../../../src/math/Vector3.js';
+
+export default QUnit.module( 'Lights', () => {
+
+ QUnit.module( 'SunLightShadow', () => {
+
+ // INHERITANCE
+ QUnit.test( 'Extending', ( assert ) => {
+
+ const object = new SunLightShadow();
+ assert.strictEqual(
+ object instanceof LightShadow, true,
+ 'SunLightShadow extends from LightShadow'
+ );
+
+ } );
+
+ // INSTANCING
+ QUnit.test( 'Instancing', ( assert ) => {
+
+ const object = new SunLightShadow();
+ assert.ok( object, 'Can instantiate a SunLightShadow.' );
+
+ } );
+
+ // PUBLIC
+ QUnit.test( 'isSunLightShadow', ( assert ) => {
+
+ const object = new SunLightShadow();
+ assert.ok(
+ object.isSunLightShadow,
+ 'SunLightShadow.isSunLightShadow should be true'
+ );
+
+ } );
+
+ QUnit.test( 'cascades', ( assert ) => {
+
+ const light = new SunLight();
+ const camera = new PerspectiveCamera( 60, 1, 0.1, 100 );
+ const shadow = light.shadow;
+ light.position.setFromSphericalCoords( 1, Math.PI / 2 - 0.9, 0.7 );
+ light.updateMatrixWorld();
+ camera.updateMatrixWorld();
+
+ shadow.camera.far = 80;
+ shadow.updateMatrices( light, camera );
+
+ assert.strictEqual( shadow.getViewportCount(), 4, 'Creates four cascade viewports' );
+ assert.deepEqual( shadow.getFrameExtents().toArray(), [ 2, 2 ], 'Uses a compact atlas layout' );
+ assert.strictEqual( shadow._cascadeSplits.length, 5, 'Creates all cascade split boundaries' );
+ assert.strictEqual( shadow._cascadeSplits[ 0 ], camera.near, 'Starts at the view camera near plane' );
+ assert.strictEqual( shadow._cascadeSplits[ 4 ], shadow.camera.far, 'Ends at the shadow camera far plane' );
+
+ const orthographicCamera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 100 );
+ orthographicCamera.updateMatrixWorld();
+ shadow.updateMatrices( light, orthographicCamera );
+ assert.ok( shadow._cascadeSplits.every( Number.isFinite ), 'Supports an orthographic view camera with a zero near plane' );
+
+ const infiniteCamera = new PerspectiveCamera( 60, 1, 0.1, 100 );
+ infiniteCamera.projectionMatrix.elements[ 10 ] = - 1;
+ infiniteCamera.projectionMatrix.elements[ 14 ] = - 2 * infiniteCamera.near;
+ infiniteCamera.projectionMatrixInverse.copy( infiniteCamera.projectionMatrix ).invert();
+ infiniteCamera.far = Infinity;
+ infiniteCamera.updateMatrixWorld();
+ shadow.camera.far = 80;
+ shadow.updateMatrices( light, infiniteCamera );
+ assert.ok( shadow._matrices.every( matrix => matrix.elements.every( Number.isFinite ) ), 'Supports an infinite-far projection matrix' );
+ assert.strictEqual( shadow._cascadeSplits[ 4 ], shadow.camera.far, 'Falls back to the shadow camera for a non-finite view far plane' );
+
+ } );
+
+ QUnit.test( 'cascade fade', ( assert ) => {
+
+ const light = new SunLight();
+ const camera = new PerspectiveCamera( 60, 1, 0.1, 100 );
+ const shadow = light.shadow;
+ light.position.setFromSphericalCoords( 1, Math.PI / 2 - 0.9, 0.7 );
+ light.updateMatrixWorld();
+ camera.updateMatrixWorld();
+
+ shadow.camera.far = 80;
+ shadow.updateMatrices( light, camera );
+
+ for ( let i = 0; i < 4; i ++ ) {
+
+ const cascade = shadow._cascadeData[ i ];
+
+ assert.strictEqual( cascade.y, shadow._cascadeSplits[ i + 1 ], `Cascade ${i} ends at its split` );
+ assert.ok( cascade.z < cascade.y && cascade.z >= shadow._cascadeSplits[ i ], `Cascade ${i} fades out within its own range` );
+
+ if ( i === 0 ) assert.ok( cascade.x < 0, 'First cascade is open below the near plane' );
+ else assert.strictEqual( cascade.x, shadow._cascadeData[ i - 1 ].z, `Cascade ${i} begins where the previous fade starts` );
+
+ }
+
+ } );
+
+ QUnit.test( 'cascade caster margin', ( assert ) => {
+
+ const light = new SunLight();
+ const camera = new PerspectiveCamera( 60, 1, 0.1, 100 );
+ const shadow = light.shadow;
+ light.position.setFromSphericalCoords( 1, Math.PI / 2 - 0.9, 0.7 );
+ light.updateMatrixWorld();
+ camera.updateMatrixWorld();
+ shadow.camera.far = 100;
+ shadow.updateMatrices( light, camera );
+
+ const lightDirection = light.position.clone().normalize().negate();
+
+ for ( let i = 0; i < 4; i ++ ) {
+
+ const depth = ( shadow._cascadeSplits[ i ] + shadow._cascadeSplits[ i + 1 ] ) * 0.5;
+ const caster = new Vector3( 0, 0, - depth ).addScaledVector( lightDirection, - 90 );
+ assert.ok( shadow.getFrustum( i ).containsPoint( caster ), `Cascade ${i} covers casters above the view frustum` );
+
+ }
+
+ } );
+
+ QUnit.test( 'cascade containment', ( assert ) => {
+
+ const light = new SunLight();
+ const camera = new PerspectiveCamera( 60, 1, 0.01, 0.1 );
+ const shadow = light.shadow;
+ light.position.setFromSphericalCoords( 1, Math.PI / 2 - 0.9, 0.7 );
+ light.updateMatrixWorld();
+ camera.updateMatrixWorld();
+ shadow.camera.far = 1;
+ shadow.updateMatrices( light, camera );
+
+ for ( let i = 0; i < 4; i ++ ) {
+
+ let containsSlice = true;
+
+ for ( const depth of shadow._cascadeSplits.slice( i, i + 2 ) ) {
+
+ const halfHeight = Math.tan( camera.fov * Math.PI / 360 ) * depth;
+
+ for ( const x of [ - 1, 1 ] ) {
+
+ for ( const y of [ - 1, 1 ] ) {
+
+ containsSlice = containsSlice && shadow.getFrustum( i ).containsPoint( new Vector3( x * halfHeight, y * halfHeight, - depth ) );
+
+ }
+
+ }
+
+ }
+
+ assert.ok( containsSlice, `Cascade ${i} contains its view slice` );
+
+ }
+
+ } );
+
+ QUnit.test( 'cascade stabilization containment', ( assert ) => {
+
+ const light = new SunLight();
+ const camera = new PerspectiveCamera( 60, 1, 1, 10 );
+ const shadow = light.shadow;
+ camera.updateMatrixWorld();
+ light.position.setFromSphericalCoords( 1, Math.PI / 2 - 0.6, Math.PI / 4 );
+ light.updateMatrixWorld();
+
+ shadow.mapSize.set( 16, 16 );
+ shadow.updateMatrices( light, camera );
+
+ for ( const depth of shadow._cascadeSplits.slice( 0, 2 ) ) {
+
+ const halfHeight = Math.tan( camera.fov * Math.PI / 360 ) * depth;
+
+ for ( const x of [ - 1, 1 ] ) {
+
+ for ( const y of [ - 1, 1 ] ) {
+
+ const corner = new Vector3( x * halfHeight * camera.aspect, y * halfHeight, - depth ).applyMatrix4( camera.matrixWorld );
+ assert.ok( shadow.getFrustum( 0 ).containsPoint( corner ), 'Texel stabilization keeps the slice corner inside its cascade' );
+
+ }
+
+ }
+
+ }
+
+ } );
+
+ QUnit.test( 'reversed depth cascade containment', ( assert ) => {
+
+ const light = new SunLight();
+ const camera = new PerspectiveCamera( 60, 1, 0.1, 100 );
+ const shadow = light.shadow;
+ light.position.setFromSphericalCoords( 1, Math.PI / 2 - 0.9, 0.7 );
+ light.updateMatrixWorld();
+ camera.updateMatrixWorld();
+
+ for ( const [ viewReversedDepth, shadowReversedDepth ] of [[ false, false ], [ false, true ], [ true, true ]] ) {
+
+ camera._reversedDepth = viewReversedDepth;
+ camera.updateProjectionMatrix();
+ shadow.camera._reversedDepth = shadowReversedDepth;
+ shadow.updateMatrices( light, camera );
+
+ for ( let i = 0; i < 4; i ++ ) {
+
+ const cascadeCamera = shadow.getCamera( i );
+ const depth = ( shadow._cascadeSplits[ i ] + shadow._cascadeSplits[ i + 1 ] ) * 0.5;
+ assert.strictEqual( cascadeCamera.reversedDepth, shadowReversedDepth, `Cascade ${i} uses the shadow map depth convention` );
+ assert.ok( shadow.getMatrix( i ).elements.every( Number.isFinite ), `Cascade ${i} has a finite shadow matrix` );
+ assert.ok( shadow.getFrustum( i ).containsPoint( new Vector3( 0, 0, - depth ) ), `Cascade ${i} contains its view slice` );
+
+ }
+
+ }
+
+ } );
+
+ // OTHERS
+ QUnit.test( 'clone/copy', ( assert ) => {
+
+ const a = new SunLightShadow();
+ const b = new SunLightShadow();
+
+ assert.notDeepEqual( a, b, 'Newly instanced shadows are not equal' );
+
+ const c = a.clone();
+ assert.smartEqual( a, c, 'Shadows are identical after clone()' );
+
+ c.mapSize.set( 1024, 1024 );
+ assert.notDeepEqual( a, c, 'Shadows are different again after change' );
+
+ b.copy( a );
+ assert.smartEqual( a, b, 'Shadows are identical after copy()' );
+
+ b.mapSize.set( 512, 512 );
+ assert.notDeepEqual( a, b, 'Shadows are different again after change' );
+
+ } );
+
+ QUnit.test( 'toJSON', ( assert ) => {
+
+ const light = new SunLight();
+ const shadow = light.shadow;
+
+ shadow.bias = 10;
+ shadow.radius = 5;
+ shadow.mapSize.set( 1024, 1024 );
+
+ const json = light.toJSON();
+ const newLight = new ObjectLoader().parse( json );
+
+ assert.ok( newLight.shadow.isSunLightShadow, 'Reloaded shadow is a SunLightShadow' );
+ assert.smartEqual(
+ newLight.shadow, light.shadow,
+ 'Reloaded shadow is identical to the original one'
+ );
+
+ } );
+
+ } );
+
+} );
diff --git a/test/unit/three.source.unit.js b/test/unit/three.source.unit.js
index d545b5d81c438c..e4bb31ec642a1f 100644
--- a/test/unit/three.source.unit.js
+++ b/test/unit/three.source.unit.js
@@ -131,6 +131,8 @@ import './src/helpers/SpotLightHelper.tests.js';
import './src/lights/AmbientLight.tests.js';
import './src/lights/DirectionalLight.tests.js';
import './src/lights/DirectionalLightShadow.tests.js';
+import './src/lights/SunLight.tests.js';
+import './src/lights/SunLightShadow.tests.js';
import './src/lights/HemisphereLight.tests.js';
import './src/lights/Light.tests.js';
import './src/lights/LightProbe.tests.js';