Skip to content

Commit 17c4ca1

Browse files
TMHSDigitalclaude
andcommitted
fix(content): OBJ uses export_eval_mode; USD modifier rule keys on export_subdivision
Measured on .scratch/blender-4.5.11-windows-x64 and blender-5.2.1 (identical): - wm.obj_export has no evaluation_mode; it is export_eval_mode with DAG_EVAL_RENDER / DAG_EVAL_VIEWPORT, default DAG_EVAL_VIEWPORT. USD and Alembic evaluation_mode default to RENDER. - usd_export with no extra kwargs on a cube with DECIMATE ships 5 points (modifier applied). With SUBSURF (levels 1, render 2): default -> 8 (cage, BEST_MATCH), TESSELLATE -> 98, TESSELLATE + VIEWPORT -> 26. So the depsgraph skill's "USD, Alembic, and OBJ exporters accept evaluation_mode" produced a TypeError for OBJ, and the rule treated a USD call without evaluation_mode as "never requests evaluated geometry" while accepting evaluation_mode='RENDER' with default BEST_MATCH, the real cage bug. - depsgraph-and-evaluated-data: per-exporter table of parameter, values and default; notes OBJ's default is the opposite of USD's. - no-unapplied-modifiers-on-export: per-exporter defaults (glTF export_apply False, FBX use_mesh_modifiers True, USD evaluates by default); flags USD only for SUBSURF without export_subdivision='TESSELLATE'. - tests/check_import_export_rules.py: the modifier check is now per call (glTF needs export_apply=True; FBX flags use_mesh_modifiers=False; USD flags SUBSURF without TESSELLATE) instead of a whole-file evaluation_mode grep. Falsified with probe files: USD+SUBSURF+evaluation_mode='RENDER' -> new ERROR, old passed; USD+DECIMATE no kwargs -> new passes, old ERROR; USD+SUBSURF+ TESSELLATE -> new passes, old ERROR; glTF+DECIMATE no export_apply -> ERROR in both. Repo scan passes. Closes #331 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f293573 commit 17c4ca1

3 files changed

Lines changed: 70 additions & 26 deletions

File tree

‎rules/no-unapplied-modifiers-on-export.mdc‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,31 @@ live in the depsgraph. An export that does not ask for evaluated geometry
1313
writes the cage: the LOD modifier is dropped, the engine gets the dense
1414
mesh, and the script still exits 0.
1515

16-
glTF: `export_apply=True` applies modifiers excluding armatures.
17-
USD: `evaluation_mode='RENDER'` or `'VIEWPORT'`.
18-
FBX: `use_mesh_modifiers=True`.
16+
Each exporter has its own switch (defaults measured on 4.5 LTS and 5.2 LTS):
17+
18+
- glTF: `export_apply` defaults to `False`. Pass `export_apply=True`
19+
(applies modifiers excluding armatures).
20+
- FBX: `use_mesh_modifiers` defaults to `True`. Only an explicit `False`
21+
drops them.
22+
- USD: modifiers are evaluated by default (`evaluation_mode` defaults to
23+
`'RENDER'`), so DECIMATE ships with no extra argument. **Subdivision is the
24+
exception:** under the default `export_subdivision='BEST_MATCH'` a SUBSURF
25+
modifier is written as the cage plus a `subdivisionScheme`, whatever
26+
`evaluation_mode` says. Pass `export_subdivision='TESSELLATE'` to ship the
27+
subdivided mesh.
1928

2029
`export_apply` is not "apply object transforms". Apply object scale first.
2130
See rule `validate-imported-mesh-scale`.
2231

2332
## What this rule flags
2433

