Print view · Examples · 1 pageChoose "Save as PDF" as the destination in the print dialog.
Back to the docs

Mesh Morpher documentation

Examples

1 page · exported 2026-09-06 · https://meshmorpher.com/docs/studio-and-graph/geometry/wrangle/examples

Studio and Graph › Geometry › Wrangle

Examples

Every recipe the Wrangle node's Examples menu can drop into the Code field, with the note each one ships with. Pick the closest one, paste it, and edit the numbers.

Inserting an example from the menu also declares the parameters it needs, so a freshly inserted snippet compiles as-is. Pasting one from this page does not - declare the parameters it names (Mask, Reference, Iterations) in the pin editor first.

Expression examples

These are the expression half: paste one into the Code field as it is.

Deform

Inflate Along Normal (Masked)

Push vertices outward along their normal, scaled by the Mask input. Run Over: Vertex. Insert already declared the classic Mask; wire a weight map into the Mask parameter.

float amount = 2.0 * mask;
P += normalize(N) * amount;

Smooth Only The Masked Region

Blend a whole-mesh relax smooth against the original position, gated by the Mask input. Run Over: Vertex. Insert already declared the classic Mask; wire a weight map into the Mask parameter.

vector smoothed = relax(4);
P = lerp(P, smoothed, mask);

Iterative Smooth (Iterations)

Average each vertex toward its one-ring neighbours - a single pass only nudges the mesh once. Run Over: Vertex. Insert already set Iterations to 5 on this node, so this pass actually runs five times, each one reading the previous pass's result - that is what turns the one-step average below into a real smooth. Compare to relax(n), which is a pre-pass that runs once, before your code, not a repeat of your code itself.

Insert also sets Iterations.

vector sum = vec3(0, 0, 0);
float count = 0;
foreach (nb in neighbours(ptnum)) { sum += pointpos(nb); count += 1; }
if (count > 0) { P = sum / count; }

Noise Displacement (Seeded)

Push each vertex by a seeded 3D noise field - change the seed for a different look without touching anything else. Run Over: Vertex.

P += vnoise(P * 0.05, 7) * 2;

Taper Along Z

Taper the mesh toward a point along Z: full width at the base, narrowed at the top. Run Over: Vertex.

float t = fit(P.z, bboxmin().z, bboxmax().z, 1, 0.3);
P.x *= t;
P.y *= t;

Distance-To-Reference Falloff

Fade toward 1 near the Reference input's surface and toward 0 far from it. Run Over: Vertex. Wire the classic Reference parameter. If you already renamed this node's Weight Map output, write wmap("YourName") = ... instead of "Weights".

float d = neardist("Reference", P);
wmap("Weights") = 1 - saturate(d / 25);

Project Onto Reference Surface

Pull vertices onto the nearest point on the mesh wired into the Reference parameter, blended by the Mask input. Run Over: Vertex. Wire the classic Reference parameter and a weight map into the Mask parameter.

vector target = nearpoint("Reference", P);
P = lerp(P, target, mask);

Blend Toward Index-Matched Reference

Blend toward the mesh wired into the Reference parameter when it shares this mesh's exact vertex order (a blend-shape target). Run Over: Vertex. Wire the classic Reference parameter. Needs a Float Parameter named "blend".

vector donorPos = pointpos("Reference", ptnum);
P = lerp(P, donorPos, ch("blend"));

Attributes

Transfer Attribute From A Second Mesh (Named Pin)

Sample a tagged attribute at the nearest surface point on a second mesh - beyond the Reference parameter - and paint it as greyscale colour. Run Over: Vertex. Needs a Parameter of type Mesh named "Donor", tagged with a Float attribute called "thickness".

float thickness = surfattr("Donor", "thickness", P);
Cd = vec3(thickness, thickness, thickness);

Skinning

Visualize Bone Weight Glob

Visualize the combined skin weight of every bone whose name matches a glob, as greyscale vertex colour. Run Over: Vertex. The subject mesh needs a skeleton with bone names matching thigh_*.

float w = weight("thigh_*");
Cd = vec3(w, w, w);

Harden A Skin Weight

Push a bone's influence to full strength wherever it already dominates; setboneweight renormalizes on apply. Run Over: Vertex. The subject mesh needs a skeleton with a bone named spine_02. Needs a Float Parameter named "threshold".

if (weight("spine_02") > ch("threshold")) { setboneweight("spine_02", 1); }

Color & UV

UV Band Mask

Repeating stripes across U, written to the node's Weights output as a paintable mask. Run Over: Vertex. If you already renamed this node's Weight Map output, write wmap("YourName") = ... instead of "Weights".

wmap("Weights") = sin(uv.x * pi * 8) * 0.5 + 0.5;

