From 2e08b19d6bc2efb24f7e167c29dd2995b088fdbc Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Fri, 31 Jul 2026 20:51:48 -0300 Subject: [PATCH] Outline MSAA (#14835) Co-authored-by: Ian Bassi --- resources/shaders/140/gouraud.fs | 97 +++++++++++++++++++++++--------- resources/shaders/140/phong.fs | 79 ++++++++++++++++++++------ src/slic3r/GUI/3DScene.cpp | 78 ++++++++++++++++++++----- 3 files changed, 198 insertions(+), 56 deletions(-) diff --git a/resources/shaders/140/gouraud.fs b/resources/shaders/140/gouraud.fs index 728b7f942c..e099984b46 100644 --- a/resources/shaders/140/gouraud.fs +++ b/resources/shaders/140/gouraud.fs @@ -1,4 +1,7 @@ #version 140 +// Multisample depth texture for the anti-aliased outline (see 3DScene.cpp render_with_outline). +// Optional on the GLSL 140 path: if unavailable, fallback to a non-multisample depth texture. +#extension GL_ARB_texture_multisample : enable const vec3 ZERO = vec3(0.0, 0.0, 0.0); //BBS: add grey and orange @@ -36,7 +39,14 @@ uniform SlopeDetection slope; //BBS: add outline_color uniform bool is_outline; +// The outline is a per-fragment discard mask, which the framebuffer MSAA cannot smooth, so the +// silhouette is resolved per sample from a multisample copy of the outlined model's depth buffer. +#ifdef GL_ARB_texture_multisample +uniform sampler2DMS depth_tex; +uniform int msaa_samples; // samples in depth_tex, 1 when MSAA is off +#else uniform sampler2D depth_tex; +#endif uniform vec2 screen_size; #ifdef ENABLE_ENVIRONMENT_MAP @@ -99,45 +109,87 @@ float GetTolerance(float d, float k) return -k*(d+A)*(d+A)/B; } -float DetectSilho(vec2 fragCoord, vec2 dir) +// Depth of sample s at integer pixel coord. +#ifdef GL_ARB_texture_multisample +float FetchDepth(ivec2 coord, int s) +{ + // texelFetch has no wrap mode, so clamp to the edge texel (sampler2D used CLAMP_TO_EDGE). + ivec2 sz = textureSize(depth_tex); + return abs(texelFetch(depth_tex, clamp(coord, ivec2(0), sz - 1), s).r); +} +#else +float FetchDepth(ivec2 coord, int s) +{ + return abs(texture(depth_tex, (vec2(coord) + 0.5) / screen_size).r); +} +#endif + +float DetectSilho(ivec2 coord, ivec2 dir, int s) { // ------------------------------------------- - // x0 ___ x1----o - // :\ : + // x0 ___ x1----o + // :\ : // r0 : \ : r1 - // : \ : + // : \ : // o---x2 ___ x3 // // r0 and r1 are the differences between actual // and expected (as if x0..3 where on the same // plane) depth values. // ------------------------------------------- - - float x0 = abs(texture(depth_tex, (fragCoord + dir*-2.0) / screen_size).r); - float x1 = abs(texture(depth_tex, (fragCoord + dir*-1.0) / screen_size).r); - float x2 = abs(texture(depth_tex, (fragCoord + dir* 0.0) / screen_size).r); - float x3 = abs(texture(depth_tex, (fragCoord + dir* 1.0) / screen_size).r); - + float x0 = FetchDepth(coord + dir*-2, s); + float x1 = FetchDepth(coord + dir*-1, s); + float x2 = FetchDepth(coord, s); + float x3 = FetchDepth(coord + dir* 1, s); + float d0 = (x1-x0); float d1 = (x2-x3); - + float r0 = x1 + d0 - x2; float r1 = x2 + d1 - x1; - - float tol = GetTolerance(x2, 0.04); - - return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0)); + float tol = GetTolerance(x2, 0.04); + + return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0)); } -float DetectSilho(vec2 fragCoord) +float DetectSilho(ivec2 coord, int s) { return max( - DetectSilho(fragCoord, vec2(1,0)), // Horizontal - DetectSilho(fragCoord, vec2(0,1)) // Vertical + DetectSilho(coord, ivec2(1,0), s), // Horizontal + DetectSilho(coord, ivec2(0,1), s) // Vertical ); } +// Full response of one sample. Reduce the max() per sample and average only afterwards: +// max(mean) <= mean(max), and averaging first hollows out diagonal and curved lines. +float DetectSilhoSample(ivec2 coord, int s) +{ + float v = DetectSilho(coord, s); + // Makes silhouettes thicker. + for (int i = 1; i <= INFLATE; ++i) + { + v = max(v, DetectSilho(coord + ivec2(i, 0), s)); + v = max(v, DetectSilho(coord + ivec2(0, i), s)); + } + return v; +} + +// Average the per-sample coverage into the sub-pixel anti-aliasing of the line. +float DetectSilho(vec2 fragCoord) +{ + ivec2 coord = ivec2(fragCoord); +#ifdef GL_ARB_texture_multisample + int n = max(msaa_samples, 1); +#else + const int n = 1; +#endif + float acc = 0.0; + for (int s = 0; s < n; ++s) + acc += DetectSilhoSample(coord, s); + return acc / float(n); +} + // Returns a lighting multiplier in [1 - shadow_intensity, 1]: < 1 where the fragment is // occluded from the light in the shadow map. 3x3 PCF softens the edges. float shadow_shade() @@ -224,14 +276,7 @@ void main() //BBS: add outline_color if (is_outline) { color = vec4((vec3(intensity.y) + color.rgb * intensity.x) * shade, color.a); - vec2 fragCoord = gl_FragCoord.xy; - float s = DetectSilho(fragCoord); - // Makes silhouettes thicker. - for(int i=1;i<=INFLATE; i++) - { - s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0))); - s = max(s, DetectSilho(fragCoord.xy + vec2(0, i))); - } + float s = DetectSilho(gl_FragCoord.xy); if (s < 0.01) discard; out_color = vec4(mix(color.rgb, getBackfaceColor(color.rgb), s), color.a); diff --git a/resources/shaders/140/phong.fs b/resources/shaders/140/phong.fs index bbde72592c..f847763658 100644 --- a/resources/shaders/140/phong.fs +++ b/resources/shaders/140/phong.fs @@ -1,4 +1,7 @@ #version 140 +// Multisample depth texture for the anti-aliased outline (see 3DScene.cpp render_with_outline). +// Optional on the GLSL 140 path: if unavailable, fallback to a non-multisample depth texture. +#extension GL_ARB_texture_multisample : enable const vec3 ZERO = vec3(0.0, 0.0, 0.0); const vec3 LightRed = vec3(0.78, 0.0, 0.0); @@ -51,7 +54,14 @@ uniform SlopeDetection slope; //BBS: add outline_color uniform bool is_outline; +// The outline is a per-fragment discard mask, which the framebuffer MSAA cannot smooth, so the +// silhouette is resolved per sample from a multisample copy of the outlined model's depth buffer. +#ifdef GL_ARB_texture_multisample +uniform sampler2DMS depth_tex; +uniform int msaa_samples; // samples in depth_tex, 1 when MSAA is off +#else uniform sampler2D depth_tex; +#endif uniform vec2 screen_size; #ifdef ENABLE_ENVIRONMENT_MAP @@ -100,12 +110,27 @@ float GetTolerance(float d, float k) return -k*(d+A)*(d+A)/B; } -float DetectSilho(vec2 fragCoord, vec2 dir) +// Depth of sample s at integer pixel coord. +#ifdef GL_ARB_texture_multisample +float FetchDepth(ivec2 coord, int s) { - float x0 = abs(texture(depth_tex, (fragCoord + dir*-2.0) / screen_size).r); - float x1 = abs(texture(depth_tex, (fragCoord + dir*-1.0) / screen_size).r); - float x2 = abs(texture(depth_tex, (fragCoord + dir* 0.0) / screen_size).r); - float x3 = abs(texture(depth_tex, (fragCoord + dir* 1.0) / screen_size).r); + // texelFetch has no wrap mode, so clamp to the edge texel (sampler2D used CLAMP_TO_EDGE). + ivec2 sz = textureSize(depth_tex); + return abs(texelFetch(depth_tex, clamp(coord, ivec2(0), sz - 1), s).r); +} +#else +float FetchDepth(ivec2 coord, int s) +{ + return abs(texture(depth_tex, (vec2(coord) + 0.5) / screen_size).r); +} +#endif + +float DetectSilho(ivec2 coord, ivec2 dir, int s) +{ + float x0 = FetchDepth(coord + dir*-2, s); + float x1 = FetchDepth(coord + dir*-1, s); + float x2 = FetchDepth(coord, s); + float x3 = FetchDepth(coord + dir* 1, s); float d0 = (x1-x0); float d1 = (x2-x3); @@ -116,17 +141,45 @@ float DetectSilho(vec2 fragCoord, vec2 dir) float tol = GetTolerance(x2, 0.04); return smoothstep(0.0, tol*tol, max( - r0*r1, 0.0)); - } -float DetectSilho(vec2 fragCoord) +float DetectSilho(ivec2 coord, int s) { return max( - DetectSilho(fragCoord, vec2(1,0)), - DetectSilho(fragCoord, vec2(0,1)) + DetectSilho(coord, ivec2(1,0), s), + DetectSilho(coord, ivec2(0,1), s) ); } +// Full response of one sample. Reduce the max() per sample and average only afterwards: +// max(mean) <= mean(max), and averaging first hollows out diagonal and curved lines. +float DetectSilhoSample(ivec2 coord, int s) +{ + float v = DetectSilho(coord, s); + // Makes silhouettes thicker. + for (int i = 1; i <= INFLATE; ++i) + { + v = max(v, DetectSilho(coord + ivec2(i, 0), s)); + v = max(v, DetectSilho(coord + ivec2(0, i), s)); + } + return v; +} + +// Average the per-sample coverage into the sub-pixel anti-aliasing of the line. +float DetectSilho(vec2 fragCoord) +{ + ivec2 coord = ivec2(fragCoord); +#ifdef GL_ARB_texture_multisample + int n = max(msaa_samples, 1); +#else + const int n = 1; +#endif + float acc = 0.0; + for (int s = 0; s < n; ++s) + acc += DetectSilhoSample(coord, s); + return acc / float(n); +} + float compute_ssao_factor(vec3 normal, vec3 view_dir, vec3 eye_pos) { vec3 normal_dx = dFdx(normal); @@ -270,13 +323,7 @@ void main() if (is_outline) { vec3 shaded_rgb = (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade; vec4 shaded_color = vec4(clamp(shaded_rgb, vec3(0.0), vec3(1.0)), color.a); - vec2 fragCoord = gl_FragCoord.xy; - float s = DetectSilho(fragCoord); - for(int i=1;i<=INFLATE; i++) - { - s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0))); - s = max(s, DetectSilho(fragCoord.xy + vec2(0, i))); - } + float s = DetectSilho(gl_FragCoord.xy); if (s < 0.01) discard; out_color = vec4(mix(shaded_color.rgb, getBackfaceColor(shaded_color.rgb), s), shaded_color.a); diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index b52a520200..bf5d1f2421 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -70,6 +70,9 @@ float FullTransparentModdifiedToFixAlpha = 0.3f; // value like 0.18f could not because in C++ (int)(0.18f * 255) == 45 however in OpenGL it renders this as 46 // which breaks the `SelectMachineDialog::record_edge_pixels_data()` function! float FULL_BLACK_THRESHOLD = 0.2f; +// Keep depth_tex away from texture unit 0 to avoid sampler-type aliasing with +// shadow/environment samplers when realistic view is disabled. +static constexpr int OUTLINE_DEPTH_TEX_UNIT = 5; Slic3r::ColorRGBA adjust_color_for_rendering(const Slic3r::ColorRGBA &colors) { @@ -518,6 +521,37 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size) glsafe(::glStencilMask(0xFF)); glsafe(::glDisable(GL_STENCIL_TEST)); // render the outline using depth buffer and discard the pixels that are not on the outline + // The silhouette is resolved per sample in the shader (see DetectSilho in gouraud.fs/phong.fs). + // That needs the GL 3.2 entry points and a shader that declares depth_tex as sampler2DMS, which + // only the 140 ones do and only under GL_ARB_texture_multisample - so ask the compiled program + // rather than the GL version, or a sampler2D ends up bound to a multisample texture. + // Only the Arb branch below allocates a multisample texture, so keep the target consistent with it. + const bool use_msaa_outline = framebuffers_type == GUI::OpenGLManager::EFramebufferType::Arb && + GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 2) && + shader->get_uniform_location("msaa_samples") >= 0; + const GLenum depth_tex_target = use_msaa_outline ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D; + // Keep the depth texture off image unit 0. The object shaders leave shadow_map (and + // environment_tex) at the default sampler value 0 whenever the shadow pass is skipped - which is + // the case with realistic view off - and GL forbids two sampler types referring to the same image + // unit. A sampler2DMS on unit 0 then makes every draw fail with INVALID_OPERATION on drivers that + // enforce it (Mesa), i.e. the model disappears entirely. Unit 5 is unused (shadow_map takes 4). + const int depth_tex_unit = OUTLINE_DEPTH_TEX_UNIT; + int aa_samples = 1; + if (use_msaa_outline) { + if (const AppConfig* app_config = GUI::wxGetApp().app_config; app_config != nullptr) { + const std::string value = app_config->get(SETTING_OPENGL_AA_SAMPLES); + if (value == "2" || value == "4" || value == "8" || value == "16") + aa_samples = ::atoi(value.c_str()); + } + // Never request more samples than the driver supports for depth textures (a 1-sample texture + // is used when MSAA is disabled, keeping a single code path for the sampler2DMS shader). + GLint max_samples = 1; + glsafe(::glGetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &max_samples)); + if (aa_samples > max_samples) + aa_samples = max_samples < 1 ? 1 : max_samples; + if (aa_samples < 1) + aa_samples = 1; + } // 1st. render pass, render the model into a separate render target that has only depth buffer GLuint depth_fbo = 0; GLuint depth_tex = 0; @@ -525,21 +559,26 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size) glsafe(::glGenFramebuffers(1, &depth_fbo)); glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, depth_fbo)); - glActiveTexture(GL_TEXTURE0); + glsafe(::glActiveTexture(GL_TEXTURE0 + depth_tex_unit)); glsafe(::glGenTextures(1, &depth_tex)); - glsafe(::glBindTexture(GL_TEXTURE_2D, depth_tex)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, cnv_size.get_width(), cnv_size.get_height(), 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr)); + glsafe(::glBindTexture(depth_tex_target, depth_tex)); + if (use_msaa_outline) { + // Multisample textures do not take filter/wrap parameters. + glsafe(::glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, aa_samples, GL_DEPTH_COMPONENT32F, cnv_size.get_width(), cnv_size.get_height(), GL_TRUE)); + } else { + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, cnv_size.get_width(), cnv_size.get_height(), 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr)); + } - glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_tex, 0)); + glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth_tex_target, depth_tex, 0)); } else { glsafe(::glGenFramebuffersEXT(1, &depth_fbo)); glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, depth_fbo)); - glActiveTexture(GL_TEXTURE0); + glsafe(::glActiveTexture(GL_TEXTURE0 + depth_tex_unit)); glsafe(::glGenTextures(1, &depth_tex)); glsafe(::glBindTexture(GL_TEXTURE_2D, depth_tex)); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); @@ -550,12 +589,15 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size) glsafe(::glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, depth_tex, 0)); } + // Unbind before drawing: the texture is this framebuffer's depth attachment, so leaving it bound + // to a sampled unit would be a feedback loop. + glsafe(::glBindTexture(depth_tex_target, 0)); + glsafe(::glActiveTexture(GL_TEXTURE0)); glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); if (tverts_range == std::make_pair(0, -1)) model.render(shader); else model.render(this->tverts_range, shader); - glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); // 2nd. render pass, just a normal render with the depth buffer passed as a texture if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Arb) { @@ -565,13 +607,17 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size) } shader->set_uniform("is_outline", true); shader->set_uniform("screen_size", Vec2f{cnv_size.get_width(), cnv_size.get_height()}); - glActiveTexture(GL_TEXTURE0); - glsafe(::glBindTexture(GL_TEXTURE_2D, depth_tex)); - shader->set_uniform("depth_tex", 0); + shader->set_uniform("msaa_samples", aa_samples); + glsafe(::glActiveTexture(GL_TEXTURE0 + depth_tex_unit)); + glsafe(::glBindTexture(depth_tex_target, depth_tex)); + glsafe(::glActiveTexture(GL_TEXTURE0)); + shader->set_uniform("depth_tex", depth_tex_unit); simple_render(shader, model_objects, colors); // Some clean up to do - glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + glsafe(::glActiveTexture(GL_TEXTURE0 + depth_tex_unit)); + glsafe(::glBindTexture(depth_tex_target, 0)); + glsafe(::glActiveTexture(GL_TEXTURE0)); shader->set_uniform("is_outline", false); if (framebuffers_type == GUI::OpenGLManager::EFramebufferType::Arb) { glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0)); @@ -1075,6 +1121,10 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, const float support_normal_z = get_selection_support_normal_z(); + // Prime depth_tex on every frame so non-outline draws do not keep the + // default sampler unit 0, which can conflict with other sampler types. + shader->set_uniform("depth_tex", OUTLINE_DEPTH_TEX_UNIT); + for (GLVolumeWithIdAndZ& volume : to_render) { #if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT if (type == ERenderType::Transparent) {