From 0ff1e52b2312e31923dc0835b07187a3ce2f64a7 Mon Sep 17 00:00:00 2001 From: Jian Huang Date: Sun, 28 Jul 2019 23:39:25 -0700 Subject: [PATCH] Add shadow effect to LightingEffect class --- docs/api-reference/layer.md | 6 + .../api-reference/lights/directional-light.md | 1 + examples/experimental/sun/src/app.js | 10 +- .../lighting/light-with-shadow-effect.js | 136 ---------------- .../src/effects/lighting/lighting-effect.js | 154 +++++++++++++++++- modules/core/src/index.js | 1 - .../effects/light-with-shadow-effect.spec.js | 64 -------- .../core/effects/lighting-effect.spec.js | 81 ++++++++- 8 files changed, 235 insertions(+), 218 deletions(-) delete mode 100644 modules/core/src/effects/lighting/light-with-shadow-effect.js delete mode 100644 test/modules/core/effects/light-with-shadow-effect.spec.js diff --git a/docs/api-reference/layer.md b/docs/api-reference/layer.md index 6c0a2fe7211..5229fa18408 100644 --- a/docs/api-reference/layer.md +++ b/docs/api-reference/layer.md @@ -424,6 +424,12 @@ Notes: - `toValue` (TypedArray) - the new value to transition to, for the current vertex - `fromChunk` (Array | TypedArray) - the existing value to transition from, for the chunk that the current vertex belongs to. A "chunk" is a group of vertices that help the callback determine the context of this transition. For most layers, all objects are in one chunk. For PathLayer and PolygonLayer, each path/polygon is a chunk. +##### `enableShadow` (Boolean, optional) **Experimental** + +* Default: `true` + +Experimental shadow effect can be toggled on and off for a layer by setting `enableShadow` prop. In order to render shadow, a [DirectionalLight](/docs/api-reference/lights/directional-light.md) with `castShadow` set to `true` must be created and passed to [LightingEffect](/docs/effects/lighting-effect.md). + ## Members diff --git a/docs/api-reference/lights/directional-light.md b/docs/api-reference/lights/directional-light.md index 8c2d0325864..5ac73bf1c72 100644 --- a/docs/api-reference/lights/directional-light.md +++ b/docs/api-reference/lights/directional-light.md @@ -33,6 +33,7 @@ const directionalLight = new DirectionalLight({color, intensity, direction}); * `color` - (*array*,) RGB color of directional light source, default value is `[255, 255, 255]`. * `intensity` - (*number*) Strength of directional light source, default value is `1.0`. * `direction` - (*array*,) 3D vector specifies the direction the light comes from, default value is `[0, 0, -1]`. +* `castShadow` - (*boolean*, optional) Enable experimental shadow effect, default value is `false`. ## Source diff --git a/examples/experimental/sun/src/app.js b/examples/experimental/sun/src/app.js index f028b0bee83..e78d0f5a54a 100644 --- a/examples/experimental/sun/src/app.js +++ b/examples/experimental/sun/src/app.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import {render} from 'react-dom'; import {StaticMap} from 'react-map-gl'; import DeckGL from 'deck.gl'; -import {AmbientLight, DirectionalLight, _LightWithShadowEffect} from '@deck.gl/core'; +import {AmbientLight, DirectionalLight, LightingEffect} from '@deck.gl/core'; import {SolidPolygonLayer} from '@deck.gl/layers'; import {PhongMaterial} from '@luma.gl/core'; @@ -21,16 +21,18 @@ const ambientLight = new AmbientLight({ const dirLight0 = new DirectionalLight({ color: [255, 255, 255], intensity: 1.0, - direction: [10, -20, -30] + direction: [10, -20, -30], + castShadow: true }); const dirLight1 = new DirectionalLight({ color: [255, 255, 255], intensity: 1.0, - direction: [-10, -20, -30] + direction: [-10, -20, -30], + castShadow: true }); -const lightingEffect = new _LightWithShadowEffect({ambientLight, dirLight0, dirLight1}); +const lightingEffect = new LightingEffect({ambientLight, dirLight0, dirLight1}); const material = new PhongMaterial({ ambient: 0.1, diff --git a/modules/core/src/effects/lighting/light-with-shadow-effect.js b/modules/core/src/effects/lighting/light-with-shadow-effect.js deleted file mode 100644 index 1c0a6d3adc5..00000000000 --- a/modules/core/src/effects/lighting/light-with-shadow-effect.js +++ /dev/null @@ -1,136 +0,0 @@ -import LightingEffect from './lighting-effect'; -import ShadowPass from '../../passes/shadow-pass'; -import {Matrix4, Vector3} from 'math.gl'; -import {Texture2D} from '@luma.gl/core'; -import {default as shadow} from '../../shaderlib/shadow/shadow'; -import {setDefaultShaderModules, getDefaultShaderModules} from '@luma.gl/core'; - -const DEFAULT_SHADOW_COLOR = [0, 0, 0, 200 / 255]; - -export default class LightWithShadowEffect extends LightingEffect { - constructor(props) { - super(props); - this.shadowColor = DEFAULT_SHADOW_COLOR; - this.shadowPasses = []; - this.lightMatrices = []; - this.dummyShadowMaps = []; - this._addShadowModule(); - } - - prepare(gl, {layers, viewports, onViewportActive, views, pixelRatio}) { - this._createLightMatrix(); - - if (this.shadowPasses.length === 0) { - this._createShadowPasses(gl, pixelRatio); - } - - if (this.dummyShadowMaps.length === 0) { - this._createDummyShadowMaps(gl); - } - - const shadowMaps = []; - - for (let i = 0; i < this.shadowPasses.length; i++) { - const shadowPass = this.shadowPasses[i]; - shadowPass.render({ - layers, - viewports, - onViewportActive, - views, - effectProps: { - shadow_lightId: i, - dummyShadowMaps: this.dummyShadowMaps, - shadow_viewProjectionMatrices: this.lightMatrices - } - }); - shadowMaps.push(shadowPass.shadowMap); - } - - return { - shadowMaps, - dummyShadowMaps: this.dummyShadowMaps, - shadow_lightId: 0, - shadowColor: this.shadowColor, - shadow_viewProjectionMatrices: this.lightMatrices - }; - } - - cleanup() { - for (const shadowPass of this.shadowPasses) { - shadowPass.delete(); - } - this.shadowPasses.length = 0; - - for (const dummyShadowMap of this.dummyShadowMaps) { - dummyShadowMap.delete(); - } - this.dummyShadowMaps.length = 0; - - this._removeShadowModule(); - } - - _createLightMatrix() { - const projectionMatrix = new Matrix4().ortho({ - left: -1, - right: 1, - bottom: -1, - top: 1, - near: 0, - far: 2 - }); - - this.lightMatrices = []; - for (const light of this.directionalLights) { - const viewMatrix = new Matrix4() - .lookAt({ - eye: new Vector3(light.direction).negate() - }) - // arbitrary number that covers enough grounds - .scale(1e-3); - const viewProjectionMatrix = projectionMatrix.clone().multiplyRight(viewMatrix); - this.lightMatrices.push(viewProjectionMatrix); - } - } - - _createShadowPasses(gl, pixelRatio) { - for (let i = 0; i < this.directionalLights.length; i++) { - this.shadowPasses.push(new ShadowPass(gl, {pixelRatio})); - } - } - _createDummyShadowMaps(gl) { - for (let i = 0; i < this.directionalLights.length; i++) { - this.dummyShadowMaps.push( - new Texture2D(gl, { - width: 1, - height: 1 - }) - ); - } - } - - _addShadowModule() { - const defaultShaderModules = getDefaultShaderModules(); - let hasShadowModule = false; - for (const module of defaultShaderModules) { - if (module.name === `shadow`) { - hasShadowModule = true; - break; - } - } - if (!hasShadowModule) { - defaultShaderModules.push(shadow); - setDefaultShaderModules(defaultShaderModules); - } - } - - _removeShadowModule() { - const defaultShaderModules = getDefaultShaderModules(); - for (let i = 0; i < defaultShaderModules.length; i++) { - if (defaultShaderModules[i].name === `shadow`) { - defaultShaderModules.splice(i, 1); - setDefaultShaderModules(defaultShaderModules); - break; - } - } - } -} diff --git a/modules/core/src/effects/lighting/lighting-effect.js b/modules/core/src/effects/lighting/lighting-effect.js index 432c2fd2d1b..0321aed543a 100644 --- a/modules/core/src/effects/lighting/lighting-effect.js +++ b/modules/core/src/effects/lighting/lighting-effect.js @@ -1,6 +1,14 @@ -import {AmbientLight} from '@luma.gl/core'; +import { + AmbientLight, + Texture2D, + setDefaultShaderModules, + getDefaultShaderModules +} from '@luma.gl/core'; import DirectionalLight from './directional-light'; import Effect from '../../lib/effect'; +import {Matrix4, Vector3} from 'math.gl'; +import ShadowPass from '../../passes/shadow-pass'; +import {default as shadow} from '../../shaderlib/shadow/shadow'; const DefaultAmbientLightProps = {color: [255, 255, 255], intensity: 1.0}; const DefaultDirectionalLightProps = [ @@ -15,6 +23,7 @@ const DefaultDirectionalLightProps = [ direction: [1, 8, -2.5] } ]; +const DefaultShadowColor = [0, 0, 0, 200 / 255]; // Class to manage ambient, point and directional light sources in deck export default class LightingEffect extends Effect { @@ -24,6 +33,12 @@ export default class LightingEffect extends Effect { this.directionalLights = []; this.pointLights = []; + this.shadowColor = DefaultShadowColor; + this.shadowPasses = []; + this.lightMatrices = []; + this.dummyShadowMaps = []; + this.castShadow = false; + for (const key in props) { const lightSource = props[key]; @@ -42,20 +57,143 @@ export default class LightingEffect extends Effect { default: } } - this.applyDefaultLights(); + this._applyDefaultLights(); + + if (this.directionalLights.some(light => light.castShadow)) { + this.castShadow = true; + this._addShadowModule(); + } + } + + prepare(gl, {layers, viewports, onViewportActive, views, pixelRatio}) { + if (!this.castShadow) return {}; + + this._createLightMatrix(); + + if (this.shadowPasses.length === 0) { + this._createShadowPasses(gl, pixelRatio); + } + + if (this.dummyShadowMaps.length === 0) { + this._createDummyShadowMaps(gl); + } + + const shadowMaps = []; + + for (let i = 0; i < this.shadowPasses.length; i++) { + const shadowPass = this.shadowPasses[i]; + shadowPass.render({ + layers, + viewports, + onViewportActive, + views, + effectProps: { + shadow_lightId: i, + dummyShadowMaps: this.dummyShadowMaps, + shadow_viewProjectionMatrices: this.lightMatrices + } + }); + shadowMaps.push(shadowPass.shadowMap); + } + + return { + shadowMaps, + dummyShadowMaps: this.dummyShadowMaps, + shadow_lightId: 0, + shadowColor: this.shadowColor, + shadow_viewProjectionMatrices: this.lightMatrices + }; } getParameters(layer) { const {ambientLight} = this; - const pointLights = this.getProjectedPointLights(layer); - const directionalLights = this.getProjectedDirectionalLights(layer); + const pointLights = this._getProjectedPointLights(layer); + const directionalLights = this._getProjectedDirectionalLights(layer); return { lightSources: {ambientLight, directionalLights, pointLights} }; } - // Private - applyDefaultLights() { + cleanup() { + for (const shadowPass of this.shadowPasses) { + shadowPass.delete(); + } + this.shadowPasses.length = 0; + + for (const dummyShadowMap of this.dummyShadowMaps) { + dummyShadowMap.delete(); + } + this.dummyShadowMaps.length = 0; + + this._removeShadowModule(); + } + + _createLightMatrix() { + const projectionMatrix = new Matrix4().ortho({ + left: -1, + right: 1, + bottom: -1, + top: 1, + near: 0, + far: 2 + }); + + this.lightMatrices = []; + for (const light of this.directionalLights) { + const viewMatrix = new Matrix4() + .lookAt({ + eye: new Vector3(light.direction).negate() + }) + // arbitrary number that covers enough grounds + .scale(1e-3); + const viewProjectionMatrix = projectionMatrix.clone().multiplyRight(viewMatrix); + this.lightMatrices.push(viewProjectionMatrix); + } + } + + _createShadowPasses(gl, pixelRatio) { + for (let i = 0; i < this.directionalLights.length; i++) { + this.shadowPasses.push(new ShadowPass(gl, {pixelRatio})); + } + } + _createDummyShadowMaps(gl) { + for (let i = 0; i < this.directionalLights.length; i++) { + this.dummyShadowMaps.push( + new Texture2D(gl, { + width: 1, + height: 1 + }) + ); + } + } + + _addShadowModule() { + const defaultShaderModules = getDefaultShaderModules(); + let hasShadowModule = false; + for (const module of defaultShaderModules) { + if (module.name === `shadow`) { + hasShadowModule = true; + break; + } + } + if (!hasShadowModule) { + defaultShaderModules.push(shadow); + setDefaultShaderModules(defaultShaderModules); + } + } + + _removeShadowModule() { + const defaultShaderModules = getDefaultShaderModules(); + for (let i = 0; i < defaultShaderModules.length; i++) { + if (defaultShaderModules[i].name === `shadow`) { + defaultShaderModules.splice(i, 1); + setDefaultShaderModules(defaultShaderModules); + break; + } + } + } + + _applyDefaultLights() { const {ambientLight, pointLights, directionalLights} = this; if (!ambientLight && pointLights.length === 0 && directionalLights.length === 0) { this.ambientLight = new AmbientLight(DefaultAmbientLightProps); @@ -64,7 +202,7 @@ export default class LightingEffect extends Effect { } } - getProjectedPointLights(layer) { + _getProjectedPointLights(layer) { const projectedPointLights = []; for (let i = 0; i < this.pointLights.length; i++) { @@ -74,7 +212,7 @@ export default class LightingEffect extends Effect { return projectedPointLights; } - getProjectedDirectionalLights(layer) { + _getProjectedDirectionalLights(layer) { const projectedDirectionalLights = []; for (let i = 0; i < this.directionalLights.length; i++) { diff --git a/modules/core/src/index.js b/modules/core/src/index.js index 40cc94ac66d..6e05d0d4183 100644 --- a/modules/core/src/index.js +++ b/modules/core/src/index.js @@ -35,7 +35,6 @@ export {default as DirectionalLight} from './effects/lighting/directional-light' export {default as _CameraLight} from './effects/lighting/camera-light'; export {default as _SunLight} from './effects/lighting/sun-light'; export {default as PostProcessEffect} from './effects/post-process-effect'; -export {default as _LightWithShadowEffect} from './effects/lighting/light-with-shadow-effect'; // Passes export {default as _LayersPass} from './passes/layers-pass'; diff --git a/test/modules/core/effects/light-with-shadow-effect.spec.js b/test/modules/core/effects/light-with-shadow-effect.spec.js deleted file mode 100644 index dcef150be3e..00000000000 --- a/test/modules/core/effects/light-with-shadow-effect.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import test from 'tape-catch'; -import LightWithShadowEffect from '@deck.gl/core/effects/lighting/light-with-shadow-effect'; -import {gl} from '@deck.gl/test-utils'; -import {MapView, PolygonLayer} from 'deck.gl'; -import * as FIXTURES from 'deck.gl-test/data'; -import {getDefaultShaderModules} from '@luma.gl/core'; - -test('LightWithShadowEffect#constructor', t => { - const lightingEffect = new LightWithShadowEffect(); - lightingEffect.cleanup(); - - t.ok(lightingEffect, 'LightWithShadowEffect created'); - t.ok(lightingEffect.ambientLight, 'Default ambient light created'); - t.equal(lightingEffect.directionalLights.length, 2, 'Default directional lights created'); - t.end(); -}); - -test('LightWithShadowEffect#prepare and cleanup', t => { - const lightingEffect = new LightWithShadowEffect(); - - const viewport = new MapView().makeViewport({ - width: 100, - height: 100, - viewState: {longitude: -122, latitude: 37, zoom: 13} - }); - - const layer = new PolygonLayer({ - data: FIXTURES.polygons.slice(0, 3), - getPolygon: f => f, - getFillColor: (f, {index}) => [index, 0, 0] - }); - - layer.context = {viewport}; - lightingEffect.prepare(gl, {layers: [layer], viewports: [viewport], pixelRatio: 1}); - - t.equal(lightingEffect.lightMatrices.length, 2, 'LightWithShadowEffect prepares light matrices'); - t.equal(lightingEffect.shadowPasses.length, 2, 'LightWithShadowEffect prepares shadow passes'); - t.equal( - lightingEffect.dummyShadowMaps.length, - 2, - 'LightWithShadowEffect prepares dummy shadow maps' - ); - - lightingEffect.cleanup(); - t.equal(lightingEffect.shadowPasses.length, 0, 'LightWithShadowEffect prepares shadow passes'); - t.equal( - lightingEffect.dummyShadowMaps.length, - 0, - 'LightWithShadowEffect prepares dummy shadow maps' - ); - t.end(); -}); - -test('LightWithShadowEffect#shadow module', t => { - const lightingEffect = new LightWithShadowEffect(); - const defaultModules = getDefaultShaderModules(); - let hasShadow = defaultModules.find(m => m.name === 'shadow'); - t.equal(hasShadow, true, 'LightWithShadowEffect adds shadow module to default correctly'); - - lightingEffect.cleanup(); - hasShadow = defaultModules.find(m => m.name === 'shadow'); - t.equal(hasShadow, false, 'LightWithShadowEffect removes shadow module to default correctly'); - t.end(); -}); diff --git a/test/modules/core/effects/lighting-effect.spec.js b/test/modules/core/effects/lighting-effect.spec.js index f35803fefc2..1230ebceec3 100644 --- a/test/modules/core/effects/lighting-effect.spec.js +++ b/test/modules/core/effects/lighting-effect.spec.js @@ -1,9 +1,10 @@ import test from 'tape-catch'; import LightingEffect from '@deck.gl/core/effects/lighting/lighting-effect'; -import {_CameraLight as CameraLight, PointLight} from '@deck.gl/core'; - -import {MapView, PolygonLayer} from 'deck.gl'; +import {_CameraLight as CameraLight, DirectionalLight, PointLight} from '@deck.gl/core'; +import {getDefaultShaderModules} from '@luma.gl/core'; +import {MapView, PolygonLayer, LayerManager} from 'deck.gl'; import * as FIXTURES from 'deck.gl-test/data'; +import {gl} from '@deck.gl/test-utils'; test('LightingEffect#constructor', t => { const lightingEffect = new LightingEffect(); @@ -31,7 +32,7 @@ test('LightingEffect#CameraLight', t => { layer.context = {viewport}; - const projectedLights = lightEffect.getProjectedPointLights(layer); + const projectedLights = lightEffect._getProjectedPointLights(layer); t.ok(projectedLights[0], 'Camera light is ok'); t.deepEqual(projectedLights[0].position, [0, 0, 150], 'Camera light projection is ok'); @@ -67,9 +68,79 @@ test('LightingEffect#PointLight', t => { pointLight.intensity = 2.0; pointLight.color = [255, 0, 0]; - const projectedLights = lightEffect.getProjectedPointLights(layer); + const projectedLights = lightEffect._getProjectedPointLights(layer); t.ok(projectedLights[0], 'point light is ok'); t.equal(projectedLights[0].intensity, 2.0, 'point light intensity is ok'); t.deepEqual(projectedLights[0].color, [255, 0, 0], 'point light color is ok'); t.end(); }); + +test('LightingEffect#prepare and cleanup', t => { + const dirLight0 = new DirectionalLight({ + color: [255, 255, 255], + intensity: 1.0, + direction: [10, -20, -30], + castShadow: true + }); + + const dirLight1 = new DirectionalLight({ + color: [255, 255, 255], + intensity: 1.0, + direction: [-10, -20, -30], + castShadow: true + }); + + const lightingEffect = new LightingEffect({dirLight0, dirLight1}); + + const viewport = new MapView().makeViewport({ + width: 100, + height: 100, + viewState: {longitude: -122, latitude: 37, zoom: 13} + }); + + const layer = new PolygonLayer({ + data: FIXTURES.polygons.slice(0, 3), + getPolygon: f => f, + getFillColor: (f, {index}) => [index, 0, 0] + }); + + layer.context = {viewport}; + + const layerManager = new LayerManager(gl, {viewport}); + layerManager.setLayers([layer]); + + lightingEffect.prepare(gl, { + layers: layerManager.getLayers(), + onViewportActive: layerManager.activateViewport, + viewports: [viewport], + pixelRatio: 1 + }); + + t.equal(lightingEffect.lightMatrices.length, 2, 'LightingEffect prepares light matrices'); + t.equal(lightingEffect.shadowPasses.length, 2, 'LightingEffect prepares shadow passes'); + t.equal(lightingEffect.dummyShadowMaps.length, 2, 'LightingEffect prepares dummy shadow maps'); + + lightingEffect.cleanup(); + t.equal(lightingEffect.shadowPasses.length, 0, 'LightingEffect prepares shadow passes'); + t.equal(lightingEffect.dummyShadowMaps.length, 0, 'LightingEffect prepares dummy shadow maps'); + t.end(); +}); + +test('LightingEffect#shadow module', t => { + const dirLight = new DirectionalLight({ + color: [255, 255, 255], + intensity: 1.0, + direction: [10, -20, -30], + castShadow: true + }); + + const lightingEffect = new LightingEffect({dirLight}); + const defaultModules = getDefaultShaderModules(); + let hasShadow = defaultModules.some(m => m.name === 'shadow'); + t.equal(hasShadow, true, 'LightingEffect adds shadow module to default correctly'); + + lightingEffect.cleanup(); + hasShadow = defaultModules.some(m => m.name === 'shadow'); + t.equal(hasShadow, false, 'LightingEffect removes shadow module to default correctly'); + t.end(); +});