feat(pipeline): add Wowee Open Light (.wol) atmosphere format

New open replacement for WoW's Light.dbc / LightParams.dbc /
LightIntBand.dbc / LightFloatBand.dbc stack — a single .wol
file holds a list of time-of-day keyframes for one zone,
each capturing the ambient + directional + fog state at that
moment. The renderer interpolates between adjacent keyframes
by time-of-day.

Binary layout:
  magic[4] = "WOLA", version (uint32),
  nameLen + name bytes,
  keyframeCount + keyframes (each 13 floats + 1 uint32 time)

Per keyframe:
  • timeOfDayMin (0..1439 = minutes since midnight)
  • ambientColor.rgb, directionalColor.rgb, directionalDir.xyz
  • fogColor.rgb, fogStart, fogEnd

CLI:
  • --gen-light <wol-base> [zoneName] — emit a starter file
    with 4-keyframe day/night cycle (midnight/dawn/noon/dusk)
    using reasonable outdoor defaults
  • --info-wol <wol-base> [--json] — inspect: zone name +
    per-keyframe time-of-day + colors + fog distances

The 7th open-format addition to the Wowee pipeline:
  M2  → WOM (model)
  WMO → WOB (building)
  WMO collision → WOC
  ADT → WOT (terrain)
  DBC → JsonDBC
  BLP → PNG
  Light.dbc family → WOL  ← new

Smoke-tested round-trip: gen → info shows correct 4 keyframes
at 00:00 / 06:00 / 12:00 / 18:00 with the canonical color
ramps. JSON output for tooling integration.
This commit is contained in:
Kelsi 2026-05-09 13:52:07 -07:00
parent 1ad1977ad6
commit d58ee0af7d
6 changed files with 318 additions and 0 deletions

View file

@ -0,0 +1,152 @@
#include "pipeline/wowee_light.hpp"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'O', 'L', 'A'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
} // namespace
bool WoweeLightLoader::save(const WoweeLight& light,
const std::string& basePath) {
std::string path = basePath;
if (path.size() < 4 || path.substr(path.size() - 4) != ".wol") {
path += ".wol";
}
std::ofstream os(path, std::ios::binary);
if (!os) return false;
os.write(kMagic, 4);
writePOD(os, kVersion);
uint32_t nameLen = static_cast<uint32_t>(light.name.size());
writePOD(os, nameLen);
if (nameLen > 0) os.write(light.name.data(), nameLen);
uint32_t kfCount = static_cast<uint32_t>(light.keyframes.size());
writePOD(os, kfCount);
for (const auto& kf : light.keyframes) {
writePOD(os, kf.timeOfDayMin);
writePOD(os, kf.ambientColor);
writePOD(os, kf.directionalColor);
writePOD(os, kf.directionalDir);
writePOD(os, kf.fogColor);
writePOD(os, kf.fogStart);
writePOD(os, kf.fogEnd);
}
return os.good();
}
WoweeLight WoweeLightLoader::load(const std::string& basePath) {
WoweeLight out;
std::string path = basePath;
if (path.size() < 4 || path.substr(path.size() - 4) != ".wol") {
path += ".wol";
}
std::ifstream is(path, std::ios::binary);
if (!is) return out;
char magic[4];
is.read(magic, 4);
if (std::memcmp(magic, kMagic, 4) != 0) return out;
uint32_t version = 0;
if (!readPOD(is, version)) return out;
if (version != kVersion) return out;
uint32_t nameLen = 0;
if (!readPOD(is, nameLen)) return out;
if (nameLen > 0) {
out.name.resize(nameLen);
is.read(out.name.data(), nameLen);
if (is.gcount() != static_cast<std::streamsize>(nameLen)) {
out.name.clear();
return out;
}
}
uint32_t kfCount = 0;
if (!readPOD(is, kfCount)) return out;
out.keyframes.resize(kfCount);
for (auto& kf : out.keyframes) {
if (!readPOD(is, kf.timeOfDayMin) ||
!readPOD(is, kf.ambientColor) ||
!readPOD(is, kf.directionalColor) ||
!readPOD(is, kf.directionalDir) ||
!readPOD(is, kf.fogColor) ||
!readPOD(is, kf.fogStart) ||
!readPOD(is, kf.fogEnd)) {
out.keyframes.clear();
return out;
}
}
return out;
}
bool WoweeLightLoader::exists(const std::string& basePath) {
std::string path = basePath;
if (path.size() < 4 || path.substr(path.size() - 4) != ".wol") {
path += ".wol";
}
std::ifstream is(path, std::ios::binary);
return is.good();
}
WoweeLight WoweeLightLoader::makeDefaultDayNight(
const std::string& zoneName) {
WoweeLight out;
out.name = zoneName;
// Midnight: cold + dim, blue-tinted ambient, sun straight down
// (it's behind the world).
out.keyframes.push_back({
0,
glm::vec3(0.06f, 0.07f, 0.10f),
glm::vec3(0.10f, 0.12f, 0.20f),
glm::vec3(0.0f, -1.0f, 0.0f),
glm::vec3(0.05f, 0.06f, 0.10f),
40.0f, 200.0f
});
// Dawn (6:00): warm horizon glow, sun rising from -X.
out.keyframes.push_back({
360,
glm::vec3(0.30f, 0.25f, 0.20f),
glm::vec3(0.95f, 0.70f, 0.55f),
glm::vec3(0.86f, -0.50f, 0.0f),
glm::vec3(0.80f, 0.55f, 0.45f),
100.0f, 600.0f
});
// Noon (12:00): bright + neutral, sun overhead.
out.keyframes.push_back({
720,
glm::vec3(0.40f, 0.42f, 0.44f),
glm::vec3(1.00f, 0.97f, 0.92f),
glm::vec3(0.0f, -1.0f, 0.0f),
glm::vec3(0.65f, 0.72f, 0.82f),
120.0f, 800.0f
});
// Dusk (18:00): orange-red glow, sun setting toward +X.
out.keyframes.push_back({
1080,
glm::vec3(0.32f, 0.22f, 0.18f),
glm::vec3(0.95f, 0.55f, 0.30f),
glm::vec3(-0.86f, -0.50f, 0.0f),
glm::vec3(0.85f, 0.50f, 0.35f),
100.0f, 500.0f
});
return out;
}
} // namespace pipeline
} // namespace wowee