feat: Bump Skin slicing and mesh baking#15335
Open
dave-hillier wants to merge 25 commits into
Open
Conversation
Introduces a sibling to Fuzzy Skin that perturbs perimeter polygons using deterministic texture patterns sampled via 3D UV projection, enabling reproducible surface textures (knurling, hex grids, carbon fiber, etc.) that wrap coherently around an object. Core algorithm ported from stlTexturizer (MIT): for each inserted point along a perimeter at layer Z, compute (u,v) via one of 7 UV projections (planar XY/XZ/YZ, cylindrical, spherical, triplanar, cubic), bilinearly sample a grayscale heightmap, and offset the point along the perimeter normal by (grey - 0.5) * 2 * amplitude. This first slice integrates at slice time (no mesh modification) and applies whole-volume only. Paint-on mask gizmo and per-region painted segmentation will follow. Adds: - src/libslic3r/Feature/TextureSkin/ (UVProjection, TextureSampler, TexturePatterns, TextureSkin) - 25 grayscale PNG pattern images under resources/textures/texture_skin/ - 11 PrintRegionConfig options (texture_skin + pattern/uv_mode/amplitude/ point_dist/uv_scale/uv_offset_u/uv_offset_v/uv_rotation/mapping_blend/ custom_image) plus 3 new enums - "Texture skin (experimental)" group in Print Settings -> Layers and perimeters tab Wiring: - PerimeterGenerator::Parameters extended with layer_z + object_bbox, populated from LayerRegion - apply_texture_skin() called after apply_fuzzy_skin() at both classic and Arachne perimeter sites (features compose) - Keys registered in s_Preset_print_options so presets load cleanly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a paint-on gizmo (Ctrl+T) mirroring GLGizmoFuzzySkin, plus the
ModelVolume data model, 3MF serialization, and UI integration needed to
mark facets for texture skin application.
Gizmo + painter infrastructure:
- GLGizmoTextureSkin derived from GLGizmoPainterBase, with brush/smart-fill
tools, circle/sphere/pointer cursors, clipping slider, clear-all button
- TEXTURE_SKIN = ENFORCER alias in TriangleStateType (binary mask)
- TEXTURE_SKIN added to PainterGizmoType
- TextureSkin = 10 in GLGizmosManager::EType, sprite_id 10 with MmSegmentation
and Measure bumped to 11/12
- Full lifecycle wiring in GLCanvas3D (force_neutral_color, ALT menubar
suppression, rectangle-selection exclusion, bottom-texture hiding)
- Copied fuzzy_skin SVGs as placeholder icons for texture_skin{,_painting,
_painting_}.svg
Data model:
- ModelVolume::texture_skin_facets (FacetsAnnotation), wired through every
copy ctor, assert block, cereal load/save, reset, set_new_unique_id path
- ModelObject::is_texture_skin_painted() / Model::is_texture_skin_painted()
- model_texture_skin_data_changed() triggers re-slice from PrintApply on
painted-data changes
- 3MF slic3rpe:texture_skin XML attribute on <triangle> for load/save
- InfoItemType::TextureSkin surfaced in object list with
texture_skin_painting_ icon, activation opens the gizmo
NOTE: the mask is stored/displayed/serialized but does not yet spatially
gate apply_texture_skin() - segmentation by painting comes next.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Makes the paint-on mask actually gate texture-skin application during slicing, mirroring the fuzzy-skin painted-region pipeline. - texture_skin_segmentation_by_painting() converts painted facets into per-layer ExPolygons, bounded by max external perimeter width - PrintObjectRegions::TextureSkinPaintedRegion struct tracks the parent VolumeRegion / PaintedRegion that a painted area overrides - generate_print_object_regions() builds texture_skin_painted_regions whenever any ModelVolume has texture_skin_facets, creating child PrintRegions with texture_skin=All forced on - apply_texture_skin_segmentation() in slice_volumes() splits each LayerRegion's slices so painted areas move to the override region - PerimeterRegion::has_compatible_perimeter_regions() now checks all texture-skin config keys so painted regions don't collapse back - PrintObject::invalidate_state_by_config_options() invalidates posPerimeters/posSupportMaterial on any texture-skin key change - PrintApply copies texture_skin_facets between old and new ModelVolumes, re-validates texture_skin_painted_regions after reslice, and asks generate_print_object_regions to build them when the volume is painted Result: with texture_skin=None globally, only painted areas get textured; with texture_skin=External, painted areas get texturing applied to all perimeters via the override region, unpainted areas only to external. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the plain pattern dropdown + custom-image path field in Print
Settings with a "Pick texture…" button that opens a modal thumbnail
grid. The grid shows all 24 built-in patterns plus a Custom… tile; that
tile opens a PNG file browser and writes pattern=Custom + the chosen
absolute path atomically.
The settings line now shows a read-only text box on the left with the
current selection ("Knurling" or "Custom: myfile.png") and the picker
button on the right; the text box auto-refreshes from config so preset
switches and Reset-to-default update it live.
Dialog is 60x44 em so at least two rows of 120px thumbnails are visible
by default, and mirrors GalleryDialog's wxListCtrl/wxImageList flow.
- New: src/slic3r/GUI/TextureSkinPickerDialog.{hpp,cpp}
- Tab.cpp: create_texture_skin_pattern_widget() using
create_line_with_widget, and texture_skin_pattern registered in
options_without_field so reset/undo takes the no-field code path
(mirrors how bed_shape handles its custom widget).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a "Bake displacement" section to the Paint-on Texture Skin gizmo
that replaces the selected ModelVolume's TriangleMesh with a displaced
version of itself. Works on any surface orientation (top/bottom faces
included), so it covers what the slice-time perimeter approach can't.
Core algorithm (Feature/TextureSkin/MeshDisplace.{hpp,cpp}):
- Adaptive midpoint subdivision of the whole mesh to a target edge
length (default 0.4 mm), tracking each output triangle's origin
triangle. Watertight via an edge-midpoint cache. Safety-capped at
5 M triangles.
- Per-output-vertex paint mask built by testing the vertex against the
painted sub-triangles from FacetsAnnotation::get_facets(TEXTURE_SKIN)
using a 3D barycentric + plane-distance check, so partial-face
painting is honoured (not just per-original-facet granularity).
- NelsonMaxWeighted smooth normals via NormalUtils::create_normals.
- Vertices with smooth normal Z below a threshold (default -0.7) are
skipped when "Skip bottom face" is enabled (on by default).
- Displace painted vertices by (grey-0.5)*2*amplitude along the smooth
normal, then QEM-decimate via its_quadric_edge_collapse down to the
target triangle count. Decimation crushes flat regions and preserves
the displaced detail naturally.
UI (GLGizmoTextureSkin):
- Sliders for amplitude, edge length, target triangle count, plus the
skip-bottom-face checkbox.
- Bake button spawns a std::thread worker with a throw-on-cancel
callback; UI shows a progress bar + Cancel button while running.
- On completion, apply_bake runs Plater::clear_before_change_mesh (now
including texture_skin_facets), swaps the volume's mesh, recomputes
convex hull, invalidates bounds, and triggers re-slice.
Plater.cpp: clear_before_change_mesh now resets texture_skin_facets
alongside the other paint annotations, so baking leaves a clean slate.
Config is read from the currently-edited Print preset via opt_enum<T>
and opt_float/opt_string so it works regardless of whether the option
is stored as a typed ConfigOptionEnum or a ConfigOptionEnumGeneric.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New SVG gizmo icons: displaced sphere with radiating orange spikes (replaces the fuzzy-skin placeholder). Both 16px and 128px variants. - Texture picker mirrored into the Paint-on Texture Skin gizmo's "Bake displacement" section. Clicking "Pick texture…" opens the thumbnail dialog; the selection is written to the Print preset via set_key_value + Tab::on_value_change so both the slice-time perimeter texturing and the mesh bake see the same pattern. The original Print Settings picker is retained as the canonical entry point and both stay in sync. - Gizmo panel is now height-clamped to the viewport with a vertical scrollbar when content overflows (instead of auto-resizing off the bottom), and is shifted up ~22 em from the gizmo toolbar anchor so the Bake button is reachable on short screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merges the two separate paint-on gizmos, paint masks, and slicing cores
into a single "Surface Texture" feature with Fuzzy / Pattern modes,
while keeping all existing config keys (fuzzy_skin*, texture_skin*)
unchanged so every vendor preset continues to work.
Slicing core:
- New shared PerturbPolyline.{hpp,cpp} in Feature/SurfaceTexture/
extracts the common polyline-walk + point-insertion + perpendicular-
offset loop. FuzzySkin.cpp and TextureSkin.cpp become thin wrappers
supplying a random OffsetProvider (jittered spacing) or a texture-
sampled OffsetProvider (constant spacing) respectively.
Paint mask:
- New ModelVolume::surface_texture_facets — 3-state FacetsAnnotation
using SURFACE_FUZZY (ENFORCER=1) and SURFACE_PATTERN (BLOCKER=2),
same encoding as FDM supports uses ENFORCER+BLOCKER on one field.
- TriangleSelector gains visit_painted_leaves() for external callers
to iterate per-leaf source_triangle + state without accessing the
protected Triangle type.
3MF migration:
- Load reads all three attrs (slic3rpe:fuzzy_skin, texture_skin,
surface_texture); legacy data auto-migrated into the unified field
via LegacyMigration::migrate_legacy_paint_to_surface_texture().
- Save writes only slic3rpe:surface_texture (old PrusaSlicer versions
silently ignore it — graceful degradation).
PrintApply bridge:
- derive_legacy_from_surface_texture() repopulates the legacy
fuzzy_skin_facets + texture_skin_facets from the unified one just
before the segmentation pipelines run, so the existing
fuzzy_skin_segmentation_by_painting and
texture_skin_segmentation_by_painting stay 100% unchanged.
Unified gizmo:
- GLGizmoSurfaceTexture replaces GLGizmoFuzzySkin + GLGizmoTextureSkin.
Radio at the top: Fuzzy / Pattern. Left-click paints the active state.
Pattern mode shows the texture picker + bake section.
Shortcut: Ctrl+H (reusing fuzzy skin's existing binding).
- Single PainterGizmoType::SURFACE_TEXTURE, single
GLGizmosManager::SurfaceTexture enum, single
InfoItemType::SurfaceTexture. All GLCanvas3D / ObjectList /
NotificationManager / ObjectDataViewModel references updated.
Removed:
- GLGizmoFuzzySkin.{hpp,cpp}
- GLGizmoTextureSkin.{hpp,cpp}
- PainterGizmoType::FUZZY_SKIN + TEXTURE_SKIN
- GLGizmosManager::FuzzySkin + TextureSkin enum values
- InfoItemType::FuzzySkin + TextureSkin
Icon: uses the original fuzzy_skin_painting.svg for the toolbar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes: 1. Paint-on slicing was broken after unification: the unified gizmo writes to surface_texture_facets but is_fuzzy_skin_painted() and is_texture_skin_painted() only checked the legacy fields (empty on the model). Segmentation never ran, producing "The print is empty". Fix: both checks now also look at surface_texture_facets. 2. Custom texture loading failed on any non-grayscale PNG (RGB, RGBA, paletted) and on JPEGs. load_image_file() now has three decode paths: grayscale PNG (fast), RGBA PNG (convert via luminance), and JPEG (via libjpeg). File dialog accepts *.png, *.jpg, *.jpeg. 3. Bake defaults tuned: amplitude lowered from 0.5mm to 0.15mm so patterns are visible (not noise). Per-pattern defaultScale from stlTexturizer applied automatically at bake time. Long pattern names truncated with ellipsis in the gizmo panel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bake displacement now has its own UV parameters (scale, offset U/V, rotation, blend) in a collapsible "UV mapping" section, collapsed by default. These are gizmo-local and independent from the Print Settings UV options (which drive slice-time perimeter texturing). When a built-in pattern is picked, the UV scale slider auto-seeds from the pattern's recommended defaultScale. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The derive_legacy_from_surface_texture approach used set_facet() which
projected sub-triangle paint to whole-triangle granularity, breaking
paint-on precision. Replaced with a dual-write architecture:
1. GLGizmoSurfaceTexture::update_model_object() writes to BOTH:
- surface_texture_facets (3-state, for gizmo display + 3MF)
- fuzzy_skin_facets (via remap: clone tree, PATTERN->NONE, keep
FUZZY as ENFORCER — full sub-triangle tree preserved)
- texture_skin_facets (via remap: clone tree, FUZZY->NONE, remap
PATTERN->ENFORCER — full sub-triangle tree preserved)
2. New TriangleSelector::remap_state(from, to) walks all leaves and
changes state values in-place, preserving the entire recursive
sub-triangle splitting hierarchy.
3. Segmentation functions revert to reading legacy fields with
num_facets_states=2 (original working architecture).
4. is_fuzzy_skin_painted() / is_texture_skin_painted() check only
their respective legacy fields (populated by the dual-write),
so each segmentation only runs when its specific paint state
actually exists on the model.
Known limitation: painting both Fuzzy AND Pattern on the same object
can cause visual interference between the two segmentation passes
(each modifies parent slices sequentially). This will be addressed
in a follow-up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When both fuzzy and pattern paint exist on the same object, the two apply_*_segmentation passes in PrintObjectSlice.cpp ran sequentially and each overwrote ALL LayerRegion slices — including regions set by the other pass. The texture pass wiped the fuzzy region's slices to empty, making fuzzy areas appear flat. Fix: each pass now tracks which by_region entries it actually modified via a `modified` flag. The final "re-create regions" loop only writes back modified entries, leaving regions owned by other passes intact. Additionally, when a steal occurs the parent region is always marked modified (even if its remaining polygon is empty after full consumption) so that fully-consumed parents get correctly emptied rather than retaining stale pre-pass slices. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bake section's UV mapping group now has a Projection combo box (Planar XY/XZ/YZ, Cylindrical, Spherical, Triplanar, Cubic) so users can choose how the texture wraps the mesh before baking. Defaults to Triplanar. Gizmo-local, independent from the Print Settings UV mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes: 1. Baked meshes could have degenerate triangles, orphan vertices, and flipped normals from the displacement + QEM decimation pipeline, causing "negative spacing" errors and extreme support generation during slicing. Added a cleanup pass after decimation: its_remove_degenerate_faces, its_compactify_vertices, and its_flip_triangles if the mesh volume is negative. 2. Added a Projection combo box (Planar XY/XZ/YZ, Cylindrical, Spherical, Triplanar, Cubic) to the UV mapping section in the bake panel. Defaults to Triplanar. Gizmo-local, independent from Print Settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New Direction combo in the bake panel controls where the zero-point sits in the greyscale range: - Outward (default): black=0, white=+amplitude. Surface only grows, no dents. Best for adding raised texture to a flat surface. - Symmetric: black=-amp, white=+amp. Both bumps and dents centered around the original surface. - Inward: black=-amp, white=0. Surface only shrinks. The formula normalises so peak displacement is always ±amplitude regardless of the zero-point choice. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The gizmo uses fuzzy_skin_painting.svg; these were never referenced. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Pick texture button in Tab.cpp no longer uses an icon. Deleted the three texture_skin SVGs (spiky-sphere placeholders, unused elsewhere). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Removed ///|/ AGPLv3 headers from all files we created (not every file in the codebase has them, keeping it consistent). - Removed derive_legacy_from_surface_texture() from LegacyMigration (dead code since the gizmo switched to dual-write via remap_state). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added direct call to schedule_background_process() after painting in the unified gizmo, alongside the existing event post. This ensures the background process detects the model change and re-enables Slice Now. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Skip segments shorter than 1 scaled unit (0.001mm) to avoid near-zero denominators producing extreme direction vectors. - Skip any offset that is NaN or infinity (from edge-case UV sampling or texture lookup) instead of writing garbage coordinates. - Applied to both polygon and Arachne extrusion-line perturbation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Performance: - std::unordered_map for edge-midpoint cache (O(1) vs O(log n)) - tbb::parallel_for on the displacement loop (~8x on multi-core) - Reverted selective subdivision (caused T-junctions and bleed to unpainted faces) but kept the other two wins Progress bar: - Subdivision now reports per-face progress (0-40%) so the bar increments smoothly instead of appearing stuck - Rebalanced phases: subdivide 0-40%, mask 40-50%, displace 50-55%, decimate 55-95%, cleanup 95-100% - Worker thread now posts set_as_dirty() via CallAfter on each progress update so the GL canvas actually repaints and shows the bar moving Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compute texture_aspect_u/v from the actual image dimensions so non-square custom textures tile proportionally instead of being stretched. aspect = max(w,h) / dim, matching stlTexturizer's Avatarsia/speed-optimize branch approach. Applied in both the slice-time path (params_from_config) and the bake path (start_bake). Built-in 512x512 patterns are unaffected (aspect = 1.0). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Texture preview: PATTERN-painted faces now show the actual grayscale texture pattern in the 3D viewport instead of flat red. Implemented as a second render pass using the flat_texture shader with per-vertex UVs computed via compute_uv(). The GL texture is uploaded from the current pattern's GrayImage and cached; the textured GLModel rebuilds when paint or pattern changes. Uses glPolygonOffset to render slightly in front of the standard blocker triangles. Destructor fix: GLGizmoSurfaceTexture now has an explicit destructor that joins the bake thread and cleans up the GL texture. Without this, app exit with a joinable std::thread member triggers std::terminate() because std::thread's destructor aborts if joinable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for the texture preview overlay:
1. GL_RED textures sample as (r,0,0,1) producing dark red. Added
GL_TEXTURE_SWIZZLE_G/B = GL_RED so the single channel maps to
all RGB → proper grayscale output.
2. The flat_texture shader's sampler2D uniform wasn't set. Added
set_uniform("uniform_texture", 0) to bind texture unit 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The texture preview render pass is commented out for now — the UV coordinates weren't mapping correctly. Painted PATTERN faces revert to the standard flat red (BLOCKER) color. The preview code remains in place for future fix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ported from stlTexturizer, MIT licensed
Extends PrusaSlicer's fuzzy skin feature into a unified Surface Texture system that adds deterministic, pattern-driven surface decoration to 3D prints. Where fuzzy skin applies random perturbation, Surface Texture samples from grayscale heightmap images to produce reproducible textures like knurling, carbon fiber, hex grids, and leather — wrapping coherently around objects via UV projection.
Two modes of operation
Slice-time perimeter texturing modulates perimeter polygon points at slice time, sampling a texture image at each point's 3D position via one of seven UV projection modes (Planar XY/XZ/YZ, Cylindrical, Spherical, Triplanar, Cubic). This works identically to fuzzy skin in the pipeline — only the offset formula changes from random to texture-sampled. Both modes share a common
perturb_polylinecore with pluggable offset providers.Mesh-level displacement bake handles surfaces that perimeters can't reach (top/bottom faces of a cube, overhangs). The gizmo's Bake section subdivides the mesh to a target edge length, displaces painted vertices along smooth normals by the texture pattern, then QEM-decimates back to a target triangle count. Includes controls for amplitude, displacement direction (outward-only, symmetric, or inward-only), UV mapping parameters, skip-bottom-face toggle, and a live progress bar.
Unified gizmo
Fuzzy Skin's paint-on gizmo and the new texture painting are merged into a single Surface Texture gizmo (Ctrl+H) with a Fuzzy/Pattern mode radio. A thumbnail picker dialog shows all 25 built-in patterns plus a Custom option that accepts any PNG or JPEG.