Sfoglia il codice sorgente

RectAreaLights: Added shadows support (PCSS).

Mr.doob 5 mesi fa
parent
commit
3be6e2e94a

File diff suppressed because it is too large
+ 321 - 26
build/three.cjs


File diff suppressed because it is too large
+ 321 - 26
build/three.core.js


File diff suppressed because it is too large
+ 0 - 0
build/three.core.min.js


File diff suppressed because it is too large
+ 0 - 0
build/three.module.js


File diff suppressed because it is too large
+ 0 - 0
build/three.module.min.js


File diff suppressed because it is too large
+ 0 - 0
build/three.webgpu.js


File diff suppressed because it is too large
+ 0 - 0
build/three.webgpu.min.js


File diff suppressed because it is too large
+ 0 - 0
build/three.webgpu.nodes.js


File diff suppressed because it is too large
+ 0 - 0
build/three.webgpu.nodes.min.js


+ 148 - 0
examples/jsm/helpers/CircleAreaLightHelper.js

@@ -0,0 +1,148 @@
+import {
+	BackSide,
+	BufferGeometry,
+	Float32BufferAttribute,
+	Line,
+	LineBasicMaterial,
+	Mesh,
+	MeshBasicMaterial
+} from 'three';
+
+/**
+ * Creates a visual aid for circle area lights.
+ *
+ * `CircleAreaLightHelper` must be added as a child of the light.
+ *
+ * ```js
+ * const light = new THREE.CircleAreaLight( 0xffffbb, 1.0, 5 );
+ * const helper = new CircleAreaLightHelper( light );
+ * light.add( helper );
+ * ```
+ *
+ * @augments Line
+ * @three_import import { CircleAreaLightHelper } from 'three/addons/helpers/CircleAreaLightHelper.js';
+ */
+class CircleAreaLightHelper extends Line {
+
+	/**
+	 * Constructs a new circle area light helper.
+	 *
+	 * @param {CircleAreaLight} light - The light to visualize.
+	 * @param {number|Color|string} [color] - The helper's color.
+	 * If this is not the set, the helper will take the color of the light.
+	 */
+	constructor( light, color ) {
+
+		// Create circle outline (32 segments for smooth appearance)
+		const segments = 32;
+		const positions = [];
+
+		for ( let i = 0; i <= segments; i ++ ) {
+
+			const angle = ( i / segments ) * Math.PI * 2;
+			positions.push( Math.cos( angle ), Math.sin( angle ), 0 );
+
+		}
+
+		const geometry = new BufferGeometry();
+		geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
+		geometry.computeBoundingSphere();
+
+		const material = new LineBasicMaterial( { fog: false } );
+
+		super( geometry, material );
+
+		/**
+		 * The light to visualize.
+		 *
+		 * @type {CircleAreaLight}
+		 */
+		this.light = light;
+
+		/**
+		 * The helper's color. If `undefined`, the helper will take the color of the light.
+		 *
+		 * @type {number|Color|string|undefined}
+		 */
+		this.color = color;
+
+		this.type = 'CircleAreaLightHelper';
+
+		//
+
+		// Create filled circle mesh (triangulated circle)
+		const positions2 = [];
+
+		// Center vertex
+		positions2.push( 0, 0, 0 );
+
+		// Circle vertices
+		for ( let i = 0; i <= segments; i ++ ) {
+
+			const angle = ( i / segments ) * Math.PI * 2;
+			positions2.push( Math.cos( angle ), Math.sin( angle ), 0 );
+
+		}
+
+		// Create triangle fan indices
+		const indices = [];
+		for ( let i = 0; i < segments; i ++ ) {
+
+			indices.push( 0, i + 1, i + 2 );
+
+		}
+
+		const geometry2 = new BufferGeometry();
+		geometry2.setAttribute( 'position', new Float32BufferAttribute( positions2, 3 ) );
+		geometry2.setIndex( indices );
+		geometry2.computeBoundingSphere();
+
+		this.add( new Mesh( geometry2, new MeshBasicMaterial( { side: BackSide, fog: false } ) ) );
+
+	}
+
+	updateMatrixWorld() {
+
+		this.scale.set( this.light.radius, this.light.radius, 1 );
+
+		if ( this.color !== undefined ) {
+
+			this.material.color.set( this.color );
+			this.children[ 0 ].material.color.set( this.color );
+
+		} else {
+
+			this.material.color.copy( this.light.color ).multiplyScalar( this.light.intensity );
+
+			// prevent hue shift
+			const c = this.material.color;
+			const max = Math.max( c.r, c.g, c.b );
+			if ( max > 1 ) c.multiplyScalar( 1 / max );
+
+			this.children[ 0 ].material.color.copy( this.material.color );
+
+		}
+
+		// ignore world scale on light
+		this.matrixWorld.extractRotation( this.light.matrixWorld ).scale( this.scale ).copyPosition( this.light.matrixWorld );
+
+		this.children[ 0 ].matrixWorld.copy( this.matrixWorld );
+
+	}
+
+	/**
+	 * Frees the GPU-related resources allocated by this instance. Call this
+	 * method whenever this instance is no longer used in your app.
+	 */
+	dispose() {
+
+		this.geometry.dispose();
+		this.material.dispose();
+		this.children[ 0 ].geometry.dispose();
+		this.children[ 0 ].material.dispose();
+
+	}
+
+}
+
+export { CircleAreaLightHelper };

+ 192 - 0
examples/webgl_lights_circlearealight.html

@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - lights - circle area light</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - THREE.CircleAreaLight with Shadows<br/>
+			CircleAreaLight approximates circular light sources using LTC and PCSS
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { CircleAreaLightHelper } from 'three/addons/helpers/CircleAreaLightHelper.js';
+			import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
+
+			let renderer, scene, camera;
+			let stats, meshKnot, meshFloor;
+			let circleLight1, circleLight2, circleLight3;
+			let animateLights = true;
+
+			init();
+
+			function init() {
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animation );
+				renderer.shadowMap.enabled = true;
+				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+				document.body.appendChild( renderer.domElement );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( 0, 5, - 15 );
+
+				scene = new THREE.Scene();
+
+				// CircleAreaLight uses the same LTC textures as RectAreaLight
+				RectAreaLightUniformsLib.init();
+
+				circleLight1 = new THREE.CircleAreaLight( 0xff0000, 5, 4 );
+				circleLight1.position.set( - 5, 5, 5 );
+				circleLight1.lookAt( 0, 0, 0 );
+				circleLight1.castShadow = true;
+				circleLight1.shadow.mapSize.width = 2048;
+				circleLight1.shadow.mapSize.height = 2048;
+				circleLight1.shadow.bias = - 0.001;
+				scene.add( circleLight1 );
+
+				circleLight2 = new THREE.CircleAreaLight( 0x00ff00, 5, 4 );
+				circleLight2.position.set( 0, 5, 5 );
+				circleLight2.lookAt( 0, 0, 0 );
+				circleLight2.castShadow = true;
+				circleLight2.shadow.mapSize.width = 2048;
+				circleLight2.shadow.mapSize.height = 2048;
+				circleLight2.shadow.bias = - 0.001;
+				scene.add( circleLight2 );
+
+				circleLight3 = new THREE.CircleAreaLight( 0x0000ff, 5, 4 );
+				circleLight3.position.set( 5, 5, 5 );
+				circleLight3.lookAt( 0, 0, 0 );
+				circleLight3.castShadow = true;
+				circleLight3.shadow.mapSize.width = 2048;
+				circleLight3.shadow.mapSize.height = 2048;
+				circleLight3.shadow.bias = - 0.001;
+				scene.add( circleLight3 );
+
+				scene.add( new CircleAreaLightHelper( circleLight1 ) );
+				scene.add( new CircleAreaLightHelper( circleLight2 ) );
+				scene.add( new CircleAreaLightHelper( circleLight3 ) );
+
+				const geoFloor = new THREE.BoxGeometry( 2000, 0.1, 2000 );
+				const matStdFloor = new THREE.MeshStandardMaterial( { color: 0xbcbcbc, roughness: 0.1, metalness: 0 } );
+				meshFloor = new THREE.Mesh( geoFloor, matStdFloor );
+				meshFloor.receiveShadow = true;
+				scene.add( meshFloor );
+
+				const geoKnot = new THREE.TorusKnotGeometry( 1.5, 0.5, 200, 16 );
+				const matKnot = new THREE.MeshStandardMaterial( { color: 0xffffff, roughness: 0, metalness: 0 } );
+				meshKnot = new THREE.Mesh( geoKnot, matKnot );
+				meshKnot.position.set( 0, 5, 0 );
+				meshKnot.castShadow = true;
+				meshKnot.receiveShadow = true;
+				scene.add( meshKnot );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.copy( meshKnot.position );
+				controls.update();
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				// GUI
+				const gui = new GUI();
+
+				const params = {
+					animateLights: animateLights
+				};
+
+				gui.add( params, 'animateLights' ).name( 'Animate Lights' ).onChange( ( value ) => {
+
+					animateLights = value;
+
+				} );
+
+				const knotFolder = gui.addFolder( 'Torus Knot Material' );
+				knotFolder.add( meshKnot.material, 'roughness', 0, 1, 0.01 ).name( 'Roughness' );
+				knotFolder.add( meshKnot.material, 'metalness', 0, 1, 0.01 ).name( 'Metalness' );
+				knotFolder.open();
+
+				const floorFolder = gui.addFolder( 'Floor Material' );
+				floorFolder.add( meshFloor.material, 'roughness', 0, 1, 0.01 ).name( 'Roughness' );
+				floorFolder.add( meshFloor.material, 'metalness', 0, 1, 0.01 ).name( 'Metalness' );
+				floorFolder.open();
+
+			}
+
+			function onWindowResize() {
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				camera.aspect = ( window.innerWidth / window.innerHeight );
+				camera.updateProjectionMatrix();
+
+			}
+
+			function animation( time ) {
+
+				meshKnot.rotation.y = time / 1000;
+
+				if ( animateLights ) {
+
+					// Animate the lights in a circular pattern
+					const t = time / 1000;
+					const radius = 6;
+					const height = 5;
+
+					// Light 1 - Red
+					circleLight1.position.x = Math.cos( t ) * radius;
+					circleLight1.position.z = Math.sin( t ) * radius;
+					circleLight1.position.y = height + Math.sin( t * 2 ) * 2;
+					circleLight1.radius = 3 + Math.sin( t * 1.5 ) * 1.5;
+					circleLight1.lookAt( 0, 0, 0 );
+
+					// Light 2 - Green (120 degrees offset)
+					circleLight2.position.x = Math.cos( t + Math.PI * 2 / 3 ) * radius;
+					circleLight2.position.z = Math.sin( t + Math.PI * 2 / 3 ) * radius;
+					circleLight2.position.y = height + Math.sin( ( t + Math.PI * 2 / 3 ) * 2 ) * 2;
+					circleLight2.radius = 3 + Math.sin( ( t + Math.PI * 2 / 3 ) * 1.5 ) * 1.5;
+					circleLight2.lookAt( 0, 0, 0 );
+
+					// Light 3 - Blue (240 degrees offset)
+					circleLight3.position.x = Math.cos( t + Math.PI * 4 / 3 ) * radius;
+					circleLight3.position.z = Math.sin( t + Math.PI * 4 / 3 ) * radius;
+					circleLight3.position.y = height + Math.sin( ( t + Math.PI * 4 / 3 ) * 2 ) * 2;
+					circleLight3.radius = 3 + Math.sin( ( t + Math.PI * 4 / 3 ) * 1.5 ) * 1.5;
+					circleLight3.lookAt( 0, 0, 0 );
+
+				}
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 110 - 7
examples/webgl_lights_rectarealight.html

@@ -9,7 +9,7 @@
 	<body>
 
 		<div id="info">
-			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - THREE.RectAreaLight<br/>
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - THREE.RectAreaLight with Shadows<br/>
 			by <a href="http://github.com/abelnation" target="_blank" rel="noopener">abelnation</a>
 		</div>
 
@@ -27,13 +27,16 @@
 			import * as THREE from 'three';
 
 			import Stats from 'three/addons/libs/stats.module.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 			import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';
 			import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
 
 			let renderer, scene, camera;
-			let stats, meshKnot;
+			let stats, meshKnot, meshFloor;
+			let rectLight1, rectLight2, rectLight3;
+			let animateLights = true;
 
 			init();
 
@@ -43,6 +46,8 @@
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animation );
+				renderer.shadowMap.enabled = true;
+				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 				document.body.appendChild( renderer.domElement );
 
 				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
@@ -52,16 +57,31 @@
 
 				RectAreaLightUniformsLib.init();
 
-				const rectLight1 = new THREE.RectAreaLight( 0xff0000, 5, 4, 10 );
+				rectLight1 = new THREE.RectAreaLight( 0xff0000, 5, 4, 10 );
 				rectLight1.position.set( - 5, 5, 5 );
+				rectLight1.lookAt( 0, 0, 0 );
+				rectLight1.castShadow = true;
+				rectLight1.shadow.mapSize.width = 2048;
+				rectLight1.shadow.mapSize.height = 2048;
+				rectLight1.shadow.bias = 0;
 				scene.add( rectLight1 );
 
-				const rectLight2 = new THREE.RectAreaLight( 0x00ff00, 5, 4, 10 );
+				rectLight2 = new THREE.RectAreaLight( 0x00ff00, 5, 4, 10 );
 				rectLight2.position.set( 0, 5, 5 );
+				rectLight2.lookAt( 0, 0, 0 );
+				rectLight2.castShadow = true;
+				rectLight2.shadow.mapSize.width = 2048;
+				rectLight2.shadow.mapSize.height = 2048;
+				rectLight2.shadow.bias = 0;
 				scene.add( rectLight2 );
 
-				const rectLight3 = new THREE.RectAreaLight( 0x0000ff, 5, 4, 10 );
+				rectLight3 = new THREE.RectAreaLight( 0x0000ff, 5, 4, 10 );
 				rectLight3.position.set( 5, 5, 5 );
+				rectLight3.lookAt( 0, 0, 0 );
+				rectLight3.castShadow = true;
+				rectLight3.shadow.mapSize.width = 2048;
+				rectLight3.shadow.mapSize.height = 2048;
+				rectLight3.shadow.bias = 0;
 				scene.add( rectLight3 );
 
 				scene.add( new RectAreaLightHelper( rectLight1 ) );
@@ -70,13 +90,40 @@
 
 				const geoFloor = new THREE.BoxGeometry( 2000, 0.1, 2000 );
 				const matStdFloor = new THREE.MeshStandardMaterial( { color: 0xbcbcbc, roughness: 0.1, metalness: 0 } );
-				const mshStdFloor = new THREE.Mesh( geoFloor, matStdFloor );
-				scene.add( mshStdFloor );
+				meshFloor = new THREE.Mesh( geoFloor, matStdFloor );
+				meshFloor.receiveShadow = true;
+				scene.add( meshFloor );
+
+				// Add static cubes on the floor
+				const geoCube = new THREE.BoxGeometry( 1, 1, 1 );
+				const matCube = new THREE.MeshStandardMaterial( { color: 0x808080, roughness: 0.5, metalness: 0.2 } );
+
+				const cubePositions = [
+					{ x: - 4, z: - 4 },
+					{ x: 4, z: - 4 },
+					{ x: - 4, z: 4 },
+					{ x: 4, z: 4 },
+					{ x: 0, z: - 6 },
+					{ x: 6, z: 0 },
+					{ x: - 6, z: 0 }
+				];
+
+				cubePositions.forEach( pos => {
+
+					const cube = new THREE.Mesh( geoCube, matCube );
+					cube.position.set( pos.x, 0.55, pos.z );
+					cube.castShadow = true;
+					cube.receiveShadow = true;
+					scene.add( cube );
+
+				} );
 
 				const geoKnot = new THREE.TorusKnotGeometry( 1.5, 0.5, 200, 16 );
 				const matKnot = new THREE.MeshStandardMaterial( { color: 0xffffff, roughness: 0, metalness: 0 } );
 				meshKnot = new THREE.Mesh( geoKnot, matKnot );
 				meshKnot.position.set( 0, 5, 0 );
+				meshKnot.castShadow = true;
+				meshKnot.receiveShadow = true;
 				scene.add( meshKnot );
 
 				const controls = new OrbitControls( camera, renderer.domElement );
