mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-30 05:17:19 +00:00
Add Parallax preview & fix Undo\Redo history
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
# Texture Displacement - Technical Notes
|
# Texture Displacement - Technical Notes
|
||||||
|
|
||||||
Branch: `feature/texture_displacement`. This document is a knowledge dump of the whole feature as
|
Branch: `feature/texture_displacement`. Reference for the feature as it stands: what it does, how the
|
||||||
it stands: architecture, file map, algorithms, known bugs found and fixed (with root causes worth
|
algorithms work, and where the code lives.
|
||||||
remembering), and what's still deferred. Written so a fresh session (or a fresh pair of eyes) can
|
|
||||||
pick this up without re-deriving everything from scratch.
|
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
@@ -14,24 +12,67 @@ A paint-style gizmo (`GLGizmoTextureDisplacement`) that lets you:
|
|||||||
(saved into `<data_dir>/textures/displacement/`, kept separate so app updates can't clobber it).
|
(saved into `<data_dir>/textures/displacement/`, kept separate so app updates can't clobber it).
|
||||||
- Combine overlapping layers with image-editor-style blend modes (Add/Subtract/Multiply/Divide).
|
- Combine overlapping layers with image-editor-style blend modes (Add/Subtract/Multiply/Divide).
|
||||||
- Preview the true displaced result live, before baking (background job, not on the UI thread).
|
- Preview the true displaced result live, before baking (background job, not on the UI thread).
|
||||||
- Optionally preview via a fast GPU bump-map shader instead (no real geometry movement, just
|
- Preview via a fast GPU shader instead (no real geometry movement) for a lighter-weight alternative.
|
||||||
shading) for a lighter-weight alternative.
|
|
||||||
- Bake into real mesh geometry on demand, restricted to the painted area only.
|
- Bake into real mesh geometry on demand, restricted to the painted area only.
|
||||||
- Subdivide a low-poly model first so there are enough vertices to show fine detail.
|
- Remesh and subdivide so a low-poly model has enough vertices to show fine detail.
|
||||||
- Unwrap a painted patch with a real CGAL LSCM parameterization and view it in a dedicated,
|
- Unwrap a painted patch with a real CGAL LSCM parameterization and view it in a dedicated,
|
||||||
dockable 2D "UV Editor" pane.
|
dockable 2D "UV Editor" pane.
|
||||||
|
|
||||||
|
## Standard vs Pro mode
|
||||||
|
|
||||||
|
A two-position slider in the panel header, right of the Dock/Undock button.
|
||||||
|
|
||||||
|
**Pro** shows every mesh-preparation control; Remesh, Subdivide and Bake are run separately by the user,
|
||||||
|
in whatever order they like.
|
||||||
|
|
||||||
|
**Standard** hides all of it and folds one fixed recipe into the Bake button, because a height map only
|
||||||
|
ever *moves vertices that already exist* - painting onto an imported 12-triangle box and pressing Bake
|
||||||
|
would otherwise do nothing visible. Standard's Bake is:
|
||||||
|
|
||||||
|
1. `plan_remesh()` + `replace_mesh_keep_all_paint()` - isotropic remesh to 1 mm, sharp edges above 40
|
||||||
|
degrees protected. Gives the subdivider an even starting density whatever the input looked like.
|
||||||
|
2. `plan_adaptive_subdivision()` + `apply_adaptive_subdivision()` - feature-adaptive refinement, max
|
||||||
|
edge 20 mm, detail 0.02 mm, min edge 0.02 mm.
|
||||||
|
3. `bake()` - the ordinary background displacement job.
|
||||||
|
|
||||||
|
Both preparation stages are *planned* before the undo snapshot and *applied* after it, so a stage with
|
||||||
|
nothing to do is skipped without leaving an empty undo step. The standalone Pro buttons share the same
|
||||||
|
plan/apply split.
|
||||||
|
|
||||||
|
**All three stages sit under one undo step.** `Plater::take_snapshot()` records the state *before* the
|
||||||
|
change, so a single snapshot taken at the top of `bake_standard()` means one Undo returns the mesh to
|
||||||
|
exactly what was imported. `TextureDisplacementBakeInput::take_snapshot` lets the caller say who owns
|
||||||
|
the undo step - true for the Pro-mode button, false for the pipeline, whose background job commits long
|
||||||
|
after that snapshot's scope has closed.
|
||||||
|
|
||||||
|
The presets live in one place (`STD_*` constants) and `apply_standard_mode_presets()` pins the hidden
|
||||||
|
controls to them every frame while Standard is active, so the live preview cannot disagree with what
|
||||||
|
Bake will do. Switching to Standard also closes the subdivision preview, whose controls have just gone.
|
||||||
|
|
||||||
|
One control survives into Standard: **"Added triangles (k)"**, the subdivision budget. It is deliberately
|
||||||
|
*not* pinned - pinning would fight the user's own slider every frame - because unlike the rest of the
|
||||||
|
recipe its right value depends on the part rather than on the method (a big model, or a fine texture,
|
||||||
|
simply needs more triangles). Default 1500. The widget is one lambda shared by both layouts.
|
||||||
|
|
||||||
|
Standard remeshes *after* painting, so the remesh has to preserve paint: `ModelVolume::restore_painting()`
|
||||||
|
only remaps the four standard channels, so `replace_mesh_keep_all_paint()` additionally runs
|
||||||
|
`TriangleSelector::remap_painting()` over the eight texture-displacement masks. The Pro Remesh button
|
||||||
|
goes through the same helper. If the remap comes back empty the pipeline stops with a message rather
|
||||||
|
than baking a flat mesh.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Data model (per `ModelVolume`)
|
### Data model (per `ModelVolume`)
|
||||||
|
|
||||||
Each of up to `TEXTURE_DISPLACEMENT_MAX_LAYERS` (8) layers gets its **own independent
|
Each of up to `TEXTURE_DISPLACEMENT_MAX_LAYERS` (8) layers gets its **own independent
|
||||||
`FacetsAnnotation`** paint mask - the exact same `TriangleSelector`/`FacetsAnnotation` machinery
|
`FacetsAnnotation`** paint mask - the same `TriangleSelector`/`FacetsAnnotation` machinery every other
|
||||||
every other paint gizmo (FdmSupports, Seam, MMU, FuzzySkin) already uses, just one full instance
|
paint gizmo (FdmSupports, Seam, MMU, FuzzySkin) already uses, just one full instance per layer slot
|
||||||
per layer slot instead of one per volume. This is what makes "layered/blended" painting work for
|
instead of one per volume. This is what makes layered/blended painting work for free: the same triangle
|
||||||
free: the same triangle can be `ENFORCER` in layer 2's mask and layer 5's mask simultaneously, and
|
can be `ENFORCER` in layer 2's mask and layer 5's mask simultaneously, and at bake/preview time each
|
||||||
at bake/preview time each layer displaces the surface left by the previous one (image-editor-layer
|
layer displaces the surface left by the previous one (image-editor-layer semantics).
|
||||||
semantics).
|
|
||||||
|
Whole-stack settings (border handling, post-process smoothing) live beside the layers in
|
||||||
|
`texture_displacement_options` (`TextureDisplacementOptions`), since they belong to no single layer.
|
||||||
|
|
||||||
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
|
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
|
||||||
|
|
||||||
@@ -42,13 +83,13 @@ same order - only the positions of displaced vertices differ.
|
|||||||
1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops
|
1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops
|
||||||
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
|
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
|
||||||
guarantee the index alignment step 3 depends on.
|
guarantee the index alignment step 3 depends on.
|
||||||
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both
|
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both projects
|
||||||
projects and displaces along these, so a vertex covered by several layers moves along one single
|
and displaces along these, so a vertex covered by several layers moves along one single well-defined
|
||||||
well-defined direction. Where the paint does *not* cover every triangle around a vertex, the normal
|
direction. Where the paint does *not* cover every triangle around a vertex, the normal is recomputed
|
||||||
is recomputed from the painted triangles alone (the union over all layers, so it stays one direction
|
from the painted triangles alone (the union over all layers, so it stays one direction per vertex):
|
||||||
per vertex). On the rim of a fully painted top face the whole-mesh normal is the 45 degrees bisector
|
on the rim of a fully painted top face the whole-mesh normal is the 45-degree bisector it shares with
|
||||||
it shares with the side wall, and displacing along that flares the rim outwards instead of raising
|
the side wall, and displacing along that flares the rim outwards instead of raising it. Interior
|
||||||
it. Interior vertices are unaffected - all their triangles are painted, so the two coincide. Paint
|
vertices are unaffected - all their triangles are painted, so the two normals coincide. Paint
|
||||||
coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`.
|
coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`.
|
||||||
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
|
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
|
||||||
the **base mesh** (never against a previous layer's output), then
|
the **base mesh** (never against a previous layer's output), then
|
||||||
@@ -61,36 +102,33 @@ same order - only the positions of displaced vertices differ.
|
|||||||
brush stroke split a triangle are appended after them), and `get_facets_strict()` emits the
|
brush stroke split a triangle are appended after them), and `get_facets_strict()` emits the
|
||||||
referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**.
|
referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**.
|
||||||
Split vertices live past the end of our array and are simply skipped - they sit on the paint
|
Split vertices live past the end of our array and are simply skipped - they sit on the paint
|
||||||
boundary anyway (splitting only happens at partial coverage), so they would be pinned regardless.
|
boundary anyway (splitting only happens at partial coverage).
|
||||||
4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is
|
4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is
|
||||||
`TextureDisplacementOptions::displace_border`, and it **does by default**. The original design
|
`TextureDisplacementOptions::displace_border`, and it does by default. Nothing can tear: the bake is
|
||||||
pinned it, justified as stopping the patch tearing away from the surrounding surface - which stopped
|
topology-preserving, so a border vertex is *one* vertex shared by both regions and moving it simply
|
||||||
being true the moment the bake became topology-preserving. There is no seam to tear: a border vertex
|
tilts the unpainted triangles that use it. Pinning it instead clamps the outermost ring of relief to
|
||||||
is *one* vertex shared by both regions, and moving it simply tilts the unpainted triangles that use
|
zero, which on a fully painted face collapses the pattern into a ring of steep ramps at the edge; it
|
||||||
it. What pinning actually does is clamp the outermost ring of relief to zero, so on a fully painted
|
is kept as an option for when the relief must not spill past the paint at all. Either way the border
|
||||||
face the pattern collapses into a ring of steep ramps right at the edge - the reported "it doesn't
|
drives the `edge_smoothing` falloff.
|
||||||
extrude at the border" artifact. Pinning is kept as an option for the case where the relief must not
|
|
||||||
spill past the paint at all. Either way the border still drives the `edge_smoothing` falloff.
|
|
||||||
5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods)
|
5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods)
|
||||||
and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's
|
and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's
|
||||||
`TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once**
|
`TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once**
|
||||||
per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract
|
per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract
|
||||||
layer would apply two or three times over depending on local triangle fan-out.
|
layer would apply two or three times over depending on local triangle fan-out.
|
||||||
6. Finally, move each touched vertex along its (step 2) normal by its accumulated total.
|
6. Move each touched vertex along its (step 2) normal by its accumulated total.
|
||||||
7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing.
|
7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing.
|
||||||
|
|
||||||
### Post-process smoothing
|
### Post-process smoothing
|
||||||
|
|
||||||
`smooth_mesh_vertices(mesh, movable, strength, iterations)` - plain Laplacian relaxation, run after all
|
`smooth_mesh_vertices(mesh, movable, strength, iterations)` - Laplacian relaxation, run after all layers
|
||||||
layers have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable
|
have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable vertex a
|
||||||
vertex a `strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the
|
`strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the previous
|
||||||
previous pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several
|
pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several times as
|
||||||
times as hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built
|
hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built once per
|
||||||
once per call.
|
call. Topology-preserving, like the bake.
|
||||||
|
|
||||||
Its job is to round off the hard steps a bitmap height map leaves behind, which is a different knob from
|
Its job is to round off the hard steps a bitmap height map leaves behind - a different knob from
|
||||||
`TextureDisplacementLayer::smoothing` - that blurs the *height map* before it is ever sampled, this
|
`TextureDisplacementLayer::smoothing`, which blurs the *height map* before it is ever sampled.
|
||||||
relaxes the *geometry* afterwards. Topology-preserving, like the bake.
|
|
||||||
|
|
||||||
Two ways in, sharing one set of settings on the volume:
|
Two ways in, sharing one set of settings on the volume:
|
||||||
- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake.
|
- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake.
|
||||||
@@ -103,14 +141,13 @@ Two ways in, sharing one set of settings on the volume:
|
|||||||
operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight
|
operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight
|
||||||
texture-displacement masks around `set_mesh()` rather than remapping or dropping them.
|
texture-displacement masks around `set_mesh()` rather than remapping or dropping them.
|
||||||
|
|
||||||
**"Ignore outer ring"** (`smooth_skip_border`, **on by default**) drops the patch's own outermost ring of
|
**"Ignore outer ring"** (`smooth_skip_border`, on by default) drops the patch's own outermost ring of
|
||||||
vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the
|
vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the
|
||||||
rim of the relief back down toward the flat surface and the pattern comes out half-melted exactly where
|
rim of the relief down toward the flat surface and the pattern comes out half-melted where it meets the
|
||||||
it meets the edge - crisp everywhere else, which is what makes it look like a bug rather than a setting.
|
edge. Held out, the border keeps the full depth the texture asked for and only the interior relaxes.
|
||||||
Held out, the border keeps the full depth the texture asked for and only the interior relaxes. Turning it
|
Turning it off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing
|
||||||
off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing falloff).
|
falloff). This is the *smoothing* rim, independent of whether that rim is displaced at all
|
||||||
Note this is the *smoothing* rim, a separate question from whether that rim is displaced at all
|
(`displace_border`, step 4 above); both default to keeping the border sharp.
|
||||||
(`displace_border`, step 4 above) - the two are independent and both default to "keep the border sharp".
|
|
||||||
|
|
||||||
### Blend modes
|
### Blend modes
|
||||||
|
|
||||||
@@ -122,50 +159,46 @@ Add/Subtract are self-explanatory. Multiply/Divide are *scaling* operations and
|
|||||||
convention: they treat the layer's own value as a **factor relative to 1 mm**. That makes `depth_mm`
|
convention: they treat the layer's own value as a **factor relative to 1 mm**. That makes `depth_mm`
|
||||||
a gain, and - the property that makes a Multiply layer usable as a mask - a layer with depth 1 mm
|
a gain, and - the property that makes a Multiply layer usable as a mask - a layer with depth 1 mm
|
||||||
sampling a white (1.0) texel multiplies by exactly 1, i.e. leaves the layers below unchanged.
|
sampling a white (1.0) texel multiplies by exactly 1, i.e. leaves the layers below unchanged.
|
||||||
Divide floors its divisor's magnitude at 0.05 - a black texel samples to *exactly* zero, so the
|
Divide floors its divisor's magnitude at 0.05: a black texel samples to *exactly* zero, so the divisor
|
||||||
divisor really does hit zero in ordinary use, and an unbounded `1/0` would fling vertices thousands
|
really does hit zero in ordinary use, and an unbounded `1/0` would fling vertices thousands of mm away
|
||||||
of mm away and poison the mesh's bounding box (and every plate/print-volume check downstream). The
|
and poison the mesh's bounding box (and every plate/print-volume check downstream). The floor doubles as
|
||||||
floor doubles as a cap on how far Divide can amplify the relief beneath it: at most 20×.
|
a cap on how far Divide can amplify the relief beneath it: at most 20×.
|
||||||
|
|
||||||
The **lowest painted layer ignores its blend mode**: it has nothing beneath it, and Multiply/Divide
|
The **lowest painted layer ignores its blend mode**: it has nothing beneath it, and Multiply/Divide
|
||||||
against an implicit zero base would annihilate (or blow up) it. Enforced in `build_texture_
|
against an implicit zero base would annihilate (or blow up) it. Enforced in
|
||||||
displacement()` (the first layer to reach a given vertex always folds in additively) and surfaced in
|
`build_texture_displacement()` (the first layer to reach a given vertex always folds in additively) and
|
||||||
the UI, which labels that layer "Base layer" instead of offering a control that silently does nothing.
|
surfaced in the UI, which labels that layer "Base layer" instead of offering a control that does nothing.
|
||||||
|
|
||||||
### Projection methods
|
### Projection methods
|
||||||
|
|
||||||
Four choices per layer (`TextureProjectionMethod`), all funneling through `apply_uv_transform()`
|
Five choices per layer (`TextureProjectionMethod`), all funneling through `apply_uv_transform()`
|
||||||
(scale by `1/tiling_scale`, rotate by `rotation_deg`, add `offset`). They are dispatched by
|
(scale by `1/tiling_scale`, rotate by `rotation_deg`, add `offset`). They are dispatched by
|
||||||
`sample_layer_height()`, which returns a **height**, not a UV - because Triplanar takes three
|
`sample_layer_height()`, which returns a **height**, not a UV - because Triplanar takes three
|
||||||
texture samples per vertex and so has no single UV that represents it.
|
texture samples per vertex and so has no single UV that represents it.
|
||||||
|
|
||||||
- **Triplanar** (default) - samples the texture on all three world planes (`(y,z)`, `(x,z)`, `(x,y)`)
|
- **Triplanar** (default) - samples the texture on all three world planes (`(y,z)`, `(x,z)`, `(x,y)`)
|
||||||
and blends the three by the vertex's own normal raised to `TRIPLANAR_BLEND_SHARPNESS` (4).
|
and blends the three by the vertex's own normal raised to `TRIPLANAR_BLEND_SHARPNESS` (4). Hard-picking
|
||||||
This is the fix for a real, user-reported bug. The previous version *hard-picked* the single axis
|
the single axis most aligned with the normal instead is discontinuous wherever that dominant axis
|
||||||
most aligned with the normal, which is discontinuous wherever that dominant axis flips: on a +X
|
flips: on a +X face the planar coordinate is `(y, z)`, on a −Y face it is `(x, z)`, so at the shared
|
||||||
face the planar coordinate is `(y, z)`, on a −Y face it is `(x, z)`, so at the shared edge `u`
|
edge `u` jumps. A weighted blend is continuous across the transition by construction, since the weight
|
||||||
jumps from `y_edge` to `x_edge`. On a box centred near the origin those two happen to **agree** at
|
of the axis being left behind falls smoothly to zero. This removes the hard *seam*; some cross-fade
|
||||||
the (+,+) and (−,−) corners and **differ by the full corner width** at the (+,−) and (−,+) corners
|
blurring in the band right at a 90° edge is inherent to triplanar mapping. A genuinely seam-free wrap
|
||||||
- which is exactly the "two bad corners, two good ones" symmetry that was observed. A weighted
|
around a box needs a real unwrap - that is what the LSCM mode is for.
|
||||||
blend is continuous across the transition by construction, since the weight of the axis being left
|
|
||||||
behind falls smoothly to zero. (Note this removes the hard *seam*; some cross-fade blurring in the
|
|
||||||
band right at a 90° edge is inherent to triplanar mapping. A genuinely seam-free wrap around a box
|
|
||||||
needs a real unwrap - that is what the LSCM mode is for.)
|
|
||||||
- **Cylindrical** - wraps around an axis through the patch centroid, axis auto-picked as the world
|
- **Cylindrical** - wraps around an axis through the patch centroid, axis auto-picked as the world
|
||||||
axis *least* aligned with the average normal (perpendicular to the outward radial normal, as a
|
axis *least* aligned with the average normal (perpendicular to the outward radial normal, as a
|
||||||
cylinder's own axis would be). `u = angle * local_radius` (arc length in mm), `v = distance along
|
cylinder's own axis would be). `u = angle * local_radius` (arc length in mm), `v = distance along
|
||||||
axis`. Approximation, not an exact fit for arbitrary geometry.
|
axis`. An approximation, not an exact fit for arbitrary geometry, and the axis/centre are not
|
||||||
|
user-overridable.
|
||||||
- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat.
|
- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat.
|
||||||
- **LSCM** - real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's
|
- **LSCM** - real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's
|
||||||
`Surface_mesh_parameterization` package, LSCM algorithm). Computed **once per patch** (not
|
`Surface_mesh_parameterization` package, LSCM algorithm). Computed **once per patch** (not
|
||||||
per-vertex like the others - it's a single global least-squares solve), then each vertex looks up
|
per-vertex like the others - it's a single global least-squares solve), then each vertex looks up
|
||||||
its precomputed UV. Requires the patch to be a single topological disk (one connected component,
|
its precomputed UV. Requires the patch to be a single topological disk (one connected component,
|
||||||
one boundary loop) - `compute_lscm_uvs()` returns empty and the layer silently falls back to
|
one boundary loop) - `compute_lscm_uvs()` returns empty and the layer falls back to Triplanar if not
|
||||||
Triplanar if not (e.g. multiple disconnected painted islands, or a fully closed patch).
|
(e.g. multiple disconnected painted islands, or a fully closed patch). CGAL's parameterizer needs a
|
||||||
CGAL's parameterizer needs a mesh with no isolated/unreferenced vertices, but `get_facets_strict()`
|
mesh with no isolated/unreferenced vertices, but `get_facets_strict()` returns the *whole* mesh's
|
||||||
returns the *whole* mesh's vertex array - so there's a compaction step
|
vertex array - so `compact_patch_with_map()` builds a clean sub-mesh plus an index map back to the
|
||||||
(`compact_patch_with_map()`) that builds a clean sub-mesh + an index map back to the original
|
original vertex numbering, purely local to this file.
|
||||||
(uncompacted) vertex numbering, purely local to this file.
|
|
||||||
- **ViewProjected** ("From view") - a flat projection along a fixed direction captured from the 3D
|
- **ViewProjected** ("From view") - a flat projection along a fixed direction captured from the 3D
|
||||||
camera, like a slide projector. `capture_view_projection()` takes the camera's right/up axes,
|
camera, like a slide projector. `capture_view_projection()` takes the camera's right/up axes,
|
||||||
transforms them into the volume's *local* frame (so the projection rides along if the part is later
|
transforms them into the volume's *local* frame (so the projection rides along if the part is later
|
||||||
@@ -174,7 +207,7 @@ texture samples per vertex and so has no single UV that represents it.
|
|||||||
projects `Vec2f(dot(pos, right), dot(pos, up))`. Single-valued per point, so - like LSCM but unlike
|
projects `Vec2f(dot(pos, right), dot(pos, up))`. Single-valued per point, so - like LSCM but unlike
|
||||||
blended Triplanar - the fast preview and UV-check overlay precompute it per vertex
|
blended Triplanar - the fast preview and UV-check overlay precompute it per vertex
|
||||||
(`compute_layer_vertex_uvs()`) and drive the shader's `use_vertex_uv` path. Faces angled away from
|
(`compute_layer_vertex_uvs()`) and drive the shader's `use_vertex_uv` path. Faces angled away from
|
||||||
the projector smear; that is inherent to view projection, not a bug.
|
the projector smear; that is inherent to view projection.
|
||||||
|
|
||||||
Two companions to this mode:
|
Two companions to this mode:
|
||||||
- **Projection frame overlay** (`TextureProjectorFrame`, see below) - a semi-transparent window
|
- **Projection frame overlay** (`TextureProjectorFrame`, see below) - a semi-transparent window
|
||||||
@@ -202,35 +235,34 @@ untouched) still forces a re-solve. Like the paint masks, seams are mesh-index-s
|
|||||||
any topology change.
|
any topology change.
|
||||||
|
|
||||||
Two ways to write to it:
|
Two ways to write to it:
|
||||||
- **Mark seam (manual, #9)** - a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses
|
- **Mark seam (manual)** - a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses painting. A
|
||||||
painting. A click raycasts the volume (`m_c->raycaster()->raycasters()[idx]->unproject_on_mesh()`,
|
click raycasts the volume (`m_c->raycaster()->raycasters()[idx]->unproject_on_mesh()`, `idx` = the
|
||||||
`idx` = the volume's slot among model-part volumes), finds the facet's edge nearest the hit point,
|
volume's slot among model-part volumes), finds the facet's edge nearest the hit point, and toggles it.
|
||||||
and toggles it. Marked edges render as a red overlay (`render_seam_overlay()`), pulled toward the
|
Marked edges render as a red overlay (`render_seam_overlay()`), pulled toward the camera so they read
|
||||||
camera so they read on top. This is the Blender mark-seam workflow.
|
on top. This is the Blender mark-seam workflow.
|
||||||
- **Cut island (auto, #17)** - `cut_island()` takes the selected chart's triangles (back-mapped from
|
- **Cut island (auto)** - `cut_island()` takes the selected chart's triangles (back-mapped from the
|
||||||
the unwrap via `source_vertex`), finds their 3D bounding box, and marks every edge that straddles
|
unwrap via `source_vertex`), finds their 3D bounding box, and marks every edge that straddles the
|
||||||
the mid-plane perpendicular to the longest axis. The re-unwrap then splits the chart across its
|
mid-plane perpendicular to the longest axis. The re-unwrap then splits the chart across its narrow
|
||||||
narrow waist - the "islands might be very long" case. Exposed as the UV pane's **Cut** button.
|
waist. Exposed as the UV pane's **Cut** button.
|
||||||
|
|
||||||
### UV-check overlays (checker / distortion)
|
### UV-check overlays (checker / distortion)
|
||||||
|
|
||||||
`resources/shaders/{110,140}/texture_displacement_uvcheck.{vs,fs}`, one shader with a `mode` uniform,
|
`resources/shaders/{110,140}/texture_displacement_uvcheck.{vs,fs}`, one shader with a `mode` uniform,
|
||||||
drawn over the painted patch (`rebuild_uvcheck_mesh()`/`render_uvcheck_mesh()`, P3N3T2: `normal.x` =
|
drawn over the painted patch (`rebuild_uvcheck_mesh()`/`render_uvcheck_mesh()`, P3N3T2: `normal.x` =
|
||||||
distortion, `tex_coord` = uv), pulled forward with a polygon offset. **Checker** (#13) samples a
|
distortion, `tex_coord` = uv), pulled forward with a polygon offset. **Checker** samples a procedural
|
||||||
procedural checkerboard at the layer's uv (per-vertex for LSCM/ViewProjected, in-shader triplanar
|
checkerboard at the layer's uv (per-vertex for LSCM/ViewProjected, in-shader triplanar otherwise) -
|
||||||
otherwise) - squares that stay square mean low distortion. **Distortion** (#14) colours each triangle
|
squares that stay square mean low distortion. **Distortion** colours each triangle blue→green→red by
|
||||||
blue→green→red by `log2(uv_area / surface_area)` centred on the patch's *median* stretch (so a
|
`log2(uv_area / surface_area)` centred on the patch's *median* stretch (so a globally-scaled unwrap
|
||||||
globally-scaled unwrap reads as uniformly ideal and only relative stretch shows), averaged to
|
reads as uniformly ideal and only relative stretch shows), averaged to vertices. A separate **Show mesh
|
||||||
vertices. A separate **Show mesh wireframe** toggle (#8) draws the whole volume's triangle edges,
|
wireframe** toggle draws the whole volume's triangle edges, rebuilt only when the vertex count changes
|
||||||
rebuilt only when the vertex count changes (not per stroke).
|
(not per stroke).
|
||||||
|
|
||||||
### Tiling
|
### Tiling
|
||||||
|
|
||||||
`DecodedHeightTexture::sample(uv, tile_enabled, tile_method)`. Two tile methods when enabled
|
`DecodedHeightTexture::sample(uv, tile_enabled, tile_method)`. Two tile methods when enabled
|
||||||
(Repeat, MirroredRepeat). **When `tile_enabled` is false, sampling outside `[0,1)` returns `0`
|
(Repeat, MirroredRepeat). **When `tile_enabled` is false, sampling outside `[0,1)` returns `0`
|
||||||
directly** - clamping the *coordinate* into range (what an earlier version did) instead smears the
|
directly** rather than clamping the *coordinate* into range, which would smear the border row/column of
|
||||||
border row/column of pixels outward to infinity in every direction, which is a real bug that was
|
pixels outward to infinity in every direction (streaky lines radiating out from the painted patch).
|
||||||
reported and fixed (visually: streaky lines radiating out from the painted patch).
|
|
||||||
|
|
||||||
### Subdivision — two modes
|
### Subdivision — two modes
|
||||||
|
|
||||||
@@ -242,16 +274,15 @@ no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no rem
|
|||||||
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
|
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
|
||||||
|
|
||||||
**Adaptive (`subdivide_mesh_adaptive()`)** — refine **only the painted area**, by **Rivara longest-edge
|
**Adaptive (`subdivide_mesh_adaptive()`)** — refine **only the painted area**, by **Rivara longest-edge
|
||||||
bisection**. This is the algorithm that was "scoped out" originally for fear of the T-junction/crack
|
bisection**, which is *conformal by construction*. Only **terminal** edges are ever bisected - an edge
|
||||||
problem; it is safe because it is *conformal by construction*. Only **terminal** edges are ever bisected
|
that is the longest edge of *every* triangle sharing it - which splits both those triangles along one
|
||||||
- an edge that is the longest edge of *every* triangle sharing it - which splits both those triangles
|
shared midpoint at once, so a hanging node is never created. The edge to split for a triangle that wants
|
||||||
along one shared midpoint at once, so a hanging node is never created. The edge to split for a triangle
|
refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of ever-longer-edged
|
||||||
that wants refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of
|
neighbours until a terminal one is reached, and bisect that. Edge length strictly increases along the
|
||||||
ever-longer-edged neighbours until a terminal one is reached, and bisect that. Edge length strictly
|
path (ties broken by mesh-vertex key, which both sides of an edge compute identically), so the walk
|
||||||
increases along the path (ties broken by mesh-vertex key, which both sides of an edge compute
|
cannot cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
|
||||||
identically), so the walk cannot cycle, and Rivara's result is that repeating it refines the original
|
of bisections. The transition triangles it pulls in just outside the painted patch are the graded band
|
||||||
triangle in a bounded number of bisections. The transition triangles it pulls in just outside the
|
that makes the size change conformal.
|
||||||
painted patch are the graded band that makes the size change conformal.
|
|
||||||
|
|
||||||
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
|
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
|
||||||
|
|
||||||
@@ -260,16 +291,11 @@ of sweeps: it holds every triangle that is over its criteria in a max-heap keyed
|
|||||||
it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle
|
it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle
|
||||||
across each edge) is built **once** and maintained incrementally through each bisection, so the cost
|
across each edge) is built **once** and maintained incrementally through each bisection, so the cost
|
||||||
scales with the refined region rather than with the whole model. `max_triangles` is the only bound;
|
scales with the refined region rather than with the whole model. `max_triangles` is the only bound;
|
||||||
stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest errors.
|
stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest
|
||||||
|
errors. A fixed sweep count instead spends itself grading the *coarse surroundings* - whose edges are
|
||||||
|
the longest, so they win every terminal-edge contest - and never reaches the painted patch.
|
||||||
|
|
||||||
This shape replaced a first version that ran a fixed 12 sweeps, each rebuilding a whole-mesh edge map and
|
**It carries the paint forward**, which is what makes it usable (uniform subdivide drops paint). Because
|
||||||
bisecting one terminal edge per active triangle. Two failure modes came out of that, and they are worth
|
|
||||||
remembering because they look like separate bugs and are not: the sweeps were consumed grading the
|
|
||||||
*coarse surroundings* (whose edges are the longest, so they win every terminal-edge contest), which both
|
|
||||||
**stopped refinement of the painted patch far short** of the requested detail and left the band outside
|
|
||||||
it looking wildly over-refined relative to the patch itself.
|
|
||||||
|
|
||||||
**It carries the paint forward**, which is what makes it usable (uniform/remesh both drop paint). Because
|
|
||||||
the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an
|
the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an
|
||||||
`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each
|
`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each
|
||||||
layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that
|
layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that
|
||||||
@@ -277,32 +303,30 @@ layer. `collect_paint_region()` derives both:
|
|||||||
- the union refine-region: **exactly** the original triangles the brush touched, read straight off
|
- the union refine-region: **exactly** the original triangles the brush touched, read straight off
|
||||||
`TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that
|
`TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that
|
||||||
is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No
|
is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No
|
||||||
dilation. An earlier version marked every triangle sharing a *vertex* with the patch, which drags in a
|
dilation: marking every triangle that shares a *vertex* with the patch drags in a whole fan of huge
|
||||||
whole fan of huge unpainted neighbours and then refines *those* down to the resolution floor, since the
|
unpainted neighbours and refines *those* down to the resolution floor, since the height field the
|
||||||
height field the detail test samples is not restricted to the painted area. The conformal closure
|
detail test samples is not restricted to the painted area. The conformal closure already grades the
|
||||||
already grades the size change outward on its own; it does not need help.
|
size change outward on its own.
|
||||||
- the per-layer fully-painted-triangle sets (a `get_facets_strict(ENFORCER)` sub-triangle with all three
|
- the per-layer fully-painted-triangle sets (a `get_facets_strict(ENFORCER)` sub-triangle with all three
|
||||||
*original* vertex indices == a whole, fully-painted original triangle; a partial stroke's sub-triangles
|
*original* vertex indices == a whole, fully-painted original triangle; a partial stroke's sub-triangles
|
||||||
always carry a split vertex).
|
always carry a split vertex).
|
||||||
|
|
||||||
The other four channels still ride the normal `restore_painting()` remap. Covered by a conformality unit
|
The other four channels ride the normal `restore_painting()` remap.
|
||||||
test (`every_edge_used_twice` on a partially-refined cube - an exact crack detector for a closed mesh),
|
|
||||||
plus tests that the target edge length is actually *reached* and that the budget caps the result without
|
|
||||||
opening a crack.
|
|
||||||
|
|
||||||
Both share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox picks
|
Both modes share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox
|
||||||
the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the wireframe
|
picks the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the
|
||||||
while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle count.
|
wireframe while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle
|
||||||
|
count.
|
||||||
|
|
||||||
**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"**
|
**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"**
|
||||||
checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. The insight:
|
checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. A flat region or
|
||||||
a flat region or a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp);
|
a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp); what needs them is
|
||||||
what needs them is **curvature** - the *second* derivative, not the gradient. So the extra predicate is a
|
**curvature** - the *second* derivative, not the gradient. So the extra predicate is a **chord-error**
|
||||||
**chord-error** test: sample the combined displacement at the triangle's three edge midpoints *and its
|
test: sample the combined displacement at the triangle's three edge midpoints *and its centroid*
|
||||||
centroid* (sampling the interior is what catches a bump sitting inside a triangle, the blind spot of an
|
(sampling the interior is what catches a bump sitting inside a triangle, the blind spot of an edge-only
|
||||||
edge-only test) and take the largest departure from the flat triangle's barycentric interpolation. Refine
|
test) and take the largest departure from the flat triangle's barycentric interpolation. Refine while
|
||||||
while that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp ⇒ untouched; high on
|
that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp ⇒ untouched; high on a
|
||||||
a bump/ridge/noise ⇒ refined until captured. Same conformal machinery, so still crack-free. The
|
bump/ridge/noise ⇒ refined until captured. Same conformal machinery, so still crack-free. The
|
||||||
per-triangle error is cached and recomputed only for the children of a split.
|
per-triangle error is cached and recomputed only for the children of a split.
|
||||||
|
|
||||||
Four knobs bracket it, and all four matter:
|
Four knobs bracket it, and all four matter:
|
||||||
@@ -320,33 +344,40 @@ The height field is `make_combined_displacement_sampler()` - it mirrors `build_t
|
|||||||
per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively")
|
per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively")
|
||||||
but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe -
|
but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe -
|
||||||
over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint
|
over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint
|
||||||
test), and edge-smoothing falloff is ignored. Note the first one is *why* the refine region must not be
|
test), and edge-smoothing falloff is ignored. The first is *why* the refine region must not be dilated -
|
||||||
dilated - outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no
|
outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no per-point UV); a
|
||||||
per-point UV); a purely LSCM stack yields a null sampler and the code falls back to the length baseline
|
purely LSCM stack yields a null sampler and the code falls back to the length baseline alone. Per-vertex
|
||||||
alone. Per-vertex heights are sampled lazily, so a small patch on a huge model never pays for the rest of
|
heights are sampled lazily, so a small patch on a huge model never pays for the rest of it.
|
||||||
it. Covered by unit tests: a Gaussian bump refines densely at its center and leaves flat corners coarse,
|
|
||||||
a linear ramp produces *zero* extra triangles (the case a gradient criterion would over-refine), and a
|
|
||||||
flat field still honours the max-edge baseline.
|
|
||||||
|
|
||||||
### Fast bump preview (GPU-only, no CPU meshing)
|
### Fast preview (GPU-only, no CPU meshing)
|
||||||
|
|
||||||
`resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}`, registered as
|
`resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}`, registered as
|
||||||
`"texture_displacement_bump"`. Perturbs the *shading* normal from the height texture's local
|
`"texture_displacement_bump"`. Shades the *displaced* surface without moving geometry - active-layer
|
||||||
gradient instead of moving geometry - active-layer-only, toggled via a "Fast preview (normal map)"
|
only, selected from the View row, and the default when the gizmo opens (`m_use_bump_preview = true`).
|
||||||
checkbox. Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the
|
Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the per-vertex paint
|
||||||
per-vertex paint weight (0/1) and `tex_coord` carries a precomputed texture UV, so it can use
|
weight (0/1), `normal.y` flags the UV island currently being dragged, and `tex_coord` carries a
|
||||||
`GLModel` normally instead of needing a hand-rolled VBO/VAO manager. Weight buffer is
|
precomputed texture UV, so it can use `GLModel` normally instead of a hand-rolled VBO/VAO manager.
|
||||||
rebuilt at the same cadence as the true-displacement preview (stroke-end/slider-release), using the
|
|
||||||
**live** `TriangleSelector` state (not the flushed model facets), so it doesn't lag by a full model
|
The mesh is **flat** (vertices not shared between triangles): every corner of a painted triangle gets
|
||||||
round-trip.
|
weight 1, every corner of an unpainted one weight 0. A coarse mesh needs that - one painted face of a raw
|
||||||
|
cube has no strictly-interior vertex, so per-vertex weighting would either bleed onto the neighbours or
|
||||||
|
vanish outright. Duplicating vertices costs no shading quality here because the shader takes its surface
|
||||||
|
normal from screen-space derivatives of position, not from a per-vertex normal.
|
||||||
|
|
||||||
|
**Both preview meshes work in the patch's vertex space, not the mesh's.** Those agree only until a
|
||||||
|
*brush* stroke splits a triangle: `get_facets_strict()` then appends the split vertices, so the patch
|
||||||
|
array is longer. `rebuild_bump_preview_mesh()` and `rebuild_uvcheck_mesh()` therefore index
|
||||||
|
`patch.vertices` throughout. The weight buffer is rebuilt at the same cadence as the true-displacement
|
||||||
|
preview (stroke-end/slider-release) but from the **live** `TriangleSelector` state, not the flushed model
|
||||||
|
facets, so it does not lag by a full model round-trip.
|
||||||
|
|
||||||
The perturbed normal is the analytic one for a height field `H = ±depth_mm · h(uv)` displaced along
|
The perturbed normal is the analytic one for a height field `H = ±depth_mm · h(uv)` displaced along
|
||||||
`N` over any orthonormal surface tangent pair `T`/`B`:
|
`N` over any orthonormal surface tangent pair `T`/`B`:
|
||||||
|
|
||||||
N' = normalize(N − (dH/da)·T − (dH/db)·B), a = dot(p,T), b = dot(p,B)
|
N' = normalize(N − (dH/da)·T − (dH/db)·B), a = dot(p,T), b = dot(p,B)
|
||||||
|
|
||||||
The two slopes have to be genuine **mm-per-mm** derivatives for the preview's apparent depth to
|
The two slopes have to be genuine **mm-per-mm** derivatives for the preview's apparent depth to match
|
||||||
match the bake's - see bug #13.
|
the bake's.
|
||||||
|
|
||||||
**Two projection paths (`use_vertex_uv` uniform):**
|
**Two projection paths (`use_vertex_uv` uniform):**
|
||||||
- **Triplanar (`use_vertex_uv = 0`)** - `uv` and the `T`/`B` axes are both derived in-shader from
|
- **Triplanar (`use_vertex_uv = 0`)** - `uv` and the `T`/`B` axes are both derived in-shader from
|
||||||
@@ -354,32 +385,58 @@ match the bake's - see bug #13.
|
|||||||
formed analytically. `T`/`B` are the projection's axis-aligned pair, exact only when the face is
|
formed analytically. `T`/`B` are the projection's axis-aligned pair, exact only when the face is
|
||||||
axis-aligned; the shader drops the along-normal component to keep the gradient in the surface. Here
|
axis-aligned; the shader drops the along-normal component to keep the gradient in the surface. Here
|
||||||
one `uv` unit is exactly `tiling_scale` mm, so the `1/tiling_scale` gradient factor is right.
|
one `uv` unit is exactly `tiling_scale` mm, so the `1/tiling_scale` gradient factor is right.
|
||||||
- **Precomputed UV (`use_vertex_uv = 1`, used for LSCM)** - `uv` comes per-vertex from the CPU
|
- **Precomputed UV (`use_vertex_uv = 1`, used for LSCM and ViewProjected)** - `uv` comes per-vertex from
|
||||||
(`compute_lscm_uvs(patch, layer)`, so island placement + tiling/rotation/offset are already folded
|
the CPU (`compute_layer_vertex_uvs()`, so island placement + tiling/rotation/offset are already folded
|
||||||
in), and the perturbed normal is built with **Mikkelsen's method** ("Bump Mapping Unparametrized
|
in), and the perturbed normal is built with **Mikkelsen's method** ("Bump Mapping Unparametrized
|
||||||
Surfaces on the GPU"): the surface gradient taken directly from the screen-space derivatives of the
|
Surfaces on the GPU"): the surface gradient taken directly from the screen-space derivatives of the
|
||||||
*sampled height* and position. **This makes no uv→mm scale assumption**, which is essential -
|
*sampled height* and position. **This makes no uv→mm scale assumption**, which is essential, because an
|
||||||
the first cut used the same global `1/tiling_scale` factor as triplanar and the depth came out
|
LSCM map is **conformal, not isometric**: it is globally area-scaled but the *local* mm-per-uv varies
|
||||||
visibly wrong, because an LSCM map is **conformal, not isometric**: it is globally area-scaled but
|
across the chart, so a single global `1/tiling_scale` factor gets the apparent depth wrong. `dFdx(h)`
|
||||||
the *local* mm-per-uv varies across the chart. `dFdx(h)` captures the true on-screen rate of change
|
captures the true on-screen rate of change however the chart is stretched. This path is also what makes
|
||||||
however the chart is stretched. **This path is also what makes the fast preview follow the UV
|
the fast preview follow the UV editor: move an island and its uv - hence its shading - moves with it
|
||||||
editor: move an island and its uv - hence its bump - moves with it** (the bump mesh rebuilds on
|
(the mesh rebuilds on drag-end, `on_island_edited(finished)` → `rebuild_preview()` →
|
||||||
drag-end, since `on_island_edited(finished)` → `rebuild_preview()` → `rebuild_bump_preview_mesh()`).
|
`rebuild_bump_preview_mesh()`). The branch is uniform and the paint weight gates by multiply, so the
|
||||||
The branch is uniform (`use_vertex_uv` is a uniform) and the paint weight gates by multiply, so the
|
|
||||||
texture derivatives stay well defined. A triangle straddling a seam has a discontinuous uv → the
|
texture derivatives stay well defined. A triangle straddling a seam has a discontinuous uv → the
|
||||||
`det≈0` guard skips it (a localised preview-only artifact, never in the bake).
|
`det≈0` guard skips it (a localised preview-only artifact, never in the bake).
|
||||||
|
|
||||||
Remaining deliberate approximation: the GPU sampler's wrap mode stands in for
|
**Parallax (triplanar path).** Perturbing the shading normal alone welds the pattern to the base surface:
|
||||||
`tile_enabled`/`tile_method`, so with tiling *off* the GPU repeats where the CPU returns 0 outside
|
it does not slide as the camera orbits, and does not get deeper as `depth_mm` grows. The triplanar path
|
||||||
|
therefore shades at the point the *displaced* surface would show at this pixel, found by **ray marching**
|
||||||
|
(parallax occlusion mapping). A point at ray parameter `s`, i.e. `P + V·s` (`P` the base point, `V` the
|
||||||
|
unit direction to the eye), sits at height `s·dot(V,n)` above the undisplaced surface. The displaced
|
||||||
|
surface lives in a shell between the extreme values of `amp·(h − midlevel)` - taken from both ends of
|
||||||
|
`h ∈ [0,1]`, so it holds for an inverted layer and a raised midlevel too, where the surface sits *below*
|
||||||
|
the undisplaced one. The march starts at the top of that shell, where the ray is outside the surface by
|
||||||
|
construction, and steps inward until the ray height drops below the sampled height. That crossing *is*
|
||||||
|
the visible point.
|
||||||
|
|
||||||
|
Solving `Q = P + V·(H(Q)/dot(V,n))` by fixed-point iteration instead is geometrically exact but the
|
||||||
|
divisor goes to zero edge-on; the sample then lands a large fraction of a tile away and the iteration
|
||||||
|
oscillates, which reads as a second, flat copy of the pattern ghosted over the real one. Clamping the
|
||||||
|
step to one tile does not help - a tile-sized shift lands on the neighbouring tile, the same pattern
|
||||||
|
again. Offset limiting (stepping along the tangential part of `V`) is stable but understates parallax
|
||||||
|
enough that the relief still flattens as soon as the camera tilts. Marching has neither problem.
|
||||||
|
|
||||||
|
The hit is interpolated between the last two samples, which keeps `PARALLAX_STEPS` (24) affordable, and
|
||||||
|
the whole march is skipped when sweeping the shell would move the sample point less than half a texel -
|
||||||
|
the head-on case, so the common view pays almost nothing. The 140 variant samples with
|
||||||
|
`textureLod(…, 0.0)` inside the loop, since implicit derivatives are undefined in non-uniform control
|
||||||
|
flow. Two uniforms exist for this: `midlevel` (parallax needs the real height, not just its derivative)
|
||||||
|
and `eye_model_pos` (the camera in the volume's local frame).
|
||||||
|
|
||||||
|
Parallax cannot change the model's silhouette or cast shadows; the View row's Normal mode is one click
|
||||||
|
away for that. The LSCM path stays plain Mikkelsen bump - it has no closed-form uv, so there is no cheap
|
||||||
|
way to re-project a marched position. One further approximation: the GPU sampler's wrap mode stands in
|
||||||
|
for `tile_enabled`/`tile_method`, so with tiling *off* the GPU repeats where the CPU returns 0 outside
|
||||||
`[0,1)`.
|
`[0,1)`.
|
||||||
|
|
||||||
### On-canvas "Adjust Texture" gizmo
|
### On-canvas "Adjust Texture" gizmo
|
||||||
|
|
||||||
A per-active-layer toggle ("Adjust placement (drag on model)") that disables painting and shows a
|
A per-active-layer toggle ("Adjust placement") that disables painting and shows a flat pan panel (free
|
||||||
flat pan panel (free 2D drag on both axes) plus two arrows along the patch's own U/V axes
|
2D drag on both axes) plus two arrows along the patch's own U/V axes (constrained single-axis drag).
|
||||||
(constrained single-axis drag). Anchored to the painted patch's centroid/average-normal
|
Anchored to the painted patch's centroid/average-normal (`compute_layer_paint_anchor()`). Hit-testing is
|
||||||
(`compute_layer_paint_anchor()`). Hit-testing is screen-space distance/point-to-segment (not real
|
screen-space distance/point-to-segment, not real 3D ray intersection against the handle geometry - simple
|
||||||
3D ray intersection against the handle geometry) - simple and good enough at this handle size.
|
and good enough at this handle size.
|
||||||
|
|
||||||
### Projection frame overlay (ViewProjected)
|
### Projection frame overlay (ViewProjected)
|
||||||
|
|
||||||
@@ -393,14 +450,14 @@ position and size *are* the placement, read on demand at Apply - which is also w
|
|||||||
visible-facet raycast runs. So dragging it is free and nothing recomputes until asked.
|
visible-facet raycast runs. So dragging it is free and nothing recomputes until asked.
|
||||||
|
|
||||||
Plain 2D (`wxPaintDC`), not a `wxGLCanvas`: a second GL canvas would have to share the app's one real
|
Plain 2D (`wxPaintDC`), not a `wxGLCanvas`: a second GL canvas would have to share the app's one real
|
||||||
`wxGLContext`, the cause of bugs #10 and #14 below. It only ever draws a bitmap and a border.
|
`wxGLContext`. It only ever draws a bitmap and a border.
|
||||||
|
|
||||||
**The projective mapping (`apply_projection_frame()`)**. The frame defines a
|
**The projective mapping (`apply_projection_frame()`)**. The frame defines a **screen-space** rectangle,
|
||||||
**screen-space** rectangle, but the bake samples from a **local-space** position, so the two have to be
|
but the bake samples from a **local-space** position, so the two have to be reconciled.
|
||||||
reconciled. `view_project_right/up` can only express an *affine* projection - exact under an
|
`view_project_right/up` can only express an *affine* projection - exact under an orthographic camera, but
|
||||||
orthographic camera, but wrong under perspective, where the near end of a part projects larger than the
|
wrong under perspective, where the near end of a part projects larger than the far end and no pair of
|
||||||
far end and no pair of axes reproduces that. So the layer instead stores a full projective map
|
axes reproduces that. So the layer instead stores a full projective map (`view_project_matrix`, row-major
|
||||||
(`view_project_matrix`, row-major 3×4, `uv = (row0·p̃/row2·p̃, row1·p̃/row2·p̃)`), built like this:
|
3×4, `uv = (row0·p̃/row2·p̃, row1·p̃/row2·p̃)`), built like this:
|
||||||
|
|
||||||
- `K = projection · view · (instance · volume)`, i.e. local → clip, the same product the renderer uses.
|
- `K = projection · view · (instance · volume)`, i.e. local → clip, the same product the renderer uses.
|
||||||
Note `Camera::get_projection_matrix()` is typed `Transform3d` (nominally affine) but its perspective
|
Note `Camera::get_projection_matrix()` is typed `Transform3d` (nominally affine) but its perspective
|
||||||
@@ -433,7 +490,7 @@ reopening keeps it where it was left.
|
|||||||
`UVEditorCanvas` (`src/slic3r/GUI/UVEditorCanvas.hpp/.cpp`) - a standalone `wxGLCanvas` rendering the
|
`UVEditorCanvas` (`src/slic3r/GUI/UVEditorCanvas.hpp/.cpp`) - a standalone `wxGLCanvas` rendering the
|
||||||
flattened LSCM islands (per-island wireframe + outline + fill) over the height texture (background
|
flattened LSCM islands (per-island wireframe + outline + fill) over the height texture (background
|
||||||
quad tiled across the whole unwrap), with mouse pan/zoom. It is wrapped in a **`UVEditorPanel`**
|
quad tiled across the whole unwrap), with mouse pan/zoom. It is wrapped in a **`UVEditorPanel`**
|
||||||
(same file) that adds a button row (Frame / Snap / Average scale) and a status line
|
(same file) that adds a button row (Frame / Snap / Avg scale / Cut / Join / Unjoin) and a status line
|
||||||
along the bottom naming the current gesture and the shortcuts in play. The *panel* is what is
|
along the bottom naming the current gesture and the shortcuts in play. The *panel* is what is
|
||||||
registered as a `wxAuiPaneInfo` pane on `Plater`'s `m_aui_mgr`; `Plater::show_uv_editor(bool)`
|
registered as a `wxAuiPaneInfo` pane on `Plater`'s `m_aui_mgr`; `Plater::show_uv_editor(bool)`
|
||||||
shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame), and
|
shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame), and
|
||||||
@@ -445,51 +502,24 @@ same call `View3D`/`Preview`/`AssembleView` make) rather than creating an indepe
|
|||||||
`"flat"`/`"flat_texture"` shaders and `GLModel` as-is, instead of needing its own shader
|
`"flat"`/`"flat_texture"` shaders and `GLModel` as-is, instead of needing its own shader
|
||||||
compilation/VBO management.
|
compilation/VBO management.
|
||||||
|
|
||||||
**Geometry is uploaded once, in the unwrap's own (raw, mm) coordinates**, one `GLModel` set per
|
**Geometry is uploaded once, in the unwrap's own (raw, mm) coordinates**, one `GLModel` set per island;
|
||||||
island; each island is then drawn through its own 2x3 affine (`island_transform_matrix()` composed
|
each island is then drawn through its own 2x3 affine (`island_transform_matrix()` composed with the
|
||||||
with the layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. A
|
layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. A drag updates one
|
||||||
drag updates one matrix per island and touches no vertex
|
matrix per island and touches no vertex buffer - `on_island_edited(!finished)` calls only
|
||||||
buffer - `on_island_edited(!finished)` calls only `set_island_transforms()`, and the full
|
`set_island_transforms()`, and the full `set_islands()` rebuild happens solely when the unwrap itself
|
||||||
`set_islands()` rebuild happens solely when the unwrap itself changes (`unwrap_changed` in
|
changes (`unwrap_changed` in `update_uv_editor()`).
|
||||||
`update_uv_editor()`).
|
|
||||||
|
|
||||||
**Gestures** (canvas-owned, reported to the gizmo as incremental deltas via `IslandEditFn`): left-drag
|
**Gestures** (canvas-owned, reported to the gizmo as incremental deltas via `IslandEditFn`): left-drag
|
||||||
= move, right-drag or **R** = rotate (hold **Shift** to snap to 15° steps - quantised on the
|
= move, right-drag or **R** = rotate (hold **Shift** to snap to 15° steps - quantised on the
|
||||||
*cumulative* rotation, not each delta, so it doesn't judder, and accumulated incrementally so it
|
*cumulative* rotation, not each delta, so it doesn't judder, and accumulated incrementally so it
|
||||||
survives crossing ±180°), **S** = scale (R/S modal, click/Enter to confirm, Esc to cancel), wheel =
|
survives crossing ±180°), **S** = scale (R/S modal, click/Enter to confirm, Esc to cancel), wheel =
|
||||||
zoom about the cursor, middle-drag = pan, **Home**/**F** = frame all. Scale writes
|
zoom about the cursor, middle-drag = pan, **Home**/**F** = frame all. Scale writes
|
||||||
`TextureIsland::scale`; "Average scale" (`average_island_scales()`) sets every island to the mean, so
|
`TextureIsland::scale`; "Avg scale" (`average_island_scales()`) sets every island to the mean, so
|
||||||
one island scaled by hand can be matched back to its neighbours' texel density. **Snap** (canvas-owned
|
one island scaled by hand can be matched back to its neighbours' texel density. **Snap** (canvas-owned
|
||||||
`m_snap_enabled`, toggled from the toolbar) sticks a dragged island's nearest boundary vertex onto a
|
`m_snap_enabled`, toggled from the toolbar) sticks a dragged island's nearest boundary vertex onto a
|
||||||
neighbouring island's at drag-*end* only - a magnet that re-applies mid-drag is very hard to pull out
|
neighbouring island's at drag-*end* only - a magnet that re-applies mid-drag is very hard to pull out
|
||||||
of. Toolbar commands the canvas can't service itself (Average scale) are forwarded to the gizmo via
|
of. Toolbar commands the canvas can't service itself (Avg scale, Cut, Join, Unjoin) are forwarded to the
|
||||||
`CommandFn`; view-only ones (Frame, Snap) it handles directly.
|
gizmo via `CommandFn`; view-only ones (Frame, Snap) it handles directly.
|
||||||
|
|
||||||
## Known limitations / deferred work
|
|
||||||
|
|
||||||
- **No `.3mf` serialization** for texture-displacement paint data or texture assets. A background
|
|
||||||
agent attempted this in an earlier session, hit its own usage limit mid-edit, and left
|
|
||||||
`bbs_3mf.cpp` with an undefined forward-declared function; that partial edit was reverted rather
|
|
||||||
than shipped broken. Practical impact: **baked** geometry round-trips fine (it's just an ordinary
|
|
||||||
part of the mesh via the existing mesh serialization path) - what does *not* survive a project
|
|
||||||
save/reload is any *unbaked* paint stroke and texture layer definition.
|
|
||||||
- **No remap-across-topology-change** for texture-displacement paint (`ModelObject::split()`, mesh
|
|
||||||
boolean ops, Simplify, uniform subdivide, and remesh all drop it via `reset_extra_facets()`). The
|
|
||||||
other four paint channels (supported/seam/mmu/fuzzy) do get remapped in these cases. The lone
|
|
||||||
exception is **adaptive subdivide**, which carries texture-displacement paint forward itself via its
|
|
||||||
source map (see the Subdivision section) - a targeted remap that only works because the operation is
|
|
||||||
driven by the paint.
|
|
||||||
- **Cylindrical/Spherical axis/center are auto-picked heuristically**, not user-controllable - no
|
|
||||||
UI to override the auto-detected wrap axis if it picks the "wrong" one for an odd shape.
|
|
||||||
- **Fast preview covers the active layer only** — and is now the **default** view when the gizmo
|
|
||||||
opens (`m_use_bump_preview = true`): it is the instant, no-CPU-meshing preview, so it is the better
|
|
||||||
first impression while painting. The exact true-displacement view is one click away in the View row.
|
|
||||||
- **Displacement resolution is capped by the mesh's own vertex density.** Baking only ever *moves*
|
|
||||||
existing vertices (it never inserts any), so a coarse patch cannot show fine texture detail no
|
|
||||||
matter how high-resolution the height map is - that is what the subdivision controls are for
|
|
||||||
(uniform, adaptive, or feature-adaptive; see the Subdivision section). Since the rewrite the bake is
|
|
||||||
topology-preserving, so this is now a hard, explicit property rather than something partly papered
|
|
||||||
over by the old per-layer re-meshing.
|
|
||||||
|
|
||||||
## File map
|
## File map
|
||||||
|
|
||||||
@@ -498,23 +528,18 @@ of. Toolbar commands the canvas can't service itself (Average scale) are forward
|
|||||||
tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing
|
tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing
|
||||||
(`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc
|
(`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc
|
||||||
comments throughout, they're kept accurate and up to date.
|
comments throughout, they're kept accurate and up to date.
|
||||||
- `src/libslic3r/MeshBoolean.hpp/.cpp` - added `parameterize_lscm()` and `remesh_isotropic()`
|
- `src/libslic3r/MeshBoolean.hpp/.cpp` - `parameterize_lscm()` and `remesh_isotropic()` in the `cgal`
|
||||||
in the `cgal` sub-namespace,
|
sub-namespace, reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already
|
||||||
reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already there for
|
there for mesh boolean ops. CGAL includes: `Polygon_mesh_processing/border.h`,
|
||||||
mesh boolean ops. New CGAL includes: `Polygon_mesh_processing/border.h`,
|
|
||||||
`Polygon_mesh_processing/connected_components.h`, `Surface_mesh_parameterization/{Error_code,
|
`Polygon_mesh_processing/connected_components.h`, `Surface_mesh_parameterization/{Error_code,
|
||||||
LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and
|
LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and the
|
||||||
the `Surface_mesh_parameterization` package headers were already present, just unused before now.
|
`Surface_mesh_parameterization` package headers were already present.
|
||||||
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
|
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
|
||||||
`texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points
|
`texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points
|
||||||
(see Data model above).
|
(see Data model above).
|
||||||
|
|
||||||
**GUI:**
|
**GUI:**
|
||||||
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo. Panel controls: dock/
|
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo and its whole panel.
|
||||||
undock toggle, brush/face/connected-area selection mode + "select whole model" button, per-layer
|
|
||||||
texture picker + depth/tiling/rotation/invert/tile-mode/projection-mode/blend-mode controls,
|
|
||||||
"Adjust placement" toggle (on-canvas gizmo), "Fast preview (normal map)" toggle, "Subdivide model"
|
|
||||||
button, Add layer/Erase all/Bake.
|
|
||||||
- `src/slic3r/GUI/TextureLibrary.hpp/.cpp` - scans the shipped + user texture folders, imports an
|
- `src/slic3r/GUI/TextureLibrary.hpp/.cpp` - scans the shipped + user texture folders, imports an
|
||||||
arbitrary image into the user folder (converting it to the 8-bit grayscale PNG libslic3r decodes),
|
arbitrary image into the user folder (converting it to the 8-bit grayscale PNG libslic3r decodes),
|
||||||
and loads a library file's bytes for a layer. The image→grayscale-PNG conversion lives here, on the
|
and loads a library file's bytes for a layer. The image→grayscale-PNG conversion lives here, on the
|
||||||
@@ -534,17 +559,22 @@ of. Toolbar commands the canvas can't service itself (Average scale) are forward
|
|||||||
- `src/slic3r/GUI/Plater.hpp/.cpp` - `uv_editor_canvas` member, AUI pane registration,
|
- `src/slic3r/GUI/Plater.hpp/.cpp` - `uv_editor_canvas` member, AUI pane registration,
|
||||||
`get_uv_editor_canvas()`/`show_uv_editor()`.
|
`get_uv_editor_canvas()`/`show_uv_editor()`.
|
||||||
- `src/slic3r/GUI/GLShadersManager.cpp` - registers `"texture_displacement_bump"`.
|
- `src/slic3r/GUI/GLShadersManager.cpp` - registers `"texture_displacement_bump"`.
|
||||||
- `resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}` - the bump-preview shader.
|
- `resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}` - the fast-preview shader.
|
||||||
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`.
|
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`.
|
||||||
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration.
|
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration.
|
||||||
- `src/slic3r/GUI/ImGuiWrapper.cpp` - the light-mode checkmark-color fix
|
|
||||||
|
|
||||||
**Tests:** `tests/libslic3r/test_texture_displacement.cpp` - **run and passing** (7 cases, 116
|
## Tests
|
||||||
assertions). Covers `decode_height_texture` round-trip, empty-layer no-op, full-cube uniform
|
|
||||||
displacement, boundary-vertex pinning on a hand-built fan mesh, and - added with the bake rewrite -
|
`tests/libslic3r/test_texture_displacement.cpp`. Covers `decode_height_texture` round-trip, empty-layer
|
||||||
a regression test that a **second layer over the same area actually contributes**,
|
no-op, full-cube uniform displacement, a second layer over the same area contributing, all four blend
|
||||||
a table-driven check of all four blend modes, and that the lowest layer ignores its
|
modes (table-driven), the lowest layer ignoring its blend mode, border displace/pin, post-process
|
||||||
blend mode. `BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them:
|
smoothing and its mask guarantees, and adaptive subdivision: conformality (`every_edge_used_twice` on a
|
||||||
|
partially-refined cube - an exact crack detector for a closed mesh), the target edge length actually
|
||||||
|
being reached, the triangle budget capping the result without opening a crack, curvature-driven
|
||||||
|
refinement (a Gaussian bump refines at its centre, a linear ramp adds nothing), and the max-edge
|
||||||
|
baseline.
|
||||||
|
|
||||||
|
`BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them:
|
||||||
|
|
||||||
cmake -S . -B build -DBUILD_TESTS=ON
|
cmake -S . -B build -DBUILD_TESTS=ON
|
||||||
cmake --build build --config Release --target libslic3r_tests -- -m
|
cmake --build build --config Release --target libslic3r_tests -- -m
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#define INTENSITY_CORRECTION 0.6
|
#define INTENSITY_CORRECTION 0.6
|
||||||
|
|
||||||
|
#define PARALLAX_STEPS 24
|
||||||
|
#define H_AT(uv) texture2D(height_tex, uv).r
|
||||||
|
|
||||||
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
||||||
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
||||||
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
|
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
|
||||||
@@ -30,6 +33,8 @@ uniform float tiling_scale;
|
|||||||
uniform float rotation_rad;
|
uniform float rotation_rad;
|
||||||
uniform vec2 uv_offset;
|
uniform vec2 uv_offset;
|
||||||
uniform bool invert;
|
uniform bool invert;
|
||||||
|
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||||
|
uniform vec3 eye_model_pos; // camera position in this volume's local space, for the view ray
|
||||||
uniform bool use_vertex_uv;
|
uniform bool use_vertex_uv;
|
||||||
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
|
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
|
||||||
// 140 variant. Identity when nothing is dragged.
|
// 140 variant. Identity when nothing is dragged.
|
||||||
@@ -96,10 +101,50 @@ void main()
|
|||||||
if (abs(det) > 1e-12)
|
if (abs(det) > 1e-12)
|
||||||
triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det);
|
triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det);
|
||||||
} else if (weight > 0.0) {
|
} else if (weight > 0.0) {
|
||||||
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
|
|
||||||
vec3 t, b;
|
vec3 t, b;
|
||||||
projection_axes(triangle_normal, t, b);
|
projection_axes(triangle_normal, t, b);
|
||||||
|
|
||||||
|
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||||
|
// first point where it drops below the displaced surface (see header).
|
||||||
|
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||||
|
vec3 view_dir = normalize(eye_model_pos - model_pos.xyz);
|
||||||
|
float v_dot_n = dot(view_dir, triangle_normal);
|
||||||
|
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
|
||||||
|
|
||||||
|
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||||
|
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||||
|
// where the surface sits *below* the undisplaced one.
|
||||||
|
float h_end_a = amp * (0.0 - midlevel);
|
||||||
|
float h_end_b = amp * (1.0 - midlevel);
|
||||||
|
float h_hi = max(h_end_a, h_end_b);
|
||||||
|
float h_lo = min(h_end_a, h_end_b);
|
||||||
|
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
|
||||||
|
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||||
|
// case of looking straight down at a surface.
|
||||||
|
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||||
|
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||||
|
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||||
|
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||||
|
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||||
|
float s = h_hi / v_dot_n;
|
||||||
|
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||||
|
vec2 prev_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
|
||||||
|
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||||
|
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||||
|
s -= ds;
|
||||||
|
vec2 cur_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
|
||||||
|
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||||
|
if (gap <= 0.0) {
|
||||||
|
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||||
|
// quantising to the step size, and so what keeps the step count affordable.
|
||||||
|
uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prev_uv = cur_uv;
|
||||||
|
prev_gap = gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
|
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
|
||||||
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
|
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
|
||||||
float hD = texture2D(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
|
float hD = texture2D(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
|
||||||
|
|||||||
@@ -19,7 +19,36 @@
|
|||||||
// Two projection paths:
|
// Two projection paths:
|
||||||
// * Triplanar (use_vertex_uv = 0): uv and the tangent axes are derived in-shader from the dominant
|
// * Triplanar (use_vertex_uv = 0): uv and the tangent axes are derived in-shader from the dominant
|
||||||
// normal axis, mirroring libslic3r's project_planar()/apply_uv_transform(), and the slope is
|
// normal axis, mirroring libslic3r's project_planar()/apply_uv_transform(), and the slope is
|
||||||
// formed analytically (there is a closed-form uv, so 1 uv unit is exactly tiling_scale mm).
|
// formed analytically (there is a closed-form uv, so 1 uv unit is exactly tiling_scale mm). This
|
||||||
|
// path also runs a parallax step before shading, see below.
|
||||||
|
//
|
||||||
|
// Parallax. A pure bump map perturbs shading only, so the pattern is welded to the base surface: it
|
||||||
|
// does not shift as the camera orbits and it does not get any deeper as depth_mm grows, which is
|
||||||
|
// exactly when the preview stops reading as real geometry. The triplanar path therefore shades at the
|
||||||
|
// point the *displaced* surface would show at this pixel rather than at the pixel's own base position.
|
||||||
|
//
|
||||||
|
// Two cheaper formulations were tried first and both are wrong here, which is worth recording:
|
||||||
|
// * Solving Q = P + V * (H(Q) / dot(V, n)) by fixed-point iteration. Geometrically exact, but the
|
||||||
|
// divisor goes to zero edge-on, and an unbounded step is not a small error - the sample lands a
|
||||||
|
// large fraction of a tile away and the iteration oscillates instead of converging. It reads as a
|
||||||
|
// *second, flat copy* of the pattern ghosted over the real one. Clamping the step to one tile does
|
||||||
|
// not help either: a tile-sized shift lands on the neighbouring tile, which is the same pattern.
|
||||||
|
// * Offset limiting (Welsh): step along the tangential part of V, whose length caps the shift at one
|
||||||
|
// depth. Stable and cheap, but it understates parallax by exactly the factor that matters - the
|
||||||
|
// relief still flattens as soon as the camera tilts, which is the complaint it was meant to fix.
|
||||||
|
//
|
||||||
|
// So this ray-marches instead (parallax occlusion mapping). A point at ray parameter s, i.e. P + V * s,
|
||||||
|
// sits at height s * dot(V, n) above the undisplaced surface. The displaced surface lives in a shell
|
||||||
|
// between the extreme values of amp * (h - midlevel); the march starts at the top of that shell, where
|
||||||
|
// the ray is outside the surface by construction, and steps inward until the ray height falls below the
|
||||||
|
// sampled height. That crossing *is* the visible point - no divergence, no ghosting, and parallax stays
|
||||||
|
// correct at any angle. The hit is interpolated between the last two samples, which is what keeps
|
||||||
|
// PARALLAX_STEPS low enough to afford. The march is skipped when sweeping the shell would move the
|
||||||
|
// sample point less than half a texel (the head-on case), so the common view pays almost nothing.
|
||||||
|
//
|
||||||
|
// The gradient/shading below is evaluated at the resulting uv, so the relief both slides correctly
|
||||||
|
// under camera motion and visibly deepens with depth_mm. What it still cannot do is change the
|
||||||
|
// model's silhouette or cast shadows; for that, switch the View row to Normal.
|
||||||
// * Precomputed uv (use_vertex_uv = 1, used for LSCM): uv comes per-vertex from the CPU (the LSCM
|
// * Precomputed uv (use_vertex_uv = 1, used for LSCM): uv comes per-vertex from the CPU (the LSCM
|
||||||
// unwrap with island placement + tiling/rotation/offset already folded in), and the perturbed
|
// unwrap with island placement + tiling/rotation/offset already folded in), and the perturbed
|
||||||
// normal is built with Mikkelsen's method -- the surface gradient taken straight from the
|
// normal is built with Mikkelsen's method -- the surface gradient taken straight from the
|
||||||
@@ -31,6 +60,11 @@
|
|||||||
|
|
||||||
#define INTENSITY_CORRECTION 0.6
|
#define INTENSITY_CORRECTION 0.6
|
||||||
|
|
||||||
|
#define PARALLAX_STEPS 24
|
||||||
|
// Explicit LOD: the march samples inside non-uniform control flow, where implicit
|
||||||
|
// derivatives are undefined.
|
||||||
|
#define H_AT(uv) textureLod(height_tex, uv, 0.0).r
|
||||||
|
|
||||||
// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31)
|
// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31)
|
||||||
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
||||||
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
||||||
@@ -58,6 +92,8 @@ uniform float tiling_scale;
|
|||||||
uniform float rotation_rad;
|
uniform float rotation_rad;
|
||||||
uniform vec2 uv_offset;
|
uniform vec2 uv_offset;
|
||||||
uniform bool invert;
|
uniform bool invert;
|
||||||
|
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||||
|
uniform vec3 eye_model_pos; // camera position in this volume's local space, for the view ray
|
||||||
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
|
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
|
||||||
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
|
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
|
||||||
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
|
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
|
||||||
@@ -144,10 +180,50 @@ void main()
|
|||||||
// Triplanar path: uv and the tangent axes are reconstructed in-shader from the dominant
|
// Triplanar path: uv and the tangent axes are reconstructed in-shader from the dominant
|
||||||
// normal component (see header). The gradient is expressed analytically because there is a
|
// normal component (see header). The gradient is expressed analytically because there is a
|
||||||
// closed-form uv here, unlike the LSCM case.
|
// closed-form uv here, unlike the LSCM case.
|
||||||
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
|
|
||||||
vec3 t, b;
|
vec3 t, b;
|
||||||
projection_axes(triangle_normal, t, b);
|
projection_axes(triangle_normal, t, b);
|
||||||
|
|
||||||
|
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||||
|
// first point where it drops below the displaced surface (see header).
|
||||||
|
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||||
|
vec3 view_dir = normalize(eye_model_pos - model_pos.xyz);
|
||||||
|
float v_dot_n = dot(view_dir, triangle_normal);
|
||||||
|
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
|
||||||
|
|
||||||
|
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||||
|
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||||
|
// where the surface sits *below* the undisplaced one.
|
||||||
|
float h_end_a = amp * (0.0 - midlevel);
|
||||||
|
float h_end_b = amp * (1.0 - midlevel);
|
||||||
|
float h_hi = max(h_end_a, h_end_b);
|
||||||
|
float h_lo = min(h_end_a, h_end_b);
|
||||||
|
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
|
||||||
|
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||||
|
// case of looking straight down at a surface.
|
||||||
|
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||||
|
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||||
|
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||||
|
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||||
|
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||||
|
float s = h_hi / v_dot_n;
|
||||||
|
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||||
|
vec2 prev_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
|
||||||
|
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||||
|
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||||
|
s -= ds;
|
||||||
|
vec2 cur_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
|
||||||
|
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||||
|
if (gap <= 0.0) {
|
||||||
|
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||||
|
// quantising to the step size, and so what keeps the step count affordable.
|
||||||
|
uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prev_uv = cur_uv;
|
||||||
|
prev_gap = gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
|
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
|
||||||
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
|
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
|
||||||
float hD = texture(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
|
float hD = texture(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
|
||||||
|
|||||||
@@ -747,9 +747,13 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
|||||||
if (patch.indices.empty())
|
if (patch.indices.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const indexed_triangle_set &base = mv->mesh().its;
|
// No "patch.vertices.size() == mesh vertex count" check here, and that is the point: a *brush*
|
||||||
if (base.vertices.size() != patch.vertices.size())
|
// stroke splits triangles, so the selector appends split vertices and the patch array is longer
|
||||||
return; // shouldn't happen: get_facets_strict() always returns the full vertex array
|
// than the mesh's. An earlier version bailed out on that as "shouldn't happen", which meant the
|
||||||
|
// bump model was never built while brushing and render_painter_gizmo() silently fell back to the
|
||||||
|
// Normal (true-displacement) preview - Fast looked broken for brush and fine for Face/Connected
|
||||||
|
// area, because only the brush splits. Everything below indexes the patch's own vertex array, so
|
||||||
|
// the extra vertices are simply carried through.
|
||||||
|
|
||||||
// Unpainted triangles, so the surrounding surface still renders (the render path hides the real
|
// Unpainted triangles, so the surrounding surface still renders (the render path hides the real
|
||||||
// model in bump mode). get_facets_strict() returns the same vertex array whatever state is asked.
|
// model in bump mode). get_facets_strict() returns the same vertex array whatever state is asked.
|
||||||
@@ -762,7 +766,7 @@ void GLGizmoTextureDisplacement::rebuild_bump_preview_mesh()
|
|||||||
// mesh rebuilds (on drag end) with them. The other projections keep projecting in-shader.
|
// mesh rebuilds (on drag end) with them. The other projections keep projecting in-shader.
|
||||||
const TextureDisplacementLayer *active = active_layer();
|
const TextureDisplacementLayer *active = active_layer();
|
||||||
std::vector<Vec2f> vertex_uv = active != nullptr ? compute_layer_vertex_uvs(patch, *active) : std::vector<Vec2f>{};
|
std::vector<Vec2f> vertex_uv = active != nullptr ? compute_layer_vertex_uvs(patch, *active) : std::vector<Vec2f>{};
|
||||||
m_bump_preview_uses_vertex_uv = vertex_uv.size() == base.vertices.size();
|
m_bump_preview_uses_vertex_uv = vertex_uv.size() == patch.vertices.size();
|
||||||
if (!m_bump_preview_uses_vertex_uv)
|
if (!m_bump_preview_uses_vertex_uv)
|
||||||
vertex_uv.clear();
|
vertex_uv.clear();
|
||||||
|
|
||||||
@@ -945,6 +949,13 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
|||||||
shader->set_uniform("rotation_rad", layer->rotation_deg * float(M_PI) / 180.f);
|
shader->set_uniform("rotation_rad", layer->rotation_deg * float(M_PI) / 180.f);
|
||||||
shader->set_uniform("uv_offset", layer->offset);
|
shader->set_uniform("uv_offset", layer->offset);
|
||||||
shader->set_uniform("invert", layer->invert);
|
shader->set_uniform("invert", layer->invert);
|
||||||
|
// The parallax step in the triplanar path needs the real height, not just its gradient, so it
|
||||||
|
// needs the midlevel the bake subtracts - and the camera position in the volume's own local space,
|
||||||
|
// to build the view ray it walks along. Without the parallax the pattern is welded to the base
|
||||||
|
// surface: it does not slide as the camera orbits and does not deepen with depth_mm, which is
|
||||||
|
// exactly when the fast preview stops looking like geometry.
|
||||||
|
shader->set_uniform("midlevel", layer->midlevel);
|
||||||
|
shader->set_uniform("eye_model_pos", Vec3f((trafo_matrix.inverse() * camera.get_position()).cast<float>()));
|
||||||
// When set, the shader samples at the per-vertex uv baked into the mesh (LSCM) rather than
|
// When set, the shader samples at the per-vertex uv baked into the mesh (LSCM) rather than
|
||||||
// projecting; see rebuild_bump_preview_mesh().
|
// projecting; see rebuild_bump_preview_mesh().
|
||||||
shader->set_uniform("use_vertex_uv", m_bump_preview_uses_vertex_uv);
|
shader->set_uniform("use_vertex_uv", m_bump_preview_uses_vertex_uv);
|
||||||
@@ -970,9 +981,11 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
|||||||
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
|
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
|
||||||
if (patch.indices.empty())
|
if (patch.indices.empty())
|
||||||
return;
|
return;
|
||||||
const indexed_triangle_set &base = mv->mesh().its;
|
// Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a
|
||||||
if (base.vertices.size() != patch.vertices.size())
|
// brush stroke splits a triangle, after which the patch array is longer - and since patch triangle
|
||||||
return;
|
// indices are used to index it, reading the mesh's array instead would run off the end. An earlier
|
||||||
|
// version guarded that by bailing out, which quietly disabled the Checker and Distortion overlays
|
||||||
|
// for anything painted with the brush.
|
||||||
const TextureDisplacementLayer *layer = active_layer();
|
const TextureDisplacementLayer *layer = active_layer();
|
||||||
if (layer == nullptr)
|
if (layer == nullptr)
|
||||||
return;
|
return;
|
||||||
@@ -980,16 +993,16 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
|||||||
// The checker samples wherever the projection puts it; the projections the shader can't
|
// The checker samples wherever the projection puts it; the projections the shader can't
|
||||||
// reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader.
|
// reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader.
|
||||||
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
|
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
|
||||||
const bool have_uvs = uv.size() == base.vertices.size();
|
const bool have_uvs = uv.size() == patch.vertices.size();
|
||||||
m_uvcheck_uses_vertex_uv = have_uvs;
|
m_uvcheck_uses_vertex_uv = have_uvs;
|
||||||
|
|
||||||
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
|
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
|
||||||
std::vector<float> distortion(base.vertices.size(), 0.5f);
|
std::vector<float> distortion(patch.vertices.size(), 0.5f);
|
||||||
if (m_uv_check_mode == UVCheckMode::Distortion && have_uvs) {
|
if (m_uv_check_mode == UVCheckMode::Distortion && have_uvs) {
|
||||||
std::vector<float> tri_log(patch.indices.size(), 0.f);
|
std::vector<float> tri_log(patch.indices.size(), 0.f);
|
||||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||||
const float a3 = 0.5f * (base.vertices[t[1]] - base.vertices[t[0]]).cross(base.vertices[t[2]] - base.vertices[t[0]]).norm();
|
const float a3 = 0.5f * (patch.vertices[t[1]] - patch.vertices[t[0]]).cross(patch.vertices[t[2]] - patch.vertices[t[0]]).norm();
|
||||||
const Vec2f e0 = uv[t[1]] - uv[t[0]];
|
const Vec2f e0 = uv[t[1]] - uv[t[0]];
|
||||||
const Vec2f e1 = uv[t[2]] - uv[t[0]];
|
const Vec2f e1 = uv[t[2]] - uv[t[0]];
|
||||||
const float a2 = 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x());
|
const float a2 = 0.5f * std::abs(e0.x() * e1.y() - e0.y() * e1.x());
|
||||||
@@ -1003,8 +1016,8 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
|||||||
std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end());
|
std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end());
|
||||||
median = sorted[sorted.size() / 2];
|
median = sorted[sorted.size() / 2];
|
||||||
}
|
}
|
||||||
std::vector<float> sum(base.vertices.size(), 0.f);
|
std::vector<float> sum(patch.vertices.size(), 0.f);
|
||||||
std::vector<int> cnt(base.vertices.size(), 0);
|
std::vector<int> cnt(patch.vertices.size(), 0);
|
||||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||||
// +/- 2 stops (4x stretch either way) spans the full blue->red range.
|
// +/- 2 stops (4x stretch either way) spans the full blue->red range.
|
||||||
const float d = std::clamp(0.5f + (tri_log[f] - median) / 4.f, 0.f, 1.f);
|
const float d = std::clamp(0.5f + (tri_log[f] - median) / 4.f, 0.f, 1.f);
|
||||||
@@ -1020,10 +1033,10 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
|
|||||||
|
|
||||||
GLModel::Geometry init_data;
|
GLModel::Geometry init_data;
|
||||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 };
|
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3T2 };
|
||||||
init_data.reserve_vertices(base.vertices.size());
|
init_data.reserve_vertices(patch.vertices.size());
|
||||||
init_data.reserve_indices(patch.indices.size() * 3);
|
init_data.reserve_indices(patch.indices.size() * 3);
|
||||||
for (size_t vi = 0; vi < base.vertices.size(); ++vi)
|
for (size_t vi = 0; vi < patch.vertices.size(); ++vi)
|
||||||
init_data.add_vertex(base.vertices[vi], Vec3f(distortion[vi], 0.f, 0.f),
|
init_data.add_vertex(patch.vertices[vi], Vec3f(distortion[vi], 0.f, 0.f),
|
||||||
have_uvs ? uv[vi] : Vec2f::Zero());
|
have_uvs ? uv[vi] : Vec2f::Zero());
|
||||||
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
for (const stl_triangle_vertex_indices &tri : patch.indices)
|
||||||
init_data.add_triangle(unsigned(tri[0]), unsigned(tri[1]), unsigned(tri[2]));
|
init_data.add_triangle(unsigned(tri[0]), unsigned(tri[1]), unsigned(tri[2]));
|
||||||
@@ -2753,51 +2766,82 @@ bool GLGizmoTextureDisplacement::collect_paint_region(
|
|||||||
return any_paint;
|
return any_paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
bool GLGizmoTextureDisplacement::plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const
|
||||||
{
|
{
|
||||||
ModelVolume *mv = texture_volume();
|
if (m_subdivide_target_mm <= 0.f)
|
||||||
ModelObject *mo = m_c->selection_info()->model_object();
|
return false;
|
||||||
if (mv == nullptr || mo == nullptr || m_subdivide_target_mm <= 0.f)
|
|
||||||
return;
|
|
||||||
|
|
||||||
update_model_object(); // flush any in-progress stroke into the committed masks first
|
std::vector<uint8_t> region;
|
||||||
|
if (!collect_paint_region(region, &out.painted_tri))
|
||||||
std::vector<uint8_t> region;
|
return false;
|
||||||
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> painted_tri;
|
|
||||||
if (!collect_paint_region(region, &painted_tri)) {
|
|
||||||
show_error(nullptr, _u8L("Paint the area you want to subdivide first - adaptive subdivision only "
|
|
||||||
"refines where you have painted."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feature-adaptive: sample the combined displacement so refinement follows texture curvature. A
|
// Feature-adaptive: sample the combined displacement so refinement follows texture curvature. A
|
||||||
// null sampler (only LSCM layers, or nothing decodable) falls back to the uniform target below.
|
// null sampler (only LSCM layers, or nothing decodable) falls back to the length baseline alone.
|
||||||
HeightFieldSampler sampler;
|
HeightFieldSampler sampler;
|
||||||
if (m_subdivide_feature) {
|
if (m_subdivide_feature) {
|
||||||
TextureDisplacementFacetsData facets{};
|
TextureDisplacementFacetsData facets{};
|
||||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
facets[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
facets[size_t(i)] = mv.texture_displacement_facet(i).get_data();
|
||||||
sampler = make_combined_displacement_sampler(mv->mesh().its, mv->texture_displacement_layers, facets);
|
sampler = make_combined_displacement_sampler(mv.mesh().its, mv.texture_displacement_layers, facets);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the (potentially slow) refinement before taking the snapshot, so a no-op leaves no empty
|
|
||||||
// undo step - mirrors remesh_model().
|
|
||||||
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down to); in
|
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down to); in
|
||||||
// plain adaptive mode the target edge length is the only criterion, so the floor must not be
|
// plain adaptive mode the target edge length is the only criterion, so the floor must not be
|
||||||
// allowed to silently override a target the user set below it.
|
// allowed to silently override a target the user set below it.
|
||||||
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
|
const float tol = m_subdivide_feature ? m_subdivide_detail_mm : 0.f;
|
||||||
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
|
const float floor = m_subdivide_feature ? m_subdivide_min_edge_mm : 0.f;
|
||||||
std::vector<int> source;
|
|
||||||
indexed_triangle_set refined;
|
|
||||||
{
|
{
|
||||||
wxBusyCursor wait;
|
wxBusyCursor wait;
|
||||||
// The budget slider is "triangles the refinement may *add*", so the model's own count is the
|
// The budget slider is "triangles the refinement may *add*", so the model's own count is the
|
||||||
// baseline - otherwise the control would be meaningless (or a dead end) on a dense model.
|
// baseline - otherwise the control would be meaningless (or a dead end) on a dense model.
|
||||||
refined = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
|
out.refined = subdivide_mesh_adaptive(mv.mesh().its, region, m_subdivide_target_mm,
|
||||||
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
int(mv.mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
|
||||||
&source, sampler, tol, floor);
|
&out.source, sampler, tol, floor);
|
||||||
}
|
}
|
||||||
if (refined.indices.size() == mv->mesh().its.indices.size()) {
|
return out.refined.indices.size() != mv.mesh().its.indices.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLGizmoTextureDisplacement::apply_adaptive_subdivision(ModelVolume &mv, SubdivisionPlan &&plan)
|
||||||
|
{
|
||||||
|
// Other paint channels ride across via the standard remap; texture-displacement paint is rebuilt
|
||||||
|
// below from the plan's source map, which is the whole point of driving this by the paint.
|
||||||
|
std::optional<TriangleSelector::SavedPainting> saved_painting = mv.save_painting();
|
||||||
|
mv.set_mesh(TriangleMesh(std::move(plan.refined)));
|
||||||
|
mv.set_new_unique_id();
|
||||||
|
mv.calculate_convex_hull();
|
||||||
|
mv.restore_painting(saved_painting); // resets extra facets (incl. texture-displacement) + remaps the rest
|
||||||
|
|
||||||
|
// Carry each layer's paint onto the new mesh: a new triangle is painted iff its source triangle
|
||||||
|
// was fully painted in that layer. Children inherit their parent's source, so this is exact.
|
||||||
|
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||||
|
if (plan.painted_tri[size_t(slot)].empty())
|
||||||
|
continue;
|
||||||
|
TriangleSelector sel(mv.mesh());
|
||||||
|
for (size_t i = 0; i < plan.source.size(); ++i)
|
||||||
|
if (plan.painted_tri[size_t(slot)][size_t(plan.source[i])])
|
||||||
|
sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||||
|
mv.texture_displacement_facet(slot).set(sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
||||||
|
{
|
||||||
|
ModelVolume *mv = texture_volume();
|
||||||
|
ModelObject *mo = m_c->selection_info()->model_object();
|
||||||
|
if (mv == nullptr || mo == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
update_model_object(); // flush any in-progress stroke into the committed masks first
|
||||||
|
|
||||||
|
if (!mv->is_texture_displacement_painted()) {
|
||||||
|
show_error(nullptr, _u8L("Paint the area you want to subdivide first - adaptive subdivision only "
|
||||||
|
"refines where you have painted."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan before the snapshot, so a no-op leaves no empty undo step - mirrors remesh_model().
|
||||||
|
SubdivisionPlan plan;
|
||||||
|
if (!plan_adaptive_subdivision(*mv, plan)) {
|
||||||
show_error(nullptr, _u8L("Nothing to subdivide - the painted area already meets the target edge "
|
show_error(nullptr, _u8L("Nothing to subdivide - the painted area already meets the target edge "
|
||||||
"length and detail tolerance, or the triangle budget is already used up."));
|
"length and detail tolerance, or the triangle budget is already used up."));
|
||||||
return;
|
return;
|
||||||
@@ -2805,26 +2849,7 @@ void GLGizmoTextureDisplacement::subdivide_model_adaptive()
|
|||||||
|
|
||||||
Plater *plater = wxGetApp().plater();
|
Plater *plater = wxGetApp().plater();
|
||||||
Plater::TakeSnapshot snapshot(plater, _u8L("Adaptive subdivide for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
Plater::TakeSnapshot snapshot(plater, _u8L("Adaptive subdivide for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||||
|
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||||
// Other paint channels ride across via the standard remap; texture-displacement paint is rebuilt
|
|
||||||
// by hand below from the source map, which is the whole point of driving this by the paint.
|
|
||||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
|
||||||
mv->set_mesh(TriangleMesh(std::move(refined))); // refined is not needed past here; source carries the paint map
|
|
||||||
mv->set_new_unique_id();
|
|
||||||
mv->calculate_convex_hull();
|
|
||||||
mv->restore_painting(saved_painting); // resets extra facets (incl. texture-displacement) + remaps the rest
|
|
||||||
|
|
||||||
// Carry each layer's paint onto the new mesh: a new triangle is painted iff its source triangle
|
|
||||||
// was fully painted in that layer. Children inherit their parent's source, so this is exact.
|
|
||||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
|
||||||
if (painted_tri[slot].empty())
|
|
||||||
continue;
|
|
||||||
TriangleSelector sel(mv->mesh());
|
|
||||||
for (size_t i = 0; i < source.size(); ++i)
|
|
||||||
if (painted_tri[slot][source[i]])
|
|
||||||
sel.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
|
||||||
mv->texture_displacement_facet(slot).set(sel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||||
const ModelObjectPtrs &objs = plater->model().objects;
|
const ModelObjectPtrs &objs = plater->model().objects;
|
||||||
@@ -2921,6 +2946,62 @@ void GLGizmoTextureDisplacement::smooth_model()
|
|||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GLGizmoTextureDisplacement::replace_mesh_keep_all_paint(ModelVolume &mv, TriangleMesh &&new_mesh)
|
||||||
|
{
|
||||||
|
const indexed_triangle_set old_its = mv.mesh().its;
|
||||||
|
std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS> saved_texture;
|
||||||
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||||
|
saved_texture[size_t(i)] = mv.texture_displacement_facet(i).get_data();
|
||||||
|
std::optional<TriangleSelector::SavedPainting> saved_painting = mv.save_painting();
|
||||||
|
|
||||||
|
mv.set_mesh(std::move(new_mesh));
|
||||||
|
mv.set_new_unique_id();
|
||||||
|
mv.calculate_convex_hull();
|
||||||
|
mv.restore_painting(saved_painting); // clears the extra facets, then remaps the four standard channels
|
||||||
|
|
||||||
|
// ... and the same spatial remap for the texture-displacement masks, which restore_painting() knows
|
||||||
|
// nothing about. Without this a remesh would wipe the texture paint, which is fine for a standalone
|
||||||
|
// "Remesh" click but fatal for Standard mode's pipeline: it remeshes *after* the user has painted,
|
||||||
|
// and the subdivision and displacement that follow are both driven by that paint.
|
||||||
|
const Transform3d to_source = Slic3r::Geometry::translation_transform(mv.mesh().get_init_shift());
|
||||||
|
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
|
||||||
|
if (saved_texture[size_t(i)].bitstream.empty())
|
||||||
|
continue;
|
||||||
|
TriangleSelector::TriangleSplittingData remapped =
|
||||||
|
TriangleSelector::remap_painting(old_its, saved_texture[size_t(i)], mv.mesh().its, to_source,
|
||||||
|
{}); // no existing paint to merge with: set_mesh() just cleared it
|
||||||
|
if (!remapped.bitstream.empty())
|
||||||
|
mv.texture_displacement_facet(i).set_data(std::move(remapped));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GLGizmoTextureDisplacement::plan_remesh(const ModelVolume &mv, float target_edge_mm, float sharp_angle_deg,
|
||||||
|
TriangleMesh &out)
|
||||||
|
{
|
||||||
|
if (target_edge_mm <= 0.f)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// CGAL isotropic remeshing can be slow on a big mesh, which is why this is separated from applying
|
||||||
|
// it: the caller runs it before taking the snapshot so a failure leaves no empty undo step.
|
||||||
|
const indexed_triangle_set &src = mv.mesh().its;
|
||||||
|
indexed_triangle_set remeshed;
|
||||||
|
{
|
||||||
|
wxBusyCursor wait;
|
||||||
|
remeshed = MeshBoolean::cgal::remesh_isotropic(src, double(target_edge_mm), 3, double(sharp_angle_deg));
|
||||||
|
}
|
||||||
|
// remesh_isotropic() signals failure by handing the input straight back, so compare against it
|
||||||
|
// structurally. Vertex count alone is not enough: a remesh that only redistributes triangles at
|
||||||
|
// roughly the current density legitimately lands on the same count, and treating that as failure
|
||||||
|
// meant a perfectly good result got thrown away with an error message.
|
||||||
|
if (remeshed.indices.empty() ||
|
||||||
|
(remeshed.vertices.size() == src.vertices.size() && remeshed.indices.size() == src.indices.size() &&
|
||||||
|
remeshed.indices == src.indices))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out = TriangleMesh(std::move(remeshed));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::remesh_model()
|
void GLGizmoTextureDisplacement::remesh_model()
|
||||||
{
|
{
|
||||||
ModelVolume *mv = texture_volume();
|
ModelVolume *mv = texture_volume();
|
||||||
@@ -2928,39 +3009,18 @@ void GLGizmoTextureDisplacement::remesh_model()
|
|||||||
if (mv == nullptr || mo == nullptr || m_remesh_target_edge_mm <= 0.f)
|
if (mv == nullptr || mo == nullptr || m_remesh_target_edge_mm <= 0.f)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Plater *plater = wxGetApp().plater();
|
TriangleMesh remeshed;
|
||||||
|
if (!plan_remesh(*mv, m_remesh_target_edge_mm, m_remesh_keep_sharp_edges ? m_remesh_sharp_angle_deg : 0.f,
|
||||||
// CGAL isotropic remeshing can be slow on a big mesh; do it before taking the snapshot so a failure
|
remeshed)) {
|
||||||
// (it returns the input unchanged) doesn't leave an empty undo step.
|
|
||||||
const indexed_triangle_set &src = mv->mesh().its;
|
|
||||||
indexed_triangle_set remeshed;
|
|
||||||
{
|
|
||||||
wxBusyCursor wait;
|
|
||||||
remeshed = MeshBoolean::cgal::remesh_isotropic(mv->mesh().its, double(m_remesh_target_edge_mm), 3,
|
|
||||||
m_remesh_keep_sharp_edges ? double(m_remesh_sharp_angle_deg) : 0.0);
|
|
||||||
}
|
|
||||||
// remesh_isotropic() signals failure by handing the input straight back, so compare against it
|
|
||||||
// structurally. Vertex count alone is not enough: a remesh that only redistributes triangles at
|
|
||||||
// roughly the current density legitimately lands on the same count, and treating that as failure
|
|
||||||
// meant a perfectly good result got thrown away with an error message.
|
|
||||||
const bool unchanged = remeshed.indices.empty() ||
|
|
||||||
(remeshed.vertices.size() == src.vertices.size() && remeshed.indices.size() == src.indices.size() &&
|
|
||||||
remeshed.indices == src.indices);
|
|
||||||
if (unchanged) {
|
|
||||||
show_error(nullptr, _u8L("Remeshing did not change the model. It may be non-manifold (open edges or "
|
show_error(nullptr, _u8L("Remeshing did not change the model. It may be non-manifold (open edges or "
|
||||||
"edges shared by more than two triangles), or the target edge length may "
|
"edges shared by more than two triangles), or the target edge length may "
|
||||||
"already be met."));
|
"already be met."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Plater *plater = wxGetApp().plater();
|
||||||
Plater::TakeSnapshot snapshot(plater, _u8L("Remesh model for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
Plater::TakeSnapshot snapshot(plater, _u8L("Remesh model for texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||||
// Same save/replace/restore-painting dance as subdivide: texture-displacement paint has no remap
|
replace_mesh_keep_all_paint(*mv, std::move(remeshed));
|
||||||
// across a topology change, so it is dropped rather than left pointing at triangles that moved.
|
|
||||||
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
|
|
||||||
mv->set_mesh(TriangleMesh(std::move(remeshed)));
|
|
||||||
mv->set_new_unique_id();
|
|
||||||
mv->calculate_convex_hull();
|
|
||||||
mv->restore_painting(saved_painting);
|
|
||||||
|
|
||||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||||
const ModelObjectPtrs &objs = plater->model().objects;
|
const ModelObjectPtrs &objs = plater->model().objects;
|
||||||
@@ -3075,7 +3135,7 @@ GLTexture *GLGizmoTextureDisplacement::get_layer_thumbnail(const TextureDisplace
|
|||||||
return m_thumbnails[slot].get();
|
return m_thumbnails[slot].get();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::bake()
|
void GLGizmoTextureDisplacement::bake(bool own_snapshot)
|
||||||
{
|
{
|
||||||
ModelVolume *mv = texture_volume();
|
ModelVolume *mv = texture_volume();
|
||||||
if (!mv || m_bake_in_progress)
|
if (!mv || m_bake_in_progress)
|
||||||
@@ -3101,7 +3161,112 @@ void GLGizmoTextureDisplacement::bake()
|
|||||||
if (m_state == On && m_c->selection_info() && m_c->selection_info()->model_object())
|
if (m_state == On && m_c->selection_info() && m_c->selection_info()->model_object())
|
||||||
update_from_model_object(false);
|
update_from_model_object(false);
|
||||||
m_parent.set_as_dirty();
|
m_parent.set_as_dirty();
|
||||||
});
|
}, own_snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard mode's fixed recipe. These are both the values the hidden controls are pinned to and what
|
||||||
|
// bake_standard() drives its remesh and subdivision with - one set of numbers, so the preview cannot
|
||||||
|
// disagree with the bake. Chosen to be safe on an arbitrary imported part rather than optimal on any
|
||||||
|
// particular one: a 1 mm isotropic remesh gives the subdivider an even starting density whatever the
|
||||||
|
// input looked like, and the subdivision then spends up to 1.5 M triangles chasing texture curvature
|
||||||
|
// down to a 0.02 mm floor, which is finer than any FDM nozzle will resolve. The triangle budget is not
|
||||||
|
// here: it is the one control Standard mode still shows, so it belongs to the user (its default is
|
||||||
|
// m_subdivide_budget_k's initialiser).
|
||||||
|
static constexpr float STD_REMESH_EDGE_MM = 1.0f;
|
||||||
|
static constexpr float STD_REMESH_SHARP_DEG = 40.f;
|
||||||
|
static constexpr float STD_SUBDIV_MAX_EDGE_MM = 20.f;
|
||||||
|
static constexpr float STD_SUBDIV_DETAIL_MM = 0.02f;
|
||||||
|
static constexpr float STD_SUBDIV_MIN_EDGE_MM = 0.02f;
|
||||||
|
|
||||||
|
bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
const auto pin = [&changed](auto &field, auto value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mv != nullptr) {
|
||||||
|
pin(mv->texture_displacement_options.displace_border, true);
|
||||||
|
pin(mv->texture_displacement_options.smooth_enabled, false);
|
||||||
|
}
|
||||||
|
pin(m_subdivide_adaptive, true);
|
||||||
|
pin(m_subdivide_feature, true);
|
||||||
|
pin(m_subdivide_target_mm, STD_SUBDIV_MAX_EDGE_MM);
|
||||||
|
pin(m_subdivide_detail_mm, STD_SUBDIV_DETAIL_MM);
|
||||||
|
pin(m_subdivide_min_edge_mm, STD_SUBDIV_MIN_EDGE_MM);
|
||||||
|
// Deliberately *not* pinned: the triangle budget stays visible and editable in Standard mode, so
|
||||||
|
// pinning it would fight the user's own slider every frame.
|
||||||
|
pin(m_remesh_target_edge_mm, STD_REMESH_EDGE_MM);
|
||||||
|
pin(m_remesh_keep_sharp_edges, true);
|
||||||
|
pin(m_remesh_sharp_angle_deg, STD_REMESH_SHARP_DEG);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLGizmoTextureDisplacement::bake_standard()
|
||||||
|
{
|
||||||
|
ModelVolume *mv = texture_volume();
|
||||||
|
ModelObject *mo = m_c->selection_info()->model_object();
|
||||||
|
if (mv == nullptr || mo == nullptr || m_bake_in_progress)
|
||||||
|
return;
|
||||||
|
|
||||||
|
update_model_object(); // flush the active layer's in-progress strokes before anything reads them
|
||||||
|
if (!mv->is_texture_displacement_painted()) {
|
||||||
|
show_error(nullptr, _u8L("Nothing is painted, there is nothing to bake."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apply_standard_mode_presets(mv); // belt and braces: never bake with values the panel is not showing
|
||||||
|
|
||||||
|
// Both stages are planned before the snapshot so a stage that has nothing to do is simply skipped.
|
||||||
|
// Skipping is normal, not a failure: a mesh that is already even needs no remesh, and one that is
|
||||||
|
// already fine enough for the texture needs no subdivision.
|
||||||
|
TriangleMesh remeshed;
|
||||||
|
const bool do_remesh = plan_remesh(*mv, STD_REMESH_EDGE_MM, STD_REMESH_SHARP_DEG, remeshed);
|
||||||
|
|
||||||
|
Plater *plater = wxGetApp().plater();
|
||||||
|
{
|
||||||
|
// ONE undo step for the whole pipeline. take_snapshot() records the state *before* the change,
|
||||||
|
// so a single Undo goes all the way back to the untouched mesh - which is the only thing "undo
|
||||||
|
// the bake" can sensibly mean when the bake is also what prepared the geometry. The background
|
||||||
|
// displacement job is told not to add its own (see queue_texture_displacement_bake's
|
||||||
|
// take_snapshot), because an undo step landing between the subdivision and the displacement
|
||||||
|
// leaves a mesh with 1.5 M extra triangles and no relief on it - and baking again from there
|
||||||
|
// subdivides that mesh a second time.
|
||||||
|
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"),
|
||||||
|
UndoRedo::SnapshotType::GizmoAction);
|
||||||
|
if (do_remesh)
|
||||||
|
replace_mesh_keep_all_paint(*mv, std::move(remeshed));
|
||||||
|
|
||||||
|
// The remesh carries the paint across spatially, but if that remap came back empty the rest of
|
||||||
|
// the pipeline has nothing to work from - stop here rather than silently baking a flat mesh.
|
||||||
|
if (!mv->is_texture_displacement_painted()) {
|
||||||
|
show_error(nullptr, _u8L("The painted area could not be transferred onto the remeshed model. Undo, "
|
||||||
|
"then switch to Pro mode to prepare the mesh before painting."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planning the subdivision has to happen inside the snapshot because it reads the mesh the
|
||||||
|
// remesh just produced. It is the expensive step, but by here we are committed anyway.
|
||||||
|
SubdivisionPlan plan;
|
||||||
|
if (plan_adaptive_subdivision(*mv, plan))
|
||||||
|
apply_adaptive_subdivision(*mv, std::move(plan));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||||
|
const ModelObjectPtrs &objs = plater->model().objects;
|
||||||
|
auto it = std::find(objs.begin(), objs.end(), mo);
|
||||||
|
if (it != objs.end())
|
||||||
|
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||||
|
}
|
||||||
|
plater->changed_object(*mo);
|
||||||
|
update_from_model_object(false); // reload selectors against the prepared mesh + carried paint
|
||||||
|
m_parent.set_as_dirty();
|
||||||
|
|
||||||
|
// ... and finally the displacement itself, in the background exactly as the Pro-mode button does -
|
||||||
|
// except that it commits into the snapshot taken above instead of pushing another one.
|
||||||
|
bake(/* own_snapshot */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GLGizmoTextureDisplacement::render_paint_cursor_hint()
|
void GLGizmoTextureDisplacement::render_paint_cursor_hint()
|
||||||
@@ -3237,6 +3402,42 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
m_imgui->tooltip(_u8L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it "
|
m_imgui->tooltip(_u8L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it "
|
||||||
"back beside the toolbar."),
|
"back beside the toolbar."),
|
||||||
m_imgui->scaled(20.f));
|
m_imgui->scaled(20.f));
|
||||||
|
|
||||||
|
// Standard / Pro, right-aligned on the header row. A two-position slider rather than a checkbox
|
||||||
|
// because it is a mode, not an option: Standard hides every mesh-preparation control and folds the
|
||||||
|
// whole recipe into Bake, Pro shows all of it and hands the ordering to the user.
|
||||||
|
{
|
||||||
|
const float mode_w = m_imgui->scaled(6.2f);
|
||||||
|
ImGui::SameLine(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - mode_w));
|
||||||
|
ImGui::PushItemWidth(mode_w);
|
||||||
|
// The format string carries no conversion, so ImGui prints it verbatim as the slider's label -
|
||||||
|
// which is how a two-position slider gets word labels instead of "0" and "1".
|
||||||
|
const std::string mode_label = pro_mode() ? _u8L("Pro") : _u8L("Standard");
|
||||||
|
if (ImGui::SliderInt("##panel_mode", &m_panel_mode, 0, 1, mode_label.c_str())) {
|
||||||
|
m_panel_mode = std::clamp(m_panel_mode, 0, 1);
|
||||||
|
if (!pro_mode()) {
|
||||||
|
// Leaving the subdivision preview open would strand a wireframe whose controls just
|
||||||
|
// disappeared, so close it as part of the switch.
|
||||||
|
m_subdivide_editing = false;
|
||||||
|
m_subdivide_preview_tris = -1;
|
||||||
|
m_subdivide_preview_glmodel.reset();
|
||||||
|
if (apply_standard_mode_presets(mv))
|
||||||
|
m_preview_params_dirty = true;
|
||||||
|
}
|
||||||
|
m_parent.set_as_dirty();
|
||||||
|
}
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("Standard - paint, then press Bake: the mesh is remeshed, refined where the "
|
||||||
|
"texture bends, and displaced in one step.\n\nPro - every mesh-preparation "
|
||||||
|
"control is shown and you run Remesh, Subdivide and Bake yourself, in the "
|
||||||
|
"order you want."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
}
|
||||||
|
// Pinned every frame while Standard is active, so what Preview shows is always what Bake will do.
|
||||||
|
if (!pro_mode() && apply_standard_mode_presets(mv))
|
||||||
|
m_preview_params_dirty = true;
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// Selection mode: which of TriangleSelector's existing click/brush mechanisms drives painting.
|
// Selection mode: which of TriangleSelector's existing click/brush mechanisms drives painting.
|
||||||
@@ -3854,8 +4055,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
}
|
}
|
||||||
// (The "Add layer" button now lives next to the "Texture layers" heading, as an icon.)
|
// (The "Add layer" button now lives next to the "Texture layers" heading, as an icon.)
|
||||||
|
|
||||||
// Settings for the whole stack rather than one layer, so they sit outside the layer list.
|
// Settings for the whole stack rather than one layer, so they sit outside the layer list. Standard
|
||||||
if (mv != nullptr) {
|
// mode pins them (see apply_standard_mode_presets()) instead of showing them.
|
||||||
|
if (pro_mode() && mv != nullptr) {
|
||||||
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
TextureDisplacementOptions &opts = mv->texture_displacement_options;
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
@@ -3912,7 +4114,34 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
m_imgui->text(_u8L("Not enough vertices for fine detail?"));
|
m_imgui->text(_u8L("Subdivision"));
|
||||||
|
|
||||||
|
// The triangle budget is the one subdivision control Standard mode keeps: its right value depends
|
||||||
|
// on the part rather than on the recipe (a big model, or a fine texture, simply needs more of them),
|
||||||
|
// and raising or lowering it is safe without understanding anything else in this section. Shared
|
||||||
|
// with the Pro layout below so there is one definition of the widget.
|
||||||
|
const auto budget_slider = [this]() {
|
||||||
|
ImGui::PushItemWidth(m_imgui->scaled(8.4f));
|
||||||
|
if (ImGui::SliderInt((_u8L("Added triangles (k)") + "##subdivbudget").c_str(), &m_subdivide_budget_k, 10, 2000)) {
|
||||||
|
m_subdivide_budget_k = std::clamp(m_subdivide_budget_k, 10, 2000);
|
||||||
|
if (m_subdivide_editing)
|
||||||
|
rebuild_subdivide_preview();
|
||||||
|
m_parent.set_as_dirty();
|
||||||
|
}
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(_u8L("How many thousand triangles the refinement may add. It always splits the "
|
||||||
|
"worst-fitting triangle first, so a run that uses the whole budget has still spent "
|
||||||
|
"it where it shows most - raise this if the result still looks too coarse."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything else from here to the Bake row is mesh preparation, which is exactly what Standard
|
||||||
|
// mode takes over: it pins those to one recipe and runs them from the Bake button (see
|
||||||
|
// bake_standard()), so showing them would only invite changing numbers that get overwritten.
|
||||||
|
if (!pro_mode())
|
||||||
|
budget_slider();
|
||||||
|
if (pro_mode()) {
|
||||||
|
|
||||||
if (ImGui::Checkbox(_u8L("Only painted area (adaptive)").c_str(), &m_subdivide_adaptive)) {
|
if (ImGui::Checkbox(_u8L("Only painted area (adaptive)").c_str(), &m_subdivide_adaptive)) {
|
||||||
if (m_subdivide_editing)
|
if (m_subdivide_editing)
|
||||||
@@ -3996,18 +4225,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
m_imgui->scaled(20.f));
|
m_imgui->scaled(20.f));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The budget. Refinement is worst-error-first, so a run that hits it has still spent its
|
|
||||||
// triangles on the biggest deviations - raising it buys detail, it does not redistribute it.
|
|
||||||
if (ImGui::SliderInt((_u8L("Added triangles (k)") + "##subdivbudget").c_str(), &m_subdivide_budget_k, 10, 2000)) {
|
|
||||||
m_subdivide_budget_k = std::clamp(m_subdivide_budget_k, 10, 2000);
|
|
||||||
preview_live();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
m_imgui->tooltip(_u8L("How many thousand triangles the refinement may add. It always splits the "
|
|
||||||
"worst-fitting triangle first, so a run that uses the whole budget has still spent "
|
|
||||||
"it where it shows most - raise this if the preview still looks too coarse."),
|
|
||||||
m_imgui->scaled(20.f));
|
|
||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
budget_slider();
|
||||||
|
|
||||||
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
|
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
|
||||||
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
|
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
|
||||||
@@ -4074,7 +4293,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
|
|
||||||
// Remesh: even out uneven triangle sizes (CGAL isotropic remeshing). GPU Delaunay isn't practical
|
// Remesh: even out uneven triangle sizes (CGAL isotropic remeshing). GPU Delaunay isn't practical
|
||||||
// here, but this delivers the same goal - a consistent triangle size across the whole model.
|
// here, but this delivers the same goal - a consistent triangle size across the whole model.
|
||||||
m_imgui->text(_u8L("Uneven triangle sizes?"));
|
m_imgui->text(_u8L("Remeshing"));
|
||||||
if (m_remesh_target_edge_mm <= 0.f && mv != nullptr) {
|
if (m_remesh_target_edge_mm <= 0.f && mv != nullptr) {
|
||||||
// Seed the target with the model's current mean edge length, so the default is a sensible
|
// Seed the target with the model's current mean edge length, so the default is a sensible
|
||||||
// "make everything about the size it already averages".
|
// "make everything about the size it already averages".
|
||||||
@@ -4114,9 +4333,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
m_imgui->tooltip(_u8L("Rebuilds the whole model with triangles close to this edge length - splitting the big "
|
m_imgui->tooltip(_u8L("Rebuilds the whole model with triangles close to this edge length - splitting the big "
|
||||||
"ones and merging the small ones - so displacement has an even density to work with. "
|
"ones and merging the small ones - so displacement has an even density to work with. "
|
||||||
"Replaces the geometry and clears any not-yet-baked paint (already-baked bumps are kept)."),
|
"Replaces the geometry; your paint is carried onto the new triangles spatially, so it "
|
||||||
|
"survives (already-baked bumps are kept too)."),
|
||||||
m_imgui->scaled(20.f));
|
m_imgui->scaled(20.f));
|
||||||
|
|
||||||
|
} // pro_mode()
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
m_imgui->disabled_begin(mv == nullptr || !mv->is_texture_displacement_painted());
|
m_imgui->disabled_begin(mv == nullptr || !mv->is_texture_displacement_painted());
|
||||||
@@ -4136,9 +4358,23 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
m_imgui->disabled_begin(m_bake_in_progress || mv == nullptr || !mv->is_texture_displacement_painted());
|
m_imgui->disabled_begin(m_bake_in_progress || mv == nullptr || !mv->is_texture_displacement_painted());
|
||||||
if (m_imgui->button(m_bake_in_progress ? _L("Baking...") : m_desc.at("bake")))
|
if (m_imgui->button(m_bake_in_progress ? _L("Baking...") : m_desc.at("bake"))) {
|
||||||
bake();
|
// Standard mode's Bake is the whole pipeline (remesh -> refine -> displace); Pro's is only the
|
||||||
|
// displacement, because there the user has already prepared the mesh with the controls above.
|
||||||
|
if (pro_mode())
|
||||||
|
bake();
|
||||||
|
else
|
||||||
|
bake_standard();
|
||||||
|
}
|
||||||
m_imgui->disabled_end();
|
m_imgui->disabled_end();
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
m_imgui->tooltip(pro_mode() ?
|
||||||
|
_u8L("Turn the painted height maps into real geometry, by moving the vertices that are "
|
||||||
|
"already there. Use Subdivide first if the mesh is too coarse to show the detail.") :
|
||||||
|
_u8L("Turn the painted height maps into real geometry. The mesh is remeshed to an even "
|
||||||
|
"density and refined where the texture bends first, so the detail has vertices to "
|
||||||
|
"land on - all in one step."),
|
||||||
|
m_imgui->scaled(20.f));
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (m_imgui->button(_L("Close")))
|
if (m_imgui->button(_L("Close")))
|
||||||
|
|||||||
@@ -67,7 +67,51 @@ private:
|
|||||||
void add_texture_layer();
|
void add_texture_layer();
|
||||||
void remove_texture_layer(int slot);
|
void remove_texture_layer(int slot);
|
||||||
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
|
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
|
||||||
void bake();
|
// `own_snapshot` false when the caller has already taken an undo step that is meant to cover the
|
||||||
|
// displacement too - see bake_standard().
|
||||||
|
void bake(bool own_snapshot = true);
|
||||||
|
|
||||||
|
// Standard (0) vs Pro (1), driven by the two-position slider in the panel header.
|
||||||
|
//
|
||||||
|
// Pro is the panel as it has always been: every geometry-preparation control is visible and the
|
||||||
|
// user drives Remesh, Subdivide and Bake themselves, in whatever order they like. Standard hides
|
||||||
|
// all of that, pins it to one fixed recipe, and folds it into the Bake button - paint, press Bake,
|
||||||
|
// done - so the common case does not require knowing that a height map can only move vertices that
|
||||||
|
// already exist. Nothing about Pro changed when Standard was added.
|
||||||
|
int m_panel_mode = 0;
|
||||||
|
bool pro_mode() const { return m_panel_mode != 0; }
|
||||||
|
// Pins every control Standard mode hides to its preset value. Idempotent, called each frame while
|
||||||
|
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
|
||||||
|
// changed something, so the caller can invalidate the preview.
|
||||||
|
bool apply_standard_mode_presets(ModelVolume *mv);
|
||||||
|
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
||||||
|
// The order matters and is the whole reason this is one button - a height map can only move
|
||||||
|
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
||||||
|
// the paint if it were not remapped across (see replace_mesh_keep_all_paint()).
|
||||||
|
void bake_standard();
|
||||||
|
|
||||||
|
// Replaces `mv`'s mesh and carries *every* paint channel onto it, texture displacement included -
|
||||||
|
// the four standard channels via ModelVolume::restore_painting(), the eight texture-displacement
|
||||||
|
// masks via the same TriangleSelector::remap_painting() spatial remap, which restore_painting()
|
||||||
|
// does not cover. Shared by Remesh and by Standard mode's pipeline.
|
||||||
|
static void replace_mesh_keep_all_paint(ModelVolume &mv, TriangleMesh &&new_mesh);
|
||||||
|
// Remesh and adaptive Subdivide are each split into a *plan* (the heavy geometry work, touching
|
||||||
|
// nothing) and an *apply* (pure mutation). That split is what lets both the standalone buttons and
|
||||||
|
// Standard mode's one-button pipeline run the expensive part **before** taking the undo snapshot,
|
||||||
|
// so a run that turns out to be a no-op does not leave an empty undo step behind - and it keeps
|
||||||
|
// snapshot ownership with the caller, which matters because the buttons want one snapshot per click
|
||||||
|
// while the pipeline wants a single one around remesh + subdivide together.
|
||||||
|
struct SubdivisionPlan
|
||||||
|
{
|
||||||
|
indexed_triangle_set refined;
|
||||||
|
std::vector<int> source; // new tri -> input tri
|
||||||
|
std::array<std::vector<uint8_t>, TEXTURE_DISPLACEMENT_MAX_LAYERS> painted_tri; // per layer, per input tri
|
||||||
|
};
|
||||||
|
// False means "nothing to refine" and `out` must not be used.
|
||||||
|
bool plan_adaptive_subdivision(const ModelVolume &mv, SubdivisionPlan &out) const;
|
||||||
|
static void apply_adaptive_subdivision(ModelVolume &mv, SubdivisionPlan &&plan);
|
||||||
|
// False means the remesh failed or changed nothing (CGAL signals failure by handing the input back).
|
||||||
|
static bool plan_remesh(const ModelVolume &mv, float target_edge_mm, float sharp_angle_deg, TriangleMesh &out);
|
||||||
|
|
||||||
// Marks every facet of every model-part volume as painted for the currently active layer -
|
// Marks every facet of every model-part volume as painted for the currently active layer -
|
||||||
// "whole model" as an alternative to brushing/clicking every triangle by hand.
|
// "whole model" as an alternative to brushing/clicking every triangle by hand.
|
||||||
@@ -327,7 +371,7 @@ private:
|
|||||||
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
|
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
|
||||||
// that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from
|
// that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from
|
||||||
// turning into an out-of-memory, or an unrenderable preview wireframe.
|
// turning into an out-of-memory, or an unrenderable preview wireframe.
|
||||||
int m_subdivide_budget_k = 200;
|
int m_subdivide_budget_k = 1500;
|
||||||
void subdivide_model_adaptive();
|
void subdivide_model_adaptive();
|
||||||
// Fills `region` (per current-mesh triangle, 1 = refine) from the union of every layer's painted
|
// Fills `region` (per current-mesh triangle, 1 = refine) from the union of every layer's painted
|
||||||
// area. If `painted_tri` is non-null, also fills, per layer, the fully-painted triangles to carry
|
// area. If `painted_tri` is non-null, also fills, per layer, the fully-painted triangles to carry
|
||||||
@@ -345,7 +389,9 @@ private:
|
|||||||
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
|
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
|
||||||
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
|
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
|
||||||
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
|
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
|
||||||
// the geometry and drops not-yet-baked paint (no remap across a topology change).
|
// the geometry, but unlike subdivide it keeps every paint channel: replace_mesh_keep_all_paint()
|
||||||
|
// remaps the texture-displacement masks spatially, which is also what lets Standard mode remesh
|
||||||
|
// *after* the user has painted.
|
||||||
float m_remesh_target_edge_mm = 0.f;
|
float m_remesh_target_edge_mm = 0.f;
|
||||||
// Dihedral angle above which an edge counts as a hard feature and is held fixed by the remesher.
|
// Dihedral angle above which an edge counts as a hard feature and is held fixed by the remesher.
|
||||||
// Off by default would round every sharp edge off, so this is on; 0 disables the protection.
|
// Off by default would round every sharp edge off, so this is on; 0 disables the protection.
|
||||||
|
|||||||
@@ -41,41 +41,55 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
|||||||
|
|
||||||
Plater *plater = wxGetApp().plater();
|
Plater *plater = wxGetApp().plater();
|
||||||
|
|
||||||
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
const auto commit = [this, plater]() {
|
||||||
|
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
|
||||||
|
if (volume == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
|
volume->set_mesh(std::move(m_result));
|
||||||
if (volume == nullptr)
|
volume->set_new_unique_id();
|
||||||
return;
|
volume->calculate_convex_hull();
|
||||||
|
|
||||||
volume->set_mesh(std::move(m_result));
|
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
|
||||||
volume->set_new_unique_id();
|
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
|
||||||
volume->calculate_convex_hull();
|
// texture layer definitions themselves (and paint outside the baked area, if any) are left
|
||||||
|
// untouched so the user can keep sculpting with the same textures.
|
||||||
|
for (const TextureDisplacementLayer &layer : m_input.layers)
|
||||||
|
if (!layer.empty() && layer.slot >= 0 && layer.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
||||||
|
volume->texture_displacement_facet(layer.slot).reset();
|
||||||
|
|
||||||
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
|
ModelObject *object = volume->get_object();
|
||||||
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
|
if (object == nullptr)
|
||||||
// texture layer definitions themselves (and paint outside the baked area, if any) are left
|
return;
|
||||||
// untouched so the user can keep sculpting with the same textures.
|
|
||||||
for (const TextureDisplacementLayer &layer : m_input.layers)
|
|
||||||
if (!layer.empty() && layer.slot >= 0 && layer.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
|
|
||||||
volume->texture_displacement_facet(layer.slot).reset();
|
|
||||||
|
|
||||||
ModelObject *object = volume->get_object();
|
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||||
if (object == nullptr)
|
const ModelObjectPtrs &objs = plater->model().objects;
|
||||||
return;
|
auto it = std::find(objs.begin(), objs.end(), object);
|
||||||
|
if (it != objs.end())
|
||||||
|
obj_list->update_info_items(size_t(it - objs.begin()));
|
||||||
|
}
|
||||||
|
|
||||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
plater->changed_object(*object);
|
||||||
const ModelObjectPtrs &objs = plater->model().objects;
|
};
|
||||||
auto it = std::find(objs.begin(), objs.end(), object);
|
|
||||||
if (it != objs.end())
|
// Standard mode's Bake is remesh -> subdivide -> displace under a single snapshot, and this job runs
|
||||||
obj_list->update_info_items(size_t(it - objs.begin()));
|
// long after that snapshot's scope has closed. Adding one here would put an undo step *between* the
|
||||||
|
// subdivision and the displacement: the first Undo would land on a mesh carrying every added
|
||||||
|
// triangle and no relief at all, and pressing Bake again from there would subdivide that mesh a
|
||||||
|
// second time. So the caller says who owns the undo step.
|
||||||
|
if (m_input.take_snapshot) {
|
||||||
|
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction);
|
||||||
|
commit();
|
||||||
|
} else {
|
||||||
|
commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
plater->changed_object(*object);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished)
|
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished,
|
||||||
|
bool take_snapshot)
|
||||||
{
|
{
|
||||||
TextureDisplacementBakeInput input;
|
TextureDisplacementBakeInput input;
|
||||||
|
input.take_snapshot = take_snapshot;
|
||||||
input.volume_id = volume.id();
|
input.volume_id = volume.id();
|
||||||
input.base_mesh = volume.mesh().its;
|
input.base_mesh = volume.mesh().its;
|
||||||
input.layers = volume.texture_displacement_layers;
|
input.layers = volume.texture_displacement_layers;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ struct TextureDisplacementBakeInput
|
|||||||
std::vector<TextureDisplacementLayer> layers;
|
std::vector<TextureDisplacementLayer> layers;
|
||||||
TextureDisplacementFacetsData facets_data;
|
TextureDisplacementFacetsData facets_data;
|
||||||
TextureDisplacementOptions options;
|
TextureDisplacementOptions options;
|
||||||
|
// Whether this job pushes its own undo step when it commits. False when the caller has already
|
||||||
|
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
|
||||||
|
// and subdivides first and has to undo as a single action.
|
||||||
|
bool take_snapshot = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
|
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
|
||||||
@@ -45,7 +49,8 @@ private:
|
|||||||
// the app's UI job worker. `on_finished` is always called once the job settles (success, failure,
|
// the app's UI job worker. `on_finished` is always called once the job settles (success, failure,
|
||||||
// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called
|
// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called
|
||||||
// from the main thread.
|
// from the main thread.
|
||||||
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished);
|
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished,
|
||||||
|
bool take_snapshot = true);
|
||||||
|
|
||||||
} // namespace Slic3r::GUI
|
} // namespace Slic3r::GUI
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user