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
pyvcadandpyvcad_compilers(see the installation guide and repositoryexamples/project_example).Familiarity with
VOLUME_FRACTIONSon 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
MaterialDefsfor 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_prefixplus the layer index plus.pngin 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 sampledVOLUME_FRACTIONSattribute 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
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_FRACTIONSare not defined there, the compiler usesfallback_material_id(default 0, typically void in the default material table) and still treats the voxel as active.set_strict_mode(True): raisesRuntimeErroron the first inside voxel that lacksVOLUME_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
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)
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
Slice - 0.05 mm voxels
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 |
|
|
|
Folder to create/write PNGs into. |
|
Prefix for slice filenames ( |
|
|
|
Distance (mm) for liquid keep-out; |
Methods (including CompilerBase)#
Method |
Role |
|---|---|
|
Run the full pipeline. |
|
Names this compiler requires (includes |
|
Request cooperative cancellation. |
|
|
|
|
|
|
|
Require |
|
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_sizeand bounding box; fine spatial detail needs sufficiently small voxels.Asymmetric
voxel_sizecomponents 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 partialVOLUME_FRACTIONSon a union.