From 3c0bfb06a8acf59222a07fe894c553ec7a78db29 Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Tue, 23 Jun 2026 15:09:30 +0100 Subject: [PATCH] fix(shader): reduce specular aliasing on envAtlas reflections The envAtlas reflection chunk only applied screen-space derivative antialiasing (shinyMipLevel) for near-mirror surfaces (ilevel == 0). All rougher materials selected the reflection mip from material gloss alone, ignoring screen-space minification, so curved/minified glossy surfaces aliased badly. Fold the screen-space minification level into the roughness level via max(level, shinyMipLevel(...)) before flooring, so reflections blur to track minification at all gloss levels. Applied to both the GLSL and WGSL chunks for backend parity. Fixes #8431 Co-Authored-By: Claude Opus 4.8 --- .../glsl/chunks/lit/frag/reflectionEnv.js | 30 +++++++++--------- .../wgsl/chunks/lit/frag/reflectionEnv.js | 31 +++++++++---------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/scene/shader-lib/glsl/chunks/lit/frag/reflectionEnv.js b/src/scene/shader-lib/glsl/chunks/lit/frag/reflectionEnv.js index aa10ba24df8..48d7fc95226 100644 --- a/src/scene/shader-lib/glsl/chunks/lit/frag/reflectionEnv.js +++ b/src/scene/shader-lib/glsl/chunks/lit/frag/reflectionEnv.js @@ -25,32 +25,32 @@ vec3 calcReflection(vec3 reflDir, float gloss) { vec3 dir = cubeMapProject(reflDir) * vec3(-1.0, 1.0, 1.0); vec2 uv = toSphericalUv(dir); - // calculate roughness level + // roughness mip level based on material gloss float level = saturate(1.0 - gloss) * 5.0; - float ilevel = floor(level); - // accessing the shiny (top level) reflection - perform manual mipmap lookup + // screen-space minification level - drives specular anti-aliasing float level2 = shinyMipLevel(uv * atlasSize); - float ilevel2 = floor(level2); + + // the reflection must be at least as blurry as screen-space minification requires, + // otherwise high-curvature / minified surfaces alias badly + level = max(level, level2); + + float ilevel = floor(level); vec2 uv0, uv1; - float weight; if (ilevel == 0.0) { - uv0 = mapShinyUv(uv, ilevel2); - uv1 = mapShinyUv(uv, ilevel2 + 1.0); - weight = level2 - ilevel2; + // sharp reflection: blend the shiny (top mip) level into roughness level 1 + uv0 = mapShinyUv(uv, 0.0); + uv1 = mapRoughnessUv(uv, 1.0); } else { - // accessing rough reflection - just sample the same part twice - uv0 = uv1 = mapRoughnessUv(uv, ilevel); - weight = 0.0; + // blurry reflection: blend two pre-convolved roughness levels + uv0 = mapRoughnessUv(uv, ilevel); + uv1 = mapRoughnessUv(uv, ilevel + 1.0); } vec3 linearA = {reflectionDecode}(texture2D(texture_envAtlas, uv0)); vec3 linearB = {reflectionDecode}(texture2D(texture_envAtlas, uv1)); - vec3 linear0 = mix(linearA, linearB, weight); - vec3 linear1 = {reflectionDecode}(texture2D(texture_envAtlas, mapRoughnessUv(uv, ilevel + 1.0))); - - return processEnvironment(mix(linear0, linear1, level - ilevel)); + return processEnvironment(mix(linearA, linearB, level - ilevel)); } void addReflection(vec3 reflDir, float gloss) { diff --git a/src/scene/shader-lib/wgsl/chunks/lit/frag/reflectionEnv.js b/src/scene/shader-lib/wgsl/chunks/lit/frag/reflectionEnv.js index a9d3bdc4d46..58ad453011e 100644 --- a/src/scene/shader-lib/wgsl/chunks/lit/frag/reflectionEnv.js +++ b/src/scene/shader-lib/wgsl/chunks/lit/frag/reflectionEnv.js @@ -26,34 +26,33 @@ fn calcReflection(reflDir: vec3f, gloss: f32) -> vec3f { let dir: vec3f = cubeMapProject(reflDir) * vec3f(-1.0, 1.0, 1.0); let uv: vec2f = toSphericalUv(dir); - // calculate roughness level - let level: f32 = saturate(1.0 - gloss) * 5.0; - let ilevel: f32 = floor(level); + // roughness mip level based on material gloss + var level: f32 = saturate(1.0 - gloss) * 5.0; - // accessing the shiny (top level) reflection - perform manual mipmap lookup + // screen-space minification level - drives specular anti-aliasing let level2: f32 = shinyMipLevel(uv * atlasSize); - let ilevel2: f32 = floor(level2); + + // the reflection must be at least as blurry as screen-space minification requires, + // otherwise high-curvature / minified surfaces alias badly + level = max(level, level2); + + let ilevel: f32 = floor(level); var uv0: vec2f; var uv1: vec2f; - var weight: f32; if (ilevel == 0.0) { - uv0 = mapShinyUv(uv, ilevel2); - uv1 = mapShinyUv(uv, ilevel2 + 1.0); - weight = level2 - ilevel2; + // sharp reflection: blend the shiny (top mip) level into roughness level 1 + uv0 = mapShinyUv(uv, 0.0); + uv1 = mapRoughnessUv(uv, 1.0); } else { - // accessing rough reflection - just sample the same part twice + // blurry reflection: blend two pre-convolved roughness levels uv0 = mapRoughnessUv(uv, ilevel); - uv1 = uv0; - weight = 0.0; + uv1 = mapRoughnessUv(uv, ilevel + 1.0); } let linearA: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, uv0)); let linearB: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, uv1)); - let linear0: vec3f = mix(linearA, linearB, weight); - let linear1: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, mapRoughnessUv(uv, ilevel + 1.0))); - - return processEnvironment(mix(linear0, linear1, level - ilevel)); + return processEnvironment(mix(linearA, linearB, level - ilevel)); } fn addReflection(reflDir: vec3f, gloss: f32) {