// 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; /** 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; /** RGB light color in the existing `0..255` convention used by luma.gl materials. */ color?: Readonly; /** Scalar intensity multiplier applied to the light color. */ intensity?: number; /** Constant, linear, and quadratic attenuation coefficients. */ attenuation?: Readonly; }; /** 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; /** RGB light color in the existing `0..255` convention used by luma.gl materials. */ color?: Readonly; /** 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; /** World-space light direction. */ direction: Readonly; /** RGB light color in the existing `0..255` convention used by luma.gl materials. */ color?: Readonly; /** Scalar intensity multiplier applied to the light color. */ intensity?: number; /** Constant, linear, and quadratic attenuation coefficients. */ attenuation?: Readonly; /** 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; /** World-space light position or a default placeholder for non-positional lights. */ position: Readonly; /** World-space light direction or a default placeholder for positional lights. */ direction: Readonly; /** Constant, linear, and quadratic attenuation coefficients. */ attenuation: Readonly; /** Cosines of the inner and outer spotlight cone angles. */ coneCos: Readonly; }; /** 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; /** Packed trailing array of non-ambient light structs. */ lights: ReadonlyArray; }; const LIGHT_UNIFORM_TYPE = { color: 'vec3', position: 'vec3', direction: 'vec3', attenuation: 'vec3', coneCos: 'vec2' } 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', 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; function getUniforms( props?: LightingProps, _prevUniforms: Partial = {} ): 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 { 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; 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)]; }