Packs and the material format
The open .openpaintmaterial format and the pack.json manifest, for anyone building or distributing packs.
On this page 5 sections
Two open formats underpin everything on this site. A .openpaintmaterial file is one material — the same file Open Painter saves from its Asset Library. A pack is a ZIP of such files with a pack.json manifest that adds what a single material deliberately lacks: identity, version, license, tags and third-party provenance. Materials+ packs, team libraries and the free starter pack all use it, and the validator that checks them is a stdlib-only Python package you can run yourself.
The material file
A .openpaintmaterial is either plain UTF-8 JSON (for constant and procedural-only materials) or a ZIP containing material.json and an assets/ folder of lossless 32-bit float EXR images:
material.json
assets/0000.exr
assets/0001.exrmaterial.json carries format_version, name, an ordered layers array (bottom to top) and optional surface parameters (anisotropy, coat weight, coat roughness, height distance). Each layer has an id, name, type (FILL, PAINT or FOLDER), optional parent_id, visible, opacity, blend_mode, its channels and its masks.
| Record | Fields |
|---|---|
| Channel | Stable ids BASE_COLOR, ROUGHNESS, METALLIC, NORMAL, HEIGHT, AO, EMISSION, OPACITY. Each has enabled, color (linear RGBA), value, image, projection (UV, TRIPLANAR or DECAL), scale, rotation, offset, per-channel opacity and blend_mode, a procedural identifier with scale, seed, detail and signed height amplitude, an ordered effects list, and an optional detail_image. |
| Image reference | {"asset": "assets/0000.exr", "colorspace": "Non-Color", "alpha_mode": "STRAIGHT"}. Assets are bundled, never base64 in JSON; a shared binding exports one EXR and imports as one packed image. |
| Mask | source (PAINT, BAKED, GENERATOR, NOISE, FILL, PROCEDURAL, FILTER, SCENE, REFERENCE, ANCHOR, STROKES, MESH, GROUP, GRAPH), operation (MULTIPLY, ADD, NORMAL, MAX, MIN), enabled, invert, black, white, strength, generator and noise parameters, reference and anchor fields, micro-detail anchors. |
| Anchors (format 3) | A mask with source ANCHOR or a channel effect with kind ANCHOR publishes the stack prefix before it; consumers store the anchor id. Import assigns fresh ids and remaps consumers; an unresolved id is reported, a cycle is refused. |
| Module (format 4) | A module block {id, version, name, root, description, created} makes the file a versioned, updatable subtree. See material modules. |
Current files are format 3 (materials) or 4 (modules). The importer accepts at most 256 layers and 512 MiB uncompressed. Baked mesh maps are resolved in the destination texture set rather than shipped; a baked map explicitly used as a layer texture is bundled. Current exports include strokes used by the saved layers and stroke masks, including brush-alpha assets; import gives them fresh identities and destination scope. Spatial placement needs checking on another asset. Aging history and overrides, physical sizes, nested mask groups and named mesh/decal references also persist. Custom Graph masks must be baked and live scene fields frozen before saving; preview caches are serialized as Live. Imported metadata is never executable, and any invalid input rolls back before touching the stack. The historical format overview is in the add-on's SMART_MATERIAL_FORMAT.md. The current serializer and loader define newer fields such as stroke records and image alpha modes. Human-readable examples are under src/open_painter/assets/smart_materials/.
Pack layout
my-pack-1.2.0.zip
├── pack.json
├── LICENSE.txt (optional)
├── README.md (optional)
├── materials/
│ └── Rusted_Plate.openpaintmaterial
└── previews/
└── Rusted_Plate.pngOnly those two directories and the listed root files are allowed. Every material needs a square PNG preview of at least 512 px. Connect installs each member as <pack-id>__<Material>.openpaintmaterial so packs from different publishers never collide in the library folder.
pack.json
{
"schema_version": 1,
"id": "rusted-industrial",
"uuid": "3f0c9c3e-2a1b-5c6d-8e7f-9a0b1c2d3e4f",
"name": "Rusted Industrial",
"version": "1.2.0",
"summary": "Twelve corroded steel and iron surfaces with editable wear.",
"publisher": { "id": "your-studio", "name": "Your Studio", "url": "https://example.com" },
"license": "CC-BY-4.0",
"min_open_painter_version": "0.8.0",
"category": "metal",
"tags": ["rust", "industrial"],
"created": "2026-09-15",
"materials": [
{
"file": "materials/Rusted_Plate.openpaintmaterial",
"name": "Rusted Plate",
"preview": "previews/Rusted_Plate.png",
"category": "metal",
"tags": ["rust"],
"provenance_refs": ["polyhaven-rust-01"]
}
],
"provenance": [
{
"id": "polyhaven-rust-01",
"publisher": "Poly Haven",
"author": "Rob Tuytel",
"source_url": "https://polyhaven.com/a/rust_coarse_01",
"license": "CC0-1.0",
"conversions": "EXR normal converted to 8-bit PNG, OpenGL convention"
}
]
}| Field | Rule |
|---|---|
| schema_version | Must be 1. |
| id | 2–64 characters, lowercase letters, digits and hyphens. Stable across versions; it becomes the install prefix. |
| uuid | A UUID that never changes for this pack. A UUID5 of the id is a fine choice. |
| name, version | Version is semver. New versions of a pack must increase it. |
| publisher | An object with at least a name. |
| license | An SPDX identifier, or LicenseRef-… for a custom license. Required. |
| min_open_painter_version | Optional semver: the lowest painter version the pack's materials need. |
| materials[] | file, name and preview are required. sha256, bytes and preview_sha256 are required in a finished pack; the build tool fills them in. |
| provenance[] | One entry per third-party source with its license; materials reference them through provenance_refs. Optional but expected for any pack that includes scanned or downloaded data. |
Building and validating a pack
The openpaint_format package in the platform repository has no dependencies beyond the Python standard library, so it runs anywhere — including inside Blender, which is how Connect verifies packs after download.
# Lay out materials/ and previews/ next to a pack.json that may omit hashes
python -m openpaint_format.build ./my_pack_src ./my-pack-1.2.0.zipThe build discovers materials by name when the manifest does not list them, computes sha256, bytes and preview_sha256, and writes a deflated ZIP. The platform's ingestion runs the same inspector on upload: ZIP safety (entry count, size, compression ratio, no symlinks or escaping paths), manifest cross-checks (every declared file present, no undeclared files, checksums match), a full parse of every material, and preview dimensions. Problems are reported with codes such as pack.checksum, manifest.missing_file or material.invalid.
Licensing
The format, the validator (packages/openpaint_format) and Connect carry their own GPL-3.0-or-later notices. Open Painter uses Community and Studio licensing. Software licensing says nothing about the materials inside a pack: each pack carries its own license in pack.json, and materials you make are yours. Materials+ packs are licensed under the Materials+ license.