Material Inkjet Compiler#

The MaterialInkjetCompiler turns OpenVCAD designs with VOLUME_FRACTIONS into a stack of RGBA PNG images: one image per build layer, where each pixel encodes the resolved material for one addressable voxel in a material-jetting workflow.

This page is both a tutorial (from a valid design to PNG slices) and a reference for compiler behavior and Python API.

Voxel-printing workflow#

The Material Inkjet compiler is meant for inkjet / PolyJet-class 3D printers that use a workflow often called voxel printing. These systems build objects by depositing droplets of photocurable polymers layer by layer. As the layers stack up, those droplets form a 3D object.

Unlike 2D inkjet paper printing, a 3D material-jetting voxel cannot hold an arbitrary blend of inks. At a given voxel location, the printer must choose one material channel to deposit. That gives the workflow full local composition control: for every addressable voxel, the slice stack can say which material should be placed there.

Large-scale gradients come from changing the density of material choices across a region rather than from storing a blended color at one voxel. For example, a region that should behave like a 50/50 red-blue mixture is represented by many neighboring voxels where roughly half are assigned red and half are assigned blue. Because inkjet printers work at very small scales, that interdigitated pattern can appear smooth at the part scale. A Stratasys J750-style workflow, for example, uses a voxel pitch around 42.3 x 84.6 x 27 microns (0.0423 x 0.0846 x 0.027 mm).

Inkjet 3D printers commonly consume a stack of PNG files. Each PNG is one Z layer, and each pixel is one addressable voxel in that layer. The pixel color is a palette value that downstream printer software maps to a physical material channel. These PNG stacks are usually composed of base material colors interdigitated across space, not full-color gradients in the image data itself, because each voxel color must identify one printable channel.

OpenVCAD provides a programmable way to generate these stacks from implicit geometry and material fields. This matters because the voxel count is far beyond what a designer can draw manually in traditional image-editing software. A large J750-class build can contain over 400 billion addressable voxels, so the practical workflow is to model the part and its material distribution parametrically, then let OpenVCAD compile the PNG stack.

OpenVCAD produces the PNG stack, but it does not perform the final printer setup. Downstream software such as Stratasys GrabCAD Print or the Voxel Print Utility is still responsible for mapping the PNG color palette into printer material channels. Likewise, the Material Inkjet compiler does not generate support material; support generation is normally handled later by the printer workflow.

Prerequisites#

  • OpenVCAD installed with pyvcad and pyvcad_compilers (see the installation guide and repository examples/project_example).

  • Familiarity with VOLUME_FRACTIONS on geometry (see Getting Started with OpenVCAD, especially Lesson 6: Volume Fractions, and the Functional Grading Guide).

What this compiler is for#

Use Material Inkjet when you need discrete material assignment per voxel derived from a volume-fraction mixture at each point, exported as slice images for hardware or software that consumes layered PNG (or similar) stacks. Typical examples include multi-material jetting systems and other pipelines that think in Z-ordered voxel planes.

If your goal is continuous RGB color per voxel through an ICC-aware pipeline that maps to CMYK + white + clear process inks, use the Color Inkjet compiler instead (COLOR_RGBA).

What “PNG stacks” means here#

After compilation you get one PNG file per Z layer in the voxel grid built from the scene’s bounding box:

  • Width x height of each PNG equals the X x Y voxel resolution for that job.

  • RGBA stores the material color from your MaterialDefs for the chosen material ID at that voxel, not a continuous mixture rendered as a color blend in the PNG (see Stochastic material choice).

  • File naming: each slice is written as your file_prefix plus the layer index plus .png in the output directory. Empty layers (no active voxels) are removed after the run to keep the stack smaller.

  • Coordinate note: slice images may use an X-axis flip relative to world axes. If your downstream tool expects a different image handedness, you may need to account for that when interpreting files.

How volume fractions become voxel assignments#

The compiler voxelizes the model’s bounding box using your voxel_size, samples the implicit design on that grid, and writes one RGBA PNG per Z layer. The important conversion is from a continuous, implicit material field to a single material assignment at each voxel.

For each voxel sample location, the compiler evaluates two ideas from the OpenVCAD design:

  • g(x, y, z): the signed-distance field for the geometry. Negative values are inside the object, zero is on the surface, and positive values are outside.

  • a(x, y, z): the sampled VOLUME_FRACTIONS attribute at that location. For this attribute, the value is a material mixture: material IDs mapped to local fractions.

