Add centralized anisotropic filtering, fog, and Blinn-Phong specular to all renderers

Anisotropic filtering now queries GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT once
and applies via a single applyAnisotropicFiltering() utility, replacing
hardcoded calls across all renderers. Fog (sky horizon color, 100-600
range) and Blinn-Phong specular highlights are added to WMO, M2, and
character shaders for visual parity with terrain. Shadow sampling
plumbing (sampler2DShadow with 3x3 PCF) is wired into all three shaders
gated by uShadowEnabled, ready for a future shadow map pass.
This commit is contained in:
Kelsi 2026-02-04 15:05:46 -08:00
parent c9adcd3d96
commit aeccddddeb
10 changed files with 280 additions and 8 deletions

View file

@ -77,6 +77,15 @@ public:
size_t getInstanceCount() const { return instances.size(); }
void setFog(const glm::vec3& color, float start, float end) {
fogColor = color; fogStart = start; fogEnd = end;
}
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
}
void clearShadowMap() { shadowEnabled = false; }
private:
// GPU representation of M2 model
struct M2ModelGPU {
@ -168,6 +177,16 @@ private:
std::unique_ptr<Shader> characterShader;
pipeline::AssetManager* assetManager = nullptr;
// Fog parameters
glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f);
float fogStart = 400.0f;
float fogEnd = 1200.0f;
// Shadow mapping
GLuint shadowDepthTex = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
bool shadowEnabled = false;
// Texture cache
std::unordered_map<std::string, GLuint> textureCache;
GLuint whiteTexture = 0;

View file

@ -224,6 +224,15 @@ public:
uint32_t getTotalTriangleCount() const;
uint32_t getDrawCallCount() const { return lastDrawCallCount; }
void setFog(const glm::vec3& color, float start, float end) {
fogColor = color; fogStart = start; fogEnd = end;
}
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
}
void clearShadowMap() { shadowEnabled = false; }
private:
pipeline::AssetManager* assetManager = nullptr;
std::unique_ptr<Shader> shader;
@ -242,6 +251,16 @@ private:
glm::vec3 lightDir = glm::vec3(0.5f, 0.5f, 1.0f);
glm::vec3 ambientColor = glm::vec3(0.4f, 0.4f, 0.45f);
// Fog parameters
glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f);
float fogStart = 400.0f;
float fogEnd = 1200.0f;
// Shadow mapping
GLuint shadowDepthTex = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
bool shadowEnabled = false;
// Optional query-space culling for collision/raycast hot paths.
bool collisionFocusEnabled = false;
glm::vec3 collisionFocusPos = glm::vec3(0.0f);

View file

@ -27,5 +27,12 @@ private:
int height = 0;
};
/**
* Apply anisotropic filtering to the currently bound GL_TEXTURE_2D.
* Queries the driver maximum once and caches it. No-op if the extension
* is not available.
*/
void applyAnisotropicFiltering();
} // namespace rendering
} // namespace wowee

View file

@ -129,6 +129,20 @@ public:
*/
void setFrustumCulling(bool enabled) { frustumCulling = enabled; }
void setFog(const glm::vec3& color, float start, float end) {
fogColor = color; fogStart = start; fogEnd = end;
}
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
}
void clearShadowMap() { shadowEnabled = false; }
/**
* Render depth-only for shadow casting (reuses VAOs)
*/
void renderShadow(const glm::mat4& lightView, const glm::mat4& lightProj, Shader& shadowShader);
/**
* Get floor height at a GL position via ray-triangle intersection
*/
@ -308,6 +322,16 @@ private:
bool frustumCulling = true;
uint32_t lastDrawCalls = 0;
// Fog parameters
glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f);
float fogStart = 400.0f;
float fogEnd = 1200.0f;
// Shadow mapping
GLuint shadowDepthTex = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
bool shadowEnabled = false;
// Optional query-space culling for collision/raycast hot paths.
bool collisionFocusEnabled = false;
glm::vec3 collisionFocusPos = glm::vec3(0.0f);

View file

