Where to Put an AI-Generated Mark in 3D Data: 3MF, glTF XMP and a C2PA Sidecar Through Blender and Slicers

When you build a model with code written by AI and hand it to someone, you may want the file itself to record, in a form a machine can read, that it was made with AI. For images, the C2PA specification defines how to embed a manifest in PNG or JPEG. For 3D data, however, no format specification yet defines a place for an AI-generated mark. By “mark” we mean not a label a person sees, but a marker written inside or next to the file for machines to read.
This article contains affiliate links. As an Amazon Associate, we earn from qualifying purchases.
So in this article we put a mark in each of four candidate locations, ran the files through tools that import and re-export them, and compared which marks survived. The four locations are (1) a custom namespace in 3MF, (2) 3MF Core metadata, (3) a .c2pa sidecar file placed next to the file, and (4) KHR_xmp_json_ld in GLB (the binary form of glTF). The GLB went through Blender 5.2.1 and trimesh 4.11.1, and the .c2pa was created and validated with c2patool 0.28.4 and a test key. For the two 3MF locations, we summarize the results of Does an “AI-generated” entry in 3MF metadata survive slicers? (published 2026-11-13), which ran the file through four versions of three slicers.
Here are the results up front. When the marked GLB was imported into Blender 5.2.1 and exported again, all three marks we had put in the GLB (the XMP in KHR_xmp_json_ld, the value in asset.extras and the value in asset.generator) disappeared, with default settings and with export_extras=True alike. In trimesh 4.11.1, importing the file as a single mesh lost all three. Importing it as a scene kept the body of the XMP packet, but the reference from asset was gone, leaving the packet attached to nothing. Converting to STL left the header empty.
The sidecar .c2pa was written even though c2patool returned exit code 1, and it validated against the original file. But validation failed against the GLB re-exported by Blender or trimesh, and against the 3MF re-saved by PrusaSlicer 2.9.6.
Each condition was run once, and the results apply only to the versions listed.
- There is no standard place yet for an AI-generated mark in 3D data
- Subject and tools: a button cap built from AI-written CAD code
- Two locations inside 3MF: Core metadata and a custom namespace
- A .c2pa sidecar: a separate file for formats that cannot embed a manifest
- KHR_xmp_json_ld in GLB: a ratified extension for putting XMP into glTF
- For the IPTC vocabulary we chose trainedAlgorithmicMedia: a library computed the shape, but a trained model wrote the steps
- STL has no standard location: nothing defines how the 80-byte header is read
- Measurement: running GLB through Blender 5.2.1 and trimesh 4.11.1, the marks on asset did not survive
- Measurement: the .c2pa was written, but validation failed against re-saved files
- The two 3MF locations fared differently in slicers
- Comparison table by location and path
- Which location should a distributor choose?
- What this test cannot tell us
- Summary: after putting an AI-generated mark in 3D data, check whether it survives each tool it passes through
- Sources
There is no standard place yet for an AI-generated mark in 3D data
First, what differs between images and 3D data. C2PA specification 2.5 (October 2026) lists PNG, JPEG 1, MP4, PDF and other formats in its table of supported formats in Appendix A.1, and the sections of Appendix A define how to embed into each format. But 3MF, STL, OBJ, glTF/GLB and USD do not appear by name in that table.
Even so, one cannot say outright that C2PA cannot handle 3D data. The table has a generic row, “Other ZIP-based formats,” and the specification also defines how to handle OPC packages among ZIP-based formats. 3MF is an OPC format, but the specification does not name 3MF and does not say whether 3MF fits this path.
The tools are in the same position. The format table of c2pa-rs, the library c2patool uses (supported-formats.md at commit c13b05f), contains no 3D formats. When we actually tried to embed a manifest into 3MF and GLB with c2patool 0.28.4, both failed with exit code 1 (Error: embedding manifest / type is unsupported) and left a 0-byte output file. With -s, which writes the manifest to a separate file, both 3MF and GLB also printed Error: Unsupported file type and returned exit code 1 (the .c2pa was written anyway; see a later section).
The 3D formats themselves have no field for AI generation either. 3MF Core Specification 1.4.0 (3MF Consortium, February 2025) defines nine default metadata names (Title, Designer, Description, Copyright, LicenseTerms, Rating, CreationDate, ModificationDate, Application), and none of them expresses AI or generation provenance. Writing it into the model element’s metadata means using a custom namespace, which the specification allows. The specification also allows adding custom parts (separate files inside the ZIP) to the package, but we did not test that in this article.
glTF 2.0 has KHR_xmp_json_ld, an extension ratified by the Khronos Group that can embed any XMP namespace. However, the specification does not mention C2PA, and it is not a field designed for AI generation. And STL, according to the Library of Congress format description, has no standard for descriptive metadata. In other words, every format has somewhere you can write, but none has a place where it has been decided that “AI generation goes here.”
The background to this comparison is Article 50(2) of the EU AI Act. For providers of AI systems (including general-purpose AI systems) that generate synthetic audio, images, video or text and were placed on the market before 2 August 2026, the transitional period for complying with paragraph 2 (machine-readable marking and detection) ends on 2 December 2026. We covered the dates and framework in Update the Law Before You Memorise It: Japan AI Act, Copyright, and the EU AI Act (published 2026-10-04) and Where AI-generated labels and marks disappear in images and 3D data (published 2026-11-09).
For 3D, the list in the text of paragraph 2 does not contain the word 3D. The European Commission’s guidelines (2026-07-20, non-binding), on the other hand, place 3-D images, video and audio and virtual, augmented and mixed reality within the scope of paragraph 2, and state that the point clouds and 3D meshes used to create them do not need to be marked.
This article covers only the format specifications and the behavior of the tools. It does not address whether a reader bears obligations under Article 50, or which location would satisfy what the law requires. This article is not legal advice.
Subject and tools: a button cap built from AI-written CAD code
The subject is CAD code for a push-button cap that we had claude -p (model Claude Sonnet 5.5, called on 2026-10-26) write for How Far You Should 3D Print Appliance Spare Parts You Cannot Buy (published 2026-11-07). The code describes the shape with Python and manifold3d (the Python version of manifold), and we read its contents before running it when it was written. This time we only copied that code and ran it again; claude -p was not called again. The call record, including the prompt and cost, lives in that article.
Note that the shape was computed by manifold3d when the code ran; it is not a mesh that AI output directly. Methods in which AI outputs a mesh directly from text or images are covered in Text-to-3D Guide 2026: From Words to Printable Models (published 2026-07-06), and methods that describe the shape in code are covered in OpenSCAD LLM Workflow 2026: Writing Code CAD with ChatGPT and Claude (published 2026-07-18). The code in this article uses manifold3d, not OpenSCAD. Whichever method produces the model, the question of where in the resulting 3D data to write the mark stays the same.
The tools and versions used:
- Shape and conversion: shape written in Python with manifold3d 3.3.2 (Apache-2.0), converted to GLB and STL with trimesh 4.11.1 (MIT)
- Blender: 5.2.1 LTS for Windows. glTF import and export through the bundled add-on (Khronos glTF Blender I/O v5.2.40)
- C2PA: c2patool 0.28.4 (contentauth/c2patool, MIT OR Apache-2.0) and its built-in test key
As of 2026-11-02, the latest Blender is 5.2.3 LTS (2026-10-22), trimesh is at 5.2.0 (2026-10-24) and manifold3d at 3.5.4 (2026-09-25), so none of the versions we used is the latest. Between Blender 5.2.1 and 5.2.3, the three files responsible for extension handling on glTF import and for asset and extras on export had identical contents. That is a source comparison, though; we did not run 5.2.3, and we cannot say the latest versions give the same result.
Signing was done only with the test key built into c2patool. As a result, validation reports that the signer is not on a trust list (signingCredential.untrusted). No real signing certificate was used. Each condition was run once, and “survived” and “lost” below describe results for these versions and these operations only.
Getting an LLM to write usable CAD code depends a lot on how you frame the request. This book is a practical guide to prompting and building on top of language models.
Two locations inside 3MF: Core metadata and a custom namespace
3MF is a format that places an XML file, 3D/3dmodel.model, inside a ZIP. Under its model element you can list metadata as name and value pairs. The default names are the nine listed above, and a human-readable description can go in Description. In the slicer comparison test, we wrote “20 mm cube. Shape written by AI-generated code (test file).” in Description. This is location (2), Core metadata.
Values meant for machines, by contrast, are written in a custom namespace. The specification makes it a MUST that a name not among the defaults carries the prefix of a namespace declared on the model element. There is also a preserve attribute: for items where it is 1 or true, a producer that modifies and saves the 3MF is recommended (SHOULD) to keep the original value. Note that this is not a requirement (MUST). The specification also says a consumer that reads metadata with an unknown name should ignore it.
The test 3MF carries three items in this site’s test namespace (prefix swb, URI https://swiftwand.com/ns/provenance/2026). swb:AIGenerated and swb:Generator have preserve="1", and swb:Note is a control without preserve. This namespace exists for testing and is not a standard, so we do not recommend reusing it. The main lines we wrote are below (the swb prefix was declared on the model element).
<metadata name="Title">provenance test cube</metadata>
<metadata name="Description">20 mm cube. Shape written by AI-generated code (test file).</metadata>
<metadata name="swb:AIGenerated" preserve="1">true</metadata>
<metadata name="swb:Generator" preserve="1">claude-sonnet-5-5 + manifold3d</metadata>
<metadata name="swb:Note">no preserve flag on this one</metadata>Application was set to a test value that does not begin with a slicer name, because starting it with a slicer name changes the import path. The shape in this 3MF is a 20 mm cube (12 triangles), not the button cap. This difference is also noted in “What this test cannot tell us” below.
We did not run 3MF through Blender, because Blender 5.2 does not read or write 3MF out of the box. 3MF is missing from the manual’s list of import and export formats, and the import and export list in our 5.2.1 (bpy.ops.wm and import_scene / export_scene) covered OBJ, PLY, STL, USD, FBX, glTF, Alembic and Grease Pencil SVG and PDF, with no 3MF. Blender Extensions offers a third-party extension, “3MF Import/Export,” but we did not install it for this experiment.
A .c2pa sidecar: a separate file for formats that cannot embed a manifest
The third location is not inside the file but beside it. C2PA 2.5 accepts placing the manifest outside the file as a way of indicating provenance. One of the reasons it gives is a format with no defined embedding method, where embedding is technically impossible. In the specification’s example, an external manifest is required for formats such as BMP that cannot embed arbitrary data. The media type for delivering an external Manifest Store is application/c2pa.
Placing it beside the file does not guarantee it will be read, however. According to the specification, when a validator cannot find a manifest, it is recommended (SHOULD) to look in the same location for the file name plus .c2pa, and if that is not found, looking for the name with its extension replaced by .c2pa is optional (MAY). The specification’s examples are image.jpg.c2pa, then image.c2pa. The specification uses SHOULD and MAY in the RFC 2119 sense, so neither lookup is mandatory.
What ties a file to a manifest is a hash called a hard binding, and a manifest may contain at most one hard binding. Leaving aside the deprecated c2pa.hash.bmff.v2, the specification accepts five labels as hard bindings: c2pa.hash.data, c2pa.hash.boxes, c2pa.hash.bmff.v3, c2pa.hash.collection.data and c2pa.hash.segment. Of these, the byte-range hash c2pa.hash.data is described by the specification as usable for any type of asset. That is the one a .c2pa beside an unsupported format can use. Note that c2pa.hash.multi-asset is not a hard binding.
ZIP-based formats take a different route. The specification hashes ZIP-based formats with c2pa.hash.collection.data and places the manifest inside the ZIP at META-INF/content_credential.c2pa. The examples it gives are EPUB, Office Open XML, Open Document and OpenXPS; 3MF is not among them. For OPC packages, the specification also warns that a consumer treats a part with no declared content type as corrupt and may remove the manifest while repairing the package. Location (3) in this article is a separate file outside the ZIP, which is a different thing from this in-ZIP method.
In c2patool, adding -s (--sidecar) writes the manifest to a separate file with the same name as the output and the extension .c2pa, leaving the original file unchanged. Validation points to that separate file with --external-manifest. Following the examples in the documentation, the commands look like this.
c2patool <source file> -s -m <manifest JSON> -o <output file>
c2patool <source file> --external-manifest <written .c2pa>The c2patool README describes it as a tool for working with files in supported formats, and the documentation does not say whether -s works with unsupported formats. So we tried it; the results are in a later section. How to express AI generation in a C2PA manifest is covered in Adding and reading provenance on AI images with C2PA (published 2026-11-10).
KHR_xmp_json_ld in GLB: a ratified extension for putting XMP into glTF
The fourth location is inside the GLB. KHR_xmp_json_ld is a glTF 2.0 extension ratified by the Khronos Group, and it replaced the earlier KHR_xmp. XMP sits in the root extension as an array of packets (packets). packets must contain at least one entry, and each packet requires @context. Objects of seven kinds (asset, scene, node, mesh, material, image and animation) then reference a packet by its index in packet.
The scope of a reference is defined too. XMP referenced from asset applies to the whole file, and if an object in an array, such as a node or mesh, has its own XMP, that takes precedence. Metadata does not affect appearance or rendering.
An extension that is used must be listed in extensionsUsed (a MUST in the specification). extensionsRequired, in contrast, is for extensions needed to load or render the asset, meaning ones that a loader without support cannot read. Since XMP does not affect rendering, we did not put it in extensionsRequired in this experiment. Reading the Blender 5.2.1 source, import stops when extensionsRequired contains an extension that is not on Blender’s supported list, and continues without stopping when the extension is only in extensionsUsed (we did not run the extensionsRequired condition). In this run, Blender did import the file without stopping.
We made the marked GLB by editing the JSON part of a GLB exported by trimesh. We added three things:
asset.extensions.KHR_xmp_json_ld(referencing packet 0), and the packet placed in the top-levelextensions.KHR_xmp_json_ld.packets(dc:creatorandIptc4xmpExt:DigitalSourceType){"swb_ai_generated": true}inasset.extrasswiftwand-d6inasset.generator
The JSON looks like this (the dc:creator value and the geometry are omitted).
{
"asset": {
"version": "2.0",
"generator": "swiftwand-d6",
"extras": {"swb_ai_generated": true},
"extensions": {"KHR_xmp_json_ld": {"packet": 0}}
},
"extensionsUsed": ["KHR_xmp_json_ld"],
"extensions": {
"KHR_xmp_json_ld": {
"packets": [{
"@context": {
"dc": "http://purl.org/dc/elements/1.1/",
"Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/"
},
"dc:creator": {"@list": ["…"]},
"Iptc4xmpExt:DigitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
}]
}
}
}Of these, asset.generator and asset.extras are fields we do not recommend as locations. By the specification’s definition, asset.generator holds the name of the tool that generated the glTF model. Reading the sources, Blender writes “Khronos glTF Blender I/O v” followed by its version, and trimesh writes its own repository URL, as fixed values on every export. extras holds application-specific data, and the Blender manual also notes that, unlike glTF extensions, extras have no defined namespace. We included these two fields to check whether fields that look like locations survive export.
asset.copyright, in the same asset, is a copyright notice for crediting the author, not a field for labeling AI generation, so we did not use it.
Now Blender’s side. The list of supported extensions in the Blender 5.2 manual (18 for import, 17 for export) does not include KHR_xmp_json_ld. Blender does not drop every glTF extension, though; it handles the ones on that list. extras become custom properties on import and are written on export only when “Custom Properties” (export_extras) is enabled, which is off by default.
The developer’s repository, glTF-Blender-IO, has an issue titled “glTF export removes XMP extension” (#2149). It was reported in March 2024 against Blender 4.0.2 and is still open as of 2026-11-02. The specification, for its part, recommends (SHOULD) copying the XMP when an object is copied into another glTF, but this is not a requirement (MUST). A tool that does not copy it cannot be called non-compliant.
For the IPTC vocabulary we chose trainedAlgorithmicMedia: a library computed the shape, but a trained model wrote the steps
For the “AI-generated” value inside the XMP, we used IPTC’s Iptc4xmpExt:DigitalSourceType. This is not something the glTF specification recommends, however. The KHR_xmp_json_ld README has a table of recommended prefixes and URIs listing nine of them (dc, xmp, xmpGImg, xmpMM, stRef, stEvt, stVer, xmpRights and rdf), and IPTC is not in it. Using IPTC is this site’s choice, based on the rule that any XMP namespace can be embedded.
Moreover, the IPTC standard here is the Photo Metadata Standard (2025.1), a standard for photo metadata with no rule for applying it to 3D data. In other words, we are borrowing an image vocabulary for 3D data.
There were two candidate terms. trainedAlgorithmicMedia is “Created using Generative AI,” defined as media created algorithmically using an AI model trained on sampled content. algorithmicMedia, by contrast, is “Pure algorithmic media,” defined as media created purely by an algorithm not based on any training data (for example, an image created by software using a mathematical formula).
The shape here was computed by manifold3d, not by a trained model. But the code, which is the procedure for that shape, was written by a trained model (Claude Sonnet 5.5). We therefore judged that it does not count as a pure algorithm without training data (algorithmicMedia), and chose trainedAlgorithmicMedia.
IPTC does not, however, decide which term applies to a shape that a separate library computed from CAD code written by AI. This is this site’s judgment, not a recommendation of the correct answer. The value was written in the form http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia.
STL has no standard location: nothing defines how the 80-byte header is read
On STL, the Library of Congress describes a binary STL as an 80-character header that can be used for annotation, a 32-bit little-endian triangle count, and a sequence of 50 bytes per facet. The UK National Archives’ PRONOM likewise describes an 80-byte header with no defined structure, so the two public institutions agree. The Library of Congress then states that binary STL has no standard for descriptive metadata, adding that the first 80 characters are sometimes used to describe the model.
ASCII STL is in much the same position. You can write the object’s name on the solid line, and there is no other metadata. Color is not in the original specification either; there are attempts to add color through binary extensions, but they sit outside the standard. So it is not that nothing can be written in an STL: you can put text in those 80 bytes, but there is no rule for how readers should interpret it.
On top of that, tools do not use the field. Reading the sources, Blender 5.2.1’s STL exporter fills the binary header with zeros and, in ASCII, leaves the name after solid empty. trimesh 4.11.1 also writes the binary header as zeros.
In practice, when we converted the marked GLB to STL with trimesh 4.11.1, the 80-byte header was empty (zero-filled), and the file contained no AI mark string. The marks put in the GLB have nowhere to go once the model becomes STL.
Measurement: running GLB through Blender 5.2.1 and trimesh 4.11.1, the marks on asset did not survive
We launched Blender 5.2.1 with --factory-startup, imported the file with import_scene.gltf and exported it to GLB with export_scene.gltf. Export was done two ways: with default settings and with export_extras=True (the setting that enables Custom Properties). In trimesh 4.11.1, we imported the file and exported it straight back to GLB. Import was done two ways: as a single mesh (force="mesh") and as a scene (no force). The table shows what we found in the output JSON (run on 2026-11-02, once each).
| Path | asset.generator | XMP reference from asset | DigitalSourceType in XMP | extensionsUsed | asset.extras |
|---|---|---|---|---|---|
| Input (marked GLB) | swiftwand-d6 | Present (packet 0) | trainedAlgorithmicMedia | KHR_xmp_json_ld | swb_ai_generated is true |
| Blender 5.2.1 (default settings) | Khronos glTF Blender I/O v5.2.40 | None | None | None | None |
| Blender 5.2.1 (export_extras=True) | Khronos glTF Blender I/O v5.2.40 | None | None | None | None |
| trimesh 4.11.1 (imported as a single mesh) | trimesh repository URL | None | None | None | None |
| trimesh 4.11.1 (imported as a scene) | trimesh repository URL | None (reference lost) | Remained in the body of the top-level packet (referenced from nowhere) | KHR_xmp_json_ld | None |
Blender did not stop on import, because an extension listed only in extensionsUsed is skipped. But the exported GLB had no KHR_xmp_json_ld, no asset.extras and no extensionsUsed field, and the generator was replaced with the Blender I/O value. Even with export_extras=True, asset.extras did not survive.
This matches our reading of the source. Blender’s exporter is built not to write asset extensions or extras, and the importer has no code that reads asset extras. What issue #2149 reported, the XMP extension disappearing on export, also happened in 5.2.1 in this run.
With trimesh 4.11.1, the outcome depended on how the file was imported. Imported as a single mesh, none of the three survived, and the generator became trimesh’s repository URL. Imported as a scene, the top-level extensions.KHR_xmp_json_ld.packets (the packet body containing DigitalSourceType and dc:creator) and extensionsUsed remained in the exported GLB. But the reference in asset.extensions.KHR_xmp_json_ld (packet 0) was gone, leaving the packet tied to no object. asset.extras was lost, and the generator became trimesh’s URL.
Both outcomes are consistent with the source. trimesh 4.11.1’s GLB importer uses asset only to check the version, and the generator it writes on export is a fixed value, so references and extras placed on asset do not survive. There is also code that puts top-level extensions into the scene’s metadata and writes them back on export, which fits the packet body surviving when the file is imported as a scene.
A packet whose reference is gone, however, carries no reference saying which object the XMP belongs to, even if the strings remain in the file. As a mark referenced from asset so that it “applies to the whole file,” it can no longer be read. In short, in Blender 5.2.1 and in trimesh 4.11.1 under either import mode, the marks attached to asset did not survive a single export. What the two tools share is that, when they open and export a GLB, they rebuild asset with their own values.
To repeat the GLB round trip yourself, it helps to know your way around importing and exporting in Blender first.
Measurement: the .c2pa was written, but validation failed against re-saved files
We used c2patool 0.28.4’s -s on the marked GLB and on the test 3MF. The manifest was written for 3D data: title set to the target file name (base.glb, base.3mf), the digitalSourceType on c2pa.created in c2pa.actions set to trainedAlgorithmicMedia, and softwareAgent set to claude-sonnet-5-5 + manifold3d (test key, no timestamp).
For both formats, c2patool printed Error: Unsupported file type and returned exit code 1, yet a .c2pa with the same name as the output had been written (the 3MF’s .c2pa was 3,547 bytes). Judged by the exit code alone it looks like a failure, but the separate file exists. Conversely, the fact that a separate file was produced does not mean c2patool supports these formats.
Next, we validated the written .c2pa with --external-manifest.
| Validated against | Result (validation_state) | Codes recorded |
|---|---|---|
| Original GLB | Valid | signingCredential.untrusted only |
| Original 3MF | Valid | signingCredential.untrusted only |
| GLB exported by Blender 5.2.1 (default settings) | Invalid | signingCredential.untrusted, assertion.dataHash.mismatch |
| GLB exported by trimesh 4.11.1 (imported as a single mesh) | Invalid | signingCredential.untrusted, assertion.dataHash.mismatch |
| 3MF re-saved by PrusaSlicer 2.9.6 | Invalid | signingCredential.untrusted, assertion.dataHash.mismatch |
At first we ran the same procedure with a test manifest reused from the image tests, and the validation conclusions were the same. The table shows the results with the manifest made for 3D data.
Valid against the original file means the signature and tamper checks passed. Because the signer is not on a trust list, it is not Trusted. And Valid does not prove the file was made with AI in the first place: what a manifest says is the creator’s declaration.
Against the re-saved files, on the other hand, the data hash did not match and the result was Invalid. A .c2pa is bound to the byte sequence of the entire file, so if re-exporting with a tool changes the bytes, validation fails. The specification says that when a hard binding does not match, a validator cannot tell whether the asset changed or a different manifest was found, and so rejects it. In other words, the separate file did not disappear; it simply no longer matches the main file.
On the scope of validation: we did not record SHA-256 hashes of the files we validated against, and match or mismatch was judged by c2patool’s hash check.
The two 3MF locations fared differently in slicers
What follows summarizes the measurements in Does an “AI-generated” entry in 3MF metadata survive slicers? (published 2026-11-13). The versions as of 2026-11-02 were PrusaSlicer 2.9.6 (2026-06-25, stable), PrusaSlicer 3.0.0-alpha12 (2026-09-21, alpha), Bambu Studio 2.8.4 Public Beta (2.8.4.57, 2026-09-22; the latest official release is 2.8.2.61) and OrcaSlicer 2.5.0-rc (2026-11-01, release candidate; no final release yet, and the latest stable version is v2.4.2).
In every case the operation was simply loading the test 3MF through the CLI and writing it with --export-3mf; nothing was sliced. For PrusaSlicer and OrcaSlicer, --datadir pointed to an empty folder. Each slicer was run once, and saving from the GUI was not measured.
| Slicer | Core metadata (location 2) | Three items in the custom namespace (location 1) |
|---|---|---|
| PrusaSlicer 2.9.6 | Title and Description replaced with the output file name (roundtrip); Designer, Copyright and LicenseTerms became empty | All three lost (including the two with preserve="1") |
| PrusaSlicer 3.0.0-alpha12 | Only CreationDate, Application and ModificationDate were written | All three lost |
| Bambu Studio 2.8.4.57 | Title through LicenseTerms survived | Strings survived; preserve and the xmlns:swb declaration were lost |
| OrcaSlicer 2.5.0-rc | Title through LicenseTerms survived; CreationDate became empty | Strings survived; preserve and the xmlns:swb declaration were lost |
Application was replaced with the exporting slicer’s value in every case. PrusaSlicer wrote PrusaSlicer-2.9.6 and PrusaSlicer-3.0.0-alpha12, and Bambu Studio wrote BambuStudio-02.08.04.57. OrcaSlicer wrote BambuStudio-02.08.01.55 and added a separate OrcaSlicer item (value 2.5.0-rc). Comments in the source give compatibility with Bambu Studio as the reason for the Application value, and “PRIVACY” as the reason for emptying the date. Because the input’s CreationDate was the same date as the run, we cannot tell whether it was replaced in the slicers other than OrcaSlicer.
The Bambu Studio and OrcaSlicer results look like survival but need care. The swb: names and value strings remained, but the output had no xmlns:swb declaring the swb prefix, so a reader that matches namespaces by URI cannot read them as “AI-generated” items. For names that are not defaults, the specification says “it MUST be prefixed with the namespace name of an XML namespace declaration,” and an undeclared prefix does not meet this MUST. Each of the two outputs had three such names.
The control item swb:Note, which had no preserve, also kept its string, so these two slicers can be read as passing items through regardless of preserve. The preserve attribute itself did not survive even once in any slicer’s output.
For PrusaSlicer, metadata being replaced on re-save was reported as issue #13920 on 2025-01-06 against 2.9.0. A bot closed the issue automatically on 2026-09-29 for inactivity. GitHub shows “closed this as completed,” but the issue history contains no fix commit and no reply from a developer. The report concerned Save As in the GUI.
If you want to print the re-saved 3MF and check the part, a printer that pairs with Bambu Studio lets you see the file handling and the result together.
Comparison table by location and path
Here are the results so far in one table of locations and paths. “Not measured” means that combination was not run. Every cell reflects one run per condition with the versions stated above.
| Location | Blender 5.2.1 (import GLB, export GLB) | trimesh 4.11.1 (same operation) | Conversion to STL (trimesh 4.11.1) | Re-save in a slicer (CLI) |
|---|---|---|---|---|
| (1) 3MF custom namespace | Not measured (no built-in 3MF import or export) | Not measured | Not measured (STL has no standard location) | Lost in both PrusaSlicer versions. In Bambu Studio and OrcaSlicer the strings survived, but the declaration and preserve were lost |
| (2) 3MF Core metadata | Not measured (same reason) | Not measured | Not measured (same reason) | PrusaSlicer 2.9.6 replaced or emptied it; 3.0.0-alpha12 wrote only three items. Bambu Studio and OrcaSlicer kept Title through LicenseTerms |
| (3) .c2pa sidecar | The separate file was untouched, but validation failed against the GLB exported with default settings | Validation failed against the GLB imported as a single mesh and exported | Not measured | Validation failed against the 3MF re-saved by PrusaSlicer 2.9.6 (other slicers not measured) |
| (4) GLB KHR_xmp_json_ld (referenced from asset) | Lost (with default settings and with export_extras=True) | Lost when imported as a single mesh. Imported as a scene, the packet body survived but the reference from asset was lost | Header empty; no mark string present | Not measured |
| For reference: GLB asset.extras | Lost (same) | Lost (in either import mode) | Not checked (header empty) | Not measured |
| For reference: GLB asset.generator | Replaced with the Blender I/O value | Replaced with the trimesh URL (in either import mode) | Not checked (header empty) | Not measured |
Reading across, every location has at least one ordinary path where it did not survive or where validation failed. Reading down, what decides survival appears to be how each tool’s exporter is built rather than the kind of location. Blender 5.2.1 and trimesh 4.11.1 rebuilt asset with their own values, and PrusaSlicer rewrote the metadata with its own values (3.0.0-alpha12 wrote only three items). Bambu Studio and OrcaSlicer, according to their source, write back the name and value pairs they read, and in the measurement the strings did survive, but the declaration and preserve did not.
The .c2pa alone behaves differently. The separate file itself is left untouched by every tool, but once the main file is re-exported, validation fails. Put another way, a .c2pa is a mechanism for confirming that a file has not changed since it was distributed, not a mechanism for carrying a mark across re-saves.
Which location should a distributor choose?
This section describes ways of choosing that follow from the measurements above. It does not address which choice satisfies the law. It is only a way of comparing, within the scope of this run, how likely the mark is to be readable when the file reaches the person who receives it.
Choose by the tool the recipient will open the file in
First, consider how the recipient will handle the file. If they will open a 3MF in a slicer and save it again, Bambu Studio and OrcaSlicer kept the Core metadata from Title through LicenseTerms, while PrusaSlicer replaced it or left it empty. The custom namespace did not survive in any slicer in a form readable as a namespace. In this run, the only files in which the custom-namespace mark could be read together with its namespace were the files before re-saving.
If the recipient will display or redistribute the GLB as it is, KHR_xmp_json_ld is a ratified extension and makes sense as a location. In this run, however, a single re-export in Blender 5.2.1 lost the XMP entirely, and in trimesh 4.11.1 the packet lost its reference from asset even when imported as a scene. For a file that is expected to be edited, you need your own procedure for re-attaching the XMP after every export. Whether other glTF tools and viewers read this extension was not checked in this article.
If you distribute STL, there is no location inside the file with a defined way of being read. If you want the mark to be machine-readable, you will have to distribute 3MF or GLB, or write it outside the file (in the description on the download page or in an included document).
Place both human-readable text and machine-readable values
Next, if you put an AI-generated mark in 3D data, it is better not to rely on a single location, because in this run the fields that failed to survive differed from tool to tool. With 3MF, for example, write human-readable text in Description and a machine-readable value in a custom namespace. When re-saved in Bambu Studio and OrcaSlicer, the custom namespace lost its declaration but the Description text survived. In both PrusaSlicer versions, though, neither field kept its original value. Writing in both places guards against the different ways a mark gets lost, and even that is not enough.
Keep the original file and the .c2pa together as a pair
A .c2pa is best kept as a pair with the file as it was distributed. In this run, the original file was Valid and the re-saved files were Invalid. Validation can therefore tell whether the file in hand is still the byte sequence that was distributed. Whether an Invalid result means a change or a different manifest, however, cannot be told apart even under the specification. And since a validator looking for a separate file is only recommended (SHOULD) or optional (MAY) under the specification, not mandatory, the recipient’s tool may never look at the .c2pa. We signed with a test key, so the signer was shown as not on a trust list. Results with a real certificate are outside the scope of this article.
Fields that look like locations but should not be used
Finally, the fields not to use. asset.generator holds the name of the exporting tool, and tools overwrote it with their own value on every export. asset.copyright is for crediting the author, not for labeling AI generation. extras holds application-specific values with no defined namespace. Also, in the Blender 5.2.1 source, putting the XMP extension in extensionsRequired makes import stop, so put it only in extensionsUsed.
Whichever location you choose, the mark is the creator’s declaration. A file whose mark cannot be read is not thereby shown to be free of AI, and the presence of a mark does not show that the content is correct.
What this test cannot tell us
The results in this article have the following limits.
- Each condition was run once, and the results are limited to these versions: Blender 5.2.1 (latest 5.2.3), trimesh 4.11.1 (latest 5.2.0), manifold3d 3.3.2 (latest 3.5.4), c2patool 0.28.4, and the slicer versions in the section above. Other versions may not give the same results
- Handling 3MF in Blender requires a third-party extension, which we did not try. There are no results for the two 3MF locations through Blender
- GLB through slicers and 3MF through trimesh were not measured
- Only XMP referenced from asset was tested. XMP referenced from a node or mesh, and extras on a node or mesh, were not tested
- We checked only whether values were written to the output file. Values loaded into a tool’s internals, such as Blender custom properties, were not counted as survived
- trimesh was tested in only two import modes: as a single mesh and as a scene
.c2pavalidation was done only against the GLBs exported by Blender (default settings) and trimesh (imported as a single mesh), and the 3MF re-saved by PrusaSlicer 2.9.6. We did not validate against other slicers’ output, and we did not use a real signing certificate- The 3MF results come from a 20 mm cube test file, not the button cap. Only CLI saves were measured; GUI saves and OrcaSlicer’s Publish 3MF were not
- Whether other glTF tools or viewers read or keep
KHR_xmp_json_ldwas not checked - This site does not judge whether STL or 3MF falls under the “3D meshes” of the European Commission’s guidelines. This article is not legal advice
Summary: after putting an AI-generated mark in 3D data, check whether it survives each tool it passes through
There is not yet a standard place for an AI-generated mark in 3D data. The C2PA 2.5 table of supported formats names no 3D format; c2patool 0.28.4 refused to embed into 3MF and GLB, and returned exit code 1 for both even with -s. The 3MF Core default names include no AI field, glTF’s KHR_xmp_json_ld is a general-purpose extension that accepts any namespace, and STL has no standard for descriptive metadata.
When we ran the four locations through tools, every one of them, in this run, either failed to survive or failed validation somewhere in ordinary use. All three GLB marks vanished on export from Blender 5.2.1, and in trimesh 4.11.1 the XMP packet lost its reference from asset even when the file was imported as a scene. The 3MF custom namespace was lost in PrusaSlicer and lost its declaration in Bambu Studio and OrcaSlicer. The sidecar .c2pa remains, but validation fails against re-saved files.
If you test this yourself, do it in this order:
- Decide which format you will distribute. STL has no in-file location with a defined way of being read, so use 3MF or GLB, or write the mark outside the file
- Put in both human-readable text (such as the 3MF Description) and machine-readable values (a custom namespace, XMP in GLB). List the XMP extension in
extensionsUsed, not inextensionsRequired - Import and export the file once in the tools the recipient is likely to use, and check for yourself whether the mark remains in the output file
- Keep the file as distributed and its
.c2patogether as a pair so you can validate it later
How marks on images survive is covered in Where AI-generated labels and marks disappear in images and 3D data (published 2026-11-09). With an AI-generated mark in 3D data, too, placing it is not the end of the job. You need to check, for each tool the file passes through, whether the mark survives.
Sources
- KHR_xmp_json_ld (glTF extension specification, commit 59ecbdb)
- List of glTF extensions (commit 59ecbdb)
- glTF 2.0 Specification (Khronos Registry)
- Blender 5.2 LTS release page
- Blender download page
- Blender 5.2 Manual: glTF 2.0
- Blender 5.2 Manual: Importing & Exporting Files
- Blender 5.2 Manual: STL
- Blender v5.2.1 glTF exporter (exporter.py)
- Blender v5.2.1 glTF importer (gltf2_io_gltf.py)
- Blender v5.2.1 STL exporter (stl_export_writer.cc)
- glTF-Blender-IO issue #2149 “glTF export removes XMP extension”
- Blender Extensions: 3MF Import/Export
- Library of Congress: STL (STereoLithography) File Format, Binary
- Library of Congress: STL (STereoLithography) File Format, ASCII
- The National Archives PRONOM: fmt/865
- C2PA Technical Specification 2.5 (CC BY 4.0)
- c2pa-rs supported formats table (commit c13b05f)
- c2patool docs/usage.md (v0.28.4)
- c2patool README (v0.28.4)
- c2patool release v0.28.4
- IPTC Photo Metadata Standard 2025.1
- IPTC NewsCodes: Digital Source Type
- trimesh glTF import and export (4.11.1)
- trimesh STL import and export (4.11.1)
- trimesh LICENSE.md (4.11.1)
- trimesh release 4.11.1
- trimesh release 5.2.0
- manifold LICENSE (v3.3.2)
- manifold release v3.3.2
- manifold release v3.5.4
- 3MF Core Specification 1.4.0 (commit 997b385)
- PrusaSlicer issue #13920
- PrusaSlicer 2.9.6 release
- PrusaSlicer 3.0.0-alpha12 release
- Bambu Studio 2.8.4 Public Beta release
- OrcaSlicer v2.5.0-rc release
- OrcaSlicer bbs_3mf.cpp (v2.5.0-rc)
- Regulation (EU) 2024/1689 (AI Act, EUR-Lex)
- Regulation (EU) 2026/1744 (EUR-Lex)
- European Commission: guidelines on Article 50 (PDF)





