Add X-Ray view mode to 3D viewport (#15770)

This commit is contained in:
Ian Bassi
2026-09-21 14:23:32 -03:00
committed by GitHub
parent b6bb1b92c9
commit bfe458507b
10 changed files with 203 additions and 1 deletions
+28
View File
@@ -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);
}
+31
View File
@@ -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;
}
+30
View File
@@ -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);
}
+31
View File
@@ -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;
}
+5 -1
View File
@@ -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();
+60
View File
@@ -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<const GLGizmoPainterBase*>(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),
+2
View File
@@ -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);
+2
View File
@@ -90,6 +90,8 @@ std::pair<bool, std::string> 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
+11
View File
@@ -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)) {}
+3
View File
@@ -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)); }