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

@ -182,6 +182,16 @@ public:
*/
uint32_t getDrawCallCount() const { return lastDrawCalls; }
/**
* Normal mapping / Parallax Occlusion Mapping settings
*/
void setNormalMappingEnabled(bool enabled) { normalMappingEnabled_ = enabled; materialSettingsDirty_ = true; }
void setPOMEnabled(bool enabled) { pomEnabled_ = enabled; materialSettingsDirty_ = true; }
void setPOMQuality(int q) { pomQuality_ = q; materialSettingsDirty_ = true; }
bool isNormalMappingEnabled() const { return normalMappingEnabled_; }
bool isPOMEnabled() const { return pomEnabled_; }
int getPOMQuality() const { return pomQuality_; }
/**
* Enable/disable wireframe rendering
*/
@ -305,14 +315,19 @@ public:
private:
// WMO material UBO — matches WMOMaterial in wmo.frag.glsl
struct WMOMaterialUBO {
int32_t hasTexture;
int32_t alphaTest;
int32_t unlit;
int32_t isInterior;
float specularIntensity;
int32_t isWindow;
float pad[2]; // pad to 32 bytes
};
int32_t hasTexture; // 0
int32_t alphaTest; // 4
int32_t unlit; // 8
int32_t isInterior; // 12
float specularIntensity; // 16
int32_t isWindow; // 20
int32_t enableNormalMap; // 24
int32_t enablePOM; // 28
float pomScale; // 32 (height scale)
int32_t pomMaxSamples; // 36 (max ray-march steps)
float heightMapVariance; // 40 (low variance = skip POM)
float pad; // 44
}; // 48 bytes total
/**
* WMO group GPU resources
@ -341,6 +356,8 @@ private:
// Pre-merged batches for efficient rendering (computed at load time)
struct MergedBatch {
VkTexture* texture = nullptr; // from cache, NOT owned
VkTexture* normalHeightMap = nullptr; // generated from diffuse, NOT owned
float heightMapVariance = 0.0f; // variance of height map (low = flat texture)
VkDescriptorSet materialSet = VK_NULL_HANDLE; // set 1
::VkBuffer materialUBO = VK_NULL_HANDLE;
VmaAllocation materialUBOAlloc = VK_NULL_HANDLE;
@ -515,6 +532,16 @@ private:
*/
VkTexture* loadTexture(const std::string& path);
/**
* Generate normal+height map from diffuse RGBA8 pixels
* @param pixels RGBA8 pixel data
* @param width Texture width
* @param height Texture height
* @param outVariance Receives height map variance (for POM threshold)
* @return Generated VkTexture (RGBA8: RGB=normal, A=height)
*/
std::unique_ptr<VkTexture> generateNormalHeightMap(const uint8_t* pixels, uint32_t width, uint32_t height, float& outVariance);
/**
* Allocate a material descriptor set from the pool
*/
@ -584,6 +611,8 @@ private:
// Texture cache (path -> VkTexture)
struct TextureCacheEntry {
std::unique_ptr<VkTexture> texture;
std::unique_ptr<VkTexture> normalHeightMap; // generated normal+height from diffuse
float heightMapVariance = 0.0f; // variance of generated height map
size_t approxBytes = 0;
uint64_t lastUse = 0;
};
@ -598,6 +627,9 @@ private:
// Default white texture
std::unique_ptr<VkTexture> whiteTexture_;
// Flat normal placeholder (128,128,255,128) = up-pointing normal, mid-height
std::unique_ptr<VkTexture> flatNormalTexture_;
// Loaded models (modelId -> ModelData)
std::unordered_map<uint32_t, ModelData> loadedModels;
size_t modelCacheLimit_ = 4000;
@ -609,6 +641,12 @@ private:
bool initialized_ = false;
// Normal mapping / POM settings
bool normalMappingEnabled_ = true; // on by default
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
// Rendering state
bool wireframeMode = false;
bool frustumCulling = true;