@deck.gl/core
Version:
deck.gl core library
207 lines (177 loc) • 6.35 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import type {Device} from '@luma.gl/core';
import {Texture} from '@luma.gl/core';
import {AmbientLight} from './ambient-light';
import {DirectionalLight} from './directional-light';
import {PointLight} from './point-light';
import {Matrix4, Vector3} from '@math.gl/core';
import ShadowPass from '../../passes/shadow-pass';
import shadow from '../../shaderlib/shadow/shadow';
import type {LightingProps} from '@luma.gl/shadertools';
import type {ShadowModuleProps} from '../../shaderlib/shadow/shadow';
import type Layer from '../../lib/layer';
import type {Effect, EffectContext, PreRenderOptions} from '../../lib/effect';
const DEFAULT_AMBIENT_LIGHT_PROPS = {
color: [255, 255, 255] as [number, number, number],
intensity: 1.0
};
const DEFAULT_DIRECTIONAL_LIGHT_PROPS = [
{
color: [255, 255, 255] as [number, number, number],
intensity: 1.0,
direction: [-1, 3, -1] as [number, number, number]
},
{
color: [255, 255, 255] as [number, number, number],
intensity: 0.9,
direction: [1, -8, -2.5] as [number, number, number]
}
];
const DEFAULT_SHADOW_COLOR = [0, 0, 0, 200 / 255] as [number, number, number, number];
export type LightingEffectProps = Record<string, PointLight | DirectionalLight | AmbientLight>;
// Class to manage ambient, point and directional light sources in deck
export default class LightingEffect implements Effect {
id = 'lighting-effect';
props!: LightingEffectProps;
shadowColor: [number, number, number, number] = DEFAULT_SHADOW_COLOR;
context?: EffectContext;
private shadow: boolean = false;
private ambientLight?: AmbientLight;
private directionalLights: DirectionalLight[] = [];
private pointLights: PointLight[] = [];
private shadowPasses: ShadowPass[] = [];
private dummyShadowMap: Texture | null = null;
private shadowMatrices?: Matrix4[];
constructor(props: LightingEffectProps = {}) {
this.setProps(props);
}
setup(context: EffectContext) {
this.context = context;
const {device, deck} = context;
if (this.shadow && !this.dummyShadowMap) {
this._createShadowPasses(device);
deck._addDefaultShaderModule(shadow);
this.dummyShadowMap = device.createTexture({
width: 1,
height: 1
});
}
}
setProps(props: LightingEffectProps) {
this.ambientLight = undefined;
this.directionalLights = [];
this.pointLights = [];
for (const key in props) {
const lightSource = props[key];
switch (lightSource.type) {
case 'ambient':
this.ambientLight = lightSource;
break;
case 'directional':
this.directionalLights.push(lightSource);
break;
case 'point':
this.pointLights.push(lightSource);
break;
default:
}
}
this._applyDefaultLights();
this.shadow = this.directionalLights.some(light => light.shadow);
if (this.context) {
// Create resources if necessary
this.setup(this.context);
}
this.props = props;
}
preRender({layers, layerFilter, viewports, onViewportActive, views}: PreRenderOptions) {
if (!this.shadow) return;
// create light matrix every frame to make sure always updated from light source
this.shadowMatrices = this._calculateMatrices();
for (let i = 0; i < this.shadowPasses.length; i++) {
const shadowPass = this.shadowPasses[i];
shadowPass.render({
layers,
layerFilter,
viewports,
onViewportActive,
views,
shaderModuleProps: {
shadow: {
shadowLightId: i,
dummyShadowMap: this.dummyShadowMap,
shadowMatrices: this.shadowMatrices
}
}
});
}
}
getShaderModuleProps(layer: Layer, otherShaderModuleProps: Record<string, any>) {
const shadowProps = this.shadow
? ({
project: otherShaderModuleProps.project,
shadowMaps: this.shadowPasses.map(shadowPass => shadowPass.getShadowMap()),
dummyShadowMap: this.dummyShadowMap!,
shadowColor: this.shadowColor,
shadowMatrices: this.shadowMatrices
} satisfies ShadowModuleProps)
: {};
// when not rendering to screen, turn off lighting by adding empty light source object
// lights shader module relies on the `lightSources` to turn on/off lighting
const lightingProps: LightingProps = {
enabled: true,
ambientLight: this.ambientLight,
directionalLights: this.directionalLights.map(directionalLight =>
directionalLight.getProjectedLight({layer})
),
pointLights: this.pointLights.map(pointLight => pointLight.getProjectedLight({layer}))
};
// @ts-expect-error material is not a Layer prop
const materialProps = layer.props.material;
return {
shadow: shadowProps,
lighting: lightingProps,
phongMaterial: materialProps,
gouraudMaterial: materialProps
};
}
cleanup(context: EffectContext): void {
for (const shadowPass of this.shadowPasses) {
shadowPass.delete();
}
this.shadowPasses.length = 0;
if (this.dummyShadowMap) {
this.dummyShadowMap.destroy();
this.dummyShadowMap = null;
context.deck._removeDefaultShaderModule(shadow);
}
}
private _calculateMatrices(): Matrix4[] {
const lightMatrices: Matrix4[] = [];
for (const light of this.directionalLights) {
const viewMatrix = new Matrix4().lookAt({
eye: new Vector3(light.direction).negate()
});
lightMatrices.push(viewMatrix);
}
return lightMatrices;
}
private _createShadowPasses(device: Device): void {
for (let i = 0; i < this.directionalLights.length; i++) {
const shadowPass = new ShadowPass(device);
this.shadowPasses[i] = shadowPass;
}
}
private _applyDefaultLights(): void {
const {ambientLight, pointLights, directionalLights} = this;
if (!ambientLight && pointLights.length === 0 && directionalLights.length === 0) {
this.ambientLight = new AmbientLight(DEFAULT_AMBIENT_LIGHT_PROPS);
this.directionalLights.push(
new DirectionalLight(DEFAULT_DIRECTIONAL_LIGHT_PROPS[0]),
new DirectionalLight(DEFAULT_DIRECTIONAL_LIGHT_PROPS[1])
);
}
}
}