25-
A `bpy.ops.export_scene.gltf`, `bpy.ops.export_scene.fbx`, or
26-
`bpy.ops.wm.usd_export` call in a file that adds modifiers (`modifiers.new`)
27-
and never requests evaluated geometry (`export_apply=True` or
28-
`evaluation_mode=`), and never applies those modifiers before export.
34+
In a file that adds modifiers (`modifiers.new`) and never applies them
35+
(`modifier_apply`):
36+
37+
- a `bpy.ops.export_scene.gltf` call without `export_apply=True`;
38+
- a `bpy.ops.export_scene.fbx` call with `use_mesh_modifiers=False`;
39+
- a `bpy.ops.wm.usd_export` call without `export_subdivision='TESSELLATE'`
40+
when one of the modifiers is SUBSURF.
2941

3042
```python
3143
# WRONG: DECIMATE on the object, glTF without export_apply
@@ -36,9 +48,10 @@ bpy.ops.export_scene.gltf(filepath=path, use_selection=True)
3648
```
3749

3850
```python
39-
# WRONG: USD viewport/render mode omitted; default BEST_MATCH writes the cage
51+
# WRONG: evaluation_mode does not help here; default BEST_MATCH writes the
52+
# 8-point cube cage plus subdivisionScheme, not the subdivided mesh
4053
obj.modifiers.new("ss", "SUBSURF").levels = 2
41-
bpy.ops.wm.usd_export(filepath=path)
54+
bpy.ops.wm.usd_export(filepath=path, evaluation_mode="RENDER")
4255
```
4356

4457
## The required pattern
@@ -58,7 +71,9 @@ bpy.ops.export_scene.gltf(
5871
)
5972
```
6073

61-
USD:
74+
USD with subdivision (`evaluation_mode` picks render vs viewport levels:
75+
a cube at levels 1 / render_levels 2 ships 26 points with `'VIEWPORT'`, 98
76+
with the default `'RENDER'`):
6277

6378
```python
6479
bpy.ops.wm.usd_export(

‎skills/depsgraph-and-evaluated-data/SKILL.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,20 @@ Read raw `obj.data` directly when:
146146

147147
For most read paths, prefer the depsgraph route. The cost is one indirection; the safety is enormous.
148148

149-
## `evaluation_mode` for exporters
149+
## Evaluation mode for exporters
150150

151-
The USD, Alembic, and OBJ exporters accept an `evaluation_mode` parameter that controls how Blender evaluates the scene before exporting:
151+
The USD and Alembic exporters take `evaluation_mode`; the OBJ exporter's equivalent is `export_eval_mode` with different values. Passing `evaluation_mode=` to `wm.obj_export` raises `TypeError`.
152152

153-
- `'RENDER'`: include render-only modifiers (subsurf at render levels, etc.). Use for final exports.
154-
- `'VIEWPORT'`: use viewport modifier levels. Use for fast preview exports.
153+
| Exporter | Parameter | Values | Default |
154+
| --- | --- | --- | --- |
155+
| `wm.usd_export` | `evaluation_mode` | `'RENDER'`, `'VIEWPORT'` | `'RENDER'` |
156+
| `wm.alembic_export` | `evaluation_mode` | `'RENDER'`, `'VIEWPORT'` | `'RENDER'` |
157+
| `wm.obj_export` | `export_eval_mode` | `'DAG_EVAL_RENDER'`, `'DAG_EVAL_VIEWPORT'` | `'DAG_EVAL_VIEWPORT'` |
158+
159+
- Render: modifiers at render levels (subsurf `render_levels`, render-only modifiers). Use for final exports.
160+
- Viewport: modifiers at viewport levels. Use for fast preview exports.
161+
162+
Note the OBJ default is the opposite of USD's.
155163

156164
Default `export_subdivision='BEST_MATCH'` writes the **cage** plus
157165
`subdivisionScheme = catmullClark`. VIEWPORT and RENDER files are then

‎tests/check_import_export_rules.py‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,20 @@
1818
)
1919

