Skip to content

Commit

Permalink
feat(map): blend area lights based on falloff
Browse files Browse the repository at this point in the history
  • Loading branch information
fallenoak committed Jan 13, 2024
1 parent 9713925 commit 06419d9
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 78 deletions.
95 changes: 19 additions & 76 deletions src/lib/map/light/MapLight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,7 +30,7 @@ class MapLight extends SceneLight {
#lights: Record<number, AreaLight[]>;

// Applicable area lights given current camera position
#selectedLights: AreaLight[];
#selectedLights: WeightedAreaLight[];

// Time in half-minutes since midnight (0 - 2879)
#time = 0;
Expand Down Expand Up @@ -74,8 +78,7 @@ class MapLight extends SceneLight {

this.#updateTime();
this.#updateSunDirection();
this.#updateColors();
this.#updateFog();
this.#updateLights();

super.update(camera);
}
Expand Down Expand Up @@ -110,89 +113,29 @@ 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) {
if (!this.#lights || this.#mapId === undefined) {
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() {
Expand Down
93 changes: 93 additions & 0 deletions src/lib/map/light/blend.ts
Original file line number Diff line number Diff line change
@@ -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 };
8 changes: 7 additions & 1 deletion src/lib/map/light/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ type AreaLight = {
params: AreaLightParams[];
};

export { AreaLight, AreaLightParams };
type WeightedAreaLight = {
light: AreaLight;
weight: number;
distance: number;
};

export { AreaLight, AreaLightParams, WeightedAreaLight };
59 changes: 58 additions & 1 deletion src/lib/map/light/util.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 };

0 comments on commit 06419d9

Please sign in to comment.