From 06419d927be1e7c76d2b440c544ac5cf6c6549c1 Mon Sep 17 00:00:00 2001 From: fallenoak Date: Fri, 12 Jan 2024 23:26:29 -0600 Subject: [PATCH] feat(map): blend area lights based on falloff --- src/lib/map/light/MapLight.ts | 95 +++++++---------------------------- src/lib/map/light/blend.ts | 93 ++++++++++++++++++++++++++++++++++ src/lib/map/light/types.ts | 8 ++- src/lib/map/light/util.ts | 59 +++++++++++++++++++++- 4 files changed, 177 insertions(+), 78 deletions(-) create mode 100644 src/lib/map/light/blend.ts diff --git a/src/lib/map/light/MapLight.ts b/src/lib/map/light/MapLight.ts index 908da44..657389a 100644 --- a/src/lib/map/light/MapLight.ts +++ b/src/lib/map/light/MapLight.ts @@ -4,16 +4,20 @@ import { LightIntBandRecord, LightParamsRecord, LightRecord, - MAP_CORNER_X, - MAP_CORNER_Y, } from '@wowserhq/format'; -import { getDayNightTime, interpolateColorTable, interpolateNumericTable } from './util.js'; +import { + getDayNightTime, + interpolateColorTable, + interpolateNumericTable, + selectLightsForPosition, +} from './util.js'; import { SUN_PHI_TABLE, SUN_THETA_TABLE } from './table.js'; import { LIGHT_FLOAT_BAND, LIGHT_INT_BAND, LIGHT_PARAM } from './const.js'; import { getAreaLightsFromDb } from './db.js'; -import { AreaLight } from './types.js'; +import { AreaLight, WeightedAreaLight } from './types.js'; import SceneLight from '../../light/SceneLight.js'; import DbManager from '../../db/DbManager.js'; +import { blendLights } from './blend.js'; type MapLightOptions = { dbManager: DbManager; @@ -26,7 +30,7 @@ class MapLight extends SceneLight { #lights: Record; // Applicable area lights given current camera position - #selectedLights: AreaLight[]; + #selectedLights: WeightedAreaLight[]; // Time in half-minutes since midnight (0 - 2879) #time = 0; @@ -74,8 +78,7 @@ class MapLight extends SceneLight { this.#updateTime(); this.#updateSunDirection(); - this.#updateColors(); - this.#updateFog(); + this.#updateLights(); super.update(camera); } @@ -110,56 +113,21 @@ class MapLight extends SceneLight { this.sunDir.set(x, y, z); } - #updateColors() { - if (!this.#selectedLights || this.#selectedLights.length === 0) { - return; - } - - const light = this.#selectedLights[0]; - const params = light.params[LIGHT_PARAM.PARAM_STANDARD]; - - interpolateColorTable( - params.intBands[LIGHT_INT_BAND.BAND_DIRECT_COLOR], - this.#timeProgression, - this.sunDiffuseColor, - ); - - interpolateColorTable( - params.intBands[LIGHT_INT_BAND.BAND_AMBIENT_COLOR], - this.#timeProgression, - this.sunAmbientColor, - ); - } - - #updateFog() { + #updateLights() { if (!this.#selectedLights || this.#selectedLights.length === 0) { return; } - const light = this.#selectedLights[0]; - const params = light.params[LIGHT_PARAM.PARAM_STANDARD]; - - interpolateColorTable( - params.intBands[LIGHT_INT_BAND.BAND_SKY_FOG_COLOR], + const { sunDiffuseColor, sunAmbientColor, fogColor, fogParams } = blendLights( + this.#selectedLights, + LIGHT_PARAM.PARAM_STANDARD, this.#timeProgression, - this.fogColor, ); - const fogEnd = interpolateNumericTable( - params.floatBands[LIGHT_FLOAT_BAND.BAND_FOG_END], - this.#timeProgression, - ); - - const fogStartScalar = interpolateNumericTable( - params.floatBands[LIGHT_FLOAT_BAND.BAND_FOG_START_SCALAR], - this.#timeProgression, - ); - - const fogStart = fogStartScalar * fogEnd; - - // TODO conditionally calculate fog rate (aka density) - - this.fogParams.set(fogStart, fogEnd, 1.0, 1.0); + this.sunDiffuseColor.copy(sunDiffuseColor); + this.sunAmbientColor.copy(sunAmbientColor); + this.fogColor.copy(fogColor); + this.fogParams.copy(fogParams); } #selectLights(position: THREE.Vector3) { @@ -167,32 +135,7 @@ class MapLight extends SceneLight { return; } - const selectedLights = []; - - // Find lights with falloff radii overlapping position - for (const light of this.#lights[this.#mapId]) { - const distance = position.distanceTo(light.position); - - if (distance <= light.falloffEnd) { - selectedLights.push(light); - } - } - - // Find default light if no other lights were in range - if (selectedLights.length === 0) { - for (const light of this.#lights[this.#mapId]) { - if ( - light.position.x === MAP_CORNER_X && - light.position.y === MAP_CORNER_Y && - light.falloffEnd === 0.0 - ) { - selectedLights.push(light); - break; - } - } - } - - this.#selectedLights = selectedLights; + this.#selectedLights = selectLightsForPosition(this.#lights[this.#mapId], position); } async #loadLights() { diff --git a/src/lib/map/light/blend.ts b/src/lib/map/light/blend.ts new file mode 100644 index 0000000..8e89678 --- /dev/null +++ b/src/lib/map/light/blend.ts @@ -0,0 +1,93 @@ +import * as THREE from 'three'; +import { WeightedAreaLight } from './types.js'; +import { interpolateColorTable, interpolateNumericTable } from './util.js'; +import { LIGHT_FLOAT_BAND, LIGHT_INT_BAND, LIGHT_PARAM } from './const.js'; + +const table = { + sunDiffuseColor: new THREE.Color(), + sunAmbientColor: new THREE.Color(), + fogColor: new THREE.Color(), + fogParams: new THREE.Vector4(), +}; + +const blend = { + sunDiffuseColor: new THREE.Color(), + sunAmbientColor: new THREE.Color(), + fogColor: new THREE.Color(), + fogParams: new THREE.Vector4(), +}; + +const tempColor = new THREE.Color(); +const tempVector = new THREE.Vector4(); + +const addWeightedColor = (color: THREE.Color, add: THREE.Color, weight: number) => { + color.add(tempColor.copy(add).multiplyScalar(weight)); +}; + +const addWeightedVector = (vector: THREE.Vector4, add: THREE.Vector4, weight: number) => { + vector.add(tempVector.copy(add).multiplyScalar(weight)); +}; + +const blendLights = ( + weightedLights: WeightedAreaLight[], + param: LIGHT_PARAM, + timeProgression: number, +) => { + blend.sunDiffuseColor.setScalar(0); + blend.sunAmbientColor.setScalar(0); + blend.fogColor.setScalar(0); + blend.fogParams.setScalar(0); + + for (const weightedLight of weightedLights) { + const { light, weight } = weightedLight; + const { intBands, floatBands } = light.params[param]; + + // Sun + + interpolateColorTable( + intBands[LIGHT_INT_BAND.BAND_DIRECT_COLOR], + timeProgression, + table.sunDiffuseColor, + ); + + addWeightedColor(blend.sunDiffuseColor, table.sunDiffuseColor, weight); + + interpolateColorTable( + intBands[LIGHT_INT_BAND.BAND_AMBIENT_COLOR], + timeProgression, + table.sunAmbientColor, + ); + + addWeightedColor(blend.sunAmbientColor, table.sunAmbientColor, weight); + + // Fog + + interpolateColorTable( + intBands[LIGHT_INT_BAND.BAND_SKY_FOG_COLOR], + timeProgression, + table.fogColor, + ); + + addWeightedColor(blend.fogColor, table.fogColor, weight); + + const fogEnd = interpolateNumericTable( + floatBands[LIGHT_FLOAT_BAND.BAND_FOG_END], + timeProgression, + ); + + const fogStartScalar = interpolateNumericTable( + floatBands[LIGHT_FLOAT_BAND.BAND_FOG_START_SCALAR], + timeProgression, + ); + + const fogStart = fogStartScalar * fogEnd; + + table.fogParams.set(fogStart, fogEnd, 1.0, 1.0); + + addWeightedVector(blend.fogParams, table.fogParams, weight); + } + + return blend; +}; + +export { blendLights }; diff --git a/src/lib/map/light/types.ts b/src/lib/map/light/types.ts index 87f56da..79c7107 100644 --- a/src/lib/map/light/types.ts +++ b/src/lib/map/light/types.ts @@ -15,4 +15,10 @@ type AreaLight = { params: AreaLightParams[]; }; -export { AreaLight, AreaLightParams }; +type WeightedAreaLight = { + light: AreaLight; + weight: number; + distance: number; +}; + +export { AreaLight, AreaLightParams, WeightedAreaLight }; diff --git a/src/lib/map/light/util.ts b/src/lib/map/light/util.ts index ce4d012..45e670d 100644 --- a/src/lib/map/light/util.ts +++ b/src/lib/map/light/util.ts @@ -1,4 +1,10 @@ import * as THREE from 'three'; +import { AreaLight, WeightedAreaLight } from './types.js'; +import { MAP_CORNER_X, MAP_CORNER_Y } from '@wowserhq/format'; + +const clamp = (value: number, min: number, max: number) => { + return Math.min(Math.max(value, min), max); +}; /** * Returns number of half minutes since midnight. @@ -122,4 +128,55 @@ const interpolateNumericTable = (table: any[], key: number): number => { return lerpNumbers(previousValue, nextValue, factor); }; -export { getDayNightTime, interpolateColorTable, interpolateNumericTable }; +const selectLightsForPosition = ( + lights: AreaLight[], + position: THREE.Vector3, +): WeightedAreaLight[] => { + const selectedLights = []; + + for (const light of lights) { + const distance = position.distanceTo(light.position); + + // Include lights if position is within falloff radii + if (distance <= light.falloffEnd) { + selectedLights.push({ light, distance, weight: 0.0 }); + } + + // Include default light + if ( + light.position.x === MAP_CORNER_X && + light.position.y === MAP_CORNER_Y && + light.falloffEnd === 0.0 + ) { + selectedLights.push({ light, distance, weight: 0.0 }); + } + } + + // Sort selected lights by distance (closer -> farther) + selectedLights.sort((selectedLight) => -selectedLight.distance); + + // Distribute weights by falloff + let availableWeight = 1.0; + for (const selectedLight of selectedLights) { + if (availableWeight === 0.0) { + break; + } + + const { light, distance } = selectedLight; + + // Default light has no falloff + const falloff = + light.falloffStart > 0.0 && light.falloffEnd > 0.0 + ? (distance - light.falloffStart) / (light.falloffEnd - light.falloffStart) + : 0.0; + + const weight = clamp(1.0 - falloff, 0.0, availableWeight); + + selectedLight.weight = weight; + availableWeight -= weight; + } + + return selectedLights; +}; + +export { getDayNightTime, interpolateColorTable, interpolateNumericTable, selectLightsForPosition };