First, g(x, y, z) decides whether the voxel is part of the build. If the sample is outside the object, the compiler writes the transparent void color (0, 0, 0, 0). If the sample is inside or on the surface, the compiler samples a(x, y, z) and uses the resulting volume fractions to choose one material.

Volume fractions follow the same normalization rule as probability distributions: the local material fractions should sum to one. The compiler uses those fractions as weights in a stochastic selection process. If a voxel location samples a 0.5 / 0.5 mixture of red and blue, that voxel has a corresponding 50% / 50% chance of being assigned red or blue. The PNG pixel receives the RGBA color for the selected material from MaterialDefs.

This process repeats for every voxel in the object’s bounding box, at the requested printer resolution, until the compiler has written the complete stack. Asymmetric voxel sizes are fully supported; inkjet printers often have different X, Y, and Z pitches, so voxel_size is a Vec3 rather than one scalar.

Per-voxel material assignment

Flowchart showing how signed distance and volume fractions become material assignments in a PNG stack

Stochastic material choice#

Where VOLUME_FRACTIONS define a mixture of materials, the PNG does not store fractional blending. The compiler performs a weighted random choice among materials according to the local mixture. Fine voxels can still look noisy when two materials are mixed; this is normal for this representation.

Liquid keep-out#

If liquid_keep_out_distance > 0, the liquid material is removed from the local mixture when the sample lies within that distance of the surface (using the signed distance). Your MaterialDefs must include a material named "liquid" or "M.Cleanser" in that mode, or compile() raises an error. Use this when your process should avoid assigning liquid near boundaries.

Missing VOLUME_FRACTIONS inside the solid#

This behavior is separate from the up-front check that the design exposes VOLUME_FRACTIONS at the root. Review Lesson 6: Volume Fractions for how the attribute is attached, and Lesson 7: The Node Hierarchy and Attribute Priority for how unions and overrides affect which regions inherit VOLUME_FRACTIONS. That is why one branch of a union can be “missing” data even when another branch is not:

  • Default: if a voxel is inside the solid but VOLUME_FRACTIONS are not defined there, the compiler uses fallback_material_id (default 0, typically void in the default material table) and still treats the voxel as active.

  • set_strict_mode(True): raises RuntimeError on the first inside voxel that lacks VOLUME_FRACTIONS, including the world-space location in the message.

If the VOLUME_FRACTIONS name never appears on the root attribute list at all, compile() fails before voxelization. See examples/attributes_by_type/undefined/material_inkjet_example.py for a union where only one child carries VOLUME_FRACTIONS.

Tutorial: design to PNG slices#

The scripts for this walkthrough live under examples/compilers/material_inkjet/ in the repository.

OpenVCAD to voxel-printing stack

Workflow from OpenVCAD design to PNG stack to downstream material channel mapping for inkjet printing

1. Build geometry with VOLUME_FRACTIONS#

Use pv.VolumeFractionsAttribute with expressions and material IDs from pv.default_materials (or your own MaterialDefs JSON). Fractions at each point should sum to 1 (see Lesson 6: Volume Fractions).

The example uses a rectangular prism with a left-to-right material gradient between blue and red. The renderer preview shows the continuous VOLUME_FRACTIONS field before it is converted into discrete voxel assignments.

VOLUME_FRACTIONS (preview)

Material inkjet guide preview render

2. Choose voxel_size#

voxel_size is a Vec3 of voxel edge lengths in millimeters (same unit convention as the rest of OpenVCAD). Resolution in each axis scales with bounding box extent / voxel size. Finer voxels increase memory use, file count, and compile time.

For inkjet workflows, voxel_size is often set to match your printer’s native voxel or slice resolution (or an integer fraction of it) so the stack aligns with what the machine or downstream printer software expects. The runnable example below uses pv.Vec3(0.0423, 0.0846, 0.027), matching the J750-style pitch discussed above.

3. Run the compiler#

import os
import shutil
import pyvcad as pv
import pyvcad_compilers as pvc
import pyvcad_rendering as viz

materials = pv.default_materials

cube = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(20, 10, 10))
fraction_gradient = pv.VolumeFractionsAttribute(
    [
        ("x/20 + 0.5", materials.id("blue")),
        ("-x/20 + 0.5", materials.id("red"))
    ]
)
cube.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, fraction_gradient)
root = cube

