@luma.gl/shadertools
Version:
Shader module system for luma.gl
200 lines • 6.81 kB
JavaScript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { log } from '@luma.gl/core';
import { lightingUniformsGLSL } from "./lighting-glsl.js";
import { lightingUniformsWGSL } from "./lighting-wgsl.js";
import { normalizeByteColor3, resolveUseByteColors } from "../../../lib/color/normalize-byte-colors.js";
/** Max number of supported lights (in addition to ambient light */
const MAX_LIGHTS = 5;
const LIGHT_UNIFORM_TYPE = {
color: 'vec3<f32>',
position: 'vec3<f32>',
direction: 'vec3<f32>',
attenuation: 'vec3<f32>',
coneCos: 'vec2<f32>'
};
/**
* 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: {},
uniforms: {},
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
};
function getUniforms(props, _prevUniforms = {}) {
// 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 = [] }) {
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) {
const lightSources = { 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 = {}, useByteColors) {
const { color = [0, 0, 0], intensity = 1.0 } = colorDef;
const normalizedColor = normalizeByteColor3(color, resolveUseByteColors(useByteColors, true));
return normalizedColor.map(component => component * intensity);
}
function createDefaultLightingUniforms() {
return {
enabled: 1,
directionalLightCount: 0,
pointLightCount: 0,
spotLightCount: 0,
ambientColor: [0.1, 0.1, 0.1],
lights: createDefaultLightUniforms()
};
}
function createDefaultLightUniforms() {
return Array.from({ length: MAX_LIGHTS }, () => createDefaultLightUniform());
}
function createDefaultLightUniform() {
return {
color: [1, 1, 1],
position: [1, 1, 2],
direction: [1, 1, 1],
attenuation: [1, 0, 0],
coneCos: [1, 0]
};
}
function getSpotConeCos(spotLight) {
const innerConeAngle = spotLight.innerConeAngle ?? 0;
const outerConeAngle = spotLight.outerConeAngle ?? Math.PI / 4;
return [Math.cos(innerConeAngle), Math.cos(outerConeAngle)];
}
//# sourceMappingURL=lighting.js.map