Vertex Colour From Weight Map

Paint a WeightMap parameter's values as greyscale vertex colour - handy for sanity-checking a mask. Run Over: Vertex. Needs a Parameter of type Weight Map named "Falloff" wired.

float w = wmap("Falloff");
Cd = vec3(w, w, w);

Selection

Boundary Ring Detect

Colour boundary vertices red and interior vertices white, to spot open edges before a boolean or a bake. Run Over: Vertex.

if (isboundary()) { Cd = vec3(1, 0, 0); } else { Cd = vec3(1, 1, 1); }

Detail Mode

Center Mesh On Origin

Recenter the whole mesh on the origin by subtracting its centroid from every vertex. Run Over: Once (Detail). Insert already declared the classic Mesh; wire a mesh into it.

Insert also sets the Run Mode the snippet needs.

vector c = centroid();
foreach (i in points()) { setpointpos(i, pointpos(i) - c); }

Triangle Mode

Area-Based Mask

Flag small triangles for cleanup by writing a 0..1 mask attribute from triangle area. Run Over: Triangle. Needs Create Missing Attributes on to let @areaMask be created.

Insert also sets the Domain the snippet needs.

@areaMask = fit(area, 0, 50, 0, 1);

Python examples

These are the Python half. A snippet that already opens with #@python is a whole document - paste it as it is; the rest are script bodies, so put them inside a #@python block.

Mixed

Mixed: Python feeds the expression

#@python
#@requires numpy
# Compute the bounding-box centre and longest axis once, in Python,
# then let the expression below use them per vertex.
if mm.num_points == 0:
    mm.note("add a Mesh parameter first")
    mm.set_detail("centre", [0.0, 0.0, 0.0])
    mm.set_detail("longest", 0.0)
else:
    import numpy as np
    p = np.frombuffer(mm.P, dtype=np.float64).reshape(-1, 3)
    lo, hi = p.min(axis=0), p.max(axis=0)
    mm.set_detail("centre", ((lo + hi) * 0.5).tolist())
    mm.set_detail("longest", float((hi - lo).max()))
#@wrangle
// Push every vertex away from the centre, scaled by the mesh's own size.
vector c = pyv("centre");
float s = py("longest");
P += normalize(P - c) * (s * 0.02);

Interactive

Tkinter Slider + Apply

Insert also declares amount (Float) and ticks Interactive Script, switching this node to an isolated interpreter.

#@python
# Tkinter Slider + Apply: a script that opens ITS OWN window and waits for the
# artist to move a slider and press Apply.
# REQUIRES: Interactive Script and an isolated interpreter (this example ticks
# both on insert). In-process is refused outright (a GUI event loop would hang
# the whole app unkillably).
import tkinter as tk

amount = [float(mm.params().get("amount", 0.0))]
root = tk.Tk()
root.title("Wrangle: Push Along Normal")
# The child spawns with a HIDDEN startup window (SW_HIDE-style startup info) -
# these three lines make that moot, forcing the window to the front the moment
# it exists.
root.deiconify()
root.lift()
root.attributes("-topmost", True)

def on_apply():
    amount[0] = slider.get()
    root.destroy()

slider = tk.Scale(root, from_=-10.0, to=10.0, resolution=0.1, orient=tk.HORIZONTAL, label="Amount")
slider.set(amount[0])
slider.pack(padx=12, pady=12)
tk.Button(root, text="Apply", command=on_apply).pack(pady=(0, 12))

def heartbeat():
    # A progress heartbeat, not a cancel check - Cancel is a hard kill in
    # isolated mode, handled entirely on the C++ side; this call only keeps
    # the dialog's own progress label alive while the window is open.
    mm.progress(0.5, "waiting for the window...")
    root.after(200, heartbeat)

root.after(200, heartbeat)
root.mainloop()

mm.set_detail("amount", amount[0])
mm.note(f"amount = {amount[0]:.2f}")
#@wrangle
// Push along the normal by the slider amount the Python window wrote.
P += normalize(N) * py("amount");

Tkinter Hosted UI

Insert also declares amount (Float) and ticks Interactive Script, switching this node to an isolated interpreter.

#@python
# Tkinter hosted by mm.ui.host(where='panel') - stdlib only.
# REQUIRES: an isolated interpreter. mm.ui.host implies Interactive Script
# and isolation even if those tickboxes are off (this example still ticks both).
# where='panel' embeds above the code editor (Windows). where='window' floats.
# tk.Tk() auto-attaches after host(where='panel'); mm.ui.attach(root) also works.
import tkinter as tk

amount = [float(mm.params().get("amount", 0.0))]
mm.ui.host(where='panel', height=220)
root = tk.Tk()
root.title("Wrangle: Push Along Normal")

