| … | … | @@ -1,16 +1,46 @@ |
| 1 | 1 | # Blender-Godot Bridge |
| 2 | 2 | |
| 3 | | Real-time bidirectional scene sync between Blender and Godot via POSIX shared memory + Unix Domain Sockets. |
| 3 | Two systems in one repo, and it matters which one you are in: |
| 4 | |
| 5 | 1. **Scene sync** — real-time bidirectional Blender <-> Godot transfer of |
| 6 | objects, meshes, materials, skeletons and textures over POSIX shared memory |
| 7 | + a Unix Domain Socket. The original bridge; most of this file. |
| 8 | 2. **proc-nodes parity** — Blender geometry-node groups that reproduce |
| 9 | [[proc-nodes]] numerically, so terrain authored in Blender evaluates |
| 10 | identically in-game. Lives in `blender_addon/geometry_deserializer.py` and |
| 11 | `test/`. **This is the active work** and has its own section below. |
| 12 | |
| 13 | **Verified 2026-09-04** against v0.58.0, Blender 5.2.0, Godot 4.7.beta custom. |
| 14 | Everything dated here was measured on that day; anything older is marked. |
| 4 | 15 | |
| 5 | 16 | ## MANDATORY: Version Bump on EVERY Change |
| 6 | 17 | |
| 7 | 18 | **Before EVERY build, bump the version in BOTH files. No exceptions.** |
| 8 | 19 | 1. `blender_addon/blender_manifest.toml` — `version = "X.Y.Z"` (what Blender shows) |
| 9 | 20 | 2. `src/blender_bridge.cpp` — `"BlenderBridge vX.Y.Z:"` string (what Godot logs at startup) |
| 10 | | 3. Rebuild C++ AFTER the bump: `scons -j$(nproc)` |
| 21 | 3. Rebuild C++ AFTER the bump — **all three targets**, see Build |
| 11 | 22 | 4. .so is **symlinked** to hyper-tps (no copy needed) |
| 12 | 23 | |
| 13 | | **Current version: 0.44.0** (branch: sdf-volume-bridge) |
| 24 | **Current version: 0.58.0** (branch: `sdf-volume-bridge`, 22 commits unpushed |
| 25 | to `bare` as of 2026-09-04) |
| 26 | |
| 27 | ## Where things are |
| 28 | |
| 29 | | | | |
| 30 | |---|---| |
| 31 | | repo | `/storage/projects/code/godot/blender-bridge/` | |
| 32 | | addon, installed | `~/.config/blender/5.2/extensions/user_default/blender_godot_bridge` -> `blender_addon/` (**symlink**, via `./install_addon.sh`) | |
| 33 | | module path in Blender | `bl_ext.user_default.blender_godot_bridge` | |
| 34 | | Blender | `/home/fleeky/3d-prog/blender/blender-5.2.0-linux-x64/blender` (**not on PATH**) | |
| 35 | | Godot | `/storage/projects/code/godot/nvidia-rtx-godot/bin/godot.linuxbsd.editor.x86_64` | |
| 36 | | consumer | `hyper-tps/bin/` — gdextension + three symlinked `.so` | |
| 37 | | sibling | `/storage/projects/code/godot/proc-nodes/` — the engine being matched | |
| 38 | |
| 39 | `install_addon.sh` symlinks into Blender 5.x `extensions/`, and refuses to |
| 40 | shadow the source. It used to write Blender 4.3's legacy `scripts/addons` path, |
| 41 | so for a while the addon that actually loaded was a hand-copied snapshot while |
| 42 | edits to the source did nothing. If behaviour does not change after an edit, |
| 43 | check what the extensions dir actually points at before debugging the code. |
| 14 | 44 | |
| 15 | 45 | ## When to Use |
| 16 | 46 | |
| … | … | @@ -18,6 +48,7 @@ Real-time bidirectional scene sync between Blender and Godot via POSIX shared me |
| 18 | 48 | - Integrating Blender workflows with Godot (hyper-tps, godot-keet) |
| 19 | 49 | - Debugging SHM protocol, UDS transport, coordinate conversion |
| 20 | 50 | - Building or modifying the Blender addon or C++ GDExtension |
| 51 | - **Any proc-nodes <-> Blender numerical parity work** (see that section) |
| 21 | 52 | |
| 22 | 53 | ## Architecture |
| 23 | 54 | |
| … | … | @@ -58,6 +89,11 @@ Godot (C++ GDExtension) Blender (Python Addon) |
| 58 | 89 | | `blender_addon/change_detector.py` | Detect Blender changes via depsgraph handler | |
| 59 | 90 | | `blender_addon/material_serializer.py` | Serialize Blender shader graph → Godot VisualShader JSON | |
| 60 | 91 | | `blender_addon/tile_baker.py` | Extract/stream texture tiles to SHM ring buffer | |
| 92 | | `blender_addon/geometry_serializer.py` | **`GEOMETRY_NODE_MAP`** (Blender type -> proc type) + Blender graph -> proc-nodes JSON | |
| 93 | | `blender_addon/geometry_deserializer.py` | proc-nodes JSON -> Blender graph, **and every `ProcNodes_*` node group** (3452 lines) | |
| 94 | | `blender_addon/procedural_baker.py` | Bake procedural textures to images | |
| 95 | | `blender_addon/ptex_converter.py`, `ptex_mmg_loader.py`, `ptex_to_shader.py` | Material Maker `.ptex` import (v0.50.0) | |
| 96 | | `blender_addon/pose_library_sync.py` | Pose library transfer | |
| 61 | 97 | |
| 62 | 98 | ### Build & Config |
| 63 | 99 | | File | Purpose | |
| … | … | @@ -66,6 +102,142 @@ Godot (C++ GDExtension) Blender (Python Addon) |
| 66 | 102 | | `blender_addon/blender_manifest.toml` | Blender 5.0+ extension manifest | |
| 67 | 103 | | `install_addon.sh` | Symlink addon into Blender's extension dir | |
| 68 | 104 | |
| 105 | --- |
| 106 | |
| 107 | # proc-nodes parity — the active work |
| 108 | |
| 109 | Blender's geometry nodes are the authoring surface; [[proc-nodes]] is the engine |
| 110 | that ships. A graph must produce the SAME NUMBERS on both sides. That is a |
| 111 | different problem from scene sync and has its own tooling. |
| 112 | |
| 113 | ## The rule that cost the most to learn |
| 114 | |
| 115 | **A transcription used as the reference can only ever agree with itself.** |
| 116 | |
| 117 | Four implementations turned out to be faithful ports of the WRONG reference — |
| 118 | the iq hash, the euler order, the axis conversions, the erosion filter — and in |
| 119 | two cases a test written afterwards certified the wrong behaviour as correct, |
| 120 | because it compared against the same transcription the port was made from. The |
| 121 | euler order was certified PASS twice that way, both times because the check |
| 122 | asserted a **spelling** rather than a rotation. |
| 123 | |
| 124 | So parity tests spawn **headless Godot** and compare against the code that |
| 125 | ships: |
| 126 | |
| 127 | ``` |
| 128 | proc-nodes/demo/parity_driver.gd value nodes, via ProcNodeGraph.evaluate_socket |
| 129 | proc-nodes/demo/phacelle_probe.gd ProcNodeGraph.debug_phacelle_noise |
| 130 | proc-nodes/demo/erosion_probe.gd ProcNodeGraph.debug_erosion_filter |
| 131 | proc-nodes/demo/socket_dump.gd every node type's input sockets, for the audit |
| 132 | ``` |
| 133 | |
| 134 | `debug_*` statics exist ONLY for this. They are declared in `proc_node_graph.h` |
| 135 | but defined in `noise_nodes.cpp`, because `erosion_cpp_shim.h` defines its |
| 136 | functions non-inline and a second translation unit would multiply-define them. |
| 137 | |
| 138 | Corollary, learned the same day: **mutate the source and check the test goes |
| 139 | red.** One mutation had a paren too many in its `sed` pattern, so the file never |
| 140 | changed and "7/7 passed" meant nothing. And a good mutation test discriminates |
| 141 | the BEHAVIOUR: dropping the fractional-octave taper fails only the |
| 142 | `max_octaves=3.5` case, because at 999 every weight is exactly 1. |
| 143 | |
| 144 | ## The node maps are written twice, in two languages |
| 145 | |
| 146 | `geometry_serializer.py:GEOMETRY_NODE_MAP` (Blender -> proc) and the reverse in |
| 147 | `geometry_deserializer.py`. Godot decides the `bl_type` it sends and Blender |
| 148 | decides what it accepts, so drift means a **silently dropped node**. |
| 149 | `test/test_node_maps.py` asserts they stay inverse bijections. Generating one |
| 150 | from the other would end the class of bug; nobody has. |
| 151 | |
| 152 | Socket resolution is **by name** since v0.54.0, falling back to the index only |
| 153 | when the type there agrees (v0.53.0 stopped it carrying a vector into a scalar |
| 154 | socket by averaging). So "the layouts differ" is not a defect; "the name is not |
| 155 | on the Blender node" is. See the audit numbers under Testing. |
| 156 | |
| 157 | ## The erosion stack |
| 158 | |
| 159 | proc-nodes' terrain filter, ported to Blender node groups bottom-up. Each layer |
| 160 | verified against the shipping C++ before the next was built, which is why the |
| 161 | top two came out exact first try. |
| 162 | |
| 163 | | C++ (`proc-nodes/src/shared/erosion_filter.inc`) | Blender group | worst delta | |
| 164 | |---|---|---| |
| 165 | | `erosion_hash` | `ProcNodes_ErosionHash` | bit-for-bit, 7 lattice points | |
| 166 | | `grad_noised_2d` | `ProcNodes_Noised` | via GradientNoise 1.000000 | |
| 167 | | `fractal_noise_2d` | `ProcNodes_FBMNoised` (Repeat Zone) | same | |
| 168 | | 5 scalar helpers | `ProcNodes_{PowInv,EaseOut,SmoothStart,NormalizeSafe}` | 18 assertions | |
| 169 | | `phacelle_noise` | `ProcNodes_Phacelle`, 64 nodes | **9.537e-07** | |
| 170 | | `erosion_filter` | `ProcNodes_ErosionFilter`, 122 nodes | **2.980e-08** | |
| 171 | | `erosion_node_eval_full` | `ProcNodes_ErosionEval` | see below | |
| 172 | |
| 173 | `ProcNodes_ErosionNoise` is the node users place, and it is a port of the |
| 174 | **SUPERSEDED** filter — the frequency approach (2018 clayjohn / 2023 Fewes), |
| 175 | where stripe frequency scaled with slope magnitude. The C++ moved to |
| 176 | runevision's **fade approach**: constant stripe width, stripes fading toward a |
| 177 | target as steepness drops. That is why it correlated **0.1848**, not because it |
| 178 | was mistuned. The `.inc` is explicit that fading erosion at low altitude was a |
| 179 | WORKAROUND for the frequency approach's bulged valleys. |
| 180 | |
| 181 | ## Integer math IS available in geometry nodes |
| 182 | |
| 183 | `ProcNodes_Hash2D` used to implement iq's FLOAT hash (`fract`/`sin`) while |
| 184 | proc-nodes uses an integer LCG with xor/shift. Both give lattice gradients in |
| 185 | [-1,1] — same character, unrelated values, correlation **-0.0043**. |
| 186 | |
| 187 | It was asserted, untested, that Blender could not reproduce a C hash, and a |
| 188 | product decision was nearly put to the user: change `erosion_hash` and with it |
| 189 | every existing terrain, or accept the preview as permanently approximate. |
| 190 | Neither was needed. **`FunctionNodeIntegerMath` wraps at 32 bits exactly like C** |
| 191 | and **`FunctionNodeBitMath` has AND/OR/XOR/NOT/SHIFT/ROTATE**. The reasoning had |
| 192 | been done from OUR NODE MAP instead of from Blender, and BitMath was one of the |
| 193 | nodes the map was missing. |
| 194 | |
| 195 | **GradientNoise went -0.0043 -> 1.000000.** And it made the erosion chain WORSE, |
| 196 | 0.5157 -> 0.1848 — two wrong things had been partially cancelling, so 0.5157 was |
| 197 | coincidence, not half-correctness. |
| 198 | |
| 199 | ## Fields have a domain, and producers are lazy |
| 200 | |
| 201 | Blender's fields are lazy functions evaluated on the CONSUMER's domain. Ours |
| 202 | were eager arrays sized by whatever geometry was passed in, and that one |
| 203 | difference was the root of four separate divergences. Fixed in two stages: |
| 204 | fields carry an `AttrDomain`, then a producer with no context leaves a PROMISE |
| 205 | the consumer redeems against its own geometry. Binding and deferral live in the |
| 206 | executor, so no intermediate node was touched. |
| 207 | |
| 208 | Same grid, same selection field, different domains, different correct answers: |
| 209 | point domain 3 verts / 0 tris, face domain 6 verts / 4 tris. |
| 210 | |
| 211 | Consequence: `Position`, `Normal` and `NamedAttribute` no longer take a Geometry |
| 212 | input, matching Blender. |
| 213 | |
| 214 | ## Traps recorded here so they are not rediscovered |
| 215 | |
| 216 | - **`SocketValue::to_float()` takes `vec.x`** from a non-field scalar, so a |
| 217 | vector wired into a float socket reads as near-constant and LOOKS plausible. |
| 218 | Blender does not hide it — it AVERAGES x, y, z per point. That asymmetry is |
| 219 | how a test scene wired slope into `ErosionStr` (socket 2) instead of `Slope` |
| 220 | (socket 8) and the difference read as "Blender has much more detail". |
| 221 | - **Blender's XYZ euler is Godot's ZYX.** One names the order rotations are |
| 222 | APPLIED, the other the order matrices are MULTIPLIED. `EULER_ORDER_XYZ` reads |
| 223 | right and is a different rotation. |
| 224 | - **The Godot editor rewrites `project.godot` on exit**, stripping comments. It |
| 225 | ate hyper-tps' 30-line Jolt experiment block twice. `hyper-tps/scripts/dev-editor.sh` |
| 226 | now restores it when the editor changed no setting. |
| 227 | - **A build guard that compares mtimes is unsound** — scons keys on content, so |
| 228 | it cries wolf on a clean tree. |
| 229 | - `graphs/terrain.tres` in hyper-tps has been **broken since 2026-03-27**: node 3 |
| 230 | is `null` while `connections` still references it, so it evaluates to no mesh. |
| 231 | It took SlopeGradient's Slope into socket 2 — the same mis-wiring as the test |
| 232 | scene — so it was probably an `ErosionNoiseNode` whose sub-resource was lost. |
| 233 | |
| 234 | ## Post-mortems |
| 235 | |
| 236 | `hyper-games` #60 (and its comment) is the full account, in the #41/#44/#45/#53/ |
| 237 | #55–#59 series. Read it before resuming parity work. |
| 238 | |
| 239 | --- |
| 240 | |
| 69 | 241 | ## SHM Protocol |
| 70 | 242 | |
| 71 | 243 | ### Header (64 bytes) |
| … | … | @@ -177,17 +349,33 @@ The Rx pair correctly reverses the glTF exporter's conversion. |
| 177 | 349 | - Do NOT compute `rest * pose` — that doubles the transform |
| 178 | 350 | - `set_bone_pose_rotation(i, rest_rot * delta_rot)` — compose rest with delta for full pose |
| 179 | 351 | |
| 180 | | ## Build |
| 352 | ## Build — THREE targets, and the editor one is the one you are running |
| 353 | |
| 354 | `@tool` scenes and the live bridge run inside the **editor**, which loads |
| 355 | `linux.editor.x86_64`, not `template_debug`. Build one target and the editor |
| 356 | happily keeps executing a months-old library while the file you just compiled |
| 357 | sits next to it looking current. |
| 181 | 358 | |
| 182 | 359 | ```bash |
| 183 | 360 | cd /storage/projects/code/godot/blender-bridge |
| 184 | | scons -j$(nproc) platform=linux target=template_debug |
| 185 | | # Output: bin/linux/libblender_bridge.linux.template_debug.x86_64.so |
| 186 | | |
| 187 | | # Copy to hyper-tps |
| 188 | | cp bin/linux/*.so /storage/projects/code/godot/hyper-tps/bin/linux/ |
| 361 | for t in editor template_debug template_release; do |
| 362 | scons -j$(nproc) platform=linux target=$t || break |
| 363 | done |
| 364 | # bin/linux/libblender_bridge.linux.{editor,template_debug,template_release}.x86_64.so |
| 189 | 365 | ``` |
| 190 | 366 | |
| 367 | No copy step — hyper-tps/bin/linux holds three **symlinks** back into this |
| 368 | `bin/linux/`. The gdextension that matters is `hyper-tps/bin/blender_bridge.gdextension` |
| 369 | and it has all three entries since 2026-09-04. |
| 370 | |
| 371 | **`blender-bridge/bin/blender_bridge.gdextension` is a dead scaffold** from the |
| 372 | first commit. It has no `linux.editor` entry and names macos/windows files that |
| 373 | do not exist. Nothing loads it (this repo has no `project.godot`) — do not |
| 374 | "fix" a bridge problem there. |
| 375 | |
| 376 | Same trap, same shape as [[reference_hypertps-gdextension-stale-so]]: an |
| 377 | untracked `.so` that CI never rebuilds and that lags its source silently. |
| 378 | |
| 191 | 379 | ## Live Testing |
| 192 | 380 | |
| 193 | 381 | 1. Open `hyper-tps/scenes/test/blender_bridge_live.tscn` in Godot editor |
| … | … | @@ -305,7 +493,7 @@ Reverse path: maps Godot classes back to Blender node types, creates `Principled |
| 305 | 493 | | **Textures** | SVT streaming tiles | File references | |
| 306 | 494 | | **Meshes** | Binary blob in SHM | Text-based .escn format | |
| 307 | 495 | | **Speed** | Real-time (< 16ms for transforms) | Manual export step | |
| 308 | | | **Status** | Production (v0.31.1) | Experimental (recommends glTF) | |
| 496 | | **Status** | Production (v0.58.0) | Experimental (recommends glTF) | |
| 309 | 497 | |
| 310 | 498 | ### Relation to Material Maker |
| 311 | 499 | |
| … | … | @@ -329,8 +517,18 @@ Both systems share the concept of node-based material graphs. Integration opport |
| 329 | 517 | - Front-face winding: effectively CW in world space (Vulkan Y-flip in projection) |
| 330 | 518 | |
| 331 | 519 | ### Bridge Translation Rules |
| 332 | | - **Coordinate conversion**: `(x,y,z)→(x,z,-y)`, det=+1, applied to positions + normals |
| 333 | | - **Winding flip**: UNCONDITIONAL swap of indices 1↔2 per triangle (Blender CCW → Godot effective CW) |
| 520 | - **Coordinate conversion**: `M = (-x, z, y)`, det=+1, applied to positions + |
| 521 | normals in BOTH directions. Until v0.55.0 this line read `(x,y,z)→(x,z,-y)`, |
| 522 | which is `Rx(-90)`, and the inbound path really did use it while the outbound |
| 523 | path used `M`. They differ by a half turn about Y, so a mesh sent to Blender |
| 524 | and read back came home rotated 180°: `(1,2,3)` -> `(-1,2,-3)`. Its own object |
| 525 | transform and its bones did not, because those pairs were already inverses. |
| 526 | `M` is self-inverse, which is why it is now used for both directions — it |
| 527 | cannot be paired wrongly again. |
| 528 | - **Winding flip**: UNCONDITIONAL swap of indices 1↔2 per triangle on the |
| 529 | Blender→Godot path only. The asymmetry is deliberate: it is a RENDER |
| 530 | convention (Blender CCW vs Godot's effective CW under the Vulkan Y-flip), not |
| 531 | a coordinate one, so it does not need an outbound partner. |
| 334 | 532 | - **Roughness**: insert clamp(0,1) — Blender `saturatef()` = `clamp(r,0,1)`, NO abs() |
| 335 | 533 | - **Negative scale** (v0.19.28): Python detects `is_negative`, negates normals + reverses winding |
| 336 | 534 | before SHM write. C++ unconditional reverse + Godot negative-det transform cancel out → correct. |
| … | … | @@ -407,28 +605,77 @@ reach fingertips (Z already aligns with chain). Works for all leaf bones (breast |
| 407 | 605 | - **Area lights**: Mapped to OmniLight3D (Godot has no area light equivalent) |
| 408 | 606 | - **Light properties**: Only transform is synced — energy, color, range, attenuation are NOT synced |
| 409 | 607 | - **Addon reload**: Blender caches Python modules; after copying updated files to the addon |
| 410 | | directory, Blender must be restarted for changes to take effect |
| 608 | directory, Blender must be restarted for changes to take effect. The addon is |
| 609 | now a SYMLINK to the source, so there is nothing to copy — but the restart is |
| 610 | still required. |
| 611 | - **The C++ test suites do not build** — source only, no SConstruct targets |
| 612 | - **ErosionNoise parity is 0.1848** until the re-port is wired up (see the |
| 613 | proc-nodes parity section) |
| 411 | 614 | |
| 412 | 615 | ## Testing |
| 413 | 616 | |
| 617 | Three kinds, and they need different runners. |
| 618 | |
| 414 | 619 | ```bash |
| 415 | | # C++ tests |
| 416 | | cd /storage/projects/code/godot/blender-bridge |
| 417 | | ./test/test_shm_protocol # 8 tests |
| 418 | | ./test/test_uds # 7 tests |
| 419 | | ./test/test_page_table # 9 tests |
| 420 | | ./test/test_tile_cache # 11 tests |
| 421 | | ./test/test_tile_requests # 10 tests |
| 422 | | ./test/test_validation # 60 tests |
| 423 | | ./test/test_benchmarks # Performance benchmarks |
| 424 | | |
| 425 | | # Python tests |
| 426 | | python3 test/test_python_shm.py # 10 tests |
| 427 | | python3 test/test_cross_language.py # 1 test |
| 428 | | python3 test/test_tile_baker.py # 26 tests |
| 620 | BB=/storage/projects/code/godot/blender-bridge |
| 621 | BL=/home/fleeky/3d-prog/blender/blender-5.2.0-linux-x64/blender # not on PATH |
| 622 | cd $BB |
| 623 | |
| 624 | # 1. pure python — no Blender, no Godot |
| 625 | python3 test/test_node_maps.py # 6/6 |
| 626 | python3 test/test_axis_convention.py # 13/13 |
| 627 | |
| 628 | # 2. inside Blender |
| 629 | $BL --background --factory-startup --python test/test_node_primitives.py # 40/40 |
| 630 | $BL --background --factory-startup --python test/test_rotation_matrix_values.py # 18/18 |
| 631 | |
| 632 | # 3. CROSS-ENGINE — Blender spawns headless Godot and compares against it |
| 633 | $BL --background --factory-startup --python test/test_phacelle_parity.py # 7/7, 9.537e-07 |
| 634 | $BL --background --factory-startup --python test/test_erosion_filter_parity.py # 6/6, 2.980e-08 |
| 635 | $BL --background --factory-startup --python test/test_rotation_matrix_parity.py # 17/17 |
| 636 | ``` |
| 637 | |
| 638 | Counts and deltas above were **measured 2026-09-04**, all green. |
| 639 | |
| 640 | The C++ suites (`test_shm_protocol.cpp`, `test_uds.cpp`, `test_page_table.cpp`, |
| 641 | `test_tile_cache.cpp`, `test_tile_requests.cpp`, `test_validation.cpp`, |
| 642 | `test_benchmarks.cpp`) have **no build rule in SConstruct** and no binaries on |
| 643 | disk. An earlier version of this file claimed "150 tests, all passing" with |
| 644 | `./test/test_shm_protocol` invocations that cannot run. They are source only |
| 645 | until someone adds the targets. |
| 646 | |
| 647 | ### Audits — measure, do not quote |
| 648 | |
| 649 | ```bash |
| 650 | # coverage of Blender's geometry-node catalogue |
| 651 | $BL --background --factory-startup --python test/audit_node_coverage.py |
| 652 | |
| 653 | # can each proc socket be found BY NAME on its Blender node? |
| 654 | # needs a socket dump, which proc-nodes writes itself: |
| 655 | $GODOT --headless --path ../proc-nodes/demo --script res://socket_dump.gd -- /tmp/ps.json |
| 656 | PROC_SOCKETS=/tmp/ps.json $BL --background --factory-startup --python test/audit_socket_names.py |
| 429 | 657 | ``` |
| 430 | 658 | |
| 431 | | 150 total tests, all passing. |
| 659 | Measured 2026-09-04, Blender 5.2.0: |
| 660 | |
| 661 | | | | |
| 662 | |---|---| |
| 663 | | registered `bpy.types.Node` classes | 571 | |
| 664 | | **instantiable in a GeometryNodeTree** | **360** — the honest universe | |
| 665 | | `GEOMETRY_NODE_MAP` entries | 98 (27.2%), 0 dead | |
| 666 | | missing | 262 — other 79, curve 39, point/volume 35, mesh 35, input 27, string 13, field/utility 10, instance 7, attribute 6, texture 6, matrix/rotation 5 | |
| 667 | | sockets by NAME | 174 (81.7%) | |
| 668 | | sockets by INDEX (fallback) | 33 (15.5%) | |
| 669 | | **UNRESOLVED** (dropped at runtime) | **6** — `CaptureAttribute:Value`, `CurveToMesh:Resolution`, `DistributePointsOnFaces:Count`, `JoinGeometry:Geometry B`, `RepeatZoneInput:Geometry`, `SimulationZoneInput:Geometry` | |
| 670 | |
| 671 | The last two are `CUSTOM` multi-sockets and may not be representable, so the |
| 672 | realistic floor is 2, not 0. |
| 673 | |
| 674 | **Do not filter Blender's sockets on `enabled`.** Blender hides sockets by MODE |
| 675 | — Noise Texture's `W` only in 4D, `Offset`/`Gain` only for multifractal types — |
| 676 | and doing so reported three of `PerlinNoiseNode`'s sockets as wrong when that |
| 677 | node matches Blender exactly. Those three were queued for DELETION to satisfy a |
| 678 | number the tool got wrong. |
| 432 | 679 | |
| 433 | 680 | ## Completed Phases |
| 434 | 681 | |
| … | … | @@ -438,3 +685,14 @@ python3 test/test_tile_baker.py # 26 tests |
| 438 | 685 | - v0.19.x: Material Graph Transfer (Blender node trees → Godot VisualShader graphs) |
| 439 | 686 | - v0.26.x: Bone/skeleton coordinate conversion fix (M→Rx for bones) |
| 440 | 687 | - v0.30.x-v0.31.x: Skeleton reconstruction (armature-space diff, Z-axis leaf tails, connected bones) |
| 688 | - v0.37.x-v0.39.x: proc-nodes graph sync over UDS, delta param protocol |
| 689 | - v0.40.x-v0.47.x: Blender node groups for the erosion terrain pipeline — |
| 690 | GradientNoise fBM Repeat Zone, Gullies, AltitudeColor, UV, Displace |
| 691 | - v0.48.0-v0.49.x: Godot->Blender param delta sync (accumulate + drain, no polling) |
| 692 | - v0.50.0: Material Maker `.ptex` import |
| 693 | - v0.52.0-v0.55.0: socket resolution by name; refuse vector-into-scalar; mesh |
| 694 | vertices moved to the self-inverse `M` conversion |
| 695 | - v0.56.0-v0.58.0: reusable integer primitives, `erosion_hash` reproduced |
| 696 | exactly, **GradientNoise parity 1.000000** |
| 697 | - 2026-09-04 (unversioned): coverage/socket audits, rotation-matrix parity |
| 698 | across 18 nodes, and the erosion re-port down to `erosion_filter` |