diff --git a/docs/api/lights/Projector.html b/docs/api/lights/Projector.html
new file mode 100644
index 00000000000000..da363135f5533c
--- /dev/null
+++ b/docs/api/lights/Projector.html
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+ [page:Object3D] → [page:Light] →
+
+ [name]
+
+
+ The projector is a light-source that emits light from a single point in one
+ direction, along a rectangular cone that increases in size the further from the light it gets.
+ The color of the light is read from a texture to produce a projector-like effect.
+
+ This light can cast shadows - see the [page:ProjectorShadow] page for details.
+
+
+
+
+
+ Example
+
+
+
+ [example:webgl_lights_projector View in Examples ]
+
+
+
+
+ Code Example
+
+ // projector shining from the side, casting a shadow
+ var projector = new THREE.Projector( 0xffffff );
+
+ projector.position.set( 100, 1000, 100 );
+ projector.map = new THREE.TextureLoader().load(textureUrl);
+ projector.fov = 30;
+ projector.aspect = 16 / 9;
+
+ projector.castShadow = true;
+
+ projector.shadow.mapSize.width = 1024;
+ projector.shadow.mapSize.height = 1024;
+
+ projector.shadow.camera.near = 500;
+ projector.shadow.camera.far = 4000;
+
+ scene.add( projector );
+
+
+ Constructor
+
+
+ [name]( [page:Integer color], [page:Float intensity], [page:Float distance], [page:Float fov], [page:Float aspect], [page:Float decay] )
+
+ [page:Integer color] - (optional) hexadecimal color of the light. Default is 0xffffff (white).
+ [page:Float intensity] - (optional) numeric value of the light's strength/intensity. Default is 1.
+ [page:Float distance] - Maximum distance from origin where light will shine whose intensity
+ is attenuated linearly based on distance from origin.
+ [page:Float fov] - The vertical field-of-view for the projector in degrees.
+ [page:Float aspect] - The aspect-ratio (width / height) of the beam. Default is 1.
+ [page:Float decay] - The amount the light dims along the distance of the light.
+
+ Creates a new [name].
+
+
+ Properties
+
+ See the base [page:Light Light] class for common properties.
+
+
+ [property:Vector3 position]
+
+ The position of the light-source in it's reference coordinate-system.
+ This is set equal to [page:Object3D.DefaultUp] (0, 1, 0), so that the light shines from the top down.
+
+
+ [property:Object3D target]
+
+ The Projector points from its [page:.position position] to target.position. The default
+ position of the target is *(0, 0, 0)*.
+
+ *Note*: For the target's position to be changed to anything other than the default,
+ it must be added to the [page:Scene scene] using
+
+ scene.add( light.target );
+
+
+ This is so that the target's [page:Object3D.matrixWorld matrixWorld] gets automatically
+ updated each frame.
+
+ It is also possible to set the target to be another object in the scene (anything with a
+ [page:Object3D.position position] property), like so:
+
+ var targetObject = new THREE.Object3D();
+ scene.add(targetObject);
+
+ projector.target = targetObject;
+
+ The projector will now track the target object.
+
+
+ [property:Boolean isProjector]
+
+ Used to check whether this or derived classes are projectors. Default is *true*.
+
+ You should not change this, as it used internally for optimisation.
+
+
+ [property:Texture map]
+
+ A [page:Texture] used to calculate the color of the light at a given fragment-position.
+
+
+ [property:Float fov]
+
+ The vertical field-of-view angle of the projector specified in degrees. Should be smaller than 180.
+
+
+ [property:Float aspect]
+
+ Aspect ratio (width / height) of the projector-beam. Default is 1.
+
+
+ [property:Float distance]
+
+ If non-zero, light will attenuate linearly from maximum intensity at the light's
+ position down to zero at this distance from the light. Default is *0.0*.
+
+
+ [property:Float decay]
+
+ The amount the light dims along the distance of the light.
+ In [page:WebGLRenderer.physicallyCorrectLights physically correct] mode, decay = 2 leads to
+ physically realistic light falloff. The default is *1*.
+
+
+ [property:Float power]
+
+ The light's power.
+ In [page:WebGLRenderer.physicallyCorrectLights physically correct] mode, the luminous
+ power of the light measured in lumens. Default is *4Math.PI*.
+
+ This is directly related to the [page:.intensity intensity] in the ratio
+
+ power = intensity * π
+
+ and changing this will also change the intensity.
+
+
+
+ [property:Boolean castShadow]
+
+ If set to *true* light will cast dynamic shadows. *Warning*: This is expensive and
+ requires tweaking to get shadows looking right. See the [page:ProjectorShadow] for details.
+ The default is *false*.
+
+
+ [property:ProjectorShadow shadow]
+
+ A [page:ProjectorShadow] used to calculate shadows for this light.
+
+
+ Methods
+
+ See the base [page:Light Light] class for common methods.
+
+ [method:Projector copy]( [page:SpotLight source] )
+
+ Copies value of all the properties from the [page:Projector source] to this
+ Projector.
+
+
+ [link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
+
+
diff --git a/docs/api/lights/shadows/ProjectorShadow.html b/docs/api/lights/shadows/ProjectorShadow.html
new file mode 100644
index 00000000000000..d893a8edf5e95c
--- /dev/null
+++ b/docs/api/lights/shadows/ProjectorShadow.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+ [page:LightShadow] →
+
+ [name]
+
+
+ This is used internally by [page:Projector Projectors] for calculating shadows.
+
+
+ Example
+
+
+//Create a WebGLRenderer and turn on shadows in the renderer
+var renderer = new THREE.WebGLRenderer();
+renderer.shadowMap.enabled = true;
+renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
+
+//Create a Projector and turn on shadows for the projector
+var projector = new THREE.Projector( 0xffffff );
+projector.map = new THREE.TextureLoader().load(TEXTURE_URL);
+projector.castShadow = true; // default false
+scene.add( projector );
+
+//Set up shadow properties for the projector
+projector.shadow.mapSize.width = 512; // default
+projector.shadow.mapSize.height = 512; // default
+projector.shadow.camera.near = 0.5; // default
+projector.shadow.camera.far = 500 // default
+
+//Create a sphere that cast shadows (but does not receive them)
+var sphereGeometry = new THREE.SphereBufferGeometry( 5, 32, 32 );
+var sphereMaterial = new THREE.MeshStandardMaterial( { color: 0xff0000 } );
+var sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
+sphere.castShadow = true; //default is false
+sphere.receiveShadow = false; //default
+scene.add( sphere );
+
+//Create a plane that receives shadows (but does not cast them)
+var planeGeometry = new THREE.PlaneBufferGeometry( 20, 20, 32, 32 );
+var planeMaterial = new THREE.MeshStandardMaterial( { color: 0x00ff00 } )
+var plane = new THREE.Mesh( planeGeometry, planeMaterial );
+plane.receiveShadow = true;
+scene.add( plane );
+
+//Create a helper for the shadow camera (optional)
+var helper = new THREE.CameraHelper( projector.shadow.camera );
+scene.add( helper );
+
+
+
+
+ Constructor
+
+ The constructor creates a [page:PerspectiveCamera PerspectiveCamera] to manage the shadow's view of the world.
+
+ Properties
+ See the base [page:LightShadow LightShadow] class for common properties.
+
+
+ [property:Camera camera]
+
+ The projector's view of the world. This is used to generate a depth map of the scene; objects behind
+ other objects from the projector's perspective will be in shadow.
+
+ The default is a [page:PerspectiveCamera] with [page:PerspectiveCamera.near near] clipping plane at 0.5.
+ The [page:PerspectiveCamera.fov fov] will track the [page:Projector.angle angle] property of the owning
+ [page:Projector Projector] via the [page:ProjectorShadow.update update] method. Similarly, the
+ [page:PerspectiveCamera.aspect aspect] property will track the aspect of the
+ [page:LightShadow.mapSize mapSize]. If the [page:Projector.distance distance] property of the projector is
+ set, the [page:PerspectiveCamera.far far] clipping plane will track that, otherwise it defaults to 500.
+
+
+
+ [property:Boolean isProjectorShadow]
+
+ Used to check whether this or derived classes are spot projector shadows. Default is *true*.
+
+ You should not change this, as it used internally for optimisation.
+
+
+ Methods
+ See the base [page:LightShadow LightShadow] class for common methods.
+
+ [method:ProjectorShadow update]( [page:Projector projector] )
+
+ Updates the internal perspective [page:.camera camera] based on the passed in [page:Projector projector].
+
+
+ Source
+
+ [link:https://github.com/mrdoob/three.js/blob/master/src/lights/[name].js src/lights/[name].js]
+
+
diff --git a/docs/list.js b/docs/list.js
index 5657c5bb599dc3..f7a506f3847b93 100644
--- a/docs/list.js
+++ b/docs/list.js
@@ -206,13 +206,15 @@ var list = {
"Light": "api/lights/Light",
"PointLight": "api/lights/PointLight",
"RectAreaLight": "api/lights/RectAreaLight",
- "SpotLight": "api/lights/SpotLight"
+ "SpotLight": "api/lights/SpotLight",
+ "Projector": "api/lights/Projector"
},
"Lights / Shadows": {
"DirectionalLightShadow": "api/lights/shadows/DirectionalLightShadow",
"LightShadow": "api/lights/shadows/LightShadow",
- "SpotLightShadow": "api/lights/shadows/SpotLightShadow"
+ "SpotLightShadow": "api/lights/shadows/SpotLightShadow",
+ "ProjectorShadow": "api/lights/shadows/ProjectorShadow"
},
"Loaders": {
diff --git a/examples/files.js b/examples/files.js
index 51bd4d98441846..474bb334192b01 100644
--- a/examples/files.js
+++ b/examples/files.js
@@ -67,6 +67,7 @@ var files = {
"webgl_lights_pointlights2",
"webgl_lights_spotlight",
"webgl_lights_spotlights",
+ "webgl_lights_projector",
"webgl_lights_rectarealight",
"webgl_lines_colors",
"webgl_lines_cubes",
diff --git a/examples/webgl_lights_projector.html b/examples/webgl_lights_projector.html
new file mode 100644
index 00000000000000..ed4d40dcaddb74
--- /dev/null
+++ b/examples/webgl_lights_projector.html
@@ -0,0 +1,246 @@
+
+
+
+ three.js webgl - lights - projector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Three.js b/src/Three.js
index 72b50d27d82b8e..60e19bb1a7f2b7 100644
--- a/src/Three.js
+++ b/src/Three.js
@@ -51,6 +51,7 @@ export { Cache } from './loaders/Cache.js';
export { AudioLoader } from './loaders/AudioLoader.js';
export { SpotLightShadow } from './lights/SpotLightShadow.js';
export { SpotLight } from './lights/SpotLight.js';
+export { Projector } from './lights/Projector.js';
export { PointLight } from './lights/PointLight.js';
export { RectAreaLight } from './lights/RectAreaLight.js';
export { HemisphereLight } from './lights/HemisphereLight.js';
diff --git a/src/lights/Projector.js b/src/lights/Projector.js
new file mode 100644
index 00000000000000..c43117f318ac36
--- /dev/null
+++ b/src/lights/Projector.js
@@ -0,0 +1,82 @@
+import { Light } from './Light.js';
+import { ProjectorShadow } from './ProjectorShadow.js';
+import { Object3D } from '../core/Object3D.js';
+
+/**
+ * @author usefulthink / https://github.com/usefulthink
+ */
+
+function Projector( color, intensity, distance, fov, aspect, decay ) {
+
+ Light.call( this, color, intensity );
+
+ this.type = 'Projector';
+
+ this.position.copy( Object3D.DefaultUp );
+ this.updateMatrix();
+
+ this.target = new Object3D();
+
+ Object.defineProperty( this, 'power', {
+ get: function () {
+
+ // intensity = power per solid angle.
+ // ref: equation (17) from http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
+ return this.intensity * Math.PI;
+
+ },
+ set: function ( power ) {
+
+ // intensity = power per solid angle.
+ // ref: equation (17) from http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf
+ this.intensity = power / Math.PI;
+
+ }
+ } );
+
+ this.distance = ( distance !== undefined ) ? distance : 0;
+ this.decay = ( decay !== undefined ) ? decay : 1; // should be 2 for physically correct lights.
+
+ this.fov = fov; // vertical field-of-view in degrees
+ this.aspect = ( aspect !== undefined ) ? aspect : 1;
+
+ this.shadow = new ProjectorShadow();
+
+ this.map = null;
+
+}
+
+Projector.prototype = Object.assign( Object.create( Light.prototype ), {
+
+ constructor: Projector,
+
+ isProjector: true,
+
+ copy: function ( source ) {
+
+ Light.prototype.copy.call( this, source );
+
+ this.position = source.position.clone();
+ this.target = source.target.clone();
+
+ this.distance = source.distance;
+ this.decay = source.decay;
+ this.fov = source.angle;
+ this.aspect = source.aspect;
+
+ this.shadow = source.shadow.clone();
+
+ if ( source.map !== null ) {
+
+ this.map = source.map.clone();
+
+ }
+
+ return this;
+
+ }
+
+} );
+
+
+export { Projector };
diff --git a/src/lights/ProjectorShadow.js b/src/lights/ProjectorShadow.js
new file mode 100644
index 00000000000000..07abd36a955a9d
--- /dev/null
+++ b/src/lights/ProjectorShadow.js
@@ -0,0 +1,38 @@
+import { LightShadow } from './LightShadow.js';
+import { PerspectiveCamera } from '../cameras/PerspectiveCamera.js';
+
+function ProjectorShadow() {
+
+ LightShadow.call( this, new PerspectiveCamera( 50, 1, 0.5, 500 ) );
+
+}
+
+ProjectorShadow.prototype = Object.assign( Object.create( LightShadow.prototype ), {
+
+ constructor: ProjectorShadow,
+
+ isProjectorShadow: true,
+
+ update: function ( light ) {
+
+ var camera = this.camera;
+ var cameraNeedsUpdate =
+ light.fov !== camera.fov
+ || light.aspect !== camera.aspect
+ || ( light.distance || camera.far ) !== camera.far;
+
+ if ( cameraNeedsUpdate ) {
+
+ camera.fov = light.fov;
+ camera.aspect = light.aspect;
+ camera.far = light.distance || camera.far;
+ camera.updateProjectionMatrix();
+
+ }
+
+ }
+
+} );
+
+
+export { ProjectorShadow };
diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js
index 805c3cf0868747..0156c40e0cfefa 100644
--- a/src/renderers/WebGLRenderer.js
+++ b/src/renderers/WebGLRenderer.js
@@ -1579,6 +1579,7 @@ function WebGLRenderer( parameters ) {
uniforms.ambientLightColor.value = lights.state.ambient;
uniforms.directionalLights.value = lights.state.directional;
uniforms.spotLights.value = lights.state.spot;
+ uniforms.projectors.value = lights.state.projector;
uniforms.rectAreaLights.value = lights.state.rectArea;
uniforms.pointLights.value = lights.state.point;
uniforms.hemisphereLights.value = lights.state.hemi;
@@ -1587,6 +1588,9 @@ function WebGLRenderer( parameters ) {
uniforms.directionalShadowMatrix.value = lights.state.directionalShadowMatrix;
uniforms.spotShadowMap.value = lights.state.spotShadowMap;
uniforms.spotShadowMatrix.value = lights.state.spotShadowMatrix;
+ uniforms.projectorTextures.value = lights.state.projectorTextures;
+ uniforms.projectorShadowMap.value = lights.state.projectorShadowMap;
+ uniforms.projectorShadowMatrix.value = lights.state.projectorShadowMatrix;
uniforms.pointShadowMap.value = lights.state.pointShadowMap;
uniforms.pointShadowMatrix.value = lights.state.pointShadowMatrix;
// TODO (abelnation): add area lights shadow info to uniforms
@@ -2298,6 +2302,7 @@ function WebGLRenderer( parameters ) {
uniforms.directionalLights.needsUpdate = value;
uniforms.pointLights.needsUpdate = value;
uniforms.spotLights.needsUpdate = value;
+ uniforms.projectors.needsUpdate = value;
uniforms.rectAreaLights.needsUpdate = value;
uniforms.hemisphereLights.needsUpdate = value;
diff --git a/src/renderers/shaders/ShaderChunk/lights_pars.glsl b/src/renderers/shaders/ShaderChunk/lights_pars.glsl
index af622b054e62f4..c5393f5669145e 100644
--- a/src/renderers/shaders/ShaderChunk/lights_pars.glsl
+++ b/src/renderers/shaders/ShaderChunk/lights_pars.glsl
@@ -121,6 +121,74 @@ vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {
#endif
+#if NUM_PROJECTOR_LIGHTS > 0
+
+ struct Projector {
+ vec3 position;
+ vec3 color;
+ float distance;
+ float decay;
+
+ mat4 projectorMatrix;
+ mat3 uvTransform;
+
+ int shadow;
+ float shadowBias;
+ float shadowRadius;
+ vec2 shadowMapSize;
+ };
+
+ uniform Projector projectors[NUM_PROJECTOR_LIGHTS];
+ uniform sampler2D projectorTextures[NUM_PROJECTOR_LIGHTS];
+
+ void getProjectorDirectLightIrradiance(
+ const in Projector projector,
+ const in sampler2D projectorTexture,
+ const in GeometricContext geometry,
+ out IncidentLight directLight)
+ {
+
+ vec3 lVector = projector.position - geometry.position;
+ float lightDistance = length( lVector );
+
+ directLight.direction = normalize( lVector );
+
+ vec4 projected = projector.projectorMatrix * vec4( geometry.position, 1.0 );
+ projected = projected / projected.w;
+
+ vec2 projectorUv = 0.5 * projected.xy + vec2( 0.5 );
+ projectorUv = ( projector.uvTransform * vec3( projectorUv, 1.0 ) ).xy;
+
+ directLight.visible = all(bvec3(
+ all( lessThanEqual( projected.xy, vec2( 1.0 ) ) ),
+ all( greaterThanEqual( projected.xy, vec2( -1.0 ) ) ),
+ projected.z >= 0.0
+ ));
+
+ if (directLight.visible) {
+
+ vec4 textureColor = texture2D( projectorTexture, projectorUv );
+ directLight.color = projector.color;
+ directLight.color *= textureColor.rgb;
+ directLight.color *= textureColor.a; // attenuate
+
+ directLight.color *= punctualLightIntensityToIrradianceFactor(
+ lightDistance,
+ projector.distance,
+ projector.decay
+ );
+
+ } else {
+
+ directLight.color = vec3( 0.0 );
+
+ }
+
+ }
+
+#endif
+
+
#if NUM_RECT_AREA_LIGHTS > 0
struct RectAreaLight {
diff --git a/src/renderers/shaders/ShaderChunk/lights_template.glsl b/src/renderers/shaders/ShaderChunk/lights_template.glsl
index 154483c4bb4209..b7b9dad81d3d97 100644
--- a/src/renderers/shaders/ShaderChunk/lights_template.glsl
+++ b/src/renderers/shaders/ShaderChunk/lights_template.glsl
@@ -61,6 +61,39 @@ IncidentLight directLight;
#endif
+#if ( NUM_PROJECTOR_LIGHTS > 0 ) && defined( RE_Direct )
+
+ Projector projector;
+
+ for ( int i = 0; i < NUM_PROJECTOR_LIGHTS; i ++ ) {
+
+ projector = projectors[ i ];
+
+ getProjectorDirectLightIrradiance(
+ projector,
+ projectorTextures[ i ],
+ geometry,
+ directLight
+ );
+
+ #ifdef USE_SHADOWMAP
+ directLight.color *= all( bvec2( projector.shadow, directLight.visible ) )
+ ? getShadow(
+ projectorShadowMap[ i ],
+ projector.shadowMapSize,
+ projector.shadowBias,
+ projector.shadowRadius,
+ vProjectorShadowCoord[ i ]
+ )
+ : 1.0;
+ #endif
+
+ RE_Direct( directLight, geometry, material, reflectedLight );
+
+ }
+
+#endif
+
#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )
DirectionalLight directionalLight;
diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl b/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl
index 5f0c598ae1f7fb..ee91d7fe92dc44 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl
+++ b/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl
@@ -14,6 +14,13 @@
#endif
+ #if NUM_PROJECTOR_LIGHTS > 0
+
+ uniform sampler2D projectorShadowMap[ NUM_PROJECTOR_LIGHTS ];
+ varying vec4 vProjectorShadowCoord[ NUM_PROJECTOR_LIGHTS ];
+
+ #endif
+
#if NUM_POINT_LIGHTS > 0
uniform sampler2D pointShadowMap[ NUM_POINT_LIGHTS ];
diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl b/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl
index 7417a5a8fa49b5..788b3b63d1e60d 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl
+++ b/src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl
@@ -14,6 +14,13 @@
#endif
+ #if NUM_PROJECTOR_LIGHTS > 0
+
+ uniform mat4 projectorShadowMatrix[ NUM_PROJECTOR_LIGHTS ];
+ varying vec4 vProjectorShadowCoord[ NUM_PROJECTOR_LIGHTS ];
+
+ #endif
+
#if NUM_POINT_LIGHTS > 0
uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHTS ];
diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl b/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl
index 65bea13829d0b4..b44fe685c0f87f 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl
+++ b/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl
@@ -20,6 +20,16 @@
#endif
+ #if NUM_PROJECTOR_LIGHTS > 0
+
+ for ( int i = 0; i < NUM_PROJECTOR_LIGHTS; i ++ ) {
+
+ vProjectorShadowCoord[ i ] = projectorShadowMatrix[ i ] * worldPosition;
+
+ }
+
+ #endif
+
#if NUM_POINT_LIGHTS > 0
for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
diff --git a/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl b/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl
index d2a49ee331ba57..9a5c9a1e9018a9 100644
--- a/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl
+++ b/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl
@@ -30,6 +30,19 @@ float getShadowMask() {
#endif
+ #if NUM_PROJECTOR_LIGHTS > 0
+
+ Projector projector;
+
+ for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {
+
+ projector = projectors[ i ];
+ shadow *= bool( projector.shadow ) ? getShadow( projectorShadowMap[ i ], projector.shadowMapSize, projector.shadowBias, projector.shadowRadius, vProjectorShadowCoord[ i ] ) : 1.0;
+
+ }
+
+ #endif
+
#if NUM_POINT_LIGHTS > 0
PointLight pointLight;
diff --git a/src/renderers/shaders/UniformsLib.js b/src/renderers/shaders/UniformsLib.js
index 6c49642ed3a015..5168bab0c3b8ea 100644
--- a/src/renderers/shaders/UniformsLib.js
+++ b/src/renderers/shaders/UniformsLib.js
@@ -139,6 +139,24 @@ var UniformsLib = {
spotShadowMap: { value: [] },
spotShadowMatrix: { value: [] },
+ projectors: { value: [], properties: {
+ color: {},
+ position: {},
+ distance: {},
+ decay: {},
+ projectorMatrix: {},
+ uvTransform: {},
+
+ shadow: {},
+ shadowBias: {},
+ shadowRadius: {},
+ shadowMapSize: {}
+ } },
+
+ projectorTextures: { value: [] },
+ projectorShadowMap: { value: [] },
+ projectorShadowMatrix: { value: [] },
+
pointLights: { value: [], properties: {
color: {},
position: {},
diff --git a/src/renderers/webgl/WebGLLights.js b/src/renderers/webgl/WebGLLights.js
index ec2565c96212b0..8d55d683a5ca41 100644
--- a/src/renderers/webgl/WebGLLights.js
+++ b/src/renderers/webgl/WebGLLights.js
@@ -3,9 +3,12 @@
*/
import { Color } from '../../math/Color.js';
+import { Matrix3 } from '../../math/Matrix3';
import { Matrix4 } from '../../math/Matrix4.js';
import { Vector2 } from '../../math/Vector2.js';
import { Vector3 } from '../../math/Vector3.js';
+import { Quaternion } from '../../math/Quaternion.js';
+import { _Math } from '../../math/Math.js';
function UniformsCache() {
@@ -54,6 +57,22 @@ function UniformsCache() {
};
break;
+ case 'Projector':
+ uniforms = {
+ position: new Vector3(),
+ color: new Color(),
+ distance: 0,
+ decay: 0,
+ projectorMatrix: new Matrix4(),
+ uvTransform: new Matrix3(),
+
+ shadow: false,
+ shadowBias: 0,
+ shadowRadius: 1,
+ shadowMapSize: new Vector2()
+ };
+ break;
+
case 'PointLight':
uniforms = {
position: new Vector3(),
@@ -115,6 +134,10 @@ function WebGLLights() {
spot: [],
spotShadowMap: [],
spotShadowMatrix: [],
+ projector: [],
+ projectorTextures: [],
+ projectorShadowMap: [],
+ projectorShadowMatrix: [],
rectArea: [],
point: [],
pointShadowMap: [],
@@ -124,6 +147,7 @@ function WebGLLights() {
};
var vector3 = new Vector3();
+ var quaternion = new Quaternion();
var matrix4 = new Matrix4();
var matrix42 = new Matrix4();
@@ -134,6 +158,7 @@ function WebGLLights() {
var directionalLength = 0;
var pointLength = 0;
var spotLength = 0;
+ var projectorLength = 0;
var rectAreaLength = 0;
var hemiLength = 0;
@@ -220,6 +245,84 @@ function WebGLLights() {
spotLength ++;
+ } else if ( light.isProjector ) {
+
+ var uniforms = cache.get( light );
+
+ uniforms.position.setFromMatrixPosition( light.matrixWorld );
+ uniforms.position.applyMatrix4( viewMatrix );
+
+ uniforms.color.copy( color ).multiplyScalar( intensity );
+ uniforms.distance = distance;
+ uniforms.decay = ( light.distance === 0 ) ? 0.0 : light.decay;
+
+ // construct a world-matrix from light-position, -target and z-rotation
+ matrix4.lookAt( light.position, light.target.position, light.up );
+ matrix4.multiply( matrix42.makeRotationZ( light.rotation.z ) );
+
+ quaternion.setFromRotationMatrix( matrix4 );
+
+ matrix4.compose( light.position, quaternion, vector3.set( 1, 1, 1 ) );
+ matrix42.getInverse( matrix4 );
+
+ var near = ( distance || 500 ) * 1e-7,
+ far = ( distance || 500 ),
+ top = near * Math.tan( 0.5 * light.fov * _Math.DEG2RAD ),
+ height = 2 * top,
+ width = light.aspect * height,
+ left = - 0.5 * width;
+
+ // construct the projector-matrix that takes view-space
+ // coordinates from GeometricContext in the fragment-shader
+ // and transforms them to texture-coordinates for the
+ // light-texture
+ uniforms.projectorMatrix
+ .makePerspective(
+ left,
+ left + width,
+ top,
+ top - height,
+ near,
+ far
+ )
+ .multiply( matrix42 )
+ .multiply( camera.matrixWorld )
+ ;
+
+ var projectorTexture = light.map;
+
+ if ( projectorTexture ) {
+
+ var offset = projectorTexture.offset;
+ var repeat = projectorTexture.repeat;
+ var rotation = projectorTexture.rotation;
+ var center = projectorTexture.center;
+
+ projectorTexture.matrix.setUvTransform( offset.x, offset.y, repeat.x, repeat.y, rotation, center.x, center.y );
+
+ uniforms.uvTransform.copy(projectorTexture.matrix);
+
+ }
+
+ uniforms.shadow = light.castShadow;
+
+ if ( light.castShadow ) {
+
+ var shadow = light.shadow;
+
+ uniforms.shadowBias = shadow.bias;
+ uniforms.shadowRadius = shadow.radius;
+ uniforms.shadowMapSize = shadow.mapSize;
+
+ }
+
+ state.projectorTextures[ projectorLength ] = projectorTexture;
+ state.projectorShadowMap[ projectorLength ] = shadowMap;
+ state.projectorShadowMatrix[ projectorLength ] = light.shadow.matrix;
+ state.projector[ projectorLength ] = uniforms;
+
+ projectorLength ++;
+
} else if ( light.isRectAreaLight ) {
var uniforms = cache.get( light );
@@ -308,12 +411,13 @@ function WebGLLights() {
state.directional.length = directionalLength;
state.spot.length = spotLength;
+ state.projector.length = projectorLength;
state.rectArea.length = rectAreaLength;
state.point.length = pointLength;
state.hemi.length = hemiLength;
// TODO (sam-g-steel) why aren't we using join
- state.hash = directionalLength + ',' + pointLength + ',' + spotLength + ',' + rectAreaLength + ',' + hemiLength + ',' + shadows.length;
+ state.hash = directionalLength + ',' + pointLength + ',' + spotLength + ',' + projectorLength + ',' + rectAreaLength + ',' + hemiLength + ',' + shadows.length;
}
diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js
index f8915534a354f7..9d70773fa61a06 100644
--- a/src/renderers/webgl/WebGLProgram.js
+++ b/src/renderers/webgl/WebGLProgram.js
@@ -144,6 +144,7 @@ function replaceLightNums( string, parameters ) {
return string
.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )
.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )
+ .replace( /NUM_PROJECTOR_LIGHTS/g, parameters.numProjectors )
.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights );
diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js
index 633a3397e7c7a1..a7cbeed8b03d1a 100644
--- a/src/renderers/webgl/WebGLPrograms.js
+++ b/src/renderers/webgl/WebGLPrograms.js
@@ -33,7 +33,7 @@ function WebGLPrograms( renderer, extensions, capabilities ) {
"flatShading", "sizeAttenuation", "logarithmicDepthBuffer", "skinning",
"maxBones", "useVertexTexture", "morphTargets", "morphNormals",
"maxMorphTargets", "maxMorphNormals", "premultipliedAlpha",
- "numDirLights", "numPointLights", "numSpotLights", "numHemiLights", "numRectAreaLights",
+ "numDirLights", "numPointLights", "numSpotLights", "numProjectors", "numHemiLights", "numRectAreaLights",
"shadowMapEnabled", "shadowMapType", "toneMapping", 'physicallyCorrectLights',
"alphaTest", "doubleSided", "flipSided", "numClippingPlanes", "numClipIntersection", "depthPacking", "dithering"
];
@@ -181,6 +181,7 @@ function WebGLPrograms( renderer, extensions, capabilities ) {
numDirLights: lights.directional.length,
numPointLights: lights.point.length,
numSpotLights: lights.spot.length,
+ numProjectors: lights.projector.length,
numRectAreaLights: lights.rectArea.length,
numHemiLights: lights.hemi.length,
diff --git a/src/renderers/webgl/WebGLShadowMap.js b/src/renderers/webgl/WebGLShadowMap.js
index 69bca5c5a8bbca..0a43848656e29c 100644
--- a/src/renderers/webgl/WebGLShadowMap.js
+++ b/src/renderers/webgl/WebGLShadowMap.js
@@ -180,7 +180,7 @@ function WebGLShadowMap( _renderer, _objects, maxTextureSize ) {
}
- if ( shadow.isSpotLightShadow ) {
+ if ( shadow.isSpotLightShadow || shadow.isProjectorShadow ) {
shadow.update( light );
@@ -207,6 +207,17 @@ function WebGLShadowMap( _renderer, _objects, maxTextureSize ) {
_lookTarget.setFromMatrixPosition( light.target.matrixWorld );
shadowCamera.lookAt( _lookTarget );
+
+ // FIXME: feels wrong here, but the only other solution I
+ // can come up with would be to have updating the shadow-cameras
+ // fully managed by the *LightShadow classes.
+
+ if ( shadow.isProjectorShadow ) {
+
+ shadowCamera.rotateZ( light.rotation.z );
+
+ }
+
shadowCamera.updateMatrixWorld();
// compute shadow matrix