Interactive shadows (#14702)

Co-authored-by: Ian Bassi <12130714+ianalexis@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
This commit is contained in:
Rodrigo Faselli
2026-07-12 21:44:30 -03:00
committed by GitHub
parent b1e64dc69c
commit 78692aa08f
12 changed files with 544 additions and 144 deletions

View File

@@ -50,6 +50,15 @@ uniform PrintVolumeDetection print_volume;
uniform float z_far;
uniform float z_near;
// Depth-based shadow map (object-on-object and self shadows). shadow_intensity == 0 disables it.
uniform sampler2D shadow_map;
uniform mat4 shadow_light_vp;
uniform float shadow_intensity;
uniform float shadow_map_texel;
// LIGHT_TOP_DIR in eye space (matches the diffuse light used for shading in gouraud.vs).
const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
varying vec3 clipping_planes_dots;
varying float color_clip_plane_dot;
@@ -125,6 +134,35 @@ float DetectSilho(vec2 fragCoord)
);
}
// 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()
{
if (shadow_intensity <= 0.0)
return 1.0;
vec4 lp = shadow_light_vp * world_pos;
vec3 proj = lp.xyz / lp.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0)
return 1.0;
// Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This
// suppresses self-shadow acne without discarding real shadows cast by other objects onto
// back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow).
float NdotL = dot(normalize(eye_normal), SHADOW_LIGHT_DIR);
float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));
// 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne.
float sum = 0.0;
for (int x = -2; x <= 2; ++x) {
for (int y = -2; y <= 2; ++y) {
float closest = texture2D(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;
sum += (proj.z - bias > closest) ? 1.0 : 0.0;
}
}
return 1.0 - shadow_intensity * (sum / 25.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -166,9 +204,11 @@ void main()
}
color.rgb = (any(lessThan(pv_check_min, ZERO)) || any(greaterThan(pv_check_max, ZERO))) ? mix(color.rgb, ZERO, 0.3333) : color.rgb;
float shade = shadow_shade();
//BBS: add outline_color
if (is_outline) {
color = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a);
color = vec4((vec3(intensity.y) + color.rgb * intensity.x) * shade, color.a);
vec2 fragCoord = gl_FragCoord.xy;
float s = DetectSilho(fragCoord);
// Makes silhouettes thicker.
@@ -176,13 +216,13 @@ void main()
{
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
}
gl_FragColor = vec4(mix(color.rgb, getBackfaceColor(color.rgb), s), color.a);
}
#ifdef ENABLE_ENVIRONMENT_MAP
else if (use_environment_tex)
gl_FragColor = vec4(0.45 * texture(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + 0.8 * color.rgb * intensity.x, color.a);
gl_FragColor = vec4((0.45 * texture(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + 0.8 * color.rgb * intensity.x) * shade, color.a);
#endif
else
gl_FragColor = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a);
gl_FragColor = vec4((vec3(intensity.y) + color.rgb * intensity.x) * shade, color.a);
}

View File

@@ -65,6 +65,12 @@ uniform float z_far;
uniform float z_near;
uniform bool enable_ssao;
// Depth-based shadow map (object-on-object and self shadows). shadow_intensity == 0 disables it.
uniform sampler2D shadow_map;
uniform mat4 shadow_light_vp;
uniform float shadow_intensity;
uniform float shadow_map_texel;
varying vec3 clipping_planes_dots;
varying float color_clip_plane_dot;
@@ -167,10 +173,39 @@ vec3 compute_window_reflection(vec3 normal, vec3 view_dir)
float intensity = window_light * bars * (0.15 + 0.15 * fresnel) * facing;
intensity = clamp(intensity, 0.0, 0.25);
return vec3(intensity);
}
// 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()
{
if (shadow_intensity <= 0.0)
return 1.0;
vec4 lp = shadow_light_vp * world_pos;
vec3 proj = lp.xyz / lp.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0)
return 1.0;
// Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This
// suppresses self-shadow acne without discarding real shadows cast by other objects onto
// back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow).
float NdotL = dot(normalize(eye_normal), LIGHT_TOP_DIR);
float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));
// 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne.
float sum = 0.0;
for (int x = -2; x <= 2; ++x) {
for (int y = -2; y <= 2; ++y) {
float closest = texture2D(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;
sum += (proj.z - bias > closest) ? 1.0 : 0.0;
}
}
return 1.0 - shadow_intensity * (sum / 25.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -226,8 +261,10 @@ void main()
// SSAO is applied in post-process pass. Keep base lighting unchanged here.
float shade = shadow_shade();
if (is_outline) {
vec3 shaded_rgb = (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS;
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);
@@ -240,8 +277,8 @@ void main()
}
#ifdef ENABLE_ENVIRONMENT_MAP
else if (use_environment_tex)
gl_FragColor = vec4(clamp((0.45 * texture2D(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + window_reflection + 0.8 * color.rgb * diffuse) * PHONG_BRIGHTNESS, vec3(0.0), vec3(1.0)), color.a);
gl_FragColor = vec4(clamp((0.45 * texture2D(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + window_reflection + 0.8 * color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
#endif
else
gl_FragColor = vec4(clamp((vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS, vec3(0.0), vec3(1.0)), color.a);
gl_FragColor = vec4(clamp((vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
}

View File

@@ -0,0 +1,40 @@
#version 110
// Draws the build-plate as a receiver of the same depth shadow map used for object/self shadows,
// so the plate, objects, and self-shadows all come from one unified technique.
uniform sampler2D shadow_map;
uniform mat4 shadow_light_vp;
uniform float shadow_intensity;
uniform float shadow_map_texel;
varying vec4 world_pos;
// Fraction of the 5x5 PCF kernel occluded from the light. Matches the object shadow shader.
float shadow_occlusion()
{
vec4 lp = shadow_light_vp * world_pos;
vec3 proj = lp.xyz / lp.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0)
return 0.0;
// The plate is a pure receiver (never rendered into the shadow map), so a tiny constant
// bias for numerical safety is enough here.
float bias = 0.0004;
float sum = 0.0;
for (int x = -2; x <= 2; ++x) {
for (int y = -2; y <= 2; ++y) {
float closest = texture2D(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;
sum += (proj.z - bias > closest) ? 1.0 : 0.0;
}
}
return sum / 25.0;
}
void main()
{
float occ = shadow_occlusion();
if (occ <= 0.0)
discard;
gl_FragColor = vec4(0.0, 0.0, 0.0, shadow_intensity * occ);
}

View File

@@ -0,0 +1,16 @@
#version 110
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
attribute vec3 v_position;
// The plate mask quad is authored directly in world coordinates (z = 0 plane),
// so v_position is already the world position of the fragment.
varying vec4 world_pos;
void main()
{
world_pos = vec4(v_position, 1.0);
gl_Position = projection_matrix * view_model_matrix * vec4(v_position, 1.0);
}

View File

@@ -49,6 +49,15 @@ uniform PrintVolumeDetection print_volume;
uniform float z_far;
uniform float z_near;
// Depth-based shadow map (object-on-object and self shadows). shadow_intensity == 0 disables it.
uniform sampler2D shadow_map;
uniform mat4 shadow_light_vp;
uniform float shadow_intensity;
uniform float shadow_map_texel;
// LIGHT_TOP_DIR in eye space (matches the diffuse light used for shading in gouraud.vs).
const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
in vec3 clipping_planes_dots;
in float color_clip_plane_dot;
@@ -124,6 +133,35 @@ float DetectSilho(vec2 fragCoord)
);
}
// 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()
{
if (shadow_intensity <= 0.0)
return 1.0;
vec4 lp = shadow_light_vp * world_pos;
vec3 proj = lp.xyz / lp.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0)
return 1.0;
// Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This
// suppresses self-shadow acne without discarding real shadows cast by other objects onto
// back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow).
float NdotL = dot(normalize(eye_normal), SHADOW_LIGHT_DIR);
float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));
// 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne.
float sum = 0.0;
for (int x = -2; x <= 2; ++x) {
for (int y = -2; y <= 2; ++y) {
float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;
sum += (proj.z - bias > closest) ? 1.0 : 0.0;
}
}
return 1.0 - shadow_intensity * (sum / 25.0);
}
out vec4 out_color;
void main()
@@ -167,9 +205,11 @@ void main()
}
color.rgb = (any(lessThan(pv_check_min, ZERO)) || any(greaterThan(pv_check_max, ZERO))) ? mix(color.rgb, ZERO, 0.3333) : color.rgb;
float shade = shadow_shade();
//BBS: add outline_color
if (is_outline) {
color = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a);
color = vec4((vec3(intensity.y) + color.rgb * intensity.x) * shade, color.a);
vec2 fragCoord = gl_FragCoord.xy;
float s = DetectSilho(fragCoord);
// Makes silhouettes thicker.
@@ -177,13 +217,13 @@ void main()
{
s = max(s, DetectSilho(fragCoord.xy + vec2(i, 0)));
s = max(s, DetectSilho(fragCoord.xy + vec2(0, i)));
}
}
out_color = vec4(mix(color.rgb, getBackfaceColor(color.rgb), s), color.a);
}
#ifdef ENABLE_ENVIRONMENT_MAP
else if (use_environment_tex)
out_color = vec4(0.45 * texture(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + 0.8 * color.rgb * intensity.x, color.a);
out_color = vec4((0.45 * texture(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + 0.8 * color.rgb * intensity.x) * shade, color.a);
#endif
else
out_color = vec4(vec3(intensity.y) + color.rgb * intensity.x, color.a);
out_color = vec4((vec3(intensity.y) + color.rgb * intensity.x) * shade, color.a);
}

View File

@@ -65,6 +65,12 @@ uniform float z_far;
uniform float z_near;
uniform bool enable_ssao;
// Depth-based shadow map (object-on-object and self shadows). shadow_intensity == 0 disables it.
uniform sampler2D shadow_map;
uniform mat4 shadow_light_vp;
uniform float shadow_intensity;
uniform float shadow_map_texel;
in vec3 clipping_planes_dots;
in float color_clip_plane_dot;
@@ -170,11 +176,40 @@ vec3 compute_window_reflection(vec3 normal, vec3 view_dir)
float intensity = window_light * bars * (0.15 + 0.15 * fresnel) * facing;
intensity = clamp(intensity, 0.0, 0.25);
intensity = clamp(intensity, 0.0, 0.25);
return vec3(intensity);
}
// 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()
{
if (shadow_intensity <= 0.0)
return 1.0;
vec4 lp = shadow_light_vp * world_pos;
vec3 proj = lp.xyz / lp.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0)
return 1.0;
// Slope-scaled depth bias: larger where the surface grazes / faces away from the light. This
// suppresses self-shadow acne without discarding real shadows cast by other objects onto
// back-facing surfaces (e.g. the shaded back/tip of a cone sitting inside a larger shadow).
float NdotL = dot(normalize(eye_normal), LIGHT_TOP_DIR);
float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));
// 5x5 PCF: softens shadow edges into a smooth penumbra and blurs residual facet acne.
float sum = 0.0;
for (int x = -2; x <= 2; ++x) {
for (int y = -2; y <= 2; ++y) {
float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;
sum += (proj.z - bias > closest) ? 1.0 : 0.0;
}
}
return 1.0 - shadow_intensity * (sum / 25.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -230,8 +265,10 @@ void main()
// SSAO is applied in post-process pass. Keep base lighting unchanged here.
float shade = shadow_shade();
if (is_outline) {
vec3 shaded_rgb = (vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS;
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);
@@ -244,8 +281,8 @@ void main()
}
#ifdef ENABLE_ENVIRONMENT_MAP
else if (use_environment_tex)
out_color = vec4(clamp((0.45 * texture(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + window_reflection + 0.8 * color.rgb * diffuse) * PHONG_BRIGHTNESS, vec3(0.0), vec3(1.0)), color.a);
out_color = vec4(clamp((0.45 * texture(environment_tex, normalize(eye_normal).xy * 0.5 + 0.5).xyz + window_reflection + 0.8 * color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
#endif
else
out_color = vec4(clamp((vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS, vec3(0.0), vec3(1.0)), color.a);
out_color = vec4(clamp((vec3(specular) + window_reflection + color.rgb * diffuse) * PHONG_BRIGHTNESS * shade, vec3(0.0), vec3(1.0)), color.a);
}

View File

@@ -0,0 +1,42 @@
#version 140
// Draws the build-plate as a receiver of the same depth shadow map used for object/self shadows,
// so the plate, objects, and self-shadows all come from one unified technique.
uniform sampler2D shadow_map;
uniform mat4 shadow_light_vp;
uniform float shadow_intensity;
uniform float shadow_map_texel;
in vec4 world_pos;
out vec4 out_color;
// Fraction of the 5x5 PCF kernel occluded from the light. Matches the object shadow shader.
float shadow_occlusion()
{
vec4 lp = shadow_light_vp * world_pos;
vec3 proj = lp.xyz / lp.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0)
return 0.0;
// The plate is a pure receiver (never rendered into the shadow map), so a tiny constant
// bias for numerical safety is enough here.
float bias = 0.0004;
float sum = 0.0;
for (int x = -2; x <= 2; ++x) {
for (int y = -2; y <= 2; ++y) {
float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;
sum += (proj.z - bias > closest) ? 1.0 : 0.0;
}
}
return sum / 25.0;
}
void main()
{
float occ = shadow_occlusion();
if (occ <= 0.0)
discard;
out_color = vec4(0.0, 0.0, 0.0, shadow_intensity * occ);
}

View File

@@ -0,0 +1,16 @@
#version 140
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
in vec3 v_position;
// The plate mask quad is authored directly in world coordinates (z = 0 plane),
// so v_position is already the world position of the fragment.
out vec4 world_pos;
void main()
{
world_pos = vec4(v_position, 1.0);
gl_Position = projection_matrix * view_model_matrix * vec4(v_position, 1.0);
}

View File

@@ -1231,6 +1231,14 @@ GLCanvas3D::~GLCanvas3D()
glsafe(::glDeleteTextures(1, &m_ssao_depth_texture_id));
m_ssao_depth_texture_id = 0;
}
if (m_shadow_map_texture_id != 0) {
glsafe(::glDeleteTextures(1, &m_shadow_map_texture_id));
m_shadow_map_texture_id = 0;
}
if (m_shadow_map_fbo != 0) {
glsafe(::glDeleteFramebuffers(1, &m_shadow_map_fbo));
m_shadow_map_fbo = 0;
}
m_plate_shadow_mask.reset();
}
m_plate_shadow_mask_key.clear();
@@ -2033,6 +2041,9 @@ void GLCanvas3D::render(bool only_init)
// draw scene
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.
m_shadow_map_valid = false;
_render_background();
//BBS add partplater rendering logic
@@ -2057,7 +2068,8 @@ void GLCanvas3D::render(bool only_init)
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
//BBS: add outline logic
_render_cast_shadows_on_plate(camera.get_view_matrix(), camera.get_projection_matrix());
// Depth pass for object-on-object and self shadows; consumed by the gouraud shader below.
_render_shadows(camera.get_view_matrix(), camera.get_projection_matrix());
_render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running());
_render_sla_slices();
_render_selection();
@@ -7858,9 +7870,8 @@ void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transfo
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid);
}
void GLCanvas3D::_render_cast_shadows_on_plate(const Transform3d& view_matrix, const Transform3d& projection_matrix)
void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix)
{
// Check if shadow rendering is enabled in configuration
if (wxGetApp().app_config == nullptr)
return;
if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE))
@@ -7874,54 +7885,181 @@ void GLCanvas3D::_render_cast_shadows_on_plate(const Transform3d& view_matrix, c
if (shader == nullptr)
return;
// Fixed light direction (pointing downward at an angle)
// Drive shadow direction from current view angle: define light in eye-space,
// then transform it to world-space with inverse view rotation.
const Vec3d light_dir_eye = Vec3d(-0.4574957, 0.4574957, 0.7624929).normalized();
const Matrix3d view_rot = view_matrix.matrix().block<3, 3>(0, 0);
const Vec3d light_dir_to_light = (view_rot.transpose() * light_dir_eye).normalized();
const Vec3d ray_dir = -light_dir_to_light; // Direction of shadow projection
if (std::abs(ray_dir.z()) < 1e-6)
if (OpenGLManager::get_framebuffers_type() == OpenGLManager::EFramebufferType::Arb) {
// Light direction (same as used in shading and plate shading)
const Vec3d light_dir_eye = Vec3d(-0.4574957, 0.4574957, 0.7624929).normalized();
const Matrix3d view_rot = view_matrix.matrix().block<3, 3>(0, 0);
const Vec3d dir_to_light = (view_rot.transpose() * light_dir_eye).normalized();
// 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 (!obj_bb.defined)
return; // no objects to cast shadows
// Orthographic light-space basis (z points toward the light).
const Vec3d up = (std::abs(dir_to_light.z()) > 0.99) ? Vec3d::UnitY() : Vec3d::UnitZ();
const Vec3d z_axis = dir_to_light;
const Vec3d x_axis = up.cross(z_axis).normalized();
const Vec3d y_axis = z_axis.cross(x_axis).normalized();
// Fit the frustum to the object AABB *and* the object's shadow projected onto the plate
// (clamped to the plate footprint). This keeps the map tight/high-res for short shadows
// while still covering long shadows at grazing light angles, which previously fell outside
// the map and were clipped.
const Vec3d ray_dir = -dir_to_light; // direction the shadow travels
const BoundingBoxf3 plate_bb = m_bed.build_volume().valid() ? m_bed.build_volume().bounding_volume() : obj_bb;
Vec3d lmin(DBL_MAX, DBL_MAX, DBL_MAX);
Vec3d lmax(-DBL_MAX, -DBL_MAX, -DBL_MAX);
auto enclose = [&](const Vec3d& p) {
const Vec3d lp(x_axis.dot(p), y_axis.dot(p), z_axis.dot(p));
lmin = lmin.cwiseMin(lp);
lmax = lmax.cwiseMax(lp);
};
for (int i = 0; i < 8; ++i) {
const Vec3d corner((i & 1) ? obj_bb.max.x() : obj_bb.min.x(),
(i & 2) ? obj_bb.max.y() : obj_bb.min.y(),
(i & 4) ? obj_bb.max.z() : obj_bb.min.z());
enclose(corner);
// Where this corner's shadow lands on z = 0, clamped to the plate so a grazing angle
// (t -> infinity) stays bounded.
if (ray_dir.z() < -1e-6) {
const double t = -corner.z() / ray_dir.z();
Vec3d s = corner + t * ray_dir;
s.x() = std::min(std::max(s.x(), plate_bb.min.x()), plate_bb.max.x());
s.y() = std::min(std::max(s.y(), plate_bb.min.y()), plate_bb.max.y());
s.z() = 0.0;
enclose(s);
}
}
// Light "camera" placed just past the nearest enclosed point, looking toward the scene.
const double range = lmax.z() - lmin.z();
const double margin = std::max(1.0, 0.05 * range);
const double cx = 0.5 * (lmin.x() + lmax.x());
const double cy = 0.5 * (lmin.y() + lmax.y());
const Vec3d eye = x_axis * cx + y_axis * cy + z_axis * (lmax.z() + margin);
Matrix4d light_view = Matrix4d::Identity();
light_view.block<1, 3>(0, 0) = x_axis.transpose();
light_view.block<1, 3>(1, 0) = y_axis.transpose();
light_view.block<1, 3>(2, 0) = z_axis.transpose();
light_view(0, 3) = -x_axis.dot(eye);
light_view(1, 3) = -y_axis.dot(eye);
light_view(2, 3) = -z_axis.dot(eye);
// Ortho fit to the light-space extent (symmetric in X/Y around cx,cy; +2% edge padding).
const double halfx = std::max(0.5 * (lmax.x() - lmin.x()), 1.0) * 1.02;
const double halfy = std::max(0.5 * (lmax.y() - lmin.y()), 1.0) * 1.02;
const double near_z = margin * 0.5;
const double far_z = range + margin * 1.5;
Matrix4d light_proj = Matrix4d::Identity();
light_proj(0, 0) = 1.0 / halfx;
light_proj(1, 1) = 1.0 / halfy;
light_proj(2, 2) = -2.0 / (far_z - near_z);
light_proj(2, 3) = -(far_z + near_z) / (far_z - near_z);
m_shadow_light_vp = Transform3d(light_proj * light_view);
// Create / resize the depth texture and FBO
const unsigned int size = 2048;
if (m_shadow_map_texture_id == 0) {
glsafe(::glGenTextures(1, &m_shadow_map_texture_id));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_shadow_map_texture_id));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER));
const float border[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
glsafe(::glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border));
m_shadow_map_size = 0;
}
if (m_shadow_map_size != size) {
glsafe(::glBindTexture(GL_TEXTURE_2D, m_shadow_map_texture_id));
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, size, size, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr));
m_shadow_map_size = size;
}
if (m_shadow_map_fbo == 0)
glsafe(::glGenFramebuffers(1, &m_shadow_map_fbo));
// Save OpenGL state that we will modify
GLint prev_viewport[4] = { 0, 0, 0, 0 };
glsafe(::glGetIntegerv(GL_VIEWPORT, prev_viewport));
GLint prev_fbo = 0;
glsafe(::glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo));
GLboolean prev_color_mask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE };
glsafe(::glGetBooleanv(GL_COLOR_WRITEMASK, prev_color_mask));
const GLboolean prev_cull = ::glIsEnabled(GL_CULL_FACE);
GLint prev_depth_func = GL_LESS;
glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func));
GLboolean prev_depth_mask = GL_TRUE;
glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask));
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, m_shadow_map_fbo));
glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadow_map_texture_id, 0));
glsafe(::glDrawBuffer(GL_NONE));
glsafe(::glReadBuffer(GL_NONE));
if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prev_fbo)));
m_shadow_map_valid = false;
} else {
glsafe(::glViewport(0, 0, size, size));
glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE));
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDepthFunc(GL_LESS));
glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
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);
}
shader->stop_using();
// Restore state
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
glsafe(::glColorMask(prev_color_mask[0], prev_color_mask[1], prev_color_mask[2], prev_color_mask[3]));
if (prev_cull)
glsafe(::glEnable(GL_CULL_FACE));
else
glsafe(::glDisable(GL_CULL_FACE));
glsafe(::glDepthFunc(prev_depth_func));
glsafe(::glDepthMask(prev_depth_mask));
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prev_fbo)));
glsafe(::glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]));
m_shadow_map_valid = true;
}
} else {
m_shadow_map_valid = false;
}
// ----------------------------------------------------------------------------------
// Unified plate shadow: draw the build-plate footprint and darken it wherever the same
// depth shadow map (built above) says the light is occluded. This replaces the old planar
// stencil projection so plate, object and self shadows all come from one technique.
// ----------------------------------------------------------------------------------
if (!m_shadow_map_valid)
return;
// Shadow projection matrix - flattens geometry onto Z=0 plane along light direction
Matrix4d shadow_proj = Matrix4d::Identity();
shadow_proj(0, 2) = -ray_dir.x() / ray_dir.z();
shadow_proj(1, 2) = -ray_dir.y() / ray_dir.z();
shadow_proj(2, 0) = 0.0;
shadow_proj(2, 1) = 0.0;
shadow_proj(2, 2) = 0.0;
shadow_proj(2, 3) = 0.01; // Bias to prevent shadow acne
GLShaderProgram* plate_shader = wxGetApp().get_shader("printbed_shadow");
if (plate_shader == nullptr)
return;
// Save OpenGL state
GLint prev_depth_func = GL_LESS;
glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func));
GLboolean prev_depth_mask = GL_TRUE;
glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask));
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));
// ============================================================
// PASS 0: Create stencil mask for the build plate (value = 1)
// ============================================================
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));
glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE));
glsafe(::glDisable(GL_DEPTH_TEST));
shader->start_using();
shader->set_uniform("projection_matrix", projection_matrix);
// Draw the build plate (cached model to avoid per-frame uploads)
if (const BuildVolume& build_volume = m_bed.build_volume(); build_volume.valid()) {
const std::string mask_key = build_volume.type() == BuildVolume_Type::Rectangle
? (boost::format("rect|%1$.5f|%2$.5f|%3$.5f|%4$.5f")
@@ -7977,87 +8115,49 @@ void GLCanvas3D::_render_cast_shadows_on_plate(const Transform3d& view_matrix, c
}
if (m_plate_shadow_mask.is_initialized()) {
shader->set_uniform("view_model_matrix", view_matrix);
m_plate_shadow_mask.render(shader);
// Blend the shadow over the already-drawn plate. Depth test keeps it behind anything
// already in front; depth writes are off, and a small negative polygon offset lifts it
// just above the bed to avoid z-fighting.
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(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glDepthFunc(GL_LEQUAL));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.0f, -1.0f));
glsafe(::glActiveTexture(GL_TEXTURE4));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_shadow_map_texture_id));
glsafe(::glActiveTexture(GL_TEXTURE0));
plate_shader->start_using();
plate_shader->set_uniform("view_model_matrix", view_matrix);
plate_shader->set_uniform("projection_matrix", projection_matrix);
plate_shader->set_uniform("shadow_map", 4);
plate_shader->set_uniform("shadow_light_vp", m_shadow_light_vp);
plate_shader->set_uniform("shadow_intensity", 0.35f);
plate_shader->set_uniform("shadow_map_texel", 1.0f / static_cast<float>(m_shadow_map_size));
m_plate_shadow_mask.render(plate_shader);
plate_shader->stop_using();
glsafe(::glActiveTexture(GL_TEXTURE4));
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
glsafe(::glActiveTexture(GL_TEXTURE0));
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
glsafe(::glDisable(GL_BLEND));
glsafe(::glDepthFunc(prev_depth_func));
glsafe(::glDepthMask(prev_depth_mask));
}
}
// ============================================================
// PASS 1: Project object shadows onto plate (increment stencil to 2)
// ============================================================
// Only render where plate exists (stencil == 1), then increment to 2
glsafe(::glStencilFunc(GL_EQUAL, 1, 0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_INCR));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthFunc(GL_ALWAYS)); // Shadows don't need depth testing
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.0f, -2.0f));
glsafe(::glDisable(GL_CULL_FACE));
// Render projected shadow geometry
for (GLVolume* volume : m_volumes.volumes) {
if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower)
continue;
// CRITICAL FIX: Apply shadow projection in object's local space, then to world, then to view
// This ensures shadows are cast from the object's actual position
Matrix4d world_matrix = volume->world_matrix().matrix();
// Project the shadow - this flattens the geometry onto Z=0 in WORLD space
Matrix4d shadow_world_matrix = shadow_proj * world_matrix;
// Transform to view space for rendering
Matrix4d view_shadow_matrix = view_matrix.matrix() * shadow_world_matrix;
shader->set_uniform("view_model_matrix", view_shadow_matrix);
shader->set_uniform("projection_matrix", projection_matrix);
volume->model.render(shader);
}
// ============================================================
// PASS 2: Draw shadow color where stencil == 2
// ============================================================
glsafe(::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE));
glsafe(::glStencilFunc(GL_EQUAL, 2, 0xFF));
glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP));
glsafe(::glStencilMask(0x00));
glsafe(::glDepthFunc(GL_ALWAYS));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
// Draw shadow fill
shader->set_uniform("view_model_matrix", Transform3d::Identity());
shader->set_uniform("projection_matrix", Transform3d::Identity());
const ColorRGBA shadow_fill_color(0.0f, 0.0f, 0.0f, 0.4f); // Darker shadow for visibility
const ColorRGBA prev_bg_color = m_background.get_geometry().color;
m_background.set_color(shadow_fill_color);
shader->set_uniform("uniform_color", shadow_fill_color);
m_background.render(shader);
m_background.set_color(prev_bg_color);
shader->set_uniform("uniform_color", prev_bg_color);
shader->stop_using();
// ============================================================
// RESTORE STATE
// ============================================================
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthMask(prev_depth_mask));
glsafe(::glDepthFunc(prev_depth_func));
glsafe(::glEnable(GL_CULL_FACE));
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
glsafe(::glDisable(GL_BLEND));
if (!prev_stencil_test)
glsafe(::glDisable(GL_STENCIL_TEST));
glsafe(::glStencilMask(prev_stencil_mask));
}
void GLCanvas3D::_render_plane() const
{
;//TODO render assemble plane
@@ -8142,6 +8242,20 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
const bool phong_ssao = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SSAO);
shader->set_uniform("enable_ssao", phong_ssao);
// Object-on-object and self shadows: sample the depth map built in _render_shadow_map_pass().
// shadow_intensity == 0 disables the effect entirely (unchanged behavior when off / unsupported).
if (m_shadow_map_valid && m_shadow_map_texture_id != 0) {
glsafe(::glActiveTexture(GL_TEXTURE4));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_shadow_map_texture_id));
glsafe(::glActiveTexture(GL_TEXTURE0));
shader->set_uniform("shadow_map", 4);
shader->set_uniform("shadow_light_vp", m_shadow_light_vp);
shader->set_uniform("shadow_map_texel", 1.0f / static_cast<float>(m_shadow_map_size));
shader->set_uniform("shadow_intensity", 0.35f);
}
else
shader->set_uniform("shadow_intensity", 0.0f);
const Size& cvn_size = get_canvas_size();
{
const Camera& camera = wxGetApp().plater()->get_camera();
@@ -8233,6 +8347,12 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
shader->set_uniform("show_wireframe", false);
}*/
if (m_shadow_map_valid && m_shadow_map_texture_id != 0) {
glsafe(::glActiveTexture(GL_TEXTURE4));
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
glsafe(::glActiveTexture(GL_TEXTURE0));
}
shader->stop_using();
}

