From 63d00f7cf1ec4a81c56b8fa84ae58ad162d429e5 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 13:10:09 +0800 Subject: [PATCH] Keep Only What the Camera's Side Can See in the Layers Skipped at Rest With the shell at rest and layers merged, the skipped layers kept every segment exposed to the layer next to it. On the tall fixture that was 25000 wipe tower segments over its whole height: wherever the tower's footprint edge lands one grid cell differently from one layer to the next, the cell reads as uncovered, though hundreds of layers sit on top. The same test also kept whatever lies under an overhang. The classification now records the highest and lowest layer occupying each cell over the whole print, in the pass that already rasterizes the layers, and a segment is visible from above when its layer is the topmost occupant of at least half its cells, and from below likewise with the bottommost. A skipped layer at rest keeps only the segments visible from the side the camera is on, which the viewer is told along with the stride. The exposed bit stays for the dragging set, which is not rebuilt with the camera. On the tall fixture with the shell at rest, against 334 ms at full detail: default view 17679 segments 18 ms (was 37585 / 85 ms) from above 7935 segments 7 ms (was 28682 / 58 ms) The two-cube fixture still keeps every facing wall, and the classification stays at 90 ms for 636 layers with the maps merged over each worker's touched rectangle only. --- src/libvgcode/include/Viewer.hpp | 6 ++ src/libvgcode/src/Settings.hpp | 3 + src/libvgcode/src/Viewer.cpp | 10 +++ src/libvgcode/src/ViewerImpl.cpp | 105 +++++++++++++++++++++++++++++-- src/libvgcode/src/ViewerImpl.hpp | 6 ++ src/slic3r/GUI/GCodeViewer.cpp | 1 + 6 files changed, 127 insertions(+), 4 deletions(-) diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index f80d07418b..77de1c972d 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -125,6 +125,12 @@ public: // uint32_t get_rest_layer_stride() const; void set_rest_layer_stride(uint32_t value); + // + // ORCA: whether the camera is above the print. With layers skipped at rest, only the surfaces + // that side of the print can see are kept in the skipped layers. + // + bool is_rest_view_from_above() const; + void set_rest_view_from_above(bool value); float get_dim_previous_layers_brightness() const; void set_dim_previous_layers_brightness(float value); // diff --git a/src/libvgcode/src/Settings.hpp b/src/libvgcode/src/Settings.hpp index 2dca1cd486..154ab2896a 100644 --- a/src/libvgcode/src/Settings.hpp +++ b/src/libvgcode/src/Settings.hpp @@ -40,6 +40,9 @@ struct Settings // tall; the exposed surfaces of every layer stay. Meant to follow how many layers fit in a // pixel at the current view, so that it changes nothing visible. uint32_t rest_layer_stride{ 1 }; + // ORCA: whether the camera is above the print; decides whether the surfaces kept in the + // skipped layers at rest are the ones visible from above or from below + bool rest_view_from_above{ true }; // // Required update flags // diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index 06e7530bb8..0c30a7baad 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -127,6 +127,16 @@ void Viewer::set_rest_layer_stride(uint32_t value) m_impl->set_rest_layer_stride(value); } +bool Viewer::is_rest_view_from_above() const +{ + return m_impl->is_rest_view_from_above(); +} + +void Viewer::set_rest_view_from_above(bool value) +{ + m_impl->set_rest_view_from_above(value); +} + void Viewer::set_dim_previous_layers(bool value) { m_impl->set_dim_previous_layers(value); diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index b1a8849928..f216267160 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -900,6 +900,8 @@ void ViewerImpl::reset() m_enabled_segments_rest_count = 0; m_shell_bitset = BitSet<>(); m_exposed_bitset = BitSet<>(); + m_top_visible_bitset = BitSet<>(); + m_bottom_visible_bitset = BitSet<>(); m_settings_used_for_ranges = std::nullopt; @@ -1403,10 +1405,16 @@ static void close_gaps(OccupancyGrid& grid, int radius, ClosingScratch& scratch) // is missing. A segment is kept when at least half of the cells it crosses are shell cells: walls // run along the shell, infill only touches it at the ends. Purely geometric, so it works as well // for the wipe tower, whose every segment shares one role, as for the objects. +// The same pass records the highest and lowest layer occupying each cell over the whole print, +// which tells the segments that are the topmost or bottommost thing at their place: exposure to the +// next layer alone would also keep whatever sits under an overhang, and the edge of a tower whose +// footprint lands a cell differently from one layer to the next. void ViewerImpl::update_shell_bitset() { m_shell_bitset = BitSet<>(m_vertices.size()); m_exposed_bitset = BitSet<>(m_vertices.size()); + m_top_visible_bitset = BitSet<>(m_vertices.size()); + m_bottom_visible_bitset = BitSet<>(m_vertices.size()); if (m_vertices.size() < 2 || m_layers.empty()) return; @@ -1438,6 +1446,7 @@ void ViewerImpl::update_shell_bitset() const int nx = static_cast((max_x - min_x) / cell) + 1 + 2 * margin; const int ny = static_cast((max_y - min_y) / cell) + 1 + 2 * margin; + const auto cell_index = [nx](int x, int y) { return static_cast(y) * nx + x; }; const auto cell_of = [&](float x, float y) { const int cx = std::clamp(static_cast((x - origin_x) / cell), margin, nx - 1 - margin); const int cy = std::clamp(static_cast((y - origin_y) / cell), margin, ny - 1 - margin); @@ -1476,10 +1485,25 @@ void ViewerImpl::update_shell_bitset() const OccupancyGrid nothing(nx, ny); // Classifies the layers in [first_layer, last_layer) and returns the segments kept, and among - // them the exposed ones. Each call owns its grids, so the layer range can be split across threads. - struct Kept { std::vector shell; std::vector exposed; }; + // them the exposed ones, plus the highest and lowest of these layers occupying each cell. Each + // call owns its grids, so the layer range can be split across threads. + static constexpr int32_t NO_LAYER = -1; + struct Kept { + std::vector shell; + std::vector exposed; + std::vector top; + std::vector bottom; + // the rectangle of cells these layers touched, inclusive; empty while min > max + int min_x{ 0 }; + int min_y{ 0 }; + int max_x{ -1 }; + int max_y{ -1 }; + }; + const size_t cells_count = static_cast(nx) * static_cast(ny); const auto classify_layers = [&](size_t first_layer, size_t last_layer) { Kept kept; + kept.top.assign(cells_count, NO_LAYER); + kept.bottom.assign(cells_count, NO_LAYER); std::vector footprints(3, OccupancyGrid(nx, ny)); OccupancyGrid shell_cells(nx, ny); OccupancyGrid exposed_cells(nx, ny); @@ -1508,10 +1532,22 @@ void ViewerImpl::update_shell_bitset() shell_cells.clear(); exposed_cells.clear(); + if (!cur.empty()) { + kept.min_x = (kept.max_x < kept.min_x) ? cur.min_x : std::min(kept.min_x, cur.min_x); + kept.min_y = (kept.max_y < kept.min_y) ? cur.min_y : std::min(kept.min_y, cur.min_y); + kept.max_x = std::max(kept.max_x, cur.max_x); + kept.max_y = std::max(kept.max_y, cur.max_y); + } for (int y = cur.min_y; y <= cur.max_y; ++y) { for (int x = cur.min_x; x <= cur.max_x; ++x) { if (!cur.at(x, y)) continue; + // layers come in ascending order, so the first occupant is the lowest + int32_t& top = kept.top[cell_index(x, y)]; + int32_t& bottom = kept.bottom[cell_index(x, y)]; + top = static_cast(layer); + if (bottom == NO_LAYER) + bottom = static_cast(layer); const bool exposed = !below.at(x, y) || !above.at(x, y); if (exposed || !cur.at(x - 1, y) || !cur.at(x + 1, y) || !cur.at(x, y - 1) || !cur.at(x, y + 1)) shell_cells.set(x, y); @@ -1546,12 +1582,60 @@ void ViewerImpl::update_shell_bitset() std::vector> futures; for (size_t first = 0; first < layers_count; first += chunk) futures.emplace_back(std::async(std::launch::async, classify_layers, first, std::min(layers_count, first + chunk))); + std::vector top_layer(cells_count, NO_LAYER); + std::vector bottom_layer(cells_count, NO_LAYER); for (auto& f : futures) { const Kept kept = f.get(); for (uint32_t i : kept.shell) m_shell_bitset.set(i); for (uint32_t i : kept.exposed) m_exposed_bitset.set(i); + for (int y = kept.min_y; y <= kept.max_y; ++y) { + for (int x = kept.min_x; x <= kept.max_x; ++x) { + const size_t c = cell_index(x, y); + if (kept.top[c] == NO_LAYER) + continue; + top_layer[c] = std::max(top_layer[c], kept.top[c]); + bottom_layer[c] = (bottom_layer[c] == NO_LAYER) ? kept.bottom[c] : std::min(bottom_layer[c], kept.bottom[c]); + } + } + } + + // A segment is visible from straight above when its layer is the topmost occupant of at least + // half its cells, and from below likewise with the bottommost. + struct Visible { std::vector top; std::vector bottom; }; + const auto find_visible = [&](size_t first_layer, size_t last_layer) { + Visible visible; + for (size_t layer = first_layer; layer < last_layer; ++layer) { + const auto [first, last] = layer_segments(layer); + for (size_t i = first; i < last; ++i) { + if (!is_drawn_extrusion(i)) + continue; + int total = 0; + int on_top = 0; + int on_bottom = 0; + for_each_cell(i, [&](int x, int y) { + ++total; + on_top += top_layer[cell_index(x, y)] == static_cast(layer); + on_bottom += bottom_layer[cell_index(x, y)] == static_cast(layer); + }); + if (2 * on_top >= total) + visible.top.push_back(static_cast(i)); + if (2 * on_bottom >= total) + visible.bottom.push_back(static_cast(i)); + } + } + return visible; + }; + std::vector> visible_futures; + for (size_t first = 0; first < layers_count; first += chunk) + visible_futures.emplace_back(std::async(std::launch::async, find_visible, first, std::min(layers_count, first + chunk))); + for (auto& f : visible_futures) { + const Visible visible = f.get(); + for (uint32_t i : visible.top) + m_top_visible_bitset.set(i); + for (uint32_t i : visible.bottom) + m_bottom_visible_bitset.set(i); } } #endif // ENABLE_OPENGL_ES @@ -1634,10 +1718,13 @@ void ViewerImpl::update_enabled_entities() #ifndef ENABLE_OPENGL_ES const bool whole_layer = v.layer_id == layers_range[0] || v.layer_id == layers_range[1]; const bool keep_anyway = whole_layer || !v.is_extrusion(); - const bool exposed = v.is_extrusion() && m_exposed_bitset.size == m_vertices.size() && m_exposed_bitset[i]; + const bool classified = v.is_extrusion() && m_exposed_bitset.size == m_vertices.size(); + const bool exposed = classified && m_exposed_bitset[i]; if (build_rest && !v.is_option()) { const bool skipped = !whole_layer && rest_stride > 1 && (v.layer_id % rest_stride) != 0; - if (skipped ? exposed : (keep_anyway || reduced_set_keeps(m_settings.rest_detail_mode, i, v))) + // a skipped layer keeps only what the camera's side of the print can see of it + const bool visible = classified && (m_settings.rest_view_from_above ? m_top_visible_bitset[i] : m_bottom_visible_bitset[i]); + if (skipped ? visible : (keep_anyway || reduced_set_keeps(m_settings.rest_detail_mode, i, v))) enabled_segments_rest.push_back(static_cast(i)); } if (!build_reduced) @@ -1920,6 +2007,16 @@ void ViewerImpl::set_rest_layer_stride(uint32_t value) m_settings.update_enabled_entities = true; } +void ViewerImpl::set_rest_view_from_above(bool value) +{ + if (m_settings.rest_view_from_above == value) + return; + m_settings.rest_view_from_above = value; + // it only matters while the shell rest set skips layers + if (m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly && m_settings.rest_layer_stride > 1) + m_settings.update_enabled_entities = true; +} + // ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to void ViewerImpl::set_dim_previous_layers(bool value) { diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index 7b2172033a..e3ffc8813f 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -105,6 +105,8 @@ public: void set_rest_detail_mode(EReducedDetailMode mode); uint32_t get_rest_layer_stride() const { return m_settings.rest_layer_stride; } void set_rest_layer_stride(uint32_t value); + bool is_rest_view_from_above() const { return m_settings.rest_view_from_above; } + void set_rest_view_from_above(bool value); float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; } void set_dim_previous_layers_brightness(float value); @@ -332,6 +334,10 @@ private: // the subset of those that are exposed from above or below: the surfaces a view from the top // or bottom sees, kept in every layer even while layers are being skipped BitSet<> m_exposed_bitset; + // narrower still: the segments that are the topmost, or the bottommost, thing at their place + // in the whole print, which is all a view from straight above, or below, can see + BitSet<> m_top_visible_bitset; + BitSet<> m_bottom_visible_bitset; #endif // ENABLE_OPENGL_ES // // Variables used for toolpaths coloring diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 3e85866365..e00470a1a0 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1954,6 +1954,7 @@ void GCodeViewer::update_rest_layer_stride() const unsigned int stride = (layer_px * MAX_STRIDE <= MERGE_BELOW_PX) ? MAX_STRIDE : std::clamp(static_cast(MERGE_BELOW_PX / layer_px), 1u, MAX_STRIDE); m_viewer.set_rest_layer_stride(stride); + m_viewer.set_rest_view_from_above(camera.get_dir_forward().z() < 0.0); } // ORCA: libvgcode only builds a reduced set while its mode is not Off, so the preference switch is