@@ -90,6 +137,29 @@
 				stats = new Stats();
 				document.body.appendChild( stats.dom );
 
+				// GUI
+				const gui = new GUI();
+
+				const params = {
+					animateLights: animateLights
+				};
+
+				gui.add( params, 'animateLights' ).name( 'Animate Lights' ).onChange( ( value ) => {
+
+					animateLights = value;
+
+				} );
+
+				const knotFolder = gui.addFolder( 'Torus Knot Material' );
+				knotFolder.add( meshKnot.material, 'roughness', 0, 1, 0.01 ).name( 'Roughness' );
+				knotFolder.add( meshKnot.material, 'metalness', 0, 1, 0.01 ).name( 'Metalness' );
+				knotFolder.open();
+
+				const floorFolder = gui.addFolder( 'Floor Material' );
+				floorFolder.add( meshFloor.material, 'roughness', 0, 1, 0.01 ).name( 'Roughness' );
+				floorFolder.add( meshFloor.material, 'metalness', 0, 1, 0.01 ).name( 'Metalness' );
+				floorFolder.open();
+
 			}
 
 			function onWindowResize() {
@@ -104,6 +174,39 @@
 
 				meshKnot.rotation.y = time / 1000;
 
+				if ( animateLights ) {
+
+					// Animate the lights in a circular pattern
+					const t = time / 1000;
+					const radius = 6;
+					const height = 5;
+
+					// Light 1 - Red
+					rectLight1.position.x = Math.cos( t ) * radius;
+					rectLight1.position.z = Math.sin( t ) * radius;
+					rectLight1.position.y = height + Math.sin( t * 2 ) * 2;
+					rectLight1.width = 4 + Math.sin( t * 1.5 ) * 2;
+					rectLight1.height = 10 + Math.cos( t * 1.3 ) * 4;
+					rectLight1.lookAt( 0, 0, 0 );
+
+					// Light 2 - Green (120 degrees offset)
+					rectLight2.position.x = Math.cos( t + Math.PI * 2 / 3 ) * radius;
+					rectLight2.position.z = Math.sin( t + Math.PI * 2 / 3 ) * radius;
+					rectLight2.position.y = height + Math.sin( ( t + Math.PI * 2 / 3 ) * 2 ) * 2;
+					rectLight2.width = 4 + Math.sin( ( t + Math.PI * 2 / 3 ) * 1.5 ) * 2;
+					rectLight2.height = 10 + Math.cos( ( t + Math.PI * 2 / 3 ) * 1.3 ) * 4;
+					rectLight2.lookAt( 0, 0, 0 );
+
+					// Light 3 - Blue (240 degrees offset)
+					rectLight3.position.x = Math.cos( t + Math.PI * 4 / 3 ) * radius;
+					rectLight3.position.z = Math.sin( t + Math.PI * 4 / 3 ) * radius;
+					rectLight3.position.y = height + Math.sin( ( t + Math.PI * 4 / 3 ) * 2 ) * 2;
+					rectLight3.width = 4 + Math.sin( ( t + Math.PI * 4 / 3 ) * 1.5 ) * 2;
+					rectLight3.height = 10 + Math.cos( ( t + Math.PI * 4 / 3 ) * 1.3 ) * 4;
+					rectLight3.lookAt( 0, 0, 0 );
+
+				}
+
 				renderer.render( scene, camera );
 
 				stats.update();

+ 28 - 0
examples/webgl_pmrem_test.html

@@ -34,6 +34,7 @@
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 			import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
+			import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
 
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
@@ -115,6 +116,8 @@
 
 					} );
 
+				gui.add( { exportGLB: exportGLB }, 'exportGLB' ).name( 'Export GLB' );
+
 			}
 
 			function createObjects() {
@@ -191,6 +194,31 @@
 
 			}
 
+			function exportGLB() {
+
+				const exporter = new GLTFExporter();
+
+				exporter.parse(
+					scene,
+					function ( result ) {
+
+						const blob = new Blob( [ result ], { type: 'application/octet-stream' } );
+						const link = document.createElement( 'a' );
+						link.href = URL.createObjectURL( blob );
+						link.download = 'pmrem_test_scene.glb';
+						link.click();
+
+					},
+					function ( error ) {
+
+						console.error( 'An error occurred during GLB export:', error );
+
+					},
+					{ binary: true }
+				);
+
+			}
+
 			Promise.resolve()
 				.then( init )
 				.then( createObjects )

+ 1 - 0
src/Three.Core.js

@@ -58,6 +58,7 @@ export { AudioLoader } from './loaders/AudioLoader.js';
 export { SpotLight } from './lights/SpotLight.js';
 export { PointLight } from './lights/PointLight.js';
 export { RectAreaLight } from './lights/RectAreaLight.js';
+export { CircleAreaLight } from './lights/CircleAreaLight.js';
 export { HemisphereLight } from './lights/HemisphereLight.js';
 export { DirectionalLight } from './lights/DirectionalLight.js';
 export { AmbientLight } from './lights/AmbientLight.js';

+ 112 - 0
src/lights/CircleAreaLight.js

@@ -0,0 +1,112 @@
+import { Light } from './Light.js';
+import { CircleAreaLightShadow } from './CircleAreaLightShadow.js';
+
+/**
+ * This class emits light uniformly across the face of a circular disc.
+ * This light type can be used to simulate light sources such as round ceiling
+ * lights or circular softboxes.
+ *
+ * Important Notes:
+ *
+ * - Only PBR materials are supported.
+ * - You have to include `RectAreaLightUniformsLib` (`WebGLRenderer`) or `RectAreaLightTexturesLib` (`WebGPURenderer`)
+ * into your app and init the uniforms/textures (CircleAreaLight uses the same LTC textures as RectAreaLight).
+ *
+ * ```js
+ * RectAreaLightUniformsLib.init(); // only relevant for WebGLRenderer
+ * THREE.RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() ); //  only relevant for WebGPURenderer
+ *
+ * const intensity = 1; const radius = 5;
+ * const circleLight = new THREE.CircleAreaLight( 0xffffff, intensity, radius );
+ * circleLight.position.set( 5, 5, 0 );
+ * circleLight.lookAt( 0, 0, 0 );
+ * scene.add( circleLight )
+ * ```
+ *
+ * @augments Light
+ */
+class CircleAreaLight extends Light {
+
+	/**
+	 * Constructs a new circular area light.
+	 *
+	 * @param {(number|Color|string)} [color=0xffffff] - The light's color.
+	 * @param {number} [intensity=1] - The light's strength/intensity.
+	 * @param {number} [radius=5] - The radius of the light.
+	 */
+	constructor( color, intensity, radius = 5 ) {
+
+		super( color, intensity );
+
+		/**
+		 * This flag can be used for type testing.
+		 *
+		 * @type {boolean}
+		 * @readonly
+		 * @default true
+		 */
+		this.isCircleAreaLight = true;
+
+		this.type = 'CircleAreaLight';
+
+		/**
+		 * The radius of the light.
+		 *
+		 * @type {number}
+		 * @default 5
+		 */
+		this.radius = radius;
+
+		/**
+		 * The shadow configuration.
+		 *
+		 * @type {CircleAreaLightShadow}
+		 */
+		this.shadow = new CircleAreaLightShadow();
+
+	}
+
+	/**
+	 * The light's power. Power is the luminous power of the light measured in lumens (lm).
+	 * Changing the power will also change the light's intensity.
+	 *
+	 * @type {number}
+	 */
+	get power() {
+
+		// compute the light's luminous power (in lumens) from its intensity (in nits)
+		// for a circular disc: area = π * r²
+		return this.intensity * Math.PI * this.radius * this.radius * Math.PI;
+
+	}
+
+	set power( power ) {
+
+		// set the light's intensity (in nits) from the desired luminous power (in lumens)
+		this.intensity = power / ( Math.PI * this.radius * this.radius * Math.PI );
+
+	}
+
+	copy( source ) {
+
+		super.copy( source );
+
+		this.radius = source.radius;
+
+		return this;
+
+	}
+
+	toJSON( meta ) {
+
+		const data = super.toJSON( meta );
+
+		data.object.radius = this.radius;
+
+		return data;
+
+	}
+
+}
+
+export { CircleAreaLight };

