mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
1b16bcf71f
commit
eaceb58e77
8 changed files with 424 additions and 33 deletions
|
|
@ -22,23 +22,104 @@ layout(set = 1, binding = 1) uniform WMOMaterial {
|
||||||
int isInterior;
|
int isInterior;
|
||||||
float specularIntensity;
|
float specularIntensity;
|
||||||
int isWindow;
|
int isWindow;
|
||||||
|
int enableNormalMap;
|
||||||
|
int enablePOM;
|
||||||
|
float pomScale;
|
||||||
|
int pomMaxSamples;
|
||||||
|
float heightMapVariance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
|
||||||
|
|
||||||
layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
|
layout(set = 0, binding = 1) uniform sampler2DShadow uShadowMap;
|
||||||
|
|
||||||
layout(location = 0) in vec3 FragPos;
|
layout(location = 0) in vec3 FragPos;
|
||||||
layout(location = 1) in vec3 Normal;
|
layout(location = 1) in vec3 Normal;
|
||||||
layout(location = 2) in vec2 TexCoord;
|
layout(location = 2) in vec2 TexCoord;
|
||||||
layout(location = 3) in vec4 VertColor;
|
layout(location = 3) in vec4 VertColor;
|
||||||
|
layout(location = 4) in vec3 Tangent;
|
||||||
|
layout(location = 5) in vec3 Bitangent;
|
||||||
|
|
||||||
layout(location = 0) out vec4 outColor;
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
// LOD factor from screen-space UV derivatives
|
||||||
|
float computeLodFactor() {
|
||||||
|
vec2 dx = dFdx(TexCoord);
|
||||||
|
vec2 dy = dFdy(TexCoord);
|
||||||
|
float texelDensity = max(dot(dx, dx), dot(dy, dy));
|
||||||
|
// Low density = close/head-on = full detail (0)
|
||||||
|
// High density = far/steep = vertex normals only (1)
|
||||||
|
return smoothstep(0.0001, 0.005, texelDensity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
float angleFactor = clamp(VdotN, 0.15, 1.0);
|
||||||
|
int maxS = pomMaxSamples;
|
||||||
|
int minS = max(maxS / 4, 4);
|
||||||
|
int numSamples = int(mix(float(minS), float(maxS), angleFactor));
|
||||||
|
numSamples = int(mix(float(minS), float(numSamples), 1.0 - 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;
|
||||||
|
vec2 deltaUV = P / float(numSamples);
|
||||||
|
|
||||||
|
vec2 currentUV = uv;
|
||||||
|
float currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
|
||||||
|
|
||||||
|
// Ray march through layers
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
if (i >= numSamples || currentLayerDepth >= currentDepthMapValue) break;
|
||||||
|
currentUV -= deltaUV;
|
||||||
|
currentDepthMapValue = 1.0 - texture(uNormalHeightMap, currentUV).a;
|
||||||
|
currentLayerDepth += layerDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate between last two layers for smooth result
|
||||||
|
vec2 prevUV = currentUV + deltaUV;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec4 texColor = hasTexture != 0 ? texture(uTexture, TexCoord) : vec4(1.0);
|
float lodFactor = computeLodFactor();
|
||||||
|
|
||||||
|
vec3 vertexNormal = normalize(Normal);
|
||||||
|
if (!gl_FrontFacing) vertexNormal = -vertexNormal;
|
||||||
|
|
||||||
|
// Compute final UV (with POM if enabled)
|
||||||
|
vec2 finalUV = TexCoord;
|
||||||
|
|
||||||
|
// Build TBN matrix
|
||||||
|
vec3 T = normalize(Tangent);
|
||||||
|
vec3 B = normalize(Bitangent);
|
||||||
|
vec3 N = vertexNormal;
|
||||||
|
mat3 TBN = mat3(T, B, N);
|
||||||
|
|
||||||
|
if (enablePOM != 0 && heightMapVariance > 0.001 && lodFactor < 0.99) {
|
||||||
|
mat3 TBN_inv = transpose(TBN);
|
||||||
|
vec3 viewDirWorld = normalize(viewPos.xyz - FragPos);
|
||||||
|
vec3 viewDirTS = TBN_inv * viewDirWorld;
|
||||||
|
finalUV = parallaxOcclusionMap(TexCoord, viewDirTS, lodFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 texColor = hasTexture != 0 ? texture(uTexture, finalUV) : vec4(1.0);
|
||||||
if (alphaTest != 0 && texColor.a < 0.5) discard;
|
if (alphaTest != 0 && texColor.a < 0.5) discard;
|
||||||
|
|
||||||
vec3 norm = normalize(Normal);
|
// Compute normal (with normal mapping if enabled)
|
||||||
if (!gl_FrontFacing) norm = -norm;
|
vec3 norm = vertexNormal;
|
||||||
|
if (enableNormalMap != 0 && lodFactor < 0.99) {
|
||||||
|
vec3 mapNormal = texture(uNormalHeightMap, finalUV).rgb * 2.0 - 1.0;
|
||||||
|
vec3 worldNormal = normalize(TBN * mapNormal);
|
||||||
|
if (!gl_FrontFacing) worldNormal = -worldNormal;
|
||||||
|
norm = normalize(mix(worldNormal, vertexNormal, lodFactor));
|
||||||
|
}
|
||||||
|
|
||||||
vec3 result;
|
vec3 result;
|
||||||
|
|
||||||
|
|
@ -82,39 +163,29 @@ void main() {
|
||||||
float alpha = texColor.a;
|
float alpha = texColor.a;
|
||||||
|
|
||||||
// Window glass: opaque but simulates dark tinted glass with reflections.
|
// Window glass: opaque but simulates dark tinted glass with reflections.
|
||||||
// No real alpha blending — we darken the base texture and add reflection
|
|
||||||
// on top so it reads as glass without needing the transparent pipeline.
|
|
||||||
if (isWindow != 0) {
|
if (isWindow != 0) {
|
||||||
vec3 viewDir = normalize(viewPos.xyz - FragPos);
|
vec3 viewDir = normalize(viewPos.xyz - FragPos);
|
||||||
float NdotV = abs(dot(norm, viewDir));
|
float NdotV = abs(dot(norm, viewDir));
|
||||||
// Fresnel: strong reflection at grazing angles
|
|
||||||
float fresnel = 0.08 + 0.92 * pow(1.0 - NdotV, 4.0);
|
float fresnel = 0.08 + 0.92 * pow(1.0 - NdotV, 4.0);
|
||||||
|
|
||||||
// Glass darkness depends on view angle — bright when sun glints off,
|
|
||||||
// darker when looking straight on with no sun reflection.
|
|
||||||
vec3 ldir = normalize(-lightDir.xyz);
|
vec3 ldir = normalize(-lightDir.xyz);
|
||||||
vec3 reflectDir = reflect(-viewDir, norm);
|
vec3 reflectDir = reflect(-viewDir, norm);
|
||||||
float sunGlint = pow(max(dot(reflectDir, ldir), 0.0), 32.0);
|
float sunGlint = pow(max(dot(reflectDir, ldir), 0.0), 32.0);
|
||||||
|
|
||||||
// Base ranges from dark (0.3) to bright (0.9) based on sun reflection
|
|
||||||
float baseBrightness = mix(0.3, 0.9, sunGlint);
|
float baseBrightness = mix(0.3, 0.9, sunGlint);
|
||||||
vec3 glass = result * baseBrightness;
|
vec3 glass = result * baseBrightness;
|
||||||
|
|
||||||
// Reflection: blend sky/ambient color based on Fresnel
|
|
||||||
vec3 reflectTint = mix(ambientColor.rgb * 1.2, vec3(0.6, 0.75, 1.0), 0.6);
|
vec3 reflectTint = mix(ambientColor.rgb * 1.2, vec3(0.6, 0.75, 1.0), 0.6);
|
||||||
glass = mix(glass, reflectTint, fresnel * 0.8);
|
glass = mix(glass, reflectTint, fresnel * 0.8);
|
||||||
|
|
||||||
// Sharp sun glint on glass
|
|
||||||
vec3 halfDir = normalize(ldir + viewDir);
|
vec3 halfDir = normalize(ldir + viewDir);
|
||||||
float spec = pow(max(dot(norm, halfDir), 0.0), 256.0);
|
float spec = pow(max(dot(norm, halfDir), 0.0), 256.0);
|
||||||
glass += spec * lightColor.rgb * 0.8;
|
glass += spec * lightColor.rgb * 0.8;
|
||||||
|
|
||||||
// Broad warm sheen when sun is nearby
|
|
||||||
float specBroad = pow(max(dot(norm, halfDir), 0.0), 12.0);
|
float specBroad = pow(max(dot(norm, halfDir), 0.0), 12.0);
|
||||||
glass += specBroad * lightColor.rgb * 0.12;
|
glass += specBroad * lightColor.rgb * 0.12;
|
||||||
|
|
||||||
result = glass;
|
result = glass;
|
||||||
// Fresnel-based transparency: more transparent at oblique angles
|
|
||||||
alpha = mix(0.4, 0.95, NdotV);
|
alpha = mix(0.4, 0.95, NdotV);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -21,17 +21,33 @@ layout(location = 0) in vec3 aPos;
|
||||||
layout(location = 1) in vec3 aNormal;
|
layout(location = 1) in vec3 aNormal;
|
||||||
layout(location = 2) in vec2 aTexCoord;
|
layout(location = 2) in vec2 aTexCoord;
|
||||||
layout(location = 3) in vec4 aColor;
|
layout(location = 3) in vec4 aColor;
|
||||||
|
layout(location = 4) in vec4 aTangent;
|
||||||
|
|
||||||
layout(location = 0) out vec3 FragPos;
|
layout(location = 0) out vec3 FragPos;
|
||||||
layout(location = 1) out vec3 Normal;
|
layout(location = 1) out vec3 Normal;
|
||||||
layout(location = 2) out vec2 TexCoord;
|
layout(location = 2) out vec2 TexCoord;
|
||||||
layout(location = 3) out vec4 VertColor;
|
layout(location = 3) out vec4 VertColor;
|
||||||
|
layout(location = 4) out vec3 Tangent;
|
||||||
|
layout(location = 5) out vec3 Bitangent;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec4 worldPos = push.model * vec4(aPos, 1.0);
|
vec4 worldPos = push.model * vec4(aPos, 1.0);
|
||||||
FragPos = worldPos.xyz;
|
FragPos = worldPos.xyz;
|
||||||
Normal = mat3(push.model) * aNormal;
|
|
||||||
|
mat3 normalMatrix = mat3(push.model);
|
||||||
|
Normal = normalMatrix * aNormal;
|
||||||
TexCoord = aTexCoord;
|
TexCoord = aTexCoord;
|
||||||
VertColor = aColor;
|
VertColor = aColor;
|
||||||
|
|
||||||
|
// Compute TBN basis vectors for normal mapping
|
||||||
|
vec3 T = normalize(normalMatrix * aTangent.xyz);
|
||||||
|
vec3 N = normalize(Normal);
|
||||||
|
// Gram-Schmidt re-orthogonalize
|
||||||
|
T = normalize(T - dot(T, N) * N);
|
||||||
|
vec3 B = cross(N, T) * aTangent.w;
|
||||||
|
|
||||||
|
Tangent = T;
|
||||||
|
Bitangent = B;
|
||||||
|
|
||||||
gl_Position = projection * view * worldPos;
|
gl_Position = projection * view * worldPos;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -182,6 +182,16 @@ public:
|
||||||
*/
|
*/
|
||||||
uint32_t getDrawCallCount() const { return lastDrawCalls; }
|
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
|
* Enable/disable wireframe rendering
|
||||||
*/
|
*/
|
||||||
|
|
@ -305,14 +315,19 @@ public:
|
||||||
private:
|
private:
|
||||||
// WMO material UBO — matches WMOMaterial in wmo.frag.glsl
|
// WMO material UBO — matches WMOMaterial in wmo.frag.glsl
|
||||||
struct WMOMaterialUBO {
|
struct WMOMaterialUBO {
|
||||||
int32_t hasTexture;
|
int32_t hasTexture; // 0
|
||||||
int32_t alphaTest;
|
int32_t alphaTest; // 4
|
||||||
int32_t unlit;
|
int32_t unlit; // 8
|
||||||
int32_t isInterior;
|
int32_t isInterior; // 12
|
||||||
float specularIntensity;
|
float specularIntensity; // 16
|
||||||
int32_t isWindow;
|
int32_t isWindow; // 20
|
||||||
float pad[2]; // pad to 32 bytes
|
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
|
* WMO group GPU resources
|
||||||
|
|
@ -341,6 +356,8 @@ private:
|
||||||
// Pre-merged batches for efficient rendering (computed at load time)
|
// Pre-merged batches for efficient rendering (computed at load time)
|
||||||
struct MergedBatch {
|
struct MergedBatch {
|
||||||
VkTexture* texture = nullptr; // from cache, NOT owned
|
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
|
VkDescriptorSet materialSet = VK_NULL_HANDLE; // set 1
|
||||||
::VkBuffer materialUBO = VK_NULL_HANDLE;
|
::VkBuffer materialUBO = VK_NULL_HANDLE;
|
||||||
VmaAllocation materialUBOAlloc = VK_NULL_HANDLE;
|
VmaAllocation materialUBOAlloc = VK_NULL_HANDLE;
|
||||||
|
|
@ -515,6 +532,16 @@ private:
|
||||||
*/
|
*/
|
||||||
VkTexture* loadTexture(const std::string& path);
|
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
|
* Allocate a material descriptor set from the pool
|
||||||
*/
|
*/
|
||||||
|
|
@ -584,6 +611,8 @@ private:
|
||||||
// Texture cache (path -> VkTexture)
|
// Texture cache (path -> VkTexture)
|
||||||
struct TextureCacheEntry {
|
struct TextureCacheEntry {
|
||||||
std::unique_ptr<VkTexture> texture;
|
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;
|
size_t approxBytes = 0;
|
||||||
uint64_t lastUse = 0;
|
uint64_t lastUse = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -598,6 +627,9 @@ private:
|
||||||
// Default white texture
|
// Default white texture
|
||||||
std::unique_ptr<VkTexture> whiteTexture_;
|
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)
|
// Loaded models (modelId -> ModelData)
|
||||||
std::unordered_map<uint32_t, ModelData> loadedModels;
|
std::unordered_map<uint32_t, ModelData> loadedModels;
|
||||||
size_t modelCacheLimit_ = 4000;
|
size_t modelCacheLimit_ = 4000;
|
||||||
|
|
@ -609,6 +641,12 @@ private:
|
||||||
|
|
||||||
bool initialized_ = false;
|
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
|
// Rendering state
|
||||||
bool wireframeMode = false;
|
bool wireframeMode = false;
|
||||||
bool frustumCulling = true;
|
bool frustumCulling = true;
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,9 @@ private:
|
||||||
bool pendingUseOriginalSoundtrack = true;
|
bool pendingUseOriginalSoundtrack = true;
|
||||||
int pendingGroundClutterDensity = 100;
|
int pendingGroundClutterDensity = 100;
|
||||||
int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x
|
int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x
|
||||||
|
bool pendingNormalMapping = true; // on by default
|
||||||
|
bool pendingPOM = false; // off by default (expensive)
|
||||||
|
int pendingPOMQuality = 1; // 0=Low(16), 1=Medium(32), 2=High(64)
|
||||||
|
|
||||||
// UI element transparency (0.0 = fully transparent, 1.0 = fully opaque)
|
// UI element transparency (0.0 = fully transparent, 1.0 = fully opaque)
|
||||||
float uiOpacity_ = 0.65f;
|
float uiOpacity_ = 0.65f;
|
||||||
|
|
@ -112,6 +115,7 @@ private:
|
||||||
bool minimapSettingsApplied_ = false;
|
bool minimapSettingsApplied_ = false;
|
||||||
bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers
|
bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers
|
||||||
bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer
|
bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer
|
||||||
|
bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied
|
||||||
|
|
||||||
// Mute state: mute bypasses master volume without touching slider values
|
// Mute state: mute bypasses master volume without touching slider values
|
||||||
bool soundMuted_ = false;
|
bool soundMuted_ = false;
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,8 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
// --- Create material descriptor set layout (set 1) ---
|
// --- Create material descriptor set layout (set 1) ---
|
||||||
// binding 0: sampler2D (diffuse texture)
|
// binding 0: sampler2D (diffuse texture)
|
||||||
// binding 1: uniform buffer (WMOMaterial)
|
// binding 1: uniform buffer (WMOMaterial)
|
||||||
std::vector<VkDescriptorSetLayoutBinding> materialBindings(2);
|
// binding 2: sampler2D (normal+height map)
|
||||||
|
std::vector<VkDescriptorSetLayoutBinding> materialBindings(3);
|
||||||
materialBindings[0] = {};
|
materialBindings[0] = {};
|
||||||
materialBindings[0].binding = 0;
|
materialBindings[0].binding = 0;
|
||||||
materialBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
materialBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
|
@ -98,6 +99,11 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
materialBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
materialBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||||
materialBindings[1].descriptorCount = 1;
|
materialBindings[1].descriptorCount = 1;
|
||||||
materialBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
materialBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
|
materialBindings[2] = {};
|
||||||
|
materialBindings[2].binding = 2;
|
||||||
|
materialBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
materialBindings[2].descriptorCount = 1;
|
||||||
|
materialBindings[2].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
|
|
||||||
materialSetLayout_ = createDescriptorSetLayout(device, materialBindings);
|
materialSetLayout_ = createDescriptorSetLayout(device, materialBindings);
|
||||||
if (!materialSetLayout_) {
|
if (!materialSetLayout_) {
|
||||||
|
|
@ -107,7 +113,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
|
|
||||||
// --- Create descriptor pool ---
|
// --- Create descriptor pool ---
|
||||||
VkDescriptorPoolSize poolSizes[] = {
|
VkDescriptorPoolSize poolSizes[] = {
|
||||||
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS },
|
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS * 2 }, // diffuse + normal/height
|
||||||
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS },
|
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -147,12 +153,13 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Vertex input ---
|
// --- Vertex input ---
|
||||||
// WMO vertex: pos3 + normal3 + texCoord2 + color4 = 48 bytes
|
// WMO vertex: pos3 + normal3 + texCoord2 + color4 + tangent4 = 64 bytes
|
||||||
struct WMOVertexData {
|
struct WMOVertexData {
|
||||||
glm::vec3 position;
|
glm::vec3 position;
|
||||||
glm::vec3 normal;
|
glm::vec3 normal;
|
||||||
glm::vec2 texCoord;
|
glm::vec2 texCoord;
|
||||||
glm::vec4 color;
|
glm::vec4 color;
|
||||||
|
glm::vec4 tangent; // xyz=tangent dir, w=handedness ±1
|
||||||
};
|
};
|
||||||
|
|
||||||
VkVertexInputBindingDescription vertexBinding{};
|
VkVertexInputBindingDescription vertexBinding{};
|
||||||
|
|
@ -160,7 +167,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
vertexBinding.stride = sizeof(WMOVertexData);
|
vertexBinding.stride = sizeof(WMOVertexData);
|
||||||
vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
|
|
||||||
std::vector<VkVertexInputAttributeDescription> vertexAttribs(4);
|
std::vector<VkVertexInputAttributeDescription> vertexAttribs(5);
|
||||||
vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
||||||
static_cast<uint32_t>(offsetof(WMOVertexData, position)) };
|
static_cast<uint32_t>(offsetof(WMOVertexData, position)) };
|
||||||
vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
||||||
|
|
@ -169,6 +176,8 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
static_cast<uint32_t>(offsetof(WMOVertexData, texCoord)) };
|
static_cast<uint32_t>(offsetof(WMOVertexData, texCoord)) };
|
||||||
vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
||||||
static_cast<uint32_t>(offsetof(WMOVertexData, color)) };
|
static_cast<uint32_t>(offsetof(WMOVertexData, color)) };
|
||||||
|
vertexAttribs[4] = { 4, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
||||||
|
static_cast<uint32_t>(offsetof(WMOVertexData, tangent)) };
|
||||||
|
|
||||||
// --- Build opaque pipeline ---
|
// --- Build opaque pipeline ---
|
||||||
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
||||||
|
|
@ -256,6 +265,14 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
whiteTexture_->upload(*vkCtx_, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
whiteTexture_->upload(*vkCtx_, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||||
whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
||||||
|
|
||||||
|
// --- Create flat normal placeholder texture ---
|
||||||
|
// (128,128,255,128) = flat normal pointing up (0,0,1), mid-height
|
||||||
|
flatNormalTexture_ = std::make_unique<VkTexture>();
|
||||||
|
uint8_t flatNormalPixel[4] = {128, 128, 255, 128};
|
||||||
|
flatNormalTexture_->upload(*vkCtx_, flatNormalPixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||||
|
flatNormalTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
||||||
textureCacheBudgetBytes_ =
|
textureCacheBudgetBytes_ =
|
||||||
envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 1024) * 1024ull * 1024ull;
|
envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 1024) * 1024ull * 1024ull;
|
||||||
modelCacheLimit_ = envSizeMBOrDefault("WOWEE_WMO_MODEL_LIMIT", 4000);
|
modelCacheLimit_ = envSizeMBOrDefault("WOWEE_WMO_MODEL_LIMIT", 4000);
|
||||||
|
|
@ -295,6 +312,7 @@ void WMORenderer::shutdown() {
|
||||||
// Free cached textures
|
// Free cached textures
|
||||||
for (auto& [path, entry] : textureCache) {
|
for (auto& [path, entry] : textureCache) {
|
||||||
if (entry.texture) entry.texture->destroy(device, allocator);
|
if (entry.texture) entry.texture->destroy(device, allocator);
|
||||||
|
if (entry.normalHeightMap) entry.normalHeightMap->destroy(device, allocator);
|
||||||
}
|
}
|
||||||
textureCache.clear();
|
textureCache.clear();
|
||||||
textureCacheBytes_ = 0;
|
textureCacheBytes_ = 0;
|
||||||
|
|
@ -303,8 +321,9 @@ void WMORenderer::shutdown() {
|
||||||
loggedTextureLoadFails_.clear();
|
loggedTextureLoadFails_.clear();
|
||||||
textureBudgetRejectWarnings_ = 0;
|
textureBudgetRejectWarnings_ = 0;
|
||||||
|
|
||||||
// Free white texture
|
// Free white texture and flat normal texture
|
||||||
if (whiteTexture_) { whiteTexture_->destroy(device, allocator); whiteTexture_.reset(); }
|
if (whiteTexture_) { whiteTexture_->destroy(device, allocator); whiteTexture_.reset(); }
|
||||||
|
if (flatNormalTexture_) { flatNormalTexture_->destroy(device, allocator); flatNormalTexture_.reset(); }
|
||||||
|
|
||||||
loadedModels.clear();
|
loadedModels.clear();
|
||||||
instances.clear();
|
instances.clear();
|
||||||
|
|
@ -540,6 +559,16 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
mb.unlit = unlit;
|
mb.unlit = unlit;
|
||||||
mb.isTransparent = (blendMode >= 2);
|
mb.isTransparent = (blendMode >= 2);
|
||||||
mb.isWindow = isWindow;
|
mb.isWindow = isWindow;
|
||||||
|
// Look up normal/height map from texture cache
|
||||||
|
if (hasTexture && tex != whiteTexture_.get()) {
|
||||||
|
for (const auto& [cacheKey, cacheEntry] : textureCache) {
|
||||||
|
if (cacheEntry.texture.get() == tex) {
|
||||||
|
mb.normalHeightMap = cacheEntry.normalHeightMap.get();
|
||||||
|
mb.heightMapVariance = cacheEntry.heightMapVariance;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GroupResources::MergedBatch::DrawRange dr;
|
GroupResources::MergedBatch::DrawRange dr;
|
||||||
dr.firstIndex = batch.startIndex;
|
dr.firstIndex = batch.startIndex;
|
||||||
|
|
@ -570,6 +599,14 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
matData.isInterior = isInterior ? 1 : 0;
|
matData.isInterior = isInterior ? 1 : 0;
|
||||||
matData.specularIntensity = 0.5f;
|
matData.specularIntensity = 0.5f;
|
||||||
matData.isWindow = mb.isWindow ? 1 : 0;
|
matData.isWindow = mb.isWindow ? 1 : 0;
|
||||||
|
matData.enableNormalMap = normalMappingEnabled_ ? 1 : 0;
|
||||||
|
matData.enablePOM = pomEnabled_ ? 1 : 0;
|
||||||
|
matData.pomScale = 0.03f;
|
||||||
|
{
|
||||||
|
static const int pomSampleTable[] = { 16, 32, 64 };
|
||||||
|
matData.pomMaxSamples = pomSampleTable[std::clamp(pomQuality_, 0, 2)];
|
||||||
|
}
|
||||||
|
matData.heightMapVariance = mb.heightMapVariance;
|
||||||
if (matBuf.info.pMappedData) {
|
if (matBuf.info.pMappedData) {
|
||||||
memcpy(matBuf.info.pMappedData, &matData, sizeof(matData));
|
memcpy(matBuf.info.pMappedData, &matData, sizeof(matData));
|
||||||
}
|
}
|
||||||
|
|
@ -585,7 +622,10 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
bufInfo.offset = 0;
|
bufInfo.offset = 0;
|
||||||
bufInfo.range = sizeof(WMOMaterialUBO);
|
bufInfo.range = sizeof(WMOMaterialUBO);
|
||||||
|
|
||||||
VkWriteDescriptorSet writes[2] = {};
|
VkTexture* nhMap = mb.normalHeightMap ? mb.normalHeightMap : flatNormalTexture_.get();
|
||||||
|
VkDescriptorImageInfo nhImgInfo = nhMap->descriptorInfo();
|
||||||
|
|
||||||
|
VkWriteDescriptorSet writes[3] = {};
|
||||||
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
writes[0].dstSet = mb.materialSet;
|
writes[0].dstSet = mb.materialSet;
|
||||||
writes[0].dstBinding = 0;
|
writes[0].dstBinding = 0;
|
||||||
|
|
@ -600,7 +640,14 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
writes[1].descriptorCount = 1;
|
writes[1].descriptorCount = 1;
|
||||||
writes[1].pBufferInfo = &bufInfo;
|
writes[1].pBufferInfo = &bufInfo;
|
||||||
|
|
||||||
vkUpdateDescriptorSets(vkCtx_->getDevice(), 2, writes, 0, nullptr);
|
writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
writes[2].dstSet = mb.materialSet;
|
||||||
|
writes[2].dstBinding = 2;
|
||||||
|
writes[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
writes[2].descriptorCount = 1;
|
||||||
|
writes[2].pImageInfo = &nhImgInfo;
|
||||||
|
|
||||||
|
vkUpdateDescriptorSets(vkCtx_->getDevice(), 3, writes, 0, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
groupRes.mergedBatches.push_back(std::move(mb));
|
groupRes.mergedBatches.push_back(std::move(mb));
|
||||||
|
|
@ -1165,6 +1212,31 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update material UBOs if settings changed
|
||||||
|
if (materialSettingsDirty_) {
|
||||||
|
materialSettingsDirty_ = false;
|
||||||
|
static const int pomSampleTable[] = { 16, 32, 64 };
|
||||||
|
int maxSamples = pomSampleTable[std::clamp(pomQuality_, 0, 2)];
|
||||||
|
for (auto& [modelId, model] : loadedModels) {
|
||||||
|
for (auto& group : model.groups) {
|
||||||
|
for (auto& mb : group.mergedBatches) {
|
||||||
|
if (!mb.materialUBO) continue;
|
||||||
|
// Read existing UBO data, update normal/POM fields
|
||||||
|
VmaAllocationInfo allocInfo{};
|
||||||
|
vmaGetAllocationInfo(vkCtx_->getAllocator(), mb.materialUBOAlloc, &allocInfo);
|
||||||
|
if (allocInfo.pMappedData) {
|
||||||
|
auto* ubo = reinterpret_cast<WMOMaterialUBO*>(allocInfo.pMappedData);
|
||||||
|
ubo->enableNormalMap = normalMappingEnabled_ ? 1 : 0;
|
||||||
|
ubo->enablePOM = pomEnabled_ ? 1 : 0;
|
||||||
|
ubo->pomScale = 0.03f;
|
||||||
|
ubo->pomMaxSamples = maxSamples;
|
||||||
|
ubo->heightMapVariance = mb.heightMapVariance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lastDrawCalls = 0;
|
lastDrawCalls = 0;
|
||||||
|
|
||||||
// Extract frustum planes for proper culling
|
// Extract frustum planes for proper culling
|
||||||
|
|
@ -1491,12 +1563,12 @@ bool WMORenderer::initializeShadow(VkRenderPass shadowRenderPass) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WMO vertex layout: pos(loc0,off0) normal(loc1,off12) texCoord(loc2,off24) color(loc3,off32), stride=48
|
// WMO vertex layout: pos(loc0,off0) normal(loc1,off12) texCoord(loc2,off24) color(loc3,off32) tangent(loc4,off48), stride=64
|
||||||
// Shadow shader locations: 0=aPos, 1=aTexCoord, 2=aBoneWeights, 3=aBoneIndicesF
|
// Shadow shader locations: 0=aPos, 1=aTexCoord, 2=aBoneWeights, 3=aBoneIndicesF
|
||||||
// useBones=0 so locations 2,3 are never read; we alias them to existing data offsets
|
// useBones=0 so locations 2,3 are never read; we alias them to existing data offsets
|
||||||
VkVertexInputBindingDescription vertBind{};
|
VkVertexInputBindingDescription vertBind{};
|
||||||
vertBind.binding = 0;
|
vertBind.binding = 0;
|
||||||
vertBind.stride = 48;
|
vertBind.stride = 64;
|
||||||
vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
std::vector<VkVertexInputAttributeDescription> vertAttrs = {
|
std::vector<VkVertexInputAttributeDescription> vertAttrs = {
|
||||||
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position
|
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position
|
||||||
|
|
@ -1594,12 +1666,13 @@ bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupRes
|
||||||
resources.boundingBoxMin = group.boundingBoxMin;
|
resources.boundingBoxMin = group.boundingBoxMin;
|
||||||
resources.boundingBoxMax = group.boundingBoxMax;
|
resources.boundingBoxMax = group.boundingBoxMax;
|
||||||
|
|
||||||
// Create vertex data (position, normal, texcoord, color)
|
// Create vertex data (position, normal, texcoord, color, tangent)
|
||||||
struct VertexData {
|
struct VertexData {
|
||||||
glm::vec3 position;
|
glm::vec3 position;
|
||||||
glm::vec3 normal;
|
glm::vec3 normal;
|
||||||
glm::vec2 texCoord;
|
glm::vec2 texCoord;
|
||||||
glm::vec4 color;
|
glm::vec4 color;
|
||||||
|
glm::vec4 tangent; // xyz=tangent dir, w=handedness ±1
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<VertexData> vertices;
|
std::vector<VertexData> vertices;
|
||||||
|
|
@ -1611,9 +1684,60 @@ bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupRes
|
||||||
vd.normal = v.normal;
|
vd.normal = v.normal;
|
||||||
vd.texCoord = v.texCoord;
|
vd.texCoord = v.texCoord;
|
||||||
vd.color = v.color;
|
vd.color = v.color;
|
||||||
|
vd.tangent = glm::vec4(0.0f);
|
||||||
vertices.push_back(vd);
|
vertices.push_back(vd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute tangents using Lengyel's method
|
||||||
|
{
|
||||||
|
std::vector<glm::vec3> tan1(vertices.size(), glm::vec3(0.0f));
|
||||||
|
std::vector<glm::vec3> tan2(vertices.size(), glm::vec3(0.0f));
|
||||||
|
|
||||||
|
const auto& indices = group.indices;
|
||||||
|
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
|
||||||
|
uint16_t i0 = indices[i], i1 = indices[i + 1], i2 = indices[i + 2];
|
||||||
|
if (i0 >= vertices.size() || i1 >= vertices.size() || i2 >= vertices.size()) continue;
|
||||||
|
|
||||||
|
const glm::vec3& p0 = vertices[i0].position;
|
||||||
|
const glm::vec3& p1 = vertices[i1].position;
|
||||||
|
const glm::vec3& p2 = vertices[i2].position;
|
||||||
|
const glm::vec2& uv0 = vertices[i0].texCoord;
|
||||||
|
const glm::vec2& uv1 = vertices[i1].texCoord;
|
||||||
|
const glm::vec2& uv2 = vertices[i2].texCoord;
|
||||||
|
|
||||||
|
glm::vec3 dp1 = p1 - p0;
|
||||||
|
glm::vec3 dp2 = p2 - p0;
|
||||||
|
glm::vec2 duv1 = uv1 - uv0;
|
||||||
|
glm::vec2 duv2 = uv2 - uv0;
|
||||||
|
|
||||||
|
float det = duv1.x * duv2.y - duv1.y * duv2.x;
|
||||||
|
if (std::abs(det) < 1e-8f) continue; // degenerate UVs
|
||||||
|
float r = 1.0f / det;
|
||||||
|
|
||||||
|
glm::vec3 sdir = (dp1 * duv2.y - dp2 * duv1.y) * r;
|
||||||
|
glm::vec3 tdir = (dp2 * duv1.x - dp1 * duv2.x) * r;
|
||||||
|
|
||||||
|
tan1[i0] += sdir; tan1[i1] += sdir; tan1[i2] += sdir;
|
||||||
|
tan2[i0] += tdir; tan2[i1] += tdir; tan2[i2] += tdir;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < vertices.size(); i++) {
|
||||||
|
glm::vec3 n = glm::normalize(vertices[i].normal);
|
||||||
|
glm::vec3 t = tan1[i];
|
||||||
|
|
||||||
|
if (glm::dot(t, t) < 1e-8f) {
|
||||||
|
// Fallback: generate tangent perpendicular to normal
|
||||||
|
glm::vec3 up = (std::abs(n.y) < 0.999f) ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0);
|
||||||
|
t = glm::normalize(glm::cross(n, up));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gram-Schmidt orthogonalize
|
||||||
|
t = glm::normalize(t - n * glm::dot(n, t));
|
||||||
|
float w = (glm::dot(glm::cross(n, t), tan2[i]) < 0.0f) ? -1.0f : 1.0f;
|
||||||
|
vertices[i].tangent = glm::vec4(t, w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upload vertex buffer to GPU
|
// Upload vertex buffer to GPU
|
||||||
AllocatedBuffer vertBuf = uploadBuffer(*vkCtx_, vertices.data(),
|
AllocatedBuffer vertBuf = uploadBuffer(*vkCtx_, vertices.data(),
|
||||||
vertices.size() * sizeof(VertexData),
|
vertices.size() * sizeof(VertexData),
|
||||||
|
|
@ -1874,6 +1998,72 @@ void WMORenderer::WMOInstance::updateModelMatrix() {
|
||||||
invModelMatrix = glm::inverse(modelMatrix);
|
invModelMatrix = glm::inverse(modelMatrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<VkTexture> WMORenderer::generateNormalHeightMap(
|
||||||
|
const uint8_t* pixels, uint32_t width, uint32_t height, float& outVariance) {
|
||||||
|
if (!vkCtx_ || width == 0 || height == 0) return nullptr;
|
||||||
|
|
||||||
|
const uint32_t totalPixels = width * height;
|
||||||
|
|
||||||
|
// Step 1: Compute height from luminance
|
||||||
|
std::vector<float> heightMap(totalPixels);
|
||||||
|
double sumH = 0.0, sumH2 = 0.0;
|
||||||
|
for (uint32_t i = 0; i < totalPixels; i++) {
|
||||||
|
float r = pixels[i * 4 + 0] / 255.0f;
|
||||||
|
float g = pixels[i * 4 + 1] / 255.0f;
|
||||||
|
float b = pixels[i * 4 + 2] / 255.0f;
|
||||||
|
float h = 0.299f * r + 0.587f * g + 0.114f * b;
|
||||||
|
heightMap[i] = h;
|
||||||
|
sumH += h;
|
||||||
|
sumH2 += h * h;
|
||||||
|
}
|
||||||
|
double mean = sumH / totalPixels;
|
||||||
|
outVariance = static_cast<float>(sumH2 / totalPixels - mean * mean);
|
||||||
|
|
||||||
|
// Step 2: Sobel 3x3 → normal map (wrap-sampled for tiling textures)
|
||||||
|
const float strength = 2.0f;
|
||||||
|
std::vector<uint8_t> output(totalPixels * 4);
|
||||||
|
|
||||||
|
auto sampleH = [&](int x, int y) -> float {
|
||||||
|
x = ((x % (int)width) + (int)width) % (int)width;
|
||||||
|
y = ((y % (int)height) + (int)height) % (int)height;
|
||||||
|
return heightMap[y * width + x];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint32_t y = 0; y < height; y++) {
|
||||||
|
for (uint32_t x = 0; x < width; x++) {
|
||||||
|
int ix = static_cast<int>(x);
|
||||||
|
int iy = static_cast<int>(y);
|
||||||
|
// Sobel X
|
||||||
|
float gx = -sampleH(ix-1, iy-1) - 2.0f*sampleH(ix-1, iy) - sampleH(ix-1, iy+1)
|
||||||
|
+ sampleH(ix+1, iy-1) + 2.0f*sampleH(ix+1, iy) + sampleH(ix+1, iy+1);
|
||||||
|
// Sobel Y
|
||||||
|
float gy = -sampleH(ix-1, iy-1) - 2.0f*sampleH(ix, iy-1) - sampleH(ix+1, iy-1)
|
||||||
|
+ sampleH(ix-1, iy+1) + 2.0f*sampleH(ix, iy+1) + sampleH(ix+1, iy+1);
|
||||||
|
|
||||||
|
float nx = -gx * strength;
|
||||||
|
float ny = -gy * strength;
|
||||||
|
float nz = 1.0f;
|
||||||
|
float len = std::sqrt(nx*nx + ny*ny + nz*nz);
|
||||||
|
if (len > 0.0f) { nx /= len; ny /= len; nz /= len; }
|
||||||
|
|
||||||
|
uint32_t idx = (y * width + x) * 4;
|
||||||
|
output[idx + 0] = static_cast<uint8_t>(std::clamp((nx * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f));
|
||||||
|
output[idx + 1] = static_cast<uint8_t>(std::clamp((ny * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f));
|
||||||
|
output[idx + 2] = static_cast<uint8_t>(std::clamp((nz * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f));
|
||||||
|
output[idx + 3] = static_cast<uint8_t>(std::clamp(heightMap[y * width + x] * 255.0f, 0.0f, 255.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Upload to GPU with mipmaps
|
||||||
|
auto tex = std::make_unique<VkTexture>();
|
||||||
|
if (!tex->upload(*vkCtx_, output.data(), width, height, VK_FORMAT_R8G8B8A8_UNORM, true)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
tex->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
if (!assetManager || !vkCtx_) {
|
if (!assetManager || !vkCtx_) {
|
||||||
return whiteTexture_.get();
|
return whiteTexture_.get();
|
||||||
|
|
@ -1997,12 +2187,24 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
texture->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
texture->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
||||||
|
|
||||||
|
// Generate normal+height map from diffuse pixels
|
||||||
|
float nhVariance = 0.0f;
|
||||||
|
std::unique_ptr<VkTexture> nhMap;
|
||||||
|
if (normalMappingEnabled_ || pomEnabled_) {
|
||||||
|
nhMap = generateNormalHeightMap(blp.data.data(), blp.width, blp.height, nhVariance);
|
||||||
|
if (nhMap) {
|
||||||
|
approxBytes *= 2; // account for normal map in budget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cache it
|
// Cache it
|
||||||
TextureCacheEntry e;
|
TextureCacheEntry e;
|
||||||
VkTexture* rawPtr = texture.get();
|
VkTexture* rawPtr = texture.get();
|
||||||
e.approxBytes = approxBytes;
|
e.approxBytes = approxBytes;
|
||||||
e.lastUse = ++textureCacheCounter_;
|
e.lastUse = ++textureCacheCounter_;
|
||||||
e.texture = std::move(texture);
|
e.texture = std::move(texture);
|
||||||
|
e.normalHeightMap = std::move(nhMap);
|
||||||
|
e.heightMapVariance = nhVariance;
|
||||||
textureCacheBytes_ += e.approxBytes;
|
textureCacheBytes_ += e.approxBytes;
|
||||||
if (!resolvedKey.empty()) {
|
if (!resolvedKey.empty()) {
|
||||||
textureCache[resolvedKey] = std::move(e);
|
textureCache[resolvedKey] = std::move(e);
|
||||||
|
|
@ -3010,6 +3212,7 @@ void WMORenderer::recreatePipelines() {
|
||||||
glm::vec3 normal;
|
glm::vec3 normal;
|
||||||
glm::vec2 texCoord;
|
glm::vec2 texCoord;
|
||||||
glm::vec4 color;
|
glm::vec4 color;
|
||||||
|
glm::vec4 tangent;
|
||||||
};
|
};
|
||||||
|
|
||||||
VkVertexInputBindingDescription vertexBinding{};
|
VkVertexInputBindingDescription vertexBinding{};
|
||||||
|
|
@ -3017,7 +3220,7 @@ void WMORenderer::recreatePipelines() {
|
||||||
vertexBinding.stride = sizeof(WMOVertexData);
|
vertexBinding.stride = sizeof(WMOVertexData);
|
||||||
vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
|
|
||||||
std::vector<VkVertexInputAttributeDescription> vertexAttribs(4);
|
std::vector<VkVertexInputAttributeDescription> vertexAttribs(5);
|
||||||
vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
||||||
static_cast<uint32_t>(offsetof(WMOVertexData, position)) };
|
static_cast<uint32_t>(offsetof(WMOVertexData, position)) };
|
||||||
vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
||||||
|
|
@ -3026,6 +3229,8 @@ void WMORenderer::recreatePipelines() {
|
||||||
static_cast<uint32_t>(offsetof(WMOVertexData, texCoord)) };
|
static_cast<uint32_t>(offsetof(WMOVertexData, texCoord)) };
|
||||||
vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
||||||
static_cast<uint32_t>(offsetof(WMOVertexData, color)) };
|
static_cast<uint32_t>(offsetof(WMOVertexData, color)) };
|
||||||
|
vertexAttribs[4] = { 4, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
||||||
|
static_cast<uint32_t>(offsetof(WMOVertexData, tangent)) };
|
||||||
|
|
||||||
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include "core/spawn_presets.hpp"
|
#include "core/spawn_presets.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
#include "rendering/renderer.hpp"
|
#include "rendering/renderer.hpp"
|
||||||
|
#include "rendering/wmo_renderer.hpp"
|
||||||
#include "rendering/terrain_manager.hpp"
|
#include "rendering/terrain_manager.hpp"
|
||||||
#include "rendering/minimap.hpp"
|
#include "rendering/minimap.hpp"
|
||||||
#include "rendering/world_map.hpp"
|
#include "rendering/world_map.hpp"
|
||||||
|
|
@ -277,6 +278,19 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
msaaSettingsApplied_ = true;
|
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)
|
// Apply auto-loot setting to GameHandler every frame (cheap bool sync)
|
||||||
gameHandler.setAutoLoot(pendingAutoLoot);
|
gameHandler.setAutoLoot(pendingAutoLoot);
|
||||||
|
|
||||||
|
|
@ -5894,6 +5908,33 @@ void GameScreen::renderSettingsWindow() {
|
||||||
}
|
}
|
||||||
saveSettings();
|
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* resLabel = "Resolution";
|
||||||
const char* resItems[kResCount];
|
const char* resItems[kResCount];
|
||||||
|
|
@ -5917,6 +5958,9 @@ void GameScreen::renderSettingsWindow() {
|
||||||
pendingShadows = kDefaultShadows;
|
pendingShadows = kDefaultShadows;
|
||||||
pendingGroundClutterDensity = kDefaultGroundClutterDensity;
|
pendingGroundClutterDensity = kDefaultGroundClutterDensity;
|
||||||
pendingAntiAliasing = 0;
|
pendingAntiAliasing = 0;
|
||||||
|
pendingNormalMapping = true;
|
||||||
|
pendingPOM = false;
|
||||||
|
pendingPOMQuality = 1;
|
||||||
pendingResIndex = defaultResIndex;
|
pendingResIndex = defaultResIndex;
|
||||||
window->setFullscreen(pendingFullscreen);
|
window->setFullscreen(pendingFullscreen);
|
||||||
window->setVsync(pendingVsync);
|
window->setVsync(pendingVsync);
|
||||||
|
|
@ -5928,6 +5972,13 @@ void GameScreen::renderSettingsWindow() {
|
||||||
tm->setGroundClutterDensityScale(static_cast<float>(pendingGroundClutterDensity) / 100.0f);
|
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();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6854,6 +6905,9 @@ void GameScreen::saveSettings() {
|
||||||
out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n";
|
out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n";
|
||||||
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
|
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
|
||||||
out << "antialiasing=" << pendingAntiAliasing << "\n";
|
out << "antialiasing=" << pendingAntiAliasing << "\n";
|
||||||
|
out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n";
|
||||||
|
out << "pom=" << (pendingPOM ? 1 : 0) << "\n";
|
||||||
|
out << "pom_quality=" << pendingPOMQuality << "\n";
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n";
|
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 == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0);
|
||||||
else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
|
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 == "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
|
// Controls
|
||||||
else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f);
|
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);
|
else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue