From bfe458507bc05007c32f3e208501fdf471e89819 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Mon, 21 Sep 2026 14:23:32 -0300 Subject: [PATCH] Add X-Ray view mode to 3D viewport (#15770) --- resources/shaders/110/xray.fs | 28 ++++++++++++++ resources/shaders/110/xray.vs | 31 +++++++++++++++ resources/shaders/140/xray.fs | 30 +++++++++++++++ resources/shaders/140/xray.vs | 31 +++++++++++++++ src/slic3r/GUI/3DScene.cpp | 6 ++- src/slic3r/GUI/GLCanvas3D.cpp | 60 +++++++++++++++++++++++++++++ src/slic3r/GUI/GLCanvas3D.hpp | 2 + src/slic3r/GUI/GLShadersManager.cpp | 2 + src/slic3r/GUI/Plater.cpp | 11 ++++++ src/slic3r/GUI/Plater.hpp | 3 ++ 10 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 resources/shaders/110/xray.fs create mode 100644 resources/shaders/110/xray.vs create mode 100644 resources/shaders/140/xray.fs create mode 100644 resources/shaders/140/xray.vs diff --git a/resources/shaders/110/xray.fs b/resources/shaders/110/xray.fs new file mode 100644 index 0000000000..27bdb17044 --- /dev/null +++ b/resources/shaders/110/xray.fs @@ -0,0 +1,28 @@ +#version 110 + +// X-Ray view: each surface is a thin translucent layer of its own color, covering more the more +// edge-on it is seen. The caller draws it with depth writes off, so every surface a view ray crosses +// composites and overlapping geometry reads as denser. Source-alpha rather than additive blending: +// additive would saturate to white on the light theme's near-white background. +#define EDGE_FALLOFF 2.0 +// Coverage head-on and edge-on. +#define FACE_DENSITY 0.12 +#define EDGE_DENSITY 0.65 + +uniform vec4 uniform_color; + +varying vec3 eye_normal; +varying vec3 eye_position; +varying vec3 clipping_planes_dots; + +void main() +{ + if (any(lessThan(clipping_planes_dots, vec3(0.0)))) + discard; + + // abs(): back faces are drawn too, and there only the angle matters. + float facing = abs(dot(normalize(eye_normal), normalize(-eye_position))); + float density = mix(EDGE_DENSITY, FACE_DENSITY, pow(facing, EDGE_FALLOFF)); + + gl_FragColor = vec4(uniform_color.rgb, density * uniform_color.a); +} diff --git a/resources/shaders/110/xray.vs b/resources/shaders/110/xray.vs new file mode 100644 index 0000000000..85f932f2df --- /dev/null +++ b/resources/shaders/110/xray.vs @@ -0,0 +1,31 @@ +#version 110 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; + +// Clipping, as in gouraud.vs: the preview's top / bottom plane and the SLA gizmo's free one. +uniform vec2 z_range; +uniform vec4 clipping_plane; + +attribute vec3 v_position; +attribute vec3 v_normal; + +varying vec3 eye_normal; +varying vec3 eye_position; +varying vec3 clipping_planes_dots; + +void main() +{ + eye_normal = view_normal_matrix * v_normal; + + vec4 position = view_model_matrix * vec4(v_position, 1.0); + eye_position = position.xyz; + + // A component below zero means the fragment is clipped away. + vec4 world_pos = volume_world_matrix * vec4(v_position, 1.0); + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); + + gl_Position = projection_matrix * position; +} diff --git a/resources/shaders/140/xray.fs b/resources/shaders/140/xray.fs new file mode 100644 index 0000000000..71c3569f8e --- /dev/null +++ b/resources/shaders/140/xray.fs @@ -0,0 +1,30 @@ +#version 140 + +// X-Ray view: each surface is a thin translucent layer of its own color, covering more the more +// edge-on it is seen. The caller draws it with depth writes off, so every surface a view ray crosses +// composites and overlapping geometry reads as denser. Source-alpha rather than additive blending: +// additive would saturate to white on the light theme's near-white background. +#define EDGE_FALLOFF 2.0 +// Coverage head-on and edge-on. +#define FACE_DENSITY 0.12 +#define EDGE_DENSITY 0.65 + +uniform vec4 uniform_color; + +in vec3 eye_normal; +in vec3 eye_position; +in vec3 clipping_planes_dots; + +out vec4 out_color; + +void main() +{ + if (any(lessThan(clipping_planes_dots, vec3(0.0)))) + discard; + + // abs(): back faces are drawn too, and there only the angle matters. + float facing = abs(dot(normalize(eye_normal), normalize(-eye_position))); + float density = mix(EDGE_DENSITY, FACE_DENSITY, pow(facing, EDGE_FALLOFF)); + + out_color = vec4(uniform_color.rgb, density * uniform_color.a); +} diff --git a/resources/shaders/140/xray.vs b/resources/shaders/140/xray.vs new file mode 100644 index 0000000000..9cb0d64b32 --- /dev/null +++ b/resources/shaders/140/xray.vs @@ -0,0 +1,31 @@ +#version 140 + +uniform mat4 view_model_matrix; +uniform mat4 projection_matrix; +uniform mat3 view_normal_matrix; +uniform mat4 volume_world_matrix; + +// Clipping, as in gouraud.vs: the preview's top / bottom plane and the SLA gizmo's free one. +uniform vec2 z_range; +uniform vec4 clipping_plane; + +in vec3 v_position; +in vec3 v_normal; + +out vec3 eye_normal; +out vec3 eye_position; +out vec3 clipping_planes_dots; + +void main() +{ + eye_normal = view_normal_matrix * v_normal; + + vec4 position = view_model_matrix * vec4(v_position, 1.0); + eye_position = position.xyz; + + // A component below zero means the fragment is clipped away. + vec4 world_pos = volume_world_matrix * vec4(v_position, 1.0); + clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z); + + gl_Position = projection_matrix * position; +} diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index fd23337b44..bf3cdc5303 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1157,6 +1157,10 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, const float support_normal_z = get_selection_support_normal_z(); + // The outline passes below are driven by is_outline, which only the object shaders have; with an + // overlay one (wireframe, x-ray) bound they would just draw the volume again. + const bool shader_can_outline = shader->get_uniform_location("is_outline") >= 0; + // 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); @@ -1257,7 +1261,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); //BBS: add outline related logic - if (volume.first->selected && GUI::wxGetApp().show_outline()) + if (volume.first->selected && shader_can_outline && GUI::wxGetApp().show_outline()) volume.first->render_with_outline(cnv_size); else volume.first->render(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 02ef95e2c6..19bc6eb98a 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -8758,6 +8758,15 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with else m_volumes.set_show_sinking_contours(!m_gizmos.is_hiding_instances()); + // Orca: X-Ray replaces both shaded passes with a single one, driven from the opaque call, and + // reuses the volume state (print volume, z range, clipping plane) set up above. + if (_is_xray_view_active()) { + if (type == GLVolumeCollection::ERenderType::Opaque) + _render_xray_volumes(); + m_camera_clipping_plane = ClippingPlane::ClipsNothing(); + return; + } + const bool realistic_mode = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE); 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"; @@ -8894,6 +8903,51 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with m_camera_clipping_plane = ClippingPlane::ClipsNothing(); } +bool GLCanvas3D::_is_xray_view_active() const +{ + if (m_canvas_type == ECanvasType::CanvasPreview || !wxGetApp().plater()->is_show_xray()) + return false; + + // A painting gizmo and the layer height editor draw the object through their own shaders, and + // would lose what they draw if X-Ray took the pass over. + if (dynamic_cast(m_gizmos.get_current()) != nullptr) + return false; + return !(m_picking_enabled && m_layers_editing.is_enabled() && m_layers_editing.last_object_id != -1 && + m_layers_editing.object_max_z() > 0.0f); +} + +void GLCanvas3D::_render_xray_volumes() +{ + if (m_volumes.empty()) + return; + + GLShaderProgram* shader = wxGetApp().get_shader("xray"); + if (shader == nullptr) + return; + + const Camera& camera = wxGetApp().plater()->get_camera(); + const ECanvasType canvas_type = m_canvas_type; + + // Depth writes off so every surface along a view ray composites, not just the nearest one, and + // back faces kept for the far wall of a hollow part. The low, near-uniform coverage the shader + // emits saturates towards the volume color whatever the order, so the volumes are not sorted. + glsafe(::glDepthMask(GL_FALSE)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + shader->start_using(); + m_volumes.render(GLVolumeCollection::ERenderType::All, true, camera.get_view_matrix(), camera.get_projection_matrix(), + get_canvas_size(), [this, canvas_type](const GLVolume& volume) { + if (canvas_type == ECanvasType::CanvasAssembleView) + return !volume.is_modifier && !volume.is_wipe_tower; + return m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0; + }); + shader->stop_using(); + + glsafe(::glDisable(GL_BLEND)); + glsafe(::glDepthMask(GL_TRUE)); +} + void GLCanvas3D::_render_wireframe_overlay() { if (!wxGetApp().plater()->is_show_wireframe()) @@ -10020,6 +10074,12 @@ void GLCanvas3D::_render_canvas_toolbar() [this, p]{p->toggle_show_wireframe(); m_dirty = true;} ); + create_menu_item( _utf8(L("X-Ray")), + m_canvas_type != ECanvasType::CanvasPreview, // not work on preview + p->is_show_xray(), + [this, p]{p->toggle_show_xray(); m_dirty = true;} + ); + create_menu_item( _utf8(L("Realistic View")), m_canvas_type != ECanvasType::CanvasPreview, // not work on preview cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE), diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index fe6108d39f..9c78581f69 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1333,6 +1333,8 @@ private: //BBS: add outline drawing logic void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true); void _render_wireframe_overlay(); + bool _is_xray_view_active() const; + void _render_xray_volumes(); //BBS: GUI refactor: add canvas size as parameters void _render_gcode(int canvas_width, int canvas_height); void _render_gcode_overlay(int canvas_width, int canvas_height); diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index 2b8a0edf0a..b35d598829 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -90,6 +90,8 @@ std::pair GLShadersManager::init() , { "ENABLE_ENVIRONMENT_MAP"sv } #endif // ENABLE_ENVIRONMENT_MAP ); + // used to render objects as translucent, edge weighted surfaces in the X-Ray view + valid &= append_shader("xray", { prefix + "xray.vs", prefix + "xray.fs" }); // used to render variable layers heights in 3d editor valid &= append_shader("variable_layer_height", { prefix + "variable_layer_height.vs", prefix + "variable_layer_height.fs" }); // used to render highlight contour around selected triangles inside the multi-material gizmo diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 502265af00..d2196fefe6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6827,6 +6827,7 @@ struct Plater::priv bool show_render_statistic_dialog{ false }; bool show_wireframe{ false }; bool wireframe_enabled{ true }; + bool show_xray{ false }; static const std::regex pattern_bundle; static const std::regex pattern_3mf; @@ -22399,6 +22400,16 @@ bool Plater::is_wireframe_enabled() const return p->wireframe_enabled; } +void Plater::toggle_show_xray() +{ + p->show_xray = !p->show_xray; +} + +bool Plater::is_show_xray() const +{ + return p->show_xray; +} + /*Plater::TakeSnapshot::TakeSnapshot(Plater *plater, const std::string &snapshot_name) : TakeSnapshot(plater, from_u8(snapshot_name)) {} diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 1474356e45..ae70a6a0bb 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -956,6 +956,9 @@ public: void enable_wireframe(bool status); bool is_wireframe_enabled() const; + void toggle_show_xray(); + bool is_show_xray() const; + // Wrapper around wxWindow::PopupMenu to suppress error messages popping out while tracking the popup menu. bool PopupMenu(wxMenu *menu, const wxPoint& pos = wxDefaultPosition); bool PopupMenu(wxMenu *menu, int x, int y) { return this->PopupMenu(menu, wxPoint(x, y)); }