diff --git a/assets/shaders/wmo.frag.glsl b/assets/shaders/wmo.frag.glsl index 8d21dc74..aaffef29 100644 --- a/assets/shaders/wmo.frag.glsl +++ b/assets/shaders/wmo.frag.glsl @@ -27,6 +27,7 @@ layout(set = 1, binding = 1) uniform WMOMaterial { float pomScale; int pomMaxSamples; float heightMapVariance; + float normalMapStrength; }; layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap; @@ -55,6 +56,10 @@ float computeLodFactor() { // Parallax Occlusion Mapping with angle-adaptive sampling vec2 parallaxOcclusionMap(vec2 uv, vec3 viewDirTS, float lodFactor) { float VdotN = abs(viewDirTS.z); // 1=head-on, 0=grazing + + // Fade out POM at grazing angles to avoid distortion + if (VdotN < 0.15) return uv; + float angleFactor = clamp(VdotN, 0.15, 1.0); int maxS = pomMaxSamples; int minS = max(maxS / 4, 4); @@ -64,8 +69,11 @@ vec2 parallaxOcclusionMap(vec2 uv, vec3 viewDirTS, float lodFactor) { float layerDepth = 1.0 / float(numSamples); float currentLayerDepth = 0.0; - // Direction to shift UV per layer - vec2 P = viewDirTS.xy / max(abs(viewDirTS.z), 0.001) * pomScale; + // Direction to shift UV per layer — clamp denominator to prevent explosion at grazing angles + vec2 P = viewDirTS.xy / max(VdotN, 0.15) * pomScale; + // Hard-clamp total UV offset to prevent texture swimming + float maxOffset = pomScale * 3.0; + P = clamp(P, vec2(-maxOffset), vec2(maxOffset)); vec2 deltaUV = P / float(numSamples); vec2 currentUV = uv; @@ -84,7 +92,11 @@ vec2 parallaxOcclusionMap(vec2 uv, vec3 viewDirTS, float lodFactor) { float afterDepth = currentDepthMapValue - currentLayerDepth; float beforeDepth = (1.0 - texture(uNormalHeightMap, prevUV).a) - currentLayerDepth + layerDepth; float weight = afterDepth / (afterDepth - beforeDepth + 0.0001); - return mix(currentUV, prevUV, weight); + vec2 result = mix(currentUV, prevUV, weight); + + // Fade toward original UV at grazing angles for smooth transition + float fadeFactor = smoothstep(0.15, 0.35, VdotN); + return mix(uv, result, fadeFactor); } void main() { @@ -114,11 +126,16 @@ void main() { // Compute normal (with normal mapping if enabled) vec3 norm = vertexNormal; - if (enableNormalMap != 0 && lodFactor < 0.99) { + if (enableNormalMap != 0 && lodFactor < 0.99 && normalMapStrength > 0.001) { vec3 mapNormal = texture(uNormalHeightMap, finalUV).rgb * 2.0 - 1.0; + // Scale XY by strength to control effect intensity + mapNormal.xy *= normalMapStrength; + mapNormal = normalize(mapNormal); vec3 worldNormal = normalize(TBN * mapNormal); if (!gl_FrontFacing) worldNormal = -worldNormal; - norm = normalize(mix(worldNormal, vertexNormal, lodFactor)); + // Blend: strength + LOD both contribute to fade toward vertex normal + float blendFactor = max(lodFactor, 1.0 - normalMapStrength); + norm = normalize(mix(worldNormal, vertexNormal, blendFactor)); } vec3 result; diff --git a/assets/shaders/wmo.frag.spv b/assets/shaders/wmo.frag.spv index 2cfb1540..f1241ab8 100644 Binary files a/assets/shaders/wmo.frag.spv and b/assets/shaders/wmo.frag.spv differ diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 328e6f7e..3376e5c7 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -186,9 +186,11 @@ public: * Normal mapping / Parallax Occlusion Mapping settings */ void setNormalMappingEnabled(bool enabled) { normalMappingEnabled_ = enabled; materialSettingsDirty_ = true; } + void setNormalMapStrength(float s) { normalMapStrength_ = s; materialSettingsDirty_ = true; } void setPOMEnabled(bool enabled) { pomEnabled_ = enabled; materialSettingsDirty_ = true; } void setPOMQuality(int q) { pomQuality_ = q; materialSettingsDirty_ = true; } bool isNormalMappingEnabled() const { return normalMappingEnabled_; } + float getNormalMapStrength() const { return normalMapStrength_; } bool isPOMEnabled() const { return pomEnabled_; } int getPOMQuality() const { return pomQuality_; } @@ -326,7 +328,7 @@ private: float pomScale; // 32 (height scale) int32_t pomMaxSamples; // 36 (max ray-march steps) float heightMapVariance; // 40 (low variance = skip POM) - float pad; // 44 + float normalMapStrength; // 44 (0=flat, 1=full, 2=exaggerated) }; // 48 bytes total /** @@ -643,6 +645,7 @@ private: // Normal mapping / POM settings bool normalMappingEnabled_ = true; // on by default + float normalMapStrength_ = 1.0f; // 0.0 = flat, 1.0 = full, 2.0 = exaggerated bool pomEnabled_ = false; // off by default (expensive) int pomQuality_ = 1; // 0=Low(16), 1=Medium(32), 2=High(64) bool materialSettingsDirty_ = false; // rebuild UBOs when settings change diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 5f269477..69450693 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -104,6 +104,7 @@ private: int pendingGroundClutterDensity = 100; int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x bool pendingNormalMapping = true; // on by default + float pendingNormalMapStrength = 1.0f; // 0.0-2.0 bool pendingPOM = false; // off by default (expensive) int pendingPOMQuality = 1; // 0=Low(16), 1=Medium(32), 2=High(64) diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index c4d8431f..2768b5df 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -601,12 +601,13 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { matData.isWindow = mb.isWindow ? 1 : 0; matData.enableNormalMap = normalMappingEnabled_ ? 1 : 0; matData.enablePOM = pomEnabled_ ? 1 : 0; - matData.pomScale = 0.03f; + matData.pomScale = 0.012f; { static const int pomSampleTable[] = { 16, 32, 64 }; matData.pomMaxSamples = pomSampleTable[std::clamp(pomQuality_, 0, 2)]; } matData.heightMapVariance = mb.heightMapVariance; + matData.normalMapStrength = normalMapStrength_; if (matBuf.info.pMappedData) { memcpy(matBuf.info.pMappedData, &matData, sizeof(matData)); } @@ -1228,9 +1229,10 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const auto* ubo = reinterpret_cast(allocInfo.pMappedData); ubo->enableNormalMap = normalMappingEnabled_ ? 1 : 0; ubo->enablePOM = pomEnabled_ ? 1 : 0; - ubo->pomScale = 0.03f; + ubo->pomScale = 0.012f; ubo->pomMaxSamples = maxSamples; ubo->heightMapVariance = mb.heightMapVariance; + ubo->normalMapStrength = normalMapStrength_; } } } @@ -2019,7 +2021,27 @@ std::unique_ptr WMORenderer::generateNormalHeightMap( double mean = sumH / totalPixels; outVariance = static_cast(sumH2 / totalPixels - mean * mean); - // Step 2: Sobel 3x3 → normal map (wrap-sampled for tiling textures) + // Step 1.5: Box blur the height map to reduce noise from diffuse textures + auto wrapSample = [&](const std::vector& map, int x, int y) -> float { + x = ((x % (int)width) + (int)width) % (int)width; + y = ((y % (int)height) + (int)height) % (int)height; + return map[y * width + x]; + }; + + std::vector blurredHeight(totalPixels); + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + int ix = static_cast(x), iy = static_cast(y); + float sum = 0.0f; + for (int dy = -1; dy <= 1; dy++) + for (int dx = -1; dx <= 1; dx++) + sum += wrapSample(heightMap, ix + dx, iy + dy); + blurredHeight[y * width + x] = sum / 9.0f; + } + } + + // Step 2: Sobel 3x3 → normal map + // Use ORIGINAL height for normals (crisp detail), blurred height for POM alpha only const float strength = 2.0f; std::vector output(totalPixels * 4); @@ -2050,7 +2072,7 @@ std::unique_ptr WMORenderer::generateNormalHeightMap( output[idx + 0] = static_cast(std::clamp((nx * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f)); output[idx + 1] = static_cast(std::clamp((ny * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f)); output[idx + 2] = static_cast(std::clamp((nz * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f)); - output[idx + 3] = static_cast(std::clamp(heightMap[y * width + x] * 255.0f, 0.0f, 255.0f)); + output[idx + 3] = static_cast(std::clamp(blurredHeight[y * width + x] * 255.0f, 0.0f, 255.0f)); } } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 42392cd8..e38d1475 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -284,6 +284,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (renderer) { if (auto* wr = renderer->getWMORenderer()) { wr->setNormalMappingEnabled(pendingNormalMapping); + wr->setNormalMapStrength(pendingNormalMapStrength); wr->setPOMEnabled(pendingPOM); wr->setPOMQuality(pendingPOMQuality); normalMapSettingsApplied_ = true; @@ -5916,6 +5917,16 @@ void GameScreen::renderSettingsWindow() { } saveSettings(); } + if (pendingNormalMapping) { + if (ImGui::SliderFloat("Normal Map Strength", &pendingNormalMapStrength, 0.0f, 2.0f, "%.1f")) { + if (renderer) { + if (auto* wr = renderer->getWMORenderer()) { + wr->setNormalMapStrength(pendingNormalMapStrength); + } + } + saveSettings(); + } + } if (ImGui::Checkbox("Parallax Mapping", &pendingPOM)) { if (renderer) { if (auto* wr = renderer->getWMORenderer()) { @@ -5959,6 +5970,7 @@ void GameScreen::renderSettingsWindow() { pendingGroundClutterDensity = kDefaultGroundClutterDensity; pendingAntiAliasing = 0; pendingNormalMapping = true; + pendingNormalMapStrength = 1.0f; pendingPOM = false; pendingPOMQuality = 1; pendingResIndex = defaultResIndex; @@ -5975,6 +5987,7 @@ void GameScreen::renderSettingsWindow() { if (renderer) { if (auto* wr = renderer->getWMORenderer()) { wr->setNormalMappingEnabled(pendingNormalMapping); + wr->setNormalMapStrength(pendingNormalMapStrength); wr->setPOMEnabled(pendingPOM); wr->setPOMQuality(pendingPOMQuality); } @@ -6906,6 +6919,7 @@ void GameScreen::saveSettings() { out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n"; out << "antialiasing=" << pendingAntiAliasing << "\n"; out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n"; + out << "normal_map_strength=" << pendingNormalMapStrength << "\n"; out << "pom=" << (pendingPOM ? 1 : 0) << "\n"; out << "pom_quality=" << pendingPOMQuality << "\n"; @@ -6987,6 +7001,7 @@ void GameScreen::loadSettings() { 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 == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f); else if (key == "pom") pendingPOM = (std::stoi(val) != 0); else if (key == "pom_quality") pendingPOMQuality = std::clamp(std::stoi(val), 0, 2); // Controls