diff --git a/resources/shaders/110/ssao.fs b/resources/shaders/110/ssao.fs index 5aada05883..db266daba1 100644 --- a/resources/shaders/110/ssao.fs +++ b/resources/shaders/110/ssao.fs @@ -7,18 +7,38 @@ uniform sampler2D color_texture; uniform sampler2D depth_texture; -uniform sampler2D normal_texture; uniform vec2 inv_tex_size; -uniform float z_near; uniform float z_far; uniform bool is_outline; +// The pass has no normal target to read, so the surface normal is reconstructed from the depth +// buffer. inv_projection_matrix unprojects a pixel back into view space and up_view is world +Z +// expressed in view space, which is what tells a top surface from a wall. +uniform mat4 inv_projection_matrix; +uniform vec3 up_view; varying vec2 tex_coord; -float linearize_depth(float depth) +// Position of the given pixel in view space. Valid under both an orthographic and a perspective +// camera, unlike the depth linearization it replaces. +vec3 view_pos(vec2 uv) { - float z = depth * 2.0 - 1.0; - return (2.0 * z_near * z_far) / (z_far + z_near - z * (z_far - z_near)); + vec2 c = clamp(uv, vec2(0.0), vec2(1.0)); + float d = texture2D(depth_texture, c).r; + vec4 ndc = vec4(c * 2.0 - 1.0, d * 2.0 - 1.0, 1.0); + vec4 view = inv_projection_matrix * ndc; + return view.xyz / view.w; +} + +// Surface normal at the given pixel, from the forward differences of the reconstructed view +// position. It rings by a pixel across a depth discontinuity, which is acceptable here: the +// normal only weights the occlusion, nothing is shaded with it. +vec3 view_normal(vec2 uv, vec3 p) +{ + vec3 px = view_pos(uv + vec2(inv_tex_size.x, 0.0)); + vec3 py = view_pos(uv + vec2(0.0, inv_tex_size.y)); + vec3 n = cross(px - p, py - p); + float len = length(n); + return (len > 1e-8) ? n / len : vec3(0.0, 0.0, 1.0); } void main() @@ -28,16 +48,21 @@ void main() return; } vec3 base = texture2D(color_texture, tex_coord).rgb; - float depth_center = linearize_depth(texture2D(depth_texture, tex_coord).r); - // Sample normal at current fragment (range: -1 to 1) - vec3 normal_center = texture2D(normal_texture, tex_coord).rgb * 2.0 - 1.0; + // Nothing was drawn here: occluding the background would only darken the gradient, and its + // reconstructed normal is degenerate anyway. + if (texture2D(depth_texture, tex_coord).r >= 0.9999) { + gl_FragColor = vec4(base, 1.0); + return; + } + + vec3 center_pos = view_pos(tex_coord); + float depth_center = -center_pos.z; + vec3 normal_center = view_normal(tex_coord, center_pos); // Calculate how much the surface faces upward - // up_factor = 1.0 for surfaces pointing straight up (0,0,1) - // up_factor = 0.0 for surfaces pointing down or sideways - float up_factor = max(0.0, normal_center.z); // Assuming Z is up axis - // Alternative: if Y is up, use normal_center.y + // up_factor = 1.0 for surfaces pointing straight up, 0.0 for walls and downward faces + float up_factor = clamp(dot(normal_center, up_view), 0.0, 1.0); // Adaptive sampling radius float radius = mix(2.0, 4.0, depth_center / z_far); @@ -52,39 +77,38 @@ void main() offsets[6] = vec2( 0.0, -1.0); offsets[7] = vec2( 0.707,-0.707); + // Occlusion is a slope, not a depth difference: how far a neighbour rises out of the + // centre's tangent plane over how far away it is. Unlike a raw difference, that sine is + // free of camera distance and zoom, so a crease reads the same from any view. + const float SLOPE_MIN = 0.08; // ~5 degrees, above the depth-buffer noise of a flat surface + const float SLOPE_MAX = 0.60; // ~37 degrees, a full crease + + const float SAMPLE_COUNT = 8.0; float occlusion = 0.0; - int valid_samples = 0; for (int i = 0; i < 8; ++i) { vec2 uv = tex_coord + offsets[i] * inv_tex_size * radius; - uv = clamp(uv, vec2(0.001), vec2(0.999)); - float sample_depth = linearize_depth(texture2D(depth_texture, uv).r); - float depth_diff = max(0.0, depth_center - sample_depth); - - float threshold = 0.015 * (0.5 + depth_center / z_far); - float contribution = smoothstep(0.001, threshold, depth_diff); + vec3 delta = view_pos(uv) - center_pos; + float dist = length(delta); + float rise = (dist > 1e-6) ? dot(delta, normal_center) / dist : 0.0; + float contribution = smoothstep(SLOPE_MIN, SLOPE_MAX, rise); float diagonal_weight = 1.0 - abs(offsets[i].x * offsets[i].y) * 0.5; occlusion += contribution * diagonal_weight; - valid_samples++; } - if (valid_samples > 0) - occlusion /= float(valid_samples); + occlusion /= SAMPLE_COUNT; // flatter/top-like surfaces get less darkening float ao_intensity = 0.55; float ambient_occlusion = 1.0 - occlusion * ao_intensity; - // Different min values for top vs bottom surfaces + // Different min values for top vs bottom surfaces. The boost that used to follow lifted a + // top surface back to within 2% of unoccluded once up_factor became a real normal rather + // than a colour, which is where the AO went; the floors alone shape the effect now. float ao_min = mix(0.45, 0.70, up_factor); // Bottom: 0.45, Top: 0.70 ambient_occlusion = clamp(ambient_occlusion, ao_min, 1.0); - // Boost brightness on top surfaces (optional) - float brightness_boost = 1.0 + up_factor * 0.15; // 15% extra brightness on top - ambient_occlusion = pow(ambient_occlusion, 2.2) * brightness_boost; - ambient_occlusion = clamp(ambient_occlusion, 0.45, 1.05); - gl_FragColor = vec4(base * ambient_occlusion, 1.0); -} \ No newline at end of file +} diff --git a/resources/shaders/140/ssao.fs b/resources/shaders/140/ssao.fs index b3b6df6877..28b561ba2c 100644 --- a/resources/shaders/140/ssao.fs +++ b/resources/shaders/140/ssao.fs @@ -1,24 +1,45 @@ #version 140 /** - * SSAO Shader - GLSL 140 version with sharp depth threshold + * SSAO Shader - GLSL 140 version with a slope-based occlusion test * Only darkens valleys/concave areas, ignores smooth variations */ uniform sampler2D color_texture; uniform sampler2D depth_texture; -uniform sampler2D normal_texture; -uniform float z_near; +uniform vec2 inv_tex_size; uniform float z_far; uniform bool is_outline; +// The pass has no normal target to read, so the surface normal is reconstructed from the depth +// buffer. inv_projection_matrix unprojects a pixel back into view space and up_view is world +Z +// expressed in view space, which is what tells a top surface from a wall. +uniform mat4 inv_projection_matrix; +uniform vec3 up_view; in vec2 tex_coord; out vec4 frag_color; -float linearize_depth(float depth) +// Position of the given pixel in view space. Valid under both an orthographic and a perspective +// camera, unlike the depth linearization it replaces. +vec3 view_pos(ivec2 pixel) { - float z = depth * 2.0 - 1.0; - return (2.0 * z_near * z_far) / (z_far + z_near - z * (z_far - z_near)); + ivec2 p = clamp(pixel, ivec2(0), textureSize(depth_texture, 0) - 1); + float d = texelFetch(depth_texture, p, 0).r; + vec4 ndc = vec4((vec2(p) + 0.5) * inv_tex_size * 2.0 - 1.0, d * 2.0 - 1.0, 1.0); + vec4 view = inv_projection_matrix * ndc; + return view.xyz / view.w; +} + +// Surface normal at the given pixel, from the forward differences of the reconstructed view +// position. It rings by a pixel across a depth discontinuity, which is acceptable here: the +// normal only weights the occlusion, nothing is shaded with it. +vec3 view_normal(ivec2 pixel, vec3 p) +{ + vec3 px = view_pos(pixel + ivec2(1, 0)); + vec3 py = view_pos(pixel + ivec2(0, 1)); + vec3 n = cross(px - p, py - p); + float len = length(n); + return (len > 1e-8) ? n / len : vec3(0.0, 0.0, 1.0); } void main() @@ -28,81 +49,72 @@ void main() return; } ivec2 pixel = ivec2(gl_FragCoord.xy); - float center_depth = linearize_depth(texelFetch(depth_texture, pixel, 0).r); - - // Sample normal buffer (stored as RGB in 0-1 range, convert to -1 to 1) - vec3 normal_center = texelFetch(normal_texture, pixel, 0).rgb * 2.0 - 1.0; - normal_center = normalize(normal_center); - + vec3 color = texture(color_texture, tex_coord).rgb; + + // Nothing was drawn here: occluding the background would only darken the gradient, and its + // reconstructed normal is degenerate anyway. + if (texelFetch(depth_texture, pixel, 0).r >= 0.9999) { + frag_color = vec4(color, 1.0); + return; + } + + vec3 center_pos = view_pos(pixel); + float center_depth = -center_pos.z; + vec3 normal_center = view_normal(pixel, center_pos); + // Calculate upward-facing factor (Z-up coordinate system) - float up_factor = clamp(normal_center.z * 1.5, 0.0, 1.0); - + float up_factor = clamp(dot(normal_center, up_view), 0.0, 1.0); + // Adaptive radius in pixel space int radius = int(mix(2.0, 4.0, center_depth / z_far)); // Optimized sampling pattern - const ivec2 offsets[12] = ivec2[]( + const int SAMPLE_COUNT = 12; + const ivec2 offsets[SAMPLE_COUNT] = ivec2[]( ivec2(1, 0), ivec2(-1, 0), ivec2(0, 1), ivec2(0, -1), ivec2(1, 1), ivec2(-1, 1), ivec2(1, -1), ivec2(-1, -1), ivec2(2, 0), ivec2(-2, 0), ivec2(0, 2), ivec2(0, -2) ); - float occlusion = 0.0; - int valid_samples = 0; + // Occlusion is a slope, not a depth difference: the sine of the angle a neighbour subtends + // above the centre's tangent plane. A raw difference depends on camera distance and zoom, + // so no fixed thresholds suit both a 0.2 mm layer step and a 5 mm overhang. + const float SLOPE_MIN = 0.08; // ~5 degrees, above the depth-buffer noise of a flat surface + const float SLOPE_MAX = 0.60; // ~37 degrees, a full crease - for (int i = 0; i < 12; i++) { + float occlusion = 0.0; + + for (int i = 0; i < SAMPLE_COUNT; i++) { + // No edge rejection: view_pos clamps, giving a near-zero delta and no occlusion. + // Rejecting one side only would bias the denominator against the other. ivec2 sample_pixel = pixel + offsets[i] * radius; - if (sample_pixel.x < 0 || sample_pixel.y < 0) - continue; - - float sample_depth = linearize_depth(texelFetch(depth_texture, sample_pixel, 0).r); - - // Sample normal at neighbor - vec3 normal_sample = texelFetch(normal_texture, sample_pixel, 0).rgb * 2.0 - 1.0; - - // Depth difference (positive if neighbor is closer to camera) - float depth_diff = center_depth - sample_depth; - - // Sharp depth threshold === - // Minimum depth difference to consider occlusion (ignores small variations) - float threshold_min = 0.008; // Higher = only deep valleys get darkened - float threshold_max = 0.04; // Transition range for full occlusion + vec3 delta = view_pos(sample_pixel) - center_pos; + float dist = length(delta); + // How far the neighbour rises towards the viewer out of the centre's tangent plane. A + // flat surface gives ~0 whatever its orientation, so this also subsumes the separate + // planar test the normals were compared for. + float rise = (dist > 1e-6) ? dot(delta, normal_center) / dist : 0.0; float contribution = 0.0; - if (depth_diff > threshold_min) { + if (rise > SLOPE_MIN) { // Abrupt mapping with power curve - contribution = (depth_diff - threshold_min) / (threshold_max - threshold_min); + contribution = (rise - SLOPE_MIN) / (SLOPE_MAX - SLOPE_MIN); contribution = clamp(contribution, 0.0, 1.0); contribution = pow(contribution, 2.0); // Steeper curve for sharper transition } - // Reduce occlusion on planar surfaces (similar normals) - float normal_similarity = dot(normal_center, normal_sample); - float planar_factor = smoothstep(0.75, 0.95, normal_similarity); - contribution *= (1.0 - planar_factor * 0.6); - occlusion += contribution; - valid_samples++; } - if (valid_samples > 0) { - // Calculate ambient occlusion factor with higher base intensity - float ao_factor = 1.0 - (occlusion / float(valid_samples)) * 0.6; - - // Keep bright areas clean (higher minimum for upward-facing surfaces) - float ao_min = mix(0.55, 0.85, up_factor); - ao_factor = clamp(ao_factor, ao_min, 1.0); - - // Slight brightness boost for upward-facing surfaces - float brightness_boost = 1.0 + up_factor * 0.15; - ao_factor = ao_factor * brightness_boost; - - occlusion = ao_factor; - } else { - occlusion = 1.0; - } + // Calculate ambient occlusion factor with higher base intensity + float ao_factor = 1.0 - (occlusion / float(SAMPLE_COUNT)) * 0.6; + + // Keep bright areas clean (higher minimum for upward-facing surfaces). The old 0.85 floor + // and 1.15 boost were set when up_factor came from the colour buffer and read ~0; with a + // real normal they capped a top surface at 2% darkening, which hid the AO entirely. + float ao_min = mix(0.45, 0.70, up_factor); + occlusion = clamp(ao_factor, ao_min, 1.0); - vec3 color = texture(color_texture, tex_coord).rgb; frag_color = vec4(color * occlusion, 1.0); -} \ No newline at end of file +} diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index c8a2c4984d..ff6444d3ef 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -303,6 +303,9 @@ void AppConfig::set_defaults() if (get(SETTING_OPENGL_REALISTIC_PHONG).empty()) set_bool(SETTING_OPENGL_REALISTIC_PHONG, true); + if (get(SETTING_OPENGL_REALISTIC_PREVIEW).empty()) + set_bool(SETTING_OPENGL_REALISTIC_PREVIEW, false); + if (get(SETTING_OPENGL_SHADING_MODEL).empty()) set(SETTING_OPENGL_SHADING_MODEL, "gouraud"); diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 24a4c2069b..2eac9f5fc6 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -42,6 +42,7 @@ using namespace nlohmann; #define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows" #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals" +#define SETTING_OPENGL_REALISTIC_PREVIEW "opengl_realistic_preview" #define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count" #define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1 diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index 5141245d96..179105032d 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -60,6 +60,22 @@ public: // using the given camera matrices. // void render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix); + // + // ORCA: realistic view. Render the toolpaths as seen from the light, to fill the caller's + // shadow map. Depth only - the caller masks colour writes and owns the framebuffer. + // + void render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position); + // + // ORCA: realistic view. The shadow map the toolpaths sample, in the given texture unit. + // intensity == 0, the default, turns the lookup off and restores the plain shading. + // + void set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size); + // + // ORCA: tone applied to the shaded toolpaths, to pay back the light the lighting term, + // the shadow and the SSAO pass each take off. 1.0/1.0, the default, is a no-op; the + // caller decides which of the two it varies with the realistic view setting. + // + void set_tone(float exposure, float saturation); // // ************************************************************************ diff --git a/src/libvgcode/src/Shaders.hpp b/src/libvgcode/src/Shaders.hpp index 1450d1a6df..596b42df5c 100644 --- a/src/libvgcode/src/Shaders.hpp +++ b/src/libvgcode/src/Shaders.hpp @@ -16,7 +16,8 @@ static const char* Segments_Vertex_Shader = "#define FIX_TWISTING\n" "const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n" "const float light_top_diffuse = 0.6 * 0.8;\n" -"const float light_top_specular = 0.6 * 0.125;\n" +// ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen. +"const float light_top_specular = 0.6 * 0.25;\n" "const float light_top_shininess = 20.0;\n" "const vec3 light_front_dir = vec3(0.6985074, 0.1397015, 0.6985074);\n" "const float light_front_diffuse = 0.6 * 0.2;\n" @@ -30,8 +31,19 @@ static const char* Segments_Vertex_Shader = "uniform samplerBuffer height_width_angle_tex;\n" "uniform samplerBuffer color_tex;\n" "uniform usamplerBuffer segment_index_tex;\n" +// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not +// world_position, so the caster would write a depth the receiver never looks up. +"uniform float bias_scale;\n" "in int vertex_id;\n" "out vec3 color;\n" +"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" +"// ambient and emissive terms in color, which a shadow does not occlude. Their sum is the\n" +"// single lighting term this replaces, so shading is unchanged while shadows are off.\n" +"out vec3 color_direct;\n" +"// ORCA: realistic view - the fragment shader looks the fragment up in the shadow map, which\n" +"// needs its world position and, for the depth bias, its eye space normal.\n" +"out vec3 world_position;\n" +"out vec3 shadow_normal;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -40,11 +52,11 @@ static const char* Segments_Vertex_Shader = " float f = 1.0 / 255.0f;\n" " return f * vec3(r, g, b);\n" "}\n" -"float lighting(vec3 eye_position, vec3 eye_normal) {\n" +"float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" " float front_diffuse = light_front_diffuse * max(dot(eye_normal, light_front_dir), 0.0);\n" " float top_specular = light_top_specular * pow(max(dot(-normalize(eye_position), reflect(-light_top_dir, eye_normal)), 0.0), light_top_shininess);\n" -" return ambient + top_diffuse + front_diffuse + top_specular + emission;\n" +" return top_diffuse + front_diffuse + top_specular;\n" "}\n" "void main() {\n" " int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n" @@ -135,19 +147,63 @@ static const char* Segments_Vertex_Shader = " }\n" " vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n" " // ORCA: Apply bias to z-position to avoid z-fighting\n" -" eye_position.z += bias;\n" +" eye_position.z += bias * bias_scale;\n" " vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n" " vec3 color_base = decode_color(texelFetch(color_tex, id).r);\n" -" color = color_base * lighting(eye_position, eye_normal);\n" +" color = color_base * (ambient + emission);\n" +" color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" +" world_position = pos;\n" +" shadow_normal = eye_normal;\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; static const char* Segments_Fragment_Shader = "#version 150\n" +"// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n" +"// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n" +"// so the toolpaths shade exactly as before whenever realistic view is off.\n" +"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform sampler2D shadow_map;\n" +"uniform mat4 shadow_light_vp;\n" +"uniform float shadow_intensity;\n" +"uniform float shadow_map_texel;\n" +// ORCA: the lighting term peaks near 0.9 and every later multiplier - the shadow, then the SSAO +// post pass - only takes more light away, so the print reads dimmer and duller than the legend +// colours. These pay that back. Both are 1.0 for an untouched image; what the caller actually +// passes in each mode is decided in GLCanvas3D::_render_gcode, not here. +"uniform float exposure;\n" +"uniform float saturation;\n" +"const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n" "in vec3 color;\n" +"in vec3 color_direct;\n" +"in vec3 world_position;\n" +"in vec3 shadow_normal;\n" "out vec4 fragment_color;\n" +"float shadow_shade() {\n" +" if (shadow_intensity <= 0.0)\n" +" return 1.0;\n" +" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n" +" vec3 proj = lp.xyz / lp.w;\n" +" proj = proj * 0.5 + 0.5;\n" +" if (proj.z > 1.0)\n" +" return 1.0;\n" +" // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" +" // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" +" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n" +" float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" +" float sum = 0.0;\n" +" for (int x = -2; x <= 2; ++x) {\n" +" for (int y = -2; y <= 2; ++y) {\n" +" float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;\n" +" sum += (proj.z - bias > closest) ? 1.0 : 0.0;\n" +" }\n" +" }\n" +" return 1.0 - shadow_intensity * (sum / 25.0);\n" +"}\n" "void main() {\n" -" fragment_color = vec4(color, 1.0);\n" +" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n" +" c = mix(vec3(dot(c, LUMA)), c, saturation);\n" +" fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n" "}\n"; static const char* Options_Vertex_Shader = diff --git a/src/libvgcode/src/ShadersES.hpp b/src/libvgcode/src/ShadersES.hpp index e688fa7b4c..4194b836e3 100644 --- a/src/libvgcode/src/ShadersES.hpp +++ b/src/libvgcode/src/ShadersES.hpp @@ -17,7 +17,8 @@ static const char* Segments_Vertex_Shader_ES = "#define FIX_TWISTING\n" "const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n" "const float light_top_diffuse = 0.6 * 0.8;\n" -"const float light_top_specular = 0.6 * 0.125;\n" +// ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen. +"const float light_top_specular = 0.6 * 0.25;\n" "const float light_top_shininess = 20.0;\n" "const vec3 light_front_dir = vec3(0.6985074, 0.1397015, 0.6985074);\n" "const float light_front_diffuse = 0.6 * 0.3;\n" @@ -33,6 +34,14 @@ static const char* Segments_Vertex_Shader_ES = "uniform usampler2D segment_index_tex;\n" "in float vertex_id_float;\n" "out vec3 color;\n" +"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" +"// ambient and emissive terms in color, which a shadow does not occlude. Their sum is the\n" +"// single lighting term this replaces, so shading is unchanged while shadows are off.\n" +"out vec3 color_direct;\n" +"// ORCA: realistic view - the fragment shader looks the fragment up in the shadow map, which\n" +"// needs its world position and, for the depth bias, its eye space normal.\n" +"out vec3 world_position;\n" +"out vec3 shadow_normal;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -41,11 +50,11 @@ static const char* Segments_Vertex_Shader_ES = " float f = 1.0 / 255.0f;\n" " return f * vec3(r, g, b);\n" "}\n" -"float lighting(vec3 eye_position, vec3 eye_normal) {\n" +"float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" " float front_diffuse = light_front_diffuse * max(dot(eye_normal, light_front_dir), 0.0);\n" " float top_specular = light_top_specular * pow(max(dot(-normalize(eye_position), reflect(-light_top_dir, eye_normal)), 0.0), light_top_shininess);\n" -" return ambient + top_diffuse + front_diffuse + top_specular + emission;\n" +" return top_diffuse + front_diffuse + top_specular;\n" "}\n" "ivec2 tex_coord(sampler2D sampler, int id) {\n" " ivec2 tex_size = textureSize(sampler, 0);\n" @@ -143,17 +152,64 @@ static const char* Segments_Vertex_Shader_ES = " vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n" " vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n" " vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r);\n" -" color = color_base * lighting(eye_position, eye_normal);\n" +" color = color_base * (ambient + emission);\n" +" color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" +" world_position = pos;\n" +" shadow_normal = eye_normal;\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; static const char* Segments_Fragment_Shader_ES = "#version 300 es\n" "precision highp float;\n" +"// ORCA: sampler2D defaults to lowp in an ES fragment shader, far too coarse to compare\n" +"// shadow map depths against.\n" +"precision highp sampler2D;\n" +"// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n" +"// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n" +"// so the toolpaths shade exactly as before whenever realistic view is off.\n" +"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform sampler2D shadow_map;\n" +"uniform mat4 shadow_light_vp;\n" +"uniform float shadow_intensity;\n" +"uniform float shadow_map_texel;\n" +// ORCA: the lighting term peaks near 0.9 and every later multiplier - the shadow, then the SSAO +// post pass - only takes more light away, so the print reads dimmer and duller than the legend +// colours. These pay that back. Both are 1.0 for an untouched image; what the caller actually +// passes in each mode is decided in GLCanvas3D::_render_gcode, not here. +"uniform float exposure;\n" +"uniform float saturation;\n" +"const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n" "in vec3 color;\n" +"in vec3 color_direct;\n" +"in vec3 world_position;\n" +"in vec3 shadow_normal;\n" "out vec4 fragment_color;\n" +"float shadow_shade() {\n" +" if (shadow_intensity <= 0.0)\n" +" return 1.0;\n" +" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n" +" vec3 proj = lp.xyz / lp.w;\n" +" proj = proj * 0.5 + 0.5;\n" +" if (proj.z > 1.0)\n" +" return 1.0;\n" +" // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" +" // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" +" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n" +" float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" +" float sum = 0.0;\n" +" for (int x = -2; x <= 2; ++x) {\n" +" for (int y = -2; y <= 2; ++y) {\n" +" float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;\n" +" sum += (proj.z - bias > closest) ? 1.0 : 0.0;\n" +" }\n" +" }\n" +" return 1.0 - shadow_intensity * (sum / 25.0);\n" +"}\n" "void main() {\n" -" fragment_color = vec4(color, 1.0);\n" +" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n" +" c = mix(vec3(dot(c, LUMA)), c, saturation);\n" +" fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n" "}\n"; static const char* Options_Vertex_Shader_ES = diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index eb606598e9..61d7310508 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -42,6 +42,21 @@ void Viewer::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix) m_impl->render(view_matrix, projection_matrix); } +void Viewer::render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position) +{ + m_impl->render_shadow_casters(view_matrix, projection_matrix, light_position); +} + +void Viewer::set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size) +{ + m_impl->set_shadow_map(texture_unit, light_view_projection, intensity, texel_size); +} + +void Viewer::set_tone(float exposure, float saturation) +{ + m_impl->set_tone(exposure, saturation); +} + EViewType Viewer::get_view_type() const { return m_impl->get_view_type(); diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index da1a601149..10fda297d4 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -763,6 +763,14 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex"); m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex"); m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex"); + // ORCA: realistic view + m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map"); + m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp"); + m_uni_segments_shadow_intensity_id = glGetUniformLocation(m_segments_shader_id, "shadow_intensity"); + m_uni_segments_shadow_map_texel_id = glGetUniformLocation(m_segments_shader_id, "shadow_map_texel"); + m_uni_segments_exposure_id = glGetUniformLocation(m_segments_shader_id, "exposure"); + m_uni_segments_saturation_id = glGetUniformLocation(m_segments_shader_id, "saturation"); + m_uni_segments_bias_scale_id = glGetUniformLocation(m_segments_shader_id, "bias_scale"); glcheck(); assert(m_uni_segments_view_matrix_id != -1 && m_uni_segments_projection_matrix_id != -1 && @@ -1321,7 +1329,7 @@ void ViewerImpl::update_colors() m_settings.update_colors = false; } -void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix) +void ViewerImpl::apply_pending_updates() { if (m_settings.update_view_full_range) update_view_full_range(); @@ -1331,6 +1339,11 @@ void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matr if (m_settings.update_colors) update_colors(); +} + +void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix) +{ + apply_pending_updates(); const Mat4x4 inv_view_matrix = inverse(view_matrix); const Vec3 camera_position = { inv_view_matrix[12], inv_view_matrix[13], inv_view_matrix[14] }; @@ -1345,6 +1358,30 @@ void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matr #endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS } +void ViewerImpl::render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position) +{ + apply_pending_updates(); + + // Only the extrusions and travels cast: the option markers are indicators, not material. + m_rendering_shadow_casters = true; + render_segments(view_matrix, projection_matrix, light_position); + m_rendering_shadow_casters = false; +} + +void ViewerImpl::set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size) +{ + m_shadow_map_texture_unit = texture_unit; + m_shadow_light_vp = light_view_projection; + m_shadow_intensity = intensity; + m_shadow_map_texel = texel_size; +} + +void ViewerImpl::set_tone(float exposure, float saturation) +{ + m_exposure = exposure; + m_saturation = saturation; +} + void ViewerImpl::set_view_type(EViewType type) { m_settings.view_type = type; @@ -1994,6 +2031,15 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data())); + // ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades + // with the lookup off. + glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit)); + glsafe(glUniformMatrix4fv(m_uni_segments_shadow_light_vp_id, 1, GL_FALSE, m_shadow_light_vp.data())); + glsafe(glUniform1f(m_uni_segments_shadow_intensity_id, m_rendering_shadow_casters ? 0.0f : m_shadow_intensity)); + glsafe(glUniform1f(m_uni_segments_shadow_map_texel_id, m_shadow_map_texel)); + glsafe(glUniform1f(m_uni_segments_exposure_id, m_exposure)); + glsafe(glUniform1f(m_uni_segments_saturation_id, m_saturation)); + glsafe(glUniform1f(m_uni_segments_bias_scale_id, m_rendering_shadow_casters ? 0.0f : 1.0f)); glsafe(glDisable(GL_CULL_FACE)); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index 4da312fc0e..9231cbdefc 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -71,6 +71,24 @@ public: // Render the toolpaths // void render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix); + // + // ORCA: realistic view. Render the toolpaths as seen from the light, to fill the caller's + // shadow map. Only depth matters here, so the caller masks colour writes; light_position + // takes the place of the camera when the segment boxes are expanded, which gives their + // silhouette as the light sees it. + // + void render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position); + // + // ORCA: realistic view. The shadow map the toolpaths sample, in the given texture unit. + // intensity == 0, the default, turns the lookup off and restores the plain shading. + // + void set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size); + // + // ORCA: tone applied to the shaded toolpaths, to pay back the light the lighting term, + // the shadow and the SSAO pass each take off. 1.0/1.0, the default, is a no-op; the + // caller decides which of the two it varies with the realistic view setting. + // + void set_tone(float exposure, float saturation); EViewType get_view_type() const { return m_settings.view_type; } void set_view_type(EViewType type); @@ -330,6 +348,13 @@ private: int m_uni_segments_height_width_angle_tex_id{ -1 }; int m_uni_segments_colors_tex_id{ -1 }; int m_uni_segments_segment_index_tex_id{ -1 }; + int m_uni_segments_shadow_map_id{ -1 }; + int m_uni_segments_shadow_light_vp_id{ -1 }; + int m_uni_segments_shadow_intensity_id{ -1 }; + int m_uni_segments_shadow_map_texel_id{ -1 }; + int m_uni_segments_exposure_id{ -1 }; + int m_uni_segments_saturation_id{ -1 }; + int m_uni_segments_bias_scale_id{ -1 }; // // Caches for OpenGL uniforms id for options shader // @@ -469,6 +494,27 @@ private: size_t m_enabled_options_tex_size{ 0 }; #endif // ENABLE_OPENGL_ES + // + // ORCA: realistic view. Shadow map state set by set_shadow_map(), consumed by the segments + // shader. m_rendering_shadow_casters forces the intensity to 0 for the depth pass, which + // must not sample the very map it is writing. + // + // Defaults past the four texture units render_segments() binds itself, so the sampler never + // aliases one of the buffer textures before the owner of the map has said where it lives. + int m_shadow_map_texture_unit{ 4 }; + Mat4x4 m_shadow_light_vp{ 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; + float m_shadow_intensity{ 0.0f }; + float m_shadow_map_texel{ 0.0f }; + bool m_rendering_shadow_casters{ false }; + + // + // ORCA: realistic view. Tone set by set_tone(), consumed by the segments shader. + // The identity values leave the shading as it is outside realistic view. + // + float m_exposure{ 1.0f }; + float m_saturation{ 1.0f }; + + void apply_pending_updates(); void update_view_full_range(); void update_color_ranges(); void update_heights_widths(); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 3b3354d86f..2070b988d5 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1641,6 +1641,29 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type()); } +void GCodeViewer::render_shadow_casters(const Transform3d& light_view_matrix, const Transform3d& light_projection_matrix, const Vec3d& light_position) +{ + if (!has_data()) + return; + + m_viewer.render_shadow_casters( + libvgcode::convert(static_cast(light_view_matrix.matrix().cast())), + libvgcode::convert(static_cast(light_projection_matrix.matrix().cast())), + libvgcode::convert(static_cast(light_position.cast()))); +} + +void GCodeViewer::set_shadow_map(int texture_unit, const Transform3d& light_view_projection, float intensity, float texel_size) +{ + m_viewer.set_shadow_map(texture_unit, + libvgcode::convert(static_cast(light_view_projection.matrix().cast())), + intensity, texel_size); +} + +void GCodeViewer::set_tone(float exposure, float saturation) +{ + m_viewer.set_tone(exposure, saturation); +} + void GCodeViewer::render_overlay(int canvas_width, int canvas_height, int right_margin) { if (m_viewer.get_extrusion_roles().empty()) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 9b82a81b2f..2b4d4e2334 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -279,6 +279,13 @@ public: void render_scene(int canvas_width, int canvas_height); // Legend, sliders, the marker's position window and the G-code window, all ImGui. void render_overlay(int canvas_width, int canvas_height, int right_margin); + // ORCA: realistic view. Depth-only pass drawing the toolpaths as the light sees them, into + // the shadow map the caller has bound, and the map they sample back in render_scene. + void render_shadow_casters(const Transform3d& light_view_matrix, const Transform3d& light_projection_matrix, const Vec3d& light_position); + void set_shadow_map(int texture_unit, const Transform3d& light_view_projection, float intensity, float texel_size); + // ORCA: tone applied to the shaded toolpaths, paying back the light the lighting term, + // the shadow and the SSAO pass each take off. 1.0/1.0 is a no-op. + void set_tone(float exposure, float saturation); //BBS // void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); // void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 190eeedbb4..b76c7f346c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2199,8 +2199,8 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) // Recorded by PartPlate::render_icons() below, when it runs. wxGetApp().plater()->get_partplate_list().clear_hover_tooltip(); glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); - // Invalidate the shadow map each frame; only the View3D path below rebuilds it. This keeps - // the Preview / Assemble canvases from sampling a stale map with an outdated light matrix. + // Invalidate the shadow map each frame; the View3D and Preview paths below rebuild it. This + // keeps the Assemble canvas from sampling a stale map with an outdated light matrix. m_shadow_map_valid = false; _render_background(); @@ -2251,6 +2251,8 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) _render_selection(); _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes); _render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, true, hover_id); + // Realistic view: the print casts a shadow onto the plate here as it does in View3D. + _render_shadows(camera.get_view_matrix(), camera.get_projection_matrix()); // BBS: GUI refactor: add canvas size as parameters _render_gcode(cnv_size.get_width(), cnv_size.get_height()); } @@ -7644,11 +7646,20 @@ bool GLCanvas3D::_is_fxaa_enabled() const return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_FXAA_ENABLED); } +bool GLCanvas3D::_is_realistic_view_enabled() const +{ + const AppConfig* cfg = wxGetApp().app_config; + if (cfg == nullptr || !cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE)) + return false; + // Prepare and Assemble follow the umbrella toggle alone; Preview needs its own opt-in. + return m_canvas_type != ECanvasType::CanvasPreview || cfg->get_bool(SETTING_OPENGL_REALISTIC_PREVIEW); +} + bool GLCanvas3D::_is_ssao_enabled() const { if (wxGetApp().app_config == nullptr) return false; - return wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE) && + return _is_realistic_view_enabled() && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SSAO); } @@ -7803,95 +7814,23 @@ void GLCanvas3D::_render_ssao_pass(unsigned int width, unsigned int height) const Camera& camera = wxGetApp().plater()->get_camera(); - GLint prev_stencil_mask = 0xFF; - glsafe(::glGetIntegerv(GL_STENCIL_WRITEMASK, &prev_stencil_mask)); - GLboolean prev_stencil_test = GL_FALSE; - glsafe(::glGetBooleanv(GL_STENCIL_TEST, &prev_stencil_test)); - GLboolean prev_depth_mask = GL_TRUE; - glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask)); - GLint prev_depth_func = GL_LESS; - glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func)); - glsafe(::glDisable(GL_DEPTH_TEST)); glsafe(::glDisable(GL_BLEND)); - // Build stencil mask for bed/plate and apply SSAO only outside this mask. - glsafe(::glEnable(GL_STENCIL_TEST)); - glsafe(::glStencilMask(0xFF)); - glsafe(::glClearStencil(0)); - glsafe(::glClear(GL_STENCIL_BUFFER_BIT)); - glsafe(::glStencilFunc(GL_ALWAYS, 1, 0xFF)); - glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)); - // Mark only visible plate pixels (do not exclude objects in front of plate). - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glDepthMask(GL_FALSE)); - glsafe(::glDepthFunc(GL_LEQUAL)); - - GLboolean prev_color_mask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE }; - glsafe(::glGetBooleanv(GL_COLOR_WRITEMASK, prev_color_mask)); - glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE)); - - if (const BuildVolume& build_volume = m_bed.build_volume(); build_volume.valid()) { - GLShaderProgram* flat = wxGetApp().get_shader("flat"); - if (flat != nullptr) { - flat->start_using(); - flat->set_uniform("projection_matrix", camera.get_projection_matrix()); - - GLModel plate_mask; - GLModel::Geometry mask; - mask.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - - if (build_volume.type() == BuildVolume_Type::Rectangle) { - const BoundingBox3Base bb = build_volume.bounding_volume(); - mask.reserve_vertices(4); - mask.reserve_indices(6); - mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.min.y(), 0.0f)); - mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.min.y(), 0.0f)); - mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.max.y(), 0.0f)); - mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.max.y(), 0.0f)); - mask.add_triangle(0, 1, 2); - mask.add_triangle(0, 2, 3); - } else if (build_volume.type() == BuildVolume_Type::Circle) { - const Vec2f c = Vec2f(unscaled(build_volume.circle().center.x()), unscaled(build_volume.circle().center.y())); - const float r = unscaled(build_volume.circle().radius); - const int segments = 64; - mask.reserve_vertices(segments + 1); - mask.reserve_indices(segments * 3); - mask.add_vertex(Vec3f(c.x(), c.y(), 0.0f)); - for (int i = 0; i < segments; ++i) { - const float a = (2.0f * float(PI) * float(i)) / float(segments); - mask.add_vertex(Vec3f(c.x() + r * std::cos(a), c.y() + r * std::sin(a), 0.0f)); - } - for (int i = 0; i < segments; ++i) { - const unsigned int i1 = 1 + i; - const unsigned int i2 = 1 + ((i + 1) % segments); - mask.add_triangle(0, i1, i2); - } - } - - if (mask.vertices_count() > 0 && mask.indices_count() > 0) { - plate_mask.init_from(std::move(mask)); - flat->set_uniform("view_model_matrix", camera.get_view_matrix()); - plate_mask.render(flat); - } - flat->stop_using(); - } - } - - glsafe(::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE)); - glsafe(::glDisable(GL_DEPTH_TEST)); - glsafe(::glStencilMask(0x00)); - glsafe(::glStencilFunc(GL_NOTEQUAL, 1, 0xFF)); - glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP)); - shader->start_using(); shader->set_uniform("view_model_matrix", Transform3d::Identity()); shader->set_uniform("projection_matrix", Transform3d::Identity()); shader->set_uniform("color_texture", 0); shader->set_uniform("depth_texture", 1); shader->set_uniform("inv_tex_size", Vec2f(1.0f / static_cast(width), 1.0f / static_cast(height))); - shader->set_uniform("z_near", camera.get_near_z()); shader->set_uniform("z_far", camera.get_far_z()); + // The shader reconstructs the surface normal from the depth buffer, there being no normal + // target to read: it unprojects a pixel back into view space, then measures the result + // against world +Z expressed in view space to tell a top surface from a wall. + const Matrix4d inv_projection_matrix = camera.get_projection_matrix().matrix().inverse(); + shader->set_uniform("inv_projection_matrix", inv_projection_matrix); + const Vec3d up_view = (camera.get_view_matrix().matrix().block<3, 3>(0, 0) * Vec3d::UnitZ()).normalized(); + shader->set_uniform("up_view", up_view); glsafe(::glActiveTexture(GL_TEXTURE0)); glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_color_texture_id)); @@ -7903,13 +7842,6 @@ void GLCanvas3D::_render_ssao_pass(unsigned int width, unsigned int height) glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); shader->stop_using(); - if (!prev_stencil_test) - glsafe(::glDisable(GL_STENCIL_TEST)); - glsafe(::glStencilMask(prev_stencil_mask)); - glsafe(::glColorMask(prev_color_mask[0], prev_color_mask[1], prev_color_mask[2], prev_color_mask[3])); - - glsafe(::glDepthMask(prev_depth_mask)); - glsafe(::glDepthFunc(prev_depth_func)); glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); @@ -8154,15 +8086,19 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform { if (wxGetApp().app_config == nullptr) return; - if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE)) + if (!_is_realistic_view_enabled()) return; if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS)) return; - if (m_volumes.empty()) - return; - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) + // The preview canvas holds no volumes of its own for FFF. Once slicing has run its printed + // geometry is the G-code toolpaths, which both cast into the map here and sample it back in + // _render_gcode; before slicing there are only shells, and nothing casts at all. View3D and + // SLA preview use m_volumes. The shells are deliberately never casters: they are a + // translucent ghost of the whole object, so they would drop the solid shadow of a print that + // has not been sliced, and at any layer below the last, one that is not there yet. + const bool toolpath_casters = m_canvas_type == ECanvasType::CanvasPreview && m_gcode_viewer.has_data(); + if (!toolpath_casters && m_volumes.empty()) return; if (OpenGLManager::get_framebuffers_type() == OpenGLManager::EFramebufferType::Arb) { @@ -8174,10 +8110,30 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform // Bounding box of the printable objects (the shadow casters). BoundingBoxf3 obj_bb; - for (const GLVolume* volume : m_volumes.volumes) { - if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) - continue; - obj_bb.merge(volume->transformed_bounding_box()); + if (toolpath_casters) { + // Merged corner by corner: BoundingBoxf3(min, max) marks itself undefined at zero + // Z extent, which a single layer print gives, and the check below would then drop + // every shadow in the frame. + const BoundingBoxf3& paths_bb = m_gcode_viewer.get_paths_bounding_box(); + if ((paths_bb.min.array() <= paths_bb.max.array()).all()) { + obj_bb.merge(paths_bb.min); + obj_bb.merge(paths_bb.max); + } + // Only the enabled layers are drawn, so fitting the map to the whole print wastes + // its depth range and makes contact shadows shift as the slider moves. The z = 0 + // shadow is enclosed separately below, so the plate shadow is unaffected. + const std::vector layer_zs = m_gcode_viewer.get_layers_zs(); + if (!layer_zs.empty()) { + const size_t top = std::min(m_gcode_viewer.get_layers_z_range()[1], layer_zs.size() - 1); + obj_bb.max.z() = std::max(obj_bb.min.z(), std::min(obj_bb.max.z(), layer_zs[top])); + } + } + else { + for (const GLVolume* volume : m_volumes.volumes) { + if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) + continue; + obj_bb.merge(volume->transformed_bounding_box()); + } } if (!obj_bb.defined) return; // no objects to cast shadows @@ -8299,16 +8255,21 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform glsafe(::glPolygonOffset(4.0f, 4.0f)); glsafe(::glDisable(GL_CULL_FACE)); - shader->start_using(); - shader->set_uniform("projection_matrix", Transform3d(light_proj)); - for (GLVolume* volume : m_volumes.volumes) { - if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) - continue; - const Transform3d view_model = Transform3d(light_view) * volume->world_matrix(); - shader->set_uniform("view_model_matrix", view_model); - volume->model.render(shader); + if (toolpath_casters) + m_gcode_viewer.render_shadow_casters(Transform3d(light_view), Transform3d(light_proj), eye); + // Only this branch draws through "flat"; the toolpaths bring their own program. + else if (GLShaderProgram* shader = wxGetApp().get_shader("flat"); shader != nullptr) { + shader->start_using(); + shader->set_uniform("projection_matrix", Transform3d(light_proj)); + for (GLVolume* volume : m_volumes.volumes) { + if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) + continue; + const Transform3d view_model = Transform3d(light_view) * volume->world_matrix(); + shader->set_uniform("view_model_matrix", view_model); + volume->model.render(shader); + } + shader->stop_using(); } - shader->stop_using(); // Restore state glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); @@ -8517,7 +8478,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with return; } - const bool realistic_mode = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE); + const bool realistic_mode = _is_realistic_view_enabled(); const bool realistic_phong = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_PHONG); const std::string shader_name = (realistic_mode && realistic_phong) ? "phong" : "gouraud"; GLShaderProgram* shader = wxGetApp().get_shader(shader_name); @@ -8741,7 +8702,34 @@ void GLCanvas3D::_render_wireframe_overlay() //BBS: GUI refactor: add canvas size as parameters void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height) { + // Realistic view: the toolpaths receive the same depth map they were rendered into by + // _render_shadows, which is what gives them object-on-object and self shadows. Intensity 0 + // short-circuits the lookup in the shader, so this is inert whenever the map is missing. + const bool receive_shadows = m_shadow_map_valid && m_shadow_map_texture_id != 0 && m_shadow_map_size != 0; + if (receive_shadows) { + glsafe(::glActiveTexture(GL_TEXTURE4)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_shadow_map_texture_id)); + glsafe(::glActiveTexture(GL_TEXTURE0)); + m_gcode_viewer.set_shadow_map(4, m_shadow_light_vp, 0.35f, 1.0f / static_cast(m_shadow_map_size)); + } + else + m_gcode_viewer.set_shadow_map(4, Transform3d::Identity(), 0.0f, 0.0f); + + // The lighting term leaves the print dimmer and duller than the legend colours. Saturation + // pays back the duller half in both modes; brightness only where something takes light off + // again - realistic view with at least one lossy pass on - else the lift would just clip. + const AppConfig* cfg = wxGetApp().app_config; + const bool lossy_passes = cfg != nullptr && _is_realistic_view_enabled() && + (cfg->get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS) || cfg->get_bool(SETTING_OPENGL_PHONG_SSAO)); + m_gcode_viewer.set_tone(lossy_passes ? 1.1f : 1.0f, 1.15f); + m_gcode_viewer.render_scene(canvas_width, canvas_height); + + if (receive_shadows) { + glsafe(::glActiveTexture(GL_TEXTURE4)); + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + glsafe(::glActiveTexture(GL_TEXTURE0)); + } } void GLCanvas3D::_render_gcode_overlay(int canvas_width, int canvas_height) @@ -9831,7 +9819,7 @@ void GLCanvas3D::_render_canvas_toolbar() ); create_menu_item( _utf8(L("Realistic View")), - m_canvas_type != ECanvasType::CanvasPreview, // not work on preview + true, // work on all cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE), [&cfg]{ cfg->set_bool(SETTING_OPENGL_REALISTIC_MODE, !cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE)); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index ad5188f2f3..75820694b0 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1324,6 +1324,7 @@ private: void _picking_pass(); void _rectangular_selection_picking_pass(); bool _is_fxaa_enabled() const; + bool _is_realistic_view_enabled() const; bool _is_ssao_enabled() const; int _get_effective_fps_cap() const; bool _is_fps_overlay_enabled() const; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index be4bfcfa63..3a7f37e9cb 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1932,6 +1932,15 @@ void PreferencesDialog::create_items() ); g_sizer->Add(item_realistic_phong); + auto item_realistic_preview = create_item_checkbox( + _L("Enable in Preview"), + _L("Also applies realistic view to the Preview canvas, not just Prepare.\n" + "Preview draws the full toolpath geometry, so shadows and SSAO cost considerably" + " more there than on a plain model."), + SETTING_OPENGL_REALISTIC_PREVIEW + ); + g_sizer->Add(item_realistic_preview); + auto item_realistic_ssao = create_item_checkbox( _L("SSAO ambient occlusion"), _L("Applies SSAO in realistic view."),