Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/gallery/assets/crate-stack-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/depsgraph-export-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/contact-sheets/crate-stack-contact-sheet.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
448 changes: 400 additions & 48 deletions docs/gallery/crate-stack/index.html

Large diffs are not rendered by default.

34 changes: 28 additions & 6 deletions docs/gallery/depsgraph-export/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
<meta property="og:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/depsgraph-export-hero.webp" />
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="720" />
<meta property="og:image:alt" content="A teal cube beside a slightly faceted teal sphere on a dark studio floor." />
<meta property="og:image:alt" content="A teal cube outlined by its twelve edges beside a faceted teal sphere covered in a dense pale wire grid, on a dark studio floor." />
<meta property="og:site_name" content="Blender Developer Tools" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="depsgraph-export — Examples — Blender Developer Tools" />
<meta name="twitter:description" content="The depsgraph lifetime contract — evaluated_get().to_mesh() paired with to_mesh_clear() — measured against an OBJ export of the same object." />
<meta name="twitter:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/depsgraph-export-hero.webp" />
<meta name="twitter:image:alt" content="A teal cube beside a slightly faceted teal sphere on a dark studio floor." />
<meta name="twitter:image:alt" content="A teal cube outlined by its twelve edges beside a faceted teal sphere covered in a dense pale wire grid, on a dark studio floor." />
<style>
/* fonts are deployed by the landing build (docs/fonts/) */
@font-face { font-family: 'Barlow Condensed'; font-weight: 600; font-display: swap;
Expand Down Expand Up @@ -268,8 +268,8 @@ <h1>depsgraph-export</h1>
<p>The depsgraph lifetime contract — evaluated_get().to_mesh() paired with to_mesh_clear() — measured against an OBJ export of the same object.</p>
</header>
<main id="main">
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A teal cube beside a slightly faceted teal sphere on a dark studio floor.">
<img src="../assets/depsgraph-export-hero.webp" alt="A teal cube beside a slightly faceted teal sphere on a dark studio floor." width="1280" height="720" />
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A teal cube outlined by its twelve edges beside a faceted teal sphere covered in a dense pale wire grid, on a dark studio floor.">
<img src="../assets/depsgraph-export-hero.webp" alt="A teal cube outlined by its twelve edges beside a faceted teal sphere covered in a dense pale wire grid, on a dark studio floor." width="1280" height="720" />
</button>
<p class="zoom-hint">Rendered headless by the example itself — click to zoom.</p>
<div class="callout"><span class="tag">witnesses</span> Exports ship evaluated geometry: the exported vertex count equals the subsurf-applied count and is strictly greater than the base mesh.</div>
Expand Down Expand Up @@ -431,6 +431,28 @@ <h2>Source</h2>
<span class="k">for</span> poly <span class="k">in</span> obj.data.polygons:
poly.use_smooth = <span class="k">True</span>

<span class="c"># Edge cages over both: the check compares vertex counts, and two teal</span>
<span class="c"># silhouettes alone read as &quot;a cube and a ball&quot;. The base cage shows the</span>
<span class="c"># 8 verts the datablock holds; the evaluated cage is the same SUBSURF</span>
<span class="c"># followed by WIREFRAME, so it draws exactly the edges the OBJ ships.</span>
<span class="c"># Render-path scaffolding only: separate objects, added after check().</span>
cage_mat = principled(<span class="s">&quot;Cage&quot;</span>, (<span class="n">0.92</span>, <span class="n">0.80</span>, <span class="n">0.56</span>, <span class="n">1.0</span>), <span class="n">0.0</span>, <span class="n">0.35</span>)
cages = []
<span class="k">for</span> src, subsurf <span class="k">in</span> ((base_obj, <span class="k">False</span>), (obj, <span class="k">True</span>)):
cage = bpy.data.objects.new(src.name + <span class="s">&quot;Cage&quot;</span>, obj.data.copy())
cage.data.materials.clear()
cage.data.materials.append(cage_mat)
cage.location = src.location
cage.rotation_euler = src.rotation_euler
<span class="k">if</span> subsurf:
cage.modifiers.new(<span class="s">&quot;ss&quot;</span>, <span class="s">&#x27;SUBSURF&#x27;</span>).levels = <span class="n">2</span>
wire = cage.modifiers.new(<span class="s">&quot;cage&quot;</span>, <span class="s">&#x27;WIREFRAME&#x27;</span>)
wire.thickness = <span class="n">0.035</span> <span class="k">if</span> <span class="k">not</span> subsurf <span class="k">else</span> <span class="n">0.018</span>
wire.offset = <span class="n">1.0</span> <span class="c"># grow outward so the lines sit on the surface</span>
wire.use_even_offset = <span class="k">True</span>
bpy.context.collection.objects.link(cage)
cages.append(cage)

floor_me = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
bm = bmesh.new()
<span class="k">try</span>:
Expand Down Expand Up @@ -505,7 +527,7 @@ <h2>Source</h2>
fcode = gallery_framing.check_framing(
scene, cam,
hero=[base_obj, obj],
elements=[base_obj, obj],
elements=[base_obj, obj] + cages,
stage=[floor, wall],
)
<span class="k">if</span> fcode:
Expand Down Expand Up @@ -554,7 +576,7 @@ <h2>Source</h2>
</main>
<dialog class="lightbox" id="lightbox" aria-label="Full-size render">
<button class="lightbox-close" type="button" autofocus>Close</button>
<img src="../assets/depsgraph-export-hero.webp" alt="A teal cube beside a slightly faceted teal sphere on a dark studio floor." />
<img src="../assets/depsgraph-export-hero.webp" alt="A teal cube outlined by its twelve edges beside a faceted teal sphere covered in a dense pale wire grid, on a dark studio floor." />
</dialog>
<footer>
<div class="statusbar">
Expand Down
4 changes: 2 additions & 2 deletions docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ <h2><a href="gn-sdf-remesh/">gn-sdf-remesh</a></h2>
</article>
<article class="card" data-tags="depsgraph export">
<a class="card-media" href="depsgraph-export/" tabindex="-1" aria-hidden="true">
<img src="assets/depsgraph-export-hero.webp" alt="A teal cube beside a slightly faceted teal sphere on a dark studio floor." loading="lazy" decoding="async" />
<img src="assets/depsgraph-export-hero.webp" alt="A teal cube outlined by its twelve edges beside a faceted teal sphere covered in a dense pale wire grid, on a dark studio floor." loading="lazy" decoding="async" />
</a>
<div class="card-body">
<h2><a href="depsgraph-export/">depsgraph-export</a></h2>
Expand Down Expand Up @@ -1190,7 +1190,7 @@ <h2><a href="hay-bale/">hay-bale</a></h2>
</article>
<article class="card" data-tags="mesh export showcase">
<a class="card-media" href="crate-stack/" tabindex="-1" aria-hidden="true">
<img src="assets/crate-stack-hero.webp" alt="Three wooden shipping crates stacked, each turned slightly differently." loading="lazy" decoding="async" />
<img src="assets/crate-stack-hero.webp" alt="Three grained wooden shipping crates stacked, each turned slightly differently, with nailed iron corner straps." loading="lazy" decoding="async" />
</a>
<div class="card-body">
<h2><a href="crate-stack/">crate-stack</a></h2>
Expand Down
24 changes: 23 additions & 1 deletion examples/depsgraph-export/depsgraph_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,28 @@ def render_still(obj, path, engine):
for poly in obj.data.polygons:
poly.use_smooth = True

# Edge cages over both: the check compares vertex counts, and two teal
# silhouettes alone read as "a cube and a ball". The base cage shows the
# 8 verts the datablock holds; the evaluated cage is the same SUBSURF
# followed by WIREFRAME, so it draws exactly the edges the OBJ ships.
# Render-path scaffolding only: separate objects, added after check().
cage_mat = principled("Cage", (0.92, 0.80, 0.56, 1.0), 0.0, 0.35)
cages = []
for src, subsurf in ((base_obj, False), (obj, True)):
cage = bpy.data.objects.new(src.name + "Cage", obj.data.copy())
cage.data.materials.clear()
cage.data.materials.append(cage_mat)
cage.location = src.location
cage.rotation_euler = src.rotation_euler
if subsurf:
cage.modifiers.new("ss", 'SUBSURF').levels = 2
wire = cage.modifiers.new("cage", 'WIREFRAME')
wire.thickness = 0.035 if not subsurf else 0.018
wire.offset = 1.0 # grow outward so the lines sit on the surface
wire.use_even_offset = True
bpy.context.collection.objects.link(cage)
cages.append(cage)

floor_me = bpy.data.meshes.new("Floor")
bm = bmesh.new()
try:
Expand Down Expand Up @@ -197,7 +219,7 @@ def light(name, loc, energy, size, col, rot):
fcode = gallery_framing.check_framing(
scene, cam,
hero=[base_obj, obj],
elements=[base_obj, obj],
elements=[base_obj, obj] + cages,
stage=[floor, wall],
)
if fcode:
Expand Down
Binary file modified examples/depsgraph-export/preview.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/gallery.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"name": "depsgraph-export",
"dir": "examples/depsgraph-export",
"teaches": "The depsgraph lifetime contract — evaluated_get().to_mesh() paired with to_mesh_clear() — measured against an OBJ export of the same object.",
"alt": "A teal cube beside a slightly faceted teal sphere on a dark studio floor.",
"alt": "A teal cube outlined by its twelve edges beside a faceted teal sphere covered in a dense pale wire grid, on a dark studio floor.",
"witnessesFix": "Exports ship evaluated geometry: the exported vertex count equals the subsurf-applied count and is strictly greater than the base mesh.",
"hero": "docs/gallery/assets/depsgraph-export-hero.webp",
"preview": "examples/depsgraph-export/preview.webp",
Expand Down
65 changes: 55 additions & 10 deletions examples/unapplied-scale-gltf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,67 @@ node has no scale) and neighbor of
[`prop-origin-transform`](../prop-origin-transform/) (bake to `(1,1,1)`
in Blender).

**Why this pathology:** `export_apply` RNA is "Apply modifiers … to mesh
objects". AI code treats it as "apply object transforms". Unapplied
scale lands on the glTF node, Y-up permuted; POSITION stays local.
## The contract

`bpy.ops.export_scene.gltf(export_apply=True)` applies **modifiers**, not
object transforms. Its RNA description, identical on 4.5.11, 5.1.2 and
5.2.1, reads:

> Apply modifiers (excluding Armatures) to mesh objects -WARNING: prevents
> exporting shape keys

An object whose `Object.scale` was never applied therefore exports with
that scale on the glTF **node** (`nodes[i].scale`), permuted to Y-up, and
with its **POSITION** accessor still holding the unscaled local
coordinates. `export_apply` has no effect on either: exporting the same
object with `export_apply=False` writes the identical node scale and
POSITION range.

**Who hits this:** a pipeline that sets `export_apply=True` believing it
bakes transforms, then ships a prop whose root carries a non-uniform
scale. In the engine the mesh looks the right size, but any child
rotated under that node skews, physics colliders sized from the mesh
bounds come out at the local size, and a script reading the node's
scale gets `(2, 0.5, 1)` instead of `(1, 1, 1)`. The fix is to apply
scale in Blender before export (`bpy.ops.object.transform_apply(scale=True)`,
or the data-API bake in `prop-origin-transform`), not an exporter flag.

## What the check asserts

**Pre-assertion (pathology exists):** `obj.scale == (2, 1, 0.5)`
(non-uniform) **and** local verts at ±1 on every axis. `--identity`
exits 3.
leaves the scale at `(1, 1, 1)` and exits 3.

**Handling (second axis):** read back from the written `.gltf` and its
`.bin`, not from Blender state:

- `nodes[0].scale == [2, 0.5, 1]` — Blender `(sx, sy, sz)` becomes glTF
`(sx, sz, sy)` under the Y-up conversion `(x, y, z) → (x, z, −y)`.
- POSITION min/max is ±1 on every axis — the local cube, unscaled.
- POSITION count is 8 either way, so a vertex count alone cannot tell
applied from unapplied. The node scale and the POSITION range are the
witnesses.

`--bake` applies the scale to the mesh data after the pre-assertion.
The node then has no `scale` key and POSITION spans x ±2, y ±0.5, z ±1,
so the check exits 4.

## Versions

Re-verified on Blender 4.5.11 LTS, 5.1.2 and 5.2.1 LTS: the RNA
description, the node scale, the POSITION range and both falsifier exits
are the same on all three. Not a version split.

**Handling (second axis):** with `export_apply=True`, node.scale is
`(2, 0.5, 1)` (`(sx, sz, sy)` from `(x,y,z)→(x,z,−y)`) and POSITION
bbox stays ±1. Vert count is 8 either way. `--bake` after the
pre-assert exits 4 (node.scale missing, POSITION x ±2).
No gallery still. A stretched box looks like modeled non-uniform size;
the defect is unapplied versus baked, which only the file shows.

Same on 4.5 LTS and 5.2 LTS. Not a version split.
## API reference

No gallery still. A stretched box looks like modeled non-uniform size.
- [`bpy.ops.export_scene.gltf`](https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf)
([4.5 LTS](https://docs.blender.org/api/4.5/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf))
— `export_apply`, `export_yup`
- [`Object.scale`](https://docs.blender.org/api/current/bpy.types.Object.html#bpy.types.Object.scale)
- [glTF 2.0 node transforms](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#transformations)

## Run

Expand Down
31 changes: 31 additions & 0 deletions showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ entry in `showcase/gallery.json`, and a rendered still.
`--skew-wheel` / `--turn-posts` break a mirror or placement budget
(exit 19). `--float-rivets` lifts fasteners off their host (exit 18).
`--round-haft` turns an oval section round (exit 19).
`--float-nails` lifts nail heads off their strap (exit 18).
`--sharp-iron` skips a chamfer pass so the edge-treatment budget fails
(file-local code; `crate-stack` uses 21).
A budget with no falsifier witnesses nothing:
prove each one fails once, and check the exit code, not just
non-zero.
Expand Down Expand Up @@ -299,6 +302,34 @@ entry in `showcase/gallery.json`, and a rendered still.
because the log's wobble drops the surface 1.4 mm across one head.
Aimed down the normal both seat at 1.50–1.54 mm. `--float-rivets` is
the falsifier.
- **Edge treatment: no right angles (file-local code).** Real objects
have chamfers that catch light. Where every box in a piece is
chamfered, count manifold edges whose faces meet within 5° of 90° and
assert **0** — a one-segment chamfer turns every 90° edge into two 45°
ones, so a survivor is a bevel pass that was skipped. Chamfer thin
stock at its own offset: `crate-stack`'s 3.5 mm iron takes 0.8 mm,
where the timber's 1.8 mm would leave no flat. Pass `material=` to
`bmesh.ops.bevel`: left at its default the chamfer faces took slot 0,
and the iron plates rendered — and were classified — as timber.
`--sharp-iron` is the falsifier. 15–19 are reserved, so the code is
the piece's next free one.
- **An L-section is one shell, not two boxes (exit 15).** Two
overlapping boxes for the legs of an angle strap share the outer
corner edge; chamfer them and both lay a strip on the same line — a
coplanar cross-shell pair at every corner (`crate-stack`: 12). Extrude
the L profile once, each cap as two convex quads meeting on the
inner-corner diagonal, and skip that flat diagonal in the bevel.
- **Classify small parts by a size derived from the host.** A nail,
rivet or stud told apart from its plate by world-AABB extent needs a
threshold taken from the plate (`IRON_WRAP * 0.5`), not from the
fastener: a yawed 10 mm head has a world AABB wider than 10 mm, and a
fixed 10 mm threshold silently dropped 8 of 48 nails.
- **Identical boards read as CG.** Planks cut from one material are one
plank repeated. Give each shell a seeded tone and its own grain
direction — its long axis, recovered from its vertices — as face
attributes, and have the shader stretch its grain along that axis
rather than a world axis, which a yawed member is off by its yaw.
Nothing here is an assertion; it is found on the inspection sheet.
- **A section that carries the read is a budget (exit 19).** Where a
cross-section is what makes a part recognisable — an axe handle is
oval, a broom handle is round — assert it as a ratio band at a named
Expand Down
Loading
Loading