---
title: Raymarching & SDFs
order: 3
---

A **signed distance field** (SDF) describes a shape by one number at every point in space: how far that point is from the shape's surface, negative inside and positive outside. **Raymarching** renders it by walking a ray from the camera in steps of that distance until it lands on the surface. Chisel's shapes, booleans and fillets are all distance fields, and its viewport is a raymarcher. This page shows the math behind that.

## A shape is a question

A polygon mesh answers the question *"where is the surface?"* with a list: these vertices, joined by these faces. Chisel's shapes answer a different question:

> **How far am I from the surface?**

Pick any point in space and the shape gives you back one number: the distance to its nearest surface. Outside the shape the number is positive. Inside it is negative. Exactly on the surface it is zero. That's the whole idea: a **signed distance field** is a shape described by how far away it is from everywhere.

Take a sphere of radius `1` sitting at the origin. Its distance function is about as simple as math gets:

```
d(p) = |p| − 1
```

Stand at `(2, 0, 0)` and it tells you `1`. You are one unit outside. Stand at `(0.5, 0, 0)` and it says `−0.5`, half a unit inside. Stand at `(1, 0, 0)` and it says `0`: you're on the skin. There is no list of points anywhere. The sphere is a rule, and the rule is true at every point in space, at any zoom, forever. That is why a Chisel sphere is round no matter how close you get.

A box is only slightly more work (measure how far you are outside each face, combine the three axes) and every other primitive is a similar few lines. Because the shapes are rules and not point lists, a Chisel scene is tiny, and editing a radius changes the rule rather than rebuilding geometry.

## The field knows which way is out

The word *field* matters. The distance isn't only defined on the surface; it's defined at every point, and it changes smoothly as you move. Walk a tiny step in any direction and see how the distance changes, and you learn which way the surface faces. In math terms the gradient of the field is the surface normal:

```
n = ∇d(p)
```

Chisel shades every pixel from that gradient. There are no vertex normals to smooth, no faceting to hide, and the shading is exactly as smooth as the surface itself, because it *is* the surface.

## Combining shapes is arithmetic

Two shapes `A` and `B` combine with plain arithmetic. The distance to their union (everything inside either) is the smaller of the two distances at that point:

```
union(A, B)        = min(A, B)
intersection(A, B) = max(A, B)
difference(A, B)   = max(A, −B)
```

Read the difference line slowly: negating `B` turns it inside out, so `max` keeps what's inside `A` *and* outside `B`. That's a cut.

Run the numbers on our sphere and a second sphere of radius `1` centred at `(1.5, 0, 0)`. At the point `(0.75, 0, 0)`, halfway between them: sphere A says `−0.25`, sphere B says `−0.25`. Union: `min = −0.25`, inside. Intersection: `max = −0.25`, also inside, the lens where they overlap. Difference: `max(−0.25, +0.25) = +0.25`, outside. B has scooped that spot out of A.

No faces were split and nothing became non-manifold: each boolean is one comparison per point, and the result is another distance field you can keep cutting into. This is why a Chisel [Boolean](../modifiers/boolean.md) cannot produce a broken mesh. There is no mesh.

## Where the fillet comes from

Push *Blend Radius* on a boolean and the seam becomes a fillet. Nothing is added to make that happen; `min` is simply replaced by a *soft* minimum. The classic one, for a blend radius `k`, is:

```
h = clamp(0.5 + 0.5·(B − A) / k, 0, 1)
smin(A, B, k) = mix(B, A, h) − k·h·(1 − h)
```

Wherever the two surfaces are further apart than `k`, the plain minimum wins and nothing changes. Within `k` of both, the two fields negotiate, and the `− k·h·(1 − h)` term pushes the surface outward exactly where they meet. That bulge is the fillet. The **radius you set is a real distance in the scene**, not a subdivision count, so the fillet stays perfectly round at any zoom.

The five [profiles](../rounding-and-blending/profiles.md) (Round, Sharp, Soft, Tight, Chamfer) are five different ways of writing that negotiation. Chamfer, for instance, joins the two surfaces with a straight line instead of an arc.

The same trick rounds a single shape. Shrink a box by `r` on every side and then subtract `r` from its distance:

```
rounded_box(p) = box(p, size − r) − r
```

Every point that was within `r` of the shrunken box is now inside, so the corners become quarter-spheres and the edges quarter-cylinders. That is the *Rounding* slider on every primitive: an offset of the field, which is why it costs nothing. The [Smooth](../modifiers/smooth.md) modifier applies the same idea to the whole model at once: one radius, every join and edge sharper than it rounds up to it.

## Modifiers bend space, not shapes

Booleans combine fields. The other modifiers change the *question* before it reaches the shape.

Take a mirror across X. Instead of building a second copy, Chisel takes your point `p`, flips the sign of `x`, and asks the original shape *that* question:

```
mirrored(p) = d( (|p.x|, p.y, p.z) )
```

A point on the left of the mirror plane is answered as if it were on the right, so the shape appears on both sides. One shape, two appearances, zero extra cost. Array is the same trick with a repeating offset. Twist rotates `p` by an angle that grows along the axis before asking. [Revolve](../modifiers/revolve.md) collapses the point onto a half-plane (its distance from the axis becomes one coordinate, its height the other) so a flat profile answers as a full ring:

```
revolved(p) = profile( √(p.x² + p.y²) − offset, p.z )
```

[Lattice](../modifiers/lattice.md) warps `p` through a cage; [Sweep](../modifiers/sweep.md) finds the nearest point on the path and asks the cross-section from there. In every case the shape is left alone and space is bent around it, which is why a bent shape is still exactly one shape, still watertight, still cuttable.

## Raymarching: finding the surface

Every pixel needs to know where the surface is along its line of sight. A mesh renderer tests triangles; Chisel marches.

Fire a ray from the camera through the pixel. Ask the field: how far is the nearest surface? Say it answers `2.3`. You don't know *which direction* that surface lies in, but you know for certain there is nothing within `2.3` units in any direction, so you can safely walk `2.3` units along the ray. Ask again. Walk again. Each step is as long as it can safely be, so the ray covers empty space in a few big strides and slows down only as it approaches a surface.

```
t = 0
repeat:
    d = field(camera + t · direction)
    if d < hit_threshold:  hit: shade this pixel
    t = t + d
    if t > max_distance or too many steps:  miss: background
```

That loop is the whole renderer, and its three knobs are the three sliders at the top of [Render → Quality](../render-engine/quality.md):

- **Hit Threshold**: how close counts as touching. Smaller is more exact, especially for thin parts and tight corners.
- **Max Steps**: how many strides a ray may take before giving up. Rays that graze a surface at a shallow angle take many small steps, which is why silhouettes go chunky first when this is too low.
- **Max Distance**: how far a ray travels before it's called a miss.

The soft minimum plays nicely here: it only ever reports a distance *smaller* than the plain minimum would, so a ray marching through a blended join steps a little more cautiously and never jumps through the fillet.

## Compared with meshes

| | Rasterization (EEVEE) | Path tracing (Cycles) | Raymarching (Chisel) |
|---|---|---|---|
| **Input** | Triangles | Triangles | Distance functions |
| **Precision** | Mesh density | Mesh density | Mathematically exact |
| **Booleans** | Mesh ops, can fail | Mesh ops, can fail | One comparison per point |
| **Fillets** | Extra geometry | Extra geometry | A term in the formula |
| **Speed** | Very fast | Slow | Real-time for SDF |

## Trade-offs

- No UV painting until you [convert to mesh](../operations/convert.md)
- GPU-bound: needs a reasonable graphics card
- Very dense scenes can slow the viewport; [Quality](../render-engine/quality.md) and [Performance](../render-engine/performance.md) are the controls for that
- Meshes come in through [Convert to SDF](../operations/convert-to-sdf.md) and go out through Convert to Mesh

## See also

- [Render Engine: Quality](../render-engine/quality.md): the sliders behind the march
- [Rounding & Blending](../rounding-and-blending/index.md): the fillet profiles in practice
- [Modifiers](../modifiers/index.md): everything that combines or bends the field
