diff --git a/CMakeLists.txt b/CMakeLists.txt index e082df88..2f5bbb6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/pipeline/wowee_light.hpp b/include/pipeline/wowee_light.hpp new file mode 100644 index 00000000..50b5efa8 --- /dev/null +++ b/include/pipeline/wowee_light.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +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 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 diff --git a/src/pipeline/wowee_light.cpp b/src/pipeline/wowee_light.cpp new file mode 100644 index 00000000..67f97139 --- /dev/null +++ b/src/pipeline/wowee_light.cpp @@ -0,0 +1,152 @@ +#include "pipeline/wowee_light.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'O', 'L', 'A'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(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(light.name.size()); + writePOD(os, nameLen); + if (nameLen > 0) os.write(light.name.data(), nameLen); + uint32_t kfCount = static_cast(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(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 diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index cf6d26c0..d79c1b5d 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -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", diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 2504e7c8..af399bec 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -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 [--json]\n"); std::printf(" Print WOC collision metadata (triangle counts, bounds) and exit\n"); + std::printf(" --info-wol [--json]\n"); + std::printf(" Print WOL lighting keyframes (zone name + per-time-of-day ambient/directional/fog) and exit\n"); + std::printf(" --gen-light [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 [--json]\n"); std::printf(" Print WOT/WHM terrain metadata (tile, chunks, height range) and exit\n"); std::printf(" --info-extract [--json]\n"); diff --git a/tools/editor/cli_world_info.cpp b/tools/editor/cli_world_info.cpp index ef097f9f..a09db6a2 100644 --- a/tools/editor/cli_world_info.cpp +++ b/tools/editor/cli_world_info.cpp @@ -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 @@ -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; }