Skip to content

Commit 223b1ee

Browse files
TMHSDigitalclaude
andauthored
fix: terrain-scatter, export-preset-axis and mesh-automasking-settings quality pass (#230)
* fix(showcase): terrain-scatter quality pass — cleaved faceted rocks that never interpenetrate The stones were smooth near-white ellipsoids on pale clay, and two pairs interpenetrated. Stones are now cleaved by closed-form planes, flat-shaded and scaled 1.35x; centres relax apart to twice a radius bound derived from the same constants. A stone-overlap budget BVH-tests every pair (exit 20, --pile-rocks). Dark mottled soil and grey stone materials. The temp glTF export is removed after measuring. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * fix(examples): export-preset-axis still shows the lying beacon in profile Render path only: props moved together and the camera made oblique so the Godot reimport's mast reads along its length. The check, its assertions and exit codes are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs(examples): mesh-automasking-settings README maps all 18 attributes and states the traps Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs(showcase): conventions for non-interpenetrating scatter and broken rock Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> --------- Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 8b0e857 commit 223b1ee

15 files changed

Lines changed: 571 additions & 76 deletions

File tree

4.8 KB
Loading
3.89 KB
Loading
14.5 KB
Loading
1.38 KB
Loading

‎docs/gallery/export-preset-axis/index.html‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,10 @@ <h2>Source</h2>
697697
scene = bpy.context.scene
698698
source.hide_render = <span class="k">True</span>
699699
source.hide_viewport = <span class="k">True</span>
700-
sit_on_floor(unity_objs, -<span class="n">2.15</span>, <span class="n">0.0</span>)
701-
sit_on_floor(godot_objs, <span class="n">1.95</span>, <span class="n">0.0</span>)
700+
<span class="c"># Closer together: at -2.15 / +1.95 the pair left an empty metre of floor</span>
701+
<span class="c"># between them, and each prop was small in its half of the frame.</span>
702+
sit_on_floor(unity_objs, -<span class="n">1.35</span>, <span class="n">0.0</span>)
703+
sit_on_floor(godot_objs, <span class="n">1.35</span>, <span class="n">0.0</span>)
702704

703705
floor_me = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
704706
bm = bmesh.new()
@@ -734,10 +736,14 @@ <h2>Source</h2>
734736
cam_data = bpy.data.cameras.new(<span class="s">&quot;Cam&quot;</span>)
735737
cam_data.lens = <span class="n">50.0</span>
736738
cam = bpy.data.objects.new(<span class="s">&quot;Cam&quot;</span>, cam_data)
737-
cam.location = (<span class="n">3.12</span>, -<span class="n">8.15</span>, <span class="n">2.45</span>)
739+
<span class="c"># Oblique rather than straight down +Y: the Godot reimport lies with its</span>
740+
<span class="c"># mast along -Y, and a camera looking along Y foreshortened that mast to</span>
741+
<span class="c"># a stub in front of the base plate, so the one thing the check proves</span>
742+
<span class="c"># (it lies) barely read. From the side-front the mast shows its length.</span>
743+
cam.location = (<span class="n">4.9</span>, -<span class="n">5.1</span>, <span class="n">2.2</span>)
738744
scene.collection.objects.link(cam)
739745
aim = bpy.data.objects.new(<span class="s">&quot;Aim&quot;</span>, <span class="k">None</span>)
740-
aim.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.85</span>)
746+
aim.location = (<span class="n">0.40</span>, -<span class="n">0.30</span>, <span class="n">1.00</span>)
741747
scene.collection.objects.link(aim)
742748
con = cam.constraints.new(<span class="s">&quot;TRACK_TO&quot;</span>)
743749
con.target = aim

‎docs/gallery/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ <h2><a href="treasure-chest/">treasure-chest</a></h2>
981981
</article>
982982
<article class="card" data-tags="mesh export showcase">
983983
<a class="card-media" href="terrain-scatter/" tabindex="-1" aria-hidden="true">
984-
<img src="assets/terrain-scatter-hero.webp" alt="A square tile of sandy rolling terrain with pale rocks scattered on the hill." loading="lazy" decoding="async" />
984+
<img src="assets/terrain-scatter-hero.webp" alt="A square tile of dark rolling earth with nine faceted grey rocks seated across the hill, none touching." loading="lazy" decoding="async" />
985985
</a>
986986
<div class="card-body">
987987
<h2><a href="terrain-scatter/">terrain-scatter</a></h2>

