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 + + + + + + +
+ three.js webgl + - projector by Martin Schuhfuss
+
+ + + + + + + + + + + + + + + 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