Design tab: studio lighting and body edges in the 3D view

Reported: once extruded, a part is hard to read; the lighting says
little about its shape. Both lights of the object shaders sit near the
camera, so the sides of a part come out in almost the same tone, and
nothing marks where one face ends and the next begins.

- The phong shader gains a studio lighting model, chosen by a new
  lighting_model uniform: a sky/ground hemisphere in world space (up
  faces cool and bright, down faces warm and dark), a key light from the
  upper left and a weak fill from the right, a plastic-like highlight,
  and a darker base with a faint sheen toward the silhouette so curved
  faces read as round. GLCanvas3D::set_studio_lighting() makes a canvas
  draw its objects with it whatever the realistic-view preferences; only
  the Design canvas turns it on. Every canvas sets the uniform on each
  use, 0 for the slicer's, so they render as before.
- Every B-rep edge of a body is drawn as a thin dark line over it, depth
  tested and pulled a few pixels toward the eye so it wins against the
  faces meeting at it and hides behind the faces in front. The seam of a
  closed surface and degenerate edges are left out
  (GeometryEngine::display_edges). Edges are sampled once per shape and
  kept across recomputes that leave a body unchanged; bodies faded by
  body focus get fainter edges, and a dress-up previewing its result
  alone hides them with the bodies.

Tests: display_edges gives a box its 12 edges at their lengths, a
cylinder its two round rims without the seam, a cone its base rim
without the seam or the apex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
This commit is contained in:
Claude
2026-09-30 08:34:46 +00:00
parent b060101707
commit b8d45c6bb1
11 changed files with 237 additions and 5 deletions
+22
View File
@@ -17,6 +17,7 @@
#include <TopTools_IndexedMapOfShape.hxx>
#include <TopTools_ListOfShape.hxx>
#include <TopTools_IndexedDataMapOfShapeListOfShape.hxx>
#include <TopTools_ListIteratorOfListOfShape.hxx>
#include <Poly_Triangulation.hxx>
#include <gp_Ax2.hxx>
#include <gp_Dir.hxx>
@@ -611,6 +612,27 @@ std::vector<Vec3d> GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, do
return pts;
}
std::vector<std::vector<Vec3d>> GeometryEngine::display_edges(const TopoDS_Shape& shape, double chord_tol)
{
TopTools_IndexedDataMapOfShapeListOfShape faces_of_edge;
TopExp::MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, faces_of_edge);
std::vector<std::vector<Vec3d>> out;
for (int i = 1; i <= faces_of_edge.Extent(); ++i) {
const TopoDS_Edge& edge = TopoDS::Edge(faces_of_edge.FindKey(i));
if (BRep_Tool::Degenerated(edge))
continue;
bool seam = false;
for (TopTools_ListIteratorOfListOfShape it(faces_of_edge.FindFromIndex(i)); it.More() && !seam; it.Next())
seam = BRep_Tool::IsClosed(edge, TopoDS::Face(it.Value()));
if (seam)
continue;
std::vector<Vec3d> pts = sample_edge_world(edge, chord_tol);
if (pts.size() >= 2)
out.push_back(std::move(pts));
}
return out;
}
Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face)
{
GProp_GProps props;
+4
View File
@@ -149,6 +149,10 @@ public:
static Vec3d face_normal_world(const TopoDS_Face& face);
// Sample an edge into a world-space polyline (>=2 pts) for pick-distance + highlight.
static std::vector<Vec3d> sample_edge_world(const TopoDS_Edge& edge, double chord_tol = 0.05);
// The edges a viewer draws over a body, each as a polyline: every edge of the shape once,
// without degenerate edges (a cone apex) and without the seam of a closed surface (the line
// down a cylinder's side), which is where OCCT closes the parameter space, not a real edge.
static std::vector<std::vector<Vec3d>> display_edges(const TopoDS_Shape& shape, double chord_tol);
// 0-based edge index into TopExp::MapShapes(shape, TopAbs_EDGE, map).
static int edge_count(const TopoDS_Shape& shape);
static TopoDS_Edge edge_by_index(const TopoDS_Shape& shape, int index);
+2
View File
@@ -46,6 +46,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// cannot be null. Borrow the plater's, as the editor canvases do.
m_canvas->set_process(&wxGetApp().plater()->background_process());
m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D);
m_canvas->set_studio_lighting(true); // see GLCanvas3D::m_studio_lighting and phong.fs
// CAD navigation, this canvas only: left-drag sweeps a selection rubber band, so orbit
// moves to middle-drag and pan to right-drag. Design is a different modality from
@@ -1347,6 +1348,7 @@ void DesignCanvas::set_body_hidden(bool on)
{
if (m_body_hidden == on) return;
m_body_hidden = on;
m_sketch_tool.set_body_edges_hidden(on);
reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
}
+91
View File
@@ -14,6 +14,10 @@
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLShader.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <BRepBndLib.hxx>
#include <Bnd_Box.hxx>
#include <Standard_Failure.hxx>
#include "libslic3r/TriangleMesh.hpp"
#include <GL/glew.h>
@@ -3424,9 +3428,94 @@ void DesignSketchTool::set_solid_pick(const std::vector<CadBody>* bodies, const
m_solid_bodies = bodies; m_solid_mesh = mesh; m_solid_tri_face = tri_face; m_solid_tri_body = tri_body;
m_solid_visible = visible; m_solid_xform = xform;
}
refresh_body_edges();
clear_solid_selection();
}
void DesignSketchTool::refresh_body_edges()
{
const size_t n = m_solid_bodies != nullptr ? m_solid_bodies->size() : 0;
m_body_edges.resize(n);
m_body_edges_key.resize(n, nullptr);
for (size_t b = 0; b < n; ++b) {
const TopoDS_Shape& shape = (*m_solid_bodies)[b].shape;
const void* key = shape.IsNull() ? nullptr : shape.TShape().get();
if (key == m_body_edges_key[b] && key != nullptr)
continue;
m_body_edges_key[b] = key;
m_body_edges[b].clear();
if (key == nullptr)
continue;
// A thousandth of the body's size: round edges stay round at any zoom that shows the
// whole body, without sampling a large import into millions of segments.
Bnd_Box box;
BRepBndLib::Add(shape, box);
const double diag = box.IsVoid() ? 1.0 : std::sqrt(box.SquareExtent());
try {
m_body_edges[b] = GeometryEngine::display_edges(shape, std::max(1e-3 * diag, 0.005));
} catch (const Standard_Failure&) {
m_body_edges[b].clear(); // an unsampleable edge costs its body the lines, nothing else
}
}
}
void DesignSketchTool::render_body_edges()
{
if (m_body_edges_hidden || m_solid_bodies == nullptr)
return;
using EPT = GLModel::Geometry::EPrimitiveType;
using EVL = GLModel::Geometry::EVertexLayout;
const Camera& cam = wxGetApp().plater()->get_camera();
const Vec3d vd = cam.get_dir_forward();
const double px = 1.0 / std::max(cam.get_zoom(), 1e-6);
const double hw = 0.75 * px; // ~1.5 px wide
// Pulled toward the eye by a few pixels, so the line wins the depth test against the two
// faces meeting at the edge while a face in front of it still hides it.
const Vec3d pull = -vd * (3.0 * px);
// Two passes: the edges of a body faded by body focus are fainter, like the body itself.
for (int pass = 0; pass < 2; ++pass) {
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
unsigned int base = 0;
for (int b = 0; b < int(m_body_edges.size()); ++b) {
if (m_solid_visible != nullptr && b < int(m_solid_visible->size()) && !(*m_solid_visible)[b])
continue;
const bool faded = m_pick_only_body >= 0 && m_pick_only_body < int(m_body_edges.size())
&& b != m_pick_only_body;
if (faded != (pass == 1))
continue;
for (const std::vector<Vec3d>& pl : m_body_edges[b])
for (size_t s = 1; s < pl.size(); ++s) {
const Vec3d a = body_xform_pt(b, pl[s - 1]) + pull, c = body_xform_pt(b, pl[s]) + pull;
Vec3d dir = c - a; if (dir.norm() < 1e-9) continue; dir.normalize();
Vec3d off = dir.cross(vd);
if (off.norm() < 1e-9) continue; // edge seen end-on: a point, nothing to draw
off = off.normalized() * hw;
g.add_vertex((Vec3f)(a + off).cast<float>());
g.add_vertex((Vec3f)(c + off).cast<float>());
g.add_vertex((Vec3f)(c - off).cast<float>());
g.add_vertex((Vec3f)(a - off).cast<float>());
g.add_triangle(base, base + 1, base + 2);
g.add_triangle(base, base + 2, base + 3); base += 4;
}
}
if (base == 0)
continue;
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glDepthFunc(GL_LEQUAL));
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
m_body_edges_model.reset();
m_body_edges_model.init_from(std::move(g));
m_body_edges_model.set_color(ColorRGBA(0.08f, 0.09f, 0.11f, pass == 0 ? 0.85f : 0.25f));
m_body_edges_model.render();
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDepthFunc(GL_LESS));
glsafe(::glDisable(GL_BLEND));
glsafe(::glDisable(GL_DEPTH_TEST));
}
}
// Map a point sampled from the (untransformed) OCCT body shape through the body's display
// transform, so edge picking/highlight track a moved body. The pick MESH is already
// transformed by the host; only OCCT-sampled edges need this.
@@ -8929,6 +9018,8 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
shader->set_uniform("view_model_matrix", camera.get_view_matrix());
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
render_body_edges();
// Persistent committed sketches (e.g. an un-consumed sketch left visible after its
// extrude is removed): faces translucent, outlines orange. Each uses its own plane.
if (!m_display_sketches.empty()) {
+12
View File
@@ -198,6 +198,9 @@ public:
// another solid is reachable without hiding anything. -1 = no restriction.
// Survives set_solid_pick() — it is owned by the panel, not by the mesh feed.
void set_pick_only_body(int b) { m_pick_only_body = b; }
// Off while the bodies themselves are hidden (a dress-up previewing its result alone), so
// their edges do not float over the preview.
void set_body_edges_hidden(bool h) { m_body_edges_hidden = h; }
void clear_solid_selection();
bool has_solid_selection() const { return m_solid_sel != SolidSel::None; }
// Every picked edge when the selection is an edge set (Shift/Ctrl+click adds and removes
@@ -1204,6 +1207,14 @@ private:
int m_pick_only_body{-1}; // >=0: only this body catches clicks (body-focus x-ray for CoordSys picking)
const std::vector<Transform3d>* m_solid_xform{nullptr}; // per-body display transform (for edge sampling)
Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform
// The bodies' B-rep edges, drawn as dark lines over the solids so faces and features read
// apart. One polyline set per body in its own shape coordinates, resampled only for a body
// whose shape changed (keyed by the TShape), since set_solid_pick runs on every recompute.
std::vector<std::vector<std::vector<Vec3d>>> m_body_edges;
std::vector<const void*> m_body_edges_key;
bool m_body_edges_hidden{false};
void refresh_body_edges();
void render_body_edges();
bool body_pickable(int b) const; // false when the body is explicitly hidden
SolidSel m_solid_sel{SolidSel::None};
int m_sel_body{-1}; // which body the face/edge selection is on
@@ -1269,6 +1280,7 @@ private:
GLModel m_mc_fill_model; // the face treatment's shaded facets
GLModel m_solid_face_model;
GLModel m_solid_edge_model;
GLModel m_body_edges_model;
GLModel m_solid_vertex_model;
int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none)
+9 -1
View File
@@ -8489,10 +8489,11 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
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";
const std::string shader_name = (m_studio_lighting || (realistic_mode && realistic_phong)) ? "phong" : "gouraud";
GLShaderProgram* shader = wxGetApp().get_shader(shader_name);
if (shader == nullptr && shader_name != "gouraud")
shader = wxGetApp().get_shader("gouraud");
const bool studio = m_studio_lighting && shader != nullptr && shader->get_name() == "phong";
ECanvasType canvas_type = this->m_canvas_type;
bool partly_inside_enable = canvas_type == ECanvasType::CanvasAssembleView ? false : true;
// The edited printer's per-extruder printable heights feed the object shader's
@@ -8505,6 +8506,13 @@ 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);
// Set on every use: the program is shared, so a canvas that leaves it unset would inherit
// the last canvas's choice.
shader->set_uniform("lighting_model", studio ? 1 : 0);
if (studio) {
const Transform3d& view = wxGetApp().plater()->get_camera().get_view_matrix();
shader->set_uniform("world_up_eye", Vec3f((view.matrix().block<3, 3>(0, 0) * Vec3d::UnitZ()).cast<float>()));
}
// 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).
+4
View File
@@ -590,6 +590,9 @@ private:
//BBS: add canvas type for assemble view usage
ECanvasType m_canvas_type;
// Objects drawn with the phong shader's studio lighting whatever the realistic-view settings
// (the Design tab's canvas). Off for every canvas of the slicer, which render as before.
bool m_studio_lighting{false};
std::array<ClippingPlane, 2> m_clipping_planes;
ClippingPlane m_camera_clipping_plane;
bool m_use_clipping_planes;
@@ -815,6 +818,7 @@ public:
void set_context(wxGLContext* context) { m_context = context; }
void set_type(ECanvasType type) { m_canvas_type = type; }
void set_studio_lighting(bool on) { m_studio_lighting = on; }
ECanvasType get_canvas_type() { return m_canvas_type; }
wxGLCanvas* get_wxglcanvas() { return m_canvas; }