View File

@@ -732,6 +732,12 @@ public:
std::array<unsigned int, 2> m_ssao_texture_size{ { 0, 0 } };
GLModel m_plate_shadow_mask;
std::string m_plate_shadow_mask_key;
// Depth-based shadow map used to cast object shadows onto other objects and themselves.
unsigned int m_shadow_map_fbo{ 0 };
unsigned int m_shadow_map_texture_id{ 0 };
unsigned int m_shadow_map_size{ 0 };
Transform3d m_shadow_light_vp{ Transform3d::Identity() };
bool m_shadow_map_valid{ false };
public:
explicit GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed);
~GLCanvas3D();
@@ -1251,7 +1257,9 @@ private:
void _render_ssao_pass(unsigned int width, unsigned int height);
void _render_background();
void _render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes);
void _render_cast_shadows_on_plate(const Transform3d& view_matrix, const Transform3d& projection_matrix);
// Build the light-space depth shadow map (consumed by gouraud/phong for object & self shadows)
// and cast it onto the build plate. Realistic view only.
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add part plate related logic
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
//BBS: add outline drawing logic

View File

@@ -68,6 +68,10 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("thumbnail", { prefix + "thumbnail.vs", prefix + "thumbnail.fs"});
// used to render printbed
valid &= append_shader("printbed", { prefix + "printbed.vs", prefix + "printbed.fs" });
#if !SLIC3R_OPENGL_ES
// used to cast the object shadow map onto the build plate (realistic view)
valid &= append_shader("printbed_shadow", { prefix + "printbed_shadow.vs", prefix + "printbed_shadow.fs" });
#endif // !SLIC3R_OPENGL_ES
valid &= append_shader("hotbed", {prefix + "hotbed.vs", prefix + "hotbed.fs"});
// used to render options in gcode preview
if (GUI::wxGetApp().is_gl_version_greater_or_equal_to(3, 3)) {

View File

@@ -1827,7 +1827,7 @@ void PreferencesDialog::create_items()
auto item_realistic_shadows = create_item_checkbox(
_L("Shadows"),
_L("Renders cast shadows on the plate in realistic view."),
_L("Renders cast shadows on the plate, other objects, and each object onto itself in realistic view."),
SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS
);
g_sizer->Add(item_realistic_shadows);