def on_apply():
    amount[0] = slider.get()
    root.destroy()

slider = tk.Scale(root, from_=-10.0, to=10.0, resolution=0.1, orient=tk.HORIZONTAL, label="Amount")
slider.set(amount[0])
slider.pack(fill='both', expand=True, padx=8, pady=8)
tk.Button(root, text="Apply", command=on_apply).pack(pady=(0, 8))
root.mainloop()

mm.set_detail("amount", amount[0])
mm.note(f"amount = {amount[0]:.2f}")
#@wrangle
// Push along the normal by the slider amount the Python window wrote.
P += normalize(N) * py("amount");

Python Sculpt Lab

Insert also ticks Interactive Script, switching this node to an isolated interpreter.

#@python
#@requires numpy
# Python Sculpt Lab: a window with its OWN interactive 3D viewport and
# custom brushes that are NOT in the core Mesh Morpher toolset
# (Snake Hook, Crease, Blob, Nudge, Vortex, Polish, Magnet, Wrinkle).
# REQUIRES: Interactive Script and an isolated interpreter (this example
# ticks both on insert). LMB sculpts; Alt-LMB orbits; Apply writes mm.set_P().
import os
import runpy

def _sculpt_lab_candidates():
    out = []
    try:
        root = os.path.normpath(os.path.join(mm.cache_dir, '..', '..', '..'))
        out.append(os.path.join(root, 'Tools', 'python', '_sculpt_lab.py'))
        out.append(os.path.join(root, 'Plugins', 'MeshMorpherGraph', 'Resources', 'Python', 'sculpt_lab.py'))
    except Exception:
        pass
    out.append(os.path.abspath('_sculpt_lab.py'))
    return out

path = None
for c in _sculpt_lab_candidates():
    if c and os.path.isfile(c):
        path = c
        break
if not path:
    raise mm.Error('Python Sculpt Lab not found (Tools/python/_sculpt_lab.py)')
mm.note('sculpt lab: ' + path)
runpy.run_path(path, init_globals={'mm': mm}, run_name='__sculpt_lab__')
#@wrangle

No Dependencies

Height Ramp

Height Ramp: normalize Z height into a 0..1 vertex attribute. mm.P is (N,3) when numpy is importable, else a flat float64 view.

p = mm.P
n = mm.num_points
def height(i):
    return float(p[i][2] if getattr(p, "ndim", 1) == 2 else p[i * 3 + 2])
buf = bytearray(n * 8)
view = memoryview(buf).cast('d')
zmin = min(height(i) for i in range(n)) if n else 0.0
zmax = max(height(i) for i in range(n)) if n else 0.0
span = (zmax - zmin) or 1.0
for i in range(n):
    view[i] = (height(i) - zmin) / span
mm.set_attrib("heightRamp", buf, "vertex", "float")
mm.note(f"height range {zmin:.1f} .. {zmax:.1f}")

CSV Import

CSV Import: read one value per vertex from a CSV file in this node's cache folder.

import csv
import os
path = os.path.join(mm.cache_dir, "values.csv")
n = mm.num_points
if not os.path.exists(path):
    mm.note(f"no values.csv in {mm.cache_dir} - using 0 everywhere")
    values = [0.0] * n
else:
    with open(path, newline='') as f:
        values = [float(row[0]) for row in csv.reader(f) if row]
    values = (values + [0.0] * n)[:n]
buf = bytearray(n * 8)
view = memoryview(buf).cast('d')
for i, v in enumerate(values):
    view[i] = v
mm.set_attrib("csvValue", buf, "vertex", "float")

Fetch Once

Fetch Once: download a reference file once and reuse it. The script itself runs every Run, but the os.path.exists guard below skips the actual download whenever the file is already sitting in mm.cache_dir, so a re-run costs almost nothing once it has fetched the file the first time.

Insert also leaves the Mesh parameter undeclared - the snippet does not need one.

import os
import urllib.request
path = os.path.join(mm.cache_dir, "reference.json")
if not os.path.exists(path):
    mm.note("downloading reference.json (first Run only)")
    urllib.request.urlretrieve("https://example.com/reference.json", path)
else:
    mm.note("using cached reference.json")
mm.set_detail("fetched_path", path)

Ramp + Transform

Ramp + Transform: sample a Ramp parameter named "Falloff" by height, then apply a Transform parameter named "Offset" to the result. Needs a Ramp parameter "Falloff" and a Transform parameter "Offset" declared on this node. mm.P is (N,3) when numpy is importable, else a flat float64 view.

