feat(render): SAGE water/ocean shaders, per-user logs, camera & culling

Terrain pass now ports the SAGE water model (Ocean.fx / OpenSAGE Water.frag): a de-gridded procedural wave normal combined with the retail ra3_deepocean flow and ra3_deepocean_nrm bump maps (appended as the last two terrain-atlas layers, no new backend binding), Schlick fresnel, sky reflection + depth-graded refraction, SAGE diffuse/specular lighting, depth-based transparency, and an underwater tint (UnderwaterDeferred.fx). Mirrored across terrain.frag / dx_terrain.hlsl / webgl_terrain_frag.glsl / webgpu_terrain.wgsl.

Also: logs move to the per-user state dir (%LOCALAPPDATA%\\OpenRA3\\logs, else XDG) and archive as openra3.<stamp>.log; the FPS label shows the active backend; middle-drag camera reset; objects and roads below the water plane are culled.
This commit is contained in:
EnderTheCoder
2026-09-30 22:18:08 +08:00
parent 10a1963eec
commit 26e1934a5d
16 changed files with 969 additions and 249 deletions
+2 -2
View File
@@ -131,13 +131,13 @@ and `log::set_sinks({...})` replaces them.
```cpp
namespace log = ender::log;
// Archive any existing enderlog.log to enderlog.log.<timestamp>, then start a
// Archive any existing enderlog.log to enderlog.<timestamp>.log, then start a
// fresh file for this run. Rotate at 64 KiB and keep the last 5 archives.
auto sink = log::add_file_sink("enderlog.log", {.max_file_size = 64 * 1024, .max_archives = 5});
```
- **No appending onto a previous run.** On open, an existing non-empty
`enderlog.log` is renamed to `enderlog.log.<YYYYmmdd-HHMMSS>` before the new
`enderlog.log` is renamed to `enderlog.<YYYYmmdd-HHMMSS>.log` before the new
file is created, so every run gets its own file and the previous run's log is
preserved. A leftover empty file is simply replaced.
- `file_options::max_file_size` (0 disables) rotates the active file mid-run the
+14 -5
View File
@@ -182,7 +182,8 @@ export namespace ender::log {
* `path` is the active file. When the sink opens it and the file already
* holds data, that file is renamed to a timestamped archive first, so a run
* never appends onto a previous run's log: every start begins a fresh file
* and the old one is preserved as `<path>.<YYYYmmdd-HHMMSS>`. The same
* and the old one is preserved with the timestamp before its extension, as
* `<stem>.<YYYYmmdd-HHMMSS>.log`. The same
* happens mid-run once the active file passes `file_options::max_file_size`.
* `file_options::max_archives` bounds how many archives are kept.
*
@@ -238,13 +239,21 @@ export namespace ender::log {
if (stream_.is_open()) stream_.close();
const auto stamp = std::format("{:%Y%m%d-%H%M%S}",
std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now()));
auto archive = path_;
archive += "." + stamp;
// Keep the original extension last, with the timestamp in the
// middle: `<stem>.<stamp>[.<n>]<ext>`.
const auto name = [&](const std::size_t counter) {
auto candidate = path_.parent_path() / path_.stem();
candidate += ".";
candidate += stamp;
if (counter > 0) candidate += std::format(".{}", counter);
candidate += path_.extension();
return candidate;
};
auto archive = name(0);
// Two rotations can land in the same second; disambiguate with a
// counter rather than overwrite the earlier archive.
for (auto counter = 1; std::filesystem::exists(archive); ++counter) {
archive = path_;
archive += std::format(".{}.{}", stamp, counter);
archive = name(counter);
}
std::filesystem::rename(path_, archive);
archives_.push_back(archive);