voxel_size = pv.Vec3(0.0423, 0.0846, 0.027)
output_dir = os.path.join(os.path.dirname(__file__), "output")
prefix = "slice_"

if os.path.isdir(output_dir):
    shutil.rmtree(output_dir)
os.makedirs(output_dir, exist_ok=True)

compiler = pvc.MaterialInkjetCompiler(root, voxel_size, output_dir, prefix, materials, 0.0)

def on_progress(p):
    print("compile progress: {:.1f}%".format(100.0 * p))

compiler.set_progress_callback(on_progress)
compiler.compile()
print("resolution (x, y, z png count):", compiler.resolution())
print("material voxel counts:", compiler.material_voxel_counts())

viz.Render(root, materials)

After compile(), output/ contains the PNG stack. Inspect a slice in any image viewer; Z order follows the file index. When the job finishes, resolution() reports (x_pixels, y_pixels, z_png_count), and material_voxel_counts() reports how many active voxels were assigned to each material ID.

The comparison slices below are from the same example scene, using two coarser voxel_size settings for readability. Smaller voxels increase in-plane resolution and make smooth VOLUME_FRACTIONS gradients look less blocky at pixel scale, at the cost of more layers and longer compile times.

Slice - 0.15 mm voxels

Material inkjet slice at 0.15 mm voxel size

Slice - 0.05 mm voxels

Material inkjet slice at 0.05 mm voxel size

From the repository root, with the project virtual environment activated: python examples/compilers/material_inkjet/01_basic_png_stack.py runs the compile and then opens the interactive preview (viz.Render does not return until you close the window). To verify only the compile step, temporarily comment out the final viz.Render line.

4. Preview in the renderer#

With viz.Render(root, materials), use the renderer’s attribute selection to view VOLUME_FRACTIONS as usual. The PNG stack, however, shows picked materials per voxel, not the continuous mixture.

Reference: Python API#

Constructor#

MaterialInkjetCompiler(root, voxel_size, output_directory, file_prefix, material_defs, liquid_keep_out_distance=0.0)

Argument

Meaning

root

Root Node of the design tree.

voxel_size

Vec3 voxel spacing (mm), including asymmetric printer pitches.

output_directory

Folder to create/write PNGs into.

file_prefix

Prefix for slice filenames (prefix + z + ".png").

material_defs

MaterialDefs (e.g. pv.default_materials) mapping IDs to RGBA.

liquid_keep_out_distance

Distance (mm) for liquid keep-out; 0 disables.

Methods (including CompilerBase)#

Method

Role

compile()

Run the full pipeline.

supported_attributes()

Names this compiler requires (includes VOLUME_FRACTIONS; same string as pv.DefaultAttributes.VOLUME_FRACTIONS).

cancel()

Request cooperative cancellation.

set_progress_callback(fn)

fn(progress) with progress in [0, 1].

resolution()

(x_pixels, y_pixels, z_png_count) after compile.

material_voxel_counts()

dict mapping material ID to active voxel count.

set_strict_mode(bool)

Require VOLUME_FRACTIONS at every inside voxel.

set_fallback_material_id(id)

Material ID when strict mode is off and samples are missing.

For full autodoc signatures, see the pyvcad_compilers section (MaterialInkjetCompiler and CompilerBase).

Limitations#

  • One discrete material per voxel in the output; mixtures become stochastic picks, not blended colors in the PNG.

  • Effective resolution is tied to voxel_size and bounding box; fine spatial detail needs sufficiently small voxels.

  • Asymmetric voxel_size components are allowed and expected for many inkjet workflows. Treat X, Y, and Z resolution separately.

  • Root attribute list must include VOLUME_FRACTIONS; missing per-voxel samples are handled by strict / fallback and are a different situation than the attribute never appearing on the tree.

  • The PNG stack is an OpenVCAD compiler output, not a complete printer job. Downstream printer software still maps palette colors to material channels and handles support generation.

Further examples#

  • examples/compilers/material_inkjet/01_basic_png_stack.py - end-to-end compile + render (this guide).

  • examples/attributes_by_type/undefined/material_inkjet_example.py - default fallback, custom fallback, and strict mode with partial VOLUME_FRACTIONS on a union.