From aeccddddeb420487b98c9a6bcf039e7ba22c3b50 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 4 Feb 2026 15:05:46 -0800 Subject: [PATCH] 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. --- include/rendering/character_renderer.hpp | 19 ++++++ include/rendering/m2_renderer.hpp | 19 ++++++ include/rendering/texture.hpp | 7 +++ include/rendering/wmo_renderer.hpp | 24 ++++++++ src/rendering/character_renderer.cpp | 60 ++++++++++++++++++- src/rendering/m2_renderer.cpp | 56 +++++++++++++++++- src/rendering/renderer.cpp | 9 ++- src/rendering/terrain_renderer.cpp | 2 + src/rendering/texture.cpp | 18 ++++++ src/rendering/wmo_renderer.cpp | 74 +++++++++++++++++++++++- 10 files changed, 280 insertions(+), 8 deletions(-) diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index e56a6a2c..1b2cdbc0 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -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 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 textureCache; GLuint whiteTexture = 0; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 8c7f6550..62db8321 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -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; @@ -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); diff --git a/include/rendering/texture.hpp b/include/rendering/texture.hpp index 9ea98d43..5baf32a4 100644 --- a/include/rendering/texture.hpp +++ b/include/rendering/texture.hpp @@ -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 diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 800fde10..7e2fe66d 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -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); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 5174082f..8f31ec21 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -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& 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; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index eba6940b..94623533 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -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); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index fd05681c..ab65391d 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -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); diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index c6e656f6..c79e04ab 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -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); diff --git a/src/rendering/texture.cpp b/src/rendering/texture.cpp index 5dcc2e15..769ba36e 100644 --- a/src/rendering/texture.cpp +++ b/src/rendering/texture.cpp @@ -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 diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 1dd02981..284673e5 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -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);