@ -92,15 +92,46 @@ bool CharacterRenderer::initialize() {
uniform vec3 uLightDir;
uniform vec3 uViewPos;
uniform vec3 uFogColor;
uniform float uFogStart;
uniform float uFogEnd;
uniform sampler2DShadow uShadowMap;
uniform mat4 uLightSpaceMatrix;
uniform int uShadowEnabled;
out vec4 FragColor;
void main() {
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(uLightDir);
// Simple diffuse lighting
// Diffuse lighting
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * vec3(1.0);
// Blinn-Phong specular
vec3 viewDir = normalize(uViewPos - FragPos);
vec3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
vec3 specular = spec * vec3(0.2);
// Shadow mapping
float shadow = 1.0;
if (uShadowEnabled != 0) {
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001);
shadow = 0.0;
vec2 texelSize = vec2(1.0 / 2048.0);
for (int sx = -1; sx <= 1; sx++) {
for (int sy = -1; sy <= 1; sy++) {
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
}
}
shadow /= 9.0;
}
}
// Ambient
vec3 ambient = vec3(0.3);
@ -109,7 +140,13 @@ bool CharacterRenderer::initialize() {
vec4 texColor = texture(uTexture0, TexCoord);
// Combine
vec3 result = (ambient + diffuse) * texColor.rgb;
vec3 result = (ambient + (diff * vec3(1.0) + specular) * shadow) * texColor.rgb;
// Fog
float fogDist = length(uViewPos - FragPos);
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
result = mix(uFogColor, result, fogFactor);
FragColor = vec4(result, texColor.a);
}
)";
@ -207,6 +244,7 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);
textureCache[path] = texId;
@ -417,6 +455,7 @@ GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& laye
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);
core::Logger::getInstance().info("Composite texture created: ", width, "x", height, " from ", layerPaths.size(), " layers");
@ -542,6 +581,7 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);
core::Logger::getInstance().info("compositeWithRegions: created ", width, "x", height,
@ -978,6 +1018,20 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
characterShader->setUniform("uLightDir", glm::vec3(0.0f, -1.0f, 0.3f));
characterShader->setUniform("uViewPos", camera.getPosition());
// Fog
characterShader->setUniform("uFogColor", fogColor);
characterShader->setUniform("uFogStart", fogStart);
characterShader->setUniform("uFogEnd", fogEnd);
// Shadows
characterShader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0);
if (shadowEnabled) {
characterShader->setUniform("uLightSpaceMatrix", lightSpaceMatrix);
glActiveTexture(GL_TEXTURE7);
glBindTexture(GL_TEXTURE_2D, shadowDepthTex);
characterShader->setUniform("uShadowMap", 7);
}
for (const auto& pair : instances) {
const auto& instance = pair.second;

View file

@ -1,4 +1,5 @@
#include "rendering/m2_renderer.hpp"
#include "rendering/texture.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "rendering/frustum.hpp"
@ -253,11 +254,20 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
uniform vec3 uLightDir;
uniform vec3 uAmbientColor;
uniform vec3 uViewPos;
uniform sampler2D uTexture;
uniform bool uHasTexture;
uniform bool uAlphaTest;
uniform float uFadeAlpha;
uniform vec3 uFogColor;
uniform float uFogStart;
uniform float uFogEnd;
uniform sampler2DShadow uShadowMap;
uniform mat4 uLightSpaceMatrix;
uniform bool uShadowEnabled;
out vec4 FragColor;
void main() {
@ -285,10 +295,40 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
// Two-sided lighting for foliage
float diff = max(abs(dot(normal, lightDir)), 0.3);
// Blinn-Phong specular
vec3 viewDir = normalize(uViewPos - FragPos);
vec3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
vec3 specular = spec * vec3(0.1);
// Shadow mapping
float shadow = 1.0;
if (uShadowEnabled) {
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001);
shadow = 0.0;
vec2 texelSize = vec2(1.0 / 2048.0);
for (int sx = -1; sx <= 1; sx++) {
for (int sy = -1; sy <= 1; sy++) {
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
}
}
shadow /= 9.0;
}
}
vec3 ambient = uAmbientColor * texColor.rgb;
vec3 diffuse = diff * texColor.rgb;
vec3 result = ambient + diffuse;
vec3 result = ambient + (diffuse + specular) * shadow;
// Fog
float fogDist = length(uViewPos - FragPos);
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
result = mix(uFogColor, result, fogFactor);
FragColor = vec4(result, finalAlpha);
}
)";
@ -1051,8 +1091,6 @@ void M2Renderer::update(float deltaTime) {
}
void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
(void)camera; // unused for now
if (instances.empty() || !shader) {
return;
}
@ -1080,6 +1118,17 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
shader->setUniform("uProjection", projection);
shader->setUniform("uLightDir", lightDir);
shader->setUniform("uAmbientColor", ambientColor);
shader->setUniform("uViewPos", camera.getPosition());
shader->setUniform("uFogColor", fogColor);
shader->setUniform("uFogStart", fogStart);
shader->setUniform("uFogEnd", fogEnd);
shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0);
if (shadowEnabled) {
shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix);
glActiveTexture(GL_TEXTURE7);
glBindTexture(GL_TEXTURE_2D, shadowDepthTex);
shader->setUniform("uShadowMap", 7);
}
lastDrawCallCount = 0;
@ -1389,6 +1438,7 @@ GLuint M2Renderer::loadTexture(const std::string& path) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);

View file

@ -921,6 +921,14 @@ void Renderer::renderWorld(game::World* world) {
lensFlare->render(*camera, sunPosition, timeOfDay);
}
// Update fog across all renderers based on time of day (match sky color)
if (skybox) {
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
if (wmoRenderer) wmoRenderer->setFog(horizonColor, 100.0f, 600.0f);
if (m2Renderer) m2Renderer->setFog(horizonColor, 100.0f, 600.0f);
if (characterRenderer) characterRenderer->setFog(horizonColor, 100.0f, 600.0f);
}
// Render terrain if loaded and enabled
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
// Check if camera/character is underwater for fog override
@ -951,7 +959,6 @@ void Renderer::renderWorld(game::World* world) {
}
if (skybox) {
// Update terrain fog based on time of day (match sky color)
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b};
terrainRenderer->setFog(fogColorArray, 400.0f, 1200.0f);