2020
IMPORT_RE = re.compile(r"bpy\.ops\.import_scene\.(gltf|fbx)\s*\(")
21-
EXPORT_RE = re.compile(
22-
r"bpy\.ops\.(export_scene\.gltf|export_scene\.fbx|wm\.usd_export)\s*\("
23-
)
2421
MESH_WORK_RE = re.compile(r"bmesh\.|modifiers\.new|from_pydata|foreach_set")
2522
UNIT_SCALE_RE = re.compile(r"unit_settings|scale_length|global_scale")
26-
EXPORT_EVAL_RE = re.compile(
27-
r"export_apply\s*=\s*True|evaluation_mode\s*=|use_mesh_modifiers\s*=\s*True"
28-
)
23+
# Per-exporter "ships the modifier result" test, applied to each call's own
24+
# arguments (measured on 4.5.11 and 5.2.1):
25+
# glTF export_apply defaults to False -> must pass export_apply=True
26+
# FBX use_mesh_modifiers defaults to True -> only an explicit False drops them
27+
# USD modifiers are evaluated by default (evaluation_mode defaults to
28+
# RENDER), but SUBSURF ships as the cage under the default
29+
# export_subdivision='BEST_MATCH' -> needs export_subdivision='TESSELLATE'
30+
GLTF_APPLY_RE = re.compile(r"export_apply\s*=\s*True")
31+
FBX_NO_MODS_RE = re.compile(r"use_mesh_modifiers\s*=\s*False")
32+
USD_TESSELLATE_RE = re.compile(r"export_subdivision\s*=\s*[\"']TESSELLATE[\"']")
33+
SUBSURF_NEW_RE = re.compile(r"modifiers\.new\([^)]*[\"']SUBSURF[\"']")
34+
USD_CALL_RE = re.compile(r"bpy\.ops\.wm\.usd_export\s*\(")
2935
MODIFIER_NEW_RE = re.compile(r"modifiers\.new")
3036
MODIFIER_APPLY_RE = re.compile(r"modifier_apply")
3137
GLTF_CALL_RE = re.compile(r"bpy\.ops\.export_scene\.gltf\s*\(")
@@ -80,12 +86,27 @@ def check_text(rel, text):
8086
"transform_apply and a unit-scale check "
8187
"(unit_settings, scale_length, or global_scale)"
8288
)
83-
if EXPORT_RE.search(text) and MODIFIER_NEW_RE.search(text):
84-
if not EXPORT_EVAL_RE.search(text) and not MODIFIER_APPLY_RE.search(text):
85-
errors.append(
86-
f"{rel}: export with modifiers.new but no export_apply=True, "
87-
"evaluation_mode, or modifier_apply"
88-
)
89+
if MODIFIER_NEW_RE.search(text) and not MODIFIER_APPLY_RE.search(text):
90+
for body in _call_bodies(text, GLTF_CALL_RE):
91+
if not GLTF_APPLY_RE.search(body):
92+
errors.append(
93+
f"{rel}: export_scene.gltf after modifiers.new without "
94+
"export_apply=True (or modifier_apply) ships the cage"
95+
)
96+
for body in _call_bodies(text, FBX_CALL_RE):
97+
if FBX_NO_MODS_RE.search(body):
98+
errors.append(
99+
f"{rel}: export_scene.fbx with use_mesh_modifiers=False "
100+
"after modifiers.new ships the cage"
101+
)
102+
if SUBSURF_NEW_RE.search(text):
103+
for body in _call_bodies(text, USD_CALL_RE):
104+
if not USD_TESSELLATE_RE.search(body):
105+
errors.append(
106+
f"{rel}: wm.usd_export after a SUBSURF modifier without "
107+
"export_subdivision='TESSELLATE' writes the cage "
108+
"(default BEST_MATCH; evaluation_mode does not change it)"
109+
)
89110
for body in _call_bodies(text, GLTF_CALL_RE):
90111
if AXIS_FORWARD_RE.search(body) or AXIS_UP_RE.search(body):
91112
errors.append(

0 commit comments

Comments
 (0)