@luma.gl/engine
Version:
3D Engine Components for luma.gl
395 lines (381 loc) • 15.2 kB
JavaScript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { Matrix4 } from '@math.gl/core';
import { Model } from "../model/model.js";
import { ShaderInputs } from "../shader-inputs.js";
const DEFAULT_POINT_LIGHT_RADIUS_FACTOR = 0.02;
const DEFAULT_SPOT_LIGHT_LENGTH_FACTOR = 0.12;
const DEFAULT_DIRECTIONAL_LIGHT_LENGTH_FACTOR = 0.15;
const DEFAULT_DIRECTIONAL_LIGHT_RADIUS_FACTOR = 0.2;
const DEFAULT_DIRECTION_FALLBACK = [0, 1, 0];
const DEFAULT_LIGHT_COLOR = [255, 255, 255];
const DEFAULT_MARKER_SCALE = 1;
const DIRECTIONAL_ANCHOR_DISTANCE_FACTOR = 0.35;
const LIGHT_COLOR_FACTOR = 255;
const MIN_SCENE_SCALE = 1;
const SPOTLIGHT_OUTER_CONE_EPSILON = 0.01;
const LIGHT_MARKER_PARAMETERS = {
depthCompare: 'less-equal',
depthWriteEnabled: false,
cullMode: 'none'
};
const INSTANCE_BUFFER_LAYOUT = [
{ name: 'instancePosition', format: 'float32x3', stepMode: 'instance' },
{ name: 'instanceDirection', format: 'float32x3', stepMode: 'instance' },
{ name: 'instanceScale', format: 'float32x3', stepMode: 'instance' },
{ name: 'instanceColor', format: 'float32x4', stepMode: 'instance' }
];
const lightMarker = {
name: 'lightMarker',
props: {},
uniforms: {},
uniformTypes: {
viewProjectionMatrix: 'mat4x4<f32>'
}
};
const CENTERED_LOCAL_POSITION_WGSL = 'inputs.positions * inputs.instanceScale';
const APEX_LOCAL_POSITION_WGSL = 'vec3<f32>(inputs.positions.x * inputs.instanceScale.x, (inputs.positions.y - 0.5) * inputs.instanceScale.y, inputs.positions.z * inputs.instanceScale.z)';
const CENTERED_LOCAL_POSITION_GLSL = 'positions * instanceScale';
const APEX_LOCAL_POSITION_GLSL = 'vec3(positions.x * instanceScale.x, (positions.y - 0.5) * instanceScale.y, positions.z * instanceScale.z)';
export class BaseLightModel extends Model {
lightModelProps;
_instanceData;
_managedBuffers;
buildInstanceData;
sizePropNames;
constructor(device, props, options) {
const instanceData = options.buildInstanceData(props);
const managedBuffers = createManagedInstanceBuffers(device, props.id || options.idPrefix, instanceData);
const shaderInputs = new ShaderInputs({ lightMarker });
shaderInputs.setProps({
lightMarker: { viewProjectionMatrix: createViewProjectionMatrix(props) }
});
const { source, vs, fs } = getLightMarkerShaders(options.anchorMode);
const modelProps = props;
super(device, {
...modelProps,
id: props.id || options.idPrefix,
source,
vs,
fs,
geometry: options.geometry,
shaderInputs,
bufferLayout: [...INSTANCE_BUFFER_LAYOUT],
attributes: managedBuffers,
instanceCount: instanceData.instanceCount,
parameters: mergeLightMarkerParameters(props.parameters)
});
this.lightModelProps = props;
this._instanceData = instanceData;
this._managedBuffers = managedBuffers;
this.buildInstanceData = options.buildInstanceData;
this.sizePropNames = options.sizePropNames;
}
destroy() {
super.destroy();
destroyManagedInstanceBuffers(this._managedBuffers);
this._managedBuffers = {};
}
draw(renderPass) {
if (this.instanceCount === 0) {
return true;
}
return super.draw(renderPass);
}
setProps(props) {
this.lightModelProps = { ...this.lightModelProps, ...props };
if (props.parameters) {
this.setParameters(mergeLightMarkerParameters(this.lightModelProps.parameters));
}
if ('viewMatrix' in props || 'projectionMatrix' in props) {
this.shaderInputs.setProps({
lightMarker: { viewProjectionMatrix: createViewProjectionMatrix(this.lightModelProps) }
});
this.setNeedsRedraw('lightMarker camera');
}
if (shouldRebuildInstanceData(props, this.sizePropNames)) {
this.rebuildInstanceData();
}
}
rebuildInstanceData() {
const nextInstanceData = this.buildInstanceData(this.lightModelProps);
const nextManagedBuffers = createManagedInstanceBuffers(this.device, this.id, nextInstanceData);
this.setAttributes(nextManagedBuffers);
this.setInstanceCount(nextInstanceData.instanceCount);
destroyManagedInstanceBuffers(this._managedBuffers);
this._managedBuffers = nextManagedBuffers;
this._instanceData = nextInstanceData;
}
}
export function buildPointLightInstanceData(props) {
const pointLights = getPointLights(props.lights);
const context = getLightMarkerContext(props);
const pointLightRadius = props.pointLightRadius ??
DEFAULT_POINT_LIGHT_RADIUS_FACTOR * context.sceneScale * context.markerScale;
return createLightMarkerInstanceData(pointLights.length, (light, _index) => ({
color: getDisplayColor(light),
direction: DEFAULT_DIRECTION_FALLBACK,
position: light.position,
scale: [pointLightRadius, pointLightRadius, pointLightRadius]
}), pointLights);
}
export function buildSpotLightInstanceData(props) {
const spotLights = getSpotLights(props.lights);
const context = getLightMarkerContext(props);
const spotLightLength = props.spotLightLength ??
DEFAULT_SPOT_LIGHT_LENGTH_FACTOR * context.sceneScale * context.markerScale;
return createLightMarkerInstanceData(spotLights.length, (light, _index) => {
const outerConeAngle = clamp(light.outerConeAngle ?? Math.PI / 4, 0, Math.PI / 2 - SPOTLIGHT_OUTER_CONE_EPSILON);
const radius = Math.tan(outerConeAngle) * spotLightLength;
return {
color: getDisplayColor(light),
direction: normalizeDirection(light.direction),
position: light.position,
scale: [radius, spotLightLength, radius]
};
}, spotLights);
}
export function buildDirectionalLightInstanceData(props) {
const directionalLights = getDirectionalLights(props.lights);
const context = getLightMarkerContext(props);
const directionalLightLength = props.directionalLightLength ??
DEFAULT_DIRECTIONAL_LIGHT_LENGTH_FACTOR * context.sceneScale * context.markerScale;
const directionalLightRadius = directionalLightLength * DEFAULT_DIRECTIONAL_LIGHT_RADIUS_FACTOR;
return createLightMarkerInstanceData(directionalLights.length, (light, _index) => {
const direction = normalizeDirection(light.direction);
const position = [
context.sceneCenter[0] -
direction[0] * context.sceneScale * DIRECTIONAL_ANCHOR_DISTANCE_FACTOR,
context.sceneCenter[1] -
direction[1] * context.sceneScale * DIRECTIONAL_ANCHOR_DISTANCE_FACTOR,
context.sceneCenter[2] -
direction[2] * context.sceneScale * DIRECTIONAL_ANCHOR_DISTANCE_FACTOR
];
return {
color: getDisplayColor(light),
direction,
position,
scale: [directionalLightRadius, directionalLightLength, directionalLightRadius]
};
}, directionalLights);
}
export function getPointLights(lights) {
return lights.filter((light) => light.type === 'point');
}
export function getSpotLights(lights) {
return lights.filter((light) => light.type === 'spot');
}
export function getDirectionalLights(lights) {
return lights.filter((light) => light.type === 'directional');
}
export function getLightMarkerContext(props) {
const bounds = getSceneBounds(props.lights, props.bounds);
const sceneCenter = [
(bounds[0][0] + bounds[1][0]) / 2,
(bounds[0][1] + bounds[1][1]) / 2,
(bounds[0][2] + bounds[1][2]) / 2
];
const sceneScale = Math.max(Math.hypot(bounds[1][0] - bounds[0][0], bounds[1][1] - bounds[0][1], bounds[1][2] - bounds[0][2]), MIN_SCENE_SCALE);
return {
bounds,
markerScale: Math.max(props.markerScale ?? DEFAULT_MARKER_SCALE, 0),
sceneCenter,
sceneScale
};
}
export function getDisplayColor(light) {
const color = light.color || DEFAULT_LIGHT_COLOR;
const intensity = Math.max(light.intensity ?? 1, 0);
const brightness = clamp(0.35 + 0.3 * Math.log10(intensity + 1), 0.35, 1);
return [
clamp(color[0] / LIGHT_COLOR_FACTOR, 0, 1) * brightness,
clamp(color[1] / LIGHT_COLOR_FACTOR, 0, 1) * brightness,
clamp(color[2] / LIGHT_COLOR_FACTOR, 0, 1) * brightness,
1
];
}
export function normalizeDirection(direction) {
const [x, y, z] = direction || DEFAULT_DIRECTION_FALLBACK;
const length = Math.hypot(x, y, z);
if (length === 0) {
return [...DEFAULT_DIRECTION_FALLBACK];
}
return [x / length, y / length, z / length];
}
function createLightMarkerInstanceData(instanceCount, getInstance, lights = []) {
const instancePositions = new Float32Array(instanceCount * 3);
const instanceDirections = new Float32Array(instanceCount * 3);
const instanceScales = new Float32Array(instanceCount * 3);
const instanceColors = new Float32Array(instanceCount * 4);
for (const [index, light] of lights.entries()) {
const instance = getInstance(light, index);
instancePositions.set(instance.position, index * 3);
instanceDirections.set(instance.direction, index * 3);
instanceScales.set(instance.scale, index * 3);
instanceColors.set(instance.color, index * 4);
}
return {
instanceCount,
instancePositions,
instanceDirections,
instanceScales,
instanceColors
};
}
function getSceneBounds(lights, bounds) {
if (bounds) {
return cloneBounds(bounds);
}
const positions = [
...getPointLights(lights).map(light => light.position),
...getSpotLights(lights).map(light => light.position)
];
if (positions.length === 0) {
return [
[-0.5, -0.5, -0.5],
[0.5, 0.5, 0.5]
];
}
const minBounds = [...positions[0]];
const maxBounds = [...positions[0]];
for (const position of positions.slice(1)) {
minBounds[0] = Math.min(minBounds[0], position[0]);
minBounds[1] = Math.min(minBounds[1], position[1]);
minBounds[2] = Math.min(minBounds[2], position[2]);
maxBounds[0] = Math.max(maxBounds[0], position[0]);
maxBounds[1] = Math.max(maxBounds[1], position[1]);
maxBounds[2] = Math.max(maxBounds[2], position[2]);
}
return [minBounds, maxBounds];
}
function cloneBounds(bounds) {
return [[...bounds[0]], [...bounds[1]]];
}
function createManagedInstanceBuffers(device, idPrefix, instanceData) {
return {
instancePosition: device.createBuffer({
id: `${idPrefix}-instance-position`,
data: getBufferDataOrPlaceholder(instanceData.instancePositions, 3)
}),
instanceDirection: device.createBuffer({
id: `${idPrefix}-instance-direction`,
data: getBufferDataOrPlaceholder(instanceData.instanceDirections, 3)
}),
instanceScale: device.createBuffer({
id: `${idPrefix}-instance-scale`,
data: getBufferDataOrPlaceholder(instanceData.instanceScales, 3)
}),
instanceColor: device.createBuffer({
id: `${idPrefix}-instance-color`,
data: getBufferDataOrPlaceholder(instanceData.instanceColors, 4)
})
};
}
function getBufferDataOrPlaceholder(data, size) {
return data.length > 0 ? data : new Float32Array(size);
}
function destroyManagedInstanceBuffers(managedBuffers) {
for (const buffer of Object.values(managedBuffers)) {
buffer?.destroy();
}
}
function createViewProjectionMatrix(props) {
return new Matrix4(props.projectionMatrix).multiplyRight(props.viewMatrix);
}
function shouldRebuildInstanceData(props, sizePropNames) {
if ('lights' in props || 'bounds' in props || 'markerScale' in props) {
return true;
}
return sizePropNames.some(sizePropName => sizePropName in props);
}
function mergeLightMarkerParameters(parameters) {
return {
...LIGHT_MARKER_PARAMETERS,
...(parameters || {})
};
}
function getLightMarkerShaders(anchorMode) {
const localPositionWGSL = anchorMode === 'apex' ? APEX_LOCAL_POSITION_WGSL : CENTERED_LOCAL_POSITION_WGSL;
const localPositionGLSL = anchorMode === 'apex' ? APEX_LOCAL_POSITION_GLSL : CENTERED_LOCAL_POSITION_GLSL;
return {
source: `\
struct lightMarkerUniforms {
viewProjectionMatrix: mat4x4<f32>,
};
var<uniform> lightMarker : lightMarkerUniforms;
struct VertexInputs {
positions : vec3<f32>,
instancePosition : vec3<f32>,
instanceDirection : vec3<f32>,
instanceScale : vec3<f32>,
instanceColor : vec4<f32>,
};
struct FragmentInputs {
Position : vec4<f32>,
color : vec4<f32>,
};
fn lightMarker_rotate(localPosition: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
let forward = normalize(direction);
var helperAxis = vec3<f32>(0.0, 1.0, 0.0);
if (abs(forward.y) > 0.999) {
helperAxis = vec3<f32>(1.0, 0.0, 0.0);
}
let tangent = normalize(cross(helperAxis, forward));
let bitangent = cross(forward, tangent);
return tangent * localPosition.x + forward * localPosition.y + bitangent * localPosition.z;
}
fn vertexMain(inputs: VertexInputs) -> FragmentInputs {
var outputs : FragmentInputs;
let localPosition = ${localPositionWGSL};
let worldPosition = inputs.instancePosition + lightMarker_rotate(localPosition, inputs.instanceDirection);
outputs.Position = lightMarker.viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
outputs.color = inputs.instanceColor;
return outputs;
}
fn fragmentMain(inputs: FragmentInputs) -> vec4<f32> {
return inputs.color;
}
`,
vs: `\
#version 300 es
in vec3 positions;
in vec3 instancePosition;
in vec3 instanceDirection;
in vec3 instanceScale;
in vec4 instanceColor;
layout(std140) uniform lightMarkerUniforms {
mat4 viewProjectionMatrix;
} lightMarker;
out vec4 vColor;
vec3 lightMarker_rotate(vec3 localPosition, vec3 direction) {
vec3 forward = normalize(direction);
vec3 helperAxis = abs(forward.y) > 0.999 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
vec3 tangent = normalize(cross(helperAxis, forward));
vec3 bitangent = cross(forward, tangent);
return tangent * localPosition.x + forward * localPosition.y + bitangent * localPosition.z;
}
void main(void) {
vec3 localPosition = ${localPositionGLSL};
vec3 worldPosition = instancePosition + lightMarker_rotate(localPosition, instanceDirection);
gl_Position = lightMarker.viewProjectionMatrix * vec4(worldPosition, 1.0);
vColor = instanceColor;
}
`,
fs: `\
#version 300 es
precision highp float;
in vec4 vColor;
out vec4 fragColor;
void main(void) {
fragColor = vColor;
}
`
};
}
function clamp(value, minValue, maxValue) {
return Math.min(maxValue, Math.max(minValue, value));
}
//# sourceMappingURL=light-model-utils.js.map