‎docs/gallery/terrain-scatter/index.html‎

Lines changed: 212 additions & 34 deletions
Large diffs are not rendered by default.

‎examples/export-preset-axis/export_preset_axis.py‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,10 @@ def render_still(source, unity_objs, godot_objs, path, engine):
391391
scene = bpy.context.scene
392392
source.hide_render = True
393393
source.hide_viewport = True
394-
sit_on_floor(unity_objs, -2.15, 0.0)
395-
sit_on_floor(godot_objs, 1.95, 0.0)
394+
# Closer together: at -2.15 / +1.95 the pair left an empty metre of floor
395+
# between them, and each prop was small in its half of the frame.
396+
sit_on_floor(unity_objs, -1.35, 0.0)
397+
sit_on_floor(godot_objs, 1.35, 0.0)
396398

397399
floor_me = bpy.data.meshes.new("Floor")
398400
bm = bmesh.new()
@@ -428,10 +430,14 @@ def render_still(source, unity_objs, godot_objs, path, engine):
428430
cam_data = bpy.data.cameras.new("Cam")
429431
cam_data.lens = 50.0
430432
cam = bpy.data.objects.new("Cam", cam_data)
431-
cam.location = (3.12, -8.15, 2.45)
433+
# Oblique rather than straight down +Y: the Godot reimport lies with its
434+
# mast along -Y, and a camera looking along Y foreshortened that mast to
435+
# a stub in front of the base plate, so the one thing the check proves
436+
# (it lies) barely read. From the side-front the mast shows its length.
437+
cam.location = (4.9, -5.1, 2.2)
432438
scene.collection.objects.link(cam)
433439
aim = bpy.data.objects.new("Aim", None)
434-
aim.location = (0.0, 0.0, 0.85)
440+
aim.location = (0.40, -0.30, 1.00)
435441
scene.collection.objects.link(aim)
436442
con = cam.constraints.new("TRACK_TO")
437443
con.target = aim
4.29 KB
Loading

‎examples/mesh-automasking-settings/README.md‎

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ with `bpy.data.brushes.new("ProbeBrush", mode="SCULPT")`. `mode="SCULPT"`
1818
is load-bearing on 5.2: a default-mode `new(name)` leaves
1919
`mesh_automasking_settings is None`.
2020

21-
**Subset** (not all 18+ attributes): `use_automasking_topology` and
21+
**Subset** (3 of the 18 attributes; the full map is below): `use_automasking_topology` and
2222
`use_automasking_cavity` keep their identifiers after the move; cavity
2323
factor does not (`Brush.automasking_cavity_factor` vs
2424
`MeshAutomaskingSettings.cavity_factor`). That pair covers the location
@@ -46,6 +46,89 @@ every binary.
4646

4747
No `SMOKE_SKIP`. Every matrix leg exercises the contract.
4848