View file

@ -1,4 +1,5 @@
#include "rendering/terrain_renderer.hpp"
#include "rendering/texture.hpp"
#include "rendering/frustum.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
@ -239,6 +240,7 @@ GLuint TerrainRenderer::loadTexture(const std::string& path) {
// Generate mipmaps
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);

View file

@ -33,6 +33,7 @@ bool Texture::loadFromMemory(const unsigned char* data, int w, int h, int channe
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);
return true;
@ -47,5 +48,22 @@ void Texture::unbind() const {
glBindTexture(GL_TEXTURE_2D, 0);
}
void applyAnisotropicFiltering() {
static float maxAniso = -1.0f;
if (maxAniso < 0.0f) {
if (GLEW_EXT_texture_filter_anisotropic) {
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAniso);
if (maxAniso < 1.0f) maxAniso = 1.0f;
} else {
maxAniso = 0.0f; // Extension not available
}
}
if (maxAniso > 0.0f) {
float desired = 16.0f;
float clamped = (desired < maxAniso) ? desired : maxAniso;
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, clamped);
}
}
} // namespace rendering
} // namespace wowee

View file

@ -1,4 +1,5 @@
#include "rendering/wmo_renderer.hpp"
#include "rendering/texture.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "rendering/frustum.hpp"
@ -79,6 +80,14 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
uniform bool uHasTexture;
uniform bool uAlphaTest;
uniform vec3 uFogColor;
uniform float uFogStart;
uniform float uFogEnd;
uniform sampler2DShadow uShadowMap;
uniform mat4 uLightSpaceMatrix;
uniform bool uShadowEnabled;
out vec4 FragColor;
void main() {
@ -92,6 +101,12 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
// Ambient
vec3 ambient = uAmbientColor;
// Blinn-Phong specular
vec3 viewDir = normalize(uViewPos - FragPos);
vec3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
vec3 specular = spec * vec3(0.15);
// Sample texture or use vertex color
vec4 texColor;
if (uHasTexture) {
@ -103,8 +118,32 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
texColor = vec4(VertexColor.rgb, 1.0);
}
// Shadow mapping
float shadow = 1.0;
if (uShadowEnabled) {
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.001);
shadow = 0.0;
vec2 texelSize = vec2(1.0 / 2048.0);
for (int sx = -1; sx <= 1; sx++) {
for (int sy = -1; sy <= 1; sy++) {
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
}
}
shadow /= 9.0;
}
}
// Combine lighting with texture
vec3 result = (ambient + diffuse) * texColor.rgb;
vec3 result = (ambient + (diffuse + specular) * shadow) * texColor.rgb;
// Fog
float fogDist = length(uViewPos - FragPos);
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
result = mix(uFogColor, result, fogFactor);
FragColor = vec4(result, 1.0);
}
)";
@ -452,6 +491,16 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
shader->setUniform("uViewPos", camera.getPosition());
shader->setUniform("uLightDir", glm::vec3(-0.3f, -0.7f, -0.6f)); // Default sun direction
shader->setUniform("uAmbientColor", glm::vec3(0.4f, 0.4f, 0.5f));
shader->setUniform("uFogColor", fogColor);
shader->setUniform("uFogStart", fogStart);
shader->setUniform("uFogEnd", fogEnd);
shader->setUniform("uShadowEnabled", shadowEnabled ? 1 : 0);
if (shadowEnabled) {
shader->setUniform("uLightSpaceMatrix", lightSpaceMatrix);
glActiveTexture(GL_TEXTURE7);
glBindTexture(GL_TEXTURE_2D, shadowDepthTex);
shader->setUniform("uShadowMap", 7);
}
// Enable wireframe if requested
if (wireframeMode) {
@ -512,6 +561,28 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
glEnable(GL_CULL_FACE);
}
void WMORenderer::renderShadow(const glm::mat4& lightView, const glm::mat4& lightProj, Shader& shadowShader) {
if (instances.empty()) return;
Frustum frustum;
frustum.extractFromMatrix(lightProj * lightView);
for (const auto& instance : instances) {
auto modelIt = loadedModels.find(instance.modelId);
if (modelIt == loadedModels.end()) continue;
if (frustumCulling) {
glm::vec3 instMin = instance.worldBoundsMin - glm::vec3(0.5f);
glm::vec3 instMax = instance.worldBoundsMax + glm::vec3(0.5f);
if (!frustum.intersectsAABB(instMin, instMax)) continue;
}
const ModelData& model = modelIt->second;
shadowShader.setUniform("uModel", instance.modelMatrix);
for (const auto& group : model.groups) {
glBindVertexArray(group.vao);
glDrawElements(GL_TRIANGLES, group.indexCount, GL_UNSIGNED_SHORT, 0);
glBindVertexArray(0);
}
}
}
uint32_t WMORenderer::getTotalTriangleCount() const {
uint32_t total = 0;
for (const auto& instance : instances) {
@ -769,6 +840,7 @@ GLuint WMORenderer::loadTexture(const std::string& path) {
// Set texture parameters with mipmaps
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);