From b7b0f382c511c8a51036bd47a62f9320b9b0f8e0 Mon Sep 17 00:00:00 2001 From: fallenoak Date: Thu, 11 Jan 2024 16:11:54 -0600 Subject: [PATCH] feat(map): use area lights (#43) --- package-lock.json | 8 +- package.json | 2 +- src/lib/db/DbManager.ts | 62 +++++++ src/lib/index.ts | 1 + src/lib/light/SceneLight.ts | 52 +++++- src/lib/light/SceneLightParams.ts | 63 +++++++ src/lib/map/DoodadManager.ts | 2 +- src/lib/map/MapLight.ts | 18 -- src/lib/map/MapManager.ts | 16 +- src/lib/map/daynight/DayNight.ts | 82 --------- src/lib/map/daynight/util.ts | 78 --------- src/lib/map/light/MapLight.ts | 209 +++++++++++++++++++++++ src/lib/map/light/const.ts | 45 +++++ src/lib/map/light/db.ts | 122 +++++++++++++ src/lib/map/{daynight => light}/table.ts | 0 src/lib/map/light/types.ts | 18 ++ src/lib/map/light/util.ts | 125 ++++++++++++++ src/lib/map/terrain/TerrainManager.ts | 2 +- src/lib/map/terrain/TerrainMaterial.ts | 2 - src/lib/map/terrain/shader/fragment.ts | 7 +- src/lib/model/ModelMaterial.ts | 2 - src/lib/model/shader/fragment.ts | 7 +- 22 files changed, 714 insertions(+), 209 deletions(-) create mode 100644 src/lib/db/DbManager.ts create mode 100644 src/lib/light/SceneLightParams.ts delete mode 100644 src/lib/map/MapLight.ts delete mode 100644 src/lib/map/daynight/DayNight.ts delete mode 100644 src/lib/map/daynight/util.ts create mode 100644 src/lib/map/light/MapLight.ts create mode 100644 src/lib/map/light/const.ts create mode 100644 src/lib/map/light/db.ts rename src/lib/map/{daynight => light}/table.ts (100%) create mode 100644 src/lib/map/light/types.ts create mode 100644 src/lib/map/light/util.ts diff --git a/package-lock.json b/package-lock.json index 12e32bb..4e0ba2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.16.1", "license": "MIT", "dependencies": { - "@wowserhq/format": "^0.12.0" + "@wowserhq/format": "^0.13.1" }, "devDependencies": { "@commitlint/config-conventional": "^18.4.3", @@ -2347,9 +2347,9 @@ } }, "node_modules/@wowserhq/format": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@wowserhq/format/-/format-0.12.0.tgz", - "integrity": "sha512-ddEXUOQJNfufCe58S21GjxtJhmQY/bkgHiCUNjvHeb/LwIQYnJEJyoTLVBTL1vQ9Br7pP36RohfHN56HZ418ZQ==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@wowserhq/format/-/format-0.13.1.tgz", + "integrity": "sha512-FGEDHZK5tZENcpud1P4JvunxpvLfhLY69k5S0ijmRaTmcwaFR06xCS5TA0ORscCFsLmtezpFWOpwUxrNUDmiCg==", "dependencies": { "@wowserhq/io": "^2.0.2", "gl-matrix": "^3.4.3" diff --git a/package.json b/package.json index 6d0ded1..5ffce17 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "three.js" ], "dependencies": { - "@wowserhq/format": "^0.12.0" + "@wowserhq/format": "^0.13.1" }, "peerDependencies": { "three": "^0.160.0" diff --git a/src/lib/db/DbManager.ts b/src/lib/db/DbManager.ts new file mode 100644 index 0000000..50b3aa6 --- /dev/null +++ b/src/lib/db/DbManager.ts @@ -0,0 +1,62 @@ +import { ClientDb, ClientDbRecord } from '@wowserhq/format'; +import { AssetHost, loadAsset, normalizePath } from '../asset.js'; + +interface Constructor { + new (...args: any[]): T; +} + +type DbManagerOptions = { + host: AssetHost; +}; + +class DbManager { + #host: AssetHost; + #loaded = new Map>(); + #loading = new Map>>(); + + constructor(options: DbManagerOptions) { + this.#host = options.host; + } + + get(name: string, RecordClass: Constructor): Promise> { + const refId = [normalizePath(name), RecordClass.prototype.constructor.name].join(':'); + + const loaded = this.#loaded.get(refId); + if (loaded) { + return Promise.resolve(loaded); + } + + const alreadyLoading = this.#loading.get(refId); + if (alreadyLoading) { + return alreadyLoading; + } + + const loading = this.#load(refId, name, RecordClass); + this.#loading.set(refId, loading); + + return loading; + } + + async #load( + refId: string, + name: string, + RecordClass: Constructor, + ): Promise> { + const path = `DBFilesClient/${name}`; + + let db: ClientDb; + try { + const data = await loadAsset(this.#host, path); + db = new ClientDb(RecordClass).load(data); + + this.#loaded.set(refId, db); + } finally { + this.#loading.delete(refId); + } + + return db; + } +} + +export default DbManager; +export { DbManager }; diff --git a/src/lib/index.ts b/src/lib/index.ts index 6929fc3..73107bb 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,5 +1,6 @@ export * from './controls/MapControls.js'; export * from './controls/OrbitControls.js'; +export * from './db/DbManager.js'; export * from './light/SceneLight.js'; export * from './map/MapManager.js'; export * from './model/ModelManager.js'; diff --git a/src/lib/light/SceneLight.ts b/src/lib/light/SceneLight.ts index fae9d38..754aab3 100644 --- a/src/lib/light/SceneLight.ts +++ b/src/lib/light/SceneLight.ts @@ -1,26 +1,60 @@ import * as THREE from 'three'; +import SceneLightParams from './SceneLightParams.js'; class SceneLight { - #sunDir = new THREE.Vector3(); - #sunDirView = new THREE.Vector3(); + #location: 'exterior' | 'interior' = 'exterior'; + + #params = { + exterior: new SceneLightParams(), + interior: new SceneLightParams(), + }; #uniforms = { - sunDir: { - value: this.#sunDirView, - }, + exterior: this.#params.exterior.uniforms, + interior: this.#params.interior.uniforms, }; + get location() { + return this.#location; + } + + set location(location: 'exterior' | 'interior') { + this.#location = location; + } + get uniforms() { - return this.#uniforms; + return this.#uniforms[this.#location]; + } + + get sunDir() { + return this.#params[this.#location].sunDir; } - setSunDir(dir: THREE.Vector3) { - this.#sunDir.copy(dir); + get sunDirView() { + return this.#params[this.#location].sunDirView; + } + + get sunDiffuseColor() { + return this.#params[this.#location].sunDiffuseColor; + } + + get sunAmbientColor() { + return this.#params[this.#location].sunAmbientColor; + } + + get fogParams() { + return this.#params[this.#location].fogParams; + } + + get fogColor() { + return this.#params[this.#location].fogColor; } update(camera: THREE.Camera) { const viewMatrix = camera.matrixWorldInverse; - this.#sunDirView.copy(this.#sunDir).transformDirection(viewMatrix).normalize(); + + this.#params.exterior.transformSunDirView(viewMatrix); + this.#params.interior.transformSunDirView(viewMatrix); } } diff --git a/src/lib/light/SceneLightParams.ts b/src/lib/light/SceneLightParams.ts new file mode 100644 index 0000000..d80f23d --- /dev/null +++ b/src/lib/light/SceneLightParams.ts @@ -0,0 +1,63 @@ +import * as THREE from 'three'; + +class SceneLightParams { + #sunDir = new THREE.Vector3(-1.0, -1.0, -1.0); + #sunDirView = new THREE.Vector3(-1.0, -1.0, -1.0); + #sunDiffuseColor = new THREE.Color(0.25, 0.5, 1.0); + #sunAmbientColor = new THREE.Color(0.5, 0.5, 0.5); + + #fogParams = new THREE.Vector4(0.0, 577.0, 1.0, 1.0); + #fogColor = new THREE.Color(0.25, 0.5, 0.8); + + #uniforms = { + sunDir: { + value: this.#sunDirView, + }, + sunDiffuseColor: { + value: this.#sunDiffuseColor, + }, + sunAmbientColor: { + value: this.#sunAmbientColor, + }, + fogParams: { + value: this.#fogParams, + }, + fogColor: { + value: this.#fogColor, + }, + }; + + get sunDir() { + return this.#sunDir; + } + + get sunDirView() { + return this.#sunDirView; + } + + get sunDiffuseColor() { + return this.#sunDiffuseColor; + } + + get sunAmbientColor() { + return this.#sunAmbientColor; + } + + get fogParams() { + return this.#fogParams; + } + + get fogColor() { + return this.#fogColor; + } + + get uniforms() { + return this.#uniforms; + } + + transformSunDirView(viewMatrix: THREE.Matrix4) { + this.#sunDirView.copy(this.#sunDir).transformDirection(viewMatrix).normalize(); + } +} + +export default SceneLightParams; diff --git a/src/lib/map/DoodadManager.ts b/src/lib/map/DoodadManager.ts index fd89ed2..b0d036c 100644 --- a/src/lib/map/DoodadManager.ts +++ b/src/lib/map/DoodadManager.ts @@ -3,7 +3,7 @@ import ModelManager from '../model/ModelManager.js'; import TextureManager from '../texture/TextureManager.js'; import { AssetHost } from '../asset.js'; import { MapAreaSpec } from './loader/types.js'; -import MapLight from './MapLight.js'; +import MapLight from './light/MapLight.js'; type DoodadManagerOptions = { host: AssetHost; diff --git a/src/lib/map/MapLight.ts b/src/lib/map/MapLight.ts deleted file mode 100644 index c43025c..0000000 --- a/src/lib/map/MapLight.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as THREE from 'three'; -import DayNight from './daynight/DayNight.js'; -import SceneLight from '../light/SceneLight.js'; - -class MapLight extends SceneLight { - #dayNight = new DayNight(); - - update(camera: THREE.Camera) { - this.#dayNight.update(); - - this.setSunDir(this.#dayNight.sunDir); - - super.update(camera); - } -} - -export default MapLight; -export { MapLight }; diff --git a/src/lib/map/MapManager.ts b/src/lib/map/MapManager.ts index ad036f8..38da49a 100644 --- a/src/lib/map/MapManager.ts +++ b/src/lib/map/MapManager.ts @@ -6,13 +6,15 @@ import DoodadManager from './DoodadManager.js'; import { AssetHost } from '../asset.js'; import MapLoader from './loader/MapLoader.js'; import { MapAreaSpec, MapSpec } from './loader/types.js'; -import MapLight from './MapLight.js'; +import MapLight from './light/MapLight.js'; +import DbManager from '../db/DbManager.js'; const DEFAULT_VIEW_DISTANCE = 1277.0; type MapManagerOptions = { host: AssetHost; textureManager?: TextureManager; + dbManager?: DbManager; viewDistance?: number; }; @@ -32,6 +34,7 @@ class MapManager { #textureManager: TextureManager; #terrainManager: TerrainManager; #doodadManager: DoodadManager; + #dbManager: DbManager; #mapLight: MapLight; @@ -53,9 +56,10 @@ class MapManager { } this.#textureManager = options.textureManager ?? new TextureManager({ host: options.host }); + this.#dbManager = options.dbManager ?? new DbManager({ host: options.host }); this.#loader = new MapLoader({ host: options.host }); - this.#mapLight = new MapLight(); + this.#mapLight = new MapLight({ dbManager: this.#dbManager }); this.#terrainManager = new TerrainManager({ host: options.host, @@ -73,6 +77,10 @@ class MapManager { this.#root.matrixWorldAutoUpdate = false; } + get clearColor() { + return this.#mapLight.fogColor; + } + get mapMame() { return this.#mapName; } @@ -81,10 +89,12 @@ class MapManager { return this.#root; } - load(mapName: string) { + load(mapName: string, mapId?: number) { this.#mapName = mapName; this.#mapDir = `world/maps/${mapName}`; + this.#mapLight.mapId = mapId; + this.#root.name = `map:${mapName}`; this.#loadMap().catch((error) => console.error(error)); diff --git a/src/lib/map/daynight/DayNight.ts b/src/lib/map/daynight/DayNight.ts deleted file mode 100644 index d6a7090..0000000 --- a/src/lib/map/daynight/DayNight.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as THREE from 'three'; -import { SUN_PHI_TABLE, SUN_THETA_TABLE } from './table.js'; -import { getDayNightTime, interpolateDayNightTable } from './util.js'; - -class DayNight { - // Time in half-minutes since midnight (0 - 2879) - #time = 0; - - // Overridden time in half-minutes since midnight (0 - 2879) - #timeOverride = null; - - // Time as a floating point range from 0.0 to 1.0 - #timeProgression = 0.0; - - #sunDir = new THREE.Vector3(); - - constructor() {} - - get sunDir() { - return this.#sunDir; - } - - get time() { - return this.#time; - } - - get timeOverride() { - return this.#timeOverride; - } - - get timeProgression() { - return this.#timeProgression; - } - - setTimeOverride(override: number) { - this.#timeOverride = override; - this.#updateTime(); - } - - clearTimeOverride() { - this.#timeOverride = null; - this.#updateTime(); - } - - update() { - this.#updateTime(); - this.#updateSunDirection(); - } - - #updateTime() { - if (this.#timeOverride) { - this.#time = this.#timeOverride; - } else { - this.#time = getDayNightTime(); - } - - this.#timeProgression = this.#time / 2880; - } - - #updateSunDirection() { - // Get spherical coordinates - const phi = interpolateDayNightTable(SUN_PHI_TABLE, this.#timeProgression); - const theta = interpolateDayNightTable(SUN_THETA_TABLE, this.#timeProgression); - - // Convert from spherical coordinates to XYZ - // x = rho * sin(phi) * cos(theta) - // y = rho * sin(phi) * sin(theta) - // z = rho * cos(phi) - - const sinPhi = Math.sin(phi); - const cosPhi = Math.cos(phi); - - const x = sinPhi * Math.cos(theta); - const y = sinPhi * Math.sin(theta); - const z = cosPhi; - - this.#sunDir.set(x, y, z); - } -} - -export default DayNight; -export { DayNight }; diff --git a/src/lib/map/daynight/util.ts b/src/lib/map/daynight/util.ts deleted file mode 100644 index 87efa45..0000000 --- a/src/lib/map/daynight/util.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Returns number of half minutes since midnight. - */ -const getDayNightTime = () => { - const d = new Date(); - - const msSinceMidnight = d.getTime() - d.setHours(0, 0, 0, 0); - - return Math.round(msSinceMidnight / 1000.0 / 30.0); -}; - -/** - * Given two values, linearly interpolate between them according to the given factor. - * - * @param value1 - * @param value2 - * @param factor - */ -const lerpNumbers = (value1: number, value2: number, factor: number) => - (1.0 - factor) * value1 + factor * value2; - -/** - * Given a DayNight table of key/value pairs and a given key, interpolate table values against the - * key. If the key is in excess of the table's size, interpolate from the last table value to the - * first table value. - * - * @param table - * @param size - * @param key - */ -const interpolateDayNightTable = (table: number[], key: number) => { - // All table entries are key/value pairs - const size = table.length / 2; - - // Clamp key - key = Math.min(Math.max(key, 0.0), 1.0); - - let previousKey: number; - let nextKey: number; - let previousValue: number; - let nextValue: number; - - for (let i = 0; i < size; i++) { - // Wrap at end - if (i + 1 >= size) { - previousKey = table[i * 2]; - nextKey = table[0] + 1.0; - - previousValue = table[i * 2 + 1]; - nextValue = table[1]; - - break; - } - - // Found matching stops - if (table[i * 2] <= key && table[(i + 1) * 2] >= key) { - previousKey = table[i * 2]; - nextKey = table[(i + 1) * 2]; - - previousValue = table[i * 2 + 1]; - nextValue = table[(i + 1) * 2 + 1]; - - break; - } - } - - const keyDistance = nextKey - previousKey; - - if (Math.abs(keyDistance) < 0.001) { - return previousValue; - } - - const factor = (key - previousKey) / keyDistance; - - return lerpNumbers(previousValue, nextValue, factor); -}; - -export { getDayNightTime, interpolateDayNightTable }; diff --git a/src/lib/map/light/MapLight.ts b/src/lib/map/light/MapLight.ts new file mode 100644 index 0000000..908da44 --- /dev/null +++ b/src/lib/map/light/MapLight.ts @@ -0,0 +1,209 @@ +import * as THREE from 'three'; +import { + LightFloatBandRecord, + LightIntBandRecord, + LightParamsRecord, + LightRecord, + MAP_CORNER_X, + MAP_CORNER_Y, +} from '@wowserhq/format'; +import { getDayNightTime, interpolateColorTable, interpolateNumericTable } 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 SceneLight from '../../light/SceneLight.js'; +import DbManager from '../../db/DbManager.js'; + +type MapLightOptions = { + dbManager: DbManager; +}; + +class MapLight extends SceneLight { + #dbManager: DbManager; + + // Area lights indexed by map id + #lights: Record; + + // Applicable area lights given current camera position + #selectedLights: AreaLight[]; + + // Time in half-minutes since midnight (0 - 2879) + #time = 0; + + // Overridden time in half-minutes since midnight (0 - 2879) + #timeOverride = null; + + // Time as a floating point range from 0.0 to 1.0 + #timeProgression = 0.0; + + // Used to filter area lights into the set appropriate for the given map + #mapId: number; + + constructor(options: MapLightOptions) { + super(); + + this.#dbManager = options.dbManager; + + this.#loadLights().catch((error) => console.error(error)); + } + + get mapId() { + return this.#mapId; + } + + set mapId(mapId: number) { + this.#mapId = mapId; + } + + get time() { + return this.#time; + } + + get timeOverride() { + return this.#timeOverride; + } + + set timeOverride(override: number) { + this.#timeOverride = override; + this.#updateTime(); + } + + update(camera: THREE.Camera) { + this.#selectLights(camera.position); + + this.#updateTime(); + this.#updateSunDirection(); + this.#updateColors(); + this.#updateFog(); + + super.update(camera); + } + + #updateTime() { + if (this.#timeOverride) { + this.#time = this.#timeOverride; + } else { + this.#time = getDayNightTime(); + } + + this.#timeProgression = this.#time / 2880; + } + + #updateSunDirection() { + // Get spherical coordinates + const phi = interpolateNumericTable(SUN_PHI_TABLE, this.#timeProgression); + const theta = interpolateNumericTable(SUN_THETA_TABLE, this.#timeProgression); + + // Convert from spherical coordinates to XYZ + // x = rho * sin(phi) * cos(theta) + // y = rho * sin(phi) * sin(theta) + // z = rho * cos(phi) + + const sinPhi = Math.sin(phi); + const cosPhi = Math.cos(phi); + + const x = sinPhi * Math.cos(theta); + const y = sinPhi * Math.sin(theta); + const z = cosPhi; + + 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() { + 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], + 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); + } + + #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; + } + + async #loadLights() { + const lightDb = await this.#dbManager.get('Light.dbc', LightRecord); + const lightParamsDb = await this.#dbManager.get('LightParams.dbc', LightParamsRecord); + const lightIntBandDb = await this.#dbManager.get('LightIntBand.dbc', LightIntBandRecord); + const lightFloatBandDb = await this.#dbManager.get('LightFloatBand.dbc', LightFloatBandRecord); + + this.#lights = getAreaLightsFromDb(lightDb, lightParamsDb, lightIntBandDb, lightFloatBandDb); + } +} + +export default MapLight; +export { MapLight }; diff --git a/src/lib/map/light/const.ts b/src/lib/map/light/const.ts new file mode 100644 index 0000000..f84e630 --- /dev/null +++ b/src/lib/map/light/const.ts @@ -0,0 +1,45 @@ +enum LIGHT_PARAM { + PARAM_STANDARD = 0, + PARAM_STANDARD_UNDERWATER, + PARAM_STORMY, + PARAM_STORMY_UNDERWATER, + PARAM_DEATH, + PARAM_5, + PARAM_6, + PARAM_7, + NUM_LIGHT_PARAMS, +} + +enum LIGHT_INT_BAND { + BAND_DIRECT_COLOR = 0, + BAND_AMBIENT_COLOR, + BAND_SKY_TOP_COLOR, + BAND_SKY_MIDDLE_COLOR, + BAND_SKY_BAND_1_COLOR, + BAND_SKY_BAND_2_COLOR, + BAND_SKY_SMOG_COLOR, + BAND_SKY_FOG_COLOR, + BAND_SUN_COLOR, + BAND_CLOUD_SUN_COLOR, + BAND_CLOUD_EMISSIVE_COLOR, + BAND_CLOUD_LAYER_1_AMBIENT_COLOR, + BAND_CLOUD_LAYER_2_AMBIENT_COLOR, + BAND_13, + BAND_OCEAN_CLOSE_COLOR, + BAND_OCEAN_FAR_COLOR, + BAND_RIVER_CLOSE_COLOR, + BAND_RIVER_FAR_COLOR, + NUM_LIGHT_INT_BANDS, +} + +enum LIGHT_FLOAT_BAND { + BAND_FOG_END, + BAND_FOG_START_SCALAR, + BAND_2, + BAND_3, + BAND_4, + BAND_5, + NUM_LIGHT_FLOAT_BANDS, +} + +export { LIGHT_PARAM, LIGHT_INT_BAND, LIGHT_FLOAT_BAND }; diff --git a/src/lib/map/light/db.ts b/src/lib/map/light/db.ts new file mode 100644 index 0000000..776257c --- /dev/null +++ b/src/lib/map/light/db.ts @@ -0,0 +1,122 @@ +import * as THREE from 'three'; +import { + ClientDb, + LightFloatBandRecord, + LightIntBandRecord, + LightParamsRecord, + LightRecord, + MAP_CORNER_X, + MAP_CORNER_Y, +} from '@wowserhq/format'; +import { LIGHT_FLOAT_BAND, LIGHT_INT_BAND } from './const.js'; +import { AreaLight } from './types.js'; + +const getAreaLightsFromDb = ( + lightDb: ClientDb, + lightParamsDb: ClientDb, + lightIntBandDb: ClientDb, + lightFloatBandDb: ClientDb, +) => { + const lights: Record = {}; + + for (let i = 0; i < lightDb.records.length; i++) { + const lightRecord = lightDb.records[i]; + + // Lights have their own coordinate system + const position = new THREE.Vector3( + MAP_CORNER_X - lightRecord.gameCoords[2] / 36, + MAP_CORNER_Y - lightRecord.gameCoords[0] / 36, + lightRecord.gameCoords[1] / 36, + ); + + const falloffStart = lightRecord.gameFalloffStart / 36; + const falloffEnd = lightRecord.gameFalloffEnd / 36; + const mapId = lightRecord.continentId; + const id = lightRecord.id; + + const params = []; + for (let p = 0; p < lightRecord.lightParamsId.length; p++) { + const paramId = lightRecord.lightParamsId[p]; + + // Unused param + if (paramId === 0) { + continue; + } + + const paramsRecord = lightParamsDb.getRecord(paramId); + const intBands = getIntBandsForParam(paramId, lightIntBandDb); + const floatBands = getFloatBandsForParam(paramId, lightFloatBandDb); + + params.push({ + ...paramsRecord, + intBands, + floatBands, + }); + } + + const light: AreaLight = { + id, + mapId, + position, + falloffStart, + falloffEnd, + params, + }; + + if (lights[mapId]) { + lights[mapId].push(light); + } else { + lights[mapId] = [light]; + } + } + + return lights; +}; + +const getFloatBandsForParam = (paramId: number, bandDb: ClientDb) => { + const floatBandCount = LIGHT_FLOAT_BAND.NUM_LIGHT_FLOAT_BANDS; + const floatBands = new Array(floatBandCount); + + for (let band = 0; band < floatBandCount; band++) { + const floatBandBase = (paramId - 1) * floatBandCount + 1; + const floatBandRecord = bandDb.getRecord(floatBandBase + band); + const floatBand = []; + + for (let b = 0; b < floatBandRecord.num; b++) { + const time = (floatBandRecord.time[b] >>> 0) / 2880; + const value = + band === LIGHT_FLOAT_BAND.BAND_FOG_END + ? floatBandRecord.data[b] / 36 + : floatBandRecord.data[b]; + floatBand.push(time, value); + } + + floatBands[band] = floatBand; + } + + return floatBands; +}; + +const getIntBandsForParam = (paramId: number, bandDb: ClientDb) => { + const intBandCount = LIGHT_INT_BAND.NUM_LIGHT_INT_BANDS; + const intBands = new Array(intBandCount); + + for (let band = 0; band < intBandCount; band++) { + const intBandBase = (paramId - 1) * intBandCount + 1; + const intBandRecord = bandDb.getRecord(intBandBase + band); + const intBand = []; + + for (let b = 0; b < intBandRecord.num; b++) { + const time = (intBandRecord.time[b] >>> 0) / 2880; + const value = new THREE.Color(intBandRecord.data[b] >>> 0); + + intBand.push(time, value); + } + + intBands[band] = intBand; + } + + return intBands; +}; + +export { getAreaLightsFromDb }; diff --git a/src/lib/map/daynight/table.ts b/src/lib/map/light/table.ts similarity index 100% rename from src/lib/map/daynight/table.ts rename to src/lib/map/light/table.ts diff --git a/src/lib/map/light/types.ts b/src/lib/map/light/types.ts new file mode 100644 index 0000000..87f56da --- /dev/null +++ b/src/lib/map/light/types.ts @@ -0,0 +1,18 @@ +import * as THREE from 'three'; + +type AreaLightParams = { + id: number; + intBands: any[][]; + floatBands: any[][]; +}; + +type AreaLight = { + id: number; + mapId: number; + position: THREE.Vector3; + falloffStart: number; + falloffEnd: number; + params: AreaLightParams[]; +}; + +export { AreaLight, AreaLightParams }; diff --git a/src/lib/map/light/util.ts b/src/lib/map/light/util.ts new file mode 100644 index 0000000..ce4d012 --- /dev/null +++ b/src/lib/map/light/util.ts @@ -0,0 +1,125 @@ +import * as THREE from 'three'; + +/** + * Returns number of half minutes since midnight. + */ +const getDayNightTime = () => { + const d = new Date(); + + const msSinceMidnight = d.getTime() - d.setHours(0, 0, 0, 0); + + return Math.round(msSinceMidnight / 1000.0 / 30.0); +}; + +/** + * Given two numbers, linearly interpolate between them according to the given factor. + * + * @param value1 - First number + * @param value2 - Second number + * @param factor - Interpolation factor + */ +const lerpNumbers = (value1: number, value2: number, factor: number) => + (1.0 - factor) * value1 + factor * value2; + +/** + * Given two colors, linearly interpolate between them according to the given factor. + * + * @param value1 - First color + * @param value2 - Second color + * @param factor - Interpolation factor + * @param color - Destination color object used to store the result of the interpolation + */ +const lerpColors = (value1: THREE.Color, value2: THREE.Color, factor: number, color: THREE.Color) => + color.lerpColors(value1, value2, factor); + +const getTableKeys = (table: any[], key: number) => { + // All table entries are key/value pairs + const size = table.length / 2; + + // Clamp key + key = Math.min(Math.max(key, 0.0), 1.0); + + let previous: number; + let previousKey: number; + let next: number; + let nextKey: number; + + for (let i = 0; i < size; i++) { + // Wrap at end + if (i + 1 >= size) { + previous = i; + previousKey = table[previous * 2]; + + next = 0; + nextKey = table[0] + 1.0; + + break; + } + + // Found matching stops + if (table[i * 2] <= key && table[(i + 1) * 2] >= key) { + previous = i; + previousKey = table[previous * 2]; + + next = i + 1; + nextKey = table[next * 2]; + + break; + } + } + + return { previous, previousKey, next, nextKey }; +}; + +/** + * Given a table of key/value pairs with color values and a key, interpolate table values against + * the key. If the key is in excess of the table's size, interpolate from the last table value to + * the first table value. + * + * @param table + * @param key + * @param color + */ +const interpolateColorTable = (table: any[], key: number, color: THREE.Color): void => { + const { previous, previousKey, next, nextKey } = getTableKeys(table, key); + + const previousValue = table[previous * 2 + 1]; + const nextValue = table[next * 2 + 1]; + + const keyDistance = nextKey - previousKey; + + if (Math.abs(keyDistance) < 0.001) { + return previousValue; + } + + const factor = (key - previousKey) / keyDistance; + + lerpColors(previousValue, nextValue, factor, color); +}; + +/** + * Given a table of key/value pairs with numeric values and a key, interpolate table values against + * the key. If the key is in excess of the table's size, interpolate from the last table value to + * the first table value. + * + * @param table + * @param key + */ +const interpolateNumericTable = (table: any[], key: number): number => { + const { previous, previousKey, next, nextKey } = getTableKeys(table, key); + + const previousValue = table[previous * 2 + 1]; + const nextValue = table[next * 2 + 1]; + + const keyDistance = nextKey - previousKey; + + if (Math.abs(keyDistance) < 0.001) { + return previousValue; + } + + const factor = (key - previousKey) / keyDistance; + + return lerpNumbers(previousValue, nextValue, factor); +}; + +export { getDayNightTime, interpolateColorTable, interpolateNumericTable }; diff --git a/src/lib/map/terrain/TerrainManager.ts b/src/lib/map/terrain/TerrainManager.ts index 2c3293b..2e739a4 100644 --- a/src/lib/map/terrain/TerrainManager.ts +++ b/src/lib/map/terrain/TerrainManager.ts @@ -4,7 +4,7 @@ import TerrainMaterial from './TerrainMaterial.js'; import TerrainMesh from './TerrainMesh.js'; import { AssetHost } from '../../asset.js'; import { MapAreaSpec, TerrainSpec } from '../loader/types.js'; -import MapLight from '../MapLight.js'; +import MapLight from '../light/MapLight.js'; const SPLAT_TEXTURE_PLACEHOLDER = new THREE.Texture(); diff --git a/src/lib/map/terrain/TerrainMaterial.ts b/src/lib/map/terrain/TerrainMaterial.ts index 1c25a4a..cdb50a4 100644 --- a/src/lib/map/terrain/TerrainMaterial.ts +++ b/src/lib/map/terrain/TerrainMaterial.ts @@ -16,8 +16,6 @@ class TerrainMaterial extends THREE.RawShaderMaterial { layerCount: { value: layerCount }, layers: { value: layerTextures }, splat: { value: splatTexture }, - fogColor: { value: new THREE.Color(0.25, 0.5, 0.8) }, - fogParams: { value: new THREE.Vector4(0.0, 1066.0, 1.0, 1.0) }, }; this.vertexShader = vertexShader; diff --git a/src/lib/map/terrain/shader/fragment.ts b/src/lib/map/terrain/shader/fragment.ts index 96fb1cc..50984ec 100644 --- a/src/lib/map/terrain/shader/fragment.ts +++ b/src/lib/map/terrain/shader/fragment.ts @@ -7,6 +7,8 @@ const FRAGMENT_SHADER_UNIFORMS = [ { name: 'layerCount', type: 'int' }, { name: 'layers[4]', type: 'sampler2D' }, { name: 'splat', type: 'sampler2D' }, + { name: 'sunDiffuseColor', type: 'vec3' }, + { name: 'sunAmbientColor', type: 'vec3' }, ]; const FRAGMENT_SHADER_INPUTS = [ @@ -55,10 +57,7 @@ color.a = 1.0; `; const FRAGMENT_SHADER_MAIN_LIGHTING = ` -// Fixed lighting -vec3 lightDiffuse = normalize(vec3(0.25, 0.5, 1.0)); -vec3 lightAmbient = normalize(vec3(0.5, 0.5, 0.5)); -color.rgb *= lightDiffuse * vLight + lightAmbient; +color.rgb *= sunDiffuseColor * vLight + sunAmbientColor; `; const FRAGMENT_SHADER_MAIN_FOG = ` diff --git a/src/lib/model/ModelMaterial.ts b/src/lib/model/ModelMaterial.ts index 4dd4b09..2c1509f 100644 --- a/src/lib/model/ModelMaterial.ts +++ b/src/lib/model/ModelMaterial.ts @@ -54,8 +54,6 @@ class ModelMaterial extends THREE.RawShaderMaterial { ...uniforms, textures: { value: textures }, alphaRef: { value: this.#alphaRef }, - fogColor: { value: new THREE.Color(0.25, 0.5, 0.8) }, - fogParams: { value: new THREE.Vector4(0.0, 1066.0, 1.0, 1.0) }, }; } diff --git a/src/lib/model/shader/fragment.ts b/src/lib/model/shader/fragment.ts index 992daaa..9993b76 100644 --- a/src/lib/model/shader/fragment.ts +++ b/src/lib/model/shader/fragment.ts @@ -7,6 +7,8 @@ const FRAGMENT_SHADER_PRECISION = 'highp float'; const FRAGMENT_SHADER_UNIFORMS = [ { name: 'textures[2]', type: 'sampler2D' }, { name: 'alphaRef', type: 'float' }, + { name: 'sunDiffuseColor', type: 'vec3' }, + { name: 'sunAmbientColor', type: 'vec3' }, ]; const FRAGMENT_SHADER_INPUTS = [{ name: 'vLight', type: 'float' }]; @@ -79,10 +81,7 @@ if (color.a < alphaRef) { `; const FRAGMENT_SHADER_MAIN_LIGHTING = ` -// Fixed lighting -vec3 lightDiffuse = normalize(vec3(0.25, 0.5, 1.0)); -vec3 lightAmbient = normalize(vec3(0.5, 0.5, 0.5)); -color.rgb *= lightDiffuse * vLight + lightAmbient; +color.rgb *= sunDiffuseColor * vLight + sunAmbientColor; `; const FRAGMENT_SHADER_MAIN_FOG = `