49+
## Who hits this
50+
51+
Sculpt add-ons, brush-preset importers and tool-setting UIs that read or
52+
write `brush.use_automasking_*` / `brush.automasking_*`. On 5.2 the first
53+
such access raises `AttributeError: 'Brush' object has no attribute ...`.
54+
A panel that draws it disappears from the UI, and an importer stops
55+
halfway through a preset.
56+
57+
Two traps on the way to fixing it:
58+
59+
- **The prefix rule is not uniform.** The ten `use_automasking_*`
60+
booleans keep their identifiers on the new struct. The eight
61+
`automasking_*` values drop the prefix, so a blanket
62+
`getattr(mas, old_name)` port breaks on exactly those eight.
63+
- **A brush made without a mode has no settings struct.**
64+
`bpy.data.brushes.new(name)` returns a brush whose
65+
`mesh_automasking_settings` is `None` on 5.2. It must be created with
66+
`mode="SCULPT"`. A reader that trusts the pointer then dereferences
67+
`None`.
68+
69+
The version-safe reader is the one this example asserts
70+
(`read_current`): take `.mesh_automasking_settings` when it exists and is
71+
not `None`, otherwise fall back to the `Brush` attributes. Writes go
72+
through the same struct and round-trip: setting `cavity_factor` and
73+
`use_automasking_topology` on it reads back unchanged on 5.2.1.
74+
75+
## Full attribute map
76+
77+
Measured by listing `Brush` RNA on 4.5.11 and 5.1.2 (18 automasking
78+
properties, identical on both), and `MeshAutomaskingSettings` RNA on 5.2.1
79+
(19 properties).
80+
81+
| 4.5 / 5.1 `Brush.` | 5.2 `Brush.mesh_automasking_settings.` |
82+
| --- | --- |
83+
| `use_automasking_topology` | `use_automasking_topology` |
84+
| `use_automasking_face_sets` | `use_automasking_face_sets` |
85+
| `use_automasking_boundary_edges` | `use_automasking_boundary_edges` |
86+
| `use_automasking_boundary_face_sets` | `use_automasking_boundary_face_sets` |
87+
| `use_automasking_cavity` | `use_automasking_cavity` |
88+
| `use_automasking_cavity_inverted` | `use_automasking_cavity_inverted` |
89+
| `use_automasking_custom_cavity_curve` | `use_automasking_custom_cavity_curve` |
90+
| `use_automasking_start_normal` | `use_automasking_start_normal` |
91+
| `use_automasking_view_normal` | `use_automasking_view_normal` |
92+
| `use_automasking_view_occlusion` | `use_automasking_view_occlusion` |
93+
| `automasking_boundary_edges_propagation_steps` | `boundary_edges_propagation_steps` |
94+
| `automasking_cavity_blur_steps` | `cavity_blur_steps` |
95+
| `automasking_cavity_curve` | `cavity_curve` |
96+
| `automasking_cavity_factor` | `cavity_factor` |
97+
| `automasking_start_normal_falloff` | `start_normal_falloff` |
98+
| `automasking_start_normal_limit` | `start_normal_limit` |
99+
| `automasking_view_normal_falloff` | `view_normal_falloff` |
100+
| `automasking_view_normal_limit` | `view_normal_limit` |
101+
| — | `cavity_curve_op` (new in 5.2, no Brush counterpart) |
102+
103+
On 5.2.1 the only automasking-named property left on `Brush` is the
104+
`mesh_automasking_settings` pointer itself.
105+
106+
## Re-verified
107+
108+
| Measurement | 4.5.11 | 5.1.2 | 5.2.1 |
109+
| --- | --- | --- | --- |
110+
| `bpy.types.MeshAutomaskingSettings` exists | no | no | yes |
111+
| Automasking properties on `Brush` | 18 | 18 | 1 (the pointer) |
112+
| `mesh_automasking_settings`, `mode="SCULPT"` | no attribute | no attribute | struct |
113+
| `mesh_automasking_settings`, default mode | no attribute | no attribute | `None` |
114+
| Brushes in factory-empty | 0 | 0 | 0 |
115+
| default exit | 0 | 0 | 0 |
116+
| `--assume-brush-attrs` exit | 0 | 0 | 5 |
117+
118+
Exiting 0 on 4.5.11 and 5.1.2 under the falsifier is correct by design.
119+
The old attributes still exist there, so the naive read works; the
120+
falsifier exists to fail where they were removed.
121+
122+
## API reference
123+
124+
- [`bpy.types.MeshAutomaskingSettings`](https://docs.blender.org/api/current/bpy.types.MeshAutomaskingSettings.html)
125+
(5.2 only)
126+
- [`bpy.types.Brush`](https://docs.blender.org/api/current/bpy.types.Brush.html)
127+
([4.5 LTS](https://docs.blender.org/api/4.5/bpy.types.Brush.html), where
128+
the `use_automasking_*` / `automasking_*` attributes live)
129+
- [`BlendDataBrushes.new`](https://docs.blender.org/api/current/bpy.types.BlendDataBrushes.html#bpy.types.BlendDataBrushes.new)
130+
— the `mode` argument
131+
49132
## Run
50133

51134
```bash

0 commit comments

Comments
 (0)