+ 97 - 0
src/lights/CircleAreaLightShadow.js

@@ -0,0 +1,97 @@
+import { LightShadow } from './LightShadow.js';
+import { OrthographicCamera } from '../cameras/OrthographicCamera.js';
+import { Matrix4 } from '../math/Matrix4.js';
+import { Vector3 } from '../math/Vector3.js';
+
+const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
+const _lightPositionWorld = /*@__PURE__*/ new Vector3();
+
+/**
+ * Represents the shadow configuration of circular area lights.
+ *
+ * @augments LightShadow
+ */
+class CircleAreaLightShadow extends LightShadow {
+
+	/**
+	 * Constructs a new circular area 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.isCircleAreaLightShadow = true;
+
+	}
+
+	updateMatrices( light ) {
+
+		const camera = this.camera;
+		const shadowMatrix = this.matrix;
+
+		const radius = light.radius;
+
+		// Expand the orthographic camera frustum to cover the 180-degree hemisphere
+		// that the CircleAreaLight illuminates. We multiply by the far distance to create
+		// a frustum that expands with distance, covering a much wider area.
+		const farDistance = camera.far;
+		const frustumScale = farDistance / 10.0; // Scale frustum based on distance
+
+		// Size the orthographic camera to match the circular light dimensions
+		// Use radius for all sides to create a square frustum that contains the circle
+		if ( radius !== Math.abs( camera.right ) ) {
+
+			camera.left = - radius - frustumScale;
+			camera.right = radius + frustumScale;
+			camera.top = radius + frustumScale;
+			camera.bottom = - radius - frustumScale;
+
+			camera.updateProjectionMatrix();
+
+		}
+
+		// Position and orient the shadow camera based on the light's transform
+		_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
+		camera.position.copy( _lightPositionWorld );
+
+		// CircleAreaLight uses its own rotation (via lookAt), not a target
+		camera.quaternion.copy( light.quaternion );
+		camera.updateMatrixWorld();
+
+		_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
+		this._frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
+
+		if ( camera.reversedDepth ) {
+
+			shadowMatrix.set(
+				0.5, 0.0, 0.0, 0.5,
+				0.0, 0.5, 0.0, 0.5,
+				0.0, 0.0, 1.0, 0.0,
+				0.0, 0.0, 0.0, 1.0
+			);
+
+		} else {
+
+			shadowMatrix.set(
+				0.5, 0.0, 0.0, 0.5,
+				0.0, 0.5, 0.0, 0.5,
+				0.0, 0.0, 0.5, 0.5,
+				0.0, 0.0, 0.0, 1.0
+			);
+
+		}
+
+		shadowMatrix.multiply( _projScreenMatrix );
+
+	}
+
+}
+
+export { CircleAreaLightShadow };

+ 8 - 1
src/lights/RectAreaLight.js

@@ -1,4 +1,5 @@
 import { Light } from './Light.js';
+import { RectAreaLightShadow } from './RectAreaLightShadow.js';
 
 /**
  * This class emits light uniformly across the face a rectangular plane.
@@ -7,7 +8,6 @@ import { Light } from './Light.js';
  *
  * Important Notes:
  *
- * - There is no shadow support.
  * - Only PBR materials are supported.
  * - You have to include `RectAreaLightUniformsLib` (`WebGLRenderer`) or `RectAreaLightTexturesLib` (`WebGPURenderer`)
  * into your app and init the uniforms/textures.
@@ -66,6 +66,13 @@ class RectAreaLight extends Light {
 		 */
 		this.height = height;
 
+		/**
+		 * The shadow configuration.
+		 *
+		 * @type {RectAreaLightShadow}
+		 */
+		this.shadow = new RectAreaLightShadow();
+
 	}
 
 	/**

+ 96 - 0
src/lights/RectAreaLightShadow.js

@@ -0,0 +1,96 @@
+import { LightShadow } from './LightShadow.js';
+import { OrthographicCamera } from '../cameras/OrthographicCamera.js';
+import { Matrix4 } from '../math/Matrix4.js';
+import { Vector3 } from '../math/Vector3.js';
+
+const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
+const _lightPositionWorld = /*@__PURE__*/ new Vector3();
+
+/**
+ * Represents the shadow configuration of rectangular area lights.
+ *
+ * @augments LightShadow
+ */
+class RectAreaLightShadow extends LightShadow {
+
+	/**
+	 * Constructs a new rectangular area 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.isRectAreaLightShadow = true;
+
+	}
+
+	updateMatrices( light ) {
+
+		const camera = this.camera;
+		const shadowMatrix = this.matrix;
+
+		const width = light.width * 0.5;
+		const height = light.height * 0.5;
+
+		// Expand the orthographic camera frustum to cover the 180-degree hemisphere
+		// that the RectAreaLight illuminates. We multiply by the far distance to create
+		// a frustum that expands with distance, covering a much wider area.
+		const farDistance = camera.far;
+		const frustumScale = farDistance / 10.0; // Scale frustum based on distance
+
+		if ( width !== Math.abs( camera.right ) || height !== Math.abs( camera.top ) ) {
+
+			camera.left = - width - frustumScale;
+			camera.right = width + frustumScale;
+			camera.top = height + frustumScale;
+			camera.bottom = - height - frustumScale;
+
+			camera.updateProjectionMatrix();
+
+		}
+
+		// Position and orient the shadow camera based on the light's transform
+		_lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
+		camera.position.copy( _lightPositionWorld );
+
+		// RectAreaLight uses its own rotation (via lookAt), not a target
+		camera.quaternion.copy( light.quaternion );
+		camera.updateMatrixWorld();
+
+		_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
+		this._frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
+
+		if ( camera.reversedDepth ) {
+
+			shadowMatrix.set(
+				0.5, 0.0, 0.0, 0.5,
+				0.0, 0.5, 0.0, 0.5,
+				0.0, 0.0, 1.0, 0.0,
+				0.0, 0.0, 0.0, 1.0
+			);
+
+		} else {
+
+			shadowMatrix.set(
+				0.5, 0.0, 0.0, 0.5,
+				0.0, 0.5, 0.0, 0.5,
+				0.0, 0.0, 0.5, 0.5,
+				0.0, 0.0, 0.0, 1.0
+			);
+
+		}
+
+		shadowMatrix.multiply( _projScreenMatrix );
+
+	}
+
+}
+
+export { RectAreaLightShadow };

+ 7 - 1
src/renderers/WebGLRenderer.js

@@ -2127,6 +2127,9 @@ class WebGLRenderer {
 				uniforms.spotLights.value = lights.state.spot;
 				uniforms.spotLightShadows.value = lights.state.spotShadow;
 				uniforms.rectAreaLights.value = lights.state.rectArea;
+				uniforms.rectAreaLightShadows.value = lights.state.rectAreaShadow;
+				uniforms.circleAreaLights.value = lights.state.circleArea;
+				uniforms.circleAreaLightShadows.value = lights.state.circleAreaShadow;
 				uniforms.ltc_1.value = lights.state.rectAreaLTC1;
 				uniforms.ltc_2.value = lights.state.rectAreaLTC2;
 				uniforms.pointLights.value = lights.state.point;
@@ -2140,7 +2143,10 @@ class WebGLRenderer {
 				uniforms.spotLightMap.value = lights.state.spotLightMap;
 				uniforms.pointShadowMap.value = lights.state.pointShadowMap;
 				uniforms.pointShadowMatrix.value = lights.state.pointShadowMatrix;
-				// TODO (abelnation): add area lights shadow info to uniforms
+				uniforms.rectAreaShadowMap.value = lights.state.rectAreaShadowMap;
+				uniforms.rectAreaShadowMatrix.value = lights.state.rectAreaShadowMatrix;
+				uniforms.circleAreaShadowMap.value = lights.state.circleAreaShadowMap;
+				uniforms.circleAreaShadowMatrix.value = lights.state.circleAreaShadowMatrix;
 
 			}
 

+ 33 - 0
src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js

@@ -155,11 +155,20 @@ IncidentLight directLight;
 #if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )
 
 	RectAreaLight rectAreaLight;
+	#if defined( USE_SHADOWMAP ) && NUM_RECT_AREA_LIGHT_SHADOWS > 0
+	RectAreaLightShadow rectAreaLightShadow;
+	#endif
 
 	#pragma unroll_loop_start
 	for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {
 
 		rectAreaLight = rectAreaLights[ i ];
+
+		#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_RECT_AREA_LIGHT_SHADOWS )
+		rectAreaLightShadow = rectAreaLightShadows[ i ];
+		rectAreaLight.color *= ( receiveShadow ) ? getShadowRectAreaPCSS( rectAreaShadowMap[ i ], rectAreaLightShadow.shadowMapSize, rectAreaLightShadow.shadowIntensity, rectAreaLightShadow.shadowBias, rectAreaLightShadow.lightSize, vRectAreaShadowCoord[ i ] ) : 1.0;
+		#endif
+
 		RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
 
 	}
@@ -167,6 +176,30 @@ IncidentLight directLight;
 
 #endif
 
+#if ( NUM_CIRCLE_AREA_LIGHTS > 0 ) && defined( RE_Direct_CircleArea )
+
+	CircleAreaLight circleAreaLight;
+	#if defined( USE_SHADOWMAP ) && NUM_CIRCLE_AREA_LIGHT_SHADOWS > 0
+	CircleAreaLightShadow circleAreaLightShadow;
+	#endif
+
+	#pragma unroll_loop_start
+	for ( int i = 0; i < NUM_CIRCLE_AREA_LIGHTS; i ++ ) {
+
+		circleAreaLight = circleAreaLights[ i ];
+
+		#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_CIRCLE_AREA_LIGHT_SHADOWS )
+		circleAreaLightShadow = circleAreaLightShadows[ i ];
+		circleAreaLight.color *= ( receiveShadow ) ? getShadowRectAreaPCSS( circleAreaShadowMap[ i ], circleAreaLightShadow.shadowMapSize, circleAreaLightShadow.shadowIntensity, circleAreaLightShadow.shadowBias, circleAreaLightShadow.lightSize, vCircleAreaShadowCoord[ i ] ) : 1.0;
+		#endif
+
+		RE_Direct_CircleArea( circleAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
+
+	}
+	#pragma unroll_loop_end
+
+#endif
+
 #if defined( RE_IndirectDiffuse )
 
 	vec3 iblIrradiance = vec3( 0.0 );

+ 23 - 5
src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js

@@ -170,6 +170,15 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi
 #endif
 
 
+#if NUM_RECT_AREA_LIGHTS > 0 || NUM_CIRCLE_AREA_LIGHTS > 0
+
+	// Pre-computed values of LinearTransformedCosine approximation of BRDF
+	// BRDF approximation Texture is 64x64
+	uniform sampler2D ltc_1; // RGBA Float
+	uniform sampler2D ltc_2; // RGBA Float
+
+#endif
+
 #if NUM_RECT_AREA_LIGHTS > 0
 
 	struct RectAreaLight {
@@ -179,15 +188,24 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi
 		vec3 halfHeight;
 	};
 
-	// Pre-computed values of LinearTransformedCosine approximation of BRDF
-	// BRDF approximation Texture is 64x64
-	uniform sampler2D ltc_1; // RGBA Float
-	uniform sampler2D ltc_2; // RGBA Float
-
 	uniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];
 
 #endif
 
+#if NUM_CIRCLE_AREA_LIGHTS > 0
+
+	struct CircleAreaLight {
+		vec3 color;
+		vec3 position;
+		vec3 axisU;
+		vec3 axisV;
+		float radius;
+	};
+
+	uniform CircleAreaLight circleAreaLights[ NUM_CIRCLE_AREA_LIGHTS ];
+
+#endif
+
 
 #if NUM_HEMI_LIGHTS > 0
 

+ 102 - 0
src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js

@@ -310,6 +310,52 @@ vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in m
 
 }
 
+// Optimized LTC evaluation for 8-vertex polygon (octagon)
+// Unrolled loop version for better performance
+vec3 LTC_Evaluate_Octagon( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 vertices[ 8 ] ) {
+
+	// bail if point is on back side of plane of light
+	vec3 v1 = vertices[ 1 ] - vertices[ 0 ];
+	vec3 v2 = vertices[ 7 ] - vertices[ 0 ];
+	vec3 lightNormal = cross( v1, v2 );
+
+	if( dot( lightNormal, P - vertices[ 0 ] ) < 0.0 ) return vec3( 0.0 );
+
+	// construct orthonormal basis around N
+	vec3 T1, T2;
+	T1 = normalize( V - N * dot( V, N ) );
+	T2 = - cross( N, T1 );
+
+	// compute transform
+	mat3 mat = mInv * transpose( mat3( T1, T2, N ) );
+
+	// transform and project all vertices (unrolled for performance)
+	vec3 L0 = normalize( mat * ( vertices[ 0 ] - P ) );
+	vec3 L1 = normalize( mat * ( vertices[ 1 ] - P ) );
+	vec3 L2 = normalize( mat * ( vertices[ 2 ] - P ) );
+	vec3 L3 = normalize( mat * ( vertices[ 3 ] - P ) );
+	vec3 L4 = normalize( mat * ( vertices[ 4 ] - P ) );
+	vec3 L5 = normalize( mat * ( vertices[ 5 ] - P ) );
+	vec3 L6 = normalize( mat * ( vertices[ 6 ] - P ) );
+	vec3 L7 = normalize( mat * ( vertices[ 7 ] - P ) );
+
+	// calculate vector form factor for all 8 edges (unrolled)
+	vec3 vectorFormFactor = LTC_EdgeVectorFormFactor( L0, L1 );
+	vectorFormFactor += LTC_EdgeVectorFormFactor( L1, L2 );
+	vectorFormFactor += LTC_EdgeVectorFormFactor( L2, L3 );
+	vectorFormFactor += LTC_EdgeVectorFormFactor( L3, L4 );
+	vectorFormFactor += LTC_EdgeVectorFormFactor( L4, L5 );
+	vectorFormFactor += LTC_EdgeVectorFormFactor( L5, L6 );
+	vectorFormFactor += LTC_EdgeVectorFormFactor( L6, L7 );
+	vectorFormFactor += LTC_EdgeVectorFormFactor( L7, L0 );
+
+	// adjust for horizon clipping
+	float result = LTC_ClippedSphereFormFactor( vectorFormFactor );
+
+	return vec3( result );
+
+}
+
 // End Rect Area Light
 
 #if defined( USE_SHEEN )
@@ -541,8 +587,64 @@ void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradia
 
 }
 
+#if NUM_CIRCLE_AREA_LIGHTS > 0
+
+	void RE_Direct_CircleArea_Physical( const in CircleAreaLight circleAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {
+
+		// CircleAreaLight approximated as octagon (8-sided polygon)
+		// Uses reversed (CW) winding order to make light emit in -Z direction
+
+		vec3 N = geometryNormal;
+		vec3 V = geometryViewDir;
+		vec3 P = geometryPosition;
+		vec3 lightPos = circleAreaLight.position;
+		vec3 axisU = circleAreaLight.axisU;
+		vec3 axisV = circleAreaLight.axisV;
+		vec3 lightColor = circleAreaLight.color;
+
+		// LTC for specular
+		vec2 uv = LTC_Uv( N, V, material.roughness );
+		vec4 t1 = texture2D( ltc_1, uv );
+		vec4 t2 = texture2D( ltc_2, uv );
+
+		mat3 mInv = mat3(
+			vec3( t1.x, 0, t1.y ),
+			vec3( 0, 1, 0 ),
+			vec3( t1.z, 0, t1.w )
+		);
+
+		vec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );
+
+		// Approximate circle as octagon - compute vertices in clockwise order
+		// Using cos/sin values for octagon (8 vertices at 45 degree intervals)
+		// cos(45°) = sin(45°) = sqrt(2)/2 ≈ 0.7071067811865476
+		const float c = 0.7071067811865476;
+
+		// Pre-compute diagonal vectors for efficiency
+		vec3 d1 = ( axisU + axisV ) * c; // diagonal at 45°
+		vec3 d2 = ( axisV - axisU ) * c; // diagonal at 135°
+
+		vec3 vertices[ 8 ];
+		vertices[ 0 ] = lightPos + axisU;
+		vertices[ 1 ] = lightPos + d1;
+		vertices[ 2 ] = lightPos + axisV;
+		vertices[ 3 ] = lightPos + d2;
+		vertices[ 4 ] = lightPos - axisU;
+		vertices[ 5 ] = lightPos - d1;
+		vertices[ 6 ] = lightPos - axisV;
+		vertices[ 7 ] = lightPos - d2;
+
+		// Evaluate octagon as a single polygon (much faster than 8 triangles)
+		reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate_Octagon( N, V, P, mInv, vertices );
+		reflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate_Octagon( N, V, P, mat3( 1.0 ), vertices );
+
+	}
+
+#endif
+
 #define RE_Direct				RE_Direct_Physical
 #define RE_Direct_RectArea		RE_Direct_RectArea_Physical
+#define RE_Direct_CircleArea	RE_Direct_CircleArea_Physical
 #define RE_IndirectDiffuse		RE_IndirectDiffuse_Physical
 #define RE_IndirectSpecular		RE_IndirectSpecular_Physical
 

+ 164 - 0
src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js

@@ -65,6 +65,42 @@ export default /* glsl */`
 
 	#endif
 
+	#if NUM_RECT_AREA_LIGHT_SHADOWS > 0
+
+		uniform sampler2D rectAreaShadowMap[ NUM_RECT_AREA_LIGHT_SHADOWS ];
+		varying vec4 vRectAreaShadowCoord[ NUM_RECT_AREA_LIGHT_SHADOWS ];
+
+		struct RectAreaLightShadow {
+			float shadowIntensity;
+			float shadowBias;
+			float shadowNormalBias;
+			float shadowRadius;
+			vec2 shadowMapSize;
+			float lightSize;
+		};
+
+		uniform RectAreaLightShadow rectAreaLightShadows[ NUM_RECT_AREA_LIGHT_SHADOWS ];
+
+	#endif
+
+	#if NUM_CIRCLE_AREA_LIGHT_SHADOWS > 0
+
+		uniform sampler2D circleAreaShadowMap[ NUM_CIRCLE_AREA_LIGHT_SHADOWS ];
+		varying vec4 vCircleAreaShadowCoord[ NUM_CIRCLE_AREA_LIGHT_SHADOWS ];
+
+		struct CircleAreaLightShadow {
+			float shadowIntensity;
+			float shadowBias;
+			float shadowNormalBias;
+			float shadowRadius;
+			vec2 shadowMapSize;
+			float lightSize;
+		};
+
+		uniform CircleAreaLightShadow circleAreaLightShadows[ NUM_CIRCLE_AREA_LIGHT_SHADOWS ];
+
+	#endif
+
 	float texture2DCompare( sampler2D depths, vec2 uv, float compare ) {
 
 		float depth = unpackRGBAToDepth( texture2D( depths, uv ) );
@@ -213,6 +249,134 @@ export default /* glsl */`
 
 	}
 
