Skip to content

feat: Bump Skin slicing and mesh baking#15335

Open
dave-hillier wants to merge 25 commits into
prusa3d:masterfrom
dave-hillier:feature/texture-skin
Open

feat: Bump Skin slicing and mesh baking#15335
dave-hillier wants to merge 25 commits into
prusa3d:masterfrom
dave-hillier:feature/texture-skin

Conversation

@dave-hillier

@dave-hillier dave-hillier commented Apr 6, 2026

Copy link
Copy Markdown

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_polyline core 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.

dave-hillier and others added 25 commits April 5, 2026 08:25
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant