# ez3d

Grayscale PNG in, 3D-printable STL out. One pixel is one unit (treat it as
a millimetre — STL is unitless and slicers assume mm), one byte of gray is
one step of terrain height, and the result is a closed, watertight solid
with a flat bottom ready for the print bed.

    ez3d [options] input.png output.stl

## Quick start

    make
    ./ez3d --height 8 --base 2 --verify drawing.png model.stl

For anything large (line art, photos), add the simplification pair:

    ./ez3d --simplify 1 --decimate 1 --verify drawing.png model.stl

## Size, scale and resolution

- **The model comes out at 1 px = 1 unit**, so a 1024x768 drawing is a
  1024x768 mm object — use the slicer's scale tool (e.g. 20% for a
  ~200 mm print). Rescaling in ez3d would just duplicate the slicer.

- **Visible pixel steps are the source image's grid.** At 20% scale each
  pixel is 0.2 mm — right at nozzle diameter — and the printer faithfully
  resolves the pixel staircase around curves (including the wobbles you
  drew). For finer steps, draw at a higher resolution: a 2048-wide snake
  at 20% has 0.1 mm pixels.

- **Height is calculated** as a simple linear equation:
  `z = base + (gray/255) * height` where both `base` and `height` can be
  provided as positive or negative values. In cases where `z < 0` this
  pixel is removed from the output (cut-away regions).

- **Alpha Channel** (if it exists) is tested against a threshold, and the
  height is regarded as negative infinity, however this value may be changed
  by command line option. See discussion about edge values.


## Cell and Vertex Diagram (Image width=3 height=2)
```
       x=0.0   x=0.5   x=1.0   x=1.5   x=2.0   x=2.5   x=3.0

       (V00)---{H00}---(V10)---{H10}---(V20)---{H20}---(V30)    y=2.0
         |  \    |    /  |  \    |    /  |  \    |    /  |
         |   \   |   /   |   \   |   /   |   \   |   /   |
         |    \  |  /    |    \  |  /    |    \  |  /    |
       {E00}---[C00]---{E10}---[C10]---{E20}---[C20]---{E30}    y=1.5
         |    /  |  \    |    /  |  \    |    /  |  \    |
         |   /   |   \   |   /   |   \   |   /   |   \   |
         |  /    |    \  |  /    |    \  |  /    |    \  |
       (V01)---{H01}---(V11)---{H11}---(V21)---{H21}---(V31)    y=1.0
         |  \    |    /  |  \    |    /  |  \    |    /  |
         |   \   |   /   |   \   |   /   |   \   |   /   |
         |    \  |  /    |    \  |  /    |    \  |  /    |
       {E01}---[C01]---{E11}---[C11]---{E21}---[C21]---{E31}    y=0.5
         |    /  |  \    |    /  |  \    |    /  |  \    |
         |   /   |   \   |   /   |   \   |   /   |   \   |
         |  /    |    \  |  /    |    \  |  /    |    \  |
       (V02)---{H02}---(V12)---{H13}---(V22)---{H23}---(V32)    y=0.0
```

The above diagram is the layout as seen in the image, note the (x,y)
values are included around the edge to show the cell middle positions
offset by 0.5 from the corners. Four different grids intersect:
 - **C** is the cell grid from the original image.
 - **V** is the vertex grid at the corners of cells.
 - **H** is the horizontal edge grid between 2 cells.
 - **E** is the vertical edge grid between 2 cells.

## How the solid is built

- **Cell vs Vertex** Each image pixel becomes a square "cell" and it has
  four verticies at the corners of the square. The height of the center is
  calculated directly from the image pixel greyscale, but the height of the
  corners is actually an estimate based on local averaging.

- **Trangle Model** Each image pixel gets converted into eight right-angle
  triangles, surrounding the center point. This avoids any preference for
  one particular fold direction, but see also the simplification and
  decimation process where triangles are removed.

- **The Cell grid** exactly matches the original image grid, thus an image
  of `W x H` will be mapped onto a floating point grid of `W x H`. Cell
  height values are calculated first because they can be directly mapped
  from the image.

- **The Vertex grid** is 1 unit larger than the original image grid, and
  offset by half a unit in physical space. Thus, an image of `W x H` will
  be mapped onto a floating point grid of `(W+1) x (H+1)` and the extra
  is equally distributed around all four edges of the image. Vertex height
  values are calculated after the cell values, by a process of trimmed
  mean averaging (start with 4 cell values that touch this vertex, remove
  highest and lowest and average the remaining two). A transparent alpha
  channel will generate `edge` values same as cells beyond the edge of the
  world. See discussion about edge values.

- **Two Edge grids** are similar to the vertex grid but sitting at the
  midpoint between two cells. They take on the average of the two connected
  cells depending on whether it is a horizontal linkage or a vertical
  linkage. These improve the overall smoothness of connections between cells
  when the square cells are decomposed into multiple triangles.

- **A Cut-away Vertex** is defined as anything with height less than zero.
  Any triangle where any corener has height less than zero will simply be
  omitted. There is a borderline case where `z == 0` in which case the
  triangle is allowed, only if at least one corner has `z > 0` but will be
  rejected if all corners are `z == 0`.

- **A Cut-away Cell** has all corners with height below zero, or with center
  height below zero. These cells emit no triangles at all.

- **A Fully-solid Cell** has all corners and center at height `z >= 0`.
  This is the standard output of four right-angle trianges.

- **A Chamfered Cell** or diagonal cutout happens when the center of the cell
  has height above zero, three corners have height above zero, but with a
  missing corner. This situation will only output two right-angle triangles,
  instead of the usual four trianges.

- **A Tiny Cell** or pointy cutout happens when the center of the cell
  has height above zero, two adjecent corners have height above zero, but
  the other two corners are missing. This situation will only output a single
  right-angle triangle, instead of four trianges.

- **A Pinch Cell** happens when the center of the cell has height above zero,
  two diagonally opposite corners have height above zero, but the other two
  corners are missing (`z < 0`). Often these will produce two squares.

- **Flat-region merging** is a first round speedup before full decimation.
  Each "fully-solid" cell is checked for "flatness" based on center height
  and all corners. This test is within a certain tolerance, and we presume
  that all the partial cells cannot ever be counted as flat.

## Edge of World Values and Cut-Aways (`--edge F`)

There's no perfect solution to handling both sharp blocky edges and also be
able to deliver nuanced control for curves, etc. Ideally, an image of size
5x5 would also produce an output 5 units by 5 units. However, some jobs
require edges to be as smooth and well-shaped as possible. Using a -INFINITY
value for cells outside the edge of the world will average to also have a
-INFINITY value on all corners touching the edge of the world, which does
deliver nice shear cuts, but also makes the 5x5 image slightly smaller and
output only a 4x4 STL model. This is caused by the removal of half a pixel
of material all around the edges.

We could impose special case processing for edge-of-world cells, but doing
that results in the unintuitive behaviour that if you put a 5x5 image inside
a 7x7 image with a one pixel border of transparent alpha, then you end up
getting a different outcome to the original 5x5 image. The normal assumption
would be that additional transparent alpha beyond the edge of the actual
visible image should have no effect, and maintaining this assumption is
necessary from the perspective of ease of use. Typical and-user audience for
this `ez3d` software would be graphical designers, or amateur artists or
even perhaps kids and young people. Avoiding unexpected confusion is best
and this audience would be familiar with common image behaviour.

Ultimately putting this compromise into the command line option seems like
the best choice. Setting `--edge=-0.001` will make the 5x5 input image
deliver an output with base very close to 5x5 but also a slight roll off
in the final half pixel, and it will nicely shape cutouts. On the other hand
leaving the default `-INFINITY` gives the brutal sharp cut and also slightly
reduces the total size of the footprint. The user will need to adjust this
as required, but at least this adjustment is intuitive and easily explained.


## Flat-region merging (`--simplify N`)

First-pass bulk reduction before any triangle-by-triangle work: solid
cells in locally-flat regions are collected into rectangles (searched
largest-first, within a flatness tolerance set by the level), and each
rectangle is re-emitted as a small perimeter fan top and bottom instead
of eight triangles per pixel. Because a group's perimeter must be
surrounded by ordinary solid cells, grouped patches never have walls of
their own and the result stays watertight.

Levels 1..5 set the flatness tolerance from `height/100` down to
`height/10`. On a 750x750 line-art frame, level 1 removes two thirds of
the facets in milliseconds; combined with `--decimate 1` the whole
pipeline (2.15M raw facets) finishes in about 3 seconds and loads
instantly in a slicer. For large images this is the recommended pair.

## Decimation (`--decimate N`)

Quadric-error edge collapse (Garland & Heckbert QEM, the same family of
algorithm as slicer "mesh optimizer" features): repeatedly merges the
vertex pair whose collapse least affects the surface shape. Unlike a
generic optimizer, ez3d's decimation is heightmap-aware and print-safe:

- **Cut-out regions are never bridged.** The print rim, hole outlines and
  wall tops are creases (sharp dihedral angles); crease vertices only
  merge along their crease, rim runs keep a vertex at least every 2 units,
  and the first ring of triangles around each rim is frozen. Narrow gaps
  and interior slots stay open (verified by dedicated bridge tests).
  For comparison, on the sample snake a generic QEM "mesh optimizer"
  bridges the cut-outs at *every* aggressiveness level -- 40% of its
  facets span transparent regions even at the gentlest setting -- because
  quadric error cannot see empty space.

- **The result stays edge-manifold and consistently wound.**
  Collapses that violate the link condition, would flatten a triangle to
  zero area, or would land a vertex on top of another vertex are refused.

- **No folded facets.** Every surviving triangle must stay within ~87
  degrees of its original orientation, and heightfield surfaces keep
  their facing (terrain up, cap down). Uncontrolled QEM can walk facets
  past vertical through a chain of individually-legal collapses; folded
  facets face into the model and show up as dark triangles in
  shader-based viewers. `--verify` checks winding consistency and signed
  volume on every run.

- **Bounded memory.** Candidate edges are deduplicated per collapse and
  the candidate heap plus per-vertex fan sizes are capped, so even a
  750x750 image with over two million facets decimates in hundreds of MB
  rather than gigabytes. On images with large flat areas, run
  `--simplify 1` first: it removes the flat bulk cheaply and leaves the
  decimator with the detailed regions only.

Five levels of aggressiveness, with quadric error budgets on a ×2 ladder
in model mm:

| Level | Budget | snake sample (4,233,360-facet input) |
| ----- | ------ | ------------------------------------ |
| 1 | 0.05 mm | 167,136 facets |
| 2 | 0.1 mm  | 128,658 facets |
| 3 | 0.2 mm  | 104,062 facets |
| 4 | 0.4 mm  | 89,502 facets  |
| 5 | 0.8 mm  | 78,364 facets  |

The budget is a quadric scale, not a strict geometric tolerance; if you
scale the model down in the slicer, the effective budget scales with it
(print at 20% and level 3 behaves like ~0.04 mm of model detail). The
rim-protection rules make our counts somewhat higher than a naive QEM at
the same budget -- that is the price of not bridging cut-outs. Note that
on images which are mostly rim (thin frames, outlines) the levels
converge to nearly the same count, because almost every remaining
triangle is protected wall or rim; higher levels pay off on gently
curved terrain where interior vertices are free to collapse.

## Options

| Option | Meaning |
| --- | --- |
| `-H, --height F` | height of white (gray 255) in mm, default 10; negative means inverted image |
| `-b, --base F` | solid base thickness under black (gray 0), default 0; negative cuts dark pixels away |
| `-e, --edge F` | height for cells beyond the image edge and for transparent pixels, default -INFINITY (hard cut) |
| `-a, --alpha-thresh N` | alpha >= N counts as solid (0-255, default 128; `256` disables cutting) |
| `-s, --simplify N` | flat-region rectangle merging, 1 = minimal .. 5 = aggressive; 0 disables |
| `-d, --decimate N` | quadric-error simplification, 1 = finest .. 5 = coarsest; 0 disables |
| `-f, --alt-fold` | prefer the alternate corner-cut triangle layout in each cell |
| `--ascii` | write ASCII STL (debugging; `--verify` is binary-only) |
| `--verify` | re-read the STL: manifold, pinch and T-junction edges, winding, signed volume |
| `-V, --version` | print version |

## Example

    $ ./ez3d --simplify 1 --decimate 1 --verify frame.png frame.stl
    ez3d: frame.png: 750x750 pixels, gray+alpha
    ez3d: cutting 425104 of 562500 pixels (alpha < 128)
    ez3d: discovered 1011 groups, containing 122994 cells
    ez3d: decimated: 364336 -> 66816 triangles (level 1, 0.05 mm budget)
    ez3d: wrote frame.stl: 66816 facets, 3.2 MB, 2577 ms
    verify: 66816 facets, 33404 vertices, 0 degenerate
    verify: bbox x [0.5, 749.5] y [0.5, 749.5] z [0, 50]
    verify: 100236 edges shared by 2 facets, 0 T-junction edges, 0 pinch
    edges, 0 over-shared, 0 unmatched
    verify: orientation: 0 inconsistently wound shared edges, signed volume 6.59e+06
    verify: PASS

A manifold mesh satisfies Euler's formula `V - E + F = 2` (topology
of a sphere, or `2 - 2*holes` if your alpha mask encloses through-holes) —
the numbers above do, before and after decimation. Two relaxations are
reported but not counted as failures: T-junction edges (a vertex closes
the gap within tolerance) and pinch edges (four facets meet along a line,
two traversing each way — the solid legitimately narrows to a line where
only two opposite quarters of a cell survive the cut).

## Visual Example

To get a good visual examination of the result, the f3d utility is excellent.

    ./ez3d --base=-1 tests/gimp/diamond_test.png --decimate=0 /tmp/test.stl
    f3d --edges /tmp/test.stl

This allows close examination of the fundamental mesh generation.

## Layout

    decimate.c   QEM edge-collapse simplification (link-condition safe)
    decimate.h   Header definitions for decimate.c
    ez3d.h       Main options and configuration structure
    image.c      stb_image wrapper -> 8-bit gray + alpha planes
    image.h      Header definitions for image.c
    main.c       CLI, statistics, timing
    mesh.c       heightfield -> terrain fans, skirt/cliff walls, bottom cap
    mesh.h       Header definitions for mesh.c
    trimesh.c    indexed mesh buffer with exact-float vertex dedup
    stl_write.c  streaming binary/ASCII STL (facet count back-patched)
    stl_write.h  Header definitions for stl_write.c
    triangle.h   Commonly used (x,y,z) structures and basic inline functions
    verify.c     STL re-reader: manifold, pinch/T-junction, winding, volume
    verify.h     Header definitions for verify.c
    vendor/      pinned stb_image.h v2.30 (public domain)
    tests/       synthetic-PNG test suite (`make test`)

PNG decoding accepts every input format stb_image handles (gray, gray+
alpha, RGB(A), palette); 16-bit sources are converted to 8-bit. Output
binary STL is little-endian float32, as every slicer expects.

Without `--decimate` the mesher streams facets straight to disk (flat
memory); with it, the mesh is buffered plus decimation working state
(bounded: roughly 1 GB for a four-million-facet input, a few hundred MB
more typically).

## Scope

ez3d deliberately does one thing: turn a finished grayscale drawing into
a printable solid. Anything an image editor already does better is
expected to be done before the image gets here — invert colours, blur
for smoother terrain, crop, resample, touch up alpha. Every command line
option maps to geometry (the height equation, the cut threshold, the
edge-of-world value) rather than image manipulation, and keeping it that
way is a design goal: no filters, no transforms, no growing a second
image editor. For the same reason there is no XY scaling (the slicer's
job) and no STL repair or viewing (f3d and the slicers handle that).

## Limitations / ideas

- Overhangs are impossible because of automatic flat base.
- A 1x1 image with the default `-INFINITY` edge cuts itself away
  entirely (every corner is an edge-of-world corner); use
  `--edge=-0.001` for tiny images.
- Sub-pixel alpha contours: the cut boundary follows the pixel grid
  (draw at higher resolution for smoother outlines).
- No XY scaling by design — the slicer already does it better.
- Slicer "mesh optimizer" features can still decimate our output
  further; ours deliberately keeps more facets to protect cut-outs.
