(guide-surface-relief)=
# Physical surface relief
Physical surface relief turns a grayscale image or a Python-defined pattern into raised,
printable geometry on a selected part of a model. You choose **where** the pattern goes, **what**
controls its height, and **how tall** it should be. OpenVCAD creates a real relief volume that can
be rendered, meshed, voxelized, simulated, and exported with the rest of the part.
This is different from a visual bump map: the outside shape of the part actually changes.
All dimensions in this guide are in millimetres.
## What you need
Every relief design has four inputs:
| Input | What you provide |
| --- | --- |
| Base solid | The CAD model or closed triangle mesh that will receive the texture |
| Selected surface | One `CADFace` or one connected, open `TriangleMeshSurface` patch |
| Height pattern | A grayscale PNG or a `FloatAttribute` function |
| Relief settings | Maximum height, embed depth, and optional edge falloff |
The complete workflow is short:
```python
# 1. Select one surface from the base model.
surface = ...
# 2. Define the height pattern.
height = pv.ImageHeightField(
"pattern.png",
amplitude_mm=1.0,
)
# 3. Turn that pattern into physical relief.
relief = pv.SurfaceReliefVolume(
surface,
height,
embed_mm=0.3,
edge_falloff_mm=1.0,
)
# 4. Optionally join the relief to the original part.
root = pv.BBoxUnion([solid, relief])
```
`relief` is already a geometry node. You can render or export it by itself when you only want the
added surface feature:
```python
root = relief
viz.Render(root)
```
Unioning is optional; it simply attaches that relief volume to the original part.
`embed_mm` extends the relief slightly into the base part so the two volumes overlap and become
one printable solid. `edge_falloff_mm` is optional: it lowers the relief smoothly near the selected
surface boundary instead of ending at full height.
## Select the surface to decorate
CAD and triangle meshes use the same relief workflow, but you select their surfaces differently.
### Select a CAD face
Load the CAD model once, choose a face from it, and convert that same model into the base solid:
```python
model = pv.CADModel.from_step("examples/data/3d_models/bracket.step")
face = model.face(20)
solid = model.to_node(use_fast_mode=True)
```
`model.face(index)` is useful when the input file is fixed and you know the face index. You can
also find faces by geometric properties:
```python
upward_faces = model.select_faces(
normal=pv.Vec3(0, 0, 1),
min_dot=0.95,
)
face = max(upward_faces, key=lambda candidate: candidate.area)
```
Available filters include face index, surface type, normal direction, area range, and
bounding-box overlap. For CAD authored in Python, first convert the CadQuery body with
`pv.CADModel.from_cadquery(...)`, then use the same `face()` or `select_faces()` methods.
The complete bracket example is
[`examples/geometry/surface_relief/cad_face_png_relief.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/examples/geometry/surface_relief/cad_face_png_relief.py).
### Select triangles from a mesh
For a closed triangle mesh, pass the IDs of the triangles that make up the surface patch:
```python
source_mesh = pv.SurfaceMesh("examples/data/3d_models/domed_tile.stl")
triangle_ids = list(range(450))
patch = pv.TriangleMeshSurface.from_selection(
source_mesh,
triangle_ids,
u_axis_hint=pv.Vec3(1, 0, 0),
)
solid = pv.Mesh(source_mesh, override_voxel_size=0.2)
```
The selected triangles must form one connected, open patch. Triangle IDs can come from a mesh
editor, a stored face group, or a Python selection rule. The
{ref}`triangle-mesh conformal mapping guide ` shows how to
select a region by triangle direction and connected component.
`u_axis_hint` is optional. Use it when the left-to-right direction of the pattern matters. Triangle
IDs follow the source file's triangle order, so update a stored selection if the mesh is
retriangulated.
You can also create a `TriangleMeshSurface` from an already-open patch file or directly from
vertices and indexed triangles. See
[`examples/geometry/surface_relief/selected_mesh_png_relief.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/examples/geometry/surface_relief/selected_mesh_png_relief.py)
for the complete imported-mesh example.
## Choose the height pattern
The height pattern supplies values from 0 to 1 across the selected surface. OpenVCAD multiplies
those values by `amplitude_mm`, so 0 stays on the original surface and 1 reaches the maximum
relief height.
### Use a PNG
`ImageHeightField` loads a grayscale or RGB PNG:
```python
height = pv.ImageHeightField(
"examples/data/height_maps/dotted_checker.png",
amplitude_mm=1.2,
channel="luminance",
mapping="repeat",
repeats=(5, 5),
)
```
The image controls are:
| Setting | Meaning |
| --- | --- |
| `path` | PNG file to map onto the selected surface |
| `amplitude_mm` | Height of a white pixel; black is 0 mm |
| `channel` | `"luminance"` by default, or `"red"`, `"green"`, or `"blue"` |
| `mapping="fit"` | Place one copy across the whole selected surface |
| `mapping="repeat"` | Tile the image using the integer `(u, v)` counts in `repeats` |
Image `(u, v) = (0, 0)` maps from the lower-left corner. If a pattern appears rotated relative to
the intended part direction, adjust the mesh `u_axis_hint` or rotate the source image.
Use `fit` for a logo, label, or one full-surface texture. Use `repeat` when a small pattern should
tile across a larger region. On a periodic CAD surface such as a cylinder, `fit` wraps one image
around the full surface; use an image whose left and right edges match when you want an invisible
seam.
### Use a function
`FunctionalHeightField` creates the pattern directly from normalized surface coordinates. In the
expression, `x` is the surface's left-to-right coordinate and `y` is its bottom-to-top coordinate:
```python
waves = pv.FunctionalHeightField(
pv.FloatAttribute(
"0.5 + 0.25*sin(10*pi*x) + 0.25*sin(8*pi*y)"
),
amplitude_mm=1.4,
)
```
The function result is limited to the range 0 to 1, then scaled by `amplitude_mm`. This is useful
for waves, ribs, gradients, procedural textures, and other patterns that should remain editable
through a few parameters rather than an image file.
The middle image is the geometry created by `SurfaceReliefVolume`: the raised top, its selected
outline, and the thin embedded portion that can overlap another solid. The last image uses
`pv.BBoxUnion([solid, relief])` to attach exactly that volume to the tile.
Run
[`examples/geometry/surface_relief/functional_wave_relief.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/examples/geometry/surface_relief/functional_wave_relief.py)
to edit the expression and amplitude. It renders `relief` by itself; change `root` to
`unioned_root` to preview the attached part instead.
## Wrap a continuous texture around a curved face
The cylindrical example combines the same ideas: CadQuery authors the base cylinder, a CAD
surface-type filter selects its curved wall, and a fitted PNG supplies the height:
```python
cylinder = cq.Workplane("XY").cylinder(40.0, 10.0)
model = pv.CADModel.from_cadquery(cylinder)
outer_wall = model.select_faces(surface_type="cylinder")[0]
skin_height = pv.ImageHeightField(
"examples/data/height_maps/skin_microrelief.png",
amplitude_mm=0.35,
mapping="fit",
)
relief = pv.SurfaceReliefVolume(
outer_wall,
skin_height,
embed_mm=0.20,
edge_falloff_mm=0.8,
)
solid = model.to_node(use_fast_mode=True)
unioned_root = pv.BBoxUnion([solid, relief])
# Preview the wrapped relief shell without the base cylinder.
root = relief
```
The packaged map is a synthetic, skin-inspired pattern with raised plateaus, furrows, and pore-like
details. Its left and right edges match, so the texture closes around the cylinder. The 0.35 mm
height is deliberately exaggerated so it is visible and printable; it is not patient-specific
measured skin.
The standalone view shows the thin wrapped volume created from the selected wall. The unioned view
adds the original cylinder, including its top and bottom, beneath that same relief. The complete
[`cylindrical_skin_relief.py`](https://github.com/MacCurdyLab/OpenVCAD/blob/main/examples/geometry/surface_relief/cylindrical_skin_relief.py)
example renders `relief` by itself; change `root` to `unioned_root` to preview the finished part.
## Choose practical relief settings
| Control | Good starting point |
| --- | --- |
| `amplitude_mm` | Start with a height that is visible at the intended print scale, then reduce it if the texture is too strong |
| `embed_mm` | Use a small positive overlap, often 0.2–0.5 mm depending on part and print resolution |
| `edge_falloff_mm` | Use 0 to keep full height at the edge, or a positive distance for a smooth transition |
| `mapping` | Use `fit` for one full pattern and `repeat` for tiled texture |
| Sampling/export resolution | Keep the voxel size small enough to resolve both the relief height and its narrowest feature |
As a starting point, use at least 4–6 voxels across the smallest pattern feature and several
voxels through the relief height. A coarse preview or export can hide fine texture even though the
relief definition is unchanged.
Surface relief currently creates **raised** geometry. It works on one selected CAD face or one
connected open triangle-mesh patch at a time. Those simple boundaries make the authoring workflow
predictable: select the region, choose the pattern, set the physical height, and join it to the
part.