+	// PCSS (Percentage-Closer Soft Shadows) for RectAreaLight and CircleAreaLight
+	#if NUM_RECT_AREA_LIGHT_SHADOWS > 0 || NUM_CIRCLE_AREA_LIGHT_SHADOWS > 0
+
+		#define BLOCKER_SEARCH_NUM_SAMPLES 32
+		#define PCSS_NUM_SAMPLES 32
+
+		// Poisson disk samples for blocker search and PCF (32 samples for high quality)
+		const vec2 poissonDisk[32] = vec2[](
+			vec2( -0.94201624, -0.39906216 ),
+			vec2( 0.94558609, -0.76890725 ),
+			vec2( -0.094184101, -0.92938870 ),
+			vec2( 0.34495938, 0.29387760 ),
+			vec2( -0.91588581, 0.45771432 ),
+			vec2( -0.81544232, -0.87912464 ),
+			vec2( -0.38277543, 0.27676845 ),
+			vec2( 0.97484398, 0.75648379 ),
+			vec2( 0.44323325, -0.97511554 ),
+			vec2( 0.53742981, -0.47373420 ),
+			vec2( -0.26496911, -0.41893023 ),
+			vec2( 0.79197514, 0.19090188 ),
+			vec2( -0.24188840, 0.99706507 ),
+			vec2( -0.81409955, 0.91437590 ),
+			vec2( 0.19984126, 0.78641367 ),
+			vec2( 0.14383161, -0.14100790 ),
+			vec2( -0.65607356, 0.08979656 ),
+			vec2( 0.51081722, 0.54806948 ),
+			vec2( 0.01330001, 0.61580825 ),
+			vec2( -0.43596888, -0.68276507 ),
+			vec2( 0.68866766, -0.24345277 ),
+			vec2( -0.11169554, 0.36159474 ),
+			vec2( 0.31261522, -0.30461493 ),
+			vec2( -0.46893163, 0.68088233 ),
+			vec2( 0.19000651, -0.61041021 ),
+			vec2( -0.57065642, -0.18034465 ),
+			vec2( 0.73607671, 0.29485054 ),
+			vec2( -0.25664163, -0.13645098 ),
+			vec2( 0.05781871, -0.00412393 ),
+			vec2( -0.03324192, -0.40658840 ),
+			vec2( 0.42046776, 0.08142974 ),
+			vec2( -0.20444609, 0.20162514 )
+		);
+
+		// Search for blockers in the shadow map
+		float findBlocker( sampler2D shadowMap, vec2 uv, float zReceiver, vec2 searchWidth ) {
+
+			float blockerDepthSum = 0.0;
+			float numBlockers = 0.0;
+
+			for ( int i = 0; i < BLOCKER_SEARCH_NUM_SAMPLES; i++ ) {
+
+				vec2 offset = poissonDisk[i] * searchWidth;
+				float shadowMapDepth = unpackRGBAToDepth( texture2D( shadowMap, uv + offset ) );
+
+				if ( shadowMapDepth < zReceiver ) {
+
+					blockerDepthSum += shadowMapDepth;
+					numBlockers += 1.0;
+
+				}
+
+			}
+
+			if ( numBlockers == 0.0 ) return -1.0;
+
+			return blockerDepthSum / numBlockers;
+
+		}
+
+		// PCF with variable filter size
+		float PCF_Filter( sampler2D shadowMap, vec2 uv, float zReceiver, vec2 filterRadius ) {
+
+			float sum = 0.0;
+
+			for ( int i = 0; i < PCSS_NUM_SAMPLES; i++ ) {
+
+				vec2 offset = poissonDisk[i] * filterRadius;
+				sum += texture2DCompare( shadowMap, uv + offset, zReceiver );
+
+			}
+
+			return sum / float( PCSS_NUM_SAMPLES );
+
+		}
+
+		// PCSS shadow for RectAreaLight with soft penumbra
+		float getShadowRectAreaPCSS( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float lightSize, vec4 shadowCoord ) {
+
+			float shadow = 1.0;
+
+			shadowCoord.xyz /= shadowCoord.w;
+			shadowCoord.z += shadowBias;
+
+			bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;
+			bool frustumTest = inFrustum && shadowCoord.z <= 1.0;
+
+			if ( frustumTest ) {
+
+				vec2 texelSize = vec2( 1.0 ) / shadowMapSize;
+
+				// Step 1: Blocker search
+				// Search radius proportional to light size
+				vec2 searchWidth = texelSize * lightSize;
+				float avgBlockerDepth = findBlocker( shadowMap, shadowCoord.xy, shadowCoord.z, searchWidth );
+
+				// If no blocker found, fully lit
+				if ( avgBlockerDepth == -1.0 ) {
+
+					return 1.0;
+
+				}
+
+				// Step 2: Penumbra size calculation
+				// penumbra = (receiver - blocker) * lightSize / blocker
+				float penumbraSize = ( shadowCoord.z - avgBlockerDepth ) * lightSize / avgBlockerDepth;
+				penumbraSize = max( penumbraSize, 0.0 );
+
+				// Step 3: Variable-size PCF filtering
+				vec2 filterRadius = texelSize * penumbraSize;
+				shadow = PCF_Filter( shadowMap, shadowCoord.xy, shadowCoord.z, filterRadius );
+
+			}
+
+			return mix( 1.0, shadow, shadowIntensity );
+
+		}
+
+	#endif
+
 	// cubeToUV() maps a 3D direction vector suitable for cube texture mapping to a 2D
 	// vector suitable for 2D texture mapping. This code uses the following layout for the
 	// 2D texture:

+ 32 - 4
src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js

@@ -59,13 +59,41 @@ export default /* glsl */`
 
 	#endif
 
-	/*
-	#if NUM_RECT_AREA_LIGHTS > 0
+	#if NUM_RECT_AREA_LIGHT_SHADOWS > 0
 
-		// TODO (abelnation): uniforms for area light shadows
+		uniform mat4 rectAreaShadowMatrix[ NUM_RECT_AREA_LIGHT_SHADOWS ];
+		varying vec4 vRectAreaShadowCoord[ NUM_RECT_AREA_LIGHT_SHADOWS ];
+
+		struct RectAreaLightShadow {
+			float shadowIntensity;
+			float shadowBias;
+			float shadowNormalBias;
+			float shadowRadius;
+			vec2 shadowMapSize;
+			float lightSize;
+		};
+
+		uniform RectAreaLightShadow rectAreaLightShadows[ NUM_RECT_AREA_LIGHT_SHADOWS ];
+
+	#endif
+
+	#if NUM_CIRCLE_AREA_LIGHT_SHADOWS > 0
+
+		uniform mat4 circleAreaShadowMatrix[ NUM_CIRCLE_AREA_LIGHT_SHADOWS ];
+		varying vec4 vCircleAreaShadowCoord[ NUM_CIRCLE_AREA_LIGHT_SHADOWS ];
+
+		struct CircleAreaLightShadow {
+			float shadowIntensity;
+			float shadowBias;
+			float shadowNormalBias;
+			float shadowRadius;
+			vec2 shadowMapSize;
+			float lightSize;
+		};
+
+		uniform CircleAreaLightShadow circleAreaLightShadows[ NUM_CIRCLE_AREA_LIGHT_SHADOWS ];
 
 	#endif
-	*/
 
 #endif
 `;

+ 23 - 5
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_POINT_LIGHT_SHADOWS > 0 || NUM_RECT_AREA_LIGHT_SHADOWS > 0 || NUM_CIRCLE_AREA_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 )
 
 	// Offsetting the position used for querying occlusion along the world normal can be used to reduce shadow acne.
 	vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );
@@ -36,13 +36,31 @@ export default /* glsl */`
 
 	#endif
 
-	/*
-	#if NUM_RECT_AREA_LIGHTS > 0
+	#if NUM_RECT_AREA_LIGHT_SHADOWS > 0
 
-		// TODO (abelnation): update vAreaShadowCoord with area light info
+		#pragma unroll_loop_start
+		for ( int i = 0; i < NUM_RECT_AREA_LIGHT_SHADOWS; i ++ ) {
+
+			shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * rectAreaLightShadows[ i ].shadowNormalBias, 0 );
+			vRectAreaShadowCoord[ i ] = rectAreaShadowMatrix[ i ] * shadowWorldPosition;
+
+		}
+		#pragma unroll_loop_end
+
+	#endif
+
+	#if NUM_CIRCLE_AREA_LIGHT_SHADOWS > 0
+
+		#pragma unroll_loop_start
+		for ( int i = 0; i < NUM_CIRCLE_AREA_LIGHT_SHADOWS; i ++ ) {
+
+			shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * circleAreaLightShadows[ i ].shadowNormalBias, 0 );
+			vCircleAreaShadowCoord[ i ] = circleAreaShadowMatrix[ i ] * shadowWorldPosition;
+
+		}
+		#pragma unroll_loop_end
 
 	#endif
-	*/
 
 #endif
 

+ 32 - 0
src/renderers/shaders/UniformsLib.js

@@ -194,6 +194,38 @@ const UniformsLib = {
 			height: {}
 		} },
 
+		rectAreaLightShadows: { value: [], properties: {
+			shadowIntensity: 1,
+			shadowBias: {},
+			shadowNormalBias: {},
+			shadowRadius: {},
+			shadowMapSize: {},
+			lightSize: {}
+		} },
+
+		rectAreaShadowMap: { value: [] },
+		rectAreaShadowMatrix: { value: [] },
+
+		circleAreaLights: { value: [], properties: {
+			color: {},
+			position: {},
+			axisU: {},
+			axisV: {},
+			radius: {}
+		} },
+
+		circleAreaLightShadows: { value: [], properties: {
+			shadowIntensity: 1,
+			shadowBias: {},
+			shadowNormalBias: {},
+			shadowRadius: {},
+			shadowMapSize: {},
+			lightSize: {}
+		} },
+
+		circleAreaShadowMap: { value: [] },
+		circleAreaShadowMatrix: { value: [] },
+
 		ltc_1: { value: null },
 		ltc_2: { value: null }
 

+ 144 - 3
src/renderers/webgl/WebGLLights.js

@@ -67,6 +67,16 @@ function UniformsCache() {
 					};
 					break;
 
+				case 'CircleAreaLight':
+					uniforms = {
+						color: new Color(),
+						position: new Vector3(),
+						axisU: new Vector3(),
+						axisV: new Vector3(),
+						radius: 0
+					};
+					break;
+
 			}
 
 			lights[ light.id ] = uniforms;
@@ -129,7 +139,25 @@ function ShadowUniformsCache() {
 					};
 					break;
 
-				// TODO (abelnation): set RectAreaLight shadow uniforms
+				case 'RectAreaLight':
+					uniforms = {
+						shadowIntensity: 1,
+						shadowBias: 0,
+						shadowNormalBias: 0,
+						shadowRadius: 1,
+						shadowMapSize: new Vector2()
+					};
+					break;
+
+				case 'CircleAreaLight':
+					uniforms = {
+						shadowIntensity: 1,
+						shadowBias: 0,
+						shadowNormalBias: 0,
+						shadowRadius: 1,
+						shadowMapSize: new Vector2()
+					};
+					break;
 
 			}
 
@@ -168,12 +196,15 @@ function WebGLLights( extensions ) {
 			pointLength: - 1,
 			spotLength: - 1,
 			rectAreaLength: - 1,
+			circleAreaLength: - 1,
 			hemiLength: - 1,
 
 			numDirectionalShadows: - 1,
 			numPointShadows: - 1,
 			numSpotShadows: - 1,
 			numSpotMaps: - 1,
+			numRectAreaShadows: - 1,
+			numCircleAreaShadows: - 1,
 
 			numLightProbes: - 1
 		},
@@ -190,8 +221,15 @@ function WebGLLights( extensions ) {
 		spotShadowMap: [],
 		spotLightMatrix: [],
 		rectArea: [],
+		rectAreaShadow: [],
+		rectAreaShadowMap: [],
+		rectAreaShadowMatrix: [],
 		rectAreaLTC1: null,
 		rectAreaLTC2: null,
+		circleArea: [],
+		circleAreaShadow: [],
+		circleAreaShadowMap: [],
+		circleAreaShadowMatrix: [],
 		point: [],
 		pointShadow: [],
 		pointShadowMap: [],
@@ -218,6 +256,7 @@ function WebGLLights( extensions ) {
 		let pointLength = 0;
 		let spotLength = 0;
 		let rectAreaLength = 0;
+		let circleAreaLength = 0;
 		let hemiLength = 0;
 
 		let numDirectionalShadows = 0;
@@ -225,6 +264,8 @@ function WebGLLights( extensions ) {
 		let numSpotShadows = 0;
 		let numSpotMaps = 0;
 		let numSpotShadowsWithMaps = 0;
+		let numRectAreaShadows = 0;
+		let numCircleAreaShadows = 0;
 
 		let numLightProbes = 0;
 
@@ -347,9 +388,74 @@ function WebGLLights( extensions ) {
 				uniforms.halfWidth.set( light.width * 0.5, 0.0, 0.0 );
 				uniforms.halfHeight.set( 0.0, light.height * 0.5, 0.0 );
 
-				state.rectArea[ rectAreaLength ] = uniforms;
 
-				rectAreaLength ++;
+			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 = shadow.mapSize;
+				shadowUniforms.lightSize = ( light.width + light.height ) * 0.5;
+
+				state.rectAreaShadow[ rectAreaLength ] = shadowUniforms;
+				state.rectAreaShadowMap[ rectAreaLength ] = shadowMap;
+				state.rectAreaShadowMatrix[ rectAreaLength ] = light.shadow.matrix;
+
+				numRectAreaShadows ++;
+
+			}
+			state.rectArea[ rectAreaLength ] = uniforms;
+
+
+
+			rectAreaLength ++;
+
+			} else if ( light.isCircleAreaLight ) {
+
+				const uniforms = cache.get( light );
+
+				uniforms.color.copy( color ).multiplyScalar( intensity );
+
+				uniforms.position.setFromMatrixPosition( light.matrixWorld );
+
+				// Circle lies in XY plane in local space, define orthogonal axes
+				// axisV is negated to work with reversed winding order for correct light direction
+				uniforms.axisU.set( light.radius, 0.0, 0.0 );
+				uniforms.axisV.set( 0.0, -light.radius, 0.0 );
+
+				uniforms.radius = light.radius;
+
+
+			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 = shadow.mapSize;
+				shadowUniforms.lightSize = light.radius * 2.0; // Diameter as light size
+
+				state.circleAreaShadow[ circleAreaLength ] = shadowUniforms;
+				state.circleAreaShadowMap[ circleAreaLength ] = shadowMap;
+				state.circleAreaShadowMatrix[ circleAreaLength ] = light.shadow.matrix;
+
+				numCircleAreaShadows ++;
+
+			}
+			state.circleArea[ circleAreaLength ] = uniforms;
+
+
+
+			circleAreaLength ++;
 
 			} else if ( light.isPointLight ) {
 
@@ -426,16 +532,20 @@ function WebGLLights( extensions ) {
 			hash.pointLength !== pointLength ||
 			hash.spotLength !== spotLength ||
 			hash.rectAreaLength !== rectAreaLength ||
+			hash.circleAreaLength !== circleAreaLength ||
 			hash.hemiLength !== hemiLength ||
 			hash.numDirectionalShadows !== numDirectionalShadows ||
 			hash.numPointShadows !== numPointShadows ||
 			hash.numSpotShadows !== numSpotShadows ||
 			hash.numSpotMaps !== numSpotMaps ||
+			hash.numRectAreaShadows !== numRectAreaShadows ||
+			hash.numCircleAreaShadows !== numCircleAreaShadows ||
 			hash.numLightProbes !== numLightProbes ) {
 
 			state.directional.length = directionalLength;
 			state.spot.length = spotLength;
 			state.rectArea.length = rectAreaLength;
+			state.circleArea.length = circleAreaLength;
 			state.point.length = pointLength;
 			state.hemi.length = hemiLength;
 
@@ -445,9 +555,15 @@ function WebGLLights( extensions ) {
 			state.pointShadowMap.length = numPointShadows;
 			state.spotShadow.length = numSpotShadows;
 			state.spotShadowMap.length = numSpotShadows;
+			state.rectAreaShadow.length = numRectAreaShadows;
+			state.rectAreaShadowMap.length = numRectAreaShadows;
+			state.circleAreaShadow.length = numCircleAreaShadows;
+			state.circleAreaShadowMap.length = numCircleAreaShadows;
 			state.directionalShadowMatrix.length = numDirectionalShadows;
 			state.pointShadowMatrix.length = numPointShadows;
 			state.spotLightMatrix.length = numSpotShadows + numSpotMaps - numSpotShadowsWithMaps;
+			state.rectAreaShadowMatrix.length = numRectAreaShadows;
+			state.circleAreaShadowMatrix.length = numCircleAreaShadows;
 			state.spotLightMap.length = numSpotMaps;
 			state.numSpotLightShadowsWithMaps = numSpotShadowsWithMaps;
 			state.numLightProbes = numLightProbes;
@@ -456,12 +572,15 @@ function WebGLLights( extensions ) {
 			hash.pointLength = pointLength;
 			hash.spotLength = spotLength;
 			hash.rectAreaLength = rectAreaLength;
+			hash.circleAreaLength = circleAreaLength;
 			hash.hemiLength = hemiLength;
 
 			hash.numDirectionalShadows = numDirectionalShadows;
 			hash.numPointShadows = numPointShadows;
 			hash.numSpotShadows = numSpotShadows;
 			hash.numSpotMaps = numSpotMaps;
+			hash.numRectAreaShadows = numRectAreaShadows;
+			hash.numCircleAreaShadows = numCircleAreaShadows;
 
 			hash.numLightProbes = numLightProbes;
 
@@ -477,6 +596,7 @@ function WebGLLights( extensions ) {
 		let pointLength = 0;
 		let spotLength = 0;
 		let rectAreaLength = 0;
+		let circleAreaLength = 0;
 		let hemiLength = 0;
 
 		const viewMatrix = camera.matrixWorldInverse;
@@ -531,6 +651,27 @@ function WebGLLights( extensions ) {
 
 				rectAreaLength ++;
 
+			} else if ( light.isCircleAreaLight ) {
+
+				const uniforms = state.circleArea[ circleAreaLength ];
+
+				uniforms.position.setFromMatrixPosition( light.matrixWorld );
+				uniforms.position.applyMatrix4( viewMatrix );
+
+				// extract local rotation of light to derive axis vectors
+				matrix42.identity();
+				matrix4.copy( light.matrixWorld );
+				matrix4.premultiply( viewMatrix );
+				matrix42.extractRotation( matrix4 );
+
+				uniforms.axisU.set( light.radius, 0.0, 0.0 );
+				uniforms.axisV.set( 0.0, -light.radius, 0.0 ); // negated for reversed winding
+
+				uniforms.axisU.applyMatrix4( matrix42 );
+				uniforms.axisV.applyMatrix4( matrix42 );
+
+				circleAreaLength ++;
+
 			} else if ( light.isPointLight ) {
 
 				const uniforms = state.point[ pointLength ];

+ 4 - 1
src/renderers/webgl/WebGLProgram.js

@@ -240,12 +240,15 @@ function replaceLightNums( string, parameters ) {
 		.replace( /NUM_SPOT_LIGHT_MAPS/g, parameters.numSpotLightMaps )
 		.replace( /NUM_SPOT_LIGHT_COORDS/g, numSpotLightCoords )
 		.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
+		.replace( /NUM_CIRCLE_AREA_LIGHTS/g, parameters.numCircleAreaLights )
 		.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
 		.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights )
 		.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 )
-		.replace( /NUM_POINT_LIGHT_SHADOWS/g, parameters.numPointLightShadows );
+		.replace( /NUM_POINT_LIGHT_SHADOWS/g, parameters.numPointLightShadows )
+		.replace( /NUM_RECT_AREA_LIGHT_SHADOWS/g, parameters.numRectAreaLightShadows )
+		.replace( /NUM_CIRCLE_AREA_LIGHT_SHADOWS/g, parameters.numCircleAreaLightShadows );
 
 }
 

+ 6 - 0
src/renderers/webgl/WebGLPrograms.js

@@ -322,12 +322,15 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 			numSpotLights: lights.spot.length,
 			numSpotLightMaps: lights.spotLightMap.length,
 			numRectAreaLights: lights.rectArea.length,
+			numCircleAreaLights: lights.circleArea.length,
 			numHemiLights: lights.hemi.length,
 
 			numDirLightShadows: lights.directionalShadowMap.length,
 			numPointLightShadows: lights.pointShadowMap.length,
 			numSpotLightShadows: lights.spotShadowMap.length,
 			numSpotLightShadowsWithMaps: lights.numSpotLightShadowsWithMaps,
+			numRectAreaLightShadows: lights.rectAreaShadowMap.length,
+			numCircleAreaLightShadows: lights.circleAreaShadowMap.length,
 
 			numLightProbes: lights.numLightProbes,
 
@@ -455,10 +458,13 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 		array.push( parameters.numSpotLightMaps );
 		array.push( parameters.numHemiLights );
 		array.push( parameters.numRectAreaLights );
+		array.push( parameters.numCircleAreaLights );
 		array.push( parameters.numDirLightShadows );
 		array.push( parameters.numPointLightShadows );
 		array.push( parameters.numSpotLightShadows );
 		array.push( parameters.numSpotLightShadowsWithMaps );
+		array.push( parameters.numRectAreaLightShadows );
+		array.push( parameters.numCircleAreaLightShadows );
 		array.push( parameters.numLightProbes );
 		array.push( parameters.shadowMapType );
 		array.push( parameters.toneMapping );

Some files were not shown because too many files changed in this diff

粤ICP备19079148号