mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
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:
parent
1ad1977ad6
commit
d58ee0af7d
6 changed files with 318 additions and 0 deletions
|
|
@ -594,6 +594,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_model_fromm2.cpp
|
||||
src/pipeline/wowee_building.cpp
|
||||
src/pipeline/wowee_collision.cpp
|
||||
src/pipeline/wowee_light.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1408,6 +1409,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_model_fromm2.cpp
|
||||
src/pipeline/wowee_building.cpp
|
||||
src/pipeline/wowee_collision.cpp
|
||||
src/pipeline/wowee_light.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
64
include/pipeline/wowee_light.hpp
Normal file
64
include/pipeline/wowee_light.hpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Light format (.wol) — novel replacement for WoW's
|
||||
// Light.dbc / LightParams.dbc / LightIntBand.dbc / LightFloatBand.dbc
|
||||
// stack. A 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 (little-endian):
|
||||
// magic[4] = "WOLA"
|
||||
// version (uint32) = current 1
|
||||
// nameLen (uint32)
|
||||
// name bytes (nameLen)
|
||||
// keyframeCount (uint32)
|
||||
// keyframes (each):
|
||||
// timeOfDayMin (uint32) -- 0..1439, minutes since midnight
|
||||
// ambientColor.rgb (3 × float)
|
||||
// directionalColor.rgb (3 × float)
|
||||
// directionalDir.xyz (3 × float) -- unit vector pointing FROM
|
||||
// the sun TO the surface
|
||||
// fogColor.rgb (3 × float)
|
||||
// fogStart (float) -- meters
|
||||
// fogEnd (float) -- meters
|
||||
struct WoweeLight {
|
||||
struct Keyframe {
|
||||
uint32_t timeOfDayMin = 0;
|
||||
glm::vec3 ambientColor{0.20f, 0.20f, 0.25f};
|
||||
glm::vec3 directionalColor{0.95f, 0.92f, 0.85f};
|
||||
glm::vec3 directionalDir{0.0f, -1.0f, 0.0f};
|
||||
glm::vec3 fogColor{0.65f, 0.70f, 0.78f};
|
||||
float fogStart = 80.0f;
|
||||
float fogEnd = 600.0f;
|
||||
};
|
||||
|
||||
std::string name; // zone or scene name
|
||||
std::vector<Keyframe> keyframes; // sorted by timeOfDayMin
|
||||
|
||||
bool isValid() const { return !keyframes.empty(); }
|
||||
};
|
||||
|
||||
class WoweeLightLoader {
|
||||
public:
|
||||
static bool save(const WoweeLight& light, const std::string& basePath);
|
||||
static WoweeLight load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Convenience: emit a 4-keyframe day/night cycle (dawn 6:00,
|
||||
// noon 12:00, dusk 18:00, midnight 0:00) with reasonable
|
||||
// outdoor defaults. Used by --gen-light to create a starter
|
||||
// file users can edit.
|
||||
static WoweeLight makeDefaultDayNight(const std::string& zoneName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
152
src/pipeline/wowee_light.cpp
Normal file
152
src/pipeline/wowee_light.cpp
Normal 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
|
||||
|
|
@ -15,6 +15,7 @@ const char* const kArgRequired[] = {
|
|||
"--list-zone-meshes-detail", "--list-project-meshes-detail", "--info-mesh",
|
||||
"--info-mesh-storage-budget", "--info-mesh-stats",
|
||||
"--info-wob", "--info-wob-stats", "--info-woc", "--info-wot",
|
||||
"--info-wol", "--gen-light",
|
||||
"--info-creatures", "--info-objects", "--info-quests",
|
||||
"--info-extract", "--info-extract-tree", "--info-extract-budget",
|
||||
"--list-missing-sidecars",
|
||||
|
|
|
|||
|
|
@ -771,6 +771,10 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Per-group + aggregate geometric stats (surface area, edges, watertight) for a WOB building\n");
|
||||
std::printf(" --info-woc <woc-path> [--json]\n");
|
||||
std::printf(" Print WOC collision metadata (triangle counts, bounds) and exit\n");
|
||||
std::printf(" --info-wol <wol-base> [--json]\n");
|
||||
std::printf(" Print WOL lighting keyframes (zone name + per-time-of-day ambient/directional/fog) and exit\n");
|
||||
std::printf(" --gen-light <wol-base> [zoneName]\n");
|
||||
std::printf(" Emit a starter .wol with the canonical 4-keyframe day/night cycle (midnight + dawn + noon + dusk)\n");
|
||||
std::printf(" --info-wot <wot-base> [--json]\n");
|
||||
std::printf(" Print WOT/WHM terrain metadata (tile, chunks, height range) and exit\n");
|
||||
std::printf(" --info-extract <dir> [--json]\n");
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "pipeline/wowee_building.hpp"
|
||||
#include "pipeline/wowee_collision.hpp"
|
||||
#include "pipeline/wowee_light.hpp"
|
||||
#include "pipeline/wowee_terrain_loader.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
|
|
@ -327,6 +328,94 @@ int handleInfoWoc(int& i, int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int handleInfoWol(int& i, int argc, char** argv) {
|
||||
// Inspect a Wowee Open Light (.wol) file: zone name + per-
|
||||
// keyframe time-of-day + ambient/directional/fog colors and
|
||||
// fog distances.
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = (i + 1 < argc &&
|
||||
std::strcmp(argv[i + 1], "--json") == 0);
|
||||
if (jsonOut) ++i;
|
||||
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wol")
|
||||
base = base.substr(0, base.size() - 4);
|
||||
if (!wowee::pipeline::WoweeLightLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WOL not found: %s.wol\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto wol = wowee::pipeline::WoweeLightLoader::load(base);
|
||||
if (!wol.isValid()) {
|
||||
std::fprintf(stderr, "WOL parse failed: %s.wol\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wol"] = base + ".wol";
|
||||
j["name"] = wol.name;
|
||||
j["keyframeCount"] = wol.keyframes.size();
|
||||
nlohmann::json kfs = nlohmann::json::array();
|
||||
for (const auto& kf : wol.keyframes) {
|
||||
kfs.push_back({
|
||||
{"timeOfDayMin", kf.timeOfDayMin},
|
||||
{"ambient", {kf.ambientColor.r, kf.ambientColor.g,
|
||||
kf.ambientColor.b}},
|
||||
{"directional", {kf.directionalColor.r,
|
||||
kf.directionalColor.g,
|
||||
kf.directionalColor.b}},
|
||||
{"directionalDir", {kf.directionalDir.x,
|
||||
kf.directionalDir.y,
|
||||
kf.directionalDir.z}},
|
||||
{"fog", {kf.fogColor.r, kf.fogColor.g, kf.fogColor.b}},
|
||||
{"fogStart", kf.fogStart},
|
||||
{"fogEnd", kf.fogEnd},
|
||||
});
|
||||
}
|
||||
j["keyframes"] = kfs;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WOL: %s.wol\n", base.c_str());
|
||||
std::printf(" zone : %s\n", wol.name.c_str());
|
||||
std::printf(" keyframes : %zu\n", wol.keyframes.size());
|
||||
for (std::size_t k = 0; k < wol.keyframes.size(); ++k) {
|
||||
const auto& kf = wol.keyframes[k];
|
||||
std::printf(" [%zu] %02u:%02u ambient=(%.2f, %.2f, %.2f) "
|
||||
"fog=(%.2f, %.2f, %.2f) [%.0f..%.0f]\n",
|
||||
k,
|
||||
kf.timeOfDayMin / 60, kf.timeOfDayMin % 60,
|
||||
kf.ambientColor.r, kf.ambientColor.g, kf.ambientColor.b,
|
||||
kf.fogColor.r, kf.fogColor.g, kf.fogColor.b,
|
||||
kf.fogStart, kf.fogEnd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenLight(int& i, int argc, char** argv) {
|
||||
// Emit a starter .wol file with the default 4-keyframe day/
|
||||
// night cycle (midnight, dawn, noon, dusk). User can edit
|
||||
// the keyframes by re-saving via a future authoring tool;
|
||||
// for now this is the canonical "make me a usable atmosphere
|
||||
// file in one command" entrypoint.
|
||||
std::string base = argv[++i];
|
||||
std::string zoneName = "Default";
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
zoneName = argv[++i];
|
||||
}
|
||||
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wol") {
|
||||
base = base.substr(0, base.size() - 4);
|
||||
}
|
||||
auto wol = wowee::pipeline::WoweeLightLoader::makeDefaultDayNight(zoneName);
|
||||
if (!wowee::pipeline::WoweeLightLoader::save(wol, base)) {
|
||||
std::fprintf(stderr, "gen-light: failed to save %s.wol\n",
|
||||
base.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("Wrote %s.wol\n", base.c_str());
|
||||
std::printf(" zone : %s\n", zoneName.c_str());
|
||||
std::printf(" keyframes : %zu (midnight + dawn + noon + dusk)\n",
|
||||
wol.keyframes.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleWorldInfo(int& i, int argc, char** argv, int& outRc) {
|
||||
|
|
@ -342,6 +431,12 @@ bool handleWorldInfo(int& i, int argc, char** argv, int& outRc) {
|
|||
if (std::strcmp(argv[i], "--info-woc") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfoWoc(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wol") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfoWol(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-light") == 0 && i + 1 < argc) {
|
||||
outRc = handleGenLight(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue