UNPKG

@luma.gl/engine

Version:

3D Engine Components for luma.gl

395 lines (381 loc) 15.2 kB
// 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>, }; @group(0) @binding(auto) var<uniform> lightMarker : lightMarkerUniforms; struct VertexInputs { @location(0) positions : vec3<f32>, @location(1) instancePosition : vec3<f32>, @location(2) instanceDirection : vec3<f32>, @location(3) instanceScale : vec3<f32>, @location(4) instanceColor : vec4<f32>, }; struct FragmentInputs { @builtin(position) Position : vec4<f32>, @location(0) 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; } @vertex 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; } @fragment fn fragmentMain(inputs: FragmentInputs) -> @location(0) 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