mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-10 10:47:16 +00:00
Cleanup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Texture Displacement — Technical Notes
|
||||
# Texture Displacement - Technical Notes
|
||||
|
||||
Branch: `feature/texture_displacement`. This document is a knowledge dump of the whole feature as
|
||||
it stands: architecture, file map, algorithms, known bugs found and fixed (with root causes worth
|
||||
@@ -26,39 +26,18 @@ A paint-style gizmo (`GLGizmoTextureDisplacement`) that lets you:
|
||||
### Data model (per `ModelVolume`)
|
||||
|
||||
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 exact same `TriangleSelector`/`FacetsAnnotation` machinery
|
||||
every other paint gizmo (FdmSupports, Seam, MMU, FuzzySkin) already uses, just one full instance
|
||||
per layer slot instead of one per volume. This is what makes "layered/blended" painting work for
|
||||
free: the same triangle can be `ENFORCER` in layer 2's mask and layer 5's mask simultaneously, and
|
||||
at bake/preview time each layer displaces the surface left by the previous one (image-editor-layer
|
||||
semantics).
|
||||
|
||||
**Important gotcha**: `ModelVolume` does **not** hold `std::array<FacetsAnnotation, 8>`. It holds
|
||||
8 individually-named fields (`texture_displacement_facets_0` .. `_7`) plus a
|
||||
`texture_displacement_facet(int slot)` accessor. Reason: `FacetsAnnotation`'s ctor is private,
|
||||
friended only to `ModelVolume`; `std::array`'s own implicitly-generated special member functions
|
||||
are generated with **`std::array`'s** access rights, not the enclosing class's, so friendship does
|
||||
not propagate through the array wrapper. This is a real MSVC C2280 if you try it — confirmed by
|
||||
attempting it. `TextureDisplacementFacetsData` (a `std::array<TriangleSelector::TriangleSplittingData, 8>`,
|
||||
used to carry paint-mask *data* around, e.g. into the bake job) is fine as a real `std::array`
|
||||
since `TriangleSplittingData` has an ordinary public ctor — only the `FacetsAnnotation` object
|
||||
itself has the friend-ctor problem.
|
||||
|
||||
Plus `std::vector<TextureDisplacementLayer> texture_displacement_layers;` — the plain-data layer
|
||||
definitions (texture bytes + params), ordinary public ctor, safe in a vector.
|
||||
|
||||
Touch points that had to mirror the existing `FacetsAnnotation` pattern (see `supported_facets` for
|
||||
the template): all constructors' asserts, copy ctors' init lists, the `-1`-id deserialization ctor,
|
||||
`set_new_unique_id()`, cereal `save`/`load`, `is_texture_displacement_painted()`, and
|
||||
`reset_extra_facets()` (called whenever a topology-changing op like Simplify or subdivision
|
||||
replaces the mesh — this is what drops any unbaked texture-displacement paint, since there's no
|
||||
remap-across-topology-change support for it yet, see Limitations).
|
||||
|
||||
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
|
||||
|
||||
`build_texture_displacement(base_mesh, layers, facets_data)` is **accumulate-then-displace, and
|
||||
topology-preserving**: the returned mesh has exactly the input's vertices and triangles, in the same
|
||||
order — only the positions of displaced vertices differ.
|
||||
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
|
||||
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
|
||||
@@ -70,36 +49,25 @@ order — only the positions of displaced vertices differ.
|
||||
the **base mesh** (never against a previous layer's output), then
|
||||
`selector.get_facets_strict(ENFORCER)` → the painted patch. Two facts are exploited:
|
||||
- `get_facets_strict()` returns the mesh's **entire** referenced vertex array regardless of which
|
||||
state was asked for — only `.indices` is filtered by state. So `get_facets_strict(ENFORCER)`
|
||||
state was asked for - only `.indices` is filtered by state. So `get_facets_strict(ENFORCER)`
|
||||
and `get_facets_strict(NONE)` share identical vertex indexing, which is what lets boundary
|
||||
detection be a plain index check instead of a position-hash lookup.
|
||||
- The selector's vertex array *starts with* the mesh's own vertices (extra ones created where a
|
||||
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`**.
|
||||
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.
|
||||
4. A vertex used by at least one **unpainted** triangle is a boundary vertex — pinned, never
|
||||
4. A vertex used by at least one **unpainted** triangle is a boundary vertex - pinned, never
|
||||
displaced (its final position is ambiguous, it belongs to both regions). Only vertices used
|
||||
exclusively by painted triangles get displaced. This is what keeps bakes seamless with zero
|
||||
remeshing/hole-filling at the seam.
|
||||
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
|
||||
`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.
|
||||
6. Finally, move each touched vertex along its (step 2) normal by its accumulated total.
|
||||
|
||||
**This replaced a sequential design** that re-meshed after each layer and carried the next layer's
|
||||
paint mask onto the result with `TriangleSelector::remap_painting()`. That was the root cause of the
|
||||
reported "the second texture is never applied" bug: remapping a mask onto a mesh whose vertices had
|
||||
just been displaced out from under it routinely produced an empty bitstream, and the layer was then
|
||||
silently `continue`d past. It was also what forced the per-layer vertex duplication and the final
|
||||
`its_compactify_vertices()` weld. The current formulation has neither problem, is substantially
|
||||
faster (no remap, no welding, one pass), makes blend modes possible at all (they need a shared
|
||||
per-vertex accumulator, which sequential re-meshing cannot provide), and — because the output keeps
|
||||
the input's exact vertex indexing — lets GUI code overlay a preview on the base mesh with no index
|
||||
translation.
|
||||
|
||||
### Blend modes
|
||||
|
||||
`TextureBlendMode` {Add, Subtract, Multiply, Divide}, per layer, applied per vertex against the
|
||||
@@ -108,9 +76,9 @@ displacement in **mm**, not a pixel value.
|
||||
|
||||
Add/Subtract are self-explanatory. Multiply/Divide are *scaling* operations and so need a unit
|
||||
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.
|
||||
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 really does hit zero in ordinary use, and an unbounded `1/0` would fling vertices thousands
|
||||
of mm away and poison the mesh's bounding box (and every plate/print-volume check downstream). The
|
||||
floor doubles as a cap on how far Divide can amplify the relief beneath it: at most 20×.
|
||||
@@ -124,64 +92,64 @@ the UI, which labels that layer "Base layer" instead of offering a control that
|
||||
|
||||
Four 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
|
||||
`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.
|
||||
|
||||
- **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).
|
||||
This is the fix for a real, user-reported bug. The previous version *hard-picked* the single axis
|
||||
most aligned with the normal, which is discontinuous wherever that dominant axis flips: on a +X
|
||||
face the planar coordinate is `(y, z)`, on a −Y face it is `(x, z)`, so at the shared edge `u`
|
||||
jumps from `y_edge` to `x_edge`. On a box centred near the origin those two happen to **agree** at
|
||||
the (+,+) and (−,−) corners and **differ by the full corner width** at the (+,−) and (−,+) corners
|
||||
— which is exactly the "two bad corners, two good ones" symmetry that was observed. A weighted
|
||||
- which is exactly the "two bad corners, two good ones" symmetry that was observed. A weighted
|
||||
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
|
||||
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
|
||||
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
|
||||
axis`. Approximation, not an exact fit for arbitrary geometry.
|
||||
- **Spherical** — longitude/latitude around the centroid, scaled by local radius. Same caveat.
|
||||
- **LSCM** — real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's
|
||||
- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat.
|
||||
- **LSCM** - real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's
|
||||
`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,
|
||||
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 silently falls back to
|
||||
Triplanar if not (e.g. multiple disconnected painted islands, or a fully closed patch).
|
||||
CGAL's parameterizer needs a mesh with no isolated/unreferenced vertices, but `get_facets_strict()`
|
||||
returns the *whole* mesh's vertex array — so there's a compaction step
|
||||
returns the *whole* mesh's vertex array - so there's a compaction step
|
||||
(`compact_patch_with_map()`) that builds a clean sub-mesh + an index map back to the original
|
||||
(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,
|
||||
transforms them into the volume's *local* frame (so the projection rides along if the part is later
|
||||
moved), and stores them as `TextureDisplacementLayer::view_project_right/up` (unit vectors, so the
|
||||
projected coordinate stays in mm and `tiling_scale` keeps meaning mm). `sample_layer_height()`
|
||||
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
|
||||
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
|
||||
(`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.
|
||||
|
||||
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
|
||||
dragged over the 3D view whose border becomes the projection's edge. Applying it stores an exact
|
||||
**projective** map in `view_project_matrix`, which supersedes the affine `right`/`up` axes above
|
||||
for that layer (`view_project_projective`).
|
||||
- **"Project only on visible"** (`select_visible_faces()`) — repaints the layer with exactly the
|
||||
- **"Project only on visible"** (`select_visible_faces()`) - repaints the layer with exactly the
|
||||
facets the camera can see, so the projected area matches the viewpoint the projector was captured
|
||||
from. Two tests: a facing test (normal vs. view direction, per triangle — under perspective the
|
||||
from. Two tests: a facing test (normal vs. view direction, per triangle - under perspective the
|
||||
view direction varies across the model, so it is taken from the eye to each centroid), then
|
||||
`MeshRaycaster::get_unobscured_idxs()` on the survivors to drop facets hidden behind other
|
||||
geometry, so a concave part's far inner wall is correctly excluded. One ray query per front-facing
|
||||
facet, hence click-driven (on the checkbox and on each "Capture current view"), never per frame.
|
||||
It **replaces** the layer's paint rather than adding to it — "project onto what I can see" would
|
||||
It **replaces** the layer's paint rather than adding to it - "project onto what I can see" would
|
||||
otherwise accumulate every angle the user had ever looked from.
|
||||
|
||||
### Manual seams and island cutting
|
||||
|
||||
`TextureDisplacementLayer::lscm_seam_edges` — undirected mesh-vertex-index edge pairs the unwrap is
|
||||
`TextureDisplacementLayer::lscm_seam_edges` - undirected mesh-vertex-index edge pairs the unwrap is
|
||||
forced to cut along, on top of the dihedral-angle seams. `segment_into_charts()` takes a set of these
|
||||
(translated from mesh → compacted-patch numbering inside `compute_patch_unwrap()`) and refuses to
|
||||
union two triangles across a marked edge whatever their angle. Both the unwrap cache key and the
|
||||
@@ -190,15 +158,15 @@ untouched) still forces a re-solve. Like the paint masks, seams are mesh-index-s
|
||||
any topology change.
|
||||
|
||||
Two ways to write to it:
|
||||
- **Mark seam (manual, #9)** — a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses
|
||||
- **Mark seam (manual, #9)** - a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses
|
||||
painting. A click raycasts the volume (`m_c->raycaster()->raycasters()[idx]->unproject_on_mesh()`,
|
||||
`idx` = the volume's slot among model-part volumes), finds the facet's edge nearest the hit point,
|
||||
and toggles it. Marked edges render as a red overlay (`render_seam_overlay()`), pulled toward the
|
||||
camera so they read 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, #17)** - `cut_island()` takes the selected chart's triangles (back-mapped from
|
||||
the unwrap via `source_vertex`), finds their 3D bounding box, and marks every edge that straddles
|
||||
the mid-plane perpendicular to the longest axis. The re-unwrap then splits the chart across its
|
||||
narrow waist — the "islands might be very long" case. Exposed as the UV pane's **Cut** button.
|
||||
narrow waist - the "islands might be very long" case. Exposed as the UV pane's **Cut** button.
|
||||
|
||||
### UV-check overlays (checker / distortion)
|
||||
|
||||
@@ -206,7 +174,7 @@ Two ways to write to it:
|
||||
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
|
||||
procedural checkerboard at the layer's uv (per-vertex for LSCM/ViewProjected, in-shader triplanar
|
||||
otherwise) — squares that stay square mean low distortion. **Distortion** (#14) colours each triangle
|
||||
otherwise) - squares that stay square mean low distortion. **Distortion** (#14) colours each triangle
|
||||
blue→green→red by `log2(uv_area / surface_area)` centred on the patch's *median* stretch (so a
|
||||
globally-scaled unwrap reads as uniformly ideal and only relative stretch shows), averaged to
|
||||
vertices. A separate **Show mesh wireframe** toggle (#8) draws the whole volume's triangle edges,
|
||||
@@ -216,7 +184,7 @@ rebuilt only when the vertex count changes (not per stroke).
|
||||
|
||||
`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`
|
||||
directly** — clamping the *coordinate* into range (what an earlier version did) instead smears the
|
||||
directly** - clamping the *coordinate* into range (what an earlier version did) instead smears the
|
||||
border row/column of pixels outward to infinity in every direction, which is a real bug that was
|
||||
reported and fixed (visually: streaky lines radiating out from the painted patch).
|
||||
|
||||
@@ -224,48 +192,26 @@ reported and fixed (visually: streaky lines radiating out from the painted patch
|
||||
|
||||
Deliberately **whole-mesh and uniform**, not limited to the painted patch. A patch-only /
|
||||
adaptive subdivision would create a classic T-junction/cracking problem where the denser
|
||||
(subdivided) and sparser (untouched) regions meet — the fine side has edge midpoints the coarse
|
||||
(subdivided) and sparser (untouched) regions meet - the fine side has edge midpoints the coarse
|
||||
side doesn't know about, producing a real (non-manifold-looking) crack in the baked geometry. This
|
||||
was consciously scoped down from the original plan's "adaptive per-patch subdivider" idea to avoid
|
||||
that correctness risk (a subtly-cracked mesh is a much worse outcome than "not implemented yet").
|
||||
Algorithm: recursive 1-to-4 triangle split via edge midpoints, with a shared per-pass midpoint cache
|
||||
(keyed by sorted vertex-index pair) so triangles sharing an edge get the *same* new vertex — capped
|
||||
(keyed by sorted vertex-index pair) so triangles sharing an edge get the *same* new vertex - capped
|
||||
at `max_iterations` (default 6) passes to bound worst-case triangle-count explosion.
|
||||
|
||||
Wired as a "Subdivide steps" slider (**0–5**, where 0 means no subdivision and previews nothing) plus
|
||||
Preview/Apply/Done in the gizmo panel. **Apply snaps the slider back to 0** — see bug #22: leaving it
|
||||
at the count just committed made the panel immediately re-preview N more passes on top of a mesh 4^N
|
||||
times denser, which is what the "subdivide lags" report was. A real, committed geometry change (like
|
||||
Preview/Apply/Done in the gizmo panel. **Apply snaps the slider back to 0**. A real, committed geometry change (like
|
||||
Bake), using the same `save_painting()`/`set_mesh()`/`restore_painting()` dance `GLGizmoSimplify`
|
||||
uses: supported/seam/mmu/fuzzy-skin masks get remapped onto the new triangles, texture-displacement
|
||||
paint does not (no remap support yet) and is dropped rather than left pointing at now-meaningless
|
||||
triangle indices.
|
||||
|
||||
### Preview pipeline (perf)
|
||||
|
||||
`rebuild_preview()` used to call `build_texture_displacement()` **synchronously on the UI thread**
|
||||
on every stroke-end and every slider release. With multiple painted layers this got slow (each
|
||||
layer's PNG sampling + vertex welding + `remap_painting()` stacks up). Fixed by moving the actual
|
||||
computation into `TextureDisplacementPreviewJob` (mirrors `TextureDisplacementBakeJob`'s
|
||||
process()/finalize() split), queued on the app's shared UI job worker via plain `queue_job()` (not
|
||||
`replace_job()` — that worker is shared app-wide, including with Bake; `replace_job()` would cancel
|
||||
an in-flight bake if one happened to be running). A `m_preview_generation` counter discards stale
|
||||
results if a burst of edits queues several jobs in a row and an older one finishes after a newer one.
|
||||
|
||||
Two other real perf fixes worth remembering:
|
||||
- Sliders in ImGui report "changed" continuously on every drag frame, not just once on release —
|
||||
gating the (then-synchronous) rebuild behind "mouse button not currently down" was necessary to
|
||||
stop dozens of rebuilds per drag.
|
||||
- `decode_height_texture()` used to re-decode the same PNG from scratch on every call. Now cached
|
||||
in `TextureDisplacement.cpp`, keyed by a `weak_ptr` to the layer's `image_data` (not just the raw
|
||||
pointer — a `weak_ptr` correctly detects a freed-then-reused address, where a raw-pointer key
|
||||
would alias a stale cache entry onto an unrelated later texture).
|
||||
|
||||
### Fast bump preview (GPU-only, no CPU meshing)
|
||||
|
||||
`resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}`, registered as
|
||||
`"texture_displacement_bump"`. Perturbs the *shading* normal from the height texture's local
|
||||
gradient instead of moving geometry — active-layer-only, toggled via a "Fast preview (normal map)"
|
||||
gradient instead of moving geometry - active-layer-only, toggled via a "Fast preview (normal map)"
|
||||
checkbox. Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the
|
||||
per-vertex paint weight (0/1) and `tex_coord` carries a precomputed texture UV, so it can use
|
||||
`GLModel` normally instead of needing a hand-rolled VBO/VAO manager. Weight buffer is
|
||||
@@ -279,24 +225,24 @@ The perturbed normal is the analytic one for a height field `H = ±depth_mm · h
|
||||
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
|
||||
match the bake's — see bug #13.
|
||||
match the bake's - see bug #13.
|
||||
|
||||
**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
|
||||
the dominant normal component, mirroring `project_planar()`/`apply_uv_transform()`, and the slope 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
|
||||
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)** - `uv` comes per-vertex from the CPU
|
||||
(`compute_lscm_uvs(patch, layer)`, so island placement + tiling/rotation/offset are already folded
|
||||
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
|
||||
*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 -
|
||||
the first cut used the same global `1/tiling_scale` factor as triplanar and the depth came out
|
||||
visibly wrong, because an LSCM map is **conformal, not isometric**: it is globally area-scaled but
|
||||
the *local* mm-per-uv varies across the chart. `dFdx(h)` captures the true on-screen rate of change
|
||||
however the chart is stretched. **This path is also what makes the fast preview follow the UV
|
||||
editor: move an island and its uv — hence its bump — moves with it** (the bump mesh rebuilds on
|
||||
editor: move an island and its uv - hence its bump - moves with it** (the bump mesh rebuilds on
|
||||
drag-end, since `on_island_edited(finished)` → `rebuild_preview()` → `rebuild_bump_preview_mesh()`).
|
||||
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
|
||||
@@ -306,49 +252,31 @@ Remaining deliberate 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)`.
|
||||
|
||||
> **Known divergence from the CPU path (not yet reconciled).** The shader's `project_uv()` mirrors
|
||||
> the *hard-axis* `project_planar()`, but the CPU's Triplanar mode is now a **blended** three-plane
|
||||
> sample (see Projection methods — that change is what fixed the 90°-corner seam). The two therefore
|
||||
> still agree on any face that is roughly axis-aligned (one blend weight ≈ 1 there, so the blend
|
||||
> degenerates to exactly the hard-axis pick), and disagree in the cross-fade band around a sharp
|
||||
> edge — precisely where the fast preview will still show the old hard seam that the true preview and
|
||||
> the bake no longer have. Reconciling it means sampling all three planes in the shader and blending
|
||||
> the three gradients by `pow(abs(N), TRIPLANAR_BLEND_SHARPNESS)`, the same weights
|
||||
> `sample_layer_height()` uses. Also note the shader is still **active-layer-only** and knows nothing
|
||||
> about `TextureBlendMode`, so a multi-layer stack cannot match the true preview by construction.
|
||||
|
||||
### On-canvas "Adjust Texture" gizmo
|
||||
|
||||
A per-active-layer toggle ("Adjust placement (drag on model)") that disables painting and shows a
|
||||
flat pan panel (free 2D drag on both axes) plus two arrows along the patch's own U/V axes
|
||||
(constrained single-axis drag). Anchored to the painted patch's centroid/average-normal
|
||||
(`compute_layer_paint_anchor()`). Hit-testing is screen-space distance/point-to-segment (not real
|
||||
3D ray intersection against the handle geometry) — simple and good enough at this handle size.
|
||||
|
||||
Known unverified detail: the **offset-drag direction/sign** is reasoning-based (increasing `offset`
|
||||
shifts which texel is sampled at a fixed world position, which visually slides the pattern the
|
||||
*opposite* way — so the code subtracts), not visually confirmed, since this environment can't
|
||||
render pixels. May need a one-line sign flip once actually tested. The rotation-arrow-implied
|
||||
direction should be reliable (it follows directly from a self-consistent 2D basis, no such
|
||||
ambiguity).
|
||||
3D ray intersection against the handle geometry) - simple and good enough at this handle size.
|
||||
|
||||
### Projection frame overlay (ViewProjected)
|
||||
|
||||
`src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` — a semi-transparent, resizable `wxFrame` the user
|
||||
`src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - a semi-transparent, resizable `wxFrame` the user
|
||||
drags **over the 3D view**, like a slide projector's gate. Whatever the model shows through it is what
|
||||
the texture is projected onto, and the window's border becomes the hard edge of the displacement.
|
||||
Press **Apply projection frame** and the gizmo reads the window's rectangle and commits it.
|
||||
|
||||
The window is deliberately **dumb**: it owns no placement state and reports nothing continuously. Its
|
||||
position and size *are* the placement, read on demand at Apply — which is also when the expensive
|
||||
position and size *are* the placement, read on demand at Apply - which is also when the expensive
|
||||
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
|
||||
`wxGLContext`, the cause of bugs #10 and #14 below. It only ever draws a bitmap and a border.
|
||||
|
||||
**The projective mapping (`apply_projection_frame()`)** is the substantive part. The frame defines a
|
||||
**The projective mapping (`apply_projection_frame()`)**. The frame defines a
|
||||
**screen-space** rectangle, but the bake samples from a **local-space** position, so the two have to be
|
||||
reconciled. `view_project_right/up` can only express an *affine* projection — exact under an
|
||||
reconciled. `view_project_right/up` can only express an *affine* projection - exact under an
|
||||
orthographic camera, but wrong under perspective, where the near end of a part projects larger than the
|
||||
far end and no pair of axes reproduces that. So the layer instead stores a full projective map
|
||||
(`view_project_matrix`, row-major 3×4, `uv = (row0·p̃/row2·p̃, row1·p̃/row2·p̃)`), built like this:
|
||||
@@ -361,19 +289,19 @@ far end and no pair of axes reproduces that. So the layer instead stores a full
|
||||
- Window coordinates follow `igl::project`'s convention (as `CameraUtils::project` does), with y
|
||||
measured downward. Writing `uv = (win − rect_origin) / rect_size` makes u and v affine in
|
||||
`ndc = clip.xyz / clip.w`; multiplying through by `clip.w` leaves a plain linear combination of `K`'s
|
||||
rows, which is exactly the 3×4 matrix — the perspective divide survives intact.
|
||||
rows, which is exactly the 3×4 matrix - the perspective divide survives intact.
|
||||
- `w > 0` is checked rather than divided blindly. A point behind the projector has `w < 0` and divides
|
||||
to a plausible-looking but **mirrored** uv — the classic way a projected decal reappears on the back
|
||||
to a plausible-looking but **mirrored** uv - the classic way a projected decal reappears on the back
|
||||
of a model. `project_uv_projective()` returns false there and the caller treats it as no height.
|
||||
|
||||
The map already includes placement, so `apply_uv_transform()` is **not** applied on top of it — the
|
||||
The map already includes placement, so `apply_uv_transform()` is **not** applied on top of it - the
|
||||
window's own position and size are the placement, and the tiling/rotation/offset sliders would shove
|
||||
the result off the frame the user just aligned. A "Clear" button drops back to the affine path where
|
||||
those controls mean something again.
|
||||
|
||||
Apply also sets `tile_enabled = false`, so `DecodedHeightTexture::sample()` returns 0 outside `[0,1)`
|
||||
and the border is a hard edge rather than the first seam of an endless repeat, and repaints the layer
|
||||
via `select_visible_faces(&matrix)` — the frame's uv square clips the selection, which both matches the
|
||||
via `select_visible_faces(&matrix)` - the frame's uv square clips the selection, which both matches the
|
||||
paint to the border and keeps the ray queries proportional to the framed area instead of the model.
|
||||
|
||||
Owned by the gizmo and **destroyed** (not just hidden) in `on_shutdown()`. Closing it only hides it, so
|
||||
@@ -381,10 +309,10 @@ reopening keeps it where it was left.
|
||||
|
||||
### UV Editor pane
|
||||
|
||||
`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
|
||||
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 Blender-style status line
|
||||
(same file) that adds a button row (Frame / Snap / Average scale) and a status line
|
||||
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)`
|
||||
shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame), and
|
||||
@@ -392,312 +320,102 @@ shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame)
|
||||
|
||||
Deliberately **shares the app's one real `wxGLContext`** (`wxGetApp().init_glcontext(*this)`, the
|
||||
same call `View3D`/`Preview`/`AssembleView` make) rather than creating an independent context like
|
||||
`SkipPartCanvas` does elsewhere in this codebase — this is what lets it reuse the already-registered
|
||||
`SkipPartCanvas` does elsewhere in this codebase - this is what lets it reuse the already-registered
|
||||
`"flat"`/`"flat_texture"` shaders and `GLModel` as-is, instead of needing its own shader
|
||||
compilation/VBO management.
|
||||
|
||||
**Geometry is uploaded once, in the unwrap's own (raw, mm) coordinates**, one `GLModel` set per
|
||||
island; each island is then drawn through its own 2x3 affine (`island_transform_matrix()` composed
|
||||
with the layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. This is
|
||||
the fix for the ~200 ms-per-frame island-drag stall (#3): the old design pre-transformed every UV on
|
||||
the CPU and re-uploaded the entire wireframe on every mouse-move event, which on a million-triangle
|
||||
patch is exactly as slow as it sounds. Now a drag updates one matrix per island and touches no vertex
|
||||
buffer — `on_island_edited(!finished)` calls only `set_island_transforms()`, and the full
|
||||
with the layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. A
|
||||
drag updates one matrix per island and touches no vertex
|
||||
buffer - `on_island_edited(!finished)` calls only `set_island_transforms()`, and the full
|
||||
`set_islands()` rebuild happens solely when the unwrap itself changes (`unwrap_changed` in
|
||||
`update_uv_editor()`).
|
||||
|
||||
**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
|
||||
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
|
||||
`TextureIsland::scale`; "Average 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
|
||||
`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
|
||||
`CommandFn`; view-only ones (Frame, Snap) it handles directly.
|
||||
|
||||
## Bugs found and fixed this session (worth remembering)
|
||||
|
||||
These were all real, confirmed root causes (found by reading the actual code path, not guessed):
|
||||
|
||||
1. **Cross-face projection distortion** — see "Triplanar" above. Fixed by projecting each vertex
|
||||
with its own normal instead of one shared patch-average normal.
|
||||
2. **Disabled-tile smearing to infinity** — clamping the UV *coordinate* into `[0,1]` instead of
|
||||
returning 0 outside it, when tiling is off. Fixed in `DecodedHeightTexture::sample()`.
|
||||
3. **Invisible checkbox/radio "checked" state in light mode** — `ImGuiWrapper::push_toolbar_style()`
|
||||
sets `ImGuiCol_CheckMark` to white while the checkbox/radio frame background is fully transparent
|
||||
(alpha 0) over a light window background — a white checkmark on an effectively-white background
|
||||
is invisible by construction. This is a **pre-existing, general app-wide bug**, not specific to
|
||||
this feature (every panel using `push_toolbar_style()` in light mode has it) — fixed by changing
|
||||
just the light-mode branch's `CheckMark` color to the app's teal accent.
|
||||
4. **Distorted (non-aspect-correct) texture thumbnails** — was forcing a square `ImGui::Image` size
|
||||
regardless of the source image's actual aspect ratio.
|
||||
5. **`std::array<FacetsAnnotation, 8>` compile error** — see Data model above (MSVC C2280,
|
||||
`std::array`'s implicit special members don't inherit element-type friendship).
|
||||
6. **Eigen ternary expression-template type mismatch** (MSVC C2446) — `cond ? (n / len) :
|
||||
Vec3f::UnitZ()` fails because the two branches are different unevaluated Eigen expression
|
||||
*types* with no common type; fixed by wrapping the non-`UnitZ()` branch in an explicit
|
||||
`Vec3f(...)` to force a concrete common type.
|
||||
7. **Post-bake stale preview** — `GLGizmoPainterBase::data_changed()`'s change-detection only
|
||||
checks object id / volume count, neither of which changes when Bake replaces a volume's mesh
|
||||
(same object, same volume count, just a new mesh/id on the volume itself) — so the gizmo kept
|
||||
rendering/painting against the pre-bake `TriangleSelectorPatch` until manually deselected and
|
||||
reselected. Fixed by explicitly calling `update_from_model_object()` in the bake-completion
|
||||
callback.
|
||||
8. **Bake job / crash-report `resources` junction going stale** — unrelated to this feature's code,
|
||||
but hit during testing: `build/src/Release/resources` was a leftover **empty plain directory**
|
||||
instead of the junction CMake's post-build step creates (`if not exist` skipped it because the
|
||||
empty folder already "existed"), so the built exe couldn't find `resources/data/hints.ini`,
|
||||
leaving `HintDatabase`'s hint list empty → `rand() % 0` divide-by-zero crash before the UI ever
|
||||
opened. Fixed by deleting the empty folder and manually recreating the `mklink /J` junction.
|
||||
9. **Fast bump preview showing a solid black object** — `GLModel::render()` **unconditionally**
|
||||
re-sets the shader's `"uniform_color"` uniform from its own internal `Geometry::color` field
|
||||
(defaulting to `ColorRGBA::BLACK()`) right before every draw call — so a manual
|
||||
`shader->set_uniform("uniform_color", ...)` call made just before `.render()` gets silently
|
||||
clobbered. Any `GLModel` that needs a specific flat color **must** call `.set_color(...)` on the
|
||||
model itself, not set the shader uniform directly. Found by reading `GLModel::render()`'s actual
|
||||
source rather than guessing at shader/lighting math.
|
||||
10. **UV editor canvas rendering nothing / showing stale content on resize** — the canvas requested
|
||||
a generic `wxGLAttributes().Defaults()` pixel format while sharing the app's one real
|
||||
`wxGLContext` (which was originally created against `View3D`'s canvas, itself requesting a
|
||||
specific RGBA/24-bit-depth/8-bit-stencil format). `wxGLCanvas::SetCurrent()` on WGL/GLX
|
||||
generally requires the target window's pixel format to be compatible with the one the context
|
||||
was created against; a mismatch can make `SetCurrent()` silently fail, leaving the canvas
|
||||
showing whatever was last in its backbuffer (looks exactly like "blank" or "stale image on
|
||||
resize"). Fixed by requesting the same explicit attribute list
|
||||
`OpenGLManager::create_wxglcanvas()` uses for the main view canvases — but see bug #14: the
|
||||
first attempt at this copied only *part* of that list and the symptom therefore survived.
|
||||
11. **`<glad/gl.h>` / `<wx/glcanvas.h>` include-order conflict** — `wx/glcanvas.h` pulls in the
|
||||
platform's real `GL/gl.h`; if that happens before `<glad/gl.h>` is processed in the same
|
||||
translation unit, glad's own header errors out (`OpenGL (gl.h) header already included`).
|
||||
Fixed by including `<glad/gl.h>` first in `UVEditorCanvas.hpp`, before `<wx/glcanvas.h>` — any
|
||||
file that includes this header (including `Plater.cpp`, transitively) needs glad to win that
|
||||
race.
|
||||
12. **Fast preview hidden behind the paint-highlight overlay** — `render_painter_gizmo()` always
|
||||
drew the selection-highlight overlay on top with a depth-bias trick (`glPolygonOffset`) that
|
||||
only makes sense for the *true*-displacement preview: real geometry moves in the painted area,
|
||||
so the depth-biased overlay only wins the depth test in the *unpainted* (coincident) region.
|
||||
The bump preview never moves geometry — its depth is identical to the overlay's *everywhere* —
|
||||
so the overlay was winning the depth test across the whole surface and hiding the bump shading
|
||||
entirely. Fixed by skipping the overlay draw entirely when the bump-preview path is active.
|
||||
13. **Fast preview's apparent depth not matching the true preview's** — the bump shader built its
|
||||
perturbed normal as `normalize(N + depth_mm * vec3(hL-hR, hD-hU, 0))`. Two things wrong with
|
||||
that. (a) `hL-hR` is a height difference across *one texel step*, i.e. `dh/du` already scaled by
|
||||
`2·texel`, and `du` is in uv units, not mm — the real surface slope needs the full chain rule
|
||||
back through `uv = R(rotation) · planar_mm / tiling_scale`, i.e. a further `1/tiling_scale` and
|
||||
a rotation of the gradient by `−rotation`. The missing `1/(2·texel·tiling_scale)` factor is
|
||||
~26× at a 1024px texture and a 20mm tile size, all in the flattening direction — which is
|
||||
exactly what "fast preview has a different height from the real preview" looks like. (b) the
|
||||
gradient was added to model-space `xy`, but the two axes the planar projection actually runs
|
||||
along are `yz`/`xz`/`xy` depending on the dominant normal component, so on any face not
|
||||
dominated by `z` the perturbation was applied to the wrong axes. Fixed by computing the real
|
||||
mm-per-mm slope and rotating it into the projection's own `T`/`B` axes (see "Fast bump preview"
|
||||
above for the derivation).
|
||||
14. **UV editor pane still blank after bug #10** — three separate causes, all of them live at once:
|
||||
- The bug-#10 fix copied `OpenGLManager::create_wxglcanvas()`'s attribute list but **dropped its
|
||||
multisampling attributes** (`WX_GL_SAMPLE_BUFFERS`/`WX_GL_SAMPLES`, 4 samples by default), on
|
||||
the reasoning that a flat 2D wireframe view doesn't need AA. But a differing sample count *is*
|
||||
a differing pixel format, so this left exactly the `wglMakeCurrent()` mismatch bug #10 set out
|
||||
to fix. It now mirrors the full list, AA included, reading `OpenGLManager::can_multisample()`
|
||||
(already resolved by then — `View3D` is constructed first).
|
||||
- `set_mesh()`/`set_background_texture()` each called `render()` **inline**, and both are reached
|
||||
from `update_uv_editor()` → `rebuild_preview()` → the gizmo's ImGui panel — i.e. from the
|
||||
middle of the *3D* canvas's GL frame, and (since `show_uv_editor(true)` is the last line of
|
||||
`update_uv_editor()`) while this pane was still **hidden**. `wxGLCanvas::SetCurrent()` returns
|
||||
false outright on a canvas that isn't shown on screen, and the old code ignored the return
|
||||
value — so every GL call in `render()`, `glViewport`/`glClear` included, silently landed on the
|
||||
3D canvas instead. These now only mark dirty + `Refresh()`; `render()` bails unless
|
||||
`IsShownOnScreen()` *and* `SetCurrent()` succeeds; and `Plater::show_uv_editor()` defers its
|
||||
AUI relayout via `CallAfter` so the pane's first size/paint can't be delivered mid-frame either.
|
||||
- Even once drawing, nothing would have been *visible*: the background quad spanned `[-1,1]²`
|
||||
while LSCM UVs land around `[0,1]²`, and the view was centered on the origin at a half-extent
|
||||
of 0.6. The quad is now the unit square in the same UV space the wireframe uses (which is also
|
||||
where `sample()` maps the texture, regardless of its pixel aspect), the projection's Y is
|
||||
negated so `v` runs down-screen (putting the texture's first pixel row at the top rather than
|
||||
upside down), and the view auto-fits to the unwrap ∪ unit square the first time a patch shows up.
|
||||
15. **A second texture layer was silently never applied** — the reported "multiple textures don't
|
||||
work reliably". Root cause was the old sequential bake: layer N's paint mask was stored against
|
||||
the volume's original mesh, so before layer N+1 could be deserialized the mask had to be carried
|
||||
onto the mesh layer N had *just displaced*, via `TriangleSelector::remap_painting()`. Remapping a
|
||||
mask onto geometry that has moved out from under it routinely returned an empty bitstream, and
|
||||
the code then did `if (data.bitstream.empty()) continue;` — i.e. dropped the layer **without any
|
||||
diagnostic**. Fixed structurally rather than patched: every layer is now evaluated against the
|
||||
base mesh and merged per vertex (see Bake algorithm), so no remap happens at all. Covered by a
|
||||
regression test.
|
||||
16. **Hard-axis triplanar seam at exactly two of a box's four vertical corners** — see "Triplanar"
|
||||
under Projection methods. Worth recording the *diagnostic* here, because the asymmetry is what
|
||||
pinned it down: the user reported the seam at the (X+,Y−) and (X−,Y+) corners with the other two
|
||||
clean. That is precisely what a dominant-axis switch predicts (`u = y` on an X face, `u = x` on a
|
||||
Y face; those agree where `x == y` and differ by the corner width where `x == −y`) and it ruled
|
||||
out every "the texture is wrong" hypothesis, since the texture itself is fine — the *mapping* is
|
||||
discontinuous. Fixed by blending the three axis projections instead of picking one.
|
||||
17. **Use-after-free when removing a texture layer** (latent, pre-existing — found while touching the
|
||||
panel, not caused by it). The layer list's "Remove" button called `remove_texture_layer()` *in
|
||||
the middle of rendering that layer's row*. That erases the layer from
|
||||
`mv->texture_displacement_layers`, shifting every later element down — after which the loop
|
||||
happily carried on dereferencing `layer` for the rest of the row's widgets (depth/tiling sliders,
|
||||
`PopID`) and kept iterating `ordered`, a vector of pointers into the storage that had just moved.
|
||||
Never crashed loudly because `vector::erase` doesn't reallocate, so the reads landed on a *valid*
|
||||
but *wrong* (shifted) layer. Fixed by recording the slot and doing the removal after the loop.
|
||||
18. **Every LSCM island collapsed to a point** (the UV editor was empty; the Tile-size slider did
|
||||
nothing; an LSCM bake came out as a flat "single-face extrude"). One line in
|
||||
`compute_patch_unwrap()`: `chart.indices = std::move(chart_mesh.indices);` ran *before*
|
||||
`area_3d(chart_mesh)` was taken. `area_3d()` iterates those indices, so on the moved-from
|
||||
(emptied) mesh it returned `0`, giving `scale = sqrt(0 / uv_area) = 0` — **every chart's UVs
|
||||
multiplied by zero**. That one zero explained all three symptoms at once (no island extent to
|
||||
draw; a zero-size unwrap is still zero after any tiling divide; the bake sampled ~one constant
|
||||
texel per chart). Fixed by measuring the 3D area before the move. Found only by instrumenting the
|
||||
actual island bbox into the panel — three rounds of reasoning from screenshots had each guessed
|
||||
wrong, because a collapsed-to-a-point unwrap and an off-screen-framed one look identical.
|
||||
|
||||
19. **Isotropic remeshing produced scrambled geometry with dropped triangles** — the "remeshing kinda
|
||||
does not work properly" report, and a genuine one-line root cause. `isotropic_remeshing()` edits
|
||||
the `Surface_mesh` **in place**, and its edge collapses only *mark* vertices and faces as removed;
|
||||
the underlying arrays keep the holes until `collect_garbage()` is called. That matters because the
|
||||
shared `cgal_to_indexed_triangle_set()` numbers its output vertices by **iteration order** (which
|
||||
skips removed slots) while reading each face's corner as the **raw integer value of the vertex
|
||||
descriptor** (which does not). The two agree only up to the first collapse; past that every
|
||||
triangle indexes the wrong vertices, and any descriptor beyond the live vertex count hits the
|
||||
converter's `iv >= vsize` guard and is silently dropped *together with its triangle*. Fixed by
|
||||
compacting (`cgal_mesh.collect_garbage()`) before converting. Note the pre-existing callers were
|
||||
unaffected: the boolean ops build their result into a fresh mesh, so only the in-place remesher
|
||||
ever handed the converter a mesh with garbage in it.
|
||||
20. **Remeshing rounded off every sharp edge** — the second half of the same report. CGAL's
|
||||
`isotropic_remeshing` relaxes vertices tangentially along the surface, which erodes hard features
|
||||
unless they are constrained: a cube came back with wobbly, eroded edges. Fixed by detecting sharp
|
||||
edges (`PMP::detect_sharp_edges()` at a user-settable dihedral angle, default 40°) plus every open
|
||||
border, constraining them, and passing `protect_constraints(true)`. That option additionally
|
||||
requires each constrained edge to already be shorter than 4/3 · target, hence the
|
||||
`PMP::split_long_edges()` pre-pass (given the same map, so the halves inherit the constraint) —
|
||||
this mirrors CGAL's own isotropic_remeshing example. Also added a guard for the case where
|
||||
`Surface_mesh::add_face()` refused faces on a non-manifold input: remeshing a mesh that silently
|
||||
lost faces yields a **punctured** model, so it now bails out and reports instead.
|
||||
21. **Remesh falsely reporting "did not change the model"** — the GUI decided success by comparing
|
||||
*vertex counts*. A remesh that redistributes triangles at roughly the current density legitimately
|
||||
lands on the same count, so a perfectly good result was discarded with an error. Now compared
|
||||
structurally against the input (which is what `remesh_isotropic()` hands back on failure).
|
||||
22. **Subdivide re-previewing at the same count after Apply** — Apply committed N passes and then
|
||||
immediately rebuilt the *preview* at N passes again, now on top of a mesh up to 4^N times denser.
|
||||
That is the single most expensive thing the panel can do, and it ran on every Apply. The slider
|
||||
now starts at 0 (a real "no subdivision" value that previews nothing), and Apply snaps back to it.
|
||||
|
||||
## 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
|
||||
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, and now `subdivide_mesh_uniform()` all drop it via `reset_extra_facets()`).
|
||||
The other four paint channels (supported/seam/mmu/fuzzy) do get remapped in these cases.
|
||||
- **Cylindrical/Spherical axis/center are auto-picked heuristically**, not user-controllable — no
|
||||
- **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**, while the true preview stacks every painted layer —
|
||||
so with more than one layer painted the two will legitimately not agree, independently of bug #13.
|
||||
- **Bump preview and true preview both only refresh at stroke-end**, not continuously during an
|
||||
active drag (a deliberate scope cut for simplicity/consistency — the original plan's "instant
|
||||
update mid-stroke" idea for the bump shader specifically was not carried through).
|
||||
- **On-canvas Adjust-Texture gizmo's offset-drag direction is unverified** (see above).
|
||||
- **The bump shader still uses hard-axis, single-layer projection** while the CPU path is now
|
||||
blended-triplanar and blend-mode aware — see the callout under "Fast bump preview".
|
||||
- **Fast preview covers the active layer only**
|
||||
- **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 "Subdivide model" button is for.
|
||||
matter how high-resolution the height map is - that is what the "Subdivide model" button is for.
|
||||
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.
|
||||
- **Placeholder toolbar icon** — reuses `toolbar_fuzzy_skin_paint.svg`, noted as a TODO in code.
|
||||
- **Textures are matched to the picker by absolute path** (`TextureDisplacementLayer::path`), so the
|
||||
picker's "which entry is selected" highlight goes blank if a project is moved between machines.
|
||||
Harmless — the layer keeps its own embedded `image_data` and still bakes correctly.
|
||||
|
||||
### Requested UV-editing features — status
|
||||
|
||||
A user working through the feature end-to-end asked for a batch of UV-editing features. All of the
|
||||
functional ones are now implemented (see the sections above): checker (#13), distortion heatmap
|
||||
(#14), mesh wireframe overlay (#8), cut island (#17), project-from-view (#6), and Blender-style mark
|
||||
seam (#9), plus per-island fills, the fast preview honouring the LSCM unwrap and island moves (#1),
|
||||
the UV pane toolbar + status line (#18), Shift-snap rotation, midlevel/bidirectional displacement
|
||||
(#19), and island scale/average/padding/snap (#15/#16/#2).
|
||||
|
||||
Remaining cosmetic / known gaps:
|
||||
- The UV pane toolbar has **text buttons, not icons** — no existing SVG reads cleanly as "average
|
||||
island scale" / "snap islands", so real icons are deferred rather than mis-assigned.
|
||||
- **Mark-seam edge picking** snaps to the nearest edge of the *hit facet* only; it does not
|
||||
highlight the candidate edge on hover before you click (a hover-preview would be a nice refinement).
|
||||
- **Cut island** always halves along the longest 3D axis; there is no UI to pick the cut line.
|
||||
|
||||
## File map
|
||||
|
||||
**libslic3r (core, no GUI dependency):**
|
||||
- `src/libslic3r/TextureDisplacement.hpp/.cpp` — data model, bake algorithm, projection methods,
|
||||
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
|
||||
tiling, subdivision. See doc comments throughout, they're kept accurate and up to date.
|
||||
- `src/libslic3r/MeshBoolean.hpp/.cpp` — added `parameterize_lscm()` and `remesh_isotropic()`
|
||||
(sharp-feature-preserving isotropic remeshing, see bugs #19–#21) in the `cgal` sub-namespace,
|
||||
- `src/libslic3r/MeshBoolean.hpp/.cpp` - added `parameterize_lscm()` and `remesh_isotropic()`
|
||||
in the `cgal` sub-namespace,
|
||||
reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already there for
|
||||
mesh boolean ops. New CGAL includes: `Polygon_mesh_processing/border.h`,
|
||||
`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 `Surface_mesh_parameterization` package headers were already present, just unused before now.
|
||||
- `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`, and all the mirrored touch points (see Data model above).
|
||||
|
||||
**GUI:**
|
||||
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` — the gizmo. Panel controls: dock/
|
||||
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo. Panel controls: dock/
|
||||
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),
|
||||
and loads a library file's bytes for a layer. The image→grayscale-PNG conversion lives here, on the
|
||||
GUI side, because libslic3r has no image toolkit; both the import path and the "pick a shipped
|
||||
texture" path go through the same one function.
|
||||
- `resources/textures/displacement/*.png` — the 10 shipped height maps (Bricks, Grid, Hexagons,
|
||||
- `resources/textures/displacement/*.png` - the 10 shipped height maps (Bricks, Grid, Hexagons,
|
||||
Knurl, Noise, Quilt, Studs, Waves, Weave, Wood Grain). All 512×512 8-bit grayscale and **seamless**
|
||||
(each is periodic over the full image in both axes, so tiling shows no seam). Generated
|
||||
procedurally; the whole `resources/` tree is installed recursively by CMake, so a new folder under
|
||||
it ships with no build-system change.
|
||||
- `src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp/.cpp` — background bake commit.
|
||||
- `src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp/.cpp` — background preview compute
|
||||
- `src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp/.cpp` - background bake commit.
|
||||
- `src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp/.cpp` - background preview compute
|
||||
(mirrors the bake job's shape but commits nothing to the Model).
|
||||
- `src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` — the semi-transparent projection-frame overlay for
|
||||
ViewProjected layers (plain 2D `wxPaintDC`, no GL context — see its section above).
|
||||
- `src/slic3r/GUI/UVEditorCanvas.hpp/.cpp` — the 2D UV unwrap viewer widget.
|
||||
- `src/slic3r/GUI/Plater.hpp/.cpp` — `uv_editor_canvas` member, AUI pane registration,
|
||||
- `src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - the semi-transparent projection-frame overlay for
|
||||
ViewProjected layers (plain 2D `wxPaintDC`, no GL context - see its section above).
|
||||
- `src/slic3r/GUI/UVEditorCanvas.hpp/.cpp` - the 2D UV unwrap viewer widget.
|
||||
- `src/slic3r/GUI/Plater.hpp/.cpp` - `uv_editor_canvas` member, AUI pane registration,
|
||||
`get_uv_editor_canvas()`/`show_uv_editor()`.
|
||||
- `src/slic3r/GUI/GLShadersManager.cpp` — registers `"texture_displacement_bump"`.
|
||||
- `resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}` — the bump-preview shader.
|
||||
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` — `PainterGizmoType::TEXTURE_DISPLACEMENT`.
|
||||
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` — `EType::TextureDisplacement` registration.
|
||||
- `src/slic3r/GUI/ImGuiWrapper.cpp` — the light-mode checkmark-color fix (bug #3 above; a
|
||||
pre-existing, general bug, not scoped to this feature).
|
||||
- `src/slic3r/GUI/GLShadersManager.cpp` - registers `"texture_displacement_bump"`.
|
||||
- `resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}` - the bump-preview shader.
|
||||
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`.
|
||||
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration.
|
||||
- `src/slic3r/GUI/ImGuiWrapper.cpp` - the light-mode checkmark-color fix
|
||||
|
||||
**CMake:** all new source files added to `src/libslic3r/CMakeLists.txt`,
|
||||
`src/slic3r/CMakeLists.txt` (in roughly-alphabetical position matching each list's existing
|
||||
convention), and `tests/libslic3r/CMakeLists.txt` for the unit test file.
|
||||
|
||||
**Tests:** `tests/libslic3r/test_texture_displacement.cpp` — **run and passing** (7 cases, 116
|
||||
**Tests:** `tests/libslic3r/test_texture_displacement.cpp` - **run and passing** (7 cases, 116
|
||||
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 —
|
||||
a regression test that a **second layer over the same area actually contributes** (the bug that
|
||||
rewrite fixed), a table-driven check of all four blend modes, and that the lowest layer ignores its
|
||||
displacement, boundary-vertex pinning on a hand-built fan mesh, and - added with the bake rewrite -
|
||||
a regression test that a **second layer over the same area actually contributes**,
|
||||
a table-driven check of all four blend modes, and that the lowest layer ignores its
|
||||
blend mode. `BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them:
|
||||
|
||||
cmake -S . -B build -DBUILD_TESTS=ON
|
||||
cmake --build build --config Release --target libslic3r_tests -- -m
|
||||
./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand
|
||||
|
||||
## Build notes
|
||||
|
||||
- Everything here lives in `libslic3r`/`libslic3r_gui`/`libslic3r_cgal` — no new external
|
||||
dependency, no `deps/` rebuild needed. CGAL's parameterization package was already vendored.
|
||||
- To build just this feature's code path fastest: `cmake --build build --config Release --target
|
||||
libslic3r_gui -- -m` (pulls in `libslic3r` and `libslic3r_cgal` as needed). The full app target
|
||||
is `OrcaSlicer_app_gui` (produces `build/src/Release/orca-slicer.exe`) — only needed to actually
|
||||
run and visually test, not to verify compilation.
|
||||
- `BUILD_TESTS` is `OFF` in the existing build cache; flip it on to actually run
|
||||
`test_texture_displacement.cpp`.
|
||||
./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand
|
||||
@@ -62,7 +62,7 @@ uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived ta
|
||||
// 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 (active > 0.5). Identity when nothing is
|
||||
// dragged, so this whole path is a no-op then. Lets a UV island drag move the bump on the model with
|
||||
// only a uniform update -- no mesh rebuild -- exactly the way Adjust placement moves the whole texture.
|
||||
// only a uniform update
|
||||
uniform vec4 island_delta_lin;
|
||||
uniform vec2 island_delta_tr;
|
||||
|
||||
@@ -76,7 +76,7 @@ in vec2 vertex_uv;
|
||||
out vec4 out_color;
|
||||
|
||||
// The two model-space axes the triplanar planar coordinate is read off, per dominant normal
|
||||
// component -- same choice libslic3r's project_planar() makes, so planar.x runs along t, planar.y
|
||||
// component - same choice libslic3r's project_planar() makes, so planar.x runs along t, planar.y
|
||||
// along b.
|
||||
void projection_axes(vec3 n, out vec3 t, out vec3 b)
|
||||
{
|
||||
@@ -113,10 +113,10 @@ void main()
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
if (use_vertex_uv) {
|
||||
// Precomputed-uv (LSCM) path -- Mikkelsen's surface-gradient bump ("Bump Mapping
|
||||
// Precomputed-uv (LSCM) path - Mikkelsen's surface-gradient bump ("Bump Mapping
|
||||
// Unparametrized Surfaces on the GPU"). The perturbed normal is derived straight from the
|
||||
// screen-space derivatives of the *sampled height* and the position, so it is scale-exact
|
||||
// with no uv->mm assumption at all -- which is the whole point here: an LSCM map is conformal,
|
||||
// with no uv->mm assumption at all - which is the whole point here: an LSCM map is conformal,
|
||||
// not isometric, so the local mm-per-uv varies across the chart and the earlier "one global
|
||||
// 1/tiling factor" got the depth visibly wrong. dFdx(h) captures the true on-screen rate of
|
||||
// change however the chart is stretched or however fine the tiling is.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
// UV-check overlay for the texture-displacement gizmo, drawn over the painted patch so the LSCM
|
||||
// unwrap can be sanity-checked on the real 3D surface (mode set by the `mode` uniform):
|
||||
// mode 0 -- Checker: a procedural checkerboard sampled at the layer's uv. Even squares that stay
|
||||
// mode 0 - Checker: a procedural checkerboard sampled at the layer's uv. Even squares that stay
|
||||
// square everywhere on the model mean the unwrap is low-distortion; squares that smear or
|
||||
// shear reveal exactly where it stretches. Same uv the bake samples, so what you see is
|
||||
// where the texture actually lands.
|
||||
// mode 1 -- Distortion heatmap: the per-vertex area-distortion carried in `distortion`, blue
|
||||
// mode 1 - Distortion heatmap: the per-vertex area-distortion carried in `distortion`, blue
|
||||
// (compressed) -> green (ideal) -> red (stretched).
|
||||
// Both are lit with the same cheap two-light diffuse the bump preview uses, so the surface still
|
||||
// reads as 3D.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
// Vertex stage for the UV-check overlay (checker / distortion heatmap) drawn over the painted patch
|
||||
// by GLGizmoTextureDisplacement. Reuses GLModel's P3N3T2 layout so it needs no bespoke buffer:
|
||||
// v_normal.x -- per-vertex UV distortion (uv-area / surface-area ratio, remapped so 0.5 = ideal);
|
||||
// v_normal.x - per-vertex UV distortion (uv-area / surface-area ratio, remapped so 0.5 = ideal);
|
||||
// only the distortion mode reads it.
|
||||
// v_tex_coord -- precomputed texture uv, valid only when use_vertex_uv is set (LSCM); the checker
|
||||
// v_tex_coord - precomputed texture uv, valid only when use_vertex_uv is set (LSCM); the checker
|
||||
// mode reconstructs uv in the fragment shader otherwise.
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
|
||||
@@ -38,7 +38,7 @@ float DecodedHeightTexture::sample(const Vec2f &uv, bool tile_enabled, TextureTi
|
||||
|
||||
if (!tile_enabled && (uv.x() < 0.f || uv.x() >= 1.f || uv.y() < 0.f || uv.y() >= 1.f))
|
||||
// Outside the single, non-repeating placement entirely: no texture there, not "smeared
|
||||
// edge pixel" -- clamping the *coordinate* to [0, 1] would otherwise keep returning the
|
||||
// edge pixel" - clamping the *coordinate* to [0, 1] would otherwise keep returning the
|
||||
// border row/column's height forever in every direction, stretching it out to infinity.
|
||||
return 0.f;
|
||||
|
||||
@@ -136,7 +136,7 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
if (layer.empty())
|
||||
return result;
|
||||
|
||||
// The raw (unsmoothed) decode is what gets cached, keyed by the image_data allocation -- decoding
|
||||
// The raw (unsmoothed) decode is what gets cached, keyed by the image_data allocation - decoding
|
||||
// a PNG is the expensive part and never changes for a given image. Smoothing is applied afterwards
|
||||
// to a throwaway copy, so moving the smoothing slider never invalidates the decode cache.
|
||||
const void *key = layer.image_data.get();
|
||||
@@ -192,7 +192,7 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
||||
// tri-planar/cube projection; a patch spanning several differently-oriented faces gets each
|
||||
// face projected along its own best-fit axis instead of all faces sharing one axis picked
|
||||
// from a single averaged normal (which looks correct on one face but visibly distorts on any
|
||||
// other face in the same patch -- exactly the bug an earlier version of this feature had).
|
||||
// other face in the same patch - exactly the bug an earlier version of this feature had).
|
||||
const Vec3f n = normal.cwiseAbs();
|
||||
if (n.x() >= n.y() && n.x() >= n.z())
|
||||
return Vec2f(position.y(), position.z());
|
||||
@@ -203,7 +203,7 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
||||
|
||||
namespace {
|
||||
// Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at
|
||||
// this point's own radius, v is the signed distance along the axis -- a reasonable approximation
|
||||
// this point's own radius, v is the signed distance along the axis - a reasonable approximation
|
||||
// for roughly cylindrical selections, not an exact fit for arbitrary geometry.
|
||||
Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis)
|
||||
{
|
||||
@@ -225,7 +225,7 @@ Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, cons
|
||||
|
||||
// Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the
|
||||
// center so the result is in roughly the same mm-ish units tiling_scale expects, rather than bare
|
||||
// radians -- again an approximation, not an exact geodesic parametrization.
|
||||
// radians - again an approximation, not an exact geodesic parametrization.
|
||||
Vec2f project_spherical(const Vec3f &position, const Vec3f &patch_center)
|
||||
{
|
||||
const Vec3f rel = position - patch_center;
|
||||
@@ -239,7 +239,7 @@ Vec2f project_spherical(const Vec3f &position, const Vec3f &patch_center)
|
||||
return Vec2f(longitude, latitude) * radius;
|
||||
}
|
||||
|
||||
// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices -- but
|
||||
// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but
|
||||
// `patch` here (from TriangleSelector::get_facets_strict()) carries the *entire* mesh's vertex
|
||||
// array, only its `indices` filtered to the painted triangles. Build a compacted copy referencing
|
||||
// only the vertices `patch.indices` actually uses, plus a map back to the original vertex index so
|
||||
@@ -328,8 +328,8 @@ std::vector<int> segment_into_charts(const indexed_triangle_set &mesh, const std
|
||||
}
|
||||
}
|
||||
|
||||
// The chart-join test compares a *neighbourhood-averaged* normal per face -- the face plus its
|
||||
// edge-adjacent neighbours (~5 samples) -- rather than the single face normal. On a finely
|
||||
// The chart-join test compares a *neighbourhood-averaged* normal per face - the face plus its
|
||||
// edge-adjacent neighbours (~5 samples) - rather than the single face normal. On a finely
|
||||
// tessellated curved surface this stops one noisy triangle from spuriously cutting (or a lone
|
||||
// near-flat sliver from wrongly merging) a chart, while a genuine sharp crease, where the whole
|
||||
// neighbourhood on each side agrees, still cuts. This is the "use 5 points, not one" refinement.
|
||||
@@ -352,7 +352,7 @@ std::vector<int> segment_into_charts(const indexed_triangle_set &mesh, const std
|
||||
for (const auto &[key, fp] : edge_faces) {
|
||||
if (fp.second < 0)
|
||||
continue; // a boundary edge of the patch, nothing on the far side to join
|
||||
// A manually/auto marked seam always cuts, whatever the dihedral angle -- that is exactly
|
||||
// A manually/auto marked seam always cuts, whatever the dihedral angle - that is exactly
|
||||
// what lets "mark seam" / "cut island" split a chart that is otherwise flat enough to merge.
|
||||
if (!seam_keys.empty() && seam_keys.count(key))
|
||||
continue;
|
||||
@@ -373,7 +373,7 @@ std::vector<int> segment_into_charts(const indexed_triangle_set &mesh, const std
|
||||
}
|
||||
|
||||
// Projects a chart onto an orthonormal basis of its own average normal. This is *isometric* for a
|
||||
// flat chart -- lengths and angles come out exactly right -- which is why a flat chart never needs a
|
||||
// flat chart - lengths and angles come out exactly right - which is why a flat chart never needs a
|
||||
// solve at all, and why this also serves as the fallback for a chart LSCM cannot handle.
|
||||
std::vector<Vec2f> project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal)
|
||||
{
|
||||
@@ -407,7 +407,7 @@ float area_2d(const std::vector<Vec2f> &uvs, const std::vector<stl_triangle_vert
|
||||
}
|
||||
|
||||
// FNV-1a over the patch's geometry plus the seam angle. The unwrap depends on nothing else about a
|
||||
// layer -- not depth, tiling, rotation, offset or even which texture is on it -- so keying the cache
|
||||
// layer - not depth, tiling, rotation, offset or even which texture is on it - so keying the cache
|
||||
// on just this is what lets every one of those sliders be dragged without paying for a re-solve.
|
||||
uint64_t unwrap_cache_key(const indexed_triangle_set &patch, float seam_angle_deg, float padding_mm,
|
||||
const std::vector<std::pair<int, int>> &seam_edges)
|
||||
@@ -532,7 +532,7 @@ PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_a
|
||||
const Vec3f chart_normal = (normal_sum.norm() > 1e-12f) ? Vec3f(normal_sum.normalized()) : Vec3f::UnitZ();
|
||||
|
||||
// Is the chart flat? Charts are grown by a *pairwise* angle threshold, so a chart can still
|
||||
// curve gradually across many triangles -- being merged is not the same as being planar. But
|
||||
// curve gradually across many triangles - being merged is not the same as being planar. But
|
||||
// when it is planar (a cube face, and after seam-cutting that is the common case), the
|
||||
// tangent-plane projection is already the exact answer, and skipping the solve is the single
|
||||
// biggest speed-up here.
|
||||
@@ -541,14 +541,14 @@ PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_a
|
||||
if (chart_of[f] == c)
|
||||
planar = normals[f].dot(chart_normal) >= 0.9998f; // ~1 degree
|
||||
|
||||
// Measured before chart_mesh.indices is moved out from under it, below -- area_3d() iterates
|
||||
// Measured before chart_mesh.indices is moved out from under it, below - area_3d() iterates
|
||||
// those indices, so taking it afterwards silently measures an empty mesh and returns 0.
|
||||
const float mesh_area_3d = area_3d(chart_mesh);
|
||||
|
||||
std::optional<std::vector<Vec2f>> uvs;
|
||||
if (!planar)
|
||||
uvs = MeshBoolean::cgal::parameterize_lscm(chart_mesh);
|
||||
// Flat chart, or one LSCM refused (not a topological disk -- closed, or holed).
|
||||
// Flat chart, or one LSCM refused (not a topological disk - closed, or holed).
|
||||
chart.uvs = uvs ? std::move(*uvs) : project_to_tangent_plane(chart_mesh, chart_normal);
|
||||
chart.indices = std::move(chart_mesh.indices);
|
||||
|
||||
@@ -871,7 +871,7 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
|
||||
return {};
|
||||
|
||||
// Manual per-vertex UV edits (UV editor Vertex/Edge modes) override the automatic raw unwrap
|
||||
// coordinate for a mesh vertex, before the island transform -- so the edit rides along with any
|
||||
// coordinate for a mesh vertex, before the island transform - so the edit rides along with any
|
||||
// island move/rotate exactly like the rest of the island. See TextureDisplacementLayer::
|
||||
// lscm_uv_overrides. Small (hand edits), so a plain map is ample.
|
||||
std::map<int, Vec2f> overrides;
|
||||
@@ -880,7 +880,7 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
|
||||
|
||||
// One UV per patch vertex: a seam vertex has several (one per chart it touches) and has to
|
||||
// settle on one, since it can only be displaced to a single position. See compute_lscm_uvs()'s
|
||||
// header comment -- the surface stays watertight regardless.
|
||||
// header comment - the surface stays watertight regardless.
|
||||
std::vector<Vec2f> per_vertex(patch.vertices.size(), Vec2f::Zero());
|
||||
std::vector<bool> assigned(patch.vertices.size(), false);
|
||||
for (size_t i = 0; i < unwrap.uvs.size(); ++i) {
|
||||
@@ -918,12 +918,12 @@ float blend_displacement(float accumulated, float value, TextureBlendMode mode)
|
||||
// (see TextureBlendMode): a 1 mm-deep layer sampling a white texel is then exactly neutral.
|
||||
case TextureBlendMode::Multiply: return accumulated * value;
|
||||
case TextureBlendMode::Divide: {
|
||||
// Every height map has black regions, and a black texel samples to *exactly* zero -- so this
|
||||
// Every height map has black regions, and a black texel samples to *exactly* zero - so this
|
||||
// divisor really does hit zero in ordinary use, not just in some contrived edge case. Floor
|
||||
// its magnitude: an unbounded 1/0 would not merely look wrong, it would fling vertices
|
||||
// thousands of mm away and poison the mesh's bounding box (and with it every plate/print
|
||||
// volume check downstream). The floor doubles as a cap on how far Divide can ever amplify
|
||||
// the relief beneath it -- at most 1/0.05 = 20x.
|
||||
// the relief beneath it - at most 1/0.05 = 20x.
|
||||
constexpr float min_divisor = 0.05f;
|
||||
const float divisor = (std::abs(value) < min_divisor) ? std::copysign(min_divisor, value < 0.f ? -1.f : 1.f) :
|
||||
value;
|
||||
@@ -983,7 +983,7 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
// Flat projection onto the captured projector plane. Single-valued per point, so unlike
|
||||
// blended triplanar it is one sample, and it is what "project from view" places.
|
||||
return sample_at(Vec2f(position.dot(layer.view_project_right), position.dot(layer.view_project_up)));
|
||||
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch -- fall back to Triplanar
|
||||
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar
|
||||
case TextureProjectionMethod::Triplanar:
|
||||
default: break;
|
||||
}
|
||||
@@ -1048,7 +1048,7 @@ bool compute_layer_paint_anchor(const indexed_triangle_set &b
|
||||
}
|
||||
|
||||
// Area-weighted vertex normals of the undisplaced mesh. build_texture_displacement() computes
|
||||
// these once, up front, and every layer both projects and displaces along them -- so a vertex
|
||||
// these once, up front, and every layer both projects and displaces along them - so a vertex
|
||||
// covered by several layers is pushed along one single, well-defined direction rather than along
|
||||
// whatever direction the surface happened to be pointing partway through the stack.
|
||||
static std::vector<Vec3f> texture_displacement_vertex_normals(const indexed_triangle_set &its)
|
||||
@@ -1122,7 +1122,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
indexed_triangle_set mesh = base_mesh;
|
||||
// TriangleSelector's vertex array starts with the mesh's own vertices (any extra ones, created
|
||||
// where a brush stroke split a triangle, are appended after them), and get_facets_strict()
|
||||
// emits exactly the *referenced* ones, in order. So selector vertex index i is our vertex i --
|
||||
// emits exactly the *referenced* ones, in order. So selector vertex index i is our vertex i -
|
||||
// but only if every vertex of `mesh` is referenced by some triangle, which is precisely what
|
||||
// this call establishes. It is a no-op (indices untouched) for any mesh that already is, which
|
||||
// in practice is all of them; it exists so an input carrying stray unreferenced vertices can't
|
||||
@@ -1140,7 +1140,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
std::sort(ordered_layers.begin(), ordered_layers.end(),
|
||||
[](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; });
|
||||
|
||||
// Every layer measures its displacement against the *original* surface -- normals included --
|
||||
// Every layer measures its displacement against the *original* surface - normals included -
|
||||
// rather than against whatever the previous layer left behind. That is what lets all the layers
|
||||
// be evaluated independently and merged per vertex, instead of having to re-mesh and remap the
|
||||
// paint masks between them (see the header for why that earlier design was dropped).
|
||||
@@ -1167,7 +1167,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
// get_facets_strict() returns the same vertex array whichever state is asked for (only the
|
||||
// triangles are filtered), so `patch` and `rest` share one indexing -- and, per the
|
||||
// triangles are filtered), so `patch` and `rest` share one indexing - and, per the
|
||||
// compactify above, it is our own.
|
||||
const indexed_triangle_set rest = selector.get_facets_strict(EnforcerBlockerType::NONE);
|
||||
|
||||
@@ -1208,7 +1208,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
else if (an.y() <= an.x() && an.y() <= an.z())
|
||||
patch_axis = Vec3f::UnitY();
|
||||
|
||||
// A real unwrap of the whole patch, computed once here rather than per vertex -- it is a
|
||||
// A real unwrap of the whole patch, computed once here rather than per vertex - it is a
|
||||
// per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this
|
||||
// for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()).
|
||||
const std::vector<Vec2f> lscm_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
|
||||
@@ -1255,14 +1255,14 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
patch_centroid, patch_axis, lscm_uv);
|
||||
|
||||
// midlevel is the height that means "stay put", so anything below it displaces
|
||||
// *inwards* -- see TextureDisplacementLayer::midlevel. At the default of 0 this is
|
||||
// *inwards* - see TextureDisplacementLayer::midlevel. At the default of 0 this is
|
||||
// exactly the old outward-only behaviour.
|
||||
const float edge_w = edge_weight.empty() ? 1.f : edge_weight[size_t(vi)];
|
||||
const float signed_height = (h - layer->midlevel) * layer->depth_mm * sign * edge_w;
|
||||
displacement[size_t(vi)] = blend_displacement(displacement[size_t(vi)], signed_height,
|
||||
displaced[size_t(vi)] ? layer->blend_mode : TextureBlendMode::Add);
|
||||
// The first layer to reach a vertex has nothing underneath it to blend with, so it
|
||||
// always starts the total off additively -- a Multiply/Divide against an implicit
|
||||
// always starts the total off additively - a Multiply/Divide against an implicit
|
||||
// zero base would otherwise annihilate (or blow up) it, which is never what the
|
||||
// user means by putting a mask on the bottom of the stack.
|
||||
displaced[size_t(vi)] = true;
|
||||
|
||||
@@ -51,13 +51,13 @@ enum class TextureProjectionMethod : int
|
||||
Triplanar = 0,
|
||||
// Wrapped around an axis running through the patch's centroid. The axis itself is picked
|
||||
// automatically as the world axis *least* aligned with the patch's average normal (since a
|
||||
// cylinder's own axis is perpendicular to its outward radial normal) -- a reasonable default
|
||||
// cylinder's own axis is perpendicular to its outward radial normal) - a reasonable default
|
||||
// for roughly cylindrical selections, not a precise fit for arbitrary geometry.
|
||||
Cylindrical = 1,
|
||||
// Wrapped around the patch's centroid using longitude/latitude -- reasonable for roughly
|
||||
// Wrapped around the patch's centroid using longitude/latitude - reasonable for roughly
|
||||
// spherical/rounded selections, again an approximation rather than an exact geodesic map.
|
||||
Spherical = 2,
|
||||
// Real UV unwrap of the painted patch -- a proper low-distortion flattening rather than a
|
||||
// Real UV unwrap of the painted patch - a proper low-distortion flattening rather than a
|
||||
// planar/cylindrical/spherical approximation. The patch is first cut into charts along its
|
||||
// sharp edges and each chart is flattened on its own (see compute_patch_unwrap()), so a patch
|
||||
// that is not a single developable surface still unwraps sensibly. Falls back to Triplanar for
|
||||
@@ -65,20 +65,20 @@ enum class TextureProjectionMethod : int
|
||||
LSCM = 3,
|
||||
// Flat projection along a fixed direction captured from the 3D camera ("project from view"): the
|
||||
// texture is laid onto the painted area as seen from that angle, like a decal projector. Single
|
||||
// planar map (no per-face axis switch), so it can smear on faces turned away from the projector --
|
||||
// planar map (no per-face axis switch), so it can smear on faces turned away from the projector -
|
||||
// that is inherent to view projection and is the user's call, not a bug. The projector's two
|
||||
// in-plane axes live in TextureDisplacementLayer::view_project_right/up.
|
||||
ViewProjected = 4,
|
||||
};
|
||||
|
||||
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
|
||||
// -- i.e. the unwrap is cut there rather than being forced to flatten across it.
|
||||
// - i.e. the unwrap is cut there rather than being forced to flatten across it.
|
||||
//
|
||||
// The whole point of this being a threshold rather than "flatten everything as one piece": three
|
||||
// faces meeting at a cube corner are not developable, so a single-chart solve has to distort them
|
||||
// badly to lie flat (they splay out into a fan, which is what "it merges all the edges into a
|
||||
// triangle" describes). Cutting at the 90-degree edges instead lets each face flatten exactly.
|
||||
// Meanwhile a smoothly curved surface -- a subdivided sphere, say -- has only small angles between
|
||||
// Meanwhile a smoothly curved surface - a subdivided sphere, say - has only small angles between
|
||||
// neighbouring triangles, stays a single chart, and unwraps as one piece the way it should.
|
||||
static constexpr float LSCM_DEFAULT_SEAM_ANGLE_DEG = 30.f;
|
||||
|
||||
@@ -95,8 +95,8 @@ static constexpr float TRIPLANAR_BLEND_SHARPNESS = 4.f;
|
||||
//
|
||||
// Add/Subtract are in mm and need no further explanation. Multiply/Divide are *scaling* operations
|
||||
// and therefore need a unit convention: they treat the layer's own value as a unitless factor
|
||||
// relative to 1 mm. That makes `depth_mm` act as a gain -- a layer with depth 1 mm and a white
|
||||
// (1.0) texel multiplies the accumulated relief by exactly 1, i.e. leaves it unchanged -- which is
|
||||
// relative to 1 mm. That makes `depth_mm` act as a gain - a layer with depth 1 mm and a white
|
||||
// (1.0) texel multiplies the accumulated relief by exactly 1, i.e. leaves it unchanged - which is
|
||||
// the behaviour that makes a Multiply layer usable as a mask over the layers beneath it.
|
||||
enum class TextureBlendMode : int
|
||||
{
|
||||
@@ -112,20 +112,20 @@ float blend_displacement(float accumulated, float value, TextureBlendMode mode);
|
||||
|
||||
// Where one unwrap island (chart) sits in UV space, on top of wherever compute_patch_unwrap() first
|
||||
// packed it. This is what the UV editor's drag/rotate gestures write to, so a user can lay the
|
||||
// islands out by hand -- move them, rotate them, overlap them -- rather than being stuck with the
|
||||
// islands out by hand - move them, rotate them, overlap them - rather than being stuck with the
|
||||
// automatic packing.
|
||||
//
|
||||
// Indexed by chart id, which compute_patch_unwrap() assigns in first-encountered-triangle order. That
|
||||
// is stable for a given patch and seam angle, but *not* across a change to either: repainting the
|
||||
// patch, or moving the seam-angle slider, can renumber the charts and so leave a hand-placed island
|
||||
// applied to a different one. Accepted deliberately -- the alternative is a persistent chart identity
|
||||
// applied to a different one. Accepted deliberately - the alternative is a persistent chart identity
|
||||
// that survives arbitrary re-segmentation, which is a much larger problem than this feature warrants.
|
||||
struct TextureIsland
|
||||
{
|
||||
Vec2f offset = Vec2f::Zero(); // in the unwrap's own mm space
|
||||
float rotation_deg = 0.f; // about the island's own centroid
|
||||
// About the island's own centroid too. 1 = the size compute_patch_unwrap() gave it, which is
|
||||
// already its true surface area in mm -- so scaling an island away from 1 deliberately makes its
|
||||
// already its true surface area in mm - so scaling an island away from 1 deliberately makes its
|
||||
// texel density differ from its neighbours'. See average_island_scales().
|
||||
float scale = 1.f;
|
||||
|
||||
@@ -168,7 +168,7 @@ struct TextureDisplacementLayer
|
||||
// The height value that means "don't move this vertex". The sampled height (0..1) has this
|
||||
// subtracted before being scaled by depth_mm, so with the default of 0 the surface only ever
|
||||
// moves *outwards* (the classic height-map convention), while 0.5 makes mid-grey neutral and
|
||||
// lets darker texels cut *into* the surface -- an engraved-and-embossed result from one map.
|
||||
// lets darker texels cut *into* the surface - an engraved-and-embossed result from one map.
|
||||
//
|
||||
// Cutting inward is not free: vertices move along their own normals, which converge inside a
|
||||
// concave corner and inside a thin wall, so a large depth_mm against a small feature really can
|
||||
@@ -197,7 +197,7 @@ struct TextureDisplacementLayer
|
||||
bool auto_connect_islands = true;
|
||||
|
||||
// When false, the texture is sampled once (clamped to its edge pixels outside [0, 1)) instead
|
||||
// of being repeated -- useful for a single decal-like placement rather than a repeating tile.
|
||||
// of being repeated - useful for a single decal-like placement rather than a repeating tile.
|
||||
bool tile_enabled = true;
|
||||
TextureTileMethod tile_method = TextureTileMethod::Repeat;
|
||||
|
||||
@@ -222,19 +222,19 @@ struct TextureDisplacementLayer
|
||||
|
||||
// Also ViewProjected, and takes precedence over the two axes above when set: an exact *projective*
|
||||
// map from a local-space position straight to a texture uv, written by the projection-frame overlay
|
||||
// (the semi-transparent window dragged over the 3D view -- its border becomes the uv unit square).
|
||||
// (the semi-transparent window dragged over the 3D view - its border becomes the uv unit square).
|
||||
//
|
||||
// Row-major 3x4, applied to the homogeneous point p~ = (x, y, z, 1):
|
||||
// uv = ( row0.p~ / row2.p~ , row1.p~ / row2.p~ )
|
||||
// The perspective divide is the whole point. view_project_right/up can only express an *affine*
|
||||
// projection, which matches an orthographic camera exactly but not a perspective one -- under
|
||||
// projection, which matches an orthographic camera exactly but not a perspective one - under
|
||||
// perspective the near end of a part projects larger than the far end, and no pair of axes
|
||||
// reproduces that. Folding the camera's full projection*view*model product into one matrix does.
|
||||
// Because a point behind the projector has row2.p~ <= 0 and no meaningful uv, sampling must check
|
||||
// the sign rather than divide blindly; see project_uv_projective().
|
||||
//
|
||||
// Note this map already includes placement, so the usual tiling/rotation/offset transform is NOT
|
||||
// applied on top of it -- the window's own position and size are the placement.
|
||||
// applied on top of it - the window's own position and size are the placement.
|
||||
bool view_project_projective = false;
|
||||
std::array<float, 12> view_project_matrix{};
|
||||
// Only used by TextureProjectionMethod::LSCM: hand placement of the unwrap's islands, indexed by
|
||||
@@ -243,7 +243,7 @@ struct TextureDisplacementLayer
|
||||
std::vector<TextureIsland> islands;
|
||||
|
||||
// Only used by TextureProjectionMethod::LSCM: persistent "join" groups, indexed by chart id. Charts
|
||||
// that share a group id move together as one in the UV editor -- this is what the explicit "Join"
|
||||
// that share a group id move together as one in the UV editor - this is what the explicit "Join"
|
||||
// command records (over and above placing the child next to its parent). An entry of -1, or an index
|
||||
// past the end of the vector, means the chart is its own singleton group (moves alone). Empty means
|
||||
// every chart is a singleton. Same chart-renumbering caveat as `islands`: a re-unwrap can reshuffle
|
||||
@@ -252,7 +252,7 @@ struct TextureDisplacementLayer
|
||||
|
||||
// Only used by TextureProjectionMethod::LSCM: manual per-vertex UV edits made in the UV editor's
|
||||
// Vertex/Edge select modes. Each pair is (mesh vertex index, its overriding raw-unwrap coordinate in
|
||||
// mm) -- the *raw* unwrap position, i.e. before the island transform, so the edited vertex still
|
||||
// mm) - the *raw* unwrap position, i.e. before the island transform, so the edited vertex still
|
||||
// moves and rotates with its island. In compute_lscm_uvs() this replaces the automatic unwrap
|
||||
// coordinate for that vertex; in the editor it edits the displayed geometry directly. Keyed in mesh-
|
||||
// vertex space like lscm_seam_edges (dropped on a topology change). The raw coordinate is only
|
||||
@@ -304,7 +304,7 @@ struct DecodedHeightTexture
|
||||
|
||||
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
|
||||
// Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false,
|
||||
// a uv outside [0, 1) samples as 0 -- the texture simply is not there, rather than its border
|
||||
// a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border
|
||||
// row/column being smeared outward forever (which is what clamping the coordinate would do, and
|
||||
// was a real reported bug). Callers rely on this to get a hard edge: it is how the projection
|
||||
// frame's border becomes the edge of the displacement.
|
||||
@@ -317,13 +317,13 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
|
||||
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
|
||||
// layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather
|
||||
// than only inline inside project_texture_displacement_uv()) so GUI code -- the on-canvas
|
||||
// "adjust texture placement" gizmo -- can map a dragged 3D point into the exact same 2D space
|
||||
// than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas
|
||||
// "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space
|
||||
// tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic.
|
||||
Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
|
||||
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
|
||||
// (in mm, dominant-axis planar, cylindrical, spherical, or CGAL LSCM output -- any of them, all
|
||||
// (in mm, dominant-axis planar, cylindrical, spherical, or CGAL LSCM output - any of them, all
|
||||
// share this same final step). Exposed separately so build_texture_displacement() can route CGAL
|
||||
// LSCM's per-patch UV solve through the same scale/rotate/offset controls as every other
|
||||
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
||||
@@ -331,7 +331,7 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer);
|
||||
|
||||
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
|
||||
// local-space point, writing the resulting texture uv. Returns false -- and leaves `uv` untouched --
|
||||
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
|
||||
// when the point lies behind the projector or on its plane (w <= 0), where there is no meaningful uv
|
||||
// and dividing would produce a mirrored or infinite coordinate. Callers treat that as "no height".
|
||||
bool project_uv_projective(const std::array<float, 12> &m, const Vec3f &position, Vec2f &uv);
|
||||
@@ -340,11 +340,11 @@ bool project_uv_projective(const std::array<float, 12> &m, const Vec3f &position
|
||||
// method, tiling scale, rotation, offset and tiling mode. Returns a height in [0, 1].
|
||||
//
|
||||
// This returns a *height* rather than a UV because TextureProjectionMethod::Triplanar is a blend
|
||||
// of three separate axis projections and therefore takes three texture samples per vertex -- there
|
||||
// of three separate axis projections and therefore takes three texture samples per vertex - there
|
||||
// is no single UV that represents it. The other methods do map to one UV internally.
|
||||
// - `normal` is this specific vertex's own normal; used only by Triplanar (for its blend weights).
|
||||
// - `patch_center`/`patch_axis` describe the painted patch as a whole (its centroid, and -- for
|
||||
// Cylindrical only -- the wrap axis); used only by the Cylindrical/Spherical methods.
|
||||
// - `patch_center`/`patch_axis` describe the painted patch as a whole (its centroid, and - for
|
||||
// Cylindrical only - the wrap axis); used only by the Cylindrical/Spherical methods.
|
||||
// - `lscm_uv`, when non-null, is this vertex's precomputed LSCM coordinate and takes precedence
|
||||
// over `layer.projection_method` (LSCM is a single per-patch solve, not a per-vertex formula,
|
||||
// so build_texture_displacement() computes it once up front and passes it in here).
|
||||
@@ -356,7 +356,7 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
const Vec2f *lscm_uv = nullptr);
|
||||
|
||||
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
|
||||
// coordinates -- the same measurements build_texture_displacement() uses to pick its dominant
|
||||
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
|
||||
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
|
||||
// wherever the layer is actually painted. Returns false (leaving the outputs untouched) if the
|
||||
// layer has nothing painted yet.
|
||||
@@ -365,7 +365,7 @@ bool compute_layer_paint_anchor(const indexed_triangle_set &b
|
||||
Vec3f &anchor_pos,
|
||||
Vec3f &anchor_normal);
|
||||
|
||||
// Extracts the currently painted patch from a volume's base mesh + stored facet data -- the same
|
||||
// Extracts the currently painted patch from a volume's base mesh + stored facet data - the same
|
||||
// extraction build_texture_displacement() and compute_layer_paint_anchor() each do internally via
|
||||
// TriangleSelector::get_facets_strict(ENFORCER). Returns an empty mesh if nothing is painted.
|
||||
// Exposed so GUI code (the LSCM "UV editor" preview pane) can get the same patch build_texture_
|
||||
@@ -388,7 +388,7 @@ struct PatchUnwrap
|
||||
std::vector<int> source_vertex; // unwrapped vertex -> index into patch.vertices
|
||||
std::vector<int> vertex_chart; // unwrapped vertex -> chart (island) id
|
||||
std::vector<stl_triangle_vertex_indices> indices; // patch triangles, re-indexed into `uvs`
|
||||
// Per chart, the centroid of its uvs -- the point a TextureIsland's rotation turns about.
|
||||
// Per chart, the centroid of its uvs - the point a TextureIsland's rotation turns about.
|
||||
std::vector<Vec2f> chart_centroid;
|
||||
// Edges belonging to exactly one triangle: the outline of each island. Indices into `uvs`. This
|
||||
// is what the UV editor draws highlighted, so the boundaries the seam angle cut are visible.
|
||||
@@ -424,13 +424,13 @@ bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector<TextureIs
|
||||
// Unwraps `patch` as described above. Charts that are flat (within a degree) are projected onto
|
||||
// their own tangent plane directly, which is both exact and far cheaper than a solve; only genuinely
|
||||
// curved charts go through CGAL's LSCM parameterizer (MeshBoolean::cgal::parameterize_lscm()). A
|
||||
// chart that LSCM cannot flatten at all (it is not a topological disk -- closed, or with a hole)
|
||||
// chart that LSCM cannot flatten at all (it is not a topological disk - closed, or with a hole)
|
||||
// falls back to that same tangent-plane projection.
|
||||
//
|
||||
// `padding_mm` is the gap the packing leaves between islands; negative means auto (see
|
||||
// TextureDisplacementLayer::island_padding_mm). `seam_edges` are extra edges to cut along regardless
|
||||
// of angle (manual/auto seams), in the patch's own vertex-index space (which is the mesh's, since the
|
||||
// patch carries the whole vertex array -- see get_facets_strict()).
|
||||
// patch carries the whole vertex array - see get_facets_strict()).
|
||||
//
|
||||
// Results are cached, keyed on the patch's geometry, the seam angle, the padding and the seam edges:
|
||||
// nothing else about a layer (depth, tiling, rotation, offset, texture, island placement) changes the
|
||||
@@ -438,8 +438,8 @@ bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector<TextureIs
|
||||
PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG,
|
||||
float padding_mm = -1.f, const std::vector<std::pair<int, int>> &seam_edges = {});
|
||||
|
||||
// One UV per patch vertex, for displacement. Displacement is inherently per-vertex -- a vertex has
|
||||
// exactly one position, so it can only be pushed out by one height -- which means a seam vertex has
|
||||
// One UV per patch vertex, for displacement. Displacement is inherently per-vertex - a vertex has
|
||||
// exactly one position, so it can only be pushed out by one height - which means a seam vertex has
|
||||
// to settle on a single one of its charts' UVs (the first, arbitrarily). That is not a compromise
|
||||
// in the result: the surface stays watertight either way, since neighbouring vertices each move
|
||||
// along their own normals and nothing depends on the UVs agreeing across the seam. It is only the
|
||||
@@ -457,7 +457,7 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
|
||||
// nothing is painted or no layer has a usable texture.
|
||||
//
|
||||
// **Topology-preserving**: the returned mesh has exactly `base_mesh`'s vertices and triangles, in
|
||||
// the same order -- only the positions of displaced vertices differ. Every layer's paint mask is
|
||||
// the same order - only the positions of displaced vertices differ. Every layer's paint mask is
|
||||
// evaluated against `base_mesh` directly, and each vertex accumulates a single signed displacement
|
||||
// (in mm) that all the layers covering it fold into, in slot order, via their TextureBlendMode.
|
||||
// The vertex is then moved once, along its base-mesh normal, by that accumulated total.
|
||||
@@ -469,8 +469,8 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
|
||||
// it routinely produced an empty bitstream, and the layer was then silently skipped. It is also
|
||||
// what forced the per-layer vertex duplication and the final its_compactify_vertices() pass. The
|
||||
// accumulate-then-displace formulation has neither problem, is substantially faster (no remap, no
|
||||
// welding, one pass over the mesh), and -- because the output keeps the input's exact vertex
|
||||
// indexing -- lets the GUI overlay a preview on the base mesh without any index translation.
|
||||
// welding, one pass over the mesh), and - because the output keeps the input's exact vertex
|
||||
// indexing - lets the GUI overlay a preview on the base mesh without any index translation.
|
||||
//
|
||||
// A vertex used by even one *unpainted* triangle of a layer's mask is that layer's boundary: its
|
||||
// displacement is pinned to zero, so the patch never tears away from the surrounding surface. Only
|
||||
@@ -498,7 +498,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
||||
// edge) until every edge is at or below max_edge_length_mm, or max_iterations passes have run,
|
||||
// whichever comes first (bounding the worst-case triangle-count explosion on a very fine target).
|
||||
//
|
||||
// This exists so a low-poly input model can still get fine-grained texture displacement detail --
|
||||
// This exists so a low-poly input model can still get fine-grained texture displacement detail -
|
||||
// build_texture_displacement() can only ever move existing vertices, so a patch with only a
|
||||
// handful of vertices to begin with cannot show much detail no matter the texture's resolution.
|
||||
//
|
||||
@@ -506,7 +506,7 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
|
||||
// mesh while leaving the rest untouched creates a classic T-junction/cracking problem where the
|
||||
// denser and sparser regions meet (the finer side has edge midpoints the coarser side doesn't
|
||||
// know about). Uniform, whole-mesh subdivision has no such seam and stays manifold, at the cost of
|
||||
// applying everywhere rather than just where texture detail is actually wanted -- meant to be run
|
||||
// applying everywhere rather than just where texture detail is actually wanted - meant to be run
|
||||
// once, deliberately, before painting (see the gizmo's "Subdivide model" button), not automatically
|
||||
// during baking.
|
||||
indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations = 6);
|
||||
|
||||
@@ -201,7 +201,7 @@ bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int
|
||||
// codebase has historically considered unreliable on some graphics cards.
|
||||
//
|
||||
// Each level is a 2x2 box filter of the level above it. Note this used to re-upload the
|
||||
// *level-0* buffer at every level instead, which does not downscale anything -- it just
|
||||
// *level-0* buffer at every level instead, which does not downscale anything - it just
|
||||
// reinterprets the image's first lod_w * lod_h texels as the whole smaller level, i.e. every
|
||||
// level below 0 held a crop of the top-left corner. It went unnoticed for as long as every
|
||||
// caller drew these textures at roughly their native size (where only level 0 is ever
|
||||
@@ -660,8 +660,8 @@ bool GLTexture::generate_texture_from_text(const std::string& text_str, wxFont&
|
||||
|
||||
found = false;
|
||||
src -= 3;
|
||||
for (int h = m_height; h > 0; --h) {
|
||||
for (int w = m_width; w > 0; --w) {
|
||||
for (int h = m_height; h > 0; -h) {
|
||||
for (int w = m_width; w > 0; -w) {
|
||||
if ((*src) != background.Red() && !found) {
|
||||
found = true;
|
||||
if (h < font.GetPointSize())
|
||||
@@ -742,7 +742,7 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
|
||||
static bool to_squared_power_of_two(const std::string& filename, int max_size_px, int& w, int& h)
|
||||
{
|
||||
auto is_power_of_two = [](int v) { return v != 0 && (v & (v - 1)) == 0; };
|
||||
auto upper_power_of_two = [](int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; };
|
||||
auto upper_power_of_two = [](int v) { v-; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; };
|
||||
|
||||
int new_w = std::max(w, h);
|
||||
if (!is_power_of_two(new_w))
|
||||
|
||||
@@ -637,10 +637,10 @@ void GLGizmoTextureDisplacement::render_seam_overlay()
|
||||
glsafe(::glEnable(GL_POLYGON_OFFSET_LINE));
|
||||
glsafe(::glPolygonOffset(-2.0f, -2.0f)); // pull further forward than the wireframe so seams read on top
|
||||
// A seam edge is geometrically the same line as a wireframe edge, so a mere polygon offset is a
|
||||
// fragile way to make the red seam beat the white wireframe -- drivers apply GL_POLYGON_OFFSET_LINE
|
||||
// fragile way to make the red seam beat the white wireframe - drivers apply GL_POLYGON_OFFSET_LINE
|
||||
// inconsistently, and the two lines then z-fight and the wireframe wins. When the wireframe is on,
|
||||
// or while actively marking, just draw the seams with depth testing off so they are unconditionally
|
||||
// on top -- being visible is the one thing this overlay has to guarantee.
|
||||
// on top - being visible is the one thing this overlay has to guarantee.
|
||||
const bool seams_on_top = m_wireframe_overlay || m_seam_edit_mode;
|
||||
if (seams_on_top)
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
@@ -812,7 +812,7 @@ void GLGizmoTextureDisplacement::compute_bump_active_vertices(const std::vector<
|
||||
m_bump_active_vertex.assign(mv->mesh().its.vertices.size(), 0);
|
||||
// Flag the base vertices of every chart being moved. For a group/multi move that is more than one
|
||||
// chart, but since such a move is a pure translation the shader applies the same delta to them all
|
||||
// (see on_island_edited) -- exactly the "joined islands move together" behaviour.
|
||||
// (see on_island_edited) - exactly the "joined islands move together" behaviour.
|
||||
for (size_t i = 0; i < u.uvs.size(); ++i) {
|
||||
if (i >= u.vertex_chart.size() ||
|
||||
std::find(charts.begin(), charts.end(), u.vertex_chart[i]) == charts.end())
|
||||
@@ -893,7 +893,7 @@ void GLGizmoTextureDisplacement::render_bump_preview_mesh()
|
||||
|
||||
// Reuses the layer-list panel's already-decoded, already-uploaded GPU thumbnail (smoothing-aware),
|
||||
// whose grayscale value lives in the R channel exactly as the shader samples it. Its width/height
|
||||
// are read straight off the texture -- decoding the PNG here every frame would re-run the smoothing
|
||||
// are read straight off the texture - decoding the PNG here every frame would re-run the smoothing
|
||||
// blur on every camera move, which is what tanked the frame rate at high smoothing.
|
||||
GLTexture *tex = get_layer_thumbnail(*layer);
|
||||
if (tex == nullptr || tex->get_width() <= 0 || tex->get_height() <= 0)
|
||||
@@ -1211,7 +1211,7 @@ void GLGizmoTextureDisplacement::update_uv_editor()
|
||||
|
||||
// A vertex/edge edit committing (or an undo reverting one) changes the per-vertex UV overrides
|
||||
// without going through the Unwrap button. Detect that and force a re-solve, so the pane's geometry
|
||||
// stays in step with what will bake -- the one exception to "only re-solve on Unwrap".
|
||||
// stays in step with what will bake - the one exception to "only re-solve on Unwrap".
|
||||
{
|
||||
size_t sig = 1469598103934665603ull; // FNV-1a seed
|
||||
const auto mix = [&sig](uint64_t x) { sig = (sig ^ x) * 1099511628211ull; };
|
||||
@@ -1236,7 +1236,7 @@ void GLGizmoTextureDisplacement::update_uv_editor()
|
||||
state.seam_edges = layer->lscm_seam_edges;
|
||||
|
||||
// The re-solve happens only when the user pressed "Unwrap" (m_uv_unwrap_pending). Every other call
|
||||
// into here -- a paint stroke ending, a slider release, the check mode changing -- must not pay for
|
||||
// into here - a paint stroke ending, a slider release, the check mode changing - must not pay for
|
||||
// a fresh LSCM solve; it just re-applies the cheap affine transforms over whatever unwrap already
|
||||
// exists. If the paint changed underneath but the user hasn't asked to re-unwrap, the pane keeps
|
||||
// showing the last unwrap on purpose (that is the whole point of making it an explicit action).
|
||||
@@ -1255,7 +1255,7 @@ void GLGizmoTextureDisplacement::update_uv_editor()
|
||||
m_uv_editor_unwrap = compute_patch_unwrap(patch, layer->lscm_seam_angle_deg, 0.f, layer->lscm_seam_edges);
|
||||
// Re-apply any stored per-vertex UV edits onto the fresh unwrap, so the pane shows exactly what
|
||||
// compute_lscm_uvs() will bake (which applies the same overrides). Keyed by mesh vertex, so every
|
||||
// unwrapped copy of that vertex gets it -- matching the bake's single-UV-per-vertex settle.
|
||||
// unwrapped copy of that vertex gets it - matching the bake's single-UV-per-vertex settle.
|
||||
if (!layer->lscm_uv_overrides.empty()) {
|
||||
std::map<int, Vec2f> ov;
|
||||
for (const auto &[mv2, uv] : layer->lscm_uv_overrides)
|
||||
@@ -1316,7 +1316,7 @@ void GLGizmoTextureDisplacement::update_uv_editor()
|
||||
|
||||
// Connected-net layout (on by default): a *fresh* unwrap is unfolded so adjacent charts sit
|
||||
// edge-to-edge (cube -> a net), rather than as separately packed squares. Only when the user pressed
|
||||
// Unwrap (m_uv_apply_connected_net) -- a re-segmentation renumbers charts anyway, so any hand
|
||||
// Unwrap (m_uv_apply_connected_net) - a re-segmentation renumbers charts anyway, so any hand
|
||||
// placement from before is already meaningless. A *refresh* re-solve (a committed vertex edit, or an
|
||||
// undo) must NOT relayout, or it would throw away every island placement on every vertex edit.
|
||||
if (unwrap_changed && m_uv_apply_connected_net && layer->auto_connect_islands) {
|
||||
@@ -1459,7 +1459,7 @@ void GLGizmoTextureDisplacement::on_uv_command(int cmd)
|
||||
return;
|
||||
}
|
||||
// Join to whichever neighbouring island (one it shares an edge with) is currently placed
|
||||
// nearest -- i.e. the one it was dragged up against.
|
||||
// nearest - i.e. the one it was dragged up against.
|
||||
const Eigen::Matrix<float, 2, 3> sel_m = island_transform_matrix(chart, m_uv_editor_unwrap, layer->islands);
|
||||
const Vec2f sel_c = sel_m.block<2, 2>(0, 0) * m_uv_editor_unwrap.chart_centroid[size_t(chart)] + sel_m.col(2);
|
||||
int best_parent = -1;
|
||||
@@ -1770,7 +1770,7 @@ void GLGizmoTextureDisplacement::on_island_edited(int island, const Vec2f &offse
|
||||
// just the primary for a rotate/scale.
|
||||
m_island_move_set = is_move ? build_island_move_set(*layer, island) : std::vector<int>{ island };
|
||||
// Set up the GPU drag: flag the moved islands' vertices and bake the mesh once (via the dirty
|
||||
// flag). From then on the drag is a uniform update, no rebuild -- see render_bump_preview_mesh().
|
||||
// flag). From then on the drag is a uniform update, no rebuild - see render_bump_preview_mesh().
|
||||
m_bump_active_chart = island;
|
||||
compute_bump_active_vertices(m_island_move_set);
|
||||
m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
|
||||
@@ -1791,7 +1791,7 @@ void GLGizmoTextureDisplacement::on_island_edited(int island, const Vec2f &offse
|
||||
if (c == island) {
|
||||
target.rotation_deg += rotation_delta;
|
||||
// Guarded: a scale that reaches zero is unrecoverable (every subsequent factor multiplies it)
|
||||
// and would collapse the island to a point -- exactly the failure this feature hit once already.
|
||||
// and would collapse the island to a point - exactly the failure this feature hit once already.
|
||||
target.scale = std::clamp(target.scale * scale_factor, 0.001f, 1000.f);
|
||||
}
|
||||
}
|
||||
@@ -1811,7 +1811,7 @@ void GLGizmoTextureDisplacement::on_island_edited(int island, const Vec2f &offse
|
||||
// interactive on a patch with a million triangles.
|
||||
uv_canvas->set_island_transforms(xf);
|
||||
}
|
||||
// Move the island on the model live through the shader's island_delta uniform -- no mesh
|
||||
// Move the island on the model live through the shader's island_delta uniform - no mesh
|
||||
// rebuild. delta = F_current * F_baked^-1 in final-uv space (the bump mesh bakes F_baked; the
|
||||
// shader applies delta to the flagged island's uv). The one rebuild that bakes the flags is
|
||||
// scheduled at drag start above and consumed once per frame by render_painter_gizmo().
|
||||
@@ -1893,7 +1893,7 @@ Vec3f GLGizmoTextureDisplacement::adjust_handle_center(const TextureDisplacement
|
||||
{
|
||||
// apply_uv_transform() maps a planar mm coordinate p to uv = R(p / tiling_scale) + offset, and
|
||||
// on_mouse_adjust_texture() drives offset by offset = offset_start - R(delta / tiling_scale).
|
||||
// Inverting that, the handle's displacement from the anchor is - R^-1(offset) * tiling_scale --
|
||||
// Inverting that, the handle's displacement from the anchor is - R^-1(offset) * tiling_scale -
|
||||
// which, substituted into the drag equation, moves the handle by exactly `delta`. So the handle
|
||||
// follows the cursor precisely, and is back on the anchor exactly when offset is zero.
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
@@ -2887,7 +2887,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// them fit a narrow window, which it does by lowering the same toolbar_icon_scale() read here.
|
||||
const float icon_btn_sz = GLToolbar::Default_Icons_Size * wxGetApp().toolbar_icon_scale() * m_parent.get_scale();
|
||||
// The icon SVGs carry their own border, so the ImGui button's own frame border and idle fill are
|
||||
// suppressed here (FrameBorderSize 0 + transparent ImGuiCol_Button) to avoid a doubled border -- the
|
||||
// suppressed here (FrameBorderSize 0 + transparent ImGuiCol_Button) to avoid a doubled border - the
|
||||
// hover/active fill is left in place for feedback. Applied only around these gizmo icon buttons.
|
||||
const auto push_borderless_icon_style = []() {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
|
||||
@@ -2950,7 +2950,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
// Selection mode: which of TriangleSelector's existing click/brush mechanisms drives painting.
|
||||
// "Face" and "Connected area" reuse the exact same underlying selection machinery every other
|
||||
// paint gizmo already has (single-facet click, and angle-limited flood fill respectively) --
|
||||
// paint gizmo already has (single-facet click, and angle-limited flood fill respectively) -
|
||||
// just exposed here as an alternative to brushing, one triangle/region at a time.
|
||||
m_imgui->text(_L("Selection mode"));
|
||||
const bool is_brush_mode = m_tool_type == ToolType::BRUSH && m_cursor_type != TriangleSelector::CursorType::POINTER;
|
||||
@@ -3059,7 +3059,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// Add-layer affordance as an icon beside the heading. It is deliberately *not* right-aligned
|
||||
// against the window edge: this panel uses ImGuiWindowFlags_AlwaysAutoResize, and positioning
|
||||
// an item at GetWindowContentRegionMax().x - w feeds the window's own width back into its
|
||||
// auto-fit, growing it by one item-spacing every frame -- which, with the panel docked and
|
||||
// auto-fit, growing it by one item-spacing every frame - which, with the panel docked and
|
||||
// anchored by its right edge, walked it left off-screen on hover. A plain SameLine can't do that.
|
||||
const unsigned int add_icon = tool_icon_id();
|
||||
const float sz = m_imgui->scaled(1.3f);
|
||||
@@ -3298,8 +3298,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
if (is_active) {
|
||||
// Explicit unwrap (#: "Add unwrap button so it does not recompute on every
|
||||
// change"). The LSCM solve runs only when this is pressed -- painting, the seam
|
||||
// angle slider and seam marking no longer trigger it -- and the pane opens right
|
||||
// change"). The LSCM solve runs only when this is pressed - painting, the seam
|
||||
// angle slider and seam marking no longer trigger it - and the pane opens right
|
||||
// afterwards. Re-press it to fold in any edits made since.
|
||||
if (m_imgui->button(_u8L("Unwrap"))) {
|
||||
m_uv_unwrap_pending = true;
|
||||
@@ -3309,7 +3309,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Flatten the painted area into UV islands and open the UV editor. The "
|
||||
"unwrap is computed only when you press this, not on every edit -- so "
|
||||
"unwrap is computed only when you press this, not on every edit - so "
|
||||
"paint, change the seam angle or mark seams first, then press Unwrap to "
|
||||
"see the result. Islands can then be moved, rotated and scaled."),
|
||||
m_imgui->scaled(20.f));
|
||||
|
||||
@@ -22,7 +22,7 @@ class TextureProjectorFrame;
|
||||
// libslic3r/TextureDisplacement.hpp) to painted areas of a model, and can bake the result into
|
||||
// real mesh geometry. See the project plan for the overall architecture; in short:
|
||||
// - each layer owns its own independent paint mask (ModelVolume::texture_displacement_facets),
|
||||
// reusing the same TriangleSelector/FacetsAnnotation machinery as every other paint gizmo --
|
||||
// reusing the same TriangleSelector/FacetsAnnotation machinery as every other paint gizmo -
|
||||
// only one layer is "active" (paintable) at a time, selected in the panel below;
|
||||
// - "Bake" runs build_texture_displacement() in a background job and commits the result exactly
|
||||
// like the Emboss/SVG "project on surface" gizmo does.
|
||||
@@ -69,7 +69,7 @@ private:
|
||||
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
|
||||
void bake();
|
||||
|
||||
// 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.
|
||||
void select_whole_model();
|
||||
|
||||
@@ -124,7 +124,7 @@ private:
|
||||
|
||||
// A texture from the picker's library (see slic3r/GUI/TextureLibrary.hpp), read and uploaded
|
||||
// once and then kept for the gizmo's lifetime. The decoded bytes are held alongside the GPU
|
||||
// thumbnail so that picking the texture can hand the layer this very same image_data buffer --
|
||||
// thumbnail so that picking the texture can hand the layer this very same image_data buffer -
|
||||
// which both avoids re-reading the file and lets decode_height_texture()'s own cache (keyed by
|
||||
// exactly this pointer) hit immediately on the first bake/preview.
|
||||
struct LibraryTexture
|
||||
@@ -149,13 +149,13 @@ private:
|
||||
|
||||
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
||||
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||
// (a "pan" -- drag anywhere on it for free 2D movement), plus two arrows along the patch's
|
||||
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
|
||||
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
|
||||
// the centroid/average-normal of the active layer's current paint patch (see
|
||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
|
||||
//
|
||||
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
|
||||
// the texture should appear to move as the handle is dragged -- it could not be visually
|
||||
// the texture should appear to move as the handle is dragged - it could not be visually
|
||||
// confirmed while writing it (no way to render/see pixels in this environment), so it may
|
||||
// need a one-line sign flip once actually tested.
|
||||
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
|
||||
@@ -170,13 +170,13 @@ private:
|
||||
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
|
||||
|
||||
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
|
||||
// Deliberately *fixed* -- independent of the layer's offset -- so that moving the handle cannot
|
||||
// Deliberately *fixed* - independent of the layer's offset - so that moving the handle cannot
|
||||
// move the plane the handle's own motion is derived from, which would be a feedback loop.
|
||||
Vec3f adjust_plane_point() const;
|
||||
|
||||
// Where the handle is actually drawn, in mesh-local coordinates. This is NOT just the patch's
|
||||
// centroid: the handle *represents the texture's placement*, so it has to travel as `offset`
|
||||
// changes. Pinning it to the centroid is why dragging it looked broken -- the texture slid but
|
||||
// changes. Pinning it to the centroid is why dragging it looked broken - the texture slid but
|
||||
// the handle stayed put. Undoing apply_uv_transform()'s scale and rotation turns the layer's
|
||||
// offset back into a displacement in mm within the patch's tangent plane, which is what gets
|
||||
// added to the anchor here. That is exactly consistent with the drag arithmetic in
|
||||
@@ -190,7 +190,7 @@ private:
|
||||
|
||||
// Recomputes m_preview_glmodel from the volume's current (unbaked) paint state, using the same
|
||||
// build_texture_displacement() algorithm as Bake. Called whenever the paint mask changes
|
||||
// (stroke end, layer switch, undo/redo reload, post-bake refresh) rather than every frame --
|
||||
// (stroke end, layer switch, undo/redo reload, post-bake refresh) rather than every frame -
|
||||
// this is real mesh work (PNG sampling, vertex welding), not something to redo per paint stroke
|
||||
// drag sample or idle repaint. With several painted layers this can be slow, so the actual
|
||||
// computation runs in a background TextureDisplacementPreviewJob; this function only queues
|
||||
@@ -201,9 +201,9 @@ private:
|
||||
// Alternate, GPU-only preview: perturbs shading normals from the active layer's height texture
|
||||
// (a classic bump map) instead of actually moving vertices, using the
|
||||
// resources/shaders/*/texture_displacement_bump.* shader. Faster than the true-displacement
|
||||
// preview (no CPU meshing at all -- just a per-vertex paint-weight buffer built at the same
|
||||
// preview (no CPU meshing at all - just a per-vertex paint-weight buffer built at the same
|
||||
// cadence as rebuild_preview()) but only shows the *active* layer, and any bump is a shading
|
||||
// illusion, not real geometry -- "Bake" always produces the true, exact result either way.
|
||||
// illusion, not real geometry - "Bake" always produces the true, exact result either way.
|
||||
void rebuild_bump_preview_mesh();
|
||||
void render_bump_preview_mesh();
|
||||
|
||||
@@ -216,7 +216,7 @@ private:
|
||||
|
||||
// Applies one island edit reported by the UV editor's drag/rotate gestures to the active layer.
|
||||
// Deltas are incremental (see UVEditorCanvas::IslandEditFn); `finished` ends the gesture, which
|
||||
// is when -- and only when -- the 3D preview is rebuilt, since doing that per mouse-move would
|
||||
// is when - and only when - the 3D preview is rebuilt, since doing that per mouse-move would
|
||||
// queue a mesh recompute for every pixel of a drag.
|
||||
void on_island_edited(int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished);
|
||||
// Applies a committed vertex/edge edit from the UV editor's Vertex/Edge modes: each entry is an
|
||||
@@ -227,7 +227,7 @@ private:
|
||||
// UV-editor sub-element select mode, mirrored into the canvas: 0 = Island, 1 = Vertex, 2 = Edge.
|
||||
int m_uv_select_mode = 0;
|
||||
// One affine per island (columns: x basis, y basis, translation), mapping the unwrap's raw mm
|
||||
// coordinates to texture UVs -- the same type as UVEditorCanvas::IslandTransform, spelled out
|
||||
// coordinates to texture UVs - the same type as UVEditorCanvas::IslandTransform, spelled out
|
||||
// here so this header needn't drag in wxGLCanvas/glad. Cheap to recompute (it is per *island*,
|
||||
// not per vertex), which is what lets an island drag update the pane without re-uploading a
|
||||
// single vertex.
|
||||
@@ -245,7 +245,7 @@ private:
|
||||
void capture_view_projection(TextureDisplacementLayer &layer);
|
||||
|
||||
// Manual seam marking (#9): a mode where clicking the model toggles the nearest mesh edge in the
|
||||
// active layer's lscm_seam_edges, so the unwrap can be cut exactly where the user wants -- the
|
||||
// active layer's lscm_seam_edges, so the unwrap can be cut exactly where the user wants - the
|
||||
// Blender "mark seam" workflow. Painting is suppressed while it is on.
|
||||
bool m_seam_edit_mode = false;
|
||||
GLModel m_seam_glmodel; // the current seam edges, highlighted on the mesh
|
||||
@@ -255,7 +255,7 @@ private:
|
||||
void render_seam_overlay();
|
||||
// The mesh edge nearest the mouse, in the volume's own vertex indices, or {-1,-1} if the ray misses.
|
||||
// Factored out of toggle_seam_at() so the same pick can drive a live hover highlight (below) that
|
||||
// shows which edge a click would toggle -- the "I don't know how it works" feedback the user hit.
|
||||
// shows which edge a click would toggle - the "I don't know how it works" feedback the user hit.
|
||||
std::pair<int, int> seam_edge_at(const Vec2d &mouse_pos) const;
|
||||
std::pair<int, int> m_seam_hover_edge{ -1, -1 };
|
||||
// The vertex a click would pick in shortest-path mode, so the target is visible on hover the same
|
||||
@@ -280,7 +280,7 @@ private:
|
||||
|
||||
// Which of the up to TEXTURE_DISPLACEMENT_MAX_LAYERS paint masks the brush currently writes
|
||||
// into. Always a valid slot index (0 by default) so the base class's per-volume selector
|
||||
// machinery always has something to work with, even before any texture has been added --
|
||||
// machinery always has something to work with, even before any texture has been added -
|
||||
// painting into a slot with no texture assigned is harmless, it just has no visible/bake
|
||||
// effect until a texture is added to that slot.
|
||||
int m_active_layer_slot = 0;
|
||||
@@ -323,13 +323,13 @@ private:
|
||||
// back to the standard paint-mask overlay like every other painting gizmo.
|
||||
GLModel m_preview_glmodel;
|
||||
// Set while a layer parameter slider has changed since the last rebuild_preview() call but the
|
||||
// mouse button driving the drag hasn't been released yet -- see on_render_input_window().
|
||||
// mouse button driving the drag hasn't been released yet - see on_render_input_window().
|
||||
bool m_preview_params_dirty = false;
|
||||
|
||||
// See rebuild_bump_preview_mesh()/render_bump_preview_mesh().
|
||||
bool m_use_bump_preview = false;
|
||||
// Set from the UV editor's per-move island edits instead of rebuilding the (potentially large) bump
|
||||
// mesh synchronously inside that mouse handler -- doing the rebuild there stalled both the UV pane
|
||||
// mesh synchronously inside that mouse handler - doing the rebuild there stalled both the UV pane
|
||||
// and the 3D view. The rebuild is instead coalesced to once per 3D frame (render_painter_gizmo).
|
||||
bool m_bump_preview_dirty = false;
|
||||
GLModel m_bump_preview_glmodel;
|
||||
@@ -339,7 +339,7 @@ private:
|
||||
|
||||
// GPU island drag: while an island is dragged in the UV editor, the bump mesh is baked once (with
|
||||
// the dragged island's vertices flagged, v_normal.y = 1) and then moved purely through the shader's
|
||||
// island_delta uniform -- one uniform update per mouse move, no rebuild -- so it tracks the cursor
|
||||
// island_delta uniform - one uniform update per mouse move, no rebuild - so it tracks the cursor
|
||||
// as smoothly as Adjust placement. m_bump_active_chart is the dragged island (or -1);
|
||||
// m_bump_active_vertex flags its base vertices; m_bump_baked_active_xf is that island's placement
|
||||
// baked into the current mesh, against which the live delta is measured; m_bump_island_delta is the
|
||||
@@ -363,7 +363,7 @@ private:
|
||||
static int island_group_of(const std::vector<int> &groups, int c);
|
||||
// Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first).
|
||||
static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count);
|
||||
// Final per-vertex texture uv for the projections the shader can't reconstruct itself -- LSCM (an
|
||||
// Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an
|
||||
// unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base
|
||||
// vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which
|
||||
// the shader projects on its own. Shared by the bump preview and the UV-check overlay.
|
||||
@@ -381,7 +381,7 @@ private:
|
||||
void render_uvcheck_mesh();
|
||||
|
||||
// The UV editor pane is opened only on the user's explicit request (this toggle in the panel),
|
||||
// never automatically just because a patch exists -- auto-popping it whenever there was "a
|
||||
// never automatically just because a patch exists - auto-popping it whenever there was "a
|
||||
// selection to process" is exactly what the user asked to stop. update_uv_editor() keeps the pane
|
||||
// hidden unless this is set. Reset on gizmo shutdown so reopening the gizmo doesn't reopen the pane.
|
||||
bool m_show_uv_editor = false;
|
||||
@@ -395,7 +395,7 @@ private:
|
||||
// leave island placements untouched.
|
||||
bool m_uv_apply_connected_net = false;
|
||||
// Signature of the per-vertex UV overrides last reflected in the pane. When it changes without the
|
||||
// user pressing Unwrap -- a vertex/edge edit committing, or an undo/redo reverting one -- the pane
|
||||
// user pressing Unwrap - a vertex/edge edit committing, or an undo/redo reverting one - the pane
|
||||
// is re-solved so its geometry follows, even though a plain edit otherwise never re-solves (#Feat2).
|
||||
size_t m_uv_overrides_sig = 0;
|
||||
// What the UV pane's background currently holds, so update_uv_editor() only re-uploads it when the
|
||||
@@ -437,7 +437,7 @@ private:
|
||||
std::map<std::string, LibraryTexture> m_library_textures;
|
||||
|
||||
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
|
||||
// every stroke end and every slider release -- but depth/tiling/rotation/offset/blend change
|
||||
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
|
||||
// none of this, so re-extracting the patch and re-solving on those edits would be pure waste.
|
||||
// Held as the real values rather than a hash: TriangleSplittingData has an exact operator==, so
|
||||
// there is no reason to accept a hash's (however unlikely) chance of showing a stale unwrap.
|
||||
@@ -460,7 +460,7 @@ private:
|
||||
};
|
||||
UVEditorState m_uv_editor_state;
|
||||
// Bounds of the UVs last handed to the pane, purely so the panel can show where the unwrap
|
||||
// actually landed -- it is packed in mm and then divided by the tile size, so it is easy for it
|
||||
// actually landed - it is packed in mm and then divided by the tile size, so it is easy for it
|
||||
// to end up far outside the texture's first tile without any of that being visible.
|
||||
Vec2f m_uv_editor_bbox_min = Vec2f::Zero();
|
||||
Vec2f m_uv_editor_bbox_max = Vec2f::Zero();
|
||||
@@ -505,7 +505,7 @@ private:
|
||||
|
||||
// Icons for the panel's selection-mode and view-mode button rows. Loaded through IconManager with
|
||||
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
|
||||
// the theme's normal (grey) foreground colour and an active one shows it in its original colours --
|
||||
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
|
||||
// matching the toolbar's selected/unselected look. Uploaded once on first panel render.
|
||||
IconManager m_panel_icons;
|
||||
std::map<std::string, IconManager::Icons> m_panel_icon_map; // file name -> [normal, colour, disabled]
|
||||
|
||||
@@ -1902,7 +1902,7 @@ void ImGuiWrapper::search_list(const ImVec2& size_, bool (*items_getter)(int, co
|
||||
scroll_up();
|
||||
else {
|
||||
if (hovered_id > 0)
|
||||
--hovered_id;
|
||||
-hovered_id;
|
||||
scroll_y(hovered_id);
|
||||
}
|
||||
});
|
||||
@@ -2293,7 +2293,7 @@ std::string ImGuiWrapper::trunc(const std::string &text,
|
||||
} else {
|
||||
// decrease letter count
|
||||
while (count_letter > 1) {
|
||||
--count_letter;
|
||||
-count_letter;
|
||||
result_text = text_.substr(0, count_letter);
|
||||
text_width = calc_text_size(result_text).x;
|
||||
if (text_width < allowed_width) break;
|
||||
@@ -2555,7 +2555,7 @@ void ImGuiWrapper::push_toolbar_style(const float scale)
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 0.00f)); // 11
|
||||
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, COL_GREEN_LIGHT); // 12
|
||||
// The checkbox/radio frame behind this is drawn fully transparent (see FrameBg above,
|
||||
// alpha 0), showing the light window background through it -- a white check mark there is
|
||||
// alpha 0), showing the light window background through it - a white check mark there is
|
||||
// invisible. Dark mode doesn't have this problem (its window background is dark), so only
|
||||
// this branch needs a check mark color with real contrast against a light background.
|
||||
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.f, 156 / 255.f, 136 / 255.f, 1.00f));//13
|
||||
|
||||
@@ -22,8 +22,8 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
{
|
||||
ctl.update_status(0, _u8L("Baking texture displacement"));
|
||||
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state --
|
||||
// never the live Model -- so this is safe to run concurrently with the UI thread.
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
||||
// never the live Model - so this is safe to run concurrently with the UI thread.
|
||||
m_result = TriangleMesh(build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ struct TextureDisplacementBakeInput
|
||||
};
|
||||
|
||||
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
|
||||
// then commits the result on the main thread -- mirrors EmbossJob's UpdateJob/update_volume()
|
||||
// then commits the result on the main thread - mirrors EmbossJob's UpdateJob/update_volume()
|
||||
// bake-and-commit pattern (see EmbossJob.cpp).
|
||||
class TextureDisplacementBakeJob : public Job
|
||||
{
|
||||
|
||||
@@ -14,8 +14,8 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
{
|
||||
ctl.update_status(0, _u8L("Computing texture displacement preview"));
|
||||
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state --
|
||||
// never the live Model -- so this is safe to run concurrently with the UI thread.
|
||||
// Only ever touches m_input (captured by value before this job was queued) and local state -
|
||||
// never the live Model - so this is safe to run concurrently with the UI thread.
|
||||
m_result = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Everything process() needs, captured by value on the main thread when the job is queued --
|
||||
// Everything process() needs, captured by value on the main thread when the job is queued -
|
||||
// mirrors TextureDisplacementBakeInput, but a preview never writes back to the Model.
|
||||
struct TextureDisplacementPreviewInput
|
||||
{
|
||||
@@ -24,7 +24,7 @@ struct TextureDisplacementPreviewInput
|
||||
// Computes the true (unbaked) displaced-mesh preview in the background. With several painted
|
||||
// layers this is real, non-trivial CPU work (PNG sampling, per-layer vertex welding), which used
|
||||
// to run synchronously on every paint stroke and parameter tweak and made editing feel slow with
|
||||
// more than one or two layers. Unlike Bake, this never touches the live Model -- a preview is
|
||||
// more than one or two layers. Unlike Bake, this never touches the live Model - a preview is
|
||||
// purely informational, there is nothing to commit.
|
||||
class TextureDisplacementPreviewJob : public Job
|
||||
{
|
||||
|
||||
@@ -695,7 +695,7 @@ void Sidebar::priv::flush_printer_sync(bool restart)
|
||||
}
|
||||
//btn_sync_printer->SetBackgroundColorNormal((*counter_sync_printer & 1) ? "#F8F8F8" :"#009688");
|
||||
m_printer_bbl_sync->SetBitmap_((*counter_sync_printer & 1) ? "printer_sync_not" : "printer_sync_ok");
|
||||
if (--*counter_sync_printer <= 0)
|
||||
if (-*counter_sync_printer <= 0)
|
||||
timer_sync_printer->Stop();
|
||||
}
|
||||
|
||||
@@ -1124,7 +1124,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
||||
btn_up->SetBackgroundColour(*wxWHITE);
|
||||
btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) {
|
||||
if (page_cur > 0)
|
||||
--page_cur;
|
||||
-page_cur;
|
||||
update_ams();
|
||||
});
|
||||
btn_up->Hide();
|
||||
@@ -1189,7 +1189,7 @@ void ExtruderGroup::update_ams()
|
||||
ams[index]->Refresh();
|
||||
ams[index]->Open();
|
||||
}
|
||||
for (size_t i = i1; i < ams_n1 && left > 0; ++i, ++index, --left) {
|
||||
for (size_t i = i1; i < ams_n1 && left > 0; ++i, ++index, -left) {
|
||||
ams[index]->Update(i < ams_1.size() ? ams_1[i] : info1);
|
||||
ams[index]->Refresh();
|
||||
ams[index]->Open();
|
||||
@@ -3781,7 +3781,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
|
||||
|
||||
for (size_t i = 0; i < merge_info.merges.size(); i++) {
|
||||
auto& cur = merge_info.merges[i];
|
||||
for (int j = cur.size() -1; j >= 1 ; j--) {
|
||||
for (int j = cur.size() -1; j >= 1 ; j-) {
|
||||
auto last_index = cur[j];
|
||||
change_filament(last_index, cur[0]);
|
||||
cur.erase(cur.begin() + j);
|
||||
@@ -4471,7 +4471,7 @@ struct Plater::priv
|
||||
AssembleView* assemble_view { nullptr };
|
||||
// Docked/resizable 2D pane showing GLGizmoTextureDisplacement's LSCM unwrap of a painted
|
||||
// patch; a sibling AUI pane alongside "sidebar"/"main", not part of the view3D/preview/
|
||||
// assemble_view sizer -- see its registration below and Plater::get_uv_editor_canvas(). The
|
||||
// assemble_view sizer - see its registration below and Plater::get_uv_editor_canvas(). The
|
||||
// pane hosts the panel (toolbar + canvas + status line); uv_editor_canvas is its inner canvas,
|
||||
// cached so the gizmo can reach it directly.
|
||||
UVEditorPanel* uv_editor_panel { nullptr };
|
||||
@@ -4697,7 +4697,7 @@ struct Plater::priv
|
||||
bool up_to_date(bool saved, bool backup);
|
||||
|
||||
void suppress_snapshots() { m_prevent_snapshots++; }
|
||||
void allow_snapshots() { m_prevent_snapshots--; }
|
||||
void allow_snapshots() { m_prevent_snapshots-; }
|
||||
// BBS: single snapshot
|
||||
void single_snapshots_enter(SingleSnapshot *single)
|
||||
{
|
||||
@@ -5141,7 +5141,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
.BottomDockable(false)
|
||||
.BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit())));
|
||||
|
||||
// UV editor pane for GLGizmoTextureDisplacement's LSCM unwrap preview -- a resizable/dockable
|
||||
// UV editor pane for GLGizmoTextureDisplacement's LSCM unwrap preview - a resizable/dockable
|
||||
// sibling of "sidebar"/"main" like everything else registered on this same AUI manager, not a
|
||||
// change to the view3D/preview/assemble_view sizer above. Hidden by default: only relevant
|
||||
// while that gizmo is active with a layer using the "Unwrap (LSCM)" projection method (see
|
||||
@@ -11808,7 +11808,7 @@ void Plater::priv::undo()
|
||||
const std::vector<UndoRedo::Snapshot> &snapshots = this->undo_redo_stack().snapshots();
|
||||
auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time()));
|
||||
// BBS: undo-redo until modify record
|
||||
while (--it_current != snapshots.begin() && !snapshot_modifies_project(*it_current));
|
||||
while (-it_current != snapshots.begin() && !snapshot_modifies_project(*it_current));
|
||||
if (it_current == snapshots.begin()) return;
|
||||
if (get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView) {
|
||||
if (it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::GizmoAction &&
|
||||
@@ -11828,7 +11828,7 @@ void Plater::priv::redo()
|
||||
while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++));
|
||||
if (it_current != snapshots.end()) {
|
||||
while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++));
|
||||
this->undo_redo_to(--it_current);
|
||||
this->undo_redo_to(-it_current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13085,7 +13085,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i
|
||||
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
|
||||
|
||||
/// --- scale ---
|
||||
/// -- scale --
|
||||
// model is created for a 0.4 nozzle, scale z with nozzle size.
|
||||
const ConfigOptionFloats* nozzle_diameter_config = printer_config->option<ConfigOptionFloats>("nozzle_diameter");
|
||||
std::vector<int> extruder_types = printer_config->option<ConfigOptionEnumsGeneric>("extruder_type")->values;
|
||||
@@ -16706,7 +16706,7 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r
|
||||
|
||||
for (auto& item : item->second.gcodes) {
|
||||
if (item.type == CustomGCode::Type::ToolChange && item.extruder > filament_id)
|
||||
item.extruder--;
|
||||
item.extruder-;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17191,7 +17191,7 @@ void Plater::show_uv_editor(bool show)
|
||||
if (!pane.IsOk() || pane.IsShown() == show)
|
||||
return;
|
||||
|
||||
// Deferred, because GLGizmoTextureDisplacement calls this from its ImGui panel -- that is, from
|
||||
// Deferred, because GLGizmoTextureDisplacement calls this from its ImGui panel - that is, from
|
||||
// the middle of the 3D canvas's GL frame. Showing an AUI pane re-lays out the window and
|
||||
// delivers the resulting size/paint events synchronously, and the UV canvas painting itself
|
||||
// makes its own surface current in the app's *shared* GL context, which mid-frame is the one
|
||||
|
||||
@@ -157,7 +157,7 @@ std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &
|
||||
return std::nullopt;
|
||||
|
||||
// Never overwrite an existing texture (the user's or, if they picked the same name twice, their
|
||||
// own earlier import) -- uniquify instead.
|
||||
// own earlier import) - uniquify instead.
|
||||
const std::string stem = boost::filesystem::path(source_path).stem().string();
|
||||
boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png");
|
||||
for (int i = 2; boost::filesystem::exists(dest); ++i)
|
||||
|
||||
@@ -40,8 +40,8 @@ std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &
|
||||
|
||||
// Encoded bytes of `path`, ready to hand to TextureDisplacementLayer::image_data. Files already in
|
||||
// the supported 8-bit grayscale PNG form (everything in the two library folders, by construction)
|
||||
// are passed through verbatim; anything else -- e.g. a colour PNG the user copied into the folder
|
||||
// by hand -- is converted on the fly, so a valid image never silently produces a blank layer.
|
||||
// are passed through verbatim; anything else - e.g. a colour PNG the user copied into the folder
|
||||
// by hand - is converted on the fly, so a valid image never silently produces a blank layer.
|
||||
// Returns nullptr with `error` set if the file cannot be read or decoded at all.
|
||||
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error);
|
||||
|
||||
|
||||
@@ -851,7 +851,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
|
||||
m_gesture_last_angle = angle;
|
||||
|
||||
// With Shift, quantise to *global* 15-degree marks (0/15/30...), i.e. snap the island's
|
||||
// absolute on-screen orientation, not 15 degrees relative to wherever it started (#10) --
|
||||
// absolute on-screen orientation, not 15 degrees relative to wherever it started (#10) -
|
||||
// snapping the target rather than each delta is what keeps it from juddering on a step.
|
||||
constexpr float STEP = 15.f;
|
||||
const float absolute = m_rot_base_deg + m_rot_raw_deg;
|
||||
@@ -1333,7 +1333,7 @@ void UVEditorCanvas::render()
|
||||
}
|
||||
|
||||
// Add/remove hint next to the cursor in Vertex/Edge mode: a green '+' when a click will add to the
|
||||
// selection (plain or Shift), a red '-' when Ctrl is held and a click will remove one -- the UV-side
|
||||
// selection (plain or Shift), a red '-' when Ctrl is held and a click will remove one - the UV-side
|
||||
// twin of the 3D paint cursor's own sign. Rebuilt each frame at the pointer, sized in pixels.
|
||||
if (m_select_mode != SelectMode::Island && m_cursor_inside) {
|
||||
const float uv_per_px = 2.f * m_zoom / float(std::max(1, std::min(size.GetWidth(), size.GetHeight())));
|
||||
@@ -1375,9 +1375,9 @@ void UVEditorCanvas::render()
|
||||
update_status();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// -----------------------------------------------
|
||||
// UVEditorPanel
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// -----------------------------------------------
|
||||
|
||||
namespace {
|
||||
enum : int {
|
||||
@@ -1394,8 +1394,8 @@ enum : int {
|
||||
UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
{
|
||||
// Icon + label buttons: each has its own dedicated SVG (see the map below), with the label kept
|
||||
// alongside it. A missing SVG simply leaves the button showing only its label -- never bitmap-less
|
||||
// garbage -- so the bar stays usable before the art lands.
|
||||
// alongside it. A missing SVG simply leaves the button showing only its label - never bitmap-less
|
||||
// garbage - so the bar stays usable before the art lands.
|
||||
auto *bar = new wxBoxSizer(wxHORIZONTAL);
|
||||
const auto set_icon = [this](wxAnyButton *b, const std::string &iconname) {
|
||||
const wxBitmap bmp = create_scaled_bitmap(iconname, this, 16);
|
||||
|
||||
@@ -23,26 +23,26 @@
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch -- shows the result of
|
||||
// Standalone 2D viewer/editor for a flattened (UV-unwrapped) mesh patch - shows the result of
|
||||
// GLGizmoTextureDisplacement's LSCM projection method as its own resizable pane (see Plater's
|
||||
// "uv_editor" AUI pane) rather than folding 2D UV-space rendering into the main 3D viewport.
|
||||
//
|
||||
// Islands can be laid out by hand, roughly the way Blender's UV editor works: click one to select
|
||||
// it, drag to move it, right-drag or press R to rotate it, S to scale it, both about its own centre.
|
||||
// Islands are free to overlap -- nothing re-packs them behind the user's back. The texture underneath
|
||||
// Islands are free to overlap - nothing re-packs them behind the user's back. The texture underneath
|
||||
// is always drawn upright and axis-aligned, and it is the islands that move over it, which is what
|
||||
// makes "rotate this island" a meaningful gesture rather than just spinning the whole texture.
|
||||
//
|
||||
// **Geometry is uploaded in the unwrap's own (raw, mm) coordinates, once**, and each island is drawn
|
||||
// through its own affine matrix passed as a shader uniform. That matters: a patch can easily run to
|
||||
// a million triangles, and the earlier design -- which pre-transformed every UV on the CPU and
|
||||
// re-uploaded the whole wireframe on every mouse-move event -- made a drag cost a couple of hundred
|
||||
// a million triangles, and the earlier design - which pre-transformed every UV on the CPU and
|
||||
// re-uploaded the whole wireframe on every mouse-move event - made a drag cost a couple of hundred
|
||||
// milliseconds per frame. Moving an island now touches a 2x3 matrix and nothing else.
|
||||
//
|
||||
// Uses the app's single shared wxGLContext (via wxGetApp().init_glcontext(), the same call
|
||||
// View3D/Preview/AssembleView each make in GUI_Preview.cpp) rather than an independent context of
|
||||
// its own, specifically so it can reuse the app's already-registered "flat"/"flat_texture"
|
||||
// shaders and GLModel as-is -- GLModel::render() looks up its shader via a GUI_App-wide "current
|
||||
// shaders and GLModel as-is - GLModel::render() looks up its shader via a GUI_App-wide "current
|
||||
// shader", which only means anything for canvases sharing the app's one real GL context.
|
||||
class UVEditorCanvas : public wxGLCanvas
|
||||
{
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
// y basis, translation).
|
||||
using IslandTransform = Eigen::Matrix<float, 2, 3>;
|
||||
|
||||
// The unwrap to display, in the unwrap's own mm coordinates -- *not* texture UVs. Changing this
|
||||
// The unwrap to display, in the unwrap's own mm coordinates - *not* texture UVs. Changing this
|
||||
// is the expensive path (it rebuilds every vertex buffer), so it must only be called when the
|
||||
// unwrap itself changes, never merely because an island moved. Pass an empty `indices` to show
|
||||
// nothing.
|
||||
@@ -71,13 +71,13 @@ public:
|
||||
// The cheap path: one transform per island. Safe to call on every mouse-move of a drag.
|
||||
void set_island_transforms(std::vector<IslandTransform> transforms);
|
||||
|
||||
// One fill colour per island, overriding the default light-green wash -- used to paint the UV
|
||||
// One fill colour per island, overriding the default light-green wash - used to paint the UV
|
||||
// distortion heatmap over the islands when the gizmo's "Distortion" check mode is on (#7/#14).
|
||||
// Pass empty to go back to the default wash. Cheap: it never touches a vertex buffer.
|
||||
void set_island_fill_colors(std::vector<ColorRGBA> colors);
|
||||
|
||||
// The layer's own tiling scale and rotation. Needed to map a gesture, which happens in texture-UV
|
||||
// space, back into the unwrap's mm space -- which is where a TextureIsland's offset actually
|
||||
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
|
||||
// lives (see apply_uv_transform()). The tile settings come along because the background has to
|
||||
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
|
||||
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
|
||||
// High-level actions the pane toolbar triggers. The canvas handles the view-only ones (framing,
|
||||
// the snap toggle) itself and forwards the rest to whoever owns the island data (the gizmo), via
|
||||
// the command callback -- the canvas has the selection and the view, the gizmo has the layer.
|
||||
// the command callback - the canvas has the selection and the view, the gizmo has the layer.
|
||||
enum class Command { FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected };
|
||||
void run_command(Command cmd);
|
||||
using CommandFn = std::function<void(Command)>;
|
||||
@@ -106,7 +106,7 @@ public:
|
||||
void set_status_callback(StatusFn fn) { m_on_status = std::move(fn); }
|
||||
|
||||
// Reports an island edit as it happens. The deltas are *incremental* (one mouse event's worth)
|
||||
// and already converted into the units a TextureIsland stores -- unwrap mm, degrees, and a scale
|
||||
// and already converted into the units a TextureIsland stores - unwrap mm, degrees, and a scale
|
||||
// *factor* to multiply the island's existing scale by. They are incremental on purpose: the owner
|
||||
// applies them and hands back fresh transforms, and if the gesture tracked geometry rather than
|
||||
// raw mouse motion that round trip would feed back into itself. `finished` marks the end of a
|
||||
@@ -116,7 +116,7 @@ public:
|
||||
void set_island_edit_callback(IslandEditFn fn) { m_on_island_edit = std::move(fn); }
|
||||
|
||||
// What a click grabs: a whole island (move/rotate/scale, groups move together), a single vertex, or
|
||||
// a single edge (both its endpoints). Vertex/Edge are free-form UV editing -- they move the actual
|
||||
// a single edge (both its endpoints). Vertex/Edge are free-form UV editing - they move the actual
|
||||
// unwrap coordinates, which the owner then folds into the layer's per-vertex UV overrides so the
|
||||
// change is baked, not just shown (see set_vertex_edit_callback).
|
||||
enum class SelectMode { Island, Vertex, Edge };
|
||||
@@ -177,7 +177,7 @@ private:
|
||||
void move_vertex_raw(int unwrapped_vertex, const Vec2f &delta_uv);
|
||||
Vec2f island_centroid(int island) const;
|
||||
// Converts a delta in texture-UV space into the unwrap's mm space, undoing the layer's scale and
|
||||
// rotation -- the inverse of what apply_uv_transform() did on the way in.
|
||||
// rotation - the inverse of what apply_uv_transform() did on the way in.
|
||||
Vec2f uv_delta_to_unwrap(const Vec2f &delta_uv) const;
|
||||
// The correction that would bring the selected island's nearest boundary vertex onto a boundary
|
||||
// vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2).
|
||||
@@ -192,7 +192,7 @@ private:
|
||||
std::vector<IslandTransform> m_transforms;
|
||||
// Per-island fill colour override (distortion heatmap); empty means use the default wash (#7).
|
||||
std::vector<ColorRGBA> m_island_fill_colors;
|
||||
// Boundary vertices per island, for snapping -- a patch's boundary is a tiny fraction of it, and
|
||||
// Boundary vertices per island, for snapping - a patch's boundary is a tiny fraction of it, and
|
||||
// rescanning the whole uv array on every snap test would not be.
|
||||
std::vector<std::vector<int>> m_island_boundary_verts;
|
||||
|
||||
@@ -203,7 +203,7 @@ private:
|
||||
std::vector<GLModel> m_island_boundary; // outline
|
||||
std::vector<GLModel> m_island_fill; // filled, for the selected island's wash
|
||||
|
||||
GLModel m_tile_outline_glmodel; // the texture's first tile, [0,1]^2 -- the "you are here"
|
||||
GLModel m_tile_outline_glmodel; // the texture's first tile, [0,1]^2 - the "you are here"
|
||||
GLModel m_grid_glmodel;
|
||||
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
|
||||
|
||||
@@ -290,7 +290,7 @@ private:
|
||||
float m_gesture_last_dist = 0.f;
|
||||
// Rotation is tracked as two running totals over the gesture: the raw mouse rotation, and how much
|
||||
// has actually been applied. With Shift held the applied total is quantised to 15-degree steps
|
||||
// (Blender-style angle snapping), so the two diverge -- and driving the applied total off the raw
|
||||
// (Blender-style angle snapping), so the two diverge - and driving the applied total off the raw
|
||||
// one, rather than snapping each incremental delta, is what makes the snap stable instead of
|
||||
// juddering. The raw/applied split also survives crossing +/-180 degrees, which a single wrapped
|
||||
// angle would not. m_rot_applied doubles as the modal-rotate undo amount for Esc.
|
||||
|
||||
Reference in New Issue
Block a user