Compare commits

...

40 Commits

Author SHA1 Message Date
ExPikaPaka
21a31660d2 Merge remote-tracking branch 'origin/feature/texture_displacement' into feature/texture_displacement
The remote's 3aefae016f is the same tree as local e245f5d069, and every change
in 013ac898ba (continuous smoothing, world-space bake, mirrored/scaled
placements, plate clamp, UV editor framing/HiDPI, layer scrolling) was already
carried forward and reworked by the local commits. Resolved to the local side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:12:02 +02:00
ExPikaPaka
db0e14ceab Texture displacement gizmo icons 2026-09-15 08:58:26 +02:00
ExPikaPaka
81b8b01a01 Tests for the step cutter, edge flips, auto resolution and v2 colours 2026-09-15 08:58:26 +02:00
ExPikaPaka
2f4c9ab66d Texture gizmo: one-run pipeline by default, auto resolution, colour fixes, debug view 2026-09-15 08:58:26 +02:00
ExPikaPaka
d766854af7 Bump and uvcheck shaders: project in the bake's world frame 2026-09-15 08:58:26 +02:00
ExPikaPaka
2355bb998d Texture displacement: step cutter, v2 colours, auto resolution, anchored bake frame 2026-09-15 08:58:26 +02:00
ExPikaPaka
e8b63da1e2 TextureBake: edge flips along the height field, stage recorder, faster displace 2026-09-15 08:58:26 +02:00
ExPikaPaka
013ac898ba Improve texture displacement smoothing and UV editor framing 2026-09-09 15:25:00 +02:00
ExPikaPaka
5c7cb6bed5 add alternative baking algorithm 2026-09-09 08:42:37 +02:00
ExPikaPaka
3aefae016f Add new bake pipeline 2026-09-03 09:13:14 +02:00
ExPikaPaka
e245f5d069 Add alternative backe pipeline 2026-09-03 08:48:03 +02:00
ExPikaPaka
96f2c8e698 Merge branch 'feature/texture_displacement' of https://github.com/OrcaSlicer/OrcaSlicer into feature/texture_displacement 2026-08-31 08:45:38 +02:00
ExPikaPaka
d855bab19d Add color suport for textures 2026-08-31 07:58:48 +02:00
SoftFever
cde889a7c3 Merge branch 'main' into feature/texture_displacement 2026-08-30 13:52:54 +08:00
ExPikaPaka
045504c880 Move computation to background thread & resolve freeze 2026-08-27 09:20:02 +02:00
ExPikaPaka
6166bac17a Improve adaptive subdivision at border & fix some visual bugs 2026-08-26 09:13:50 +02:00
ExPikaPaka
165a1e9f4c Measure post-process smoothing by edge energy instead of height spread 2026-08-20 07:14:21 +02:00
ExPikaPaka
c215d0291b Merge branch 'main' into feature/texture_displacement 2026-08-19 09:13:53 +02:00
ExPikaPaka
d926f7c490 Add Parallax preview & fix Undo\Redo history 2026-08-12 10:14:38 +02:00
ExPikaPaka
2db770b672 Displace the painted patch border and add post-process smoothing 2026-07-29 08:44:16 +02:00
ExPikaPaka
2a46197322 Rewrite adaptive subdivision to refine worst-first against a triangle budget 2026-07-28 11:42:40 +02:00
ExPikaPaka
15fd3fc97c Add adaptive subdivision 2026-07-24 09:03:06 +02:00
ExPikaPaka
00f778e863 Merge branch 'feature/texture_displacement' of https://github.com/OrcaSlicer/OrcaSlicer into feature/texture_displacement 2026-07-21 14:24:39 +02:00
ExPikaPaka
dc1e8b6dc3 Rename reserved GLSL word on AMD GPU 2026-07-21 14:07:51 +02:00
SoftFever
753054974b Merge branch 'main' into feature/texture_displacement 2026-07-21 19:50:35 +08:00
ExPikaPaka
eacc236ccb Fix typo again 2026-07-21 12:53:00 +02:00
ExPikaPaka
345369e005 Fix typo after cleanup 2026-07-21 11:28:51 +02:00
ExPikaPaka
18c5d31fb2 Merge branch 'feature/texture_displacement' of https://github.com/OrcaSlicer/OrcaSlicer into feature/texture_displacement 2026-07-21 10:12:16 +02:00
ExPikaPaka
61d2d4355a Cleanup 2026-07-21 10:12:09 +02:00
ExPikaPaka
ab023f3f6d Add texture projection frame overlay, fix remeshing and subdivision 2026-07-21 09:44:25 +02:00
Ian Bassi
b073aa4d23 Merge branch 'main' into feature/texture_displacement 2026-07-17 09:07:31 -03:00
ExPikaPaka
8247514ae2 Fix cmake config 2026-07-17 09:04:39 +02:00
ExPikaPaka
00f55639d3 Add new icons 2026-07-16 08:52:15 +02:00
ExPikaPaka
68d754d946 Add texture displacement documentation 2026-07-16 08:43:35 +02:00
ExPikaPaka
dc5a48bdfd Add texture displacement toolbar icon and textures 2026-07-16 08:43:27 +02:00
ExPikaPaka
05083bb6ab Add texture displacement gizmo and UV editor 2026-07-16 08:43:21 +02:00
ExPikaPaka
a393b21642 Add texture displacement bump and UV-check shaders 2026-07-16 08:43:08 +02:00
ExPikaPaka
a7c8dcc58d Add texture displacement baking, LSCM unwrap and remesh core 2026-07-16 08:42:55 +02:00
ExPikaPaka
3514249197 Removed files that were accidently added 2026-07-09 08:40:37 +02:00
ExPikaPaka
7f2598d0d6 POC 2026-07-08 08:50:47 +02:00
119 changed files with 27125 additions and 22 deletions

581
TEXTURE_DISPLACEMENT.md Normal file
View File

