@luma.gl/shadertools
Version:
Shader module system for luma.gl
366 lines (325 loc) • 11.6 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {log} from '@luma.gl/core';
import {ShaderModule} from '../../../lib/shader-module/shader-module';
import {lightingUniformsGLSL} from './lighting-glsl';
import {lightingUniformsWGSL} from './lighting-wgsl';
import type {NumberArray2, NumberArray3} from '@math.gl/core';
import {normalizeByteColor3, resolveUseByteColors} from '../../../lib/color/normalize-byte-colors';
/** Max number of supported lights (in addition to ambient light */
const MAX_LIGHTS = 5;
/** Supported light source descriptions accepted by the lighting shader module. */
export type Light = AmbientLight | PointLight | SpotLight | DirectionalLight;
/** Ambient light contribution shared across the entire scene. */
export type AmbientLight = {
/** Discriminator used to identify ambient lights in `lights: Light[]`. */
type: 'ambient';
/** RGB light color in the existing `0..255` convention used by luma.gl materials. */
color?: Readonly<NumberArray3>;
/** Scalar intensity multiplier applied to the light color. */
intensity?: number;
};
/** Omnidirectional point light emitted from a world-space position. */
export type PointLight = {
/** Discriminator used to identify point lights in `lights: Light[]`. */
type: 'point';
/** World-space light position. */
position: Readonly<NumberArray3>;
/** RGB light color in the existing `0..255` convention used by luma.gl materials. */
color?: Readonly<NumberArray3>;
/** Scalar intensity multiplier applied to the light color. */
intensity?: number;
/** Constant, linear, and quadratic attenuation coefficients. */
attenuation?: Readonly<NumberArray3>;
};
/** Directional light defined only by its incoming world-space direction. */
export type DirectionalLight = {
/** Discriminator used to identify directional lights in `lights: Light[]`. */
type: 'directional';
/** World-space light direction. */
direction: Readonly<NumberArray3>;
/** RGB light color in the existing `0..255` convention used by luma.gl materials. */
color?: Readonly<NumberArray3>;
/** Scalar intensity multiplier applied to the light color. */
intensity?: number;
};
/** Cone-shaped light emitted from a position and focused along a direction. */
export type SpotLight = {
/** Discriminator used to identify spot lights in `lights: Light[]`. */
type: 'spot';
/** World-space light position. */
position: Readonly<NumberArray3>;
/** World-space light direction. */
direction: Readonly<NumberArray3>;
/** RGB light color in the existing `0..255` convention used by luma.gl materials. */
color?: Readonly<NumberArray3>;
/** Scalar intensity multiplier applied to the light color. */
intensity?: number;
/** Constant, linear, and quadratic attenuation coefficients. */
attenuation?: Readonly<NumberArray3>;
/** Inner spotlight cone angle in radians. */
innerConeAngle?: number;
/** Outer spotlight cone angle in radians. */
outerConeAngle?: number;
};
/** Public JavaScript props accepted by the `lighting` shader module. */
export type LightingProps = {
/** Enables or disables lighting calculations for the module. */
enabled?: boolean;
/** When true, light colors are interpreted as byte-style 0-255 values. */
useByteColors?: boolean;
/** Preferred API for supplying mixed ambient, point, spot, and directional lights. */
lights?: Light[];
/**
* Legacy ambient-light prop.
* @deprecated Use `lights` with `{type: 'ambient', ...}` entries instead.
*/
ambientLight?: AmbientLight;
/**
* Legacy point-light prop.
* @deprecated Use `lights` with `{type: 'point', ...}` entries instead.
*/
pointLights?: PointLight[];
/**
* Legacy spot-light prop.
* @deprecated Use `lights` with `{type: 'spot', ...}` entries instead.
*/
spotLights?: SpotLight[];
/**
* Legacy directional-light prop.
* @deprecated Use `lights` with `{type: 'directional', ...}` entries instead.
*/
directionalLights?: DirectionalLight[];
};
/** Packed per-light data written into the module's fixed-size uniform array. */
export type LightingLightUniform = {
/** Light color converted to normalized shader-space RGB. */
color: Readonly<NumberArray3>;
/** World-space light position or a default placeholder for non-positional lights. */
position: Readonly<NumberArray3>;
/** World-space light direction or a default placeholder for positional lights. */
direction: Readonly<NumberArray3>;
/** Constant, linear, and quadratic attenuation coefficients. */
attenuation: Readonly<NumberArray3>;
/** Cosines of the inner and outer spotlight cone angles. */
coneCos: Readonly<NumberArray2>;
};
/** Fully normalized uniform values produced by the `lighting` shader module. */
export type LightingUniforms = {
/** `1` when lighting is enabled, otherwise `0`. */
enabled: number;
/** Number of packed directional lights in the `lights` array. */
directionalLightCount: number;
/** Number of packed point lights in the `lights` array. */
pointLightCount: number;
/** Number of packed spot lights in the `lights` array. */
spotLightCount: number;
/** Accumulated ambient color converted to normalized shader-space RGB. */
ambientColor: Readonly<NumberArray3>;
/** Packed trailing array of non-ambient light structs. */
lights: ReadonlyArray<LightingLightUniform>;
};
const LIGHT_UNIFORM_TYPE = {
color: 'vec3<f32>',
position: 'vec3<f32>',
direction: 'vec3<f32>',
attenuation: 'vec3<f32>',
coneCos: 'vec2<f32>'
} as const;
/**
* Portable lighting shader module shared by the Phong, Gouraud, and PBR material modules.
*
* The public JavaScript API accepts `lights: Light[]`, while the uniform buffer packs
* non-ambient lights into a fixed-size trailing array for portability across WebGL2 and WebGPU.
*/
export const lighting = {
props: {} as LightingProps,
uniforms: {} as LightingUniforms,
name: 'lighting',
defines: {
// MAX_LIGHTS
},
uniformTypes: {
enabled: 'i32',
directionalLightCount: 'i32',
pointLightCount: 'i32',
spotLightCount: 'i32',
ambientColor: 'vec3<f32>',
lights: [LIGHT_UNIFORM_TYPE, MAX_LIGHTS]
},
defaultUniforms: createDefaultLightingUniforms(),
bindingLayout: [{name: 'lighting', group: 2}],
firstBindingSlot: 0,
source: lightingUniformsWGSL,
vs: lightingUniformsGLSL,
fs: lightingUniformsGLSL,
getUniforms
} as const satisfies ShaderModule<LightingProps, LightingUniforms, {}>;
function getUniforms(
props?: LightingProps,
_prevUniforms: Partial<LightingUniforms> = {}
): LightingUniforms {
// Copy props so we can modify
props = props ? {...props} : props;
// TODO legacy
if (!props) {
return createDefaultLightingUniforms();
}
// Support for array of lights. Type of light is detected by type field
if (props.lights) {
props = {...props, ...extractLightTypes(props.lights), lights: undefined};
}
// Specify lights separately
const {useByteColors, ambientLight, pointLights, spotLights, directionalLights} = props || {};
const hasLights =
ambientLight ||
(pointLights && pointLights.length > 0) ||
(spotLights && spotLights.length > 0) ||
(directionalLights && directionalLights.length > 0);
// TODO - this may not be the correct decision
if (!hasLights) {
return {
...createDefaultLightingUniforms(),
enabled: 0
};
}
const uniforms = {
...createDefaultLightingUniforms(),
...getLightSourceUniforms({
useByteColors,
ambientLight,
pointLights,
spotLights,
directionalLights
})
};
if (props.enabled !== undefined) {
uniforms.enabled = props.enabled ? 1 : 0;
}
return uniforms;
}
function getLightSourceUniforms({
useByteColors,
ambientLight,
pointLights = [],
spotLights = [],
directionalLights = []
}: LightingProps): Omit<LightingUniforms, 'enabled'> {
const lights = createDefaultLightUniforms();
let currentLight = 0;
let pointLightCount = 0;
let spotLightCount = 0;
let directionalLightCount = 0;
for (const pointLight of pointLights) {
if (currentLight >= MAX_LIGHTS) {
break;
}
lights[currentLight] = {
...lights[currentLight],
color: convertColor(pointLight, useByteColors),
position: pointLight.position,
attenuation: pointLight.attenuation || [1, 0, 0]
};
currentLight++;
pointLightCount++;
}
for (const spotLight of spotLights) {
if (currentLight >= MAX_LIGHTS) {
break;
}
lights[currentLight] = {
...lights[currentLight],
color: convertColor(spotLight, useByteColors),
position: spotLight.position,
direction: spotLight.direction,
attenuation: spotLight.attenuation || [1, 0, 0],
coneCos: getSpotConeCos(spotLight)
};
currentLight++;
spotLightCount++;
}
for (const directionalLight of directionalLights) {
if (currentLight >= MAX_LIGHTS) {
break;
}
lights[currentLight] = {
...lights[currentLight],
color: convertColor(directionalLight, useByteColors),
direction: directionalLight.direction
};
currentLight++;
directionalLightCount++;
}
if (pointLights.length + spotLights.length + directionalLights.length > MAX_LIGHTS) {
log.warn(`MAX_LIGHTS exceeded, truncating to ${MAX_LIGHTS}`)();
}
return {
ambientColor: convertColor(ambientLight, useByteColors),
directionalLightCount,
pointLightCount,
spotLightCount,
lights
};
}
function extractLightTypes(lights: Light[]): LightingProps {
const lightSources: LightingProps = {pointLights: [], spotLights: [], directionalLights: []};
for (const light of lights || []) {
switch (light.type) {
case 'ambient':
// Note: Only uses last ambient light
// TODO - add ambient light sources on CPU?
lightSources.ambientLight = light;
break;
case 'directional':
lightSources.directionalLights?.push(light);
break;
case 'point':
lightSources.pointLights?.push(light);
break;
case 'spot':
lightSources.spotLights?.push(light);
break;
default:
// eslint-disable-next-line
// console.warn(light.type);
}
}
return lightSources;
}
/** Take color 0-255 and intensity as input and output 0.0-1.0 range */
function convertColor(
colorDef: {color?: Readonly<NumberArray3>; intensity?: number} = {},
useByteColors?: boolean
): NumberArray3 {
const {color = [0, 0, 0], intensity = 1.0} = colorDef;
const normalizedColor = normalizeByteColor3(color, resolveUseByteColors(useByteColors, true));
return normalizedColor.map(component => component * intensity) as NumberArray3;
}
function createDefaultLightingUniforms(): LightingUniforms {
return {
enabled: 1,
directionalLightCount: 0,
pointLightCount: 0,
spotLightCount: 0,
ambientColor: [0.1, 0.1, 0.1],
lights: createDefaultLightUniforms()
};
}
function createDefaultLightUniforms(): LightingLightUniform[] {
return Array.from({length: MAX_LIGHTS}, () => createDefaultLightUniform());
}
function createDefaultLightUniform(): LightingLightUniform {
return {
color: [1, 1, 1],
position: [1, 1, 2],
direction: [1, 1, 1],
attenuation: [1, 0, 0],
coneCos: [1, 0]
};
}
function getSpotConeCos(spotLight: SpotLight): NumberArray2 {
const innerConeAngle = spotLight.innerConeAngle ?? 0;
const outerConeAngle = spotLight.outerConeAngle ?? Math.PI / 4;
return [Math.cos(innerConeAngle), Math.cos(outerConeAngle)];
}