p = mm.P
n = mm.num_points
ramp = mm.param("Falloff")
xf = mm.param("Offset")
tmp = memoryview(bytearray(n * 3 * 8)).cast('d')
def xyz(i):
    if getattr(p, "ndim", 1) == 2:
        return float(p[i][0]), float(p[i][1]), float(p[i][2])
    return float(p[i * 3]), float(p[i * 3 + 1]), float(p[i * 3 + 2])
for i in range(n):
    x, y, z = xyz(i)
    t = ramp(max(0.0, min(1.0, z / 100.0)))
    tmp[i * 3] = x
    tmp[i * 3 + 1] = y
    tmp[i * 3 + 2] = z + t * 5.0
mm.set_P(xf.transform_points(tmp))

Python-only Node

Python-only Node: this document is nothing but a #@python block - there is no #@wrangle section at all, so there is no expression to compile; this script alone is the whole node. Legal exactly the way an empty document (no python either) is. mm.P is (N,3) when numpy is importable, else a flat float64 view.

p = mm.P
n = mm.num_points
def x_of(i):
    return float(p[i][0] if getattr(p, "ndim", 1) == 2 else p[i * 3])
buf = bytearray(n * 8)
view = memoryview(buf).cast('d')
cx = (sum(x_of(i) for i in range(n)) / n) if n else 0.0
for i in range(n):
    view[i] = abs(x_of(i) - cx)
mm.set_attrib("distFromCenterX", buf, "vertex", "float")
mm.note("Python-only: no expression needed")

Button: Bake To Attribute

Button: Bake To Attribute. Declares a button every run (mm.ui.button); an ordinary Run press only shows a preview count, but pressing the button itself (mm.event == "button:Bake") writes the count into a real attribute.

mm.ui.button("Bake", "Write the vertex count into @baked")
n = mm.num_points
if mm.event == "button:Bake":
    buf = bytearray(n * 8)
    view = memoryview(buf).cast('d')
    for i in range(n):
        view[i] = float(i)
    mm.set_attrib("baked", buf, "vertex", "float")
    mm.note(f"baked {n} value(s)")
else:
    mm.note(f"press Bake to write {n} value(s) into @baked")

Needs A Package

Curvature Attribute (numpy)

#@requires numpy
# Curvature Attribute (numpy): mean curvature proxy from vertex-normal disagreement
# across every edge. The '#@requires numpy' line above installs it into the managed
# folder automatically on next Run.
if mm.num_points == 0:
    mm.note("add a Mesh parameter first")
else:
    import numpy as np
    n_pts = mm.num_points
    p = np.frombuffer(mm.P, dtype=np.float64).reshape(-1, 3)
    nrm = np.frombuffer(mm.N, dtype=np.float32).reshape(-1, 3).astype(np.float64)
    tris = np.frombuffer(mm.tris, dtype=np.int32).reshape(-1, 3)
    accum = np.zeros(n_pts)
    counts = np.zeros(n_pts)
    for a, b, c in tris:
        for i, j in ((a, b), (b, c), (c, a)):
            d = 1.0 - float(np.dot(nrm[i], nrm[j]))
            accum[i] += d; accum[j] += d
            counts[i] += 1; counts[j] += 1
    curv = np.divide(accum, counts, out=np.zeros_like(accum), where=counts > 0)
    mm.set_attrib("curvature", curv.astype(np.float64).tobytes(), "vertex", "float")

KD-Tree Donor Distance (scipy)

Insert also declares Donor (Mesh) and turns Create Missing Attributes on.

#@python
#@requires scipy
# KD-Tree Donor Distance (scipy): distance from each vertex to the nearest point on a
# donor Mesh parameter named "Donor". The '#@requires scipy' line above installs it
# into the managed folder automatically on next Run. Needs a Parameter of type Mesh
# named "Donor" wired.
if mm.num_points == 0:
    mm.note("add a Mesh parameter first")
    mm.set_detail("gap_max", 0.0)
else:
    from scipy.spatial import cKDTree
    import numpy as np
    donor = mm.param("Donor")
    donor_p = np.frombuffer(donor.P, dtype=np.float64).reshape(-1, 3)
    p = np.frombuffer(mm.P, dtype=np.float64).reshape(-1, 3)
    tree = cKDTree(donor_p)
    dist, _ = tree.query(p)
    mm.set_attrib("gap", dist.astype(np.float64).tobytes(), "vertex", "float")
    mm.set_detail("gap_max", float(dist.max()) if len(dist) else 0.0)
#@wrangle
// Distance to the Donor mesh as a 0..1 weight (1 = on the donor). If you already renamed this node's Weight Map output, write wmap("YourName") = ... instead of "Weights".
wmap("Weights") = 1 - saturate(@gap / py("gap_max"));