@@ -0,0 +1,581 @@
# Texture Displacement - Technical Notes
Branch: `feature/texture_displacement`. Reference for the feature as it stands: what it does, how the
algorithms work, and where the code lives.
## What it does
A paint-style gizmo (`GLGizmoTextureDisplacement`) that lets you:
- Paint one or more "layers" onto a model's surface, each a height-map texture with its own
depth/tiling/rotation/offset/invert/tile-mode/projection-mode/blend-mode.
- Pick a texture from a shipped library (`resources/textures/displacement/`) or import your own
(saved into `<data_dir>/textures/displacement/`, kept separate so app updates can't clobber it).
- Combine overlapping layers with image-editor-style blend modes (Add/Subtract/Multiply/Divide).
- Preview the true displaced result live, before baking (background job, not on the UI thread).
- Preview via a fast GPU shader instead (no real geometry movement) for a lighter-weight alternative.
- Bake into real mesh geometry on demand, restricted to the painted area only.
- Remesh and subdivide so a low-poly model has enough vertices to show fine detail.
- Unwrap a painted patch with a real CGAL LSCM parameterization and view it in a dedicated,
dockable 2D "UV Editor" pane.
## Standard vs Pro mode
A two-position slider in the panel header, right of the Dock/Undock button.
**Pro** shows every mesh-preparation control; Remesh, Subdivide and Bake are run separately by the user,
in whatever order they like.
**Standard** hides all of it and folds one fixed recipe into the Bake button, because a height map only
ever *moves vertices that already exist* - painting onto an imported 12-triangle box and pressing Bake
would otherwise do nothing visible. Standard's Bake is:
1. `plan_remesh()` + `replace_mesh_keep_all_paint()` - isotropic remesh to 1 mm, sharp edges above 40
degrees protected. Gives the subdivider an even starting density whatever the input looked like.
2. `plan_adaptive_subdivision()` + `apply_adaptive_subdivision()` - feature-adaptive refinement, max
edge 20 mm, detail 0.02 mm, min edge 0.02 mm.
3. `bake()` - the ordinary background displacement job.
Both preparation stages are *planned* before the undo snapshot and *applied* after it, so a stage with
nothing to do is skipped without leaving an empty undo step. The standalone Pro buttons share the same
plan/apply split.
**All three stages sit under one undo step.** `Plater::take_snapshot()` records the state *before* the
change, so a single snapshot taken at the top of `bake_standard()` means one Undo returns the mesh to
exactly what was imported. `TextureDisplacementBakeInput::take_snapshot` lets the caller say who owns
the undo step - true for the Pro-mode button, false for the pipeline, whose background job commits long
after that snapshot's scope has closed.
The presets live in one place (`STD_*` constants) and `apply_standard_mode_presets()` pins the hidden
controls to them every frame while Standard is active, so the live preview cannot disagree with what
Bake will do. Switching to Standard also closes the subdivision preview, whose controls have just gone.
One control survives into Standard: **"Added triangles (k)"**, the subdivision budget. It is deliberately
*not* pinned - pinning would fight the user's own slider every frame - because unlike the rest of the
recipe its right value depends on the part rather than on the method (a big model, or a fine texture,
simply needs more triangles). Default 1500. The widget is one lambda shared by both layouts.
Standard remeshes *after* painting, so the remesh has to preserve paint: `ModelVolume::restore_painting()`
only remaps the four standard channels, so `replace_mesh_keep_all_paint()` additionally runs
`TriangleSelector::remap_painting()` over the eight texture-displacement masks. The Pro Remesh button
goes through the same helper. If the remap comes back empty the pipeline stops with a message rather
than baking a flat mesh.
## Architecture
### Data model (per `ModelVolume`)
Each of up to `TEXTURE_DISPLACEMENT_MAX_LAYERS` (8) layers gets its **own independent
`FacetsAnnotation`** paint mask - the 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).
Whole-stack settings (border handling, post-process smoothing) live beside the layers in
`texture_displacement_options` (`TextureDisplacementOptions`), since they belong to no single layer.
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
`build_texture_displacement(base_mesh, layers, facets_data, options)` 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.
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
guarantee the index alignment step 3 depends on.
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both projects
and displaces along these, so a vertex covered by several layers moves along one single well-defined
direction. Where the paint does *not* cover every triangle around a vertex, the normal is recomputed
from the painted triangles alone (the union over all layers, so it stays one direction per vertex):
on the rim of a fully painted top face the whole-mesh normal is the 45-degree bisector it shares with
the side wall, and displacing along that flares the rim outwards instead of raising it. Interior
vertices are unaffected - all their triangles are painted, so the two normals coincide. Paint
coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`.
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
the **base mesh** (never against a previous layer's output), then
`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)`
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
boundary anyway (splitting only happens at partial coverage).
4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is
`TextureDisplacementOptions::displace_border`, and it does by default. Nothing can tear: the bake is
topology-preserving, so a border vertex is *one* vertex shared by both regions and moving it simply
tilts the unpainted triangles that use it. Pinning it instead clamps the outermost ring of relief to
zero, which on a fully painted face collapses the pattern into a ring of steep ramps at the edge; it
is kept as an option for when the relief must not spill past the paint at all. Either way the border
drives the `edge_smoothing` falloff.
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
layer would apply two or three times over depending on local triangle fan-out.
6. Move each touched vertex along its (step 2) normal by its accumulated total.
7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing.
### Post-process smoothing
`smooth_mesh_vertices(mesh, movable, strength, iterations)` - Laplacian relaxation, run after all layers
have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable vertex a
`strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the previous
pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several times as
hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built once per
call. Topology-preserving, like the bake.
Its job is to round off the hard steps a bitmap height map leaves behind - a different knob from
`TextureDisplacementLayer::smoothing`, which blurs the *height map* before it is ever sampled.
Two ways in, sharing one set of settings on the volume:
- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake.
`movable` is exactly the set of vertices the displacement moved, so the untouched part of the model
keeps its exact geometry and the ring just outside the displaced set anchors the relaxation (the
relief cannot creep outward).
- **"Smooth baked mesh now"** (`GLGizmoTextureDisplacement::smooth_model()`) applies the same settings to
the volume's *committed* geometry, for relief that is already baked in. `movable` there is the painted
triangles' vertices. Because smoothing never touches the triangle list, this is the one geometry
operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight
texture-displacement masks around `set_mesh()` rather than remapping or dropping them.
**"Ignore outer ring"** (`smooth_skip_border`, on by default) drops the patch's own outermost ring of
vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the
rim of the relief down toward the flat surface and the pattern comes out half-melted where it meets the
edge. Held out, the border keeps the full depth the texture asked for and only the interior relaxes.
Turning it off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing
falloff). This is the *smoothing* rim, independent of whether that rim is displaced at all
(`displace_border`, step 4 above); both default to keeping the border sharp.
### Blend modes
`TextureBlendMode` {Add, Subtract, Multiply, Divide}, per layer, applied per vertex against the
total accumulated by the layers **below** it (lower slots). The quantity blended is a signed
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
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 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×.
The **lowest painted layer ignores its blend mode**: it has nothing beneath it, and Multiply/Divide
against an implicit zero base would annihilate (or blow up) it. Enforced in
`build_texture_displacement()` (the first layer to reach a given vertex always folds in additively) and
surfaced in the UI, which labels that layer "Base layer" instead of offering a control that does nothing.
### Projection methods
Five choices per layer (`TextureProjectionMethod`), all funneling through `apply_uv_transform()`
(scale by `1/tiling_scale`, rotate by `rotation_deg`, add `offset`). They are dispatched by
`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)`)
and blends the three by the vertex's own normal raised to `TRIPLANAR_BLEND_SHARPNESS` (4). Hard-picking
the single axis most aligned with the normal instead 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. A weighted blend is continuous across the transition by construction, since the weight
of the axis being left behind falls smoothly to zero. 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
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`. An approximation, not an exact fit for arbitrary geometry, and the axis/centre are not
user-overridable.
- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat.
- **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
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 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 `compact_patch_with_map()` builds a clean sub-mesh plus an index map back to the
original vertex numbering, purely local to this file.
- **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
(`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.
Two companions to this mode:
- **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
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
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
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
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
gizmo's `UVEditorState` include the seam list, so marking a seam (which leaves the paint mask
untouched) still forces a re-solve. Like the paint masks, seams are mesh-index-space and so dropped on
any topology change.
Two ways to write to it:
- **Mark seam (manual)** - 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)** - `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. Exposed as the UV pane's **Cut** button.
### UV-check overlays (checker / distortion)
`resources/shaders/{110,140}/texture_displacement_uvcheck.{vs,fs}`, one shader with a `mode` uniform,
drawn over the painted patch (`rebuild_uvcheck_mesh()`/`render_uvcheck_mesh()`, P3N3T2: `normal.x` =
distortion, `tex_coord` = uv), pulled forward with a polygon offset. **Checker** 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** 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 draws the whole volume's triangle edges, rebuilt only when the vertex count changes
(not per stroke).
### Tiling
`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** rather than clamping the *coordinate* into range, which would smear the border row/column of
pixels outward to infinity in every direction (streaky lines radiating out from the painted patch).
### Subdivision — two modes
**Uniform (`subdivide_mesh_uniform()`)** — whole-mesh, 1-to-4 split. Recursive edge-midpoint split with
a shared per-pass midpoint cache (keyed by sorted vertex-index pair) so triangles sharing an edge get
the *same* new vertex - capped at `max_iterations` (default 6). Whole-mesh so it never leaves a
T-junction, at the cost of densifying everywhere. Wired as a "Subdivide steps" slider (**05**, 0 =
no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no remap) via the standard
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
**Adaptive (`subdivide_mesh_adaptive()`)** — refine **only the painted area**, by **Rivara longest-edge
bisection**, which is *conformal by construction*. Only **terminal** edges are ever bisected - an edge
that is the longest edge of *every* triangle sharing it - which splits both those triangles along one
shared midpoint at once, so a hanging node is never created. The edge to split for a triangle that wants
refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of ever-longer-edged
neighbours until a terminal one is reached, and bisect that. Edge length strictly increases along the
path (ties broken by mesh-vertex key, which both sides of an edge compute identically), so the walk
cannot cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
of bisections. The transition triangles it pulls in just outside the painted patch are the graded band
that makes the size change conformal.
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
**Run to completion, worst-first, against a triangle budget.** The refinement loop is not a fixed number
of sweeps: it holds every triangle that is over its criteria in a max-heap keyed by *how many times over*
it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle
across each edge) is built **once** and maintained incrementally through each bisection, so the cost
scales with the refined region rather than with the whole model. `max_triangles` is the only bound;
stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest
errors. A fixed sweep count instead spends itself grading the *coarse surroundings* - whose edges are
the longest, so they win every terminal-edge contest - and never reaches the painted patch.
**It carries the paint forward**, which is what makes it usable (uniform subdivide drops paint). Because
the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an
`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each
layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that
layer. `collect_paint_region()` derives both:
- the union refine-region: **exactly** the original triangles the brush touched, read straight off
`TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that
is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No
dilation: marking every triangle that shares a *vertex* with the patch drags in a whole fan of huge
unpainted neighbours and refines *those* down to the resolution floor, since the height field the
detail test samples is not restricted to the painted area. The conformal closure already grades the
size change outward on its own.
- the per-layer fully-painted-triangle sets (a `get_facets_strict(ENFORCER)` sub-triangle with all three
*original* vertex indices == a whole, fully-painted original triangle; a partial stroke's sub-triangles
always carry a split vertex).
The other four channels ride the normal `restore_painting()` remap.
Both modes share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox
picks the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the
wireframe while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle
count.
**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"**
checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. A flat region or
a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp); what needs them is
**curvature** - the *second* derivative, not the gradient. So the extra predicate is a **chord-error**
test: sample the combined displacement at the triangle's three edge midpoints *and its centroid*
(sampling the interior is what catches a bump sitting inside a triangle, the blind spot of an edge-only
test) and take the largest departure from the flat triangle's barycentric interpolation. Refine while
that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp ⇒ untouched; high on a
bump/ridge/noise ⇒ refined until captured. Same conformal machinery, so still crack-free. The
per-triangle error is cached and recomputed only for the children of a split.
Four knobs bracket it, and all four matter:
- **"Max edge (mm)"** (`target_edge_length_mm`) is a **baseline that applies in feature mode too**.
Without it the chord test aliases: a big triangle over a fine pattern can sample four points that all
land at similar heights, report no error, and stall before refinement ever starts. The baseline
guarantees a sampling density fine enough for the curvature test to see the texture at all.
- **"Detail (mm)"** is the chord tolerance above.
- **"Min edge (mm)"** is a hard floor under both, and is what guarantees termination across a sharp
texture *step*, where the error never falls however fine the mesh gets.
- **"Added triangles (k)"** is the budget, passed as `max_triangles` (the model's own triangle count plus
the slider, so the control still means something on an already-dense model).
The height field is `make_combined_displacement_sampler()` - it mirrors `build_texture_displacement()`'s
per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively")
but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe -
over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint
test), and edge-smoothing falloff is ignored. The first is *why* the refine region must not be dilated -
outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no per-point UV); a
purely LSCM stack yields a null sampler and the code falls back to the length baseline alone. Per-vertex
heights are sampled lazily, so a small patch on a huge model never pays for the rest of it.
### Fast preview (GPU-only, no CPU meshing)
`resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}`, registered as
`"texture_displacement_bump"`. Shades the *displaced* surface without moving geometry - active-layer
only, selected from the View row, and the default when the gizmo opens (`m_use_bump_preview = true`).
Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the per-vertex paint
weight (0/1), `normal.y` flags the UV island currently being dragged, and `tex_coord` carries a
precomputed texture UV, so it can use `GLModel` normally instead of a hand-rolled VBO/VAO manager.
The mesh is **flat** (vertices not shared between triangles): every corner of a painted triangle gets
weight 1, every corner of an unpainted one weight 0. A coarse mesh needs that - one painted face of a raw
cube has no strictly-interior vertex, so per-vertex weighting would either bleed onto the neighbours or
vanish outright. Duplicating vertices costs no shading quality here because the shader takes its surface
normal from screen-space derivatives of position, not from a per-vertex normal.
**Both preview meshes work in the patch's vertex space, not the mesh's.** Those agree only until a
*brush* stroke splits a triangle: `get_facets_strict()` then appends the split vertices, so the patch
array is longer. `rebuild_bump_preview_mesh()` and `rebuild_uvcheck_mesh()` therefore index
`patch.vertices` throughout. The weight buffer is rebuilt at the same cadence as the true-displacement
preview (stroke-end/slider-release) but from the **live** `TriangleSelector` state, not the flushed model
facets, so it does not lag by a full model round-trip.
The perturbed normal is the analytic one for a height field `H = ±depth_mm · h(uv)` displaced along
`N` over any orthonormal surface tangent pair `T`/`B`:
N' = normalize(N (dH/da)·T (dH/db)·B), a = dot(p,T), b = dot(p,B)
The two slopes have to be genuine **mm-per-mm** derivatives for the preview's apparent depth to match
the bake's.
**Two projection paths (`use_vertex_uv` uniform):**
- **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 and ViewProjected)** - `uv` comes per-vertex from
the CPU (`compute_layer_vertex_uvs()`, so island placement + tiling/rotation/offset are already folded
in), and the perturbed normal is built with **Mikkelsen's method** ("Bump Mapping Unparametrized
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, because an
LSCM map is **conformal, not isometric**: it is globally area-scaled but the *local* mm-per-uv varies
across the chart, so a single global `1/tiling_scale` factor gets the apparent depth wrong. `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 shading - moves with it
(the mesh rebuilds on drag-end, `on_island_edited(finished)``rebuild_preview()`
`rebuild_bump_preview_mesh()`). The branch is 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
`det≈0` guard skips it (a localised preview-only artifact, never in the bake).
**Parallax (triplanar path).** Perturbing the shading normal alone welds the pattern to the base surface:
it does not slide as the camera orbits, and does not get deeper as `depth_mm` grows. The triplanar path
therefore shades at the point the *displaced* surface would show at this pixel, found by **ray marching**
(parallax occlusion mapping). A point at ray parameter `s`, i.e. `P + V·s` (`P` the base point, `V` the
unit direction to the eye), sits at height `s·dot(V,n)` above the undisplaced surface. The displaced
surface lives in a shell between the extreme values of `amp·(h midlevel)` - taken from both ends of
`h ∈ [0,1]`, so it holds for an inverted layer and a raised midlevel too, where the surface sits *below*
the undisplaced one. The march starts at the top of that shell, where the ray is outside the surface by
construction, and steps inward until the ray height drops below the sampled height. That crossing *is*
the visible point.
Solving `Q = P + V·(H(Q)/dot(V,n))` by fixed-point iteration instead is geometrically exact but the
divisor goes to zero edge-on; the sample then lands a large fraction of a tile away and the iteration
oscillates, which reads as a second, flat copy of the pattern ghosted over the real one. Clamping the
step to one tile does not help - a tile-sized shift lands on the neighbouring tile, the same pattern
again. Offset limiting (stepping along the tangential part of `V`) is stable but understates parallax
enough that the relief still flattens as soon as the camera tilts. Marching has neither problem.
The hit is interpolated between the last two samples, which keeps `PARALLAX_STEPS` (24) affordable, and
the whole march is skipped when sweeping the shell would move the sample point less than half a texel -
the head-on case, so the common view pays almost nothing. The 140 variant samples with
`textureLod(…, 0.0)` inside the loop, since implicit derivatives are undefined in non-uniform control
flow. Two uniforms exist for this: `midlevel` (parallax needs the real height, not just its derivative)
and `eye_model_pos` (the camera in the volume's local frame).
Parallax cannot change the model's silhouette or cast shadows; the View row's Normal mode is one click
away for that. The LSCM path stays plain Mikkelsen bump - it has no closed-form uv, so there is no cheap
way to re-project a marched position. One further approximation: the GPU sampler's wrap mode stands in
for `tile_enabled`/`tile_method`, so with tiling *off* the GPU repeats where the CPU returns 0 outside
`[0,1)`.
### On-canvas "Adjust Texture" gizmo
A per-active-layer toggle ("Adjust placement") 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.
### Projection frame overlay (ViewProjected)
`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
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`. It only ever draws a bitmap and a border.
**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 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:
- `K = projection · view · (instance · volume)`, i.e. local → clip, the same product the renderer uses.
Note `Camera::get_projection_matrix()` is typed `Transform3d` (nominally affine) but its perspective
form explicitly writes a `(0, 0, 1, 0)` bottom row into the underlying 4×4, so `clip.w = z_eye` is
genuinely carried. The build therefore multiplies **`.matrix()` products** (plain `Matrix4d`), never
`Transform3d` products, which would not compose that row correctly.
- 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.
- `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
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
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
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
reopening keeps it where it was left.
### UV Editor pane
`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 / Avg scale / Cut / Join / Unjoin) and a status line
along the bottom naming the current gesture and the shortcuts in play. The *panel* is what is
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
`get_uv_editor_canvas()` returns the inner canvas the gizmo talks to.
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
`"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`. 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
*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`; "Avg scale" (`average_island_scales()`) sets every island to the mean, so
one island scaled by hand can be matched back to its neighbours' texel density. **Snap** (canvas-owned
`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
of. Toolbar commands the canvas can't service itself (Avg scale, Cut, Join, Unjoin) are forwarded to the
gizmo via `CommandFn`; view-only ones (Frame, Snap) it handles directly.
## File map
**libslic3r (core, no GUI dependency):**
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing
(`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc
comments throughout, they're kept accurate and up to date.
- `src/libslic3r/MeshBoolean.hpp/.cpp` - `parameterize_lscm()` and `remesh_isotropic()` in the `cgal`
sub-namespace, reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already
there for mesh boolean ops. 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 the
`Surface_mesh_parameterization` package headers were already present.
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
`texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points
(see Data model above).
**GUI:**
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo and its whole panel.
- `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,
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
(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,
`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 fast-preview shader.
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`.
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration.
## Tests
`tests/libslic3r/test_texture_displacement.cpp`. Covers `decode_height_texture` round-trip, empty-layer
no-op, full-cube uniform displacement, a second layer over the same area contributing, all four blend
modes (table-driven), the lowest layer ignoring its blend mode, border displace/pin, post-process
smoothing and its mask guarantees, and adaptive subdivision: conformality (`every_edge_used_twice` on a
partially-refined cube - an exact crack detector for a closed mesh), the target edge length actually
being reached, the triangle budget capping the result without opening a crack, curvature-driven
refinement (a Gaussian bump refines at its centre, a linear ramp adds nothing), and the max-edge
baseline.
`BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them:
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build --config Release --target libslic3r_tests -- -m
./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand

View File

@@ -0,0 +1,326 @@
# Texture Displacement — Feature & Controls Guide
Texture Displacement is a paint-style gizmo that stamps height-map textures onto a model's surface
and turns them into real relief — engraved or embossed detail — either as a live preview or baked
into actual mesh geometry. You paint where the texture applies, stack multiple textures as blended
layers, choose how each is projected onto the surface, and (for the unwrap projection) lay the result
out by hand in a dedicated 2D **UV Editor** pane.
This document describes every feature and control. For the internal architecture and algorithms, see
`TEXTURE_DISPLACEMENT.md`.
---
## Table of contents
1. [Quick start](#quick-start)
2. [Entering the tool](#entering-the-tool)
3. [Selection modes](#selection-modes)
4. [View modes](#view-modes)
5. [Auto update](#auto-update)
6. [Texture layers](#texture-layers)
7. [Per-layer settings](#per-layer-settings)
8. [Projection methods](#projection-methods)
9. [The UV Editor](#the-uv-editor)
10. [Seams](#seams)
11. [Adjust placement (on-model)](#adjust-placement-on-model)
12. [Preparing the mesh: Subdivide & Remesh](#preparing-the-mesh-subdivide--remesh)
13. [Baking & resetting](#baking--resetting)
14. [Controls reference](#controls-reference)
15. [Tips & limitations](#tips--limitations)
---
## Quick start
1. Select an object and open the **Texture displacement** gizmo from the left toolbar.
2. A texture layer is added automatically. Pick a texture from the layer's picker, or import your own.
3. **Paint** the area you want the texture to affect (or press **Select whole model**).
4. The relief appears live on the model. Tune **Depth**, **Tile size**, **Rotation**, etc.
5. If the model is low-poly, use **Subdivide** or **Remesh** so there are enough vertices for detail.
6. Press **Bake** to convert the preview into real geometry, or leave it as a live preview.
> The tool only ever affects the **painted** area. Everything you don't paint keeps its original
> surface, and bake blends the relief seamlessly into it.
---
## Entering the tool
The gizmo lives on the left gizmo toolbar (icon: `toolbar_texture_displacement.svg`). Its settings
panel opens beside the toolbar. You can **Dock panel / Undock panel** (top of the panel) to pin it or
float it freely over the 3D view, and **Close** at the bottom exits the gizmo.
When you first open the tool on a never-textured object it starts with **one texture layer already
added**, so you can paint straight away.
---
## Selection modes
Choose *how* you paint. All three write into the **active layer's** mask.
| Mode | What it does |
|------|--------------|
| **Brush** | Free-hand painting with a round brush. Shows a **Brush size** slider and a **Circle / Sphere** choice (circle = surface disc, sphere = 3D ball that also paints around curves). |
| **Face** | Click a single triangle to paint it. |
| **Connected area** | Click to flood-fill a region; the **Angle threshold** slider limits how far the fill spreads across changes in surface angle. |
- **Select whole model** — marks the entire model as painted for the active layer, instead of
brushing it by hand.
---
## View modes
A row of icon buttons labelled **View** controls how the painted area is shown. The first four are a
radio group; **Wireframe** is an independent toggle. Hover any icon for its tooltip.
| View | Meaning |
|------|---------|
| **Normal** | The true displaced geometry — exactly what **Bake** produces. Rebuilt in the background. |
| **Fast** | A GPU bump-shaded approximation of the *active layer only*. No real geometry movement — quick to update, not exact. Best while tuning or dragging islands. |
| **Checker** | A test grid painted over the unwrap so you can see stretching (squares stay square where the map isn't distorted). |
| **Distortion** | A blue→green→red heatmap of how much each area is compressed or stretched in UV space. Needs the **Unwrap (LSCM)** projection. |
| **Wireframe** | Overlays the mesh edges (white). Independent of the view above; in **Normal** view it sits on the displaced surface. |
---
## Auto update
**Auto update** (on by default) rebuilds the true displaced geometry as soon as *anything* changes —
painting, swapping textures, moving sliders. Turn it off on very heavy models to only rebuild when you
release a slider (painting still updates on stroke end).
---
## Texture layers
You can stack up to **8** texture layers. Each has its own independent paint mask, its own texture,
and its own parameters, and they combine in slot order like layers in an image editor.
- **Add a layer** — the ** icon** to the right of the *Texture layers* heading (reuses the tool icon
for now).
- **Remove** — the button on each layer's header row.
- **Active layer** — click a layer's header (or anywhere in its block) to make it active. The active
layer is the one you paint into and the one whose block is tinted. Only one layer is active at a time.
- **Erase all** — clears the active layer's paint.
Each layer shows a texture **picker** (large preview + name). Open it to choose from the shipped
library or import your own image (any png/jpg/bmp; it's converted to an 8-bit grayscale height map and
copied into your user texture folder so app updates can't overwrite it).
---
## Per-layer settings
| Control | Range / options | What it does |
|---------|-----------------|--------------|
| **Depth (mm)** | 0.0110 (log) | Maximum displacement along the surface normal. |
| **Tile size (mm)** | 0.2200 (log) | Physical size of one texture tile on the surface. |
| **Rotation** | 0360° | Rotates the texture on the surface. |
| **Midlevel** | 010 | The grey level that means "don't move". At 0 the texture only pushes outward; raise it and darker texels cut *inward* (one map both embosses and engraves). 0.5 makes mid-grey neutral. |
| **Smoothing** | 01 | Blurs the height texture before it displaces — rounds hard edges and removes speckle without needing a softer source image. |
| **Edge smoothing** | checkbox + **Edge amount** 01 | Fades the relief to flat toward the *edge of the painted area*, so it blends into the surrounding surface. A small amount softens only a thin band at the very edge; the maximum flattens the whole painted face. |
| **Invert** | checkbox | Flips the height map (peaks become valleys). |
| **Blend** | Add / Subtract / Multiply / Divide | How this layer combines with the layers **below** it where they overlap. Add/Subtract pile relief on or carve it away; Multiply/Divide scale the relief underneath (a mask). The lowest painted layer is the **Base** and always behaves additively. |
| **Tile** | checkbox + **Repeat / Mirrored repeat** | When off, the texture is placed once (a decal) instead of repeating. Mirrored repeat flips every other tile to hide seams. |
| **Projection** | see below | How the texture is mapped onto the painted surface. |
> **Midlevel warning:** cutting inward can fold the surface through itself in sharp concave corners or
> thin walls. Keep Depth small relative to the feature you're cutting into; the panel warns when a deep
> inward setting is risky.
---
## Projection methods
How the 2D texture is wrapped onto the 3D painted area.
| Method | Best for | Notes |
|--------|----------|-------|
| **Triplanar (blended)** | Patches wrapping around edges | Projects from all three axes at once and blends, so there's no seam across a sharp edge. |
| **Cylindrical** | Round, tube-like selections | Wraps the texture around the patch's own centre/axis. |
| **Spherical** | Ball-like selections | Longitude/latitude wrap around the patch centre. |
| **Unwrap (LSCM)** | Flat, controlled layout | A real conformal unwrap. Cuts the area into pieces at sharp edges (see **Seam angle**), flattens each, and lets you lay them out by hand in the **UV Editor**. Unlocks Checker/Distortion, seams, and island editing. |
| **From view** | Decals / slide-projector look | Projects straight onto the surface from the current camera direction. Use **Capture current view** to re-lay it from wherever you're looking. |
### LSCM-only controls
These appear when a layer uses **Unwrap (LSCM)**:
- **Seam angle** (590°) — edges sharper than this are cut so each piece lies flat. Lower cuts more
(less stretching, more seams); raise to keep more in one piece. A box's 90° corners are cut by
default. *Ignored once you've marked any seam by hand* (your seams then define the pieces).
- **Connect islands** (on by default) — lays the unwrap out as a **connected net**: pieces that share
an edge are unfolded next to each other (a cube becomes a joined net instead of six loose squares).
They stay separate islands, so you can still move any of them by hand. Turn off for the classic
packed-grid layout.
- **Open UV editor** — shows the flattened unwrap in a side pane (see below). Opens *only* when you
turn this on — it never pops up on its own.
- **Mark seams** / **Path** / **Clear seams** — see [Seams](#seams).
- An **Unwrap: N islands, F faces, V verts** read-out tells you what the unwrap actually produced.
---
## The UV Editor
A dockable 2D pane (enable **Open UV editor** on an LSCM layer) showing the flattened unwrap over the
height texture. Islands are the flattened pieces; you can rearrange them freely — nothing re-packs them
behind your back. Moving an island updates the model **live** (in Fast view it tracks the cursor
smoothly, via a shader uniform — no rebuild until you release).
### Navigation
| Action | Control |
|--------|---------|
| Pan | Middle-drag |
| Zoom | Mouse wheel (zooms about the cursor) |
| Frame everything | **Home** or **F**, or the **Frame** toolbar button |
### Editing an island
| Action | Control |
|--------|---------|
| Select | Left-click an island |
| Move | Left-drag |
| Rotate | Right-drag, or press **R** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Rotate snapped | Hold **Shift** while rotating — snaps to **global** 15° marks (0/15/30…). A protractor dial with tick marks and the current angle is shown. |
| Scale | Press **S** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Undo / Redo | **Ctrl+Z** / **Ctrl+Shift+Z** or **Ctrl+Y** |
The **selected** island gets a bold light-green outline and a brighter wireframe; unselected islands
are a translucent light-green wash. The texture underneath repeats exactly as it will when baked.
A **status line** along the bottom always names the current gesture and the shortcuts in play.
### Toolbar
| Button | Action |
|--------|--------|
| **Frame** | Frame all islands (same as Home). |
| **Snap** | Toggle magnetic snapping — a dragged island sticks its boundary to a neighbour's when they come close. |
| **Avg scale** | Give every island the same texel density (Blender's "Average Islands Scale"). |
| **Cut** | Split the selected island across its long axis (useful for very long islands). |
| **Join** | Unfold the selected island onto its nearest neighbour along their shared edge — keeps both as separate islands with their own borders. |
| **Unjoin** | Send the selected island back to its own packed position. |
> **Checker / Distortion in the UV editor:** selecting those View modes also colours the UV pane — a
> checker background, or a per-island distortion heatmap — so you can judge stretch in 2D as well as
> on the model.
---
## Seams
Seams are edges the unwrap is forced to cut along, on top of whatever the Seam angle cuts — the
Blender "mark seam" workflow. They let you control exactly where the unwrap splits.
Enable **Mark seams** on an active LSCM layer, then:
- **Click an edge** on the model to mark it (it turns **red**); click a red edge again to unmark it.
The edge under the cursor is highlighted **yellow** so you can see what a click will toggle.
- **Path mode** (the **Path** checkbox) — for dense meshes where clicking each edge is tedious: click a
start point, then an end point, and the whole **shortest path** between them is seamed at once. It
chains (each click extends from the last point); the start vertex is shown in **green**.
- **Ctrl+drag** rotates/pans the camera while in seam mode.
- **Clear seams** removes them all.
Once any seam is marked, the automatic Seam-angle cutting is disabled so *your* seams define the
islands — pieces you leave un-seamed merge together.
---
## Adjust placement (on-model)
**Adjust placement** (on an active layer) lets you position the texture by dragging a handle on the
model instead of nudging the Rotation/offset numbers. The handle is a flat panel in the patch's
tangent plane (drag anywhere on it to move freely) plus U/V arrows for single-axis nudges. It's
anchored to the painted patch, so paint something first.
---
## Preparing the mesh: Subdivide & Remesh
Displacement can only move vertices that exist, so a coarse model needs more of them first.
### Subdivide
Splits every triangle into four, **15 times** (each step roughly quadruples the triangle count).
- **Subdivide steps** (15) — how many times to split.
- **Preview subdivision** — shows the result as a **cyan wireframe** without changing the model.
- **Apply** — commits the subdivision to the geometry.
- **Done** — ends the preview and leaves the model as it is.
### Remesh
Rebuilds the whole model with triangles close to a target edge length — evens out a mesh with wildly
varying triangle sizes (CGAL isotropic remeshing).
- **Target edge (mm)** — desired triangle edge length (seeded to the model's current average).
- **Remesh** — splits the big triangles and merges the small ones to that size.
> Both Subdivide-Apply and Remesh **replace the geometry** and clear any *not-yet-baked* paint on it
> (already-baked relief is kept). If you had the mesh **Wireframe** on before, it stays on afterward.
---
## Baking & resetting
- **Bake** — converts the current preview into real, permanent mesh geometry, restricted to the
painted area. Runs in the background; the button shows *Baking…* while it works.
- **Erase all** — clears the active layer's paint.
Baking is the exact same algorithm as the **Normal** preview, so what you see is what you get.
---
## Controls reference
### Mouse — 3D view (while painting)
| Input | Action |
|-------|--------|
| Left-drag | Paint the active layer |
| Ctrl + drag | Rotate / pan the camera (works in seam mode too) |
| Wheel | Zoom |
### Mouse & keys — UV Editor
| Input | Action |
|-------|--------|
| Left-click | Select island |
| Left-drag | Move island |
| Right-drag | Rotate island |
| **R** / **S** | Modal rotate / scale (mouse drives it, click or Enter confirms, Esc cancels) |
| **Shift** (while rotating) | Snap to global 15° marks |
| Middle-drag | Pan |
| Wheel | Zoom about cursor |
| **Home** / **F** | Frame all islands |
| **Ctrl+Z** / **Ctrl+Shift+Z** / **Ctrl+Y** | Undo / redo |
### Seam mode
| Input | Action |
|-------|--------|
| Click edge | Mark / unmark a seam (yellow = hover, red = marked) |
| Click (Path mode) | Set start, then seam the shortest path to the next click |
| Ctrl + drag | Rotate / pan camera |
---
## Tips & limitations
- **Paint first, then bake.** The preview is free to explore; only Bake changes the real mesh.
- **Not enough detail?** Subdivide or Remesh before painting fine textures.
- **Inward cuts** (high Midlevel + big Depth) can self-intersect on thin walls or sharp concave
corners — keep Depth modest there.
- **Fast vs Normal:** Fast preview shades a bump and shows only the active layer; use it for quick
tuning and smooth UV dragging, but trust **Normal**/**Bake** for the exact result.
- **Topology changes drop unbaked paint.** Subdivide-Apply, Remesh, and Simplify replace the mesh, and
texture-displacement paint isn't remapped across that change (already-baked relief is unaffected).
- **Island placements** are tied to the current unwrap. Re-painting or changing the Seam angle can
re-segment the charts and renumber them, so a re-unwrap re-lays the connected net and discards
hand placements made before it.
- **Connect islands** is on by default; turn it off (per layer) for the classic packed-grid layout, or
if an unfold looks wrong on an unusual mesh.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="7.5" cy="7.5" r="7" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="4" x2="7.5" y2="11" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="4" y1="7.5" x2="11" y2="7.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="7.5" cy="7.5" r="7" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="4" y1="7.5" x2="11" y2="7.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 315 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="4" height="4" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><path d="M7.5 0.5v4M7.5 10.5v4M0.5 7.5h4M10.5 7.5h4M5.5 2.5l2-2 2 2M5.5 12.5l2 2 2-2M2.5 5.5l-2 2 2 2M12.5 5.5l2 2-2 2" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -0,0 +1,330 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_group.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="25.28125"
inkscape:cy="18.359375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><g
id="g31"
transform="translate(-1.2926181,-1.328125)"><path
d="m 11.472656,12.726388 v 6.047224 A 1.2263883,1.2263883 45 0 0 12.699044,20 h 6.074568 A 1.2263883,1.2263883 135 0 0 20,18.773612 V 12.726388 A 1.2263883,1.2263883 45 0 0 18.773612,11.5 h -6.074568 a 1.2263883,1.2263883 135 0 0 -1.226388,1.226388 z m 1.730947,-0.0096 h 5.065451 a 0.47508735,0.47508735 45 0 1 0.475087,0.475087 v 5.303732 a 0.47508735,0.47508735 135 0 1 -0.475087,0.475087 h -5.065451 a 0.47508735,0.47508735 45 0 1 -0.475087,-0.475087 l 0,-5.303732 a 0.47508735,0.47508735 135 0 1 0.475087,-0.475087 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path30"
inkscape:path-effect="#path-effect31"
inkscape:original-d="M 11.472656,11.5 V 20 H 20 v -8.5 z m 1.25586,1.216797 h 6.015625 v 6.253906 h -6.015625 z" /><path
d="m 11.472656,12.726388 v 6.047224 A 1.2263883,1.2263883 45 0 0 12.699044,20 h 6.074568 A 1.2263883,1.2263883 135 0 0 20,18.773612 V 12.726388 A 1.2263883,1.2263883 45 0 0 18.773612,11.5 h -6.074568 a 1.2263883,1.2263883 135 0 0 -1.226388,1.226388 z m 1.730947,-0.0096 h 5.065451 a 0.47508735,0.47508735 45 0 1 0.475087,0.475087 v 5.303732 a 0.47508735,0.47508735 135 0 1 -0.475087,0.475087 h -5.065451 a 0.47508735,0.47508735 45 0 1 -0.475087,-0.475087 l 0,-5.303732 a 0.47508735,0.47508735 135 0 1 0.475087,-0.475087 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path30-4"
inkscape:path-effect="#path-effect31-9"
inkscape:original-d="M 11.472656,11.5 V 20 H 20 v -8.5 z m 1.25586,1.216797 h 6.015625 v 6.253906 h -6.015625 z"
transform="translate(11.058594)" /><path
d="m 11.472656,12.726388 v 6.047224 A 1.2263883,1.2263883 45 0 0 12.699044,20 h 6.074568 A 1.2263883,1.2263883 135 0 0 20,18.773612 V 12.726388 A 1.2263883,1.2263883 45 0 0 18.773612,11.5 h -6.074568 a 1.2263883,1.2263883 135 0 0 -1.226388,1.226388 z m 1.730947,-0.0096 h 5.065451 a 0.47508735,0.47508735 45 0 1 0.475087,0.475087 v 5.303732 a 0.47508735,0.47508735 135 0 1 -0.475087,0.475087 h -5.065451 a 0.47508735,0.47508735 45 0 1 -0.475087,-0.475087 l 0,-5.303732 a 0.47508735,0.47508735 135 0 1 0.475087,-0.475087 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path30-4-3"
inkscape:path-effect="#path-effect31-9-1"
inkscape:original-d="M 11.472656,11.5 V 20 H 20 v -8.5 z m 1.25586,1.216797 h 6.015625 v 6.253906 h -6.015625 z"
transform="translate(0,11.15625)" /></g></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polygon points="12.5 0.5 15.5 3.5 10 9 7 6" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M6.5 7C4.7 6.8 3.2 8 2.9 9.9c-.2 1.3-.8 2.2-2.4 3 2.8 1.5 6.6 1 7.5-1.6.5-1.5.3-3.1-1.5-4.3Z" style="fill:#009688"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="0.5" y="0.5" width="7" height="7" style="fill:#009688"/><rect x="7.5" y="7.5" width="7" height="7" style="fill:#009688"/><rect x="0.5" y="0.5" width="14" height="14" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="0.5" x2="7.5" y2="14.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="0.5" y1="7.5" x2="14.5" y2="7.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M7.5 7.5 L14.5 7.5 L11 13.5 Z" style="fill:#009688"/><path d="M7.5 7.5 L11 13.5 L4 13.5 Z" style="fill:#009688"/><path d="M7.5 7.5 L4 13.5 L0.5 7.5 Z" style="fill:#009688"/><polygon points="14.5 7.5 11 13.5 4 13.5 0.5 7.5 4 1.5 11 1.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="7.5" x2="14.5" y2="7.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="7.5" x2="11" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="7.5" x2="4" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="7.5" x2="0.5" y2="7.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="7.5" x2="4" y2="1.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="7.5" y1="7.5" x2="11" y2="1.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><line x1="2.5" y1="2.5" x2="12.5" y2="12.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="12.5" y1="2.5" x2="2.5" y2="12.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -0,0 +1,359 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_cut.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="15.375"
inkscape:cy="16.140625"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
d="m 16.082031,8.7625 v 22.475 a 0.965625,0.965625 45 0 0 0.965625,0.965625 h 5.85 A 0.965625,0.965625 135 0 0 23.863281,31.2375 V 8.7625 A 0.965625,0.965625 45 0 0 22.897656,7.796875 h -5.85 A 0.965625,0.965625 135 0 0 16.082031,8.7625 Z m 1.325781,-0.032031 h 5.184376 a 0.3257813,0.3257813 45 0 1 0.325781,0.3257813 V 30.94375 a 0.3257813,0.3257813 135 0 1 -0.325781,0.325781 H 17.407812 A 0.3257813,0.3257813 45 0 1 17.082031,30.94375 V 9.0562501 a 0.3257813,0.3257813 135 0 1 0.325781,-0.3257813 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path33"
inkscape:path-effect="#path-effect34"
inkscape:original-d="m 16.082031,7.796875 v 24.40625 h 7.78125 V 7.796875 Z m 1,0.9335938 h 5.835938 V 31.269531 h -5.835938 z" /><path
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect32"
width="1.0000267"
height="22.719467"
x="27.527292"
y="-12.430544"
transform="rotate(47.337307)"
inkscape:path-effect="#path-effect33"
sodipodi:type="rect"
d="m 28.027306,-12.430544 c 0.277007,0 0.500013,0.223006 0.500013,0.500013 V 9.7889099 c 0,0.2770071 -0.223006,0.5000131 -0.500013,0.5000131 -0.277008,0 -0.500014,-0.223006 -0.500014,-0.5000131 V -11.930531 c 0,-0.277007 0.223006,-0.500013 0.500014,-0.500013 z"
ry="0.50001335" /></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="0.5" y="0.5" width="14" height="14" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="0.5" y1="7.5" x2="14.5" y2="7.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="2.5" y1="0.5" x2="2.5" y2="14.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="5" y1="0.5" x2="5" y2="14.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="8.5" y1="0.5" x2="8.5" y2="14.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 688 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="5.5" cy="3.5" r="1.25" style="fill:#949494"/><circle cx="9.5" cy="3.5" r="1.25" style="fill:#949494"/><circle cx="5.5" cy="7.5" r="1.25" style="fill:#949494"/><circle cx="9.5" cy="7.5" r="1.25" style="fill:#949494"/><circle cx="5.5" cy="11.5" r="1.25" style="fill:#949494"/><circle cx="9.5" cy="11.5" r="1.25" style="fill:#949494"/></svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polygon points="0.5 13.5 7.5 1.5 14.5 13.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polygon points="4 7.5 11 7.5 7.5 13.5" style="fill:#009688;stroke:#009688;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="0.5" y="10.5" width="14" height="4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polygon points="9 0.5 4.5 5.5 7.5 5.5 6 9 10.5 4 7.5 4" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@@ -0,0 +1,242 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_frame.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="16"
inkscape:cx="11.6875"
inkscape:cy="6.28125"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><g
id="g5-0"
transform="translate(6.4998844,6.0000382)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2"
style="fill:#4e2ca6;fill-opacity:1;fill-rule:nonzero" /></g><g
id="g5-0-8"
transform="matrix(-1,0,0,1,33.44613,6.0000382)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1-3"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2-5"
style="fill:#ff2323;fill-opacity:1" /></g><g
id="g5-0-8-2"
transform="rotate(180,16.723064,16.999981)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1-3-0"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2-5-1"
style="fill:#ff2323;fill-opacity:1" /></g><g
id="g5-0-8-2-4"
transform="matrix(1,0,0,-1,6.4998854,33.999963)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1-3-0-0"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2-5-1-6"
style="fill:#ff2323;fill-opacity:1" /></g><path
id="path13"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.407813 A 0.490625,0.490625 45 0 0 11.463281,17.5 h 0.0094 a 0.4999428,0.4999428 135 0 0 0.499943,-0.499943 V 15.601562 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.255468 a 0.4984375,0.4984375 135 0 0 0.498438,-0.498437 v -0.0018 A 0.4997516,0.4997516 45 0 0 17.472904,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect15" /><path
id="path16"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.439063 A 0.459375,0.459375 45 0 0 11.432031,17.5 h 0.04066 a 0.499968,0.499968 135 0 0 0.499968,-0.499968 l 0,-1.39847 V 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.271094 a 0.4828125,0.4828125 135 0 0 0.482812,-0.482812 V 11.00584 A 0.50584,0.50584 45 0 0 17.466816,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect17"
transform="matrix(-1,0,0,1,39.939926,2.52e-5)" /><path
id="path17"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.439063 A 0.459375,0.459375 45 0 0 11.432031,17.5 h 0.04066 a 0.499968,0.499968 135 0 0 0.499968,-0.499968 l 0,-1.39847 V 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.271094 a 0.4828125,0.4828125 135 0 0 0.482812,-0.482812 V 11.00584 A 0.50584,0.50584 45 0 0 17.466816,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect19"
transform="rotate(180,19.972885,20.000096)" /><path
id="path19"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.439063 A 0.459375,0.459375 45 0 0 11.432031,17.5 h 0.04066 a 0.499968,0.499968 135 0 0 0.499968,-0.499968 l 0,-1.39847 V 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.271094 a 0.4828125,0.4828125 135 0 0 0.482812,-0.482812 V 11.00584 A 0.50584,0.50584 45 0 0 17.466816,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect21"
transform="matrix(1,0,0,-1,0.0060898,39.99995)" /></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,387 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_join.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="18.3125"
inkscape:cy="22.328125"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
id="rect35"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(0.294922,2.953125)" /><path
id="rect35-3"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35-7"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(9.345703,2.953125)" /></svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="4" height="4" style="fill:#009688"/><rect x="5.5" y="1.5" width="4" height="4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><rect x="1.5" y="5.5" width="4" height="4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><rect x="5.5" y="5.5" width="4" height="4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><rect x="9.5" y="5.5" width="4" height="4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><rect x="5.5" y="9.5" width="4" height="4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 755 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="2.5" cy="7.5" r="2" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="4.3" y1="6.5" x2="11.5" y2="1.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="4.3" y1="8.5" x2="11.5" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><rect x="11.5" y="1.5" width="3" height="12" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 562 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><line x1="7.5" y1="1.5" x2="7.5" y2="12" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polyline points="3.5 8.5 7.5 12.5 11.5 8.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><line x1="7.5" y1="3" x2="7.5" y2="13.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polyline points="3.5 6.5 7.5 2.5 11.5 6.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polyline points="0.5 7.5 0.5 14.5 14.5 14.5 14.5 7.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M0.5 7.5c1.2-3 2.3-3 3.5 0s2.3 3 3.5 0 2.3-3 3.5 0 2.3 3 3.5 0" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_snap.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="45.254834"
inkscape:cx="14.363106"
inkscape:cy="12.827359"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
d="M 20.422985,9.2501548 V 29.968905 H 28.78236 A 1.015625,1.015625 135 0 0 29.797985,28.95328 V 9.2501548 A 1.015625,1.015625 45 0 0 28.78236,8.2345298 h -7.34375 a 1.015625,1.015625 135 0 0 -1.015625,1.015625 z m 1.015625,-0.015625 h 7.34375 V 28.968905 h -7.34375 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path23"
inkscape:path-effect="#path-effect24"
inkscape:original-d="M 20.422985,8.2345298 V 29.968905 h 9.375 V 8.2345298 Z m 1.015625,1 h 7.34375 V 28.968905 h -7.34375 z" /><path
d="m 11.047985,11.324808 v 19.616307 a 1.059034,1.059034 45 0 0 1.059034,1.059034 h 7.256932 a 1.059034,1.059034 135 0 0 1.059034,-1.059034 V 10.265774 h -8.315966 a 1.059034,1.059034 135 0 0 -1.059034,1.059034 z m 1.015625,-0.05903 h 7.34375 v 19.734375 h -7.34375 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path23-8"
inkscape:path-effect="#path-effect25"
inkscape:original-d="m 11.047985,10.265774 v 21.734375 h 9.375 V 10.265774 Z m 1.015625,1 h 7.34375 v 19.734375 h -7.34375 z" /><path
id="use25"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="M 21.439453 8.234375 A 1.015625 1.015625 0 0 0 20.423828 9.25 L 20.423828 10.265625 L 12.107422 10.265625 A 1.059034 1.059034 0 0 0 11.048828 11.324219 L 11.048828 30.941406 A 1.059034 1.059034 0 0 0 12.107422 32 L 19.363281 32 A 1.059034 1.059034 0 0 0 20.423828 30.941406 L 20.423828 29.96875 L 28.783203 29.96875 A 1.015625 1.015625 0 0 0 29.798828 28.953125 L 29.798828 9.25 A 1.015625 1.015625 0 0 0 28.783203 8.234375 L 21.439453 8.234375 z M 21.439453 9.234375 L 28.783203 9.234375 L 28.783203 28.96875 L 21.439453 28.96875 L 21.439453 9.234375 z M 12.064453 11.265625 L 19.408203 11.265625 L 19.408203 31 L 12.064453 31 L 12.064453 11.265625 z " /></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polygon points="0.5 14.5 1.5 14.5 5.5 7.5 1.5 0.5 0.5 0.5 0.5 14.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><rect x="7" width="1" height="3" style="fill:#009688"/><rect x="7" y="4" width="1" height="1" style="fill:#009688"/><rect x="7" y="6" width="1" height="3" style="fill:#009688"/><rect x="7" y="10" width="1" height="1" style="fill:#009688"/><rect x="7" y="12" width="1" height="3" style="fill:#009688"/><polygon points="9.5 14.5 10.5 14.5 14.5 7.5 10.5 0.5 9.5 0.5 9.5 14.5" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 686 B

View File

@@ -0,0 +1,387 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_unjoin.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="18.3125"
inkscape:cy="22.328125"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
id="rect35"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(-1.142612,2.9530855)" /><path
id="rect35-3"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35-7"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(10.783203,2.9530855)" /></svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="5.5" width="4" height="4" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><rect x="6" y="5.5" width="4" height="4" fill="none" stroke="#009688" stroke-linecap="round" stroke-linejoin="round"/><rect x="11" y="5.5" width="4" height="4" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12.5h12" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1 1.6"/></svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="1.5" y="1.5" width="13" height="13" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><path d="M1.5 10c2-3 3.5-3 5 0s3 3 5 0c1-1.5 2-1.5 3-.5" fill="none" stroke="#009688" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6.5h6a3.5 3.5 0 0 1 0 7H6" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><polyline points="6.5 4 4 6.5 6.5 9" fill="none" stroke="#009688" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 320 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="2.5" y="2.5" width="11" height="11" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><line x1="1" y1="15" x2="15" y2="1" fill="none" stroke="#009688" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.5 5V1.5H5M11 1.5h3.5V5M14.5 11v3.5H11M5 14.5H1.5V11" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><rect x="5.5" y="5.5" width="5" height="5" fill="none" stroke="#009688" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="1.5" y="4.5" width="6" height="7" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><rect x="7.5" y="4.5" width="7" height="7" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><line x1="7.5" y1="4.5" x2="7.5" y2="11.5" stroke="#009688" stroke-width="1.8"/></svg>

After

Width:  |  Height:  |  Size: 410 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polyline points="2.5 12.5 6 8 9.5 9.5 13 3.5" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><circle cx="2.5" cy="12.5" r="1.7" fill="#009688"/><circle cx="13" cy="3.5" r="1.7" fill="#009688"/></svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polygon points="1.5 13.5 7.5 1.5 13.5 13.5" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><line x1="7.5" y1="1.5" x2="7.5" y2="13.5" stroke="#009688" stroke-width="1.6" stroke-linecap="round" stroke-dasharray="2 1.7"/></svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polygon points="1.5 13.5 3.5 2.5 13.5 1.5 12.5 12.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="3.5" y1="2.5" x2="12.5" y2="12.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="3.5" y1="2.5" x2="13.5" y2="1.5" style="fill:none;stroke:#009688;stroke-width:2;stroke-linecap:round"/></svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polygon points="0.5 1.5 6.5 0.5 7.5 6.5 1.5 7.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polygon points="9.5 1.5 14.5 2.5 11.5 6.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polygon points="1.5 9.5 5.5 9 6 14.5 0.5 13.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><polygon points="8.5 8.5 14.5 7.5 13.5 14.5 7.5 13.5" style="fill:#009688;stroke:#009688;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><polygon points="1.5 13.5 3.5 2.5 13.5 1.5 12.5 12.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><line x1="3.5" y1="2.5" x2="12.5" y2="12.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><circle cx="1.5" cy="13.5" r="1" style="fill:#949494"/><circle cx="12.5" cy="12.5" r="1" style="fill:#949494"/><circle cx="3.5" cy="2.5" r="1" style="fill:#949494"/><circle cx="13.5" cy="1.5" r="1.8" style="fill:#009688"/></svg>

After

Width:  |  Height:  |  Size: 564 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 2v5.5a5 5 0 0 0 10 0V2" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.4 2v5.5a2.6 2.6 0 0 0 5.2 0V2" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 2h2.4M10.6 2H13" stroke="#009688" stroke-width="2" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 410 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="0.5" y="4.5" width="5.5" height="7" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><rect x="10" y="4.5" width="5.5" height="7" fill="none" stroke="#949494" stroke-linecap="round" stroke-linejoin="round"/><line x1="8" y1="3" x2="8" y2="13" stroke="#009688" stroke-dasharray="1.6 1.6"/></svg>

After

Width:  |  Height:  |  Size: 413 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="0.5" y="0.5" width="14" height="14" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M0.5 7.5h14M7.5 0.5v14M0.5 0.5l14 14M14.5 0.5l-14 14" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="toolbar_big_brush.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17" />
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="5.6568543"
inkscape:cx="-13.081475"
inkscape:cy="98.994949"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<path
id="path30"
style="fill:#009688;fill-opacity:1;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="M 30.109375 6.9648438 A 1.8710937 1.8710937 0 0 0 28.705078 7.5996094 A 1.8710937 1.8710937 0 0 0 28.591797 7.7421875 L 18.103516 20.615234 A 6.1070809 6.1070809 0 0 0 17.226562 20.552734 A 6.1070809 6.1070809 0 0 0 11.513672 24.503906 A 6.1070809 6.1070809 0 0 0 11.121094 26.658203 A 6.1070809 6.1070809 0 0 0 11.121094 26.757812 A 4.4931102 4.4931102 0 0 1 11.15625 27.3125 A 4.4931102 4.4931102 0 0 1 7.7890625 31.662109 A 4.4931102 4.4931102 0 0 1 7.5839844 31.708984 A 0.55609465 0.55242634 0 0 0 7.2558594 32.212891 A 0.55609465 0.55242634 0 0 0 7.8105469 32.765625 A 0.55609465 0.55242634 0 0 0 7.8125 32.765625 L 7.9570312 32.765625 L 17.224609 32.765625 L 17.226562 32.765625 A 6.1070809 6.1070809 0 0 0 23.333984 26.658203 A 6.1070809 6.1070809 0 0 0 22.792969 24.144531 L 31.666016 9.875 A 1.8710937 1.8710937 0 0 1 31.638672 9.9101562 A 1.8710937 1.8710937 0 0 0 31.699219 9.8242188 A 1.8710937 1.8710937 0 0 0 31.980469 8.8359375 A 1.8710937 1.8710937 0 0 0 31.724609 7.890625 A 1.8710937 1.8710937 0 0 0 30.480469 7.0019531 A 1.8710937 1.8710937 0 0 0 30.109375 6.9648438 z M 28.525391 7.8476562 A 1.8710937 1.8710937 0 0 0 28.472656 7.9335938 A 1.8710937 1.8710937 0 0 1 28.478516 7.9199219 A 1.8710937 1.8710937 0 0 1 28.525391 7.8476562 z M 30.109375 7.9570312 A 0.87817276 0.87817276 0 0 1 30.988281 8.8359375 A 0.87817276 0.87817276 0 0 1 30.90625 9.2050781 A 0.87817276 0.87817276 0 0 1 30.791016 9.390625 L 29.974609 10.703125 L 22.230469 23.15625 A 6.1070809 6.1070809 0 0 0 19.183594 20.873047 L 28.388672 9.5703125 L 29.359375 8.3808594 A 0.87817276 0.87817276 0 0 1 29.511719 8.1933594 A 0.87817276 0.87817276 0 0 1 30.109375 7.9570312 z M 28.457031 7.9628906 A 1.8710937 1.8710937 0 0 0 28.373047 8.1425781 A 1.8710937 1.8710937 0 0 1 28.457031 7.9628906 z M 28.300781 8.359375 A 1.8710937 1.8710937 0 0 0 28.263672 8.5507812 A 1.8710937 1.8710937 0 0 1 28.298828 8.3613281 A 1.8710937 1.8710937 0 0 1 28.300781 8.359375 z M 17.226562 21.65625 A 5.0022264 5.0022264 0 0 1 22.230469 26.658203 A 5.0022264 5.0022264 0 0 1 17.226562 31.662109 L 13.724609 31.662109 L 10.199219 31.662109 A 5.6057596 5.6057596 0 0 0 11.822266 29.501953 A 5.6057596 5.6057596 0 0 0 12.267578 27.3125 A 5.0022264 5.0022264 0 0 1 12.224609 26.658203 A 5.0022264 5.0022264 0 0 1 12.224609 26.621094 A 5.0022264 5.0022264 0 0 1 17.226562 21.65625 z " />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="toolbar_face.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="22.627417"
inkscape:cx="23.113553"
inkscape:cy="18.318485"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path42"
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers;fill-opacity:1"
d="M 19.58431,7.9401469 19.519531,8.0820312 9.1699219,30.736328 a 0.61329334,0.61329334 42.827026 0 0 0.828125,0.767578 l 0,0 20.7832031,-7.320312 0.0019,-6.61e-4 a 0.7874119,0.7874119 107.77086 0 0 0.382891,-1.194652 L 20.714844,8.0449219 20.578195,7.8494958 A 0.57476577,0.57476577 174.78854 0 0 19.58431,7.9401469 Z M 20.232422,9.1015625 30.1875,23.332031 10.576172,30.240234 Z"
inkscape:path-effect="#path-effect48"
inkscape:original-d="M 20.001953,7.0253906 19.519531,8.0820312 9.1699219,30.736328 8.5917969,32 9.9980469,31.503906 30.78125,24.183594 31.761719,23.837891 31.166016,22.988281 20.714844,8.0449219 Z M 20.232422,9.1015625 30.1875,23.332031 10.576172,30.240234 Z" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="toolbar_texture_displacement.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17" />
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="27.578125"
inkscape:cy="27.671875"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<g
id="g17"
transform="translate(0.69176086,0.249987)">
<path
d="m 27.659236,35.176022 h -4.399987 c -0.508996,0 -0.844996,0 -0.954999,-0.394015 a 0.59299998,0.59299998 0 0 1 0.36,-0.669997 2.3,2.3 0 0 0 1.2,-2.188989 3.253,3.253 0 1 1 3.800012,3.205984 z M 24.12125,34.175997 h 2.988 a 2.253,2.253 0 1 0 -2.252999,-2.252976 3.445,3.445 0 0 1 -0.735001,2.252976 z"
style="fill:#009688;stroke-width:0.999999"
id="path1-6" />
<path
d="m 30.281245,30.538996 -0.846009,-0.534993 4.364031,-6.899981 A 0.222,0.222 0 0 0 33.439456,22.84588 l -5.4,6.644032 -0.776013,-0.63103 5.400001,-6.643994 a 1.222,1.222 0 0 1 1.981984,1.422992 z"
style="fill:#009688;stroke-width:0.999999"
id="path2-0" />
<path
d="m 33.281245,35.250026 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 a 0.49999998,0.49999998 0 0 0 0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.281251,35.250026 H 5.28125 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 5.750019 A 1.5,1.5 0 0 1 5.28125,4.25 h 27.999995 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 5.750019 A 0.49999998,0.49999998 0 0 0 33.281245,5.250025 H 5.28125 A 0.49999998,0.49999998 0 0 0 4.781253,5.750019 V 33.750007 A 0.49999998,0.49999998 0 0 0 5.28125,34.25 h 14.000001 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path4-6" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-1"
width="39.379108"
height="0.99609375"
x="-19.909107"
y="27.094706"
ry="0.49804688"
transform="rotate(-45)" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2"
width="27.566595"
height="0.99609375"
x="7.9031763"
y="-0.27849278"
ry="0.49487707"
transform="rotate(45)" />
<g
id="g7"
transform="matrix(3.7795276,0,0,3.7795276,-350.69182,-427.25001)">
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2"
width="2.8453603"
height="0.2635498"
x="150.08304"
y="13.289762"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2"
width="1.7498204"
height="0.2635498"
x="12.763216"
y="-149.44461"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2-2"
width="0.82876241"
height="0.27337581"
x="13.654455"
y="-148.49495"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6"
width="2.5927844"
height="0.25224927"
x="150.38367"
y="12.286384"
ry="0.12612464"
transform="rotate(45)"
rx="0.12612464" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6-1"
width="1.9647908"
height="0.2635498"
x="151.01166"
y="11.21043"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
</g>
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-8"
width="10.754118"
height="0.99609375"
x="-10.211728"
y="-31.661213"
ry="0.49804688"
transform="rotate(135)"
rx="0.49804688" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-2-9"
width="6.6134944"
height="0.99609375"
x="-33.794979"
y="12.679931"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-2-2-3"
width="3.1323304"
height="1.0332314"
x="-30.282875"
y="16.213961"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
<g
id="g7-1"
transform="matrix(0,-3.7795276,3.7795276,0,-427.55829,389.70233)">
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-9"
width="2.8453603"
height="0.2635498"
x="150.08304"
y="13.289762"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2-8"
width="1.7498204"
height="0.2635498"
x="12.763216"
y="-149.44461"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2-2-6"
width="0.82876241"
height="0.27337581"
x="13.654455"
y="-148.49495"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6-5"
width="2.5927844"
height="0.25224927"
x="150.38367"
y="12.286384"
ry="0.12612464"
transform="rotate(45)"
rx="0.12612464" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6-1-0"
width="1.9647908"
height="0.2635498"
x="151.01166"
y="11.21043"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
</g>
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-9-4"
width="9.0822706"
height="0.99609375"
x="-36.379971"
y="-4.5927153"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-6-5-5"
width="9.7995005"
height="0.95338309"
x="-36.915619"
y="-8.385005"
ry="0.47669154"
transform="rotate(-135)"
rx="0.47669154" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-6-1-0-0"
width="7.425981"
height="0.99609375"
x="-34.54211"
y="-12.451604"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,257 @@
#version 110
// See resources/shaders/140/texture_displacement_bump.fs for full documentation; this is the
// GLSL 1.10 compatibility variant (same logic, older syntax).
#define INTENSITY_CORRECTION 0.6
#define PARALLAX_STEPS 24
#define H_AT(uv) texture2D(height_tex, uv).r
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform vec4 uniform_color;
// The printable palette, in **CIELAB** as well as RGB, and how many entries are real. Lab because the
// match has to be perceptual - the same reason the CPU side uses CIEDE2000 - and converting the
// palette once on the CPU is what lets the fragment shader match with a plain squared distance.
// Count 0 means nothing is colouring, and every fragment falls back to uniform_color as before.
uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
uniform mat3 view_normal_matrix;
uniform sampler2D height_tex;
uniform vec2 height_tex_texel;
uniform float depth_mm;
uniform float tiling_scale;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
uniform bool use_vertex_uv;
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
// 140 variant. Identity when nothing is dragged.
uniform vec4 island_delta_lin;
uniform vec2 island_delta_tr;
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float weight;
varying float island_active;
varying vec2 vertex_uv;
void projection_axes(vec3 n, out vec3 t, out vec3 b)
{
vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
t = vec3(0.0, 1.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else { // planar = p.xy
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 1.0, 0.0);
}
}
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
}
// sRGB -> CIELAB, matching slic3r/Utils/ColorSpaceConvert's RGB2Lab so this picks the same entry the
// bake does.
vec3 srgb_to_lab(vec3 c)
{
vec3 v = vec3(c.r > 0.04045 ? pow((c.r + 0.055) / 1.055, 2.4) : c.r / 12.92,
c.g > 0.04045 ? pow((c.g + 0.055) / 1.055, 2.4) : c.g / 12.92,
c.b > 0.04045 ? pow((c.b + 0.055) / 1.055, 2.4) : c.b / 12.92);
vec3 xyz = vec3(dot(v, vec3(0.4124, 0.3576, 0.1805)) / 0.95047,
dot(v, vec3(0.2126, 0.7152, 0.0722)),
dot(v, vec3(0.0193, 0.1192, 0.9505)) / 1.08883);
vec3 f = vec3(xyz.x > 0.008856 ? pow(xyz.x, 1.0 / 3.0) : (7.787 * xyz.x) + 16.0 / 116.0,
xyz.y > 0.008856 ? pow(xyz.y, 1.0 / 3.0) : (7.787 * xyz.y) + 16.0 / 116.0,
xyz.z > 0.008856 ? pow(xyz.z, 1.0 / 3.0) : (7.787 * xyz.z) + 16.0 / 116.0);
return vec3(116.0 * f.y - 16.0, 500.0 * (f.x - f.y), 200.0 * (f.y - f.z));
}
// Nearest printable colour to a sampled one. Quantizing per *fragment* rather than per facet is the
// whole point of this path: it shows the image at the texture's resolution instead of the mesh's,
// which is what you need while choosing a texture and placing it. The Normal view is where the
// facet-resolution truth - what actually bakes - is shown.
//
// Squared distance in Lab (CIE76) rather than the CPU's CIEDE2000: the two agree except on near-ties,
// and CIEDE2000 per fragment across 64 entries is not worth its cost in a preview.
vec3 quantize_to_palette(vec3 rgb)
{
vec3 lab = srgb_to_lab(rgb);
int best = 0;
float bd = 1.0e20;
for (int i = 0; i < 64; ++i) {
if (i >= palette_count)
break;
vec3 d = lab - palette_lab[i];
float d2 = dot(d, d);
if (d2 < bd) {
bd = d2;
best = i;
}
}
return palette_rgb[best];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
// World millimetres throughout, like the bake - see the 140 variant.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
if (volume_mirrored)
triangle_normal = -triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
// has to follow it exactly, or the colour would slide off the relief as the camera orbits.
vec2 color_uv = vec2(0.0);
bool have_uv = false;
if (use_vertex_uv) {
// Mikkelsen surface-gradient bump; see the 140 variant for the full rationale. Scale-exact
// for a conformal LSCM map (no global 1/tiling assumption), and gated by the paint weight
// via a multiply so the branch stays uniform (use_vertex_uv is a uniform).
vec2 uv = (island_active > 0.5)
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv;
color_uv = uv;
have_uv = true;
float h = texture2D(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 sigmaS = dFdx(world_pos.xyz);
vec3 sigmaT = dFdy(world_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal);
vec3 R2 = cross(triangle_normal, sigmaS);
float det = dot(sigmaS, R1);
float dHdx = k * dFdx(h);
float dHdy = k * dFdy(h);
if (abs(det) > 1e-12)
triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det);
} else if (weight > 0.0) {
vec3 t, b;
projection_axes(triangle_normal, t, b);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
// where the surface sits *below* the undisplaced one.
float h_end_a = amp * (0.0 - midlevel);
float h_end_b = amp * (1.0 - midlevel);
float h_hi = max(h_end_a, h_end_b);
float h_lo = min(h_end_a, h_end_b);
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
// quantising to the step size, and so what keeps the step count affordable.
uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0));
break;
}
prev_uv = cur_uv;
prev_gap = gap;
}
}
color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true;
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
float hD = texture2D(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0);
vec2 intensity = vec2(0.0);
intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE;
vec3 position = (view_model_matrix * model_pos).xyz;
intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS);
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = quantize_to_palette(texture2D(color_tex, color_uv).rgb);
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}

View File

@@ -0,0 +1,34 @@
#version 110
// See resources/shaders/140/texture_displacement_bump.vs for full documentation; this is the
// GLSL 1.10 compatibility variant.
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
uniform vec2 z_range;
uniform vec4 clipping_plane;
attribute vec3 v_position;
attribute vec3 v_normal; // .x = paint weight (0/1); .y = 1 for the dragged island's vertices
attribute vec2 v_tex_coord; // precomputed texture uv, used only when use_vertex_uv is set
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float weight;
varying float island_active;
varying vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
weight = v_normal.x;
island_active = v_normal.y;
vertex_uv = v_tex_coord;
}

View File

@@ -0,0 +1,73 @@
#version 110
// See resources/shaders/140/texture_displacement_uvcheck.fs; GLSL 1.10 compatibility variant.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform mat3 view_normal_matrix;
uniform bool volume_mirrored;
uniform int mode;
uniform float checker_freq;
uniform float tiling_scale;
uniform vec3 tex_anchor; // the volume's origin in world space
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float distortion;
varying vec2 vertex_uv;
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
}
vec3 heatmap(float t)
{
t = clamp(t, 0.0, 1.0);
return clamp(vec3(1.5 - abs(4.0 * t - 3.0),
1.5 - abs(4.0 * t - 2.0),
1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
// World space anchored at the volume's origin, like the bake and the bump preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor;
if (volume_mirrored)
triangle_normal = -triangle_normal;
vec3 base;
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float intensity = INTENSITY_AMBIENT + max(dot(eye_normal, LIGHT_TOP_DIR), 0.0) * LIGHT_TOP_DIFFUSE
+ max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0) * LIGHT_FRONT_DIFFUSE;
gl_FragColor = vec4(base * intensity, 1.0);
}

View File

@@ -0,0 +1,31 @@
#version 110
// See resources/shaders/140/texture_displacement_uvcheck.vs; GLSL 1.10 compatibility variant.
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
uniform vec2 z_range;
uniform vec4 clipping_plane;
attribute vec3 v_position;
attribute vec3 v_normal; // .x = per-vertex uv distortion
attribute vec2 v_tex_coord; // precomputed texture uv, used only when use_vertex_uv is set
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float distortion;
varying vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
distortion = v_normal.x;
vertex_uv = v_tex_coord;
}

View File

@@ -0,0 +1,343 @@
#version 140
// Fast, geometry-free preview of texture displacement: perturbs the *shading* normal from the
// height texture's local gradient (a bump map), faded out by the per-vertex paint weight. The
// true, exact result is what "Bake" produces via libslic3r/TextureDisplacement.cpp on the CPU.
//
// The bake displaces each surface point along its normal by H = +/- depth_mm * (h(uv) - midlevel),
// with uv from the layer's projection. The perturbed normal is the analytic
//
// N' = normalize(N - (dH/da) * T - (dH/db) * B)
//
// over any orthonormal surface tangent pair (T, B), where the two slopes are real mm-per-mm
// derivatives. Two things have to be right for the preview's apparent depth to match the bake's:
// the tangent frame the gradient is expressed in, and the uv->mm scale that turns a texel
// difference into a slope. Getting the scale wrong is a uniform flattening (a raw texel difference
// is dh over one texel step, not over one mm); getting the frame wrong tilts the bump along the
// wrong axes.
//
// Two projection paths:
// * Triplanar (use_vertex_uv = 0): uv and the tangent axes are derived in-shader from the dominant
// normal axis, mirroring libslic3r's project_planar()/apply_uv_transform(), and the slope is
// formed analytically (there is a closed-form uv, so 1 uv unit is exactly tiling_scale mm). This
// path also runs a parallax step before shading, see below.
//
// Parallax. A pure bump map perturbs shading only, so the pattern is welded to the base surface: it
// does not shift as the camera orbits and it does not get any deeper as depth_mm grows, which is
// exactly when the preview stops reading as real geometry. The triplanar path therefore shades at the
// point the *displaced* surface would show at this pixel rather than at the pixel's own base position.
//
// Two cheaper formulations were tried first and both are wrong here, which is worth recording:
// * Solving Q = P + V * (H(Q) / dot(V, n)) by fixed-point iteration. Geometrically exact, but the
// divisor goes to zero edge-on, and an unbounded step is not a small error - the sample lands a
// large fraction of a tile away and the iteration oscillates instead of converging. It reads as a
// *second, flat copy* of the pattern ghosted over the real one. Clamping the step to one tile does
// not help either: a tile-sized shift lands on the neighbouring tile, which is the same pattern.
// * Offset limiting (Welsh): step along the tangential part of V, whose length caps the shift at one
// depth. Stable and cheap, but it understates parallax by exactly the factor that matters - the
// relief still flattens as soon as the camera tilts, which is the complaint it was meant to fix.
//
// So this ray-marches instead (parallax occlusion mapping). A point at ray parameter s, i.e. P + V * s,
// sits at height s * dot(V, n) above the undisplaced surface. The displaced surface lives in a shell
// between the extreme values of amp * (h - midlevel); the march starts at the top of that shell, where
// the ray is outside the surface by construction, and steps inward until the ray height falls below the
// sampled height. That crossing *is* the visible point - no divergence, no ghosting, and parallax stays
// correct at any angle. The hit is interpolated between the last two samples, which is what keeps
// PARALLAX_STEPS low enough to afford. The march is skipped when sweeping the shell would move the
// sample point less than half a texel (the head-on case), so the common view pays almost nothing.
//
// The gradient/shading below is evaluated at the resulting uv, so the relief both slides correctly
// under camera motion and visibly deepens with depth_mm. What it still cannot do is change the
// model's silhouette or cast shadows; for that, switch the View row to Normal.
// * Precomputed uv (use_vertex_uv = 1, used for LSCM): uv comes per-vertex from the CPU (the LSCM
// unwrap with island placement + tiling/rotation/offset already folded in), and the perturbed
// normal is built with Mikkelsen's method -- the surface gradient taken straight from the
// screen-space derivatives of the sampled height and position. This makes no uv->mm scale
// assumption, which matters because an LSCM map is conformal, not isometric: the local mm-per-uv
// varies across the chart, so a single global 1/tiling factor (what an earlier version used) got
// the apparent depth wrong. 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.
#define INTENSITY_CORRECTION 0.6
#define PARALLAX_STEPS 24
// Explicit LOD: the march samples inside non-uniform control flow, where implicit
// derivatives are undefined.
#define H_AT(uv) textureLod(height_tex, uv, 0.0).r
// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31)
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
// normalized values for (1./1.43, 0.2/1.43, 1./1.43)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform vec4 uniform_color;
// The printable palette, in **CIELAB** as well as RGB, and how many entries are real. Lab because the
// match has to be perceptual - the same reason the CPU side uses CIEDE2000 - and converting the
// palette once on the CPU is what lets the fragment shader match with a plain squared distance.
// Count 0 means nothing is colouring, and every fragment falls back to uniform_color as before.
uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
uniform mat3 view_normal_matrix;
uniform sampler2D height_tex;
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
uniform float depth_mm;
uniform float tiling_scale;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
// the island currently being dragged in the UV editor (island_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
uniform vec4 island_delta_lin;
uniform vec2 island_delta_tr;
in vec3 clipping_planes_dots;
in vec4 model_pos;
in vec4 world_pos;
in float weight;
in float island_active;
in vec2 vertex_uv;
out vec4 out_color;
// The two world-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
// along b.
void projection_axes(vec3 n, out vec3 t, out vec3 b)
{
vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
t = vec3(0.0, 1.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else { // planar = p.xy
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 1.0, 0.0);
}
}
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
}
// sRGB -> CIELAB, matching slic3r/Utils/ColorSpaceConvert's RGB2Lab so this picks the same entry the
// bake does.
vec3 srgb_to_lab(vec3 c)
{
vec3 v = vec3(c.r > 0.04045 ? pow((c.r + 0.055) / 1.055, 2.4) : c.r / 12.92,
c.g > 0.04045 ? pow((c.g + 0.055) / 1.055, 2.4) : c.g / 12.92,
c.b > 0.04045 ? pow((c.b + 0.055) / 1.055, 2.4) : c.b / 12.92);
vec3 xyz = vec3(dot(v, vec3(0.4124, 0.3576, 0.1805)) / 0.95047,
dot(v, vec3(0.2126, 0.7152, 0.0722)),
dot(v, vec3(0.0193, 0.1192, 0.9505)) / 1.08883);
vec3 f = vec3(xyz.x > 0.008856 ? pow(xyz.x, 1.0 / 3.0) : (7.787 * xyz.x) + 16.0 / 116.0,
xyz.y > 0.008856 ? pow(xyz.y, 1.0 / 3.0) : (7.787 * xyz.y) + 16.0 / 116.0,
xyz.z > 0.008856 ? pow(xyz.z, 1.0 / 3.0) : (7.787 * xyz.z) + 16.0 / 116.0);
return vec3(116.0 * f.y - 16.0, 500.0 * (f.x - f.y), 200.0 * (f.y - f.z));
}
// Nearest printable colour to a sampled one. Quantizing per *fragment* rather than per facet is the
// whole point of this path: it shows the image at the texture's resolution instead of the mesh's,
// which is what you need while choosing a texture and placing it. The Normal view is where the
// facet-resolution truth - what actually bakes - is shown.
//
// Squared distance in Lab (CIE76) rather than the CPU's CIEDE2000: the two agree except on near-ties,
// and CIEDE2000 per fragment across 64 entries is not worth its cost in a preview.
vec3 quantize_to_palette(vec3 rgb)
{
vec3 lab = srgb_to_lab(rgb);
int best = 0;
float bd = 1.0e20;
for (int i = 0; i < 64; ++i) {
if (i >= palette_count)
break;
vec3 d = lab - palette_lab[i];
float d2 = dot(d, d);
if (d2 < bd) {
bd = d2;
best = i;
}
}
return palette_rgb[best];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
// Everything below runs in world millimetres, like the bake: a tile is tiling_scale mm on the
// printed part whatever the instance's scale or rotation, so the preview has to project from the
// world position and perturb the world normal.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
if (volume_mirrored)
triangle_normal = -triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
// has to follow it exactly, or the colour would slide off the relief as the camera orbits.
vec2 color_uv = vec2(0.0);
bool have_uv = false;
if (use_vertex_uv) {
// 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,
// 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.
//
// use_vertex_uv is a uniform, so this whole branch is uniform control flow and the texture
// derivatives are well defined; the paint weight gates the result by a plain multiply (k)
// rather than a per-fragment branch, keeping it that way.
// The dragged island's uv rides a uniform affine so its bump moves without a rebuild; every
// other vertex (island_active == 0) samples its baked uv unchanged.
vec2 uv = (island_active > 0.5)
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv;
color_uv = uv;
have_uv = true;
float h = texture(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 sigmaS = dFdx(world_pos.xyz);
vec3 sigmaT = dFdy(world_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal);
vec3 R2 = cross(triangle_normal, sigmaS);
float det = dot(sigmaS, R1);
float dHdx = k * dFdx(h);
float dHdy = k * dFdy(h);
if (abs(det) > 1e-12)
triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det);
} else if (weight > 0.0) {
// Triplanar path: uv and the tangent axes are reconstructed in-shader from the dominant
// normal component (see header). The gradient is expressed analytically because there is a
// closed-form uv here, unlike the LSCM case.
vec3 t, b;
projection_axes(triangle_normal, t, b);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
// where the surface sits *below* the undisplaced one.
float h_end_a = amp * (0.0 - midlevel);
float h_end_b = amp * (1.0 - midlevel);
float h_hi = max(h_end_a, h_end_b);
float h_lo = min(h_end_a, h_end_b);
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
// quantising to the step size, and so what keeps the step count affordable.
uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0));
break;
}
prev_uv = cur_uv;
prev_gap = gap;
}
}
color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true;
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
float hD = texture(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
float hU = texture(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
// Central difference, per uv unit (not per texel).
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
// gradient into a real surface slope.
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
// gradient back into the axes' frame.
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0);
vec2 intensity = vec2(0.0);
intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE;
vec3 position = (view_model_matrix * model_pos).xyz;
intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS);
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = quantize_to_palette(texture(color_tex, color_uv).rgb);
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}

View File

@@ -0,0 +1,44 @@
#version 140
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
// Clipping plane - general orientation. Used by the SLA gizmo.
uniform vec4 clipping_plane;
in vec3 v_position;
// GLModel's P3N3T2 layout (position + normal + texcoord), reused so this mesh builds and renders
// like any other GLModel rather than needing a bespoke vertex buffer. The two spare channels carry
// what the bump preview actually needs per vertex:
// v_normal.x -- the active layer's paint weight, 0 (untouched) or 1 (painted).
// v_normal.y -- 1 for a vertex of the island currently being dragged in the UV editor, else 0.
// The fragment shader applies island_delta to those vertices' uv, so a UV drag is a
// single uniform update rather than a whole-mesh rebuild (like Adjust placement).
// v_tex_coord -- the precomputed texture uv for this vertex, valid only when use_vertex_uv is set
// (i.e. the LSCM projection, where uv can't be reconstructed in the shader). The
// triplanar path ignores it and projects in the fragment shader instead.
in vec3 v_normal;
in vec2 v_tex_coord;
out vec3 clipping_planes_dots;
out vec4 model_pos;
out vec4 world_pos;
out float weight;
out float island_active;
out vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
weight = v_normal.x;
island_active = v_normal.y;
vertex_uv = v_tex_coord;
}

View File

@@ -0,0 +1,86 @@
#version 140
// 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
// 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
// (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.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform mat3 view_normal_matrix;
uniform bool volume_mirrored;
uniform int mode; // 0 checker, 1 distortion
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
uniform float tiling_scale;
uniform vec3 tex_anchor; // the volume's origin in world space
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
in vec3 clipping_planes_dots;
in vec4 model_pos;
in vec4 world_pos;
in float distortion;
in vec2 vertex_uv;
out vec4 out_color;
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
}
// Blue -> cyan -> green -> yellow -> red over t in [0,1].
vec3 heatmap(float t)
{
t = clamp(t, 0.0, 1.0);
return clamp(vec3(1.5 - abs(4.0 * t - 3.0),
1.5 - abs(4.0 * t - 2.0),
1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
// World space anchored at the volume's origin, like the bake and the bump preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor;
if (volume_mirrored)
triangle_normal = -triangle_normal;
vec3 base;
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
// Two distinct greys, plus a faint tint on one set so orientation is readable at a glance.
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float intensity = INTENSITY_AMBIENT + max(dot(eye_normal, LIGHT_TOP_DIR), 0.0) * LIGHT_TOP_DIFFUSE
+ max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0) * LIGHT_FRONT_DIFFUSE;
out_color = vec4(base * intensity, 1.0);
}

View File

@@ -0,0 +1,36 @@
#version 140
// 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);
// only the distortion mode reads it.
// 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;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
uniform vec2 z_range;
uniform vec4 clipping_plane;
in vec3 v_position;
in vec3 v_normal;
in vec2 v_tex_coord;
out vec3 clipping_planes_dots;
out vec4 model_pos;
out vec4 world_pos;
out float distortion;
out vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
distortion = v_normal.x;
vertex_uv = v_tex_coord;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -462,6 +462,30 @@ set(lisbslic3r_sources
Tesselate.cpp
Tesselate.hpp
TextConfiguration.hpp
TextureDisplacement.cpp
TextureDisplacement.hpp
TextureBake/TextureBakeIndex.cpp
TextureBake/TextureBakeIndex.hpp
TextureBake/TextureBakeSubdivide.cpp
TextureBake/TextureBakeSubdivide.hpp
TextureBake/TextureBakeRegularize.cpp
TextureBake/TextureBakeRegularize.hpp
TextureBake/TextureBakeRelocate.cpp
TextureBake/TextureBakeRelocate.hpp
TextureBake/TextureBakeFlip.cpp
TextureBake/TextureBakeFlip.hpp
TextureBake/TextureBakeDebug.cpp
TextureBake/TextureBakeDebug.hpp
TextureBake/TextureBakeDisplace.cpp
TextureBake/TextureBakeDisplace.hpp
TextureBake/TextureBakeDecimate.cpp
TextureBake/TextureBakeDecimate.hpp
TextureBake/TextureBakeRepair.cpp
TextureBake/TextureBakeRepair.hpp
TextureBake/TextureBakePipeline.cpp
TextureBake/TextureBakePipeline.hpp
TextureBake/TextureBakeMesh.cpp
TextureBake/TextureBakeMesh.hpp
Thread.cpp
Thread.hpp
Time.cpp

View File

@@ -26,6 +26,13 @@
#include <CGAL/property_map.h>
#include <CGAL/boost/graph/copy_face_graph.h>
#include <CGAL/boost/graph/Face_filtered_graph.h>
// For parameterize_lscm()
#include <CGAL/Polygon_mesh_processing/border.h>
#include <CGAL/Polygon_mesh_processing/connected_components.h>
#include <CGAL/Polygon_mesh_processing/detect_features.h>
#include <CGAL/Surface_mesh_parameterization/Error_code.h>
#include <CGAL/Surface_mesh_parameterization/LSCM_parameterizer_3.h>
#include <CGAL/Surface_mesh_parameterization/parameterize.h>
// BBS: for boolean using mcut
#include "mcut/include/mcut/mcut.h"
@@ -249,6 +256,151 @@ indexed_triangle_set cgal_to_indexed_triangle_set(const CGALMesh &cgalmesh)
return cgal_to_indexed_triangle_set(cgalmesh.m);
}
// /////////////////////////////////////////////////////////////////////////////
// Isotropic remeshing
// /////////////////////////////////////////////////////////////////////////////
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
unsigned n_iterations, double sharp_angle_deg,
unsigned n_relaxation_steps)
{
if (mesh.indices.empty() || target_edge_length <= 0.0)
return mesh;
_EpicMesh cgal_mesh;
triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh);
if (cgal_mesh.is_empty() || cgal_mesh.number_of_faces() == 0)
return mesh;
// Surface_mesh::add_face() refuses any face that would make the mesh non-manifold and returns a
// null descriptor instead. Remeshing a mesh that silently lost faces that way produces holes in
// the output, so bail out and let the caller report it rather than hand back a punctured model.
if (cgal_mesh.number_of_faces() != mesh.indices.size())
return mesh;
using edge_descriptor = boost::graph_traits<_EpicMesh>::edge_descriptor;
try {
// Sharp edges and open borders are pinned before remeshing. Without that, the tangential
// relaxation pass slides vertices along the surface and rounds every hard feature off - a
// cube comes back with wobbly, eroded edges, which is the most visible way "remeshing does
// not work properly". protect_constraints() forbids splitting or collapsing them, but it
// requires each constrained edge to already be shorter than 4/3 * target, hence the split
// first (passing the map so the halves inherit the constraint). This mirrors CGAL's own
// isotropic_remeshing example.
auto ecm = cgal_mesh.add_property_map<edge_descriptor, bool>("e:is_constrained", false).first;
if (sharp_angle_deg > 0.0)
CGALProc::detect_sharp_edges(cgal_mesh, sharp_angle_deg, ecm);
for (edge_descriptor e : edges(cgal_mesh)) {
const auto h = halfedge(e, cgal_mesh);
if (is_border(h, cgal_mesh) || is_border(opposite(h, cgal_mesh), cgal_mesh))
put(ecm, e, true);
}
std::vector<edge_descriptor> constrained;
for (edge_descriptor e : edges(cgal_mesh))
if (get(ecm, e))
constrained.push_back(e);
if (!constrained.empty())
CGALProc::split_long_edges(constrained, target_edge_length, cgal_mesh,
CGALParams::edge_is_constrained_map(ecm));
CGALProc::isotropic_remeshing(faces(cgal_mesh), target_edge_length, cgal_mesh,
CGALParams::number_of_iterations(n_iterations)
.number_of_relaxation_steps(n_relaxation_steps)
.edge_is_constrained_map(ecm)
.protect_constraints(true));
} catch (const std::exception &) {
return mesh; // CGAL throws on some non-manifold / degenerate inputs; leave the mesh untouched
}
if (cgal_mesh.number_of_faces() == 0)
return mesh;
// isotropic_remeshing edits in place, and its edge collapses only *mark* vertices and faces as
// removed - the underlying arrays keep the holes until the garbage is collected. That matters
// because 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). Past the first collapse the two disagree, so every triangle
// points at the wrong vertices, and any descriptor beyond the live vertex count is dropped
// together with its triangle. Compacting first makes descriptor == iteration order again.
cgal_mesh.collect_garbage();
return cgal_to_indexed_triangle_set(cgal_mesh);
}
// /////////////////////////////////////////////////////////////////////////////
// UV parameterization
// /////////////////////////////////////////////////////////////////////////////
std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &mesh)
{
namespace SMP = CGAL::Surface_mesh_parameterization;
if (mesh.indices.empty())
return std::nullopt;
_EpicMesh cgal_mesh;
triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh);
using vertex_descriptor = boost::graph_traits<_EpicMesh>::vertex_descriptor;
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
// LSCM assumes a single topological disk: one connected component, one boundary loop. A patch
// with several disconnected painted islands, or with a hole in it, doesn't qualify -- bail out
// rather than silently parameterizing just one arbitrary piece of it.
{
std::vector<std::size_t> component_id(num_faces(cgal_mesh));
const std::size_t num_components = CGAL::Polygon_mesh_processing::connected_components(
cgal_mesh, CGAL::make_property_map(component_id));
if (num_components != 1)
return std::nullopt;
}
const halfedge_descriptor border = CGAL::Polygon_mesh_processing::longest_border(cgal_mesh).first;
if (border == halfedge_descriptor())
return std::nullopt; // no boundary at all -- a closed patch, which isn't a disk either
// ...and exactly one boundary loop. One connected component is not enough on its own: a patch with
// a hole in it (paint a ring, or erase the middle of a stroke) is a single component with two
// loops, and LSCM will happily "parameterize" it into an overlapping, folded-over chart rather
// than fail. Walk the border halfedges and check every one of them belongs to the longest loop.
{
std::size_t border_halfedges = 0;
for (halfedge_descriptor h : halfedges(cgal_mesh))
if (is_border(h, cgal_mesh))
++border_halfedges;
std::size_t loop_length = 0;
halfedge_descriptor h = border;
do {
++loop_length;
h = next(h, cgal_mesh);
} while (h != border && loop_length <= border_halfedges);
if (loop_length != border_halfedges)
return std::nullopt; // more than one boundary loop -- not a topological disk
}
using Point_2 = EpicKernel::Point_2;
using UV_pmap = _EpicMesh::Property_map<vertex_descriptor, Point_2>;
UV_pmap uv_map = cgal_mesh.add_property_map<vertex_descriptor, Point_2>("h:uv", Point_2(0, 0)).first;
using Parameterizer = SMP::LSCM_parameterizer_3<_EpicMesh>;
const SMP::Error_code err = SMP::parameterize(cgal_mesh, Parameterizer(), border, uv_map);
if (err != SMP::OK)
return std::nullopt;
// triangle_mesh_to_cgal() adds vertices in the exact same order as mesh.vertices (see above),
// and Surface_mesh assigns indices sequentially on insertion into a fresh mesh, so a
// vertex_descriptor's index here is guaranteed to match the original input vertex index --
// the same assumption cgal_to_indexed_triangle_set() above already relies on.
std::vector<Vec2f> result(mesh.vertices.size(), Vec2f::Zero());
for (vertex_descriptor vd : vertices(cgal_mesh)) {
const std::size_t idx = std::size_t(vd);
if (idx < result.size()) {
const Point_2 &uv = uv_map[vd];
result[idx] = Vec2f(float(uv.x()), float(uv.y()));
}
}
return result;
}
// /////////////////////////////////////////////////////////////////////////////
// Boolean operations for CGAL meshes
// /////////////////////////////////////////////////////////////////////////////

View File

@@ -3,6 +3,8 @@
#include <memory>
#include <exception>
#include <optional>
#include <vector>
#include <libslic3r/TriangleMesh.hpp>
#include <Eigen/Geometry>
@@ -73,6 +75,27 @@ bool empty(const CGALMesh &mesh);
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
// component with exactly one boundary loop, true for a typical single brush stroke/patch but not
// guaranteed for multiple disconnected painted islands merged into one mesh.
std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &mesh);
// Isotropic remeshing (CGAL): rebuilds the mesh so its triangles are close to a uniform target edge
// length, splitting oversized triangles and collapsing undersized ones. Used to even out a model with
// wildly varying triangle sizes so texture displacement has a consistent vertex density to work with.
// Edges whose dihedral angle exceeds `sharp_angle_deg`, and any open border, are held fixed so hard
// features survive instead of being eroded by the relaxation pass; pass 0 to remesh everything.
// Returns the input unchanged if remeshing fails (e.g. a non-manifold or self-intersecting input).
// `n_relaxation_steps` is the number of tangential relaxation passes run inside each iteration. That
// relaxation is what actually evens out the triangle distribution - splitting and collapsing alone
// only bring edge *lengths* near the target, leaving the vertices wherever they happened to land. CGAL
// defaults it to 1, which on a few iterations is not enough to look uniform.
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
unsigned n_iterations = 3, double sharp_angle_deg = 40.0,
unsigned n_relaxation_steps = 1);
}
namespace mcut {

View File

@@ -2056,6 +2056,11 @@ void ModelVolume::reset_extra_facets()
this->seam_facets.reset();
this->mmu_segmentation_facets.reset();
this->fuzzy_skin_facets.reset();
// Texture-displacement paint data has no remap-across-topology-change support yet (see
// build_texture_displacement()'s documented limitation), so it must be dropped here rather
// than left referring to a mesh that no longer matches it.
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).reset();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const

View File

@@ -19,6 +19,7 @@
#include "TextConfiguration.hpp"
#include "EmbossShape.hpp"
#include "TriangleSelector.hpp"
#include "TextureDisplacement.hpp"
//BBS: add bbs 3mf
#include "Format/bbs_3mf.hpp"
@@ -28,6 +29,7 @@
#include "Format/STL.hpp"
#include "Format/OBJ.hpp"
#include <array>
#include <map>
#include <memory>
#include <string>
@@ -883,6 +885,69 @@ public:
// List of mesh facets painted for fuzzy skin.
FacetsAnnotation fuzzy_skin_facets;
// One independent paint mask per texture-displacement layer slot (see texture_displacement_layers
// below). Unlike the other facets fields above, a triangle may be painted (ENFORCER) in more
// than one of these simultaneously -- that overlap is what makes the layers "blend".
//
// These are 8 plain named fields rather than a std::array<FacetsAnnotation, N>: FacetsAnnotation's
// default/copy constructors are private and friended only to ModelVolume, but std::array's own
// implicitly-defined default/copy constructors are generated with std::array's access rights,
// not ModelVolume's -- so an array of FacetsAnnotation ends up with its default/copy
// constructors implicitly deleted regardless of the friend declaration. Use
// texture_displacement_facet(slot) below for array-like indexed access.
FacetsAnnotation texture_displacement_facets_0;
FacetsAnnotation texture_displacement_facets_1;
FacetsAnnotation texture_displacement_facets_2;
FacetsAnnotation texture_displacement_facets_3;
FacetsAnnotation texture_displacement_facets_4;
FacetsAnnotation texture_displacement_facets_5;
FacetsAnnotation texture_displacement_facets_6;
FacetsAnnotation texture_displacement_facets_7;
FacetsAnnotation& texture_displacement_facet(int slot) {
switch (slot) {
case 0: return texture_displacement_facets_0;
case 1: return texture_displacement_facets_1;
case 2: return texture_displacement_facets_2;
case 3: return texture_displacement_facets_3;
case 4: return texture_displacement_facets_4;
case 5: return texture_displacement_facets_5;
case 6: return texture_displacement_facets_6;
default: assert(slot == 7); return texture_displacement_facets_7;
}
}
const FacetsAnnotation& texture_displacement_facet(int slot) const { return const_cast<ModelVolume*>(this)->texture_displacement_facet(slot); }
// Small helpers for the constructor asserts below (kept out of line-noise at each call site).
bool texture_displacement_facets_ids_valid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).id().valid() || texture_displacement_facet(i).id() == this->id())
return false;
return true;
}
bool texture_displacement_facets_ids_invalid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (texture_displacement_facet(i).id().valid())
return false;
return true;
}
bool texture_displacement_facets_all_empty() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).empty())
return false;
return true;
}
// Texture assets (height maps) and their projection/displacement parameters. Element order
// is not meaningful for baking (layers are applied in TextureDisplacementLayer::slot order,
// see build_texture_displacement()); it only reflects UI insertion order.
std::vector<TextureDisplacementLayer> texture_displacement_layers;
// Whole-stack displacement settings (border handling, post-process smoothing) - see
// TextureDisplacementOptions. They live beside the layers rather than on one of them because
// they are not a property of any single layer.
TextureDisplacementOptions texture_displacement_options;
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1020,13 +1085,18 @@ public:
this->seam_facets.set_new_unique_id();
this->mmu_segmentation_facets.set_new_unique_id();
this->fuzzy_skin_facets.set_new_unique_id();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).set_new_unique_id();
}
bool is_fdm_support_painted() const { return !this->supported_facets.empty(); }
bool is_seam_painted() const { return !this->seam_facets.empty(); }
bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); }
bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); }
bool is_any_painted() const { return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted(); }
bool is_texture_displacement_painted() const { return !this->texture_displacement_facets_all_empty(); }
bool is_any_painted() const {
return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted() || is_texture_displacement_painted();
}
// Orca: Implement prusa's filament shrink compensation approach
// Returns 0-based indices of extruders painted by multi-material painting gizmo.
@@ -1079,6 +1149,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1095,6 +1166,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1109,6 +1181,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1122,10 +1195,17 @@ private:
name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull),
config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation),
supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets),
fuzzy_skin_facets(other.fuzzy_skin_facets), cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
fuzzy_skin_facets(other.fuzzy_skin_facets),
texture_displacement_facets_0(other.texture_displacement_facets_0), texture_displacement_facets_1(other.texture_displacement_facets_1),
texture_displacement_facets_2(other.texture_displacement_facets_2), texture_displacement_facets_3(other.texture_displacement_facets_3),
texture_displacement_facets_4(other.texture_displacement_facets_4), texture_displacement_facets_5(other.texture_displacement_facets_5),
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
texture_displacement_layers(other.texture_displacement_layers),
texture_displacement_options(other.texture_displacement_options),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{
assert(this->id().valid());
assert(this->config.id().valid());
assert(this->id().valid());
assert(this->config.id().valid());
assert(this->supported_facets.id().valid());
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
@@ -1175,6 +1255,8 @@ private:
assert(this->seam_facets.empty());
assert(this->mmu_segmentation_facets.empty());
assert(this->fuzzy_skin_facets.empty());
assert(this->texture_displacement_facets_all_empty());
assert(this->texture_displacement_layers.empty());
}
ModelVolume& operator=(ModelVolume &rhs) = delete;
@@ -1182,13 +1264,17 @@ private:
friend class cereal::access;
friend class UndoRedo::StackImpl;
// Used for deserialization, therefore no IDs are allocated.
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1), object(nullptr) {
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1),
texture_displacement_facets_0(-1), texture_displacement_facets_1(-1), texture_displacement_facets_2(-1), texture_displacement_facets_3(-1),
texture_displacement_facets_4(-1), texture_displacement_facets_5(-1), texture_displacement_facets_6(-1), texture_displacement_facets_7(-1),
object(nullptr) {
assert(this->id().invalid());
assert(this->config.id().invalid());
assert(this->supported_facets.id().invalid());
assert(this->seam_facets.id().invalid());
assert(this->mmu_segmentation_facets.id().invalid());
assert(this->fuzzy_skin_facets.id().invalid());
assert(this->texture_displacement_facets_ids_invalid());
}
template<class Archive> void load(Archive &ar) {
bool has_convex_hull;
@@ -1208,6 +1294,13 @@ private:
mesh_changed |= t != mmu_segmentation_facets.timestamp();
cereal::load_by_value(ar, fuzzy_skin_facets);
mesh_changed |= t != fuzzy_skin_facets.timestamp();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
FacetsAnnotation &f = texture_displacement_facet(i);
Timestamp tf = f.timestamp();
cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp();
}
ar(texture_displacement_layers, texture_displacement_options);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1229,6 +1322,9 @@ private:
cereal::save_by_value(ar, seam_facets);
cereal::save_by_value(ar, mmu_segmentation_facets);
cereal::save_by_value(ar, fuzzy_skin_facets);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
cereal::save_by_value(ar, texture_displacement_facet(i));
ar(texture_displacement_layers, texture_displacement_options);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);

View File

@@ -0,0 +1,219 @@
#include "TextureBakeDebug.hpp"
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace {
// Half-edge key, low index first so both sides of an edge form the same one.
inline uint64_t edge_key(int a, int b)
{
const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b));
return (uint64_t(lo) << 32) | uint64_t(hi);
}
} // namespace
void bake_stage_topology(const BakeStageMesh &mesh, size_t &open_edges, size_t &non_manifold_edges,
size_t &degenerate)
{
open_edges = non_manifold_edges = degenerate = 0;
// Sorted half-edges rather than a hash map: same answer, but it is one allocation and a sort
// instead of three million node allocations, which on a stage this size is the whole cost.
std::vector<uint64_t> keys;
keys.reserve(mesh.indices.size() * 3);
for (const Vec3i32 &t : mesh.indices) {
if (t[0] == t[1] || t[1] == t[2] || t[0] == t[2]) {
++degenerate;
continue; // a collapsed face has no edges worth counting
}
const Vec3f &a = mesh.vertices[size_t(t[0])];
if ((mesh.vertices[size_t(t[1])] - a).cross(mesh.vertices[size_t(t[2])] - a).squaredNorm() <= 0.f)
++degenerate; // zero area but three distinct corners: still counted as an edge carrier
for (int e = 0; e < 3; ++e)
keys.push_back(edge_key(t[e], t[(e + 1) % 3]));
}
std::sort(keys.begin(), keys.end());
for (size_t i = 0; i < keys.size();) {
size_t j = i + 1;
while (j < keys.size() && keys[j] == keys[i])
++j;
const size_t incident = j - i;
if (incident == 1)
++open_edges;
else if (incident > 2)
++non_manifold_edges;
i = j;
}
}
void BakeStageRecorder::finish(BakeStageSnapshot &s)
{
s.triangles = s.mesh.indices.size();
s.vertices = s.mesh.vertices.size();
if (m_check_topology)
bake_stage_topology(s.mesh, s.open_edges, s.non_manifold_edges, s.degenerate);
s.topology_checked = m_check_topology;
if (s.triangles > m_mesh_cap) {
s.mesh_dropped = true;
s.mesh.vertices.clear();
s.mesh.vertices.shrink_to_fit();
s.mesh.indices.clear();
s.mesh.indices.shrink_to_fit();
}
m_stages.push_back(std::move(s));
}
void BakeStageRecorder::capture(const char *name, const TextureBake::TriSoup &soup, double ms,
const std::string &detail)
{
if (!m_enabled)
return;
BakeStageSnapshot s;
s.name = name;
s.detail = detail;
s.ms = ms;
// The pipeline works on non-indexed soup, so welding here is what turns it back into something
// renderable. The geometry grid, matching to_indexed_triangle_set(), so the debug view shows the
// same sharing the bake's own output would have.
const size_t n = soup.pos.size();
TextureBake::QuantizedPointMap map(TextureBake::WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
s.mesh.vertices.reserve(n / 3);
for (size_t i = 0; i < n; ++i) {
id[i] = map.get_or_set(soup.pos[i], int(s.mesh.vertices.size()));
if (map.inserted())
s.mesh.vertices.push_back(soup.pos[i]);
}
s.mesh.indices.reserve(n / 3);
for (size_t t = 0; t + 2 < n; t += 3) {
// Corners that welded together carry no area; the bake's own conversion drops them too, so
// dropping them here keeps the stage count honest against what would be committed.
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
continue;
s.mesh.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
}
finish(s);
}
void BakeStageRecorder::capture(const char *name, const std::vector<Vec3f> &vertices,
const std::vector<Vec3i32> &indices, double ms,
const std::string &detail)
{
if (!m_enabled)
return;
BakeStageSnapshot s;
s.name = name;
s.detail = detail;
s.ms = ms;
s.mesh.vertices = vertices;
s.mesh.indices = indices;
finish(s);
}
void BakeStageRecorder::capture_note(const char *name, double ms, const std::string &detail)
{
if (!m_enabled)
return;
BakeStageSnapshot s;
s.name = name;
s.detail = detail;
s.ms = ms;
s.topology_checked = false;
m_stages.push_back(std::move(s));
}
void BakeStageRecorder::rebase(size_t from, const Transform3d *to_local, bool flip_winding)
{
for (size_t i = from; i < m_stages.size(); ++i) {
BakeStageMesh &m = m_stages[i].mesh;
if (to_local != nullptr)
for (Vec3f &v : m.vertices)
v = (*to_local * v.cast<double>()).cast<float>();
if (flip_winding)
for (Vec3i32 &t : m.indices)
std::swap(t[1], t[2]);
}
}
double BakeStageRecorder::total_ms() const
{
double sum = 0.0;
for (const BakeStageSnapshot &s : m_stages)
sum += s.ms;
return sum;
}
size_t dump_bake_stages(const std::vector<BakeStageSnapshot> &stages, const std::string &dir)
{
boost::system::error_code ec;
boost::filesystem::create_directories(dir, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "BakeStageRecorder: cannot create " << dir << ": " << ec.message();
return 0;
}
size_t written = 0;
for (size_t i = 0; i < stages.size(); ++i) {
const BakeStageSnapshot &s = stages[i];
if (s.mesh.empty())
continue;
// Stage names carry spaces and punctuation; keep the filename to what every shell and viewer
// handles without quoting.
std::string safe;
for (const char c : s.name)
safe += (std::isalnum(static_cast<unsigned char>(c)) != 0) ? c : '_';
char path[1024];
std::snprintf(path, sizeof(path), "%s/%02zu_%s.obj", dir.c_str(), i, safe.c_str());
std::FILE *f = std::fopen(path, "wb");
if (f == nullptr) {
BOOST_LOG_TRIVIAL(error) << "BakeStageRecorder: cannot write " << path;
continue;
}
std::fprintf(f, "# texture bake stage %zu: %s\n", i, s.name.c_str());
if (!s.detail.empty())
std::fprintf(f, "# %s\n", s.detail.c_str());
std::fprintf(f, "# %zu triangles, %.2f ms\n", s.triangles, s.ms);
for (const Vec3f &v : s.mesh.vertices)
std::fprintf(f, "v %.6f %.6f %.6f\n", double(v.x()), double(v.y()), double(v.z()));
for (const Vec3i32 &t : s.mesh.indices) // OBJ indices are 1-based
std::fprintf(f, "f %d %d %d\n", t[0] + 1, t[1] + 1, t[2] + 1);
std::fclose(f);
++written;
}
char summary[1024];
std::snprintf(summary, sizeof(summary), "%s/stages.txt", dir.c_str());
if (std::FILE *f = std::fopen(summary, "wb"); f != nullptr) {
std::fprintf(f, "%-3s %-24s %10s %12s %10s %8s %8s %8s %s\n", "#", "stage", "ms", "triangles",
"vertices", "open", "nonman", "degen", "detail");
double total = 0.0;
for (size_t i = 0; i < stages.size(); ++i) {
const BakeStageSnapshot &s = stages[i];
total += s.ms;
std::fprintf(f, "%-3zu %-24s %10.2f %12zu %10zu ", i, s.name.c_str(), s.ms, s.triangles,
s.vertices);
if (s.topology_checked)
std::fprintf(f, "%8zu %8zu %8zu", s.open_edges, s.non_manifold_edges, s.degenerate);
else
std::fprintf(f, "%8s %8s %8s", "-", "-", "-");
std::fprintf(f, " %s%s\n", s.detail.c_str(), s.mesh_dropped ? " [mesh over cap, not written]" : "");
}
std::fprintf(f, "\ntotal %.2f ms across %zu stages\n", total, stages.size());
std::fclose(f);
}
return written;
}
} // namespace Slic3r

View File

@@ -0,0 +1,123 @@
#pragma once
// Step-by-step capture of a bake.
//
// A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the
// only useful question is which stage made it wrong. This records the geometry, the wall time and the
// topology after every stage, which is what the gizmo's debug view steps through and what the
// benchmark's --dump-stages writes out.
//
// Deliberately independent of TriangleMesh: a stage is held as a plain vertex/index pair, which is
// layout-compatible with indexed_triangle_set's own members (stl_vertex is Vec3f,
// stl_triangle_vertex_indices is Vec3i32), so the GUI assigns rather than converts and the standalone
// benchmark does not have to link admesh to use this.
//
// Recording is off unless enable(true) was called, and every capture site is a null-pointer check, so
// a normal bake pays nothing for this being here.
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
struct BakeStageMesh
{
std::vector<Vec3f> vertices;
std::vector<Vec3i32> indices;
bool empty() const { return indices.empty(); }
size_t triangle_count() const { return indices.size(); }
};
struct BakeStageSnapshot
{
std::string name; // "remesh", "subdivide", ...
std::string detail; // whatever the stage has to say: collapse counts, rejected moves, ...
BakeStageMesh mesh; // empty when the stage was over the memory cap - see mesh_dropped
double ms = 0.0;
size_t triangles = 0;
size_t vertices = 0;
// Filled only when the recorder was asked to check topology: it is a sort over every half-edge,
// which on a multi-million triangle stage costs more than the stage being measured.
size_t open_edges = 0;
size_t non_manifold_edges = 0;
size_t degenerate = 0;
bool topology_checked = false;
// The geometry was dropped to stay inside the memory cap; every count above is still real.
bool mesh_dropped = false;
};
// Edge and area defects of a captured stage. Split out so a caller can run it on its own.
void bake_stage_topology(const BakeStageMesh &mesh, size_t &open_edges, size_t &non_manifold_edges,
size_t &degenerate);
// Writes `<dir>/NN_name.obj` for every stage that still holds geometry, plus a `stages.txt` summary.
// Returns how many meshes were written. Existing files with the same names are overwritten.
//
// A free function rather than a recorder method because by the time anyone wants the files the
// recorder is usually gone and only the stages survive - that is how the gizmo holds them.
size_t dump_bake_stages(const std::vector<BakeStageSnapshot> &stages, const std::string &dir);
class BakeStageRecorder
{
public:
// Nothing is recorded until this is on.
void enable(bool on) { m_enabled = on; }
bool enabled() const { return m_enabled; }
// The edge scan is optional because it is O(n log n) over every half-edge, and a debug run that
// only wants to see the geometry should not pay for it on every stage.
void set_check_topology(bool on) { m_check_topology = on; }
bool check_topology() const { return m_check_topology; }
// Stages above this keep their counts but not their geometry. A debug run holds every stage at
// once, and a 4 M triangle stage is about 150 MB on its own, so without a cap stepping through a
// fine bake would need more memory than the bake did.
void set_mesh_cap(size_t triangles) { m_mesh_cap = triangles; }
size_t mesh_cap() const { return m_mesh_cap; }
// `ms` is passed in rather than measured here: the caller is already timing the stage, and the
// capture itself (a weld, a copy, possibly an edge scan) must not land inside that measurement.
void capture(const char *name, const TextureBake::TriSoup &soup, double ms,
const std::string &detail = {});
void capture(const char *name, const std::vector<Vec3f> &vertices,
const std::vector<Vec3i32> &indices, double ms, const std::string &detail = {});
// For a stage that changed nothing a caller can still show, e.g. a skipped remesh.
void capture_note(const char *name, double ms, const std::string &detail);
// Index of the next stage to be recorded. Paired with rebase() to fix up a range afterwards.
size_t mark() const { return m_stages.size(); }
// Brings stages [from, end) into the caller's own space and winding. The bake runs in world
// millimetres and, for a mirrored placement, against a reversed winding; the debug view draws in
// the volume's local frame, so a captured range has to be brought back the same way the bake's
// own result is. `to_local` may be null for no transform.
void rebase(size_t from, const Transform3d *to_local, bool flip_winding);
const std::vector<BakeStageSnapshot> &stages() const { return m_stages; }
std::vector<BakeStageSnapshot> take() { return std::move(m_stages); }
void clear() { m_stages.clear(); }
bool empty() const { return m_stages.empty(); }
// Total recorded wall time, which is the bake's own time minus whatever it does outside a stage.
double total_ms() const;
size_t dump_obj(const std::string &dir) const { return dump_bake_stages(m_stages, dir); }
private:
void finish(BakeStageSnapshot &s);
std::vector<BakeStageSnapshot> m_stages;
bool m_enabled = false;
bool m_check_topology = true;
size_t m_mesh_cap = 4'000'000;
};
} // namespace Slic3r

View File

@@ -0,0 +1,550 @@
#include "TextureBakeDecimate.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <queue>
#include <boost/log/trivial.hpp>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
namespace {
// Symmetric 4x4 quadric, as its 10 upper-triangle values.
struct Quadric
{
std::array<double, 10> q{};
void add_plane(double a, double b, double c, double d)
{
q[0] += a * a; q[1] += a * b; q[2] += a * c; q[3] += a * d;
q[4] += b * b; q[5] += b * c; q[6] += b * d;
q[7] += c * c; q[8] += c * d;
q[9] += d * d;
}
void operator+=(const Quadric &o)
{
for (int i = 0; i < 10; ++i)
q[size_t(i)] += o.q[size_t(i)];
}
double eval(double x, double y, double z) const
{
return q[0] * x * x + 2 * q[1] * x * y + 2 * q[2] * x * z + 2 * q[3] * x +
q[4] * y * y + 2 * q[5] * y * z + 2 * q[6] * y +
q[7] * z * z + 2 * q[8] * z + q[9];
}
};
double eval_sum(const std::vector<Quadric> &qs, int v1, int v2, const Vec3d &p)
{
return qs[size_t(v1)].eval(p.x(), p.y(), p.z()) + qs[size_t(v2)].eval(p.x(), p.y(), p.z());
}
// The position minimising the summed quadric, if the system is well conditioned enough to trust.
bool solve_q(const std::vector<Quadric> &qs, int v1, int v2, Vec3d &out)
{
const auto &A = qs[size_t(v1)].q;
const auto &B = qs[size_t(v2)].q;
const double a00 = A[0] + B[0], a01 = A[1] + B[1], a02 = A[2] + B[2];
const double a11 = A[4] + B[4], a12 = A[5] + B[5], a22 = A[7] + B[7];
const double b0 = -(A[3] + B[3]), b1 = -(A[6] + B[6]), b2 = -(A[8] + B[8]);
const double det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02) +
a02 * (a01 * a12 - a11 * a02);
const double max_el = std::max({ std::abs(a00), std::abs(a01), std::abs(a02), std::abs(a11),
std::abs(a12), std::abs(a22) });
// Scaled with the matrix, so it means the same at any model scale.
const double threshold = max_el * max_el * max_el * 1e-10;
if (std::abs(det) < std::max(threshold, 1e-30))
return false;
const double inv = 1.0 / det;
out.x() = inv * (b0 * (a11 * a22 - a12 * a12) - a01 * (b1 * a22 - a12 * b2) + a02 * (b1 * a12 - a11 * b2));
out.y() = inv * (a00 * (b1 * a22 - a12 * b2) - b0 * (a01 * a22 - a12 * a02) + a02 * (a01 * b2 - b1 * a02));
out.z() = inv * (a00 * (a11 * b2 - b1 * a12) - a01 * (a01 * b2 - b1 * a02) + b0 * (a01 * a12 - a11 * a02));
return true;
}
Vec3d face_normal_unit(const std::vector<Vec3d> &pos, int a, int b, int c)
{
const Vec3d n = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]);
const double len = n.norm();
return (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
}
// Versions are captured at push time; a mismatch on pop means a later collapse invalidated the entry.
// Lazy deletion, far cheaper than removing entries eagerly - but it means the heap accumulates stale
// duplicates, so its size has to be reserved up front and it is compacted once the dead entries
// dominate (see maybe_compact below).
//
// The collapse target is not stored. A matching version stamp means neither endpoint's quadric nor its
// position has changed since the push, so the target recomputes to exactly the same value on pop - and
// the entry drops from 40 bytes to 24. Sifting is most of this stage's time, and it is memory traffic.
struct HeapEntry
{
double cost;
int v1, v2;
uint32_t ver1, ver2;
bool operator>(const HeapEntry &o) const { return cost > o.cost; }
};
} // namespace
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat,
double harvest_tol, const std::vector<uint8_t> &locked_faces,
const DecimateProgressFn &on_progress)
{
DecimateResult result;
const size_t n = geometry.pos.size();
if (n < 3) {
result.geometry = geometry;
return result;
}
// The finest grid. Anything coarser fuses distinct fine-feature vertices on a displaced mesh,
// leaving it non-manifold before decimation starts and producing open edges afterwards.
QuantizedPointMap vert_map(WELD_GRID_DECIMATION, std::min(n, size_t(1) << 22));
std::vector<Vec3d> pos;
std::vector<int> remap(n);
for (size_t i = 0; i < n; ++i) {
const int idx = vert_map.get_or_set(geometry.pos[i], int(pos.size()));
if (vert_map.inserted())
pos.push_back(geometry.pos[i].cast<double>());
remap[i] = idx;
}
const size_t vert_count = pos.size();
const size_t face_count = n / 3;
std::vector<int> faces(face_count * 3);
for (size_t i = 0; i < n; ++i)
faces[i] = remap[i];
if (face_count <= target_triangles && !harvest_flat) {
result.geometry = geometry;
return result;
}
// An edge with a locked endpoint never reaches the heap.
std::vector<uint8_t> locked_vert;
size_t locked_face_count = 0;
if (!locked_faces.empty()) {
locked_vert.assign(vert_count, 0);
for (size_t f = 0; f < face_count && f < locked_faces.size(); ++f) {
if (!locked_faces[f])
continue;
++locked_face_count;
for (int k = 0; k < 3; ++k)
locked_vert[size_t(faces[f * 3 + size_t(k)])] = 1;
}
}
// With the locked faces alone at the target, chasing it would grind the free region to its guard
// limit for nothing - harvest only, and say so.
const bool locked_over_budget =
!locked_vert.empty() && face_count > target_triangles && locked_face_count >= target_triangles;
result.locked_over_budget = locked_over_budget;
if (locked_over_budget && !harvest_flat) {
result.geometry = geometry;
return result;
}
std::vector<Quadric> quadrics(vert_count);
{
// The plane per face is independent; accumulating it into the three incident vertices is not,
// so only the first half is parallel.
std::vector<Vec4d> planes(face_count, Vec4d::Zero());
tbb::parallel_for(tbb::blocked_range<size_t>(0, face_count),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t f = range.begin(); f < range.end(); ++f) {
const int a = faces[f * 3], b = faces[f * 3 + 1], c = faces[f * 3 + 2];
if (a < 0)
continue;
const Vec3d nrm = face_normal_unit(pos, a, b, c);
if (nrm.isZero())
continue;
planes[f] = Vec4d(nrm.x(), nrm.y(), nrm.z(), -nrm.dot(pos[size_t(a)]));
}
});
for (size_t f = 0; f < face_count; ++f) {
const Vec4d &pl = planes[f];
if (pl.head<3>().isZero())
continue;
for (int k = 0; k < 3; ++k)
quadrics[size_t(faces[f * 3 + size_t(k)])].add_plane(pl.x(), pl.y(), pl.z(), pl.w());
}
}
// Two penalty planes per endpoint on a sharp interior edge, each perpendicular to one adjacent
// face and containing the edge, constraining the vertex to the crease line.
{
struct EdgeRec { int va, vb, f0, f1; uint8_t count; };
std::vector<EdgeRec> edges;
QuantizedPointMap edge_idx(1.0, std::min(face_count * 3, size_t(1) << 22));
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
for (int e = 0; e < 3; ++e) {
const int va = faces[f * 3 + size_t(e)];
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
const int lo = std::min(va, vb), hi = std::max(va, vb);
const int ei = edge_idx.get_or_set_key(lo, hi, 0, int(edges.size()));
if (edge_idx.inserted())
edges.push_back({ lo, hi, int(f), -1, 1 });
else if (edges[size_t(ei)].count == 1) {
edges[size_t(ei)].f1 = int(f);
edges[size_t(ei)].count = 2;
} else
// Non-manifold; never feeds a crease.
edges[size_t(ei)].count = 3;
}
}
const double sqrt_w = std::sqrt(DECIMATE_CREASE_WEIGHT);
for (const EdgeRec &er : edges) {
if (er.count != 2)
continue; // boundary or non-manifold
const Vec3d n0 = face_normal_unit(pos, faces[size_t(er.f0) * 3], faces[size_t(er.f0) * 3 + 1],
faces[size_t(er.f0) * 3 + 2]);
const Vec3d n1 = face_normal_unit(pos, faces[size_t(er.f1) * 3], faces[size_t(er.f1) * 3 + 1],
faces[size_t(er.f1) * 3 + 2]);
if (n0.dot(n1) >= DECIMATE_CREASE_COS)
continue; // smooth enough to be no crease
const Vec3d e = pos[size_t(er.vb)] - pos[size_t(er.va)];
const double elen = e.norm();
if (elen <= 0.0)
continue;
const Vec3d ed = e / elen;
for (const Vec3d &fn : { n0, n1 }) {
Vec3d pn = fn.cross(ed);
const double plen = pn.norm();
if (plen < 1e-10)
continue; // edge parallel to the face normal
pn /= plen;
const double d = -pn.dot(pos[size_t(er.va)]);
// sqrt(w) on the inputs gives w times the accumulated products.
for (const int v : { er.va, er.vb })
quadrics[size_t(v)].add_plane(pn.x() * sqrt_w, pn.y() * sqrt_w, pn.z() * sqrt_w,
d * sqrt_w);
}
}
}
// Vertex-face incidence as intrusive linked lists of slots over flat arrays.
const size_t S = face_count * 3;
std::vector<int> vf_head(vert_count, -1), slot_face(S), slot_vert(S), slot_next(S, -1),
slot_prev(S, -1), face_slot(S, -1);
for (size_t f = 0; f < face_count; ++f)
for (int k = 0; k < 3; ++k) {
const int s = int(f) * 3 + k;
const int v = faces[size_t(s)];
slot_face[size_t(s)] = int(f);
slot_vert[size_t(s)] = v;
slot_next[size_t(s)] = vf_head[size_t(v)];
slot_prev[size_t(s)] = -1;
if (vf_head[size_t(v)] >= 0)
slot_prev[size_t(vf_head[size_t(v)])] = s;
vf_head[size_t(v)] = s;
face_slot[size_t(s)] = s;
}
const auto unlink_slot = [&](int s) {
const int p = slot_prev[size_t(s)], nx = slot_next[size_t(s)];
if (p >= 0) slot_next[size_t(p)] = nx;
else vf_head[size_t(slot_vert[size_t(s)])] = nx;
if (nx >= 0) slot_prev[size_t(nx)] = p;
};
const auto move_slot = [&](int s, int nv) {
unlink_slot(s);
slot_next[size_t(s)] = vf_head[size_t(nv)];
slot_prev[size_t(s)] = -1;
if (vf_head[size_t(nv)] >= 0)
slot_prev[size_t(vf_head[size_t(nv)])] = s;
vf_head[size_t(nv)] = s;
slot_vert[size_t(s)] = nv;
};
std::vector<uint8_t> active(vert_count, 1);
std::vector<uint32_t> version(vert_count, 0);
std::vector<uint32_t> nb_stamp(vert_count, 0), lk_stamp(vert_count, 0);
uint32_t epoch = 1, lk_epoch = 1;
size_t active_faces = face_count;
// A plain vector driven by the heap algorithms, so the capacity can be reserved. Lazy deletion
// means roughly one entry per edge plus one per re-push after each collapse; the reserve below is
// sized from the edge count and simply grows if a mesh needs more.
std::vector<HeapEntry> heap;
heap.reserve(std::min<size_t>(face_count * 3, size_t(1) << 24));
const auto heap_push = [&](HeapEntry e) {
heap.push_back(e);
std::push_heap(heap.begin(), heap.end(), std::greater<HeapEntry>());
};
const auto heap_pop = [&]() {
std::pop_heap(heap.begin(), heap.end(), std::greater<HeapEntry>());
const HeapEntry e = heap.back();
heap.pop_back();
return e;
};
size_t pops = 0, stale_pops = 0, compactions = 0;
// An entry is stale once either endpoint has been removed or moved by a later collapse.
const auto is_stale = [&](const HeapEntry &e) {
return !active[size_t(e.v1)] || !active[size_t(e.v2)] || version[size_t(e.v1)] != e.ver1 ||
version[size_t(e.v2)] != e.ver2;
};
// Measured on a 2.4 M -> 750 k run, 82% of pops were stale: every collapse re-pushes the survivor's
// edges and orphans the old ones, so the heap grows to several times the live edge set and every
// sift walks that much further through memory. Dropping the dead entries and re-heapifying once
// they dominate costs one linear pass, amortised against the growth that triggered it.
//
// Keyed to the live face count rather than to the heap's own size: lazy popping keeps the heap from
// ever doubling, but the live edge set (about 1.5 per face) shrinks as decimation proceeds, so by the
// end the heap is several times what is still collapsible. Compact once it passes twice that.
const auto maybe_compact = [&]() {
if (heap.size() < std::max<size_t>(size_t(1) << 16, active_faces * 3))
return;
heap.erase(std::remove_if(heap.begin(), heap.end(), is_stale), heap.end());
std::make_heap(heap.begin(), heap.end(), std::greater<HeapEntry>());
++compactions;
};
// Where an edge collapses to. Also re-run on pop instead of stored - see HeapEntry.
const auto collapse_target = [&](int v1, int v2) -> Vec3d {
Vec3d p;
if (!solve_q(quadrics, v1, v2, p)) {
const Vec3d mid = (pos[size_t(v1)] + pos[size_t(v2)]) * 0.5;
const double e1 = eval_sum(quadrics, v1, v2, pos[size_t(v1)]);
const double e2 = eval_sum(quadrics, v1, v2, pos[size_t(v2)]);
const double em = eval_sum(quadrics, v1, v2, mid);
const double emin = std::min({ e1, e2, em });
const double etol = emin * 1e-2 + 1e-12;
// The midpoint when the three are near-equal, i.e. flat: it moves adjacent triangles
// least, so fewer normal flips and no stalling on coplanar geometry.
if (em <= emin + etol) p = mid;
else if (e1 <= e2) p = pos[size_t(v1)];
else p = pos[size_t(v2)];
}
return p;
};
const auto push_edge = [&](int v1, int v2) {
const Vec3d p = collapse_target(v1, v2);
// The cost is evaluated at the exact target and the collapse moves to its float rounding -
// the same split as when the rounded target was stored in the entry, so no ordering changes.
// Where quadric costs are all near zero, shorter edges first keeps triangle quality up.
const double len2 = (pos[size_t(v2)] - pos[size_t(v1)]).squaredNorm();
heap_push({ eval_sum(quadrics, v1, v2, p) + len2 * 1e-8, v1, v2, version[size_t(v1)],
version[size_t(v2)] });
};
{
QuantizedPointMap seed_seen(1.0, std::min(face_count * 3, size_t(1) << 22));
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
for (int e = 0; e < 3; ++e) {
const int va = faces[f * 3 + size_t(e)];
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
if (!locked_vert.empty() && (locked_vert[size_t(va)] || locked_vert[size_t(vb)]))
continue;
seed_seen.get_or_set_key(std::min(va, vb), std::max(va, vb), 0, 1);
if (seed_seen.inserted())
push_edge(va, vb);
}
}
}
// 0 means a stale entry, 1 a boundary edge, 2 or more safe.
const auto shared_face_count = [&](int v1, int v2) {
int count = 0;
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k)
if (faces[size_t(f) * 3 + size_t(k)] == v2) {
if (++count >= 2)
return 2;
break;
}
}
return count;
};
// Safe only when the sole common neighbours of the endpoints are the apexes of the faces the edge
// already shares; any other would pile a third triangle onto an edge after the collapse.
const auto has_link_violation = [&](int v1, int v2, uint32_t ep) {
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k)
if (const int x = faces[size_t(f) * 3 + size_t(k)]; x != v1)
lk_stamp[size_t(x)] = ep;
}
int shared = 0;
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
const int a = faces[size_t(f) * 3], b = faces[size_t(f) * 3 + 1], c = faces[size_t(f) * 3 + 2];
if (a == v2 || b == v2 || c == v2) {
++shared;
const int apex = (a != v1 && a != v2) ? a : (b != v1 && b != v2) ? b : c;
lk_stamp[size_t(apex)] = ep + 1; // a legal shared-face apex
}
}
if (shared > 2)
return true; // already non-manifold
for (int s = vf_head[size_t(v2)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k) {
const int x = faces[size_t(f) * 3 + size_t(k)];
if (x != v2 && x != v1 && lk_stamp[size_t(x)] == ep)
return true;
}
}
return false;
};
// Squared-dot, so no square root or division. Faces containing the other endpoint are the ones
// being removed, so they are skipped.
const auto check_flipped = [&](int vc, int vo, const Vec3d &np) {
for (int s = vf_head[size_t(vc)]; s >= 0; s = slot_next[size_t(s)]) {
const size_t f = size_t(slot_face[size_t(s)]);
if (faces[f * 3] < 0)
continue;
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
if (fa == vo || fb == vo || fc == vo)
continue;
const Vec3d oa = pos[size_t(fa)], ob = pos[size_t(fb)], oc = pos[size_t(fc)];
const Vec3d on = (ob - oa).cross(oc - oa);
const Vec3d na = (fa == vc) ? np : oa;
const Vec3d nb = (fb == vc) ? np : ob;
const Vec3d nc = (fc == vc) ? np : oc;
const Vec3d nn = (nb - na).cross(nc - na);
const double raw = on.dot(nn);
if (raw < 0.0)
return true;
if (raw * raw < DECIMATE_FLIP_DOT * DECIMATE_FLIP_DOT * on.squaredNorm() * nn.squaredNorm())
return true;
}
return false;
};
const size_t init_faces = active_faces;
const size_t to_remove = std::max<size_t>(1, init_faces > target_triangles
? init_faces - target_triangles : init_faces);
const double harvest_ceil = harvest_tol * harvest_tol;
bool reached_target = locked_over_budget;
double last_progress = 0.0;
while (!heap.empty()) {
if (active_faces <= target_triangles) {
if (!harvest_flat)
break;
reached_target = true;
}
const HeapEntry top = heap_pop();
++pops;
// The popped entry is the cheapest left, so exceeding the tolerance ends the run.
if (reached_target && top.cost > harvest_ceil)
break;
const int v1 = top.v1, v2 = top.v2;
if (is_stale(top)) {
++stale_pops;
continue;
}
if (shared_face_count(v1, v2) < 2)
continue;
lk_epoch += 2; // +2 so ep and ep+1 cannot collide with the next call
if (has_link_violation(v1, v2, lk_epoch))
continue;
const Vec3d target = collapse_target(v1, v2).cast<float>().cast<double>();
if (check_flipped(v1, v2, target) || check_flipped(v2, v1, target))
continue;
// v1 survives at the new position, v2 goes.
pos[size_t(v1)] = target;
quadrics[size_t(v1)] += quadrics[size_t(v2)];
++version[size_t(v1)];
for (int s = vf_head[size_t(v2)]; s >= 0;) {
const size_t f = size_t(slot_face[size_t(s)]);
const int s_next = slot_next[size_t(s)]; // read before the list is modified
if (faces[f * 3] >= 0) {
for (int k = 0; k < 3; ++k)
if (faces[f * 3 + size_t(k)] == v2) {
faces[f * 3 + size_t(k)] = v1;
break;
}
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
if (fa == fb || fb == fc || fa == fc) {
for (int k = 0; k < 3; ++k)
if (const int sk = face_slot[f * 3 + size_t(k)]; sk >= 0) {
unlink_slot(sk);
face_slot[f * 3 + size_t(k)] = -1;
}
faces[f * 3] = faces[f * 3 + 1] = faces[f * 3 + 2] = -1;
--active_faces;
} else
move_slot(s, v1);
}
s = s_next;
}
active[size_t(v2)] = 0;
++epoch;
for (int sv = vf_head[size_t(v1)]; sv >= 0; sv = slot_next[size_t(sv)]) {
const size_t f = size_t(slot_face[size_t(sv)]);
if (faces[f * 3] < 0)
continue;
for (int k = 0; k < 3; ++k) {
const int nb = faces[f * 3 + size_t(k)];
if (nb == v1 || nb_stamp[size_t(nb)] == epoch)
continue;
nb_stamp[size_t(nb)] = epoch;
// v1 is never locked - a locked edge never entered the heap.
if (active[size_t(nb)] && (locked_vert.empty() || !locked_vert[size_t(nb)]))
push_edge(v1, nb);
}
}
maybe_compact();
if (on_progress) {
const double p = std::min(1.0, double(init_faces - active_faces) / double(to_remove));
if (p - last_progress > 0.005) {
last_progress = p;
if (!on_progress(p))
break;
}
}
}
BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: pops=" << pops << " stale=" << stale_pops
<< " compactions=" << compactions << " heap_peak=" << heap.capacity()
<< " faces=" << active_faces;
// Rebuild from the surviving faces, with per-face normals.
TriSoup &out = result.geometry;
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
const Vec3f a = pos[size_t(faces[f * 3])].cast<float>();
const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast<float>();
const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast<float>();
const Vec3f nrm = (b - a).cross(c - a).normalized();
out.pos.insert(out.pos.end(), { a, b, c });
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,56 @@
#pragma once
// Quadric error metric decimation (Garland & Heckbert), with two additions that matter on a
// displaced mesh.
//
// Crease quadrics: an interior edge sharper than the threshold gets penalty planes at both endpoints,
// perpendicular to each adjacent face and through the edge, weighted so such edges collapse last or
// not at all. A texture's hard step keeps its geometry while the flat ground around it reduces.
//
// Flat-face harvesting: the loop keeps going past the triangle target while each collapse's error
// stays under an absolute bound, so flat faces that cost nothing to remove are not left behind.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Reject a collapse deviating more than about 78 degrees from the old face normal.
static constexpr double DECIMATE_FLIP_DOT = 0.2;
// Edges sharper than 60 degrees are treated as creases.
static constexpr double DECIMATE_CREASE_COS = 0.5;
// Quadric penalty weight for a crease plane.
static constexpr double DECIMATE_CREASE_WEIGHT = 1e4;
// Upper bound in mm on the deviation a harvested collapse may introduce; the real one is smaller,
// since the cost sums squared distances over all incident faces.
//
// Absolute, not relative to the cost at which the target was crossed. A relative band fails in the
// case with the most to shed: when the target is reached with a large flat surplus left, the crossing
// cost is essentially zero, so the band is too and nothing is harvested.
static constexpr double DECIMATE_DEFAULT_HARVEST_TOL = 0.005;
// Returns false to cancel.
using DecimateProgressFn = std::function<bool(double fraction)>;
struct DecimateResult
{
TriSoup geometry;
// The locked faces alone met the target, so it was unreachable without touching preserved
// geometry.
bool locked_over_budget = false;
};
// `locked_faces`: one entry per input triangle; a vertex touching one may neither move nor be
// removed, which also pins the ring between the two regions.
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat = true,
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL,
const std::vector<uint8_t> &locked_faces = {},
const DecimateProgressFn &on_progress = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,285 @@
#include "TextureBakeDisplace.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
const DisplaceSettings &settings, const DisplaceBounds &bounds,
const DisplaceProgressFn &on_progress)
{
TriSoup out;
const size_t count = geometry.pos.size();
if (count == 0 || !sample)
return geometry;
out.pos.resize(count);
out.nrm.resize(count);
// Everything below is keyed by this id, which is what makes one vector per position expressible.
const bool need_id_positions = settings.boundary_falloff > 0.f;
QuantizedPointMap dedup(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
std::vector<int> vertex_id(count);
std::vector<Vec3f> id_pos;
int next_id = 0;
for (size_t i = 0; i < count; ++i) {
const int id = dedup.get_or_set(geometry.pos[i], next_id);
if (dedup.inserted()) {
++next_id;
if (need_id_positions)
id_pos.push_back(geometry.pos[i]);
}
vertex_id[i] = id;
}
const size_t unique_count = size_t(next_id);
// Pass 1: area-weighted smooth normals per position, plus what masking and falloff need.
std::vector<Vec3d> smooth_nrm(unique_count, Vec3d::Zero());
std::vector<double> masked_area(unique_count, 0.0), total_area(unique_count, 0.0);
const bool have_weights = !geometry.exclude_weight.empty();
std::vector<uint8_t> user_excluded_face(have_weights ? count / 3 : 0, 0);
std::vector<uint8_t> excluded_pos(have_weights ? unique_count : 0, 0);
for (size_t t = 0; t + 2 < count; t += 3) {
const Vec3d a = geometry.pos[t].cast<double>();
const Vec3d face_n = (geometry.pos[t + 1].cast<double>() - a).cross(geometry.pos[t + 2].cast<double>() - a);
const double face_area = face_n.norm(); // twice the triangle area, so weighting is natural
const double nz = face_area > 1e-12 ? face_n.z() / face_area : 0.0;
const double face_angle = std::acos(std::min(1.0, std::abs(nz))) * (180.0 / M_PI);
const bool angle_masked =
nz < 0.0 ? (settings.bottom_angle_limit > 0.f && face_angle <= settings.bottom_angle_limit)
: (settings.top_angle_limit > 0.f && face_angle <= settings.top_angle_limit);
// Thresholded high, not at a half: merging by maximum leaves a face bordering an excluded one
// with two corners at 1.0, averaging about 0.67, which a half threshold would misread.
bool user_excluded = false;
if (have_weights) {
const float avg = (geometry.exclude_weight[t] + geometry.exclude_weight[t + 1] +
geometry.exclude_weight[t + 2]) / 3.f;
user_excluded = avg > 0.99f;
if (user_excluded)
user_excluded_face[t / 3] = 1;
}
for (int v = 0; v < 3; ++v) {
const size_t vid = size_t(vertex_id[t + size_t(v)]);
if (user_excluded && have_weights)
excluded_pos[vid] = 1;
// Subdivision split vertices at sharp edges, so these are smooth across soft edges and
// sharp across hard ones - no faceting on round surfaces, no rounding of corners.
smooth_nrm[vid] += geometry.nrm[t + size_t(v)].cast<double>() * face_area;
if (angle_masked)
masked_area[vid] += face_area;
total_area[vid] += face_area;
}
}
// The pre-normalisation magnitude over the total area says how much the neighbouring faces agree:
// near 1 they do, near 0 they cancelled, meaning a knife edge with no usable surface direction.
std::vector<double> reliability(unique_count, 0.0);
for (size_t id = 0; id < unique_count; ++id) {
const double len = smooth_nrm[id].norm();
reliability[id] = (len > 0.0 && total_area[id] > 0.0) ? len / total_area[id] : 0.0;
smooth_nrm[id] = (len > 0.0) ? Vec3d(smooth_nrm[id] / len) : Vec3d(0.0, 0.0, 1.0);
}
// Pass 1.5: the smoothed blend normal - see the header for why it is separate.
std::vector<Vec3d> blend_nrm = smooth_nrm;
if (settings.blend_normal_smoothing > 0 && unique_count > 0) {
// CSR adjacency over the welded graph, deliberately a multigraph: duplicates weight a pair by
// how often it shares an edge, so a well-connected surface couples more strongly.
std::vector<uint32_t> degree(unique_count, 0);
const auto add_degree = [&](int a, int b) {
if (a != b) { ++degree[size_t(a)]; ++degree[size_t(b)]; }
};
for (size_t t = 0; t + 2 < count; t += 3) {
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
add_degree(a, b); add_degree(b, c); add_degree(c, a);
}
std::vector<uint32_t> csr_start(unique_count + 1, 0);
for (size_t id = 0; id < unique_count; ++id)
csr_start[id + 1] = csr_start[id] + degree[id];
std::vector<uint32_t> neighbors(csr_start[unique_count]);
std::vector<uint32_t> cursor(unique_count, 0);
const auto add_edge = [&](int a, int b) {
if (a == b)
return;
neighbors[csr_start[size_t(a)] + cursor[size_t(a)]++] = uint32_t(b);
neighbors[csr_start[size_t(b)] + cursor[size_t(b)]++] = uint32_t(a);
};
for (size_t t = 0; t + 2 < count; t += 3) {
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
add_edge(a, b); add_edge(b, c); add_edge(c, a);
}
std::vector<Vec3d> cur = smooth_nrm, nxt(unique_count, Vec3d::Zero());
for (int iter = 0; iter < settings.blend_normal_smoothing; ++iter) {
// Jacobi, so every vertex reads the previous iteration and the rows are independent.
tbb::parallel_for(tbb::blocked_range<size_t>(0, unique_count, 4096), [&](const tbb::blocked_range<size_t> &r) {
for (size_t id = r.begin(); id < r.end(); ++id) {
const uint32_t s = csr_start[id], e = csr_start[id + 1];
if (e == s) {
nxt[id] = cur[id];
continue;
}
Vec3d sum = Vec3d::Zero();
for (uint32_t k = s; k < e; ++k)
sum += cur[neighbors[k]];
sum /= double(e - s);
const double len = sum.norm();
// Cancelling neighbours mean a knife edge; keep what we had.
nxt[id] = (len > 1e-12) ? Vec3d(sum / len) : cur[id];
}
});
cur.swap(nxt);
}
blend_nrm = std::move(cur);
}
// A boundary position borders both masked and unmasked faces, or sits on the exclusion seam.
// Every other position gets its distance to the nearest one, ramped to 1 at the falloff distance.
std::vector<double> falloff;
if (settings.boundary_falloff > 0.f && unique_count > 0) {
std::vector<Vec3f> boundary;
for (size_t id = 0; id < unique_count; ++id) {
const double frac = total_area[id] > 0.0 ? masked_area[id] / total_area[id] : 0.0;
const bool on_excl = !excluded_pos.empty() && excluded_pos[id] != 0;
if (on_excl || (frac > 0.0 && frac < 1.0))
boundary.push_back(id_pos[id]);
}
falloff.assign(unique_count, 1.0);
if (!boundary.empty()) {
// A uniform grid: the query is nearest-point only, so a tree costs more than it saves.
Vec3f lo = boundary.front(), hi = boundary.front();
for (const Vec3f &p : boundary) {
lo = lo.cwiseMin(p);
hi = hi.cwiseMax(p);
}
const Vec3f span = (hi - lo).cwiseMax(Vec3f(1e-6f, 1e-6f, 1e-6f));
const int res = std::clamp(int(std::ceil(std::cbrt(double(boundary.size())) * 2.0)), 4, 128);
const Vec3f cell = span / float(res);
const float cell_min = cell.minCoeff();
const auto cell_of = [&](const Vec3f &p) {
Vec3i32 c;
for (int k = 0; k < 3; ++k)
c[k] = std::clamp(int((p[k] - lo[k]) / span[k] * float(res)), 0, res - 1);
return c;
};
const auto cell_index = [&](int x, int y, int z) {
return size_t(z) * size_t(res) * size_t(res) + size_t(y) * size_t(res) + size_t(x);
};
std::vector<std::vector<int>> grid(size_t(res) * size_t(res) * size_t(res));
for (size_t i = 0; i < boundary.size(); ++i) {
const Vec3i32 c = cell_of(boundary[i]);
grid[cell_index(c.x(), c.y(), c.z())].push_back(int(i));
}
const double radius = double(settings.boundary_falloff);
for (size_t id = 0; id < unique_count; ++id) {
const Vec3f &p = id_pos[id];
const Vec3i32 c = cell_of(p);
double best = std::numeric_limits<double>::max();
// Anything in shell r is at least (r - 1) cells away, so once the best found is within
// that bound nothing closer can be hiding further out.
for (int r = 0; r < res; ++r) {
for (int dz = -r; dz <= r; ++dz)
for (int dy = -r; dy <= r; ++dy)
for (int dx = -r; dx <= r; ++dx) {
// The shell only; its interior was covered by a smaller r.
if (r > 0 && std::abs(dx) != r && std::abs(dy) != r && std::abs(dz) != r)
continue;
const int qx = c.x() + dx, qy = c.y() + dy, qz = c.z() + dz;
if (qx < 0 || qy < 0 || qz < 0 || qx >= res || qy >= res || qz >= res)
continue;
for (const int bi : grid[cell_index(qx, qy, qz)])
best = std::min(best, double((boundary[size_t(bi)] - p).norm()));
}
if (best <= double(r) * double(cell_min))
break;
}
falloff[id] = (best == std::numeric_limits<double>::max() || radius <= 0.0)
? 1.0
: std::clamp(best / radius, 0.0, 1.0);
}
}
}
// Pass 2: one sample per unique position. A representative corner is picked first so the sampling
// itself is a flat parallel loop - it is a texture fetch plus projection maths per layer, and by
// far the most expensive thing in this stage.
std::vector<double> grey(unique_count, 0.0);
std::vector<int> representative(unique_count, -1);
for (size_t i = 0; i < count; ++i)
if (representative[size_t(vertex_id[i])] < 0)
representative[size_t(vertex_id[i])] = int(i);
tbb::parallel_for(tbb::blocked_range<size_t>(0, unique_count),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t vid = range.begin(); vid < range.end(); ++vid) {
const int rep = representative[vid];
if (rep < 0)
continue;
grey[vid] = double(sample(geometry.pos[size_t(rep)],
smooth_nrm[vid].cast<float>(),
blend_nrm[vid].cast<float>()));
}
});
// Pass 3: move every copy of a position by the identical vector. Each iteration writes only its
// own output slot, so the loop is independent per corner.
tbb::parallel_for(tbb::blocked_range<size_t>(0, count), [&](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
const Vec3f &p = geometry.pos[i];
const size_t vid = size_t(vertex_id[i]);
// Only angle masking uses the per-position blend, so an excluded face never dims its
// neighbours through a shared vertex.
const bool face_excluded = !user_excluded_face.empty() && user_excluded_face[i / 3] != 0;
// Pinned where an included face shares a position with an excluded one, sealing the boundary.
const bool sealed_boundary =
!face_excluded && !excluded_pos.empty() && excluded_pos[vid] != 0;
const double masked_frac = total_area[vid] > 0.0 ? masked_area[vid] / total_area[vid] : 0.0;
const double centered = settings.symmetric ? (grey[vid] - 0.5) : grey[vid];
const double ramp = falloff.empty() ? 1.0 : falloff[vid];
const double disp = (face_excluded || sealed_boundary)
? 0.0
: ramp * (1.0 - masked_frac) * centered * double(settings.amplitude);
Vec3d moved = p.cast<double>() + smooth_nrm[vid] * disp;
// Stop a partly masked vertex poking through the surface it borders.
if (masked_frac > 0.0) {
if (settings.bottom_angle_limit > 0.f && moved.z() < double(p.z())) moved.z() = double(p.z());
if (settings.top_angle_limit > 0.f && moved.z() > double(p.z())) moved.z() = double(p.z());
}
if (settings.no_downward_z && moved.z() < double(p.z()))
moved.z() = double(p.z());
// A vertex starting on the bottom plane stays there: otherwise a downward-facing face pulls
// *up* where the sample is below mid-grey, leaving bed-contact vertices at differing heights.
if (settings.no_downward_z && double(p.z()) <= double(bounds.min.z()) + 1e-5)
moved.z() = double(p.z());
out.pos[i] = moved.cast<float>();
}
});
// Per-face, not averaged across shared positions: averaging can flip an excluded face's normal
// when its neighbours moved outward.
tbb::parallel_for(tbb::blocked_range<size_t>(0, count / 3), [&](const tbb::blocked_range<size_t> &r) {
for (size_t f = r.begin(); f < r.end(); ++f) {
const size_t t = f * 3;
const Vec3f n = (out.pos[t + 1] - out.pos[t]).cross(out.pos[t + 2] - out.pos[t]).normalized();
out.nrm[t] = out.nrm[t + 1] = out.nrm[t + 2] = n;
}
});
out.exclude_weight = geometry.exclude_weight;
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,68 @@
#pragma once
// Displacement along surface normals.
//
// The mesh is non-indexed, so at a shared edge two triangles hold the same position with different
// face normals; displacing each copy along its own normal sends them to different points and opens a
// crack. So one smooth (area-weighted) normal per unique position drives both the sample lookup and
// the displacement direction, every copy moves by the same vector, and the result is watertight by
// construction. Displaced normals are then smooth at hard edges, but the geometry is still faceted,
// so printed edges stay sharp.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Height at a point, called once per unique welded position. `smooth_normal` is the vector the
// displacement will move along; `blend_normal` is that after smoothing, for projection blend weights.
using HeightSampleFn = std::function<float(const Vec3f &position, const Vec3f &smooth_normal,
const Vec3f &blend_normal)>;
struct DisplaceSettings
{
// Displacement height in mm, applied to the sampled value.
float amplitude = 0.4f;
// Sample around a mid-grey rest level rather than displacing outward only.
bool symmetric = false;
// Faces flatter than these (degrees from horizontal) are held back, leaving bed-contact and top
// surfaces alone. 0 disables that side.
float bottom_angle_limit = 5.f;
float top_angle_limit = 0.f;
// Never move a vertex below its original Z, so no new overhang. The sideways component is kept.
bool no_downward_z = false;
// Distance in mm over which displacement ramps up from a mask boundary. 0 leaves a hard edge.
float boundary_falloff = 0.f;
// Laplacian iterations on the blend normal only - the displacement direction must stay the exact
// smooth normal or copies of a position move differently and the mesh cracks. Inside a blend band
// the weight gradient is largest, so a few degrees of vertex-to-vertex jitter multiplies the
// difference between two unrelated height samples into visible seam noise. A no-op on an
// already-smooth surface.
int blend_normal_smoothing = 32;
};
// Model extents; only the minimum Z is read, for the bottom-plane clamp.
struct DisplaceBounds
{
Vec3f min = Vec3f::Zero();
Vec3f max = Vec3f::Zero();
};
// Returns false to cancel.
using DisplaceProgressFn = std::function<bool(double fraction)>;
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
const DisplaceSettings &settings, const DisplaceBounds &bounds,
const DisplaceProgressFn &on_progress = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,212 @@
#include "TextureBakeFlip.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/parallel_sort.h>
namespace Slic3r {
namespace TextureBake {
FlipResult flip_edges_to_height(const TriSoup &geometry, const std::vector<int> &face_parent_id,
const HeightSampleFn &sample, const FlipSettings &settings,
const std::vector<uint8_t> &locked)
{
FlipResult result;
result.geometry = geometry;
result.face_parent_id = face_parent_id;
const size_t count = geometry.pos.size();
const size_t tri_ct = count / 3;
if (count == 0 || !sample || settings.passes <= 0)
return result;
// Weld, so a flip rewrites triangles in terms of shared vertices rather than positions.
QuantizedPointMap weld(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
std::vector<int> vid(count);
std::vector<Vec3f> pos;
std::vector<float> weight; // exclude weight per unique vertex, the max over its copies
const bool has_weight = !geometry.exclude_weight.empty();
for (size_t i = 0; i < count; ++i) {
vid[i] = weld.get_or_set(geometry.pos[i], int(pos.size()));
if (weld.inserted()) {
pos.push_back(geometry.pos[i]);
weight.push_back(has_weight ? geometry.exclude_weight[i] : 0.f);
} else if (has_weight) {
weight[size_t(vid[i])] = std::max(weight[size_t(vid[i])], geometry.exclude_weight[i]);
}
}
const size_t nv = pos.size();
// Triangles as vertex ids; a locked or excluded triangle never takes part.
std::vector<std::array<int, 3>> tri(tri_ct);
std::vector<uint8_t> fixed(tri_ct, 0);
for (size_t t = 0; t < tri_ct; ++t) {
tri[t] = { vid[t * 3], vid[t * 3 + 1], vid[t * 3 + 2] };
if (!locked.empty() && t < locked.size() && locked[t])
fixed[t] = 1;
if (has_weight && geometry.exclude_weight[t * 3] > 0.99f)
fixed[t] = 1;
}
// Height per unique vertex along its area-weighted normal, the direction displacement will use.
std::vector<Vec3f> nrm(nv, Vec3f::Zero());
std::vector<Vec3f> face_n(tri_ct);
const auto rebuild_normals = [&]() {
std::fill(nrm.begin(), nrm.end(), Vec3f::Zero());
for (size_t t = 0; t < tri_ct; ++t) {
const Vec3f fn = (pos[size_t(tri[t][1])] - pos[size_t(tri[t][0])]).cross(pos[size_t(tri[t][2])] - pos[size_t(tri[t][0])]);
face_n[t] = fn;
for (int k = 0; k < 3; ++k)
nrm[size_t(tri[t][size_t(k)])] += fn;
}
for (Vec3f &n : nrm) {
const float l = n.norm();
n = (l > 0.f) ? Vec3f(n / l) : Vec3f(0.f, 0.f, 1.f);
}
};
rebuild_normals();
std::vector<float> h(nv, 0.f);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
for (size_t v = r.begin(); v < r.end(); ++v)
h[v] = sample(pos[v], nrm[v], nrm[v]);
});
float h_lo = std::numeric_limits<float>::max(), h_hi = -h_lo;
for (const float x : h) { h_lo = std::min(h_lo, x); h_hi = std::max(h_hi, x); }
const float range = h_hi - h_lo;
if (!(range > 0.f))
return result; // flat: every diagonal is as good as the other
const float min_gain = float(settings.min_gain_fraction) * range;
const float planar = float(settings.min_planar_cos);
struct Candidate
{
uint32_t t1, t2; // the two triangles
uint8_t k1, k2; // corner index in each where the shared edge starts (t1: a->c, t2: c->a)
float gain;
};
for (int pass = 0; pass < settings.passes; ++pass) {
// Half-edges keyed by their undirected edge, sorted so the two halves of an interior edge land
// next to each other; a run of exactly two with opposite directions is a manifold interior
// edge. Sorting beats hashing here by an order of magnitude on a few million triangles.
struct Half { uint64_t key; uint32_t corner; };
std::vector<Half> half;
half.reserve(count);
for (size_t t = 0; t < tri_ct; ++t)
for (int k = 0; k < 3; ++k) {
const int from = tri[t][size_t(k)], to = tri[t][size_t((k + 1) % 3)];
if (from == to) continue;
const uint32_t lo = uint32_t(std::min(from, to)), hi = uint32_t(std::max(from, to));
half.push_back({ (uint64_t(lo) << 32) | hi, uint32_t(t * 3 + size_t(k)) });
}
tbb::parallel_sort(half.begin(), half.end(), [](const Half &x, const Half &y) {
return x.key != y.key ? x.key < y.key : x.corner < y.corner;
});
std::vector<std::pair<uint32_t, uint32_t>> edges; // (corner in t1, corner in t2), t1 < t2
edges.reserve(half.size() / 2);
for (size_t i = 0; i < half.size();) {
size_t j = i + 1;
while (j < half.size() && half[j].key == half[i].key) ++j;
if (j - i == 2) {
// Opposite directions: the lower vertex id is `from` in exactly one of the two.
const uint32_t c1 = half[i].corner, c2 = half[i + 1].corner;
const int f1 = tri[c1 / 3][size_t(c1 % 3)], f2 = tri[c2 / 3][size_t(c2 % 3)];
if (f1 != f2)
edges.emplace_back(c1, c2);
}
i = j;
}
std::vector<Candidate> cands(edges.size());
std::vector<uint8_t> valid(edges.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, edges.size()), [&](const tbb::blocked_range<size_t> &r) {
for (size_t i = r.begin(); i < r.end(); ++i) {
const uint32_t c1 = uint32_t(edges[i].first), c2 = uint32_t(edges[i].second);
const uint32_t t1 = c1 / 3, t2 = c2 / 3;
const int k1 = int(c1 % 3), k2 = int(c2 % 3);
if (fixed[t1] || fixed[t2]) continue;
if (!face_parent_id.empty() && face_parent_id[t1] != face_parent_id[t2]) continue;
// a->c is the shared edge in t1, with b opposite; t2 runs c->a with d opposite.
const int a = tri[t1][size_t(k1)], c = tri[t1][size_t((k1 + 1) % 3)], b = tri[t1][size_t((k1 + 2) % 3)];
const int d = tri[t2][size_t((k2 + 2) % 3)];
if (b == d) continue;
// Level quads have nothing to gain; the test is on the corners, before any sampling.
const float hmin = std::min({ h[size_t(a)], h[size_t(b)], h[size_t(c)], h[size_t(d)] });
const float hmax = std::max({ h[size_t(a)], h[size_t(b)], h[size_t(c)], h[size_t(d)] });
if (hmax - hmin < min_gain) continue;
// Coplanar enough to have a real alternative, and convex so the alternative is valid:
// the new triangles (a, d, b) and (d, c, b) must both face the way the quad does, with
// a decent share of its area.
const Vec3f n1 = face_n[t1], n2 = face_n[t2];
const float l1 = n1.norm(), l2 = n2.norm();
if (l1 <= 0.f || l2 <= 0.f || n1.dot(n2) < planar * l1 * l2) continue;
const Vec3f quad_n = (n1 + n2).normalized();
const Vec3f &pa = pos[size_t(a)], &pb = pos[size_t(b)], &pc = pos[size_t(c)], &pd = pos[size_t(d)];
const Vec3f m1 = (pd - pa).cross(pb - pa), m2 = (pc - pd).cross(pb - pd);
const float area_old = l1 + l2, area_new = m1.dot(quad_n) + m2.dot(quad_n);
const float min_part = 0.05f * area_old;
if (m1.dot(quad_n) < min_part || m2.dot(quad_n) < min_part) continue;
if (std::abs(area_new - area_old) > 0.02f * area_old) continue; // not the same quad: folded
// The diagonals' midpoint errors.
const auto mid_err = [&](int u, int w) {
const Vec3f p = 0.5f * (pos[size_t(u)] + pos[size_t(w)]);
Vec3f n = nrm[size_t(u)] + nrm[size_t(w)];
const float l = n.norm();
n = (l > 0.f) ? Vec3f(n / l) : quad_n;
return std::abs(sample(p, n, n) - 0.5f * (h[size_t(u)] + h[size_t(w)]));
};
const float gain = mid_err(a, c) - mid_err(b, d);
if (gain < min_gain) continue;
cands[i] = { t1, t2, uint8_t(k1), uint8_t(k2), gain };
valid[i] = 1;
}
});
std::vector<Candidate> chosen;
for (size_t i = 0; i < cands.size(); ++i)
if (valid[i]) chosen.push_back(cands[i]);
std::sort(chosen.begin(), chosen.end(), [](const Candidate &x, const Candidate &y) {
return x.gain != y.gain ? x.gain > y.gain : (x.t1 != y.t1 ? x.t1 < y.t1 : x.t2 < y.t2);
});
// Best first, and a triangle changes at most once per pass.
std::vector<uint8_t> touched(tri_ct, 0);
size_t applied = 0;
for (const Candidate &cd : chosen) {
if (touched[cd.t1] || touched[cd.t2]) continue;
const int a = tri[cd.t1][cd.k1], c = tri[cd.t1][size_t((cd.k1 + 1) % 3)], b = tri[cd.t1][size_t((cd.k1 + 2) % 3)];
const int d = tri[cd.t2][size_t((cd.k2 + 2) % 3)];
tri[cd.t1] = { a, d, b };
tri[cd.t2] = { d, c, b };
touched[cd.t1] = touched[cd.t2] = 1;
++applied;
}
result.flipped += applied;
if (applied == 0)
break;
rebuild_normals(); // face normals feed the planarity test of the next pass
}
if (result.flipped == 0)
return result;
// Back to the soup: positions, weights and per-face normals from the (possibly rewritten) triangles.
for (size_t t = 0; t < tri_ct; ++t) {
for (int k = 0; k < 3; ++k) {
const size_t i = t * 3 + size_t(k);
const int v = tri[t][size_t(k)];
result.geometry.pos[i] = pos[size_t(v)];
if (has_weight)
result.geometry.exclude_weight[i] = weight[size_t(v)];
}
Vec3f n = (result.geometry.pos[t * 3 + 1] - result.geometry.pos[t * 3]).cross(result.geometry.pos[t * 3 + 2] - result.geometry.pos[t * 3]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
result.geometry.nrm[t * 3] = result.geometry.nrm[t * 3 + 1] = result.geometry.nrm[t * 3 + 2] = n;
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,57 @@
#pragma once
// Data-dependent edge flipping: choose each quad's diagonal to follow the height field, before any
// displacement happens.
//
// Refinement produces a regular grid, and a step in the height map that crosses that grid at an
// angle lands on alternating corners: one triangle of a quad gets a raised corner, the next does not,
// and the displaced wall comes out as a sawtooth the size of the grid. Finer triangles make the teeth
// smaller, never straight. The cause is the diagonal, not the density: with the diagonal running
// along the step both triangles of the quad sit cleanly on one side or the other, and the wall is a
// straight line between them.
//
// So, for every interior edge, compare the two diagonals of the quad it spans by how well each
// interpolates the field at its own midpoint - the sampled height there against the mean of its two
// endpoints - and keep the better one. A quad whose four corners are level is skipped outright, so
// flat regions cost nothing; a non-planar quad (a model crease) is never touched, nor is one whose
// triangles belong to different source faces or straddle the painted boundary.
#include <cstdint>
#include <vector>
#include "TextureBakeDisplace.hpp"
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct FlipSettings
{
// Passes over all edges. Flips interact through shared triangles, so a pass applies non-conflicting
// ones and the next pass picks up the rest; two or three settle a grid.
int passes = 3;
// A flip has to reduce the midpoint error by at least this fraction of the height range, so noise
// on a rough surface does not toggle diagonals for nothing.
double min_gain_fraction = 0.02;
// The two triangles have to be this coplanar (cosine of their normals' angle) for the quad to have
// a meaningful alternative diagonal at all: across a real crease there is none.
double min_planar_cos = 0.985; // ~10 degrees
};
struct FlipResult
{
TriSoup geometry;
std::vector<int> face_parent_id;
size_t flipped = 0;
};
// `face_parent_id` may be empty; when given it is carried through unchanged (a flip never crosses a
// parent boundary). `locked` flags triangles that must not change (per triangle, may be empty).
FlipResult flip_edges_to_height(const TriSoup &geometry, const std::vector<int> &face_parent_id,
const HeightSampleFn &sample, const FlipSettings &settings,
const std::vector<uint8_t> &locked);
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,25 @@
#include "TextureBakeIndex.hpp"
#include <algorithm>
namespace Slic3r {
namespace TextureBake {
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant)
{
WeldResult out;
QuantizedPointMap map(quant, std::min<size_t>(positions.size(), size_t(1) << 22));
out.vertex_id.resize(positions.size());
int next_id = 0;
for (size_t i = 0; i < positions.size(); ++i) {
const int id = map.get_or_set(positions[i], next_id);
if (map.inserted())
++next_id;
out.vertex_id[i] = id;
}
out.unique_count = next_id;
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,158 @@
#pragma once
// Vertex welding for the texture bake pipeline. The pipeline works on non-indexed triangle soup, so
// a shared point exists once per incident triangle with float noise between the copies; welding maps
// each quantised position to one integer id.
//
// The three grids below are deliberately not unified - changing one at a call site changes
// watertightness. 100 um matches the precision files are written with; 10 um keeps small fillet
// vertices distinct (they merge at 100 um, giving needle artifacts after displacement) while still
// absorbing float noise; 1 um is what collapse positioning needs.
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <vector>
#include "../Point.hpp"
namespace Slic3r {
namespace TextureBake {
static constexpr double WELD_GRID_EXPORT = 1e4; // 100 um
static constexpr double WELD_GRID_GEOMETRY = 1e5; // 10 um
static constexpr double WELD_GRID_DECIMATION = 1e6; // 1 um
// Round half toward positive infinity. Quantised coordinates hit exact halves often enough that the
// tie rule matters.
inline int64_t grid_round(double v) { return int64_t(std::floor(v + 0.5)); }
// Open-addressing table, linear probing: no allocation per lookup, exact integer key comparison.
// Values must be non-negative; -1 is the empty sentinel and what get() returns on a miss.
//
// Key and value live together in one 32-byte cell. They used to be four parallel arrays, which made a
// single probe touch four cache lines - and probing this table was 13% of a whole bake, because every
// stage welds the full soup through it.
class QuantizedPointMap
{
public:
explicit QuantizedPointMap(double quant, size_t expected = 256) : m_quant(quant)
{
size_t cap = 16;
const size_t target = std::max<size_t>(16, size_t(std::ceil(double(expected) / 0.6)));
while (cap < target)
cap *= 2;
alloc(cap);
}
size_t size() const { return m_size; }
// Whether the last get_or_set() inserted rather than found.
bool inserted() const { return m_inserted; }
int get(float x, float y, float z)
{
return m_cells[slot(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant),
grid_round(double(z) * m_quant))].val;
}
int get(const Vec3f &p) { return get(p.x(), p.y(), p.z()); }
// The value already stored for this position's grid cell; if there is none, store `value` and
// return it. inserted() then says which of the two happened.
int get_or_set(float x, float y, float z, int value)
{
return get_or_set_key(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant),
grid_round(double(z) * m_quant), value);
}
int get_or_set(const Vec3f &p, int value) { return get_or_set(p.x(), p.y(), p.z(), value); }
// The same table as a set of integer tuples (edge marking, midpoint cache). Quantisation is
// bypassed: routing ids through the float overloads loses precision above 2^24.
int get_key(int64_t a, int64_t b, int64_t c) { return m_cells[slot(a, b, c)].val; }
int get_or_set_key(int64_t a, int64_t b, int64_t c, int value)
{
const size_t i = slot(a, b, c);
Cell &cell = m_cells[i];
if (cell.val != -1) {
m_inserted = false;
return cell.val;
}
cell.qx = a; cell.qy = b; cell.qz = c;
cell.val = value;
m_inserted = true;
if (++m_size > size_t(double(m_cap) * 0.7))
grow();
return value;
}
private:
struct Cell
{
int64_t qx = 0, qy = 0, qz = 0;
int32_t val = -1;
};
void alloc(size_t cap)
{
m_cap = cap;
m_mask = cap - 1;
m_cells.assign(cap, Cell{});
}
size_t slot(int64_t qx, int64_t qy, int64_t qz) const
{
// Unsigned multiplies: the signed versions overflowed on nearly every key, which is undefined
// behaviour. The resulting bits are identical on every target OrcaSlicer builds for.
//
// A stronger 64-bit finalizer was tried and measured no faster - the probing that shows up in a
// profile is subdivide's parallel mark count, spread over every core, not long probe chains.
uint32_t h = (uint32_t(qx) * 0x9E3779B1u) ^ (uint32_t(qy) * 0x85EBCA77u) ^ (uint32_t(qz) * 0xC2B2AE3Du);
h ^= h >> 15;
size_t i = size_t(h) & m_mask;
// Equality is checked against the stored 64-bit keys, so truncating to 32 bits for the hash
// costs collisions at worst, never a wrong answer.
while (m_cells[i].val != -1) {
const Cell &c = m_cells[i];
if (c.qx == qx && c.qy == qy && c.qz == qz)
return i;
i = (i + 1) & m_mask;
}
return i;
}
void grow()
{
std::vector<Cell> old = std::move(m_cells);
alloc(m_cap * 2);
for (const Cell &c : old)
if (c.val != -1)
m_cells[slot(c.qx, c.qy, c.qz)] = c;
}
double m_quant;
size_t m_cap = 0, m_mask = 0, m_size = 0;
bool m_inserted = false;
std::vector<Cell> m_cells;
};
// Three consecutive entries per triangle. The indexers turn this into shared vertices where a stage
// needs adjacency.
struct TriSoup
{
std::vector<Vec3f> pos;
std::vector<Vec3f> nrm; // parallel to pos
std::vector<float> exclude_weight; // parallel to pos; empty when nothing is excluded
size_t triangle_count() const { return pos.size() / 3; }
bool empty() const { return pos.empty(); }
};
// Assign each vertex the sequential id of its quantised position, first occurrence winning.
struct WeldResult
{
std::vector<int> vertex_id;
int unique_count = 0;
};
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant);
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,62 @@
#include "TextureBakeMesh.hpp"
#include <algorithm>
namespace Slic3r {
namespace TextureBake {
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded)
{
TriSoup out;
const size_t n = its.indices.size();
out.pos.resize(n * 3);
out.nrm.resize(n * 3);
const bool have_excl = face_excluded.size() == n;
if (have_excl)
out.exclude_weight.resize(n * 3);
for (size_t t = 0; t < n; ++t) {
const stl_triangle_vertex_indices &tri = its.indices[t];
const Vec3f a = its.vertices[size_t(tri[0])];
const Vec3f b = its.vertices[size_t(tri[1])];
const Vec3f c = its.vertices[size_t(tri[2])];
Vec3f nrm = (b - a).cross(c - a);
const float len = nrm.norm();
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
out.pos[t * 3] = a;
out.pos[t * 3 + 1] = b;
out.pos[t * 3 + 2] = c;
// Per-face on purpose: the accurate indexer derives smooth normals and splits at sharp edges
// itself, so averaged ones would pre-empt that.
out.nrm[t * 3] = out.nrm[t * 3 + 1] = out.nrm[t * 3 + 2] = nrm;
if (have_excl) {
const float w = face_excluded[t] ? 1.f : 0.f;
out.exclude_weight[t * 3] = out.exclude_weight[t * 3 + 1] = out.exclude_weight[t * 3 + 2] = w;
}
}
return out;
}
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
{
indexed_triangle_set out;
const size_t n = soup.pos.size();
out.indices.reserve(n / 3);
QuantizedPointMap map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
for (size_t i = 0; i < n; ++i) {
id[i] = map.get_or_set(soup.pos[i], int(out.vertices.size()));
if (map.inserted())
out.vertices.push_back(soup.pos[i]);
}
for (size_t t = 0; t + 2 < n; t += 3) {
// Welded-together corners carry no area.
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
continue;
out.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
}
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,19 @@
#pragma once
// Conversion between the pipeline's triangle soup and the indexed mesh used elsewhere. The pipeline
// stays on soup because each stage welds on its own grid, and those differences are load-bearing.
#include "TextureBakeIndex.hpp"
#include "../TriangleMesh.hpp"
namespace Slic3r {
namespace TextureBake {
// `face_excluded`: one entry per input triangle, becoming the soup's per-corner exclusion weight.
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded = {});
// Welds at the geometry grid.
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup);
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,291 @@
#include "TextureBakePipeline.hpp"
#include "TextureBakeDebug.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <string>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace TextureBake {
void clamp_below_bottom(TriSoup &geometry, float bottom_z)
{
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
bool dirty = false;
for (int k = 0; k < 3; ++k)
if (geometry.pos[t + size_t(k)].z() < bottom_z) {
geometry.pos[t + size_t(k)].z() = bottom_z;
dirty = true;
}
if (!dirty)
continue;
Vec3f n = (geometry.pos[t + 1] - geometry.pos[t]).cross(geometry.pos[t + 2] - geometry.pos[t]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
geometry.nrm[t] = geometry.nrm[t + 1] = geometry.nrm[t + 2] = n;
}
}
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol)
{
const size_t vert_count = geometry.pos.size();
const size_t tri_count = vert_count / 3;
if (tri_count == 0 || tol <= 0.0)
return 0;
// Weld at the finest grid: by this point copies of one position are bit-identical, because every
// earlier stage moved them by the same vector.
QuantizedPointMap weld(WELD_GRID_DECIMATION, std::min(vert_count, size_t(1) << 22));
std::vector<int> vid(vert_count);
int unique = 0;
for (size_t i = 0; i < vert_count; ++i) {
vid[i] = weld.get_or_set(geometry.pos[i], unique);
if (weld.inserted())
++unique;
}
// Incident corners per position, CSR style.
std::vector<uint32_t> start(size_t(unique) + 1, 0);
for (size_t i = 0; i < vert_count; ++i)
++start[size_t(vid[i]) + 1];
for (size_t id = 0; id < size_t(unique); ++id)
start[id + 1] += start[id];
std::vector<uint32_t> inc(vert_count), cursor(size_t(unique), 0);
for (size_t i = 0; i < vert_count; ++i)
inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i);
const double fold_cos = std::cos(75.0 * M_PI / 180.0);
std::vector<uint8_t> dirty_tri(tri_count, 0);
for (size_t id = 0; id < size_t(unique); ++id) {
const float z = geometry.pos[inc[start[id]]].z();
if (z == bottom_z || std::abs(double(z) - double(bottom_z)) > tol)
continue;
// Simulate the move: every incident triangle must keep positive area and must not fold.
bool ok = true;
for (uint32_t k = start[id]; k < start[id + 1] && ok; ++k) {
const size_t t = size_t(inc[k]) / 3;
Vec3f p[3];
for (int v = 0; v < 3; ++v) {
p[v] = geometry.pos[t * 3 + size_t(v)];
if (vid[t * 3 + size_t(v)] == int(id))
p[v].z() = bottom_z;
}
const Vec3d on = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]).cast<double>();
const Vec3d nn = (p[1] - p[0]).cross(p[2] - p[0]).cast<double>();
const double o2 = on.squaredNorm(), n2 = nn.squaredNorm();
if (n2 < 1e-20) { ok = false; break; } // would collapse to zero area
if (o2 < 1e-20) continue; // already degenerate, cannot judge a rotation
const double dot = on.dot(nn);
if (dot < 0.0 || dot * dot < fold_cos * fold_cos * o2 * n2)
ok = false;
}
if (!ok)
continue;
for (uint32_t k = start[id]; k < start[id + 1]; ++k) {
geometry.pos[inc[k]].z() = bottom_z;
dirty_tri[size_t(inc[k]) / 3] = 1;
}
}
size_t dirty = 0;
for (size_t t = 0; t < tri_count; ++t) {
if (!dirty_tri[t])
continue;
++dirty;
Vec3f n = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
geometry.nrm[t * 3] = geometry.nrm[t * 3 + 1] = geometry.nrm[t * 3 + 2] = n;
}
return dirty;
}
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
const PipelineSettings &settings, const DisplaceBounds &bounds,
PipelineMode mode, const std::vector<uint8_t> &face_excluded,
const PipelineProgressFn &on_progress, BakeStageRecorder *debug)
{
PipelineResult result;
const auto report = [&](const char *stage, double f) {
return !on_progress || on_progress(stage, f);
};
// Per-stage wall time. The stages differ in cost by orders of magnitude depending on the model, so
// without this it is guesswork which one to attack.
auto clock_now = [] { return std::chrono::steady_clock::now(); };
auto t_stage = clock_now();
// One call site for both the log line and the debug capture, so a stage cannot appear in one and
// be missing from the other. The capture happens after the elapsed time is read: welding the soup
// and scanning its edges costs more than some of the stages do, and must not land inside the
// measurement it is reporting.
const auto lap = [&](const char *stage, const TriSoup &geometry, const std::string &detail = {}) {
const double ms = std::chrono::duration<double, std::milli>(clock_now() - t_stage).count();
BOOST_LOG_TRIVIAL(info) << "TextureBake " << stage << ": " << ms << " ms, "
<< geometry.triangle_count() << " tris";
if (debug != nullptr)
debug->capture(stage, geometry, ms, detail);
t_stage = clock_now();
};
if (input.empty() || !sample) {
result.geometry = input;
return result;
}
if (debug != nullptr)
debug->capture("input", input, 0.0, "as handed to the pipeline");
t_stage = clock_now(); // the capture above is not part of the first stage
// 1. Refine to the target edge length.
SubdivideResult sub = subdivide(
input, settings.refine_length, face_excluded, /* fast */ false, settings.safety_cap,
[&](double f, size_t, double) { return report("subdivide", f); });
result.safety_cap_hit = sub.safety_cap_hit;
lap("subdivide", sub.geometry);
if (!report("subdivide", 1.0)) {
result.canceled = true;
return result;
}
// 2. Dissolve the slivers refinement inherited, then recover the edges that lengthened.
if (settings.regularize) {
RegularizeOptions ropts = settings.regularize_opts;
ropts.preserve_excluded = settings.preserve_untextured;
RegularizeResult reg = regularize_mesh(sub.geometry, sub.face_parent_id,
settings.refine_length, ropts);
result.collapse_count = reg.collapse_count;
lap("regularize", reg.geometry, std::to_string(reg.collapse_count) + " collapses");
if (!report("regularize", 1.0)) {
result.canceled = true;
return result;
}
if (reg.collapse_count > 0) {
// Excluded faces are carried on the soup itself, so the flag is re-derived rather than
// indexed across the collapse.
std::vector<uint8_t> excl;
if (!reg.geometry.exclude_weight.empty()) {
excl.assign(reg.geometry.triangle_count(), 0);
for (size_t t = 0; t < excl.size(); ++t)
excl[t] = reg.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
sub = subdivide(reg.geometry, settings.refine_length * settings.regularize_second_pass_mul,
excl, false, settings.safety_cap,
[&](double f, size_t, double) { return report("re-subdivide", f); });
result.safety_cap_hit = result.safety_cap_hit || sub.safety_cap_hit;
// The second pass renumbers faces, so the parent map has to be composed through it.
std::vector<int> composed(sub.face_parent_id.size());
for (size_t i = 0; i < composed.size(); ++i) {
const int mid = sub.face_parent_id[i];
composed[i] = (mid >= 0 && size_t(mid) < reg.face_parent_id.size())
? reg.face_parent_id[size_t(mid)] : -1;
}
sub.face_parent_id = std::move(composed);
lap("re-subdivide", sub.geometry);
} else {
sub.geometry = std::move(reg.geometry);
sub.face_parent_id = std::move(reg.face_parent_id);
}
}
// 3. Align the mesh to the height field's edges, then displace.
if (settings.relocate) {
std::vector<uint8_t> locked;
if (settings.preserve_untextured && !sub.geometry.exclude_weight.empty()) {
locked.assign(sub.geometry.triangle_count(), 0);
for (size_t t = 0; t < locked.size(); ++t)
locked[t] = sub.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
RelocateResult rel = relocate_to_contours(sub.geometry, sample, settings.relocate_opts, locked);
BOOST_LOG_TRIVIAL(info) << "TextureBake relocate: moved=" << rel.moved
<< " rejected=" << rel.rejected;
sub.geometry = std::move(rel.geometry);
lap("relocate", sub.geometry,
"moved " + std::to_string(rel.moved) + ", rejected " + std::to_string(rel.rejected));
}
// 3b. Diagonals along the height field's steps, so they displace into straight walls.
if (settings.flip_edges) {
std::vector<uint8_t> locked;
if (settings.preserve_untextured && !sub.geometry.exclude_weight.empty()) {
locked.assign(sub.geometry.triangle_count(), 0);
for (size_t t = 0; t < locked.size(); ++t)
locked[t] = sub.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
FlipResult fl = flip_edges_to_height(sub.geometry, sub.face_parent_id, sample, settings.flip_opts, locked);
sub.geometry = std::move(fl.geometry);
sub.face_parent_id = std::move(fl.face_parent_id);
lap("align edges", sub.geometry, std::to_string(fl.flipped) + " flips");
if (!report("align edges", 1.0)) {
result.canceled = true;
return result;
}
}
TriSoup displaced = apply_displacement(sub.geometry, sample, settings.displace, bounds,
[&](double f) { return report("displace", f); });
lap("displace", displaced);
if (!report("displace", 1.0)) {
result.canceled = true;
return result;
}
// 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys.
std::vector<int> parent = std::move(sub.face_parent_id);
if (mode == PipelineMode::Export) {
// Only when the mesh is actually over budget. Harvesting flat faces on a mesh that already fits
// cost several times the decimation itself and degraded the relief it was handed; it now only
// runs as part of a decimation that has to happen anyway. The repair pass below keys off the
// same decision (it runs only when decimation did), so an under-budget bake skips both.
const bool needs_decimation = displaced.triangle_count() > settings.max_triangles;
if (needs_decimation) {
std::vector<uint8_t> locked;
if (settings.preserve_untextured && !displaced.exclude_weight.empty()) {
locked.assign(displaced.triangle_count(), 0);
for (size_t t = 0; t < locked.size(); ++t)
locked[t] = displaced.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
DecimateResult dec = decimate(displaced, settings.max_triangles, settings.harvest_flat,
settings.harvest_tol, locked,
[&](double f) { return report("decimate", f); });
result.locked_over_budget = dec.locked_over_budget;
displaced = std::move(dec.geometry);
lap("decimate", displaced, "over budget, simplified");
parent.clear(); // no longer meaningful
}
if (!report("decimate", 1.0)) {
result.canceled = true;
return result;
}
}
// 5. Flatten the bed-contact surface.
{
const bool clamped = settings.clamp_below_plate || settings.displace.bottom_angle_limit > 0.f;
if (clamped)
clamp_below_bottom(displaced, bounds.min.z());
size_t snapped = 0;
if (settings.bottom_snap_tol > 0.0)
snapped = snap_bottom_to_flat(displaced, bounds.min.z(), settings.bottom_snap_tol);
if (clamped || settings.bottom_snap_tol > 0.0)
lap("bottom clamp + snap", displaced, std::to_string(snapped) + " triangles snapped flat");
}
// 6. Close the T-junctions decimation left behind. Only meaningful when it ran.
if (mode == PipelineMode::Export && parent.empty()) {
displaced = resolve_t_junctions(displaced);
lap("repair", displaced);
}
result.geometry = std::move(displaced);
result.face_parent_id = std::move(parent);
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,124 @@
#pragma once
// The bake pipeline:
//
// subdivide -> [regularize -> re-subdivide] -> [relocate] -> displace -> [decimate]
// -> bottom clamp -> bottom snap -> [resolve T-junctions]
//
// Regularization sits between two subdivisions on purpose: it dissolves the slivers refinement
// inherited, which lengthens some edges past the target, and the second pass brings those back.
// Before any subdivision it would have nothing to work on, since the slivers come from refining a
// needle; after a single pass it would leave the mesh coarser than asked for.
//
// Decimation and repair are export-only - decimation drops the output-to-input face mapping a bake
// needs to carry per-face data forward.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeDecimate.hpp"
#include "TextureBakeDisplace.hpp"
#include "TextureBakeIndex.hpp"
#include "TextureBakeRegularize.hpp"
#include "TextureBakeFlip.hpp"
#include "TextureBakeRelocate.hpp"
#include "TextureBakeRepair.hpp"
#include "TextureBakeSubdivide.hpp"
namespace Slic3r {
// Optional step-by-step capture; see TextureBakeDebug.hpp. A pointer, and forward declared, so the
// pipeline header stays free of the mesh types the recorder converts into.
class BakeStageRecorder;
namespace TextureBake {
enum class PipelineMode
{
// Keeps the face-parent mapping; skips decimation and repair.
Bake,
// The full sequence, including decimation and repair.
Export,
};
struct PipelineSettings
{
// Target edge length for the refinement, in mm.
double refine_length = 1.0;
// Sliver removal between the two subdivision passes.
bool regularize = true;
RegularizeOptions regularize_opts;
// Slightly above the first pass, so it recovers the edges regularization lengthened instead of
// re-refining what it just merged.
double regularize_second_pass_mul = 1.1;
// Slide vertices onto the height map's own edges before displacing, so a step lands on a mesh
// edge instead of being quantised to wherever the grid fell.
bool relocate = false;
RelocateSettings relocate_opts;
// Choose each quad's diagonal to follow the height field before displacing, so a step that crosses
// the grid at an angle comes out as a straight wall instead of a sawtooth. See TextureBakeFlip.hpp.
bool flip_edges = true;
FlipSettings flip_opts;
DisplaceSettings displace;
// Export mode only.
size_t max_triangles = 750'000;
// Keep removing zero-cost flat faces past the target. Only applies when decimation runs, i.e. when
// the displaced mesh is over max_triangles - an under-budget mesh is never decimated.
bool harvest_flat = true;
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL;
// Lock the untextured region against both regularization and decimation.
bool preserve_untextured = true;
// Push anything that displaced below the plate back up to it. Downward movement is otherwise left
// alone, so relief on the underside is kept - only what would sink through the plate is stopped.
bool clamp_below_plate = false;
// Snap vertices within this of the bottom plane onto it. 0 disables.
double bottom_snap_tol = 0.1;
int safety_cap = SUBDIVIDE_SAFETY_CAP;
};
// Stage name and a fraction within it. Returning false cancels the run.
using PipelineProgressFn = std::function<bool(const char *stage, double fraction)>;
struct PipelineResult
{
TriSoup geometry;
// Output face -> input face. Empty in Export mode, where decimation invalidates it.
std::vector<int> face_parent_id;
bool safety_cap_hit = false;
bool locked_over_budget = false;
size_t collapse_count = 0;
bool canceled = false;
};
// `debug`, when given and enabled, receives the mesh after every stage that ran - which is the only
// way to tell which stage a bad result came from, since each one rewrites the whole mesh.
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
const PipelineSettings &settings, const DisplaceBounds &bounds,
PipelineMode mode, const std::vector<uint8_t> &face_excluded = {},
const PipelineProgressFn &on_progress = {},
BakeStageRecorder *debug = nullptr);
// Snap anything that ended below the model's original bottom back up to it.
void clamp_below_bottom(TriSoup &geometry, float bottom_z);
// Flatten the bed-contact surface by snapping positions within `tol` of the bottom plane onto it.
//
// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture bumps
// near the base, folding them coplanar into the bottom face. Folded faces overlap the plate, so edges
// there pick up four incident faces - non-manifold edges and phantom shells on re-import. All copies
// of a position move together, and the move is rejected if any incident triangle would go degenerate
// or rotate more than about 75 degrees. A real bed-contact sliver rotates by a fraction of a degree
// and still snaps. Returns how many triangles moved.
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol = 0.1);
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,393 @@
#include "TextureBakeRegularize.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
namespace {
// Vertex-to-triangle lists as intrusive doubly linked lists of corner slots over flat arrays. Slot s
// is corner (triangle * 3 + k), owned by corners[s]. Deleted and moved corners are unlinked, so a
// collapse costs no allocation.
struct SlotLists
{
std::vector<int> head, next, prev;
void init(size_t vertex_count, size_t slot_count)
{
head.assign(vertex_count, -1);
next.assign(slot_count, -1);
prev.assign(slot_count, -1);
}
void link(int s, const std::vector<int> &corners)
{
const int v = corners[size_t(s)];
const int h = head[size_t(v)];
prev[size_t(s)] = -1;
next[size_t(s)] = h;
if (h != -1)
prev[size_t(h)] = s;
head[size_t(v)] = s;
}
void unlink(int s, const std::vector<int> &corners)
{
const int p = prev[size_t(s)], n = next[size_t(s)];
if (p != -1) next[size_t(p)] = n;
else head[size_t(corners[size_t(s)])] = n;
if (n != -1) prev[size_t(n)] = p;
}
};
} // namespace
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
double max_edge_length, const RegularizeOptions &opts)
{
RegularizeResult result;
const size_t tri_count = geometry.triangle_count();
if (tri_count == 0 || max_edge_length <= 0.0) {
result.geometry = geometry;
result.face_parent_id = face_parent_id;
return result;
}
const double base_max_len_sq = (max_edge_length * opts.slack) * (max_edge_length * opts.slack);
const double aggr_max_len_sq =
(max_edge_length * opts.aggressive_slack) * (max_edge_length * opts.aggressive_slack);
const double extreme_aspect2 = opts.extreme_sliver_aspect * opts.extreme_sliver_aspect;
const double aspect_thr2 = opts.aspect_threshold * opts.aspect_threshold;
// Double precision: a collapse writes a midpoint back and later collapses read it, so rounding
// would accumulate.
QuantizedPointMap pos_map(WELD_GRID_GEOMETRY, std::min(tri_count * 3, size_t(1) << 22));
std::vector<Vec3d> vert;
std::vector<int> corners(tri_count * 3);
vert.reserve(tri_count);
for (size_t i = 0; i < tri_count * 3; ++i) {
const Vec3f &p = geometry.pos[i];
const int id = pos_map.get_or_set(p, int(vert.size()));
if (pos_map.inserted())
vert.push_back(p.cast<double>());
corners[i] = id;
}
const size_t vert_count = vert.size();
std::vector<Vec3d> tri_nrm(tri_count, Vec3d::Zero());
std::vector<uint8_t> tri_deleted(tri_count, 0);
result.face_parent_id = face_parent_id;
if (result.face_parent_id.size() != tri_count)
result.face_parent_id.assign(tri_count, 0);
const auto sq_dist = [&](int a, int b) { return (vert[size_t(a)] - vert[size_t(b)]).squaredNorm(); };
const auto recompute_face_normal = [&](size_t t) {
const Vec3d &a = vert[size_t(corners[t * 3])];
const Vec3d n = (vert[size_t(corners[t * 3 + 1])] - a).cross(vert[size_t(corners[t * 3 + 2])] - a);
const double len = n.norm();
tri_nrm[t] = (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
};
for (size_t t = 0; t < tri_count; ++t)
recompute_face_normal(t);
// Never updated - the normal gate measures against these, so drift cannot compound across rounds.
const std::vector<Vec3d> orig_nrm = tri_nrm;
// Squared thinness, the longest edge over the shortest altitude:
// thinness = lmax / hmin = lmax^2 / (2 * area), so thinness^2 = lmax^4 / |AB x AC|^2
//
// Not lmax/lmin, which misses what matters here: three near-collinear points can have all edges
// similar, so an edge ratio reports about 2 and the gate skips a triangle with near-zero area
// whose corners sample three unrelated texels. An equilateral scores about 1.15.
const auto tri_aspect_sq = [&](size_t t) -> double {
const Vec3d &a = vert[size_t(corners[t * 3])];
const Vec3d ab = vert[size_t(corners[t * 3 + 1])] - a;
const Vec3d ac = vert[size_t(corners[t * 3 + 2])] - a;
const Vec3d bc = vert[size_t(corners[t * 3 + 2])] - vert[size_t(corners[t * 3 + 1])];
const double lmax2 = std::max({ ab.squaredNorm(), ac.squaredNorm(), bc.squaredNorm() });
const double cross2 = ab.cross(ac).squaredNorm();
return cross2 > 0.0 ? lmax2 * lmax2 / cross2 : std::numeric_limits<double>::infinity();
};
SlotLists slots;
slots.init(vert_count, tri_count * 3);
for (size_t s = 0; s < tri_count * 3; ++s)
slots.link(int(s), corners);
// O(1) membership without clearing a set per collapse.
std::vector<uint32_t> vert_stamp(vert_count, 0), tri_stamp(tri_count, 0);
uint32_t stamp_gen = 0;
// Both endpoints of a hard edge are barred from being collapse endpoints, preserving such
// corners exactly while leaving flat-face interiors free.
//
// Skipped when either triangle is an extreme sliver: a sliver's normal is dominated by where its
// far apex sits, so noise pivots it tens of degrees with no feature behind it, and freezing on
// that would lock the very chains this pass exists to dissolve. Genuine features are bordered by
// well-shaped triangles and are unaffected.
std::vector<uint8_t> frozen_vert(vert_count, 0);
{
std::vector<double> tri_thin2(tri_count);
for (size_t t = 0; t < tri_count; ++t)
tri_thin2[t] = tri_aspect_sq(t);
QuantizedPointMap edge_seen(1.0, std::min(tri_count * 3, size_t(1) << 22));
for (size_t t = 0; t < tri_count; ++t)
for (int e = 0; e < 3; ++e) {
const int u = corners[t * 3 + size_t(e)];
const int v = corners[t * 3 + size_t((e + 1) % 3)];
const int lo = std::min(u, v), hi = std::max(u, v);
const int other = edge_seen.get_or_set_key(lo, hi, 0, int(t));
if (edge_seen.inserted())
continue;
if (tri_thin2[t] > extreme_aspect2 || tri_thin2[size_t(other)] > extreme_aspect2)
continue;
if (tri_nrm[t].dot(tri_nrm[size_t(other)]) < opts.sharp_edge_cos) {
frozen_vert[size_t(u)] = 1;
frozen_vert[size_t(v)] = 1;
}
}
}
// Exclusion freeze. The weight is constant across a face's corners, so the first one answers.
if (opts.preserve_excluded && !geometry.exclude_weight.empty())
for (size_t t = 0; t < tri_count; ++t)
if (geometry.exclude_weight[t * 3] > 0.99f)
for (int k = 0; k < 3; ++k)
frozen_vert[size_t(corners[t * 3 + size_t(k)])] = 1;
std::vector<int> wing_scratch, affected_scratch;
const auto third_vertex = [&](size_t t, int u, int v) {
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
if (a != u && a != v) return a;
if (b != u && b != v) return b;
return c;
};
const auto triangles_sharing_edge = [&](int u, int v) -> std::vector<int> & {
wing_scratch.clear();
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
if (corners[t * 3] == v || corners[t * 3 + 1] == v || corners[t * 3 + 2] == v)
wing_scratch.push_back(int(t));
}
return wing_scratch;
};
RegularizeRejectStats &stats = result.reject_stats;
const auto try_collapse = [&](int u, int v) -> bool {
if (u == v)
return false;
if (frozen_vert[size_t(u)] || frozen_vert[size_t(v)]) { ++stats.frozen; return false; }
// Two wings means a manifold interior edge.
std::vector<int> &wings = triangles_sharing_edge(u, v);
if (wings.size() != 2) { ++stats.wing_count; return false; }
const size_t w0 = size_t(wings[0]), w1 = size_t(wings[1]);
const int apex1 = third_vertex(w0, u, v), apex2 = third_vertex(w1, u, v);
if (apex1 == apex2) { ++stats.folded_apex; return false; }
// The edge cap loosens if *either* wing is extreme, since the re-subdivision recovers an
// over-long edge. The normal cap needs *both*, which is what protects fillets.
const double w1a = tri_aspect_sq(w0), w2a = tri_aspect_sq(w1);
const bool either_extreme = w1a > extreme_aspect2 || w2a > extreme_aspect2;
const bool both_extreme = w1a > extreme_aspect2 && w2a > extreme_aspect2;
const double eff_max_len_sq = either_extreme ? aggr_max_len_sq : base_max_len_sq;
const double eff_normal_cos =
both_extreme ? opts.aggressive_normal_delta_cos : opts.max_normal_delta_cos;
// A vertex sharing a triangle with both endpoints, other than the wing apexes, would go
// non-manifold. Stamp one side's neighbours, scan the other against them.
++stamp_gen;
for (int s = slots.head[size_t(v)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k)
if (const int x = corners[t * 3 + size_t(k)]; x != v)
vert_stamp[size_t(x)] = stamp_gen;
}
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k) {
const int x = corners[t * 3 + size_t(k)];
if (x != u && x != v && x != apex1 && x != apex2 && vert_stamp[size_t(x)] == stamp_gen) {
++stats.link_condition;
return false;
}
}
}
const Vec3d m = (vert[size_t(u)] + vert[size_t(v)]) * 0.5;
// Everything using either endpoint; the wings are being deleted.
++stamp_gen;
affected_scratch.clear();
for (const int endpoint : { u, v })
for (int s = slots.head[size_t(endpoint)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t] || t == w0 || t == w1)
continue;
if (tri_stamp[t] != stamp_gen) {
tri_stamp[t] = stamp_gen;
affected_scratch.push_back(int(t));
}
}
// Validate every affected triangle before touching anything.
for (const int ti : affected_scratch) {
const size_t t = size_t(ti);
Vec3d p[3];
for (int k = 0; k < 3; ++k) {
const int x = corners[t * 3 + size_t(k)];
p[k] = (x == u || x == v) ? m : vert[size_t(x)];
}
const double ab2 = (p[1] - p[0]).squaredNorm();
const double bc2 = (p[2] - p[1]).squaredNorm();
const double ca2 = (p[0] - p[2]).squaredNorm();
if (ab2 > eff_max_len_sq || bc2 > eff_max_len_sq || ca2 > eff_max_len_sq) {
++stats.edge_cap;
return false;
}
const Vec3d n = (p[1] - p[0]).cross(p[2] - p[0]);
const double nlen = n.norm();
if (nlen <= 0.0) { ++stats.degenerate; return false; }
if ((n / nlen).dot(orig_nrm[t]) < eff_normal_cos) { ++stats.normal_change; return false; }
}
// Apply: move u to the merged position and redirect every reference to v.
vert[size_t(u)] = m;
for (const size_t w : { w0, w1 }) {
tri_deleted[w] = 1;
for (int k = 0; k < 3; ++k)
slots.unlink(int(w * 3) + k, corners);
}
// A non-wing triangle contains v exactly once, so moving its slots suffices.
for (int s = slots.head[size_t(v)]; s != -1;) {
const int ns = slots.next[size_t(s)];
slots.unlink(s, corners);
corners[size_t(s)] = u;
slots.link(s, corners);
recompute_face_normal(size_t(s) / 3);
s = ns;
}
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (!tri_deleted[t])
recompute_face_normal(t);
}
return true;
};
// Per-triangle thinness for a round's candidate scan; -1 marks "not a candidate". Scored in
// parallel, since the scan only reads the mesh, then gathered serially in index order.
std::vector<double> round_aspect(tri_count);
for (int round = 0; round < opts.maxrounds; ++round) {
// Rebuilt each round so earlier collapses inform the priorities.
tbb::parallel_for(tbb::blocked_range<size_t>(0, tri_count), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
round_aspect[t] = -1.0;
if (tri_deleted[t])
continue;
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
if (std::min({ sq_dist(a, b), sq_dist(b, c), sq_dist(c, a) }) <= 0.0)
continue;
const double aspect2 = tri_aspect_sq(t);
if (aspect2 >= aspect_thr2)
round_aspect[t] = aspect2;
}
});
// Worst first; ties keep ascending triangle order so the pass is deterministic. Sorting the
// (thinness, triangle) pairs themselves gives exactly the order the index sort with its
// indirect comparator did, without that comparator's extra lookup on every comparison.
std::vector<std::pair<double, int>> cand;
for (size_t t = 0; t < tri_count; ++t)
if (round_aspect[t] >= 0.0)
cand.emplace_back(round_aspect[t], int(t));
std::sort(cand.begin(), cand.end(), [](const std::pair<double, int> &x, const std::pair<double, int> &y) {
return x.first != y.first ? x.first > y.first : x.second < y.second;
});
size_t round_collapses = 0;
for (const std::pair<double, int> &entry : cand) {
const size_t t = size_t(entry.second);
if (tri_deleted[t])
continue;
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
// All three edges, shortest first: a sliver straddling a seam has its shortest edge
// crossing it, which the normal gate refuses, while a long edge along one surface
// collapses safely. Trying only the shortest would leave those stuck.
struct Cand { double len2; int u, v; };
Cand e[3] = { { sq_dist(a, b), a, b }, { sq_dist(b, c), b, c }, { sq_dist(c, a), c, a } };
std::stable_sort(std::begin(e), std::end(e),
[](const Cand &x, const Cand &y) { return x.len2 < y.len2; });
if (try_collapse(e[0].u, e[0].v) || try_collapse(e[1].u, e[1].v) ||
try_collapse(e[2].u, e[2].v))
++round_collapses;
}
result.collapse_count += round_collapses;
if (round_collapses == 0)
break;
}
// Drop deleted triangles and rebuild the soup.
const bool have_weights = !geometry.exclude_weight.empty();
const size_t survivors = tri_count - size_t(std::count(tri_deleted.begin(), tri_deleted.end(), uint8_t(1)));
std::vector<int> out_parent;
TriSoup &out = result.geometry;
out_parent.reserve(survivors);
out.pos.reserve(survivors * 3);
if (have_weights)
out.exclude_weight.reserve(survivors * 3);
for (size_t t = 0; t < tri_count; ++t) {
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k)
out.pos.push_back(vert[size_t(corners[t * 3 + size_t(k)])].cast<float>());
if (have_weights) {
// Constant across a face's corners.
const float w = geometry.exclude_weight[t * 3];
out.exclude_weight.insert(out.exclude_weight.end(), { w, w, w });
}
out_parent.push_back(result.face_parent_id[t]);
}
result.face_parent_id = std::move(out_parent);
// Rebuilt from the compacted geometry - the collapses moved vertices.
out.nrm.assign(out.pos.size(), Vec3f::Zero());
{
QuantizedPointMap weld(WELD_GRID_GEOMETRY, out.pos.size());
std::vector<int> vid(out.pos.size());
int next = 0;
for (size_t i = 0; i < out.pos.size(); ++i) {
vid[i] = weld.get_or_set(out.pos[i], next);
if (weld.inserted())
++next;
}
std::vector<Vec3d> vn(size_t(next), Vec3d::Zero());
for (size_t t = 0; t * 3 < out.pos.size(); ++t) {
const Vec3d a = out.pos[t * 3].cast<double>();
const Vec3d n = (out.pos[t * 3 + 1].cast<double>() - a).cross(out.pos[t * 3 + 2].cast<double>() - a);
for (int k = 0; k < 3; ++k)
vn[size_t(vid[t * 3 + size_t(k)])] += n;
}
for (size_t i = 0; i < out.pos.size(); ++i) {
const Vec3d &n = vn[size_t(vid[i])];
const double l = n.norm();
out.nrm[i] = (l > 0.0) ? Vec3d(n / l).cast<float>() : Vec3f(0.f, 0.f, 1.f);
}
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,73 @@
#pragma once
// Sliver removal by short-edge collapse.
//
// Subdivision turns tessellation needles into chains of slivers that are within the edge-length
// budget but still poor triangles. A sliver's three vertices land on three unrelated texels, so the
// relief picks up noise that is an artifact of the tessellation rather than of the image.
//
// A candidate's edge is collapsed to its midpoint only if it passes three gates: no affected
// triangle may exceed the target edge times a slack factor; every affected triangle must keep its
// face normal within a bound of its *original* direction (which is what stops curved surfaces being
// flattened); and the link condition must hold, or the result would be non-manifold. Boundary and
// non-manifold edges are skipped outright. Rounds repeat until one achieves nothing.
#include <cstdint>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct RegularizeOptions
{
// Candidate threshold. Set to catch real slivers - chains measure in the hundreds - without
// sweeping up moderate fillet triangles, which sit between 2 and 5.
double aspect_threshold = 5.0;
// The base tier is loose on purpose: non-sliver boundary collapses must keep succeeding, since
// those give a chain the room to dissolve. A tight base leaves chains worse than before. The
// aggressive tier applies when at least one wing is an extreme sliver.
double slack = 3.0;
double aggressive_slack = 8.0;
// Thinness above which a wing counts as extreme: longest edge over shortest altitude.
double extreme_sliver_aspect = 8.0;
// Measured against each triangle's normal from before any collapse ran, so rounds of small
// allowed drift cannot compound into corner damage. Asymmetric two-tier: the loose bound needs
// *both* wings extreme, which matches a needle chain on a curved face but not a sliver beside a
// fillet, so fillets keep the tight bound.
double max_normal_delta_cos = 0.965925826289; // cos(15 degrees)
double aggressive_normal_delta_cos = 0.906307787037; // cos(25 degrees)
// Vertices on edges sharper than this are frozen, so hard features keep every original vertex.
double sharp_edge_cos = 0.866025403784; // cos(30 degrees)
int maxrounds = 8;
// Freeze excluded faces entirely, so untextured geometry is never modified.
bool preserve_excluded = false;
};
// Which gate blocked a collapse - the only practical way to tell why a region failed to merge.
struct RegularizeRejectStats
{
size_t frozen = 0, wing_count = 0, link_condition = 0, edge_cap = 0, normal_change = 0,
degenerate = 0, folded_apex = 0;
};
struct RegularizeResult
{
TriSoup geometry;
std::vector<int> face_parent_id;
size_t collapse_count = 0;
RegularizeRejectStats reject_stats;
};
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
double max_edge_length, const RegularizeOptions &opts = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,205 @@
#include "TextureBakeRelocate.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
namespace Slic3r {
namespace TextureBake {
namespace {
// Any two unit vectors orthogonal to n. Which two does not matter - the gradient is expressed in this
// basis and converted straight back, so the result is basis independent.
void tangent_basis(const Vec3f &n, Vec3f &t1, Vec3f &t2)
{
const Vec3f a = (std::abs(n.x()) < 0.9f) ? Vec3f(1.f, 0.f, 0.f) : Vec3f(0.f, 1.f, 0.f);
t1 = n.cross(a).normalized();
t2 = n.cross(t1).normalized();
}
} // namespace
RelocateResult relocate_to_contours(const TriSoup &geometry, const HeightSampleFn &sample,
const RelocateSettings &settings, const std::vector<uint8_t> &locked)
{
RelocateResult result;
result.geometry = geometry;
const size_t count = geometry.pos.size();
const size_t tri_ct = count / 3;
if (count == 0 || !sample || settings.iterations <= 0)
return result;
// Weld, so every copy of a position moves together and the mesh cannot come apart.
QuantizedPointMap weld(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
std::vector<int> vid(count);
std::vector<Vec3f> pos;
for (size_t i = 0; i < count; ++i) {
vid[i] = weld.get_or_set(geometry.pos[i], int(pos.size()));
if (weld.inserted())
pos.push_back(geometry.pos[i]);
}
const size_t nv = pos.size();
// Incident corners per position, CSR style, plus the mean incident edge length that sets the scale
// for both the finite difference and the move limit.
std::vector<uint32_t> start(nv + 1, 0);
for (size_t i = 0; i < count; ++i)
++start[size_t(vid[i]) + 1];
for (size_t v = 0; v < nv; ++v)
start[v + 1] += start[v];
std::vector<uint32_t> inc(count), cursor(nv, 0);
for (size_t i = 0; i < count; ++i)
inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i);
std::vector<uint8_t> frozen(nv, 0);
if (!locked.empty())
for (size_t t = 0; t < tri_ct && t < locked.size(); ++t)
if (locked[t])
for (int k = 0; k < 3; ++k)
frozen[size_t(vid[t * 3 + size_t(k)])] = 1;
std::vector<float> edge_len(nv, 0.f), normal_len(nv, 0.f);
std::vector<Vec3f> nrm(nv, Vec3f::Zero());
const auto rebuild_frames = [&]() {
std::fill(nrm.begin(), nrm.end(), Vec3f::Zero());
std::fill(edge_len.begin(), edge_len.end(), 0.f);
std::vector<uint32_t> deg(nv, 0);
for (size_t t = 0; t < tri_ct; ++t) {
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
const Vec3f fn = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]);
for (int k = 0; k < 3; ++k) {
const int u = vid[t * 3 + size_t(k)], w = vid[t * 3 + size_t((k + 1) % 3)];
nrm[size_t(u)] += fn;
edge_len[size_t(u)] += (pos[size_t(w)] - pos[size_t(u)]).norm();
++deg[size_t(u)];
}
}
for (size_t v = 0; v < nv; ++v) {
const float l = nrm[v].norm();
nrm[v] = (l > 0.f) ? Vec3f(nrm[v] / l) : Vec3f(0.f, 0.f, 1.f);
edge_len[v] = deg[v] > 0 ? edge_len[v] / float(deg[v]) : 0.f;
}
};
rebuild_frames();
// The level to snap onto, taken from the height actually present on this patch rather than assumed.
// A texture that never reaches full black or white would otherwise be measured against a range it
// does not occupy.
double h_lo = std::numeric_limits<double>::max(), h_hi = -h_lo;
{
std::vector<float> h0(nv, 0.f);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
for (size_t v = r.begin(); v < r.end(); ++v)
h0[v] = sample(pos[v], nrm[v], nrm[v]);
});
for (const float h : h0) {
h_lo = std::min(h_lo, double(h));
h_hi = std::max(h_hi, double(h));
}
}
const double h_range = h_hi - h_lo;
if (!(h_range > 0.0))
return result; // a flat height field has no contour to snap to
const double target = h_lo + h_range * settings.contour_level;
// A gradient is worth acting on when the height changes by this much across one edge length.
const double min_grad = h_range * settings.min_gradient_fraction;
std::vector<uint8_t> ever_moved(nv, 0);
for (int iter = 0; iter < settings.iterations; ++iter) {
std::vector<Vec3f> proposal(nv);
std::vector<uint8_t> want(nv, 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nv), [&](const tbb::blocked_range<size_t> &r) {
for (size_t v = r.begin(); v < r.end(); ++v) {
if (frozen[v] || edge_len[v] <= 0.f)
continue;
const Vec3f n = nrm[v];
Vec3f t1, t2;
tangent_basis(n, t1, t2);
const float eps = edge_len[v] * float(settings.gradient_step_fraction);
if (eps <= 0.f)
continue;
// Central differences in the tangent plane. Sampling the field itself, not the mesh,
// so the gradient is the image's, at whatever resolution the mesh happens to have.
const double h = double(sample(pos[v], n, n));
const double gx = (double(sample(pos[v] + t1 * eps, n, n)) -
double(sample(pos[v] - t1 * eps, n, n))) / (2.0 * double(eps));
const double gy = (double(sample(pos[v] + t2 * eps, n, n)) -
double(sample(pos[v] - t2 * eps, n, n))) / (2.0 * double(eps));
const double g2 = gx * gx + gy * gy;
if (g2 <= 0.0)
continue;
// Scale-free test: how much the height changes across one edge, versus the patch range.
if (std::sqrt(g2) * double(edge_len[v]) < min_grad)
continue;
// Newton step onto the level set h = target, expressed back in 3D.
const double s = -(h - target) / g2;
Vec3f d = t1 * float(s * gx) + t2 * float(s * gy);
const float cap = edge_len[v] * float(settings.max_move_fraction);
const float len = d.norm();
if (len <= 0.f)
continue;
if (len > cap)
d *= cap / len;
proposal[v] = pos[v] + d;
want[v] = 1;
}
});
// Apply one at a time: a move is only valid against the neighbourhood as it stands, and two
// adjacent vertices moving together can invert a triangle neither would have on its own.
size_t applied = 0;
for (size_t v = 0; v < nv; ++v) {
if (!want[v])
continue;
const Vec3f old = pos[v];
pos[v] = proposal[v];
bool ok = true;
for (uint32_t k = start[v]; k < start[v + 1] && ok; ++k) {
const size_t t = size_t(inc[k]) / 3;
const Vec3f &a = pos[size_t(vid[t * 3])];
const Vec3f n2 = (pos[size_t(vid[t * 3 + 1])] - a).cross(pos[size_t(vid[t * 3 + 2])] - a);
// Compared against the frame this vertex carried before the move: a triangle that
// flips or collapses means the move crossed a neighbour.
if (n2.squaredNorm() <= 0.f || n2.normalized().dot(nrm[v]) < 0.f)
ok = false;
}
if (ok) {
++applied;
ever_moved[v] = 1;
} else {
pos[v] = old;
++result.rejected;
}
}
if (applied == 0)
break;
rebuild_frames();
}
for (size_t v = 0; v < nv; ++v)
if (ever_moved[v])
++result.moved;
// Write the relocated positions back to every copy, and rebuild the per-face normals.
for (size_t i = 0; i < count; ++i)
result.geometry.pos[i] = pos[size_t(vid[i])];
for (size_t t = 0; t < tri_ct; ++t) {
Vec3f n = (result.geometry.pos[t * 3 + 1] - result.geometry.pos[t * 3])
.cross(result.geometry.pos[t * 3 + 2] - result.geometry.pos[t * 3]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
result.geometry.nrm[t * 3] = result.geometry.nrm[t * 3 + 1] = result.geometry.nrm[t * 3 + 2] = n;
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,72 @@
#pragma once
// Tangential relocation: slide vertices along the surface so triangle edges land on the height map's
// own edges, before any displacement happens.
//
// Displacement moves vertices along the normal only, so a step in the height map - the wall of a
// mortar groove, the rim of an embossed shape - is reproduced wherever the triangle grid happens to
// fall, as a staircase quantised to triangle boundaries. Refining further only makes the steps
// smaller; it never straightens them, because the edge in the image still does not coincide with any
// edge in the mesh.
//
// This pass fixes the cause rather than the symptom. For a vertex sitting near a step it takes a
// Newton step onto the contour: with h the sampled height and g its tangential gradient, the move
//
// d = -(h - target) * g / |g|^2
//
// lands on the level set h = target to first order. The ring of vertices nearest each step therefore
// snaps onto it, the triangle edges between them follow the step, and the displaced result has a
// straight wall instead of a sawtooth.
//
// Vertices in flat regions have no gradient to speak of and are left alone, so the pass costs nothing
// where there is nothing to align.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeDisplace.hpp"
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct RelocateSettings
{
// Passes. Each is a Newton step, so a couple converge for vertices that start reasonably close;
// more mainly helps ones that begin further away.
int iterations = 3;
// How far a vertex may move in one pass, as a fraction of the mean length of its incident edges.
// Below a half it cannot pass a neighbour, which is what keeps the triangulation valid without
// needing a full topological check.
double max_move_fraction = 0.35;
// A vertex is only pulled when the height varies enough across its own footprint to mean
// something - as a fraction of the height range over the whole patch. Below this the gradient is
// noise, and chasing it would scramble flat regions.
double min_gradient_fraction = 0.05;
// The level to snap onto, as a position in the sampled height range: 0.5 is midway between the
// lowest and highest point of the relief, which is where the wall of a step is steepest.
double contour_level = 0.5;
// Sampling offset for the finite-difference gradient, as a fraction of the local edge length.
double gradient_step_fraction = 0.25;
};
struct RelocateResult
{
TriSoup geometry;
size_t moved = 0; // positions that were relocated at least once
size_t rejected = 0; // moves refused because a triangle would have inverted
};
// `locked`, when non-empty, has one entry per input triangle; a vertex touching a locked triangle is
// never moved, so an excluded region keeps its exact vertex positions.
RelocateResult relocate_to_contours(const TriSoup &geometry, const HeightSampleFn &sample,
const RelocateSettings &settings = {},
const std::vector<uint8_t> &locked = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,215 @@
#include "TextureBakeRepair.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <unordered_map>
#include <unordered_set>
namespace Slic3r {
namespace TextureBake {
namespace {
inline uint64_t edge_key(int a, int b)
{
const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b));
return (uint64_t(lo) << 32) | uint64_t(hi);
}
// On the export grid a squared cross product is either 0 (collinear) or at least about 1e-16, the
// smallest real triangle being one grid unit per leg, so this separates the two cleanly.
constexpr double DEGENERATE_AREA_SQ = 1e-18;
} // namespace
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant)
{
EdgeDefects out;
const size_t n = geometry.pos.size();
out.triangles = n / 3;
QuantizedPointMap vmap(quant, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
int next = 0;
for (size_t i = 0; i < n; ++i) {
id[i] = vmap.get_or_set(geometry.pos[i], next);
if (vmap.inserted())
++next;
}
std::unordered_map<uint64_t, int> counts;
for (size_t t = 0; t + 2 < n; t += 3) {
const int a = id[t], b = id[t + 1], c = id[t + 2];
if (a == b || b == c || a == c)
continue;
const int tri[3] = { a, b, c };
for (int e = 0; e < 3; ++e)
++counts[edge_key(tri[e], tri[(e + 1) % 3])];
}
for (const auto &[key, c] : counts) {
(void) key;
if (c == 1) ++out.open;
else if (c > 2) ++out.non_manifold;
}
return out;
}
size_t count_area_slivers(const TriSoup &geometry)
{
size_t n = 0;
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
const Vec3d u = (geometry.pos[t + 1] - geometry.pos[t]).cast<double>();
const Vec3d v = (geometry.pos[t + 2] - geometry.pos[t]).cast<double>();
// The threshold a slicer applies: area below 1e-12 mm^2.
if (u.cross(v).squaredNorm() < 1e-24)
++n;
}
return n;
}
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
{
const size_t n_tri = geometry.triangle_count();
const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol;
const double Q = opts.weld_quant;
// Snapped, not just welded: keeping unrounded coordinates lets a thin triangle pass the
// degeneracy test here and then collapse to collinear once the file is written, punching the very
// hole this pass prevents. Snapping makes the check see what will be written.
QuantizedPointMap vmap(Q, std::min(n_tri * 3, size_t(1) << 22));
std::vector<Vec3d> vert;
std::vector<int> vid(n_tri * 3);
for (size_t i = 0; i < n_tri * 3; ++i) {
const Vec3f &p = geometry.pos[i];
const int id = vmap.get_or_set(p, int(vert.size()));
if (vmap.inserted())
vert.emplace_back(double(grid_round(double(p.x()) * Q)) / Q,
double(grid_round(double(p.y()) * Q)) / Q,
double(grid_round(double(p.z()) * Q)) / Q);
vid[i] = id;
}
// Dropped: faces whose corners welded together, and needles - distinct but collinear on this
// grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly
// the on-edge-vertex topology the pass below closes.
std::vector<std::array<int, 3>> faces;
faces.reserve(n_tri);
for (size_t t = 0; t < n_tri; ++t) {
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
if (a == b || b == c || a == c)
continue;
const Vec3d u = vert[size_t(b)] - vert[size_t(a)];
const Vec3d w = vert[size_t(c)] - vert[size_t(a)];
if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ)
continue;
faces.push_back({ a, b, c });
}
for (int iter = 0; iter < opts.max_iters; ++iter) {
std::unordered_map<uint64_t, int> e_count;
for (const auto &f : faces)
for (int e = 0; e < 3; ++e)
++e_count[edge_key(f[size_t(e)], f[size_t((e + 1) % 3)])];
std::unordered_set<int> bverts;
for (const auto &[key, c] : e_count) {
if (c != 1)
continue;
bverts.insert(int(uint32_t(key >> 32)));
bverts.insert(int(uint32_t(key & 0xFFFFFFFFu)));
}
if (bverts.empty())
break;
const std::vector<int> bv(bverts.begin(), bverts.end());
struct Split { int a, b; std::vector<int> mids; };
std::unordered_map<size_t, Split> splits;
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto &f = faces[fi];
for (int e = 0; e < 3; ++e) {
const int a = f[size_t(e)], b = f[size_t((e + 1) % 3)];
if (e_count[edge_key(a, b)] != 1)
continue; // only a boundary edge carries an unresolved T-junction
const Vec3d A = vert[size_t(a)];
const Vec3d ev = vert[size_t(b)] - A;
const double elen2 = ev.squaredNorm();
if (elen2 < 1e-20)
continue;
std::vector<std::pair<double, int>> found;
for (const int c : bv) {
if (c == a || c == b)
continue;
const Vec3d cv = vert[size_t(c)] - A;
const double tp = cv.dot(ev) / elen2;
if (tp <= 1e-4 || tp >= 1.0 - 1e-4)
continue; // strictly between the ends
if ((cv - ev * tp).squaredNorm() < on_tol2)
found.emplace_back(tp, c);
}
if (!found.empty()) {
std::sort(found.begin(), found.end(),
[](const auto &x, const auto &y) { return x.first < y.first; });
Split sp{ a, b, {} };
for (const auto &m : found)
sp.mids.push_back(m.second);
splits.emplace(fi, std::move(sp));
break; // one site per face per pass; iteration handles cascades
}
}
}
if (splits.empty())
break;
std::vector<std::array<int, 3>> next;
next.reserve(faces.size() + splits.size() * 2);
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto it = splits.find(fi);
if (it == splits.end()) {
next.push_back(faces[fi]);
continue;
}
const auto &f = faces[fi];
const auto &sp = it->second;
const int apex = (f[0] != sp.a && f[0] != sp.b) ? f[0]
: (f[1] != sp.a && f[1] != sp.b) ? f[1]
: f[2];
// Walk the base the way the face already traverses it, so the winding survives.
bool dir_ab = false;
for (int e = 0; e < 3; ++e)
if (f[size_t(e)] == sp.a && f[size_t((e + 1) % 3)] == sp.b) {
dir_ab = true;
break;
}
std::vector<int> seq;
if (dir_ab) {
seq.push_back(sp.a);
seq.insert(seq.end(), sp.mids.begin(), sp.mids.end());
seq.push_back(sp.b);
} else {
seq.push_back(sp.b);
seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend());
seq.push_back(sp.a);
}
for (size_t s = 0; s + 1 < seq.size(); ++s)
next.push_back({ seq[s], seq[s + 1], apex });
}
faces.swap(next);
}
TriSoup out;
out.pos.reserve(faces.size() * 3);
out.nrm.reserve(faces.size() * 3);
for (const auto &f : faces) {
const Vec3f a = vert[size_t(f[0])].cast<float>();
const Vec3f b = vert[size_t(f[1])].cast<float>();
const Vec3f c = vert[size_t(f[2])].cast<float>();
Vec3f nrm = (b - a).cross(c - a);
const float len = nrm.norm();
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
out.pos.insert(out.pos.end(), { a, b, c });
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
}
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,48 @@
#pragma once
// T-junction resolution and edge-defect accounting.
//
// Decimation can collapse a long edge whose interior still carries neighbouring triangles' vertices.
// Those then sit *on* an edge rather than at an end: watertight vertex-for-vertex, but the edge has
// one incident face on one side, which a slicer reads as an open boundary. This splits the offending
// face into a fan so every on-edge vertex becomes a real corner.
#include <cstdint>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct EdgeDefects
{
size_t open = 0, non_manifold = 0, triangles = 0;
};
// Welds at the export grid first: counting on the un-snapped mesh reports defects the file does not
// have and misses ones it does.
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant = WELD_GRID_EXPORT);
// Triangles a slicer would drop as degenerate. Each one, removed, punches a hole - so a non-zero
// count means watertight only on paper.
size_t count_area_slivers(const TriSoup &geometry);
struct RepairOptions
{
// Coordinates are snapped onto this grid, matching the precision files are written with.
double weld_quant = WELD_GRID_EXPORT;
// How far off an edge a vertex may sit and still count as on it. Well above the harvest
// tolerance, since harvesting leaves a region flat only to within that, making a collapsed edge a
// chord the on-edge vertices deviate from by about as much. Still far below the weld grid.
double on_seg_tol = 0.02;
// Splitting one face can expose another behind it, so the pass cascades.
int max_iters = 16;
};
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,443 @@
#include "TextureBakeSubdivide.hpp"
#include <algorithm>
#include <cmath>
#include <unordered_map>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/parallel_reduce.h>
namespace Slic3r {
namespace TextureBake {
namespace {
double edge_len_sq(const VertStore &v, int a, int b)
{
return (v.pos[size_t(a)] - v.pos[size_t(b)]).squaredNorm();
}
// Both indexers accumulate raw, area-weighted cross products and normalise once at the end.
void normalize_store_normals(VertStore &verts)
{
for (Vec3d &n : verts.nrm) {
const double len = n.norm();
n = (len > 0.0) ? Vec3d(n / len) : Vec3d(0.0, 0.0, 1.0);
}
}
// Keyed by the raw parent-vertex pair rather than by position: two sharp-edge copies of one point
// need their own midpoints, since their normals differ even though the position does not.
int get_midpoint(VertStore &verts, QuantizedPointMap &cache, int a, int b,
QuantizedPointMap *pos_canon_map)
{
const int lo = std::min(a, b), hi = std::max(a, b);
if (const int cached = cache.get_key(lo, hi, 0); cached != -1)
return cached;
const Vec3d m = (verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5;
Vec3d n = verts.nrm[size_t(a)] + verts.nrm[size_t(b)];
const double nl = n.norm();
n = (nl > 0.0) ? Vec3d(n / nl) : verts.nrm[size_t(a)];
const int idx = verts.push(m, n);
if (!verts.wgt.empty())
verts.wgt.push_back((verts.wgt[size_t(a)] + verts.wgt[size_t(b)]) * 0.5);
if (!verts.canon.empty() && pos_canon_map != nullptr)
verts.canon.push_back(pos_canon_map->get_or_set(float(m.x()), float(m.y()), float(m.z()), idx));
cache.get_or_set_key(lo, hi, 0, idx);
return idx;
}
struct PassResult
{
std::vector<int> indices;
std::vector<uint8_t> face_excluded;
std::vector<int> face_parent_id;
bool changed = false;
bool capped = false;
};
// Three steps, so that no T-junction can appear:
// 1. Mark every too-long edge globally, so both triangles on a shared edge decide alike.
// 1.5 Predict the exact resulting count from the marks (0->1, 1->2, 2->3, 3->4) and abort the
// *whole* pass if it exceeds the cap - a partial pass leaves split parents beside unsplit
// neighbours, the very crack step 1 prevents.
// 2. Rebuild, allocating once at the now-known size.
PassResult subdivide_pass(VertStore &verts, const std::vector<int> &indices, double max_edge_length,
int safety_cap, const std::vector<uint8_t> &face_excluded,
QuantizedPointMap *pos_canon_map, const std::vector<int> &face_parent_id)
{
PassResult out;
const double max_sq = max_edge_length * max_edge_length;
const size_t tri_count = indices.size() / 3;
const bool have_canon = !verts.canon.empty();
// Sized from the pass rather than grown from 64 k: a fine pass marks on the order of one edge per
// triangle, and growing to that by doubling rehashes the whole table a dozen times.
const size_t expect = std::max<size_t>(size_t(1) << 16, tri_count);
QuantizedPointMap mid_cache(1.0, expect);
QuantizedPointMap split_edges(1.0, expect);
// With canonical ids the key is the canonical *position* id, so split copies either side of a
// sharp edge see one another's decision; without them the vertex index serves.
const auto key_of = [&](int v) -> int64_t { return have_canon ? verts.canon[size_t(v)] : v; };
const auto mark_edge = [&](int a, int b) {
const int64_t u = key_of(a), v = key_of(b);
if (u < v) split_edges.get_or_set_key(u, v, 0, 1);
else split_edges.get_or_set_key(v, u, 0, 1);
};
const auto is_marked = [&](int a, int b) {
const int64_t u = key_of(a), v = key_of(b);
return (u < v ? split_edges.get_key(u, v, 0) : split_edges.get_key(v, u, 0)) != -1;
};
// Step 1. An excluded triangle marks none of its own edges, so its interior never refines; its
// boundary edges are still marked by an included neighbour, and it follows that split.
for (size_t t = 0; t < tri_count; ++t) {
if (!face_excluded.empty() && face_excluded[t])
continue;
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
if (edge_len_sq(verts, a, b) > max_sq) mark_edge(a, b);
if (edge_len_sq(verts, b, c) > max_sq) mark_edge(b, c);
if (edge_len_sq(verts, c, a) > max_sq) mark_edge(c, a);
}
if (split_edges.size() == 0) {
out.indices = indices;
out.face_excluded = face_excluded;
out.face_parent_id = face_parent_id;
return out; // changed stays false: nothing left to refine
}
// Step 1.5. Read-only against the finished mark set, so it runs in parallel - and it keeps each
// triangle's three marks (bit 0 = ab, 1 = bc, 2 = ca), so the serial rebuild below reads a byte
// instead of probing the hash map three more times per triangle.
std::vector<uint8_t> tri_marks(tri_count);
const size_t predicted = tbb::parallel_reduce(
tbb::blocked_range<size_t>(0, tri_count), size_t(0),
[&](const tbb::blocked_range<size_t> &range, size_t acc) {
for (size_t t = range.begin(); t < range.end(); ++t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
const uint8_t m = uint8_t(is_marked(a, b)) | uint8_t(is_marked(b, c) << 1) |
uint8_t(is_marked(c, a) << 2);
tri_marks[t] = m;
const int n = (m & 1) + ((m >> 1) & 1) + ((m >> 2) & 1);
acc += (n == 0) ? 1 : size_t(n + 1);
}
return acc;
},
std::plus<size_t>());
if (predicted > size_t(safety_cap)) {
out.indices = indices;
out.face_excluded = face_excluded;
out.face_parent_id = face_parent_id;
out.capped = true;
return out; // coarser than asked for, but watertight
}
// Step 2.
out.indices.resize(predicted * 3);
if (!face_excluded.empty())
out.face_excluded.resize(predicted);
if (!face_parent_id.empty())
out.face_parent_id.resize(predicted);
size_t wi = 0, fi = 0;
const auto emit_face_data = [&](uint8_t excl, int pid, int times) {
for (int k = 0; k < times; ++k) {
if (!out.face_excluded.empty()) out.face_excluded[fi] = excl;
if (!out.face_parent_id.empty()) out.face_parent_id[fi] = pid;
++fi;
}
};
const auto emit = [&](int x, int y, int z) {
out.indices[wi++] = x; out.indices[wi++] = y; out.indices[wi++] = z;
};
for (size_t t = 0; t < tri_count; ++t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
const uint8_t excl = face_excluded.empty() ? uint8_t(0) : face_excluded[t];
const int pid = face_parent_id.empty() ? 0 : face_parent_id[t];
const bool s_ab = (tri_marks[t] & 1) != 0, s_bc = (tri_marks[t] & 2) != 0, s_ca = (tri_marks[t] & 4) != 0;
const int n = int(s_ab) + int(s_bc) + int(s_ca);
if (n == 0) {
emit(a, b, c);
emit_face_data(excl, pid, 1);
} else if (n == 3) {
// a
// / \
// mCA-mAB
// / \ / \
// c--mBC--b
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, m_ab, m_ca);
emit(m_ab, b, m_bc);
emit(m_ca, m_bc, c);
emit(m_ab, m_bc, m_ca);
emit_face_data(excl, pid, 4);
} else if (n == 1) {
if (s_ab) {
const int m = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
emit(a, m, c);
emit(m, b, c);
} else if (s_bc) {
const int m = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
emit(a, b, m);
emit(a, m, c);
} else {
const int m = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, b, m);
emit(m, b, c);
}
emit_face_data(excl, pid, 2);
} else {
// A corner triangle on the untouched-edge vertex, then the remaining quadrilateral split
// along the midpoint-to-midpoint diagonal, which keeps the winding consistent.
//
// A sliver parent propagates: that inner diagonal inherits half the short edge and hands
// the sliver to two children per pass. No better diagonal exists - one avoiding the
// midpoints must pass through one of them, giving a zero-area triangle. Regularization
// removes such slivers before the mesh reaches here.
if (!s_ab) { // fan from c
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, b, m_bc);
emit(a, m_bc, m_ca);
emit(c, m_ca, m_bc);
} else if (!s_bc) { // fan from a
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, m_ab, m_ca);
emit(m_ab, b, c);
emit(m_ab, c, m_ca);
} else { // fan from b
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
emit(b, m_bc, m_ab);
emit(a, m_ab, m_bc);
emit(a, m_bc, c);
}
emit_face_data(excl, pid, 3);
}
}
out.changed = true;
return out;
}
} // namespace
IndexedMesh to_indexed_fast(const TriSoup &geometry)
{
// Preview path: a plain position merge - no clustering, no sharp-edge splitting, no canonical ids.
IndexedMesh out;
const size_t n = geometry.pos.size();
QuantizedPointMap vert_map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
out.indices.resize(n);
const bool has_w = !geometry.exclude_weight.empty();
for (size_t i = 0; i < n; ++i) {
const Vec3f &p = geometry.pos[i];
const Vec3f nf = geometry.nrm.empty() ? Vec3f(0.f, 0.f, 1.f) : geometry.nrm[i];
const int idx = vert_map.get_or_set(p, int(out.verts.count()));
if (vert_map.inserted()) {
out.verts.push(p.cast<double>(), nf.cast<double>());
if (has_w)
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
} else {
out.verts.nrm[size_t(idx)] += nf.cast<double>();
// Merge exclusion by maximum: any excluded face marks the shared vertex.
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(idx)])
out.verts.wgt[size_t(idx)] = double(geometry.exclude_weight[i]);
}
out.indices[i] = idx;
}
normalize_store_normals(out.verts);
return out;
}
IndexedMesh to_indexed(const TriSoup &geometry)
{
// Export path. Two vertices at one position merge only when their face normals agree to within
// SUBDIVIDE_SHARP_ANGLE_DEG, which keeps a cylinder from faceting while stopping a cube's edge
// normal from leaking into the flat face interiors as subdivision carries it inward.
IndexedMesh out;
out.has_canon = true;
const size_t n = geometry.pos.size();
const bool has_w = !geometry.exclude_weight.empty();
const double sharp_cos = std::cos(SUBDIVIDE_SHARP_ANGLE_DEG * M_PI / 180.0);
// Per-face normals: unit for the angle test, raw for the area-weighted accumulation.
std::vector<Vec3d> face_unit(n), face_raw(n);
tbb::parallel_for(tbb::blocked_range<size_t>(0, n / 3), [&](const tbb::blocked_range<size_t> &range) {
for (size_t f = range.begin(); f < range.end(); ++f) {
const size_t t = f * 3;
const Vec3d a = geometry.pos[t].cast<double>();
const Vec3d r = (geometry.pos[t + 1].cast<double>() - a).cross(
geometry.pos[t + 2].cast<double>() - a);
const double len = r.norm();
const Vec3d u = (len > 0.0) ? Vec3d(r / len) : Vec3d(0.0, 0.0, 1.0);
for (int v = 0; v < 3; ++v) {
face_unit[t + size_t(v)] = u;
face_raw[t + size_t(v)] = r;
}
}
});
out.indices.resize(n);
out.pos_canon_map = QuantizedPointMap(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
struct Cluster { int idx; Vec3d fn_unit; };
std::unordered_map<int, std::vector<Cluster>> clusters_by_canon;
for (size_t i = 0; i < n; ++i) {
const Vec3f &p = geometry.pos[i];
// The first vertex at a position becomes its canonical id; later split copies share it.
const int canon_id = out.pos_canon_map.get_or_set(p, int(out.verts.count()));
const bool fresh_position = out.pos_canon_map.inserted();
const auto add_vertex = [&](int canon) {
const int idx = out.verts.push(p.cast<double>(), face_raw[i]);
if (has_w)
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
out.verts.canon.push_back(canon);
return idx;
};
if (fresh_position) {
const int idx = add_vertex(canon_id);
clusters_by_canon[canon_id].push_back({ idx, face_unit[i] });
out.indices[i] = idx;
continue;
}
std::vector<Cluster> &clusters = clusters_by_canon[canon_id];
bool matched = false;
for (Cluster &cl : clusters) {
if (cl.fn_unit.dot(face_unit[i]) < sharp_cos)
continue;
out.verts.nrm[size_t(cl.idx)] += face_raw[i];
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(cl.idx)])
out.verts.wgt[size_t(cl.idx)] = double(geometry.exclude_weight[i]);
// Track the running average, so gradual curvature stays in one cluster instead of
// fragmenting when a distant face exceeds the threshold against the seed's fixed normal.
cl.fn_unit += face_unit[i];
if (const double rl = cl.fn_unit.norm(); rl > 0.0)
cl.fn_unit /= rl;
out.indices[i] = cl.idx;
matched = true;
break;
}
if (!matched) {
// A sharp-edge split: a new vertex at the same position, sharing its canonical id.
const int idx = add_vertex(canon_id);
clusters.push_back({ idx, face_unit[i] });
out.indices[i] = idx;
}
}
normalize_store_normals(out.verts);
return out;
}
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
const std::vector<uint8_t> &face_excluded)
{
TriSoup out;
const size_t tri_count = indices.size() / 3;
out.pos.resize(tri_count * 3);
out.nrm.resize(tri_count * 3);
const bool want_weights = !face_excluded.empty() || !verts.wgt.empty();
if (want_weights)
out.exclude_weight.resize(tri_count * 3);
// Each triangle writes only its own three slots, so this is a plain parallel gather.
tbb::parallel_for(tbb::blocked_range<size_t>(0, tri_count), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
// The per-face flag, not the interpolated weight: merging by maximum can push an *included*
// face's corners to 1 when it borders two excluded neighbours, wrongly excluding it.
const bool have_face_flag = !face_excluded.empty();
const float face_w = have_face_flag ? (face_excluded[t] ? 1.f : 0.f) : 0.f;
for (int v = 0; v < 3; ++v) {
const size_t vidx = size_t(indices[t * 3 + size_t(v)]);
out.pos[t * 3 + size_t(v)] = verts.pos[vidx].cast<float>();
out.nrm[t * 3 + size_t(v)] = verts.nrm[vidx].cast<float>();
if (want_weights)
out.exclude_weight[t * 3 + size_t(v)] =
have_face_flag ? face_w : float(verts.wgt[vidx]);
}
}
});
return out;
}
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
const std::vector<uint8_t> &face_excluded, bool fast, int safety_cap,
const SubdivideProgressFn &on_progress)
{
SubdivideResult result;
if (geometry.empty() || max_edge_length <= 0.0) {
result.geometry = geometry;
return result;
}
IndexedMesh indexed = fast ? to_indexed_fast(geometry) : to_indexed(geometry);
QuantizedPointMap *canon_map = indexed.has_canon ? &indexed.pos_canon_map : nullptr;
const size_t initial_tris = indexed.indices.size() / 3;
std::vector<int> current_indices = std::move(indexed.indices); // nothing reads it again
std::vector<uint8_t> current_excluded = face_excluded;
std::vector<int> current_parent(initial_tris);
for (size_t i = 0; i < initial_tris; ++i)
current_parent[i] = int(i);
for (int iter = 0; iter < SUBDIVIDE_MAX_ITERATIONS; ++iter) {
if (current_indices.size() / 3 >= size_t(safety_cap)) {
result.safety_cap_hit = true;
break;
}
PassResult pass = subdivide_pass(indexed.verts, current_indices, max_edge_length, safety_cap,
current_excluded, canon_map, current_parent);
current_indices = std::move(pass.indices);
if (!pass.face_excluded.empty())
current_excluded = std::move(pass.face_excluded);
if (!pass.face_parent_id.empty())
current_parent = std::move(pass.face_parent_id);
if (pass.capped || current_indices.size() / 3 >= size_t(safety_cap))
result.safety_cap_hit = true;
if (on_progress) {
// Reported after the pass, so the value falls each iteration instead of lagging a step.
// A max is order-independent, so the scan reduces in parallel to the same value.
const double max_edge_sq = tbb::parallel_reduce(
tbb::blocked_range<size_t>(0, current_indices.size() / 3), 0.0,
[&](const tbb::blocked_range<size_t> &r, double acc) {
for (size_t f = r.begin(); f < r.end(); ++f) {
const int a = current_indices[f * 3], b = current_indices[f * 3 + 1],
c = current_indices[f * 3 + 2];
acc = std::max({ acc, edge_len_sq(indexed.verts, a, b), edge_len_sq(indexed.verts, b, c),
edge_len_sq(indexed.verts, c, a) });
}
return acc;
},
[](double x, double y) { return std::max(x, y); });
if (!on_progress(std::min(0.95, double(iter + 1) / SUBDIVIDE_MAX_ITERATIONS),
current_indices.size() / 3, std::sqrt(max_edge_sq)))
break; // whole passes only, so what we have is still crack-free
}
if (!pass.changed || result.safety_cap_hit)
break;
}
result.geometry = to_non_indexed(indexed.verts, current_indices, current_excluded);
result.face_parent_id = std::move(current_parent);
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,83 @@
#pragma once
// Adaptive subdivision to a target edge length, by global marked-edge (red-green) refinement rather
// than longest-edge bisection. Marking is global, so two triangles sharing an edge always agree and
// the result is crack-free by construction. A triangle is rebuilt from its marked-edge count: 0
// keeps, 1 bisects, 2 fans into three, 3 does the regular 1->4 split.
//
// The 1->4 case is what keeps the tessellation regular - its children are similar to the parent. An
// irregular one shows up after displacement as adjacent triangles tilting alternately, i.e. noise.
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Memory guard for the stages downstream. At roughly 145 bytes per triangle this is about 2.9 GB.
static constexpr int SUBDIVIDE_SAFETY_CAP = 16'000'000;
// Vertices at one position stay separate when their faces disagree by more than this: a cube keeps
// hard edges, a cylinder keeps averaged ones.
static constexpr double SUBDIVIDE_SHARP_ANGLE_DEG = 30.0;
// A depth bound, not a work bound: the loop stops as soon as a pass changes nothing.
static constexpr int SUBDIVIDE_MAX_ITERATIONS = 12;
// Built by the indexers, appended to by the passes. Double precision so repeated midpointing does
// not drift.
struct VertStore
{
std::vector<Vec3d> pos;
std::vector<Vec3d> nrm;
std::vector<double> wgt; // exclusion weights; empty when the caller supplied none
std::vector<int> canon; // canonical position ids; empty in fast mode
size_t count() const { return pos.size(); }
int push(const Vec3d &p, const Vec3d &n)
{
const int idx = int(pos.size());
pos.push_back(p);
nrm.push_back(n);
return idx;
}
};
struct IndexedMesh
{
VertStore verts;
std::vector<int> indices; // 3 per triangle
QuantizedPointMap pos_canon_map{ WELD_GRID_GEOMETRY, 256 };
bool has_canon = false;
};
// Fraction, triangle count, longest remaining edge. Returning false cancels; what comes back is
// still watertight, because passes apply whole or not at all.
using SubdivideProgressFn = std::function<bool(double fraction, size_t triangles, double longest_edge)>;
struct SubdivideResult
{
TriSoup geometry;
// Output triangle -> input triangle it descends from, so per-face data survives with no remap.
std::vector<int> face_parent_id;
bool safety_cap_hit = false;
};
// `face_excluded`: one entry per input triangle; non-zero means its interior is never refined. Its
// edges still split when an included neighbour marks them, so no T-junction appears at the boundary.
// `fast` selects the cheap position-only indexer for previews.
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
const std::vector<uint8_t> &face_excluded = {}, bool fast = false,
int safety_cap = SUBDIVIDE_SAFETY_CAP,
const SubdivideProgressFn &on_progress = {});
// Displacement needs the same welding and sharp-edge clustering.
IndexedMesh to_indexed(const TriSoup &geometry);
IndexedMesh to_indexed_fast(const TriSoup &geometry);
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
const std::vector<uint8_t> &face_excluded);
} // namespace TextureBake
} // namespace Slic3r

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1519,9 +1519,11 @@ void TriangleSelector::get_facets(std::vector<indexed_triangle_set>& facets_per_
}
}
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source) const
{
indexed_triangle_set out;
if (out_source)
out_source->clear();
size_t num_vertices = 0;
for (const Vertex &v : m_vertices)
@@ -1535,8 +1537,13 @@ indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType sta
out.vertices.emplace_back(v.v);
}
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle)
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) {
this->get_facets_strict_recursive(m_triangles[itriangle], m_neighbors[itriangle], state, out.indices);
// Everything the recursion just appended came from this original triangle, whatever depth it
// was split to. Recording it here keeps the recursive helpers untouched.
if (out_source)
out_source->resize(out.indices.size(), itriangle);
}
for (auto &triangle : out.indices)
for (int i = 0; i < 3; ++ i)

View File

@@ -350,7 +350,14 @@ public:
// Get facets at a given state. Don't triangulate T-joints.
indexed_triangle_set get_facets(EnforcerBlockerType state) const;
// Get facets at a given state. Triangulate T-joints.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state) const;
// Sub-triangles in `state`, with the *whole* mesh's referenced vertex array (only .indices is
// filtered by state, so two calls with different states share one indexing).
//
// `out_source`, when given, is filled parallel to the returned .indices with the index of the
// original mesh triangle each sub-triangle came from. That is what lets a caller carry partial
// paint - the pieces of a triangle a brush stroke only partly covered - across a refinement of
// the same surface, instead of having to round each source triangle to wholly painted or not.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source = nullptr) const;
// Get edges around the selected area by seed fill.
std::vector<Vec2i32> get_seed_fill_contour() const;

View File

@@ -200,6 +200,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmosManager.hpp
GUI/Gizmos/GLGizmoSVG.cpp
GUI/Gizmos/GLGizmoSVG.hpp
GUI/Gizmos/GLGizmoTextureDisplacement.cpp
GUI/Gizmos/GLGizmoTextureDisplacement.hpp
GUI/Gizmos/GLGizmoUtils.cpp
GUI/Gizmos/GLGizmoUtils.hpp
#GUI/Gizmos/GLGizmoText.cpp
@@ -325,6 +327,14 @@ set(SLIC3R_GUI_SOURCES
GUI/Jobs/SLAImportDialog.hpp
GUI/Jobs/SLAImportJob.cpp
GUI/Jobs/SLAImportJob.hpp
GUI/Jobs/TextureDisplacementBakeJob.cpp
GUI/Jobs/TextureDisplacementBakeJob.hpp
GUI/Jobs/TextureDisplacementPrepareJob.cpp
GUI/Jobs/TextureDisplacementPrepareJob.hpp
GUI/Jobs/TextureDisplacementDebugJob.cpp
GUI/Jobs/TextureDisplacementDebugJob.hpp
GUI/Jobs/TextureDisplacementPreviewJob.cpp
GUI/Jobs/TextureDisplacementPreviewJob.hpp
GUI/Jobs/ThreadSafeQueue.hpp
GUI/Jobs/UpgradeNetworkJob.cpp
GUI/Jobs/UpgradeNetworkJob.hpp
@@ -506,6 +516,10 @@ set(SLIC3R_GUI_SOURCES
GUI/TaskManager.hpp
GUI/TextLines.cpp
GUI/TextLines.hpp
GUI/TextureLibrary.cpp
GUI/TextureLibrary.hpp
GUI/TextureProjectorFrame.cpp
GUI/TextureProjectorFrame.hpp
GUI/TickCode.cpp
GUI/TickCode.hpp
GUI/TroubleshootDialog.cpp
@@ -522,6 +536,8 @@ set(SLIC3R_GUI_SOURCES
GUI/UserManager.hpp
GUI/UserNotification.cpp
GUI/UserNotification.hpp
GUI/UVEditorCanvas.cpp
GUI/UVEditorCanvas.hpp
GUI/WebDownPluginDlg.cpp
GUI/WebDownPluginDlg.hpp
GUI/WebGuideDialog.cpp

View File

@@ -104,6 +104,11 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" }, { "FLIP_TRIANGLE_NORMALS"sv });
else
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
// Fast bump-map preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
valid &= append_shader("texture_displacement_bump", { prefix + "texture_displacement_bump.vs", prefix + "texture_displacement_bump.fs" });
// UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the
// painted patch, to sanity-check the unwrap.
valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" });
return { valid, error };
}

View File

@@ -171,7 +171,8 @@ bool GLTexture::load_from_svg_file(const std::string& filename, bool use_mipmaps
return false;
}
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy)
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy,
bool use_mipmaps)
{
m_width = w;
m_height = h;
@@ -195,18 +196,51 @@ bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
bool use_mipmaps = true;
if (use_mipmaps) {
// we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards
int lod_w = m_width;
int lod_h = m_height;
// We generate the mipmap chain ourselves rather than calling glGenerateMipmap(), which this
// 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
// 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
// sampled); it shows up the moment one is drawn small enough to select a lower level, as a
// texture that visibly turns into something else as it shrinks.
std::vector<unsigned char> scratch;
const std::vector<unsigned char> *src = &data;
int src_w = m_width;
int src_h = m_height;
GLint level = 0;
while (lod_w > 1 || lod_h > 1) {
while (src_w > 1 || src_h > 1) {
++level;
lod_w = std::max(lod_w / 2, 1);
lod_h = std::max(lod_h / 2, 1);
n_pixels = lod_w * lod_h;
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
const int lod_w = std::max(src_w / 2, 1);
const int lod_h = std::max(src_h / 2, 1);
std::vector<unsigned char> lod(size_t(lod_w) * size_t(lod_h) * 4);
for (int y = 0; y < lod_h; ++y) {
// min() rather than a plain 2*y+1: an odd source extent leaves the last output texel
// with only one source row/column to average, not two.
const int y0 = std::min(2 * y, src_h - 1);
const int y1 = std::min(2 * y + 1, src_h - 1);
for (int x = 0; x < lod_w; ++x) {
const int x0 = std::min(2 * x, src_w - 1);
const int x1 = std::min(2 * x + 1, src_w - 1);
for (int c = 0; c < 4; ++c) {
const unsigned int sum = (*src)[(size_t(y0) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y0) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)];
lod[(size_t(y) * size_t(lod_w) + size_t(x)) * 4 + size_t(c)] = (unsigned char)(sum / 4);
}
}
}
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)lod.data()));
scratch = std::move(lod);
src = &scratch;
src_w = lod_w;
src_h = lod_h;
}
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level));

View File

@@ -100,7 +100,10 @@ namespace GUI {
bool load_from_file(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy);
bool load_from_svg_file(const std::string& filename, bool use_mipmaps, bool compress, bool apply_anisotropy, unsigned int max_size_px);
//BBS load GLTexture from raw pixel data
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false);
// `data` is RGBA, w * h * 4 bytes. With use_mipmaps, a real box-filtered mipmap chain is
// built, so the texture may safely be drawn smaller than its pixel size.
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false,
bool use_mipmaps = true);
// meanings of states: (std::pair<int, bool>)
// first field (int):
// 0 -> no changes

View File

@@ -27,7 +27,8 @@ enum class PainterGizmoType {
FDM_SUPPORTS,
SEAM,
MM_SEGMENTATION,
FUZZY_SKIN
FUZZY_SKIN,
TEXTURE_DISPLACEMENT
};
class TriangleSelectorGUI : public TriangleSelector {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,872 @@
#ifndef slic3r_GLGizmoTextureDisplacement_hpp_
#define slic3r_GLGizmoTextureDisplacement_hpp_
#include "GLGizmoPainterBase.hpp"
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLTexture.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/IconManager.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include <array>
#include <atomic>
#include <map>
#include <memory>
#include <string>
namespace Slic3r::GUI {
class TextureProjectorFrame;
// Paint-style gizmo that assigns one or more texture-displacement "layers" (see
// 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 -
// 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.
class GLGizmoTextureDisplacement : public GLGizmoPainterBase
{
public:
GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
// The whole of mesh preparation - remesh, carry the paint across, refine where the texture bends -
// as one pure function over plain data: no ModelVolume, no Model, no GUI, no undo. That is what lets
// TextureDisplacementPrepareJob run it on the job worker instead of on the UI thread, where CGAL's
// remesher and a several-hundred-thousand-triangle refinement together freeze the window for tens
// of seconds with nothing to look at and no way to cancel.
//
// `progress` is called with 0..100 and aborts the run when it returns false; an aborted run reports
// an empty result. An empty result is also how "nothing needed doing" is reported - see
// TextureDisplacementPrepareResult.
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementPrepareParams &params,
const std::vector<PrintableColor> &palette,
const DisplacementProgressFn &progress,
// Optional step capture: receives the mesh
// after the remesh and after the refinement,
// so a debug run can show the whole recipe.
BakeStageRecorder *debug = nullptr);
// The volume's eight texture-displacement masks, gathered into the array every pure function here
// (and every job input) takes.
static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv);
using PaletteEntry = PrintableColor;
// The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can
// address), plus - when `mixing` - every pair of them at evenly spaced ratios.
//
// Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too
// finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and
// blue banded together read as a desaturated grey-green, and that is what the preview must promise
// - blending them subtractively would show a green the printer cannot produce this way.
//
// How many ratios depends on how many filaments there are, so the palette stays bounded: the
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
// filaments there are already plenty of colours without mixing any of them.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing);
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
// a grey of similar brightness).
//
// Precomputed into a lookup cube rather than matched per call: the subdivision's colour criterion
// samples up to seven points per triangle and re-samples both children of every split, so a live
// match would dominate the refinement. The returned closure owns the cube, so it is safe to hand
// to a worker thread and outlives the palette it was built from.
static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette);
// Turns a palette index plus a position into the filament to print there, interleaving the two
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither
// cells. See ColorResolveFn for why this is separate from the quantizer.
static ColorResolveFn make_mix_resolver(const std::vector<PaletteEntry> &palette, ColorMixMode mode,
float layer_height, float cell_mm);
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
// height, despeckle. Empty when no layer is actually colouring.
TextureColorSettings color_settings_for(const ModelVolume &mv);
// The printable palette for the current filaments and mixing setting, rebuilt only when either
// actually changes - see the definition for why that caching is not optional.
const std::vector<PaletteEntry> &cached_palette();
std::vector<PaletteEntry> m_palette_cache;
std::vector<ColorRGBA> m_palette_filaments;
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer;
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
static std::vector<ColorRGBA> filament_palette();
// The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be
// read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything.
static float print_layer_height();
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer
// sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex
// colour attribute, and so no change to GLModel's vertex layouts.
//
// The *index buffer* is what gets reordered, never m_preview_its: the paint overlay and the
// wireframe index into that by the volume's own triangle numbering (the bake is
// topology-preserving), and permuting it would silently misplace both.
struct PreviewColorRun
{
std::pair<size_t, size_t> range; // into the GLModel's index buffer, in elements
ColorRGBA color;
};
std::vector<PreviewColorRun> m_preview_color_runs;
// True if any of the volume's layers would actually colour something: colour turned on, and a
// texture that has colour to give. What decides whether a palette is captured into a job at all,
// and so whether the colour criterion and the mmu write ever run.
static bool any_layer_colors(const ModelVolume &mv);
void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
// rotation handles instead of painting); otherwise forwards to the normal painting handling.
bool on_mouse(const wxMouseEvent &mouse_event) override;
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
std::string get_gizmo_entering_text() const override { return _u8L("Entering Texture displacement painting"); }
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Texture displacement painting"); }
std::string get_action_snapshot_name() const override { return _u8L("Texture displacement editing"); }
// The panel's Paint / Erase toggle swaps what the two buttons do, so the right button always does the
// opposite of the left. Shift still erases in both modes (GLGizmoPainterBase::gizmo_event()).
EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; }
EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; }
private:
bool on_init() override;
void update_model_object() override;
void update_from_model_object(bool first_update) override;
void on_opening() override {}
void on_shutdown() override;
PainterGizmoType get_painter_type() const override;
// Phase 1 restricts the texture layer list to the first model-part volume of the current
// object (the common single-volume case); multi-part objects only get texture layers on
// their first part until a later phase. Returns nullptr if there is no model part.
ModelVolume* texture_volume();
const ModelVolume* texture_volume() const;
void add_texture_layer();
void remove_texture_layer(int slot);
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
// the slot each one occupies, so their paint masks move with them on every model part.
void move_texture_layer(int slot, int to_index);
// Exchanges everything two slots hold - layer definition, paint masks, panel caches. No snapshot and
// no selector reload; move_texture_layer() does both once around a run of these.
void swap_layer_slots(int slot_a, int slot_b);
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
// `own_snapshot` false when the caller has already taken an undo step that is meant to cover the
// displacement too - see bake_standard().
void bake(bool own_snapshot = true);
// Standard (0) vs Pro (1), driven by the two-position slider in the panel header.
//
// Pro is the panel as it has always been: every geometry-preparation control is visible and the
// user drives Remesh, Subdivide and Bake themselves, in whatever order they like. Standard hides
// all of that, pins it to one fixed recipe, and folds it into the Bake button - paint, press Bake,
// done - so the common case does not require knowing that a height map can only move vertices that
// already exist. Nothing about Pro changed when Standard was added.
int m_panel_mode = 0;
bool pro_mode() const { return m_panel_mode != 0; }
// Pins every control Standard mode hides to its preset value. Idempotent, called each frame while
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
// changed something, so the caller can invalidate the preview.
bool apply_standard_mode_presets(ModelVolume *mv);
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
// The order matters and is the whole reason this is one button - a height map can only move
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
// the paint if it were not carried across (see prepare_mesh()).
void bake_standard();
// Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every
// mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's
// Bake differ only in which stages `params` enables and in what happens afterwards:
// - `snapshot_name` is the single undo step the commit opens. With `then_bake` it is also the step
// the displacement job that follows commits into, rather than pushing its own - an undo landing
// between the two would leave a mesh carrying every added triangle and no relief on it, and
// baking again from there would prepare it a second time.
// - `unchanged_msg`, when not empty, is shown if the run had nothing to do. Standard's Bake passes
// nothing: a mesh that already meets the criteria is not an error there, it just goes straight
// on to the displacement.
void queue_prepare(const TextureDisplacementPrepareParams &params, const std::string &snapshot_name,
bool then_bake, const std::string &unchanged_msg);
// Set from queue_prepare() until its job's result has been committed. Distinct from
// m_bake_in_progress because Standard's Bake sets both in turn, and because every button that would
// read or replace the mesh has to stay disabled for the whole of it.
bool m_prepare_in_progress = false;
// How one layer's paint sits on the pre-subdivision mesh, precise enough to carry across the
// refinement without rounding each source triangle to wholly painted or not.
//
// Rounding is what made the outline of a painted region come out ragged: a source triangle near a
// smooth brush boundary is wholly painted essentially at random, so "painted iff the source was
// full" turns a clean curve into a noisy fringe of isolated painted and unpainted triangles - and
// once the border band refines the mesh there, that fringe is reproduced faithfully instead of
// being blurred away by coarse geometry.
struct LayerPaintMap
{
std::vector<uint8_t> full; // per source triangle: covered edge to edge
std::vector<int> part_start; // CSR offsets into `part`, size (source tris + 1)
std::vector<std::array<Vec3f, 3>> part; // painted pieces of partly covered source triangles
bool empty() const { return full.empty(); }
};
// Rebuilds every layer's mask on a subdivided mesh from `source` (new triangle -> the input triangle
// it descends from) and the pre-subdivision coverage in `paint`.
static TextureDisplacementFacetsData masks_after_subdivision(
const TriangleMesh &new_mesh, const std::vector<int> &source,
const std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> &paint);
// False means the remesh failed or changed nothing (CGAL signals failure by handing the input back),
// and `out` must not be used. `target_edge_mm` is a request rather than a promise: it is clamped
// against the part's surface area first, because CGAL's cost grows with the square of 1/target.
static bool plan_remesh(const indexed_triangle_set &src, float target_edge_mm, float sharp_angle_deg,
indexed_triangle_set &out);
// 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();
// The mesh raycasters are built one per model-part volume, in that order; this is the texture
// volume's slot among them, or -1 if it has none (no selection, or the lists disagree).
int texture_volume_raycaster_index() const;
// Paints exactly the facets currently visible from the camera onto the active layer, replacing
// whatever that layer had painted. "Visible" is two tests: the facet faces the camera, and its
// centroid is not hidden behind other geometry (a real raycast, so a concave part's far inner
// wall is correctly excluded). When `uv_clip` is given (the projection frame's matrix), facets
// whose centroid falls outside the frame's uv unit square are skipped first - which both clips
// the selection to the frame and spares the raycast for everything outside it. Costs one ray
// query per surviving facet, so it is a one-shot action, never a per-frame one. Returns the
// number of facets selected.
int select_visible_faces(const std::array<float, 12> *uv_clip = nullptr);
// When set, "Capture current view" also re-selects the visible faces, so the viewpoint the
// projector was captured from and the area it projects onto stay the same. Independent of the
// projection frame below: this takes every visible facet, the frame clips to its rectangle.
// Off by default, because turning it on replaces whatever the layer had painted.
bool m_project_only_visible = false;
// The projection-frame overlay for a ViewProjected layer: a semi-transparent window dragged over
// the 3D view whose border becomes the projection's edge. Created lazily and owned here; hidden
// rather than destroyed when closed, so reopening keeps it where the user left it.
TextureProjectorFrame *m_projector_frame = nullptr;
int m_projector_opacity = 140;
// What the overlay's texture was last built from, so repeated updates don't rebuild the bitmap
// from unchanged pixels. Same shape as the m_thumbnail_source/m_thumbnail_smoothing pair above.
const void *m_projector_tex_source = nullptr;
float m_projector_tex_smoothing = -1.f;
void show_projector(bool show);
// Pushes the active layer's texture into the overlay. Cheap, and a no-op while it is hidden.
void update_projector();
// Reads the overlay's rectangle and commits it as the layer's projection: builds the exact
// projective local->uv matrix from the camera and that rectangle, turns tiling off so the border
// is a hard edge, and repaints the layer with the visible facets inside the frame. Returns the
// number of facets selected, or -1 if the frame could not be used at all.
int apply_projection_frame();
// Uniformly subdivides the volume's mesh (see libslic3r::subdivide_mesh_uniform()) so a
// low-poly input model has enough vertices to actually show texture-displacement detail.
// A real, committed geometry change (like Bake), so it needs its own snapshot; unlike Bake it
// has no target region, so any not-yet-baked paint on the volume is dropped rather than
// remapped (texture-displacement paint has no remap-across-topology-change support yet).
void subdivide_model();
// The layer height map's width / height, for apply_uv_transform()'s non-square handling. 1 when
// there is no usable texture.
static float layer_texture_aspect(const TextureDisplacementLayer &layer);
// Returns a cached GPU thumbnail of layer's texture (decoding + uploading it the first time it
// is requested, or whenever its image_data changes), or nullptr if it has no usable texture.
// Panel-sized: box-filtered down to THUMBNAIL_MAX_PX, which is right for a list row and wrong for
// anything the shader samples - see get_layer_height_texture().
GLTexture *get_layer_thumbnail(const TextureDisplacementLayer &layer);
// The same texture at full resolution, for the fast-preview shader. One slot, shared by whichever
// layer is active, because that is the only one the bump shader ever shades.
GLTexture *get_layer_height_texture(const TextureDisplacementLayer &layer);
// The layer's colour texture for the fast preview's per-fragment quantization. Null when the
// layer is not colouring or its texture is grayscale.
GLTexture *get_layer_color_texture(const TextureDisplacementLayer &layer);
// 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 -
// 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
{
std::shared_ptr<std::vector<unsigned char>> image_data;
std::unique_ptr<GLTexture> thumbnail;
};
const LibraryTexture *get_library_texture(const std::string &path);
// The texture library: a popup grid of thumbnails beside the panel, shipped textures and the user's
// own under separate headings, with a tile that imports an image from disk. Opened for
// m_picker_slot by a click on a layer's thumbnail or name. `panel_min`/`panel_max` are the panel
// window's screen rectangle, which the popup is placed against.
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
int m_picker_slot = -1;
bool m_picker_open_request = false;
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer);
// Paint / Erase, from the panel. See get_left_button_state_type().
bool m_erase_mode = false;
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
// Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
// "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
// 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
// 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
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
void render_adjust_texture_gizmo();
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint();
// Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
// dominant-axis convention so dragging on-canvas maps consistently onto offset.
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
// 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
// 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
// on_mouse_adjust_texture(): the handle then tracks the cursor 1:1, and sits back on the anchor
// precisely when offset is zero.
Vec3f adjust_handle_center(const TextureDisplacementLayer &layer) const;
// The layer painted by the active slot, or nullptr if that slot has no layer yet.
TextureDisplacementLayer *active_layer();
const TextureDisplacementLayer *active_layer() const;
// 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 -
// 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
// it and returns immediately, and m_preview_glmodel is updated later when it completes.
void rebuild_preview();
void render_preview_mesh();
// 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
// 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.
void rebuild_bump_preview_mesh();
void render_bump_preview_mesh();
// Feeds the active layer's painted patch + LSCM unwrap (if it's using that projection method)
// into Plater's docked UV-editor pane and shows it, or hides the pane if the active layer
// isn't using LSCM (or nothing is painted). Called whenever something that could change what
// the pane should show happens: paint changes, layer switch, projection method change, bake,
// and on shutdown (to hide it).
void update_uv_editor();
// 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
// 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
// unwrapped-vertex index and its new raw-unwrap coordinate. Maps the unwrapped index to a mesh
// vertex and stores a per-vertex UV override on the layer (see lscm_uv_overrides), then rebuilds the
// preview so the baked geometry follows.
void on_uv_vertex_edited(const std::vector<std::pair<int, Vec2f>> &edits);
// 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
// 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.
std::vector<Eigen::Matrix<float, 2, 3>> uv_editor_island_transforms(const TextureDisplacementLayer &layer);
// Handles a toolbar command forwarded from the UV pane that needs the layer data the canvas
// doesn't hold (average island scale, cut island). Takes the command as an int (a cast of
// UVEditorCanvas::Command) so this header needn't pull in glad/wxGLCanvas via the canvas header.
void on_uv_command(int cmd, float value);
// Pane commands, queued by on_uv_command() and run from the panel render: pane clicks arrive in wx event
// handlers, outside the 3D canvas's GL frame, and several of these rebuild GPU meshes.
std::vector<std::pair<int, float>> m_uv_command_queue;
void process_uv_commands();
void run_uv_command(int cmd, float value);
// Sends the UV editor pane what its controls show (see UVEditorCanvas::PaneState).
void push_uv_pane_state();
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
// background buttons, so both switch views the same way.
void apply_view_mode(int mode);
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
static constexpr int UV_THUMB_PX = 64;
const void *m_uv_thumb_source = nullptr;
std::vector<unsigned char> m_uv_thumb_rgb;
// Splits one unwrap chart in two by marking the mesh edges that straddle the plane through its
// 3D centroid, perpendicular to its longest axis, as seams (#17). The re-unwrap then separates it.
void cut_island(TextureDisplacementLayer &layer, int chart);
// Captures the current camera's right/up axes into the layer's projector (#6), transformed into
// the volume's local space so the projection is stable as the object is later moved/rotated.
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
// 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
bool on_mouse_seam(const wxMouseEvent &mouse_event);
void toggle_seam_at(const Vec2d &mouse_pos);
void rebuild_seam_overlay();
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.
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
// way the edge is in normal mode. -1 when nothing is under the cursor (or not in path mode).
int m_seam_hover_vertex = -1;
GLModel m_seam_hover_glmodel;
void rebuild_seam_hover_overlay();
// Shortest-path seam marking, for dense meshes where clicking every single triangle edge is
// tedious: in this sub-mode a click picks the nearest vertex, and the next click marks every edge
// on the shortest surface path between the two as a seam - so a whole seam line is drawn with two
// clicks. The end vertex becomes the next start, so a multi-segment seam chains click by click.
bool m_seam_path_mode = false;
int m_seam_path_anchor = -1; // mesh vertex the path starts from, or -1
GLModel m_seam_anchor_glmodel; // the anchor's incident edges, highlighted
int seam_vertex_at(const Vec2d &mouse_pos) const; // nearest mesh vertex under the cursor
void mark_seam_path(int v_from, int v_to); // seam every edge on the shortest path
void rebuild_seam_anchor_overlay();
// Set while an island gesture is in flight, so the undo snapshot is taken once at the start of
// the drag (capturing the state *before* it) rather than on every motion event.
bool m_island_drag_active = false;
// 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 -
// 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;
bool m_bake_in_progress = false;
// When set, the true-displacement geometry is rebuilt on every parameter change (live), instead of
// only once the slider being dragged is released. On by default so painting/added textures show
// straight away without needing to nudge a slider first.
bool m_auto_update = true;
// Subdivision is now count-based (split the whole mesh 1..5 times) rather than a target edge
// length, and is previewed as a wireframe before it is committed: nothing is written to the model
// until "Apply". While previewing, the would-be subdivided mesh is drawn as a wireframe overlay so
// the added density is visible; "Done" ends the preview without touching the model. The normal
// "Show mesh wireframe" toggle is left alone, so a wireframe the user already had on stays on.
// 0 is a real value meaning "no subdivision": it previews nothing and Apply is a no-op. Apply
// snaps the slider back to it, because each pass quadruples the triangle count - leaving the
// count where it was would immediately re-preview N more passes on top of the mesh that was just
// committed, i.e. the most expensive thing the panel can do, on every Apply.
int m_subdivide_count = 1;
bool m_subdivide_editing = false;
int m_subdivide_preview_tris = -1; // triangle count of the previewed result, shown in the panel
GLModel m_subdivide_preview_glmodel;
void rebuild_subdivide_preview();
void render_subdivide_preview();
// Adaptive subdivision: refine only the painted area, down to a target edge length, via
// conformal longest-edge bisection (subdivide_mesh_adaptive()). Unlike the count-based uniform
// path it does not touch the unpainted rest of the model, and - because it is driven by the paint
// - it can carry that paint forward across the topology change (children of a painted triangle
// are painted), so the region survives the subdivision instead of being dropped.
bool m_subdivide_adaptive = false;
float m_subdivide_target_mm = 0.f; // 0 = not yet seeded; filled from the mesh on first show
// Feature-adaptive sub-mode: put the triangles where the *displaced surface* bends (texture
// curvature) rather than spreading them evenly. `detail_mm` is the chord-error tolerance ("Detail"
// slider: how far the true surface may sit off the flat triangle before it is split); the target
// above stays in play as a coarse baseline ("Max edge"), and `min_edge_mm` is the hard floor
// ("Min edge"). See subdivide_mesh_adaptive().
bool m_subdivide_feature = false;
float m_subdivide_detail_mm = 0.05f;
float m_subdivide_min_edge_mm = 0.1f;
// Edge length the band straddling the paint's boundary is refined to (0 = leave it alone). Applies
// in both adaptive sub-modes, because it is not a texture-detail criterion: the bake steps the
// surface from full displacement to zero across that boundary whatever the texture is doing, and
// the chord-error test cannot see that step at all - its sampler has no per-point paint test, so
// just outside the paint it goes on reporting the same smooth height field. Without this the
// transition keeps the input's density and the rim of an unpainted island comes out as a ring of
// large, steeply tilted triangles. See collect_paint_region() and subdivide_mesh_adaptive().
float m_subdivide_border_mm = 0.4f;
// Edge length triangles straddling a *colour* boundary are refined to (0 = ignore colour). Its own
// control rather than a share of "Detail (mm)" because the two measure different things: Detail is
// a chord error in mm of surface deviation, this is a triangle size in mm along a step the chord
// test cannot see at all - the height field is perfectly smooth across a change of filament, so
// without this a colour boundary lands on whatever triangles the relief happened to need, which on
// a flat surface is none. Only ever costs anything where a boundary actually runs.
float m_subdivide_color_mm = 0.3f;
// How many thousand triangles refinement may *add* (the mesh's own count is added on before it is
// passed as subdivide_mesh_adaptive()'s absolute cap, so the control still means something on a
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
// that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from
// turning into an out-of-memory, or an unrenderable preview wireframe.
//
// The default used to be 1500 (i.e. +1.5 M triangles), which is what made Standard mode's Bake
// take minutes: every stage after the subdivision - the displacement itself, the convex hull, the
// GLModel upload, and the re-slice changed_object() triggers - then runs on a mesh two orders of
// magnitude denser than the input. 750k is still far finer than any FDM nozzle resolves at the
// 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more.
int m_subdivide_budget_k = 750;
void subdivide_model_adaptive();
// Fills `region` (per current-mesh triangle, a REFINE_* bitmask) from the union of every layer's
// painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer
// coverage map the subdivision carries forward - the expensive half, skipped by the live preview,
// which only needs the region. Returns false when nothing is painted at all.
static bool collect_paint_region(const TriangleMesh &mesh, const TextureDisplacementFacetsData &facets,
std::vector<uint8_t> &region,
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint);
// Runs the volume's TextureDisplacementOptions smoothing over the *already committed* geometry,
// restricted to the painted area. The same settings are folded into Preview/Bake automatically;
// this is the escape hatch for relief that has already been baked in, where there is no
// displacement pass left to attach them to. Topology-preserving, so unlike subdivide and remesh it
// keeps every paint channel - including texture displacement - exactly as it was.
void smooth_model();
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
// the geometry, but unlike subdivide it keeps every paint channel: prepare_mesh() carries the
// texture-displacement masks across spatially, which is also what lets Standard mode remesh *after*
// the user has painted.
float m_remesh_target_edge_mm = 0.f;
// Dihedral angle above which an edge counts as a hard feature and is held fixed by the remesher.
// Off by default would round every sharp edge off, so this is on; 0 disables the protection.
float m_remesh_sharp_angle_deg = 40.f;
bool m_remesh_keep_sharp_edges = true;
void remesh_model();
// ---- Bake stage debug view ----
//
// A bake is a chain of stages that each rewrite the whole mesh, so when the result looks wrong the
// only useful question is which stage made it wrong. "Capture stages" runs the same recipe Bake
// runs, keeps every intermediate mesh, and draws the selected one through the ordinary
// true-displacement preview - so this needs no rendering code of its own.
//
// The run commits nothing: TextureDisplacementDebugJob never touches the Model.
std::vector<BakeStageSnapshot> m_debug_stages;
// Which stage is on screen. -1 means the debug view is off and the live preview owns
// m_preview_glmodel again; rebuild_preview() checks this before replacing it.
int m_debug_stage = -1;
bool m_debug_in_progress = false;
// The open / non-manifold counts come from a sort over every half-edge, which on a multi-million
// triangle stage costs more than the stage did. On by default because a stage that tore the mesh
// is exactly what this exists to find.
bool m_debug_check_topology = true;
// The default pipeline's automatic resolution/budget for the volume, cached on what it depends on.
const V2Resolution &v2_recommendation(const ModelVolume &mv);
V2Resolution m_v2_rec;
std::string m_v2_rec_key;
// Queues the capture run. Uses Standard mode's recipe (remesh, refine, displace) unless Pro mode
// has already prepared the mesh or the experimental pipeline is on, either of which has nothing
// to prepare.
void run_stage_debug();
// Puts stage `index` on screen. A stage over the memory cap has counts but no geometry and is
// skipped.
void show_debug_stage(int index);
// Drops the captured stages and hands m_preview_glmodel back to the live preview.
void exit_debug_view();
// The panel: capture button, stage list, and what each stage produced.
void render_debug_stage_panel(ModelVolume *mv);
// Live, pre-bake preview of the true displaced geometry (built by the same algorithm Bake
// uses). Empty/uninitialized whenever nothing is painted yet, in which case the gizmo falls
// 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().
bool m_preview_params_dirty = false;
// See rebuild_bump_preview_mesh()/render_bump_preview_mesh(). On by default: it is the cheap,
// instant-updating preview, so it is the better first impression while painting. The true-
// displacement view (a background CPU remesh) is one click away in the View row when the user
// wants an exact look at what Bake will produce.
bool m_use_bump_preview = true;
// 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
// 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;
// Translucent tint over the active layer's painted triangles, drawn on top of whichever preview
// is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used
// in either preview mode - it is coincident with the surface and simply covers it - so the only
// paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all
// until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
// stroke.
GLModel m_paint_overlay_glmodel;
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
// separate from m_bump_preview_dirty so a stroke refreshes only the small painted patch per frame,
bool m_paint_overlay_dirty = false;
void rebuild_paint_overlay();
void render_paint_overlay();
// Whether render_bump_preview_mesh() would actually draw something. Checked before the real volume
// is hidden: with no layer, no texture or no shader the bump path draws nothing, and hiding the
// volume for it left the model invisible.
bool bump_preview_ready() const;
// Whether the current bump mesh carries a precomputed per-vertex uv (LSCM) that the shader
// should sample at directly, rather than projecting in-shader. Set by rebuild_bump_preview_mesh().
bool m_bump_preview_uses_vertex_uv = false;
// The palette the fast preview's per-triangle filament indices were built against, captured when
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked
// into the mesh can never be resolved against a different set of filaments than they were computed
// from - loading a filament mid-session would otherwise recolour a stale preview at random.
std::vector<PaletteEntry> m_bump_preview_palette;
// 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
// 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
// resulting final-uv-space affine handed to the shader (identity except mid-drag).
int m_bump_active_chart = -1;
std::vector<uint8_t> m_bump_active_vertex;
Eigen::Matrix<float, 2, 3> m_bump_baked_active_xf = Eigen::Matrix<float, 2, 3>::Identity();
Eigen::Matrix<float, 2, 3> m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
void compute_bump_active_vertices(const std::vector<int> &charts);
// The set of islands the current UV-editor drag moves together: the pane's multi-selection unioned
// with each selected island's join group (see build_island_move_set()). Populated at drag start and
// cleared when it finishes. A move applies the same offset to every island in it; rotate/scale act
// only on the primary. Empty when no move drag is in flight.
std::vector<int> m_island_move_set;
// All islands that must move with `primary`: the pane's multi-selection plus, for each of those, the
// charts sharing its join group in `layer`. Always contains `primary`.
std::vector<int> build_island_move_set(const TextureDisplacementLayer &layer, int primary) const;
// The join-group id of chart `c`: its explicit entry in `groups`, or `c` itself (its own singleton)
// when unset. Two charts move together iff this matches.
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
// 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.
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer) const;
// `patch` with its vertices moved into world millimetres - the space the bake maps the texture in
// (see build_texture_displacement()). Returned by value because the caller usually still needs the
// original: the patch doubles as render geometry, which is drawn through the volume's own matrix.
indexed_triangle_set patch_in_world(const indexed_triangle_set &patch) const;
// UV-check overlay drawn over the painted patch to sanity-check the unwrap (#13/#14). Built by
// rebuild_uvcheck_mesh(), drawn by render_uvcheck_mesh() with the "texture_displacement_uvcheck"
// shader. Checker works for any projection; Distortion needs the per-vertex LSCM uv.
enum class UVCheckMode { None, Checker, Distortion };
UVCheckMode m_uv_check_mode = UVCheckMode::None;
GLModel m_uvcheck_glmodel;
bool m_uvcheck_uses_vertex_uv = false;
void rebuild_uvcheck_mesh();
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
// 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;
// The unwrap is expensive, so it is recomputed only when the user explicitly asks for it (the
// "Unwrap" button), not on every paint stroke or slider nudge. This is set by that button and
// consumed by the next update_uv_editor() call, which is the only path that re-solves the unwrap;
// every other call merely refreshes the cheap per-island affine transforms over the existing one.
bool m_uv_unwrap_pending = false;
// Set alongside m_uv_unwrap_pending only by the Unwrap button, so the connected-net auto-layout runs
// on a genuine re-unwrap but not on a refresh re-solve (a vertex-edit commit or undo), which must
// 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
// 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
// choice actually changes (the height texture is large; re-sending it every stroke would be waste).
enum class UVBackground { None, Height, Checker };
UVBackground m_uv_editor_bg = UVBackground::None;
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
// while the Distortion check mode is on. Empty otherwise.
std::vector<ColorRGBA> m_uv_editor_distortion_colors;
void compute_uv_editor_distortion_colors(const indexed_triangle_set &patch);
// Plain triangle-edge overlay on the mesh (#8), toggled independently of the check modes.
bool m_wireframe_overlay = false;
GLModel m_wireframe_overlay_glmodel;
size_t m_wireframe_overlay_vcount = 0; // topology signature, so it rebuilds only on a real change
void rebuild_wireframe_overlay(); // from the base mesh (bump/paint mode)
void build_wireframe_from_its(const indexed_triangle_set &its); // from an explicit mesh, no early-out
void refresh_wireframe(); // pick base vs displaced source for the current view
void render_wireframe_overlay();
// The displaced preview geometry the last preview job produced, kept so the wireframe overlay can be
// drawn on the raised surface actually shown in the true-displacement view (#: "wireframe in real mode").
indexed_triangle_set m_preview_its;
// Bumped on every rebuild_preview() call; a background TextureDisplacementPreviewJob's result
// is only applied if this hasn't moved on since the job was queued (see rebuild_preview()),
// so a burst of edits can't have an earlier, now-stale job clobber a later one's result.
//
// Shared with the worker thread (hence the atomic) so a running job can notice mid-computation
// that it has been superseded and abort, instead of running to completion for a result that will
// only be discarded on arrival.
std::shared_ptr<std::atomic<uint64_t>> m_preview_generation = std::make_shared<std::atomic<uint64_t>>(0);
// At most one preview job is ever queued. The UI job worker is a single FIFO queue shared with
// Bake (and with arrange/orient/send), and rebuild_preview() is called on every stroke end, every
// slider release and - with "Auto update" on - every frame of a slider drag. Queuing one full
// displacement per call built a backlog that took minutes to drain: the preview appeared frozen,
// and a Bake pressed afterwards sat behind the whole queue. So a request made while a job is in
// flight is recorded here and issued once that job settles, collapsing any number of edits into a
// single follow-up run.
bool m_preview_job_running = false;
bool m_preview_job_pending = false;
void queue_preview_job();
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
// Full-resolution height texture for the bump shader, keyed the same way (see
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
// blur the bake will apply.
std::unique_ptr<GLTexture> m_height_tex;
const void *m_height_tex_source = nullptr;
float m_height_tex_smoothing = -1.f;
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
std::unique_ptr<GLTexture> m_color_tex;
const void *m_color_tex_source = nullptr;
float m_color_tex_smoothing = -1.f;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
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
// 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.
struct UVEditorState
{
int slot = -1;
const void *image_data = nullptr;
float seam_angle = -1.f;
float padding = -2.f;
TriangleSelector::TriangleSplittingData facets;
// Manual/auto seam edges also change the unwrap, so a change here must force a re-solve just
// like the facets do (marking a seam leaves the paint mask untouched).
std::vector<std::pair<int, int>> seam_edges;
bool operator==(const UVEditorState &other) const
{
return slot == other.slot && image_data == other.image_data && seam_angle == other.seam_angle &&
padding == other.padding && facets == other.facets && seam_edges == other.seam_edges;
}
};
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
// 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();
// The unwrap m_uv_editor_state produced, kept so that changing tiling/rotation/offset only costs
// re-running apply_uv_transform() over it, not another extraction and solve.
PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
// open/close within a session, so the choice sticks while working.
bool m_undocked = false;
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
// moves a *target* offset instead; the real scroll is eased toward it over the following frames.
float m_panel_scroll_target = 0.f;
float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
// Last frame's body content height and footer height. The body is a child window that has to be
// given its height before its content is laid out, so it is sized from the previous frame: as tall
// as its content, capped so the footer still fits above the bottom of the canvas.
float m_panel_body_h = 0.f;
float m_panel_footer_h = 0.f;
// See the "Adjust Texture" block of private methods above.
bool m_adjust_texture_mode = false;
bool m_adjust_anchor_valid = false;
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
// the corresponding arrow, moves offset along only that one axis.
enum class AdjustHandle { None, Pan, AxisU, AxisV };
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
// moment the current drag started; every subsequent frame's delta is measured against this,
// rather than accumulated frame-to-frame, to avoid drift.
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
// both the U and V axis arrows), transformed into place at render time.
GLModel m_adjust_panel_glmodel;
GLModel m_adjust_arrow_glmodel;
std::map<std::string, wxString> m_desc;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). 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 -
// 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]
bool m_panel_icons_tried = false;
void ensure_panel_icons();
};
} // namespace Slic3r::GUI
#endif // slic3r_GLGizmoTextureDisplacement_hpp_

View File

@@ -22,6 +22,7 @@
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
@@ -164,6 +165,13 @@ void GLGizmosManager::switch_gizmos_icon_filename()
case(EType::FuzzySkin):
gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg");
break;
case(EType::TextureDisplacement):
// One shared icon in both themes (no dedicated dark variant yet) - but it must still be
// *this* gizmo's icon. Handing it the fuzzy-skin one here quietly replaced the icon set at
// construction, so the toolbar ended up showing two identical fuzzy-skin buttons after any
// light/dark switch.
gizmo->set_icon_filename("toolbar_texture_displacement.svg");
break;
case(EType::MeshBoolean):
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
break;
@@ -213,6 +221,8 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam));
m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::FuzzySkin));
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
@@ -524,6 +534,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
return dynamic_cast<GLGizmoCut3D*>(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == FuzzySkin)
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == TextureDisplacement)
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == MeshBoolean)
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == BrimEars)
@@ -537,6 +549,7 @@ bool GLGizmosManager::is_paint_gizmo()
return m_current == EType::FdmSupports ||
m_current == EType::MmSegmentation ||
m_current == EType::FuzzySkin ||
m_current == EType::TextureDisplacement ||
m_current == EType::Seam;
}
@@ -1514,6 +1527,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
return "Color Painting";
case GLGizmosManager::EType::FuzzySkin:
return "Fuzzy Skin Painting";
case GLGizmosManager::EType::TextureDisplacement:
return "Texture Displacement";
default:
return "";
}

View File

@@ -84,6 +84,7 @@ public:
Seam,
FuzzySkin,
MmSegmentation,
TextureDisplacement,
Emboss,
Svg,
Measure,

Some files were not shown because too many files have changed in this diff Show More