Texture displacement: animations that play on their own, and art for the
rest of the panel The animations advanced one frame per mouse movement and stood still whenever the mouse did. schedule_extra_frame() only guarantees an idle event at the moment the next frame is due; GLCanvas3D::on_idle() returns without rendering anything unless something marked the canvas dirty by then, and nothing had. set_dirty() on the gizmo is what on_idle reads, through GLGizmoBase::update_items_state(), to find out there are frames still to draw. Thirty-odd more pictures, covering the paint tools, brush size, the connected-area fill, the five views, the five projections, subdivision, feature-adaptive refinement, remeshing, hard edges, slivers, geometry smoothing, filament mixing and denoise. The generator grew three more drawing languages beside the shaded relief, each picked by what the setting is rather than by taste: a flat mesh of triangles where the subject is the mesh or a tool, a section where the subject is where the surface sits relative to where it was, and a flat ramp of greys where the subject is which filament a spot ends up as. Lighting a colour illustration or looking down on a vertical change would both add a second thing moving between two pictures meant to differ in one. Mapping is one ball with the checker laid on it five ways, which is the whole of what choosing a projection does - except Unwrap, which is not a projection onto the model and so is drawn as the flat layout it produces.
@@ -55,25 +55,45 @@ so an animation authored against that plays at the speed it was authored at.
|
||||
|
||||
The animation restarts when its tooltip opens, rather than running off a free-running clock: these are
|
||||
short loops of one setting going from one value to another, and joining one halfway through shows the
|
||||
end of a change the user never saw the start of. The canvas only redraws on demand, so each frame asks
|
||||
for the next through `GLCanvas3D::schedule_extra_frame()`, timed to when that frame is actually due.
|
||||
end of a change the user never saw the start of.
|
||||
|
||||
Driving the frames takes two calls, not one. `GLCanvas3D::schedule_extra_frame()` only guarantees an
|
||||
idle event at the moment the next frame is due; `GLCanvas3D::on_idle()` returns without rendering
|
||||
anything unless something has marked the canvas dirty by then, so an animation left to the timer alone
|
||||
advances one frame per mouse movement and stands still whenever the mouse does. `set_dirty()` on the
|
||||
gizmo is what on_idle reads, through `GLGizmoBase::update_items_state()`, to find out there are frames
|
||||
still to draw. The pair of them is what makes an animation play at its own speed and stop costing
|
||||
anything the moment its tooltip closes.
|
||||
|
||||
## Making one
|
||||
|
||||
Captures of the real tool are the first choice for anything involving the model, the mesh or colour.
|
||||
Settings whose meaning is a shape are drawn from the same arithmetic instead, by
|
||||
`scripts/gen_texture_displacement_tips.py`: it shades a height field as a relief catching the light,
|
||||
and covers Depth, Tile, Tile size, Rotation, Repeat/Mirror, Midlevel, Smoothing, Edge fade, Invert and
|
||||
Blend that way.
|
||||
`scripts/gen_texture_displacement_tips.py` draws the lot, in four languages, picked by what the
|
||||
setting actually is:
|
||||
|
||||
One motif - a ring inside a square frame - runs through nearly all of them, because using one shape
|
||||
everywhere is what lets two pictures be compared: whatever differs between them is the setting, since
|
||||
nothing else is.
|
||||
- **A shaded relief**, for the settings that are about a pattern: Depth, Tile, Tile size, Rotation,
|
||||
Repeat/Mirror, Smoothing, Edge fade, Invert, Blend and the Normal, Fast, Checker and Distortion
|
||||
views. One motif - a ring inside a square frame - runs through all of them, because using one shape
|
||||
everywhere is what lets two pictures be compared: whatever differs between them is the setting,
|
||||
since nothing else is.
|
||||
- **A flat mesh of triangles**, for the settings that are about the mesh or about a tool: the paint
|
||||
tools, brush size, the connected-area fill, subdivision, feature-adaptive refinement, remeshing,
|
||||
hard edges, slivers and the wireframe view. These carry the one accent colour in the set, meaning
|
||||
"painted", because that is the only thing in such a picture that is not geometry.
|
||||
- **A section**, for Midlevel and for smoothing the geometry. The reason generalises: shading reads
|
||||
slopes, so a height field moved up or down as a whole shades identically to where it started, and
|
||||
any setting whose meaning is *where the surface sits relative to where it was* is invisible from
|
||||
above and has to be drawn from the side.
|
||||
- **A flat ramp of greys**, for Mix filaments, Mixed colors and Denoise. Those are about which
|
||||
filament a spot ends up as; lighting one would only add a second thing changing between two
|
||||
pictures meant to differ in one.
|
||||
|
||||
Midlevel is drawn in section rather than as a relief, and the reason generalises. Shading reads
|
||||
slopes, so a height field moved up or down as a whole shades identically to where it started; any
|
||||
setting whose meaning is *where the relief sits relative to the surface* is invisible in a top view
|
||||
and has to be drawn from the side, against the bare surface as a reference.
|
||||
The five Mapping pictures are one ball with a checker laid on it five ways, which is the whole of what
|
||||
choosing a projection does - except Unwrap, which is not a projection onto the model at all and so is
|
||||
drawn as the flat layout it produces.
|
||||
|
||||
Captures of the real tool are still the first choice for anything involving a real model or real
|
||||
filament colour: Bake, the mixed-colour count on a photograph, Adjust placement, the frame window and
|
||||
the UV editor's seam tool have no picture yet for that reason.
|
||||
|
||||
Either way:
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 112 KiB |
@@ -21,7 +21,7 @@ Needs numpy and pillow.
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
SIZE = 256 # px, square - the panel draws these at whatever width the tooltip has
|
||||
GIF_SIZE = 192 # animations are the same picture, smaller: every frame of one ships in resources/
|
||||
@@ -83,7 +83,7 @@ def save_png(name, rgb):
|
||||
|
||||
def save_gif(name, frames, delay_ms):
|
||||
path = os.path.join(OUT_DIR, name)
|
||||
images = [Image.fromarray(vignette(f)).convert("P", palette=Image.ADAPTIVE, colors=32) for f in frames]
|
||||
images = [Image.fromarray(vignette(f)).convert("P", palette=Image.ADAPTIVE, colors=24) for f in frames]
|
||||
images[0].save(path, save_all=True, append_images=images[1:], duration=delay_ms, loop=0, optimize=True)
|
||||
print("wrote", os.path.normpath(path), os.path.getsize(path) // 1024, "KB")
|
||||
|
||||
@@ -225,6 +225,404 @@ def dome(size=SIZE):
|
||||
return smoothstep(0.52, 0.02, np.hypot(x, y))
|
||||
|
||||
|
||||
# --- meshes, cursors and sections ---------------------------------------------------------------
|
||||
#
|
||||
# The second family of pictures. The settings above are about a pattern, and a shaded relief is what a
|
||||
# pattern looks like; these are about a tool or about the mesh, where what matters is which triangles
|
||||
# exist and which of them are painted. So they are drawn flat, the way the panel's own wireframe and
|
||||
# paint overlays draw them, with one accent colour meaning "painted" and nothing else coloured.
|
||||
|
||||
SS = 3 # supersampling: PIL fills polygons without antialiasing, so draw big and shrink
|
||||
|
||||
INK = (0.62, 0.67, 0.67) # mesh edges
|
||||
BARE = (0.28, 0.30, 0.31) # unpainted surface
|
||||
PAINT = (0.20, 0.47, 0.45) # painted with the active layer, in the panel's own accent
|
||||
CURSOR = (0.92, 0.95, 0.95)
|
||||
MARGIN = 0.05 # keeps the mesh off the picture's border, where the vignette darkens it
|
||||
|
||||
|
||||
def _px(p, size):
|
||||
"""Unit coordinates to pixels, inside the margin."""
|
||||
return ((MARGIN + p[0] * (1.0 - 2.0 * MARGIN)) * size, (MARGIN + p[1] * (1.0 - 2.0 * MARGIN)) * size)
|
||||
|
||||
|
||||
def _rgb255(c):
|
||||
return tuple(int(round(v * 255.0)) for v in c)
|
||||
|
||||
|
||||
def raster(draw_fn, size=SIZE, background=BARE):
|
||||
"""Runs draw_fn(draw, size_in_pixels) on a supersampled canvas and hands back the shrunk result.
|
||||
|
||||
`background` is a colour, or a picture already drawn by the relief half of this script - which is
|
||||
how a mesh is shown sitting on the surface it refines.
|
||||
"""
|
||||
if isinstance(background, np.ndarray):
|
||||
image = Image.fromarray(background).resize((size * SS, size * SS), Image.BILINEAR)
|
||||
else:
|
||||
image = Image.new("RGB", (size * SS, size * SS), _rgb255(background))
|
||||
draw_fn(ImageDraw.Draw(image), size * SS)
|
||||
return np.asarray(image.resize((size, size), Image.LANCZOS)).astype(np.uint8)
|
||||
|
||||
|
||||
def tri_mesh(n, jitter=0.0, seed=1):
|
||||
"""A square patch of triangles, optionally with its inner vertices nudged off the grid.
|
||||
|
||||
Jitter is not decoration: a perfectly regular grid reads as graph paper, and the point of these
|
||||
pictures is a mesh over a surface.
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
v = np.stack(np.meshgrid(np.linspace(0.0, 1.0, n + 1), np.linspace(0.0, 1.0, n + 1)), axis=-1)
|
||||
if jitter:
|
||||
v[1:n, 1:n] += (rng.random((n - 1, n - 1, 2)) - 0.5) * (jitter / n)
|
||||
tris = []
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
tris.append((v[i, j], v[i, j + 1], v[i + 1, j + 1]))
|
||||
tris.append((v[i, j], v[i + 1, j + 1], v[i + 1, j]))
|
||||
return tris
|
||||
|
||||
|
||||
def centroids(tris):
|
||||
return np.array([np.mean(t, axis=0) for t in tris])
|
||||
|
||||
|
||||
def neighbours(tris):
|
||||
"""Which triangles share an edge with which, keyed on the rounded vertex positions."""
|
||||
edges = {}
|
||||
for i, t in enumerate(tris):
|
||||
for a, b in ((0, 1), (1, 2), (2, 0)):
|
||||
key = tuple(sorted((tuple(np.round(t[a], 6)), tuple(np.round(t[b], 6)))))
|
||||
edges.setdefault(key, []).append(i)
|
||||
out = [[] for _ in tris]
|
||||
for shared in edges.values():
|
||||
if len(shared) == 2:
|
||||
out[shared[0]].append(shared[1])
|
||||
out[shared[1]].append(shared[0])
|
||||
return out
|
||||
|
||||
|
||||
def draw_mesh(draw, size, tris, painted=None, edge=INK, edge_width=0.7):
|
||||
fill_bare, fill_paint = _rgb255(BARE), _rgb255(PAINT)
|
||||
for i, t in enumerate(tris):
|
||||
draw.polygon([_px(p, size) for p in t],
|
||||
fill=fill_paint if painted is not None and painted[i] else fill_bare,
|
||||
outline=_rgb255(edge), width=max(1, int(edge_width * SS)))
|
||||
|
||||
|
||||
def draw_brush(draw, size, centre, radius, filled=False):
|
||||
"""The brush cursor: a ring, which is how the gizmo draws it on the model."""
|
||||
c = _px(centre, size)
|
||||
r = radius * (1.0 - 2.0 * MARGIN) * size
|
||||
if filled:
|
||||
draw.ellipse([c[0] - r, c[1] - r, c[0] + r, c[1] + r], fill=(255, 255, 255, 40))
|
||||
draw.ellipse([c[0] - r, c[1] - r, c[0] + r, c[1] + r], outline=_rgb255(CURSOR), width=max(1, int(1.6 * SS)))
|
||||
|
||||
|
||||
def draw_pointer(draw, size, at):
|
||||
"""A small arrow, for the tools that are clicked rather than dragged."""
|
||||
x, y = _px(at, size)
|
||||
k = 0.055 * size
|
||||
draw.polygon([(x, y), (x, y + 1.45 * k), (x + 0.38 * k, y + 1.05 * k),
|
||||
(x + 0.62 * k, y + 1.55 * k), (x + 0.90 * k, y + 1.40 * k),
|
||||
(x + 0.66 * k, y + 0.92 * k), (x + 1.05 * k, y + 0.84 * k)],
|
||||
fill=_rgb255(CURSOR), outline=(30, 32, 34), width=max(1, int(0.9 * SS)))
|
||||
|
||||
|
||||
def stroke_path(t):
|
||||
"""The path a demonstrated brush stroke follows, t in 0..1."""
|
||||
return np.array([0.14 + 0.72 * t, 0.46 + 0.22 * np.sin(2.0 * np.pi * t * 1.1)])
|
||||
|
||||
|
||||
def brush_frames(size, erase, steps=14, radius=0.115, hold=3):
|
||||
tris = tri_mesh(9, jitter=0.55)
|
||||
cents = centroids(tris)
|
||||
painted = np.ones(len(tris), bool) if erase else np.zeros(len(tris), bool)
|
||||
frames = []
|
||||
for k in range(steps):
|
||||
centre = stroke_path(k / (steps - 1.0))
|
||||
hit = np.linalg.norm(cents - centre, axis=1) < radius
|
||||
painted = painted & ~hit if erase else painted | hit
|
||||
state = painted.copy()
|
||||
frames.append(raster(lambda d, s, st=state, c=centre: (draw_mesh(d, s, tris, st), draw_brush(d, s, c, radius)), size))
|
||||
frames += [frames[-1]] * hold
|
||||
return frames
|
||||
|
||||
|
||||
def face_frames(size, steps=9, hold=3):
|
||||
"""The Face tool: one triangle per click, under a pointer."""
|
||||
tris = tri_mesh(7, jitter=0.5, seed=4)
|
||||
cents = centroids(tris)
|
||||
order = np.argsort(np.linalg.norm(cents - np.array([0.5, 0.5]), axis=1))[:steps]
|
||||
painted = np.zeros(len(tris), bool)
|
||||
frames = []
|
||||
for i in order:
|
||||
painted[i] = True
|
||||
state = painted.copy()
|
||||
at = cents[i]
|
||||
frames.append(raster(lambda d, s, st=state, a=at: (draw_mesh(d, s, tris, st), draw_pointer(d, s, a)), size))
|
||||
frames += [frames[-1]] * hold
|
||||
return frames
|
||||
|
||||
|
||||
def flood_frames(size, creases, steps=12, hold=3, seed=7):
|
||||
"""The Connected area tool: a flood from one click, stopped by the creases it may not cross.
|
||||
|
||||
`creases` is a list of x positions; a triangle pair straddling one cannot pass the fill between
|
||||
them, which is what an edge sharper than the angle threshold does on the model.
|
||||
"""
|
||||
tris = tri_mesh(9, jitter=0.5, seed=seed)
|
||||
cents = centroids(tris)
|
||||
adj = neighbours(tris)
|
||||
blocked = lambda i, j: any((cents[i][0] - x) * (cents[j][0] - x) < 0.0 for x in creases)
|
||||
|
||||
seed_tri = int(np.argmin(np.linalg.norm(cents - np.array([0.22, 0.5]), axis=1)))
|
||||
painted = np.zeros(len(tris), bool)
|
||||
painted[seed_tri] = True
|
||||
frontier = [seed_tri]
|
||||
frames = []
|
||||
for _ in range(steps):
|
||||
state = painted.copy()
|
||||
frames.append(raster(lambda d, s, st=state: (draw_mesh(d, s, tris, st),
|
||||
draw_creases(d, s, creases),
|
||||
draw_pointer(d, s, cents[seed_tri])), size))
|
||||
nxt = []
|
||||
for i in frontier:
|
||||
for j in adj[i]:
|
||||
if not painted[j] and not blocked(i, j):
|
||||
painted[j] = True
|
||||
nxt.append(j)
|
||||
frontier = nxt
|
||||
frames += [frames[-1]] * hold
|
||||
return frames
|
||||
|
||||
|
||||
def draw_creases(draw, size, creases):
|
||||
"""A fold too sharp for the fill to cross: drawn dark with a bright crest, like a real one."""
|
||||
for x in creases:
|
||||
a, b = _px((x, -0.02), size), _px((x, 1.02), size)
|
||||
draw.line([a, b], fill=_rgb255((0.08, 0.09, 0.10)), width=max(1, int(4.0 * SS)))
|
||||
draw.line([a, b], fill=_rgb255((0.97, 0.99, 0.99)), width=max(1, int(1.4 * SS)))
|
||||
|
||||
|
||||
|
||||
def split4(tri):
|
||||
a, b, c = tri
|
||||
ab, bc, ca = (a + b) * 0.5, (b + c) * 0.5, (c + a) * 0.5
|
||||
return [(a, ab, ca), (ab, b, bc), (ca, bc, c), (ab, bc, ca)]
|
||||
|
||||
|
||||
def refine(tris, density, max_depth, threshold=0.45):
|
||||
"""Splits a triangle into four wherever `density` at its middle asks for it, up to max_depth."""
|
||||
|
||||
def go(tri, depth):
|
||||
if depth >= max_depth:
|
||||
return [tri]
|
||||
centre = np.mean(tri, axis=0)
|
||||
if density(centre[0], centre[1]) < threshold:
|
||||
return [tri]
|
||||
return [out for part in split4(tri) for out in go(part, depth + 1)]
|
||||
|
||||
return [out for tri in tris for out in go(tri, 0)]
|
||||
|
||||
|
||||
def sampler(field):
|
||||
"""Looks a numpy field up at unit coordinates, for use as a density."""
|
||||
n = field.shape[0]
|
||||
return lambda x, y: float(field[int(np.clip(y * n, 0, n - 1)), int(np.clip(x * n, 0, n - 1))])
|
||||
|
||||
|
||||
def feature_density(size=192):
|
||||
"""High where the pattern bends, which is where feature-adaptive refinement spends triangles.
|
||||
|
||||
One large copy of the motif rather than a field of them: the point is that the triangles gather
|
||||
somewhere and not elsewhere, and a pattern that bends everywhere has no elsewhere to show.
|
||||
"""
|
||||
m = motif(1, size)
|
||||
gy, gx = np.gradient(m)
|
||||
g = blur(np.hypot(gx, gy), 2)
|
||||
return g / max(g.max(), 1e-6)
|
||||
|
||||
|
||||
def irregular_mesh(seed=3):
|
||||
"""The uneven mesh a model arrives with: coarse here, fine there, nothing of one size."""
|
||||
rng = np.random.default_rng(seed)
|
||||
tris = tri_mesh(4, jitter=0.9, seed=seed)
|
||||
out = []
|
||||
for t in tris:
|
||||
out.extend(split4(t) if rng.random() < 0.45 else [t])
|
||||
final = []
|
||||
for t in out:
|
||||
final.extend(split4(t) if rng.random() < 0.3 else [t])
|
||||
return final
|
||||
|
||||
|
||||
def snap_to_crease(tris, crease, distance=0.07):
|
||||
"""Pulls every vertex near the crease onto it, so mesh edges run along it instead of across."""
|
||||
(x0, y0), (x1, y1) = crease
|
||||
d = np.array([x1 - x0, y1 - y0], float)
|
||||
d /= np.linalg.norm(d)
|
||||
|
||||
def snap(p):
|
||||
t = np.dot(p - np.array([x0, y0]), d)
|
||||
foot = np.array([x0, y0]) + t * d
|
||||
return foot if np.linalg.norm(p - foot) < distance else p
|
||||
|
||||
return [tuple(snap(np.asarray(v, float)) for v in t) for t in tris]
|
||||
|
||||
|
||||
def sliver_mesh(seed=5):
|
||||
"""A mesh carrying the thin triangles refinement inherits from a model's own triangulation."""
|
||||
rng = np.random.default_rng(seed)
|
||||
tris = tri_mesh(7, jitter=0.4, seed=seed)
|
||||
out = []
|
||||
for t in tris:
|
||||
if rng.random() < 0.45:
|
||||
a, b, c = (np.asarray(v, float) for v in t)
|
||||
mid = (b + c) * 0.5
|
||||
a = mid + (a - mid) * 0.08 # all but flattened onto the opposite edge
|
||||
out.append((a, b, c))
|
||||
else:
|
||||
out.append(t)
|
||||
return out
|
||||
|
||||
|
||||
def draw_profile(draw, size, height_1d, baseline=0.62, amplitude=0.30):
|
||||
"""A side view of a surface, as a filled region under its profile."""
|
||||
n = len(height_1d)
|
||||
top = [(i / (n - 1.0) * size, (baseline - (height_1d[i] - 0.5) * amplitude) * size) for i in range(n)]
|
||||
draw.polygon(top + [(size, size * 1.1), (0, size * 1.1)], fill=_rgb255((0.44, 0.46, 0.47)))
|
||||
draw.line(top, fill=_rgb255((0.82, 0.88, 0.87)), width=max(1, int(1.8 * SS)))
|
||||
|
||||
|
||||
def bayer(size, n=4):
|
||||
"""An ordered dither matrix tiled over the picture, for the mixing illustrations."""
|
||||
m = np.array([[0]])
|
||||
while m.shape[0] < n:
|
||||
m = np.block([[4 * m, 4 * m + 2], [4 * m + 3, 4 * m + 1]])
|
||||
m = (m + 0.5) / m.size
|
||||
return np.tile(m, (size // m.shape[0] + 1, size // m.shape[1] + 1))[:size, :size]
|
||||
|
||||
|
||||
def levels_image(levels, dither=False, speckle=0.0, size=SIZE, seed=11):
|
||||
"""A smooth ramp reduced to a handful of greys - what a bake does to a photograph's colours.
|
||||
|
||||
Flat rather than lit: this family of settings is about which filament a pixel ends up as, and a
|
||||
relief would only add a second thing changing between two pictures that are meant to differ in
|
||||
one.
|
||||
"""
|
||||
x, y = grid(size)
|
||||
ramp = np.clip(0.5 + 0.5 * np.cos(2.0 * np.pi * (x + 0.25 * y)), 0.0, 1.0)
|
||||
noise = bayer(size) - 0.5 if dither else 0.0
|
||||
q = np.round(np.clip(ramp + noise / max(levels - 1, 1), 0.0, 1.0) * (levels - 1)) / (levels - 1)
|
||||
if speckle > 0.0:
|
||||
rng = np.random.default_rng(seed)
|
||||
hit = rng.random((size, size)) < speckle
|
||||
q = np.where(hit, rng.integers(0, levels, (size, size)) / (levels - 1), q)
|
||||
grey = 0.16 + 0.62 * q
|
||||
return (np.dstack([grey, grey * 1.03, grey * 1.02]).clip(0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
|
||||
|
||||
|
||||
def draw_mesh_lines(draw, size, tris, colour=(0.90, 0.94, 0.94), width=0.8):
|
||||
"""Only the edges, for drawing a mesh over a picture of the surface it sits on."""
|
||||
for t in tris:
|
||||
pts = [_px(p, size) for p in t]
|
||||
draw.line(pts + [pts[0]], fill=_rgb255(colour), width=max(1, int(width * SS)))
|
||||
|
||||
|
||||
def draw_line(draw, size, segment, colour=(0.97, 0.99, 0.99)):
|
||||
a, b = _px(segment[0], size), _px(segment[1], size)
|
||||
draw.line([a, b], fill=_rgb255((0.08, 0.09, 0.10)), width=max(1, int(4.0 * SS)))
|
||||
draw.line([a, b], fill=_rgb255(colour), width=max(1, int(1.4 * SS)))
|
||||
|
||||
|
||||
def posterize(rgb, levels):
|
||||
"""The shaded picture reduced to flat bands - what the fast preview's per-fragment quantizing
|
||||
does to a surface that the real preview shades smoothly."""
|
||||
v = rgb.astype(float) / 255.0
|
||||
return (np.round(v * (levels - 1)) / (levels - 1) * 255.0 + 0.5).astype(np.uint8)
|
||||
|
||||
|
||||
def with_checker(rgb, squares=10, strength=0.30, warp=False):
|
||||
"""A checker laid over the surface, evenly or stretched.
|
||||
|
||||
Even squares are what the Checker view is for - a texture laid out without stretching. The warped
|
||||
version is the same squares through a mapping that does stretch, which is what the Distortion view
|
||||
is there to find: cells crowded in one place and pulled apart in another.
|
||||
"""
|
||||
size = rgb.shape[0]
|
||||
x, y = grid(size)
|
||||
u, v = (np.sign(x) * np.abs(2.0 * x) ** 2.4 * 0.5, y * (1.0 + 0.9 * np.abs(x))) if warp else (x, y)
|
||||
cells = (np.floor((u + 0.5) * squares) + np.floor((v + 0.5) * squares)) % 2.0
|
||||
return (rgb * (1.0 - strength * cells)[..., None]).astype(np.uint8)
|
||||
|
||||
|
||||
def projected_sphere(kind, size=SIZE, squares=9):
|
||||
"""A ball with a checker laid on it by one of the panel's projections.
|
||||
|
||||
One shape, five mappings: what differs between the pictures is only where the squares go, which is
|
||||
the whole of what choosing a projection does. A sphere is the shape that shows it - every one of
|
||||
these mappings is well behaved on a flat face and tells itself apart on a curved one.
|
||||
"""
|
||||
a = (np.arange(size) + 0.5) / size * 2.24 - 1.12
|
||||
x, y = np.meshgrid(a, a)
|
||||
inside = x * x + y * y <= 1.0
|
||||
z = np.sqrt(np.clip(1.0 - x * x - y * y, 0.0, 1.0))
|
||||
|
||||
lam = np.clip(x * LIGHT[0] + y * LIGHT[1] + z * LIGHT[2], 0.0, 1.0)
|
||||
rgb = SHADOW + (HIGHLIGHT - SHADOW) * (lam ** 0.85)[..., None]
|
||||
|
||||
around = np.arctan2(x, z) / (2.0 * np.pi) + 0.5
|
||||
if kind == "sphere":
|
||||
u, v = around, np.arccos(np.clip(-y, -1.0, 1.0)) / np.pi
|
||||
elif kind == "cylinder":
|
||||
u, v = around, (y + 1.0) * 0.5
|
||||
elif kind == "view":
|
||||
u, v = (x + 1.0) * 0.5, (y + 1.0) * 0.5
|
||||
elif kind == "cube":
|
||||
axis = np.argmax(np.abs(np.dstack([x, y, z])), axis=2)
|
||||
u = np.where(axis == 0, z, x)
|
||||
v = np.where(axis == 1, z, y)
|
||||
u, v = (u + 1.0) * 0.5, (v + 1.0) * 0.5
|
||||
else:
|
||||
raise ValueError(kind)
|
||||
|
||||
cells = (np.floor(u * squares * 2.0) + np.floor(v * squares)) % 2.0
|
||||
rgb = rgb * (1.0 - 0.34 * cells)[..., None]
|
||||
rgb = np.where(inside[..., None], rgb, np.array([0.13, 0.14, 0.15]))
|
||||
return (np.clip(rgb, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
|
||||
|
||||
|
||||
def uv_islands(size=SIZE, squares=9, seed=12):
|
||||
"""The flat layout an unwrap produces: the surface cut into islands and laid out side by side.
|
||||
|
||||
Not a projection onto the model like the other four, so not drawn on one - an unwrap is the model
|
||||
carrying its own coordinates, and this is what those look like.
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
x, y = grid(size)
|
||||
cells = (np.floor((x + 0.5) * squares * 2.0) + np.floor((y + 0.5) * squares)) % 2.0
|
||||
checker = (np.array([0.40, 0.43, 0.43]) + np.array([0.22, 0.24, 0.24]) * (1.0 - cells)[..., None])
|
||||
|
||||
def blob(centre, radius, lobes):
|
||||
angles = np.linspace(0.0, 2.0 * np.pi, 26, endpoint=False)
|
||||
wobble = 1.0 + 0.22 * np.sin(lobes * angles + rng.random() * 6.0)
|
||||
return [(centre[0] + np.cos(t) * radius * w, centre[1] + np.sin(t) * radius * w)
|
||||
for t, w in zip(angles, wobble)]
|
||||
|
||||
islands = [blob((0.31, 0.32), 0.21, 3), blob((0.72, 0.30), 0.17, 4),
|
||||
blob((0.34, 0.74), 0.17, 4), blob((0.72, 0.71), 0.20, 3)]
|
||||
|
||||
mask = np.asarray(raster(lambda d, px: [d.polygon([(p[0] * px, p[1] * px) for p in island], fill=(255, 255, 255))
|
||||
for island in islands],
|
||||
size, background=(0.0, 0.0, 0.0))).astype(float)[..., 0] / 255.0
|
||||
rgb = checker * mask[..., None] + np.array([0.13, 0.14, 0.15]) * (1.0 - mask)[..., None]
|
||||
out = (np.clip(rgb, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
|
||||
return raster(lambda d, px: [d.polygon([(p[0] * px, p[1] * px) for p in island], outline=_rgb255(INK),
|
||||
width=max(1, int(1.1 * SS)))
|
||||
for island in islands],
|
||||
size, background=out)
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
@@ -288,6 +686,108 @@ def main():
|
||||
frames = [profile_view(wave, t, size=GIF_SIZE) for t in sweep]
|
||||
save_gif("midlevel.gif", frames, delay_ms=100)
|
||||
|
||||
# The paint tools, each shown doing the one thing that tells it apart from the others.
|
||||
save_gif("paint.gif", brush_frames(GIF_SIZE, erase=False), delay_ms=110)
|
||||
save_gif("erase.gif", brush_frames(GIF_SIZE, erase=True), delay_ms=110)
|
||||
save_gif("tool_face.gif", face_frames(GIF_SIZE), delay_ms=180)
|
||||
save_gif("tool_smart_fill.gif", flood_frames(GIF_SIZE, creases=[0.63]), delay_ms=110)
|
||||
|
||||
# Brush size: the ring, and what it covers, over one size and back.
|
||||
brush_tris = tri_mesh(9, jitter=0.55, seed=2)
|
||||
brush_cents = centroids(brush_tris)
|
||||
frames = []
|
||||
for i in range(steps):
|
||||
r = 0.07 + 0.16 * (0.5 - 0.5 * np.cos(2.0 * np.pi * i / steps))
|
||||
under = np.linalg.norm(brush_cents - np.array([0.5, 0.5]), axis=1) < r
|
||||
frames.append(raster(lambda d, sz, u=under, rr=r: (draw_mesh(d, sz, brush_tris, u),
|
||||
draw_brush(d, sz, (0.5, 0.5), rr)), GIF_SIZE))
|
||||
save_gif("cursor_size.gif", frames, delay_ms=90)
|
||||
|
||||
# Angle threshold: the same fill, let past one fold and then past both.
|
||||
for name, creases in (("smart_fill_angle_low.png", [0.40, 0.70]), ("smart_fill_angle_high.png", [0.70])):
|
||||
done = flood_frames(SIZE, creases=creases, steps=22, hold=0)[-1]
|
||||
save_png(name, done)
|
||||
|
||||
# Whole model: every face painted, and every face cleared.
|
||||
whole = tri_mesh(9, jitter=0.55, seed=2)
|
||||
save_png("select_all.png", raster(lambda d, sz: draw_mesh(d, sz, whole, np.ones(len(whole), bool)), SIZE))
|
||||
save_png("erase_all.png", raster(lambda d, sz: draw_mesh(d, sz, whole, np.zeros(len(whole), bool)), SIZE))
|
||||
|
||||
# The views the panel offers over the painted area.
|
||||
relief = shade(motif(3))
|
||||
save_png("view_fast.png", posterize(relief, 3))
|
||||
# Checker and Distortion are about how the texture is laid out, not about the texture, so they go
|
||||
# on a plain swell where the checker itself is what there is to look at.
|
||||
plain = shade(0.25 + 0.5 * dome())
|
||||
save_png("view_checker.png", with_checker(plain, squares=7, strength=0.40))
|
||||
save_png("view_distortion.png", with_checker(plain, squares=7, strength=0.40, warp=True))
|
||||
save_png("view_wireframe.png", raster(lambda d, sz: draw_mesh_lines(d, sz, tri_mesh(9, jitter=0.55, seed=2)),
|
||||
SIZE, background=relief))
|
||||
|
||||
# The projections: one ball, the checker put on it five different ways.
|
||||
for kind in ("cube", "cylinder", "sphere", "view"):
|
||||
save_png("map_%s.png" % kind, projected_sphere(kind))
|
||||
save_png("map_unwrap.png", uv_islands())
|
||||
|
||||
# Refinement: where the triangles go, and how many.
|
||||
density_feature = sampler(feature_density())
|
||||
coarse = tri_mesh(3)
|
||||
save_png("adaptive_off.png", raster(lambda d, sz: draw_mesh(d, sz, refine(coarse, lambda x, y: 1.0, 2)), SIZE))
|
||||
save_png("adaptive_on.png", raster(lambda d, sz: draw_mesh(d, sz, refine(coarse, density_feature, 3)), SIZE))
|
||||
|
||||
disc = sampler(painted_area(192, radius=0.34))
|
||||
painted_mesh = refine(coarse, disc, 3)
|
||||
save_png("painted_only_on.png",
|
||||
raster(lambda d, sz: draw_mesh(d, sz, painted_mesh,
|
||||
np.array([disc(*np.mean(t, axis=0)) > 0.5 for t in painted_mesh])), SIZE))
|
||||
everywhere = refine(coarse, lambda x, y: 1.0, 3)
|
||||
save_png("painted_only_off.png",
|
||||
raster(lambda d, sz: draw_mesh(d, sz, everywhere,
|
||||
np.array([disc(*np.mean(t, axis=0)) > 0.5 for t in everywhere])), SIZE))
|
||||
|
||||
# Subdivide: every triangle into four, a step at a time.
|
||||
level = tri_mesh(2)
|
||||
frames = []
|
||||
for _ in range(4):
|
||||
frames.append(raster(lambda d, sz, m=level: draw_mesh(d, sz, m), GIF_SIZE))
|
||||
level = [out for t in level for out in split4(t)]
|
||||
save_gif("subdivide.gif", frames, delay_ms=650)
|
||||
|
||||
# Remesh: the model's own uneven triangulation, rebuilt at one size.
|
||||
save_png("remesh_before.png", raster(lambda d, sz: draw_mesh(d, sz, irregular_mesh()), SIZE))
|
||||
save_png("remesh_after.png", raster(lambda d, sz: draw_mesh(d, sz, tri_mesh(8, jitter=0.35, seed=9)), SIZE))
|
||||
|
||||
# Hard edges: a fold the remesh runs across, against one it lays its edges along.
|
||||
crease = ((0.06, 0.84), (0.94, 0.20))
|
||||
across = tri_mesh(5, jitter=0.45, seed=6)
|
||||
save_png("preserve_edges_off.png",
|
||||
raster(lambda d, sz: (draw_mesh(d, sz, across), draw_line(d, sz, crease)), SIZE))
|
||||
save_png("preserve_edges_on.png",
|
||||
raster(lambda d, sz: (draw_mesh(d, sz, snap_to_crease(across, crease, 0.13)),
|
||||
draw_line(d, sz, crease)), SIZE))
|
||||
|
||||
# Thin triangles, and the same mesh without them.
|
||||
save_png("collapse_slivers_off.png", raster(lambda d, sz: draw_mesh(d, sz, sliver_mesh()), SIZE))
|
||||
save_png("collapse_slivers_on.png", raster(lambda d, sz: draw_mesh(d, sz, tri_mesh(7, jitter=0.4, seed=5)), SIZE))
|
||||
|
||||
# Smoothing the geometry after the texture is in it: the steps relax out of the surface.
|
||||
stepped_profile = np.floor(profile_wave(GIF_SIZE, periods=2.0) * 5.0) / 4.0
|
||||
frames = []
|
||||
for t in sweep:
|
||||
h = stepped_profile.copy()
|
||||
for _ in range(int(round(t * 6))):
|
||||
h = blur(h[None, :], 2)[0]
|
||||
frames.append(raster(lambda d, sz, hh=h: draw_profile(d, sz, hh), GIF_SIZE, background=(0.11, 0.12, 0.13)))
|
||||
save_gif("geometry_smoothing.gif", frames, delay_ms=100)
|
||||
|
||||
# Mixing, how many mixes, and denoise - all about which filament a spot ends up as, so all flat.
|
||||
save_png("mix_off.png", levels_image(4))
|
||||
save_png("mix_on.png", levels_image(4, dither=True))
|
||||
save_png("mixed_colors_few.png", levels_image(3, dither=True))
|
||||
save_png("mixed_colors_many.png", levels_image(8, dither=True))
|
||||
save_png("denoise_off.png", levels_image(5, dither=True, speckle=0.05))
|
||||
save_png("denoise_on.png", levels_image(5, dither=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -3791,10 +3791,18 @@ void GLGizmoTextureDisplacement::render_tip_media(const std::vector<TipMedia> &m
|
||||
ImGui::SetCursorPosX(cell_x + std::round(0.5f * (cell_w - w)));
|
||||
ImGui::Image((ImTextureID) (intptr_t) id, ImVec2(w, h));
|
||||
ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), frame, style.FrameRounding);
|
||||
// The canvas only redraws on demand, so an animation has to ask - and asks for exactly when its
|
||||
// next frame is due, rather than for a redraw as fast as the machine will go.
|
||||
if (texture->is_animated())
|
||||
// The canvas only redraws on demand, so an animation has to ask for its own frames - and for
|
||||
// exactly when the next one is due, rather than for a redraw as fast as the machine will go.
|
||||
//
|
||||
// Both halves are needed. schedule_extra_frame() alone only guarantees an idle event at that
|
||||
// moment; GLCanvas3D::on_idle() returns without rendering anything unless something has marked
|
||||
// the canvas dirty by then, which is why an animation left to that alone advanced one frame per
|
||||
// mouse movement and stood still whenever the mouse did. set_dirty() is what on_idle reads
|
||||
// (through GLGizmoBase::update_items_state()) to find out this gizmo still has frames to draw.
|
||||
if (texture->is_animated()) {
|
||||
set_dirty();
|
||||
m_parent.schedule_extra_frame(int(next * 1000.0));
|
||||
}
|
||||
|
||||
if (!entry.caption.empty()) {
|
||||
// Centred under its own illustration, and never wider than its column.
|
||||
@@ -5962,8 +5970,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// `unavailable`, when not empty, draws it faded but still hoverable, so the tooltip can say why it
|
||||
// cannot be used - a disabled ImGui item would swallow the hover and leave the user guessing.
|
||||
const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, float sz, const wxString &label,
|
||||
const wxString &tip_text, const wxString &unavailable = wxString(),
|
||||
const std::vector<TipMedia> &media = {}) -> bool {
|
||||
const wxString &tip_text, const std::vector<TipMedia> &media = {},
|
||||
const wxString &unavailable = wxString()) -> bool {
|
||||
const bool na = !unavailable.empty();
|
||||
const auto it = m_panel_icon_map.find(iconfile);
|
||||
const float pad = std::max(1.f, std::round(sz * 0.14f));
|
||||
@@ -6215,11 +6223,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SetCursorPosX(toggles_x);
|
||||
if (icon_toggle(820, "texture_displacement_add.svg", !m_erase_mode, icon_sm, _L("Paint"),
|
||||
_L("Paint - add the active layer where you click or drag. The right button erases, and so does "
|
||||
"holding Shift.")))
|
||||
"holding Shift."),
|
||||
{ { "paint.gif" } }))
|
||||
m_erase_mode = false;
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(821, "texture_displacement_add_negative.svg", m_erase_mode, icon_sm, _L("Erase"),
|
||||
_L("Erase - remove the active layer where you click or drag. The right button paints.")))
|
||||
_L("Erase - remove the active layer where you click or drag. The right button paints."),
|
||||
{ { "erase.gif" } }))
|
||||
m_erase_mode = true;
|
||||
}
|
||||
|
||||
@@ -6233,20 +6243,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const bool is_area_mode = m_tool_type == ToolType::SMART_FILL;
|
||||
const float row_y = ImGui::GetCursorPosY();
|
||||
if (icon_toggle(801, "texture_displacement_brush.svg", is_brush_mode, icon_md, _L("Brush"),
|
||||
_L("Brush - paint over the surface by dragging"))) {
|
||||
_L("Brush - paint over the surface by dragging"), { { "paint.gif" } })) {
|
||||
m_tool_type = ToolType::BRUSH;
|
||||
if (m_cursor_type == TriangleSelector::CursorType::POINTER)
|
||||
m_cursor_type = TriangleSelector::CursorType::CIRCLE;
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(802, "texture_displacement_face.svg", is_face_mode, icon_md, _L("Face"), _L("Face - click individual triangles"))) {
|
||||
if (icon_toggle(802, "texture_displacement_face.svg", is_face_mode, icon_md, _L("Face"), _L("Face - click individual triangles"),
|
||||
{ { "tool_face.gif" } })) {
|
||||
m_tool_type = ToolType::BRUSH;
|
||||
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(803, "texture_displacement_connected_area.svg", is_area_mode, icon_md, _L("Connected area"),
|
||||
_L("Connected area - flood-fill the region reachable without crossing an edge sharper than the "
|
||||
"angle threshold"))) {
|
||||
"angle threshold"),
|
||||
{ { "tool_smart_fill.gif" } })) {
|
||||
m_tool_type = ToolType::SMART_FILL;
|
||||
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
||||
}
|
||||
@@ -6260,11 +6272,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
wxString();
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(806, "texture_displacement_select_all.svg", false, icon_md, _L("Select whole model"),
|
||||
_L("Select whole model - paint every face of the model with the active layer"), whole_na))
|
||||
_L("Select whole model - paint every face of the model with the active layer"),
|
||||
{ { "select_all.png" } }, whole_na))
|
||||
select_whole_model();
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(807, "texture_displacement_erase_all.svg", false, icon_md, _L("Erase whole model"),
|
||||
_L("Erase whole model - clear the active layer's paint from every face"), erase_na)) {
|
||||
_L("Erase whole model - clear the active layer's paint from every face"),
|
||||
{ { "erase_all.png" } }, erase_na)) {
|
||||
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reset texture displacement selection"),
|
||||
UndoRedo::SnapshotType::GizmoAction);
|
||||
int idx = -1;
|
||||
@@ -6287,7 +6301,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
centre_on_row(frame_h);
|
||||
ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm",
|
||||
ImGuiSliderFlags_AlwaysClamp);
|
||||
hover_tip(m_desc.at("cursor_size"));
|
||||
hover_tip(m_desc.at("cursor_size"), { { "cursor_size.gif" } });
|
||||
vsep(icon_sm);
|
||||
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
|
||||
centre_on_row(icon_sm);
|
||||
@@ -6304,7 +6318,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
centre_on_row(frame_h);
|
||||
ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°",
|
||||
ImGuiSliderFlags_AlwaysClamp);
|
||||
hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this"));
|
||||
hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this"),
|
||||
{ { "smart_fill_angle_low.png", _L("Low") }, { "smart_fill_angle_high.png", _L("High") } });
|
||||
} else {
|
||||
centre_on_row(frame_h);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -6341,19 +6356,21 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f)));
|
||||
if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"),
|
||||
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update")))
|
||||
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update"),
|
||||
{ { "tile_on.png" } }))
|
||||
new_mode = 0;
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"),
|
||||
_L("Fast - the relief is only shaded on, not built, and only for the layer you are "
|
||||
"editing. Updates instantly while you paint")))
|
||||
"editing. Updates instantly while you paint"),
|
||||
{ { "view_fast.png" } }))
|
||||
new_mode = 1;
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
|
||||
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
|
||||
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
{ { "view_checker.png" } }, uv_view_na)) {
|
||||
new_mode = 2;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
@@ -6361,13 +6378,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"),
|
||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
{ { "view_distortion.png" } }, uv_view_na)) {
|
||||
new_mode = 3;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
vsep(icon_md);
|
||||
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"),
|
||||
_L("Wireframe - overlay the mesh edges; independent of the view above")))
|
||||
_L("Wireframe - overlay the mesh edges; independent of the view above"),
|
||||
{ { "view_wireframe.png" } }))
|
||||
wf_toggle = true;
|
||||
|
||||
const std::string auto_label = _u8L("Auto");
|
||||
@@ -6671,7 +6689,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Interleaves two filaments to fake the colors in between, so a handful "
|
||||
"of filaments can cover a photo or a gradient. An image of flat colors "
|
||||
"prints the same either way. Off uses one filament per area."));
|
||||
"prints the same either way. Off uses one filament per area."),
|
||||
{ { "mix_off.png", _L("Off") }, { "mix_on.png", _L("On") } });
|
||||
if (opts.color_mix_enabled) {
|
||||
cached_palette(); // brings m_palette_filaments up to date
|
||||
// Every mix can become a filament slot, and there are only so many beside the
|
||||
@@ -6680,7 +6699,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
|
||||
"texture's colors, and only the ones the bake actually uses are created."));
|
||||
"texture's colors, and only the ones the bake actually uses are created."),
|
||||
{ { "mixed_colors_few.png", _L("Few") }, { "mixed_colors_many.png", _L("Many") } });
|
||||
// After the slider, so a change shows in the same frame.
|
||||
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
|
||||
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
|
||||
@@ -6689,7 +6709,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Cleans up single stray triangles of the wrong color, which detail finer "
|
||||
"than the mesh leaves behind. Raise it if the result looks speckled, "
|
||||
"lower it if small features are being swallowed."));
|
||||
"lower it if small features are being swallowed."),
|
||||
{ { "denoise_off.png", _L("Off") }, { "denoise_on.png", _L("On") } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6705,29 +6726,35 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const char *file;
|
||||
wxString label;
|
||||
wxString tip;
|
||||
const char *picture; // the same ball with the checker laid on it this way
|
||||
};
|
||||
const MappingIcon mappings[] = {
|
||||
{ "menu_obj_cube.svg", _L("Triplanar (blended)"),
|
||||
_L("Triplanar - projects the texture from all three axes at once and blends between them, so a "
|
||||
"patch wrapping around a sharp edge has no seam.") },
|
||||
"patch wrapping around a sharp edge has no seam."),
|
||||
"map_cube.png" },
|
||||
{ "menu_obj_cylinder.svg", _L("Cylindrical"),
|
||||
_L("Cylindrical - wraps the texture around the painted area's own center, for round shapes.") },
|
||||
_L("Cylindrical - wraps the texture around the painted area's own center, for round shapes."),
|
||||
"map_cylinder.png" },
|
||||
{ "menu_obj_sphere.svg", _L("Spherical"),
|
||||
_L("Spherical - wraps the texture around the painted area's own center in both directions.") },
|
||||
_L("Spherical - wraps the texture around the painted area's own center in both directions."),
|
||||
"map_sphere.png" },
|
||||
{ "texture_displacement_map_unwrap.svg", _L("Unwrap (LSCM)"),
|
||||
_L("Unwrap - flattens the painted area and maps the texture onto it with as little stretching as "
|
||||
"possible. The area is cut into pieces at its sharp edges first (see Seam angle), so each "
|
||||
"piece can lie flat on its own.") },
|
||||
"piece can lie flat on its own."),
|
||||
"map_unwrap.png" },
|
||||
{ "texture_displacement_map_view.svg", _L("From view"),
|
||||
_L("From view - projects straight onto the painted area from where you are looking, like a slide "
|
||||
"projector.") },
|
||||
"projector."),
|
||||
"map_view.png" },
|
||||
};
|
||||
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
|
||||
if (mi > 0)
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
const auto method = static_cast<TextureProjectionMethod>(mi);
|
||||
if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label,
|
||||
mappings[mi].tip) &&
|
||||
mappings[mi].tip, { { mappings[mi].picture } }) &&
|
||||
layer.projection_method != method) {
|
||||
// Capture the current view the moment "From view" is chosen, so it does something
|
||||
// sensible immediately rather than projecting from a stale/default direction.
|
||||
@@ -6837,14 +6864,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
const wxString tile_na = layer.tile_enabled ? wxString() : _L("Turn Tile on to choose how the texture repeats.");
|
||||
if (icon_toggle(840, "texture_displacement_tile_repeat.svg", layer.tile_method == static_cast<TextureTileMethod>(0),
|
||||
icon_sm, _L("Repeat"), _L("Repeat"), tile_na, { { "tile_repeat.png" } })) {
|
||||
icon_sm, _L("Repeat"), _L("Repeat"), { { "tile_repeat.png" } }, tile_na)) {
|
||||
layer.tile_method = static_cast<TextureTileMethod>(0);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(841, "menu_mirror_x.svg", layer.tile_method == static_cast<TextureTileMethod>(1), icon_sm,
|
||||
_L("Mirrored repeat"), _L("Mirrored repeat - every other tile is flipped, so edges meet seamlessly"),
|
||||
tile_na, { { "tile_mirror.png" } })) {
|
||||
{ { "tile_mirror.png" } }, tile_na)) {
|
||||
layer.tile_method = static_cast<TextureTileMethod>(1);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
@@ -7066,7 +7093,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
hover_tip(_u8L("Adds triangles only where you painted, instead of everywhere. On a large part with a small "
|
||||
"decal that is the difference between thousands of triangles and millions, and your paint "
|
||||
"survives the refinement."));
|
||||
"survives the refinement."),
|
||||
{ { "painted_only_off.png", _L("Off") }, { "painted_only_on.png", _L("On") } });
|
||||
|
||||
if (m_subdivide_adaptive) {
|
||||
if (m_subdivide_target_mm <= 0.f && mv != nullptr) {
|
||||
@@ -7094,7 +7122,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
preview_live();
|
||||
hover_tip(_u8L("Spends the triangles where the texture actually bends - packed along ridges and edges, "
|
||||
"sparse over flat ground - instead of spreading them evenly. The same detail for fewer "
|
||||
"triangles on most textures."));
|
||||
"triangles on most textures."),
|
||||
{ { "adaptive_off.png", _L("Off") }, { "adaptive_on.png", _L("On") } });
|
||||
|
||||
// The edge-length target is a baseline in both modes. In feature mode it is what guarantees the
|
||||
// curvature test can actually see the texture: left too coarse, a big triangle over a fine
|
||||
@@ -7151,7 +7180,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
hover_tip(_u8L("Splits every triangle of the model into four, this many times over. Each step "
|
||||
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
|
||||
"only the painted area unless you need the whole model finer."));
|
||||
"only the painted area unless you need the whole model finer."),
|
||||
{ { "subdivide.gif" } });
|
||||
}
|
||||
|
||||
{
|
||||
@@ -7215,12 +7245,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f);
|
||||
hover_tip(_u8L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
|
||||
"even mesh; this is what makes one out of an uneven import."));
|
||||
"even mesh; this is what makes one out of an uneven import."),
|
||||
{ { "remesh_before.png", _L("Before") }, { "remesh_after.png", _L("After") } });
|
||||
|
||||
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
|
||||
hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it "
|
||||
"the remesher slides vertices along the surface and rounds every crisp edge off - "
|
||||
"a cube comes back with wobbly edges."));
|
||||
"a cube comes back with wobbly edges."),
|
||||
{ { "preserve_edges_off.png", _L("Off") }, { "preserve_edges_on.png", _L("On") } });
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
|
||||
ImGui::SetNextItemWidth(-1.f);
|
||||
@@ -7236,7 +7268,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(_u8L("Rebuilds the whole model with triangles close to this edge length - splitting the big "
|
||||
"ones and merging the small ones - so displacement has an even density to work with. "
|
||||
"Replaces the geometry; your paint is carried onto the new triangles spatially, so it "
|
||||
"survives (already-baked relief is kept too)."));
|
||||
"survives (already-baked relief is kept too)."),
|
||||
{ { "remesh_before.png", _L("Before") }, { "remesh_after.png", _L("After") } });
|
||||
|
||||
// Settings for the whole stack rather than one layer. Standard mode pins them instead of showing them.
|
||||
if (mv != nullptr) {
|
||||
@@ -7252,7 +7285,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Smooth result"), opts.smooth_enabled);
|
||||
hover_tip(_u8L("Smooths the geometry after the texture has been applied, to take the hard steps out of a "
|
||||
"low-resolution image. Only what the displacement moved is touched. The Smoothing slider "
|
||||
"on a layer is a different thing: it blurs the image before it is used."));
|
||||
"on a layer is a different thing: it blurs the image before it is used."),
|
||||
{ { "geometry_smoothing.gif" } });
|
||||
if (opts.smooth_enabled) {
|
||||
float percent = opts.smooth_strength * 100.f;
|
||||
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) {
|
||||
@@ -7316,7 +7350,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"Relief already baked into the rest of the model is kept on top of it, so a "
|
||||
"second bake somewhere else gets the same budget as the first. Raise it if the "
|
||||
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
|
||||
"the refinement produced, however many that is."));
|
||||
"the refinement produced, however many that is."),
|
||||
{ { "subdivide.gif" } });
|
||||
}
|
||||
if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) {
|
||||
// Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines
|
||||
@@ -7331,7 +7366,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(_u8L("Collapse the thin triangles refinement inherits from the model's own "
|
||||
"tessellation, before displacement samples them. A sliver's three corners "
|
||||
"land on three unrelated parts of the texture, which is what makes the "
|
||||
"relief look jagged."));
|
||||
"relief look jagged."),
|
||||
{ { "collapse_slivers_off.png", _L("Off") }, { "collapse_slivers_on.png", _L("On") } });
|
||||
}
|
||||
// Below both pipelines' own settings, because it replays whichever of them is selected.
|
||||
if (SHOW_PIPELINE_DEV_CONTROLS)
|
||||
@@ -7442,7 +7478,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
|
||||
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
|
||||
"four times as many, and once they no longer fit the budget the bake simplifies "
|
||||
"back down and the detail goes with it."));
|
||||
"back down and the detail goes with it."),
|
||||
{ { "subdivide.gif" } });
|
||||
|
||||
// What this resolution costs over what is painted, against what the budget allows. Refining
|
||||
// past the budget is not an error - the bake simplifies back down to it - but the result
|
||||
@@ -7485,7 +7522,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->disabled_end();
|
||||
hover_tip(_u8L("How many thousand triangles the refinement may add to the model. The triangle "
|
||||
"that fits worst is always split first, so even a run that spends the lot has "
|
||||
"spent it where it shows most. Raise it if the relief still looks coarse."));
|
||||
"spent it where it shows most. Raise it if the relief still looks coarse."),
|
||||
{ { "subdivide.gif" } });
|
||||
}
|
||||
|
||||
const float button_h = std::round(frame_h * 1.25f);
|
||||
|
||||