Add normal mapping and parallax occlusion mapping for WMO surfaces

Generate normal+height maps from diffuse textures at load time using
luminance-to-height and Sobel 3x3 filtering. Compute per-vertex tangents
via Lengyel's method for TBN basis construction.

Fragment shader uses screen-space UV derivatives (dFdx/dFdy) for smooth
LOD crossfade and angle-adaptive POM sample counts. Flat textures
naturally produce low height variance, causing POM to self-select off.

Settings: Normal Mapping on by default, POM off by default with
Low/Medium/High quality presets. Persisted across sessions.
This commit is contained in:
Kelsi 2026-02-23 01:10:58 -08:00
parent 1b16bcf71f
commit eaceb58e77
8 changed files with 424 additions and 33 deletions

View file

@ -6,6 +6,7 @@
#include "core/spawn_presets.hpp"
#include "core/input.hpp"
#include "rendering/renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/minimap.hpp"
#include "rendering/world_map.hpp"
@ -277,6 +278,19 @@ void GameScreen::render(game::GameHandler& gameHandler) {
msaaSettingsApplied_ = true;
}
// Apply saved normal mapping / POM settings once when WMO renderer is available
if (!normalMapSettingsApplied_) {
auto* renderer = core::Application::getInstance().getRenderer();
if (renderer) {
if (auto* wr = renderer->getWMORenderer()) {
wr->setNormalMappingEnabled(pendingNormalMapping);
wr->setPOMEnabled(pendingPOM);
wr->setPOMQuality(pendingPOMQuality);
normalMapSettingsApplied_ = true;
}
}
}
// Apply auto-loot setting to GameHandler every frame (cheap bool sync)
gameHandler.setAutoLoot(pendingAutoLoot);
@ -5894,6 +5908,33 @@ void GameScreen::renderSettingsWindow() {
}
saveSettings();
}
if (ImGui::Checkbox("Normal Mapping", &pendingNormalMapping)) {
if (renderer) {
if (auto* wr = renderer->getWMORenderer()) {
wr->setNormalMappingEnabled(pendingNormalMapping);
}
}
saveSettings();
}
if (ImGui::Checkbox("Parallax Mapping", &pendingPOM)) {
if (renderer) {
if (auto* wr = renderer->getWMORenderer()) {
wr->setPOMEnabled(pendingPOM);
}
}
saveSettings();
}
if (pendingPOM) {
const char* pomLabels[] = { "Low", "Medium", "High" };
if (ImGui::Combo("Parallax Quality", &pendingPOMQuality, pomLabels, 3)) {
if (renderer) {
if (auto* wr = renderer->getWMORenderer()) {
wr->setPOMQuality(pendingPOMQuality);
}
}
saveSettings();
}
}
const char* resLabel = "Resolution";
const char* resItems[kResCount];
@ -5917,6 +5958,9 @@ void GameScreen::renderSettingsWindow() {
pendingShadows = kDefaultShadows;
pendingGroundClutterDensity = kDefaultGroundClutterDensity;
pendingAntiAliasing = 0;
pendingNormalMapping = true;
pendingPOM = false;
pendingPOMQuality = 1;
pendingResIndex = defaultResIndex;
window->setFullscreen(pendingFullscreen);
window->setVsync(pendingVsync);
@ -5928,6 +5972,13 @@ void GameScreen::renderSettingsWindow() {
tm->setGroundClutterDensityScale(static_cast<float>(pendingGroundClutterDensity) / 100.0f);
}
}
if (renderer) {
if (auto* wr = renderer->getWMORenderer()) {
wr->setNormalMappingEnabled(pendingNormalMapping);
wr->setPOMEnabled(pendingPOM);
wr->setPOMQuality(pendingPOMQuality);
}
}
saveSettings();
}
@ -6854,6 +6905,9 @@ void GameScreen::saveSettings() {
out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n";
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
out << "antialiasing=" << pendingAntiAliasing << "\n";
out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n";
out << "pom=" << (pendingPOM ? 1 : 0) << "\n";
out << "pom_quality=" << pendingPOMQuality << "\n";
// Controls
out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n";
@ -6932,6 +6986,9 @@ void GameScreen::loadSettings() {
else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0);
else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0);
else if (key == "pom") pendingPOM = (std::stoi(val) != 0);
else if (key == "pom_quality") pendingPOMQuality = std::clamp(std::stoi(val), 0, 2);
// Controls
else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f);
else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0);