OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb commits / f13521e

skills: bring blender-bridge current, and write the missing proc-nodes one
blender-bridge/SKILL.md was last touched 2026-03-22 at v0.44.0; the repo is at v0.58.0. Worse than stale, it CONTRADICTED itself: "Bridge Translation Rules" gave the mesh conversion as (x,y,z)->(x,z,-y), which is Rx(-90) and is exactly the non-inverse-pair bug v0.55.0 fixed, while the section above it correctly gave M = (-x, z, y). Anyone reading top-down would have re-introduced it. Corrected or replaced, each against source or a run: - version, branch, addon symlink path, Blender 5.2 and Godot binary paths - the coordinate rule, with why M is self-inverse and why the winding asymmetry is deliberate - Build: THREE targets. The editor loads linux.editor.x86_64, and building one target leaves the editor executing a months-old library - Testing: the C++ suites have no SConstruct target and no binaries. The old "150 tests, all passing" listed commands that cannot run - proc-nodes' bin/blender_bridge.gdextension named as the dead scaffold it is Added the whole parity dimension, which the file did not mention at all: the node maps, socket resolution by name, the erosion stack, integer math in geometry nodes, fields-with-domains, and the rule that a transcription used as a reference can only agree with itself. proc-nodes had NO skill despite being half of the pair. Written from source: 120 registered classes against a README that says 59, the socket types, the lazy-field promise, the one-source-three-compilations erosion filter, the debug probes and why they live in noise_nodes.cpp, and the five places a new node type has to be registered -- miss one of the last two and a Blender graph loses the node silently. Numbers in both files were MEASURED 2026-09-04, not quoted: coverage 98/360 (27.2%) sockets by name 174 (81.7%), by index 33, UNRESOLVED 6 node_maps 6/6 axis 13/13 primitives 40/40 rot values 18/18 rot parity 17/17 phacelle 7/7 (9.5e-07) erosion_filter 6/6 (3.0e-08) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f13521e016b3be56a954fcfa316a460f415f1deb
by plankatron <fleeky@gmail.com> · 2026-09-04T19:01:31.000Z

Changed files (2) +574 -28

.claude/skills/blender-bridge/SKILL.md +286 -28
……@@ -1,16 +1,46 @@
11# Blender-Godot Bridge
22
3Real-time bidirectional scene sync between Blender and Godot via POSIX shared memory + Unix Domain Sockets.
3Two systems in one repo, and it matters which one you are in:
4
51. **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.
82. **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.
14Everything dated here was measured on that day; anything older is marked.
415
516## MANDATORY: Version Bump on EVERY Change
617
718**Before EVERY build, bump the version in BOTH files. No exceptions.**
8191. `blender_addon/blender_manifest.toml` — `version = "X.Y.Z"` (what Blender shows)
9202. `src/blender_bridge.cpp` — `"BlenderBridge vX.Y.Z:"` string (what Godot logs at startup)
103. Rebuild C++ AFTER the bump: `scons -j$(nproc)`
213. Rebuild C++ AFTER the bump — **all three targets**, see Build
11224. .so is **symlinked** to hyper-tps (no copy needed)
1223
13**Current version: 0.44.0** (branch: sdf-volume-bridge)
24**Current version: 0.58.0** (branch: `sdf-volume-bridge`, 22 commits unpushed
25to `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
40shadow the source. It used to write Blender 4.3's legacy `scripts/addons` path,
41so for a while the addon that actually loaded was a hand-copied snapshot while
42edits to the source did nothing. If behaviour does not change after an edit,
43check what the extensions dir actually points at before debugging the code.
1444
1545## When to Use
1646
……@@ -18,6 +48,7 @@ Real-time bidirectional scene sync between Blender and Godot via POSIX shared me
1848- Integrating Blender workflows with Godot (hyper-tps, godot-keet)
1949- Debugging SHM protocol, UDS transport, coordinate conversion
2050- Building or modifying the Blender addon or C++ GDExtension
51- **Any proc-nodes <-> Blender numerical parity work** (see that section)
2152
2253## Architecture
2354
……@@ -58,6 +89,11 @@ Godot (C++ GDExtension) Blender (Python Addon)
5889| `blender_addon/change_detector.py` | Detect Blender changes via depsgraph handler |
5990| `blender_addon/material_serializer.py` | Serialize Blender shader graph → Godot VisualShader JSON |
6091| `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 |
6197
6298### Build & Config
6399| File | Purpose |
……@@ -66,6 +102,142 @@ Godot (C++ GDExtension) Blender (Python Addon)
66102| `blender_addon/blender_manifest.toml` | Blender 5.0+ extension manifest |
67103| `install_addon.sh` | Symlink addon into Blender's extension dir |
68104
105---
106
107# proc-nodes parity — the active work
108
109Blender's geometry nodes are the authoring surface; [[proc-nodes]] is the engine
110that ships. A graph must produce the SAME NUMBERS on both sides. That is a
111different 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
117Four implementations turned out to be faithful ports of the WRONG reference —
118the iq hash, the euler order, the axis conversions, the erosion filter — and in
119two cases a test written afterwards certified the wrong behaviour as correct,
120because it compared against the same transcription the port was made from. The
121euler order was certified PASS twice that way, both times because the check
122asserted a **spelling** rather than a rotation.
123
124So parity tests spawn **headless Godot** and compare against the code that
125ships:
126
127```
128proc-nodes/demo/parity_driver.gd value nodes, via ProcNodeGraph.evaluate_socket
129proc-nodes/demo/phacelle_probe.gd ProcNodeGraph.debug_phacelle_noise
130proc-nodes/demo/erosion_probe.gd ProcNodeGraph.debug_erosion_filter
131proc-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`
135but defined in `noise_nodes.cpp`, because `erosion_cpp_shim.h` defines its
136functions non-inline and a second translation unit would multiply-define them.
137
138Corollary, learned the same day: **mutate the source and check the test goes
139red.** One mutation had a paren too many in its `sed` pattern, so the file never
140changed and "7/7 passed" meant nothing. And a good mutation test discriminates
141the 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
148decides what it accepts, so drift means a **silently dropped node**.
149`test/test_node_maps.py` asserts they stay inverse bijections. Generating one
150from the other would end the class of bug; nobody has.
151
152Socket resolution is **by name** since v0.54.0, falling back to the index only
153when the type there agrees (v0.53.0 stopped it carrying a vector into a scalar
154socket by averaging). So "the layouts differ" is not a defect; "the name is not
155on the Blender node" is. See the audit numbers under Testing.
156
157## The erosion stack
158
159proc-nodes' terrain filter, ported to Blender node groups bottom-up. Each layer
160verified against the shipping C++ before the next was built, which is why the
161top 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),
175where stripe frequency scaled with slope magnitude. The C++ moved to
176runevision's **fade approach**: constant stripe width, stripes fading toward a
177target as steepness drops. That is why it correlated **0.1848**, not because it
178was mistuned. The `.inc` is explicit that fading erosion at low altitude was a
179WORKAROUND 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
184proc-nodes uses an integer LCG with xor/shift. Both give lattice gradients in
185[-1,1] — same character, unrelated values, correlation **-0.0043**.
186
187It was asserted, untested, that Blender could not reproduce a C hash, and a
188product decision was nearly put to the user: change `erosion_hash` and with it
189every existing terrain, or accept the preview as permanently approximate.
190Neither was needed. **`FunctionNodeIntegerMath` wraps at 32 bits exactly like C**
191and **`FunctionNodeBitMath` has AND/OR/XOR/NOT/SHIFT/ROTATE**. The reasoning had
192been done from OUR NODE MAP instead of from Blender, and BitMath was one of the
193nodes the map was missing.
194
195**GradientNoise went -0.0043 -> 1.000000.** And it made the erosion chain WORSE,
1960.5157 -> 0.1848 — two wrong things had been partially cancelling, so 0.5157 was
197coincidence, not half-correctness.
198
199## Fields have a domain, and producers are lazy
200
201Blender's fields are lazy functions evaluated on the CONSUMER's domain. Ours
202were eager arrays sized by whatever geometry was passed in, and that one
203difference was the root of four separate divergences. Fixed in two stages:
204fields carry an `AttrDomain`, then a producer with no context leaves a PROMISE
205the consumer redeems against its own geometry. Binding and deferral live in the
206executor, so no intermediate node was touched.
207
208Same grid, same selection field, different domains, different correct answers:
209point domain 3 verts / 0 tris, face domain 6 verts / 4 tris.
210
211Consequence: `Position`, `Normal` and `NamedAttribute` no longer take a Geometry
212input, 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
69241## SHM Protocol
70242
71243### Header (64 bytes)
……@@ -177,17 +349,33 @@ The Rx pair correctly reverses the glTF exporter's conversion.
177349- Do NOT compute `rest * pose` — that doubles the transform
178350- `set_bone_pose_rotation(i, rest_rot * delta_rot)` — compose rest with delta for full pose
179351
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
356happily keeps executing a months-old library while the file you just compiled
357sits next to it looking current.
181358
182359```bash
183360cd /storage/projects/code/godot/blender-bridge
184scons -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
188cp bin/linux/*.so /storage/projects/code/godot/hyper-tps/bin/linux/
361for t in editor template_debug template_release; do
362 scons -j$(nproc) platform=linux target=$t || break
363done
364# bin/linux/libblender_bridge.linux.{editor,template_debug,template_release}.x86_64.so
189365```
190366
367No 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`
369and it has all three entries since 2026-09-04.
370
371**`blender-bridge/bin/blender_bridge.gdextension` is a dead scaffold** from the
372first commit. It has no `linux.editor` entry and names macos/windows files that
373do not exist. Nothing loads it (this repo has no `project.godot`) — do not
374"fix" a bridge problem there.
375
376Same trap, same shape as [[reference_hypertps-gdextension-stale-so]]: an
377untracked `.so` that CI never rebuilds and that lags its source silently.
378
191379## Live Testing
192380
1933811. 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
305493| **Textures** | SVT streaming tiles | File references |
306494| **Meshes** | Binary blob in SHM | Text-based .escn format |
307495| **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) |
309497
310498### Relation to Material Maker
311499
……@@ -329,8 +517,18 @@ Both systems share the concept of node-based material graphs. Integration opport
329517- Front-face winding: effectively CW in world space (Vulkan Y-flip in projection)
330518
331519### 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.
334532- **Roughness**: insert clamp(0,1) — Blender `saturatef()` = `clamp(r,0,1)`, NO abs()
335533- **Negative scale** (v0.19.28): Python detects `is_negative`, negates normals + reverses winding
336534 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
407605- **Area lights**: Mapped to OmniLight3D (Godot has no area light equivalent)
408606- **Light properties**: Only transform is synced — energy, color, range, attenuation are NOT synced
409607- **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)
411614
412615## Testing
413616
617Three kinds, and they need different runners.
618
414619```bash
415# C++ tests
416cd /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
426python3 test/test_python_shm.py # 10 tests
427python3 test/test_cross_language.py # 1 test
428python3 test/test_tile_baker.py # 26 tests
620BB=/storage/projects/code/godot/blender-bridge
621BL=/home/fleeky/3d-prog/blender/blender-5.2.0-linux-x64/blender # not on PATH
622cd $BB
623
624# 1. pure python — no Blender, no Godot
625python3 test/test_node_maps.py # 6/6
626python3 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
638Counts and deltas above were **measured 2026-09-04**, all green.
639
640The 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
643disk. 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
645until 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
656PROC_SOCKETS=/tmp/ps.json $BL --background --factory-startup --python test/audit_socket_names.py
429657```
430658
431150 total tests, all passing.
659Measured 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
671The last two are `CUSTOM` multi-sockets and may not be representable, so the
672realistic 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 —
676and doing so reported three of `PerlinNoiseNode`'s sockets as wrong when that
677node matches Blender exactly. Those three were queued for DELETION to satisfy a
678number the tool got wrong.
432679
433680## Completed Phases
434681
……@@ -438,3 +685,14 @@ python3 test/test_tile_baker.py # 26 tests
438685- v0.19.x: Material Graph Transfer (Blender node trees → Godot VisualShader graphs)
439686- v0.26.x: Bone/skeleton coordinate conversion fix (M→Rx for bones)
440687- 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`
.claude/skills/proc-nodes/SKILL.md +288 -0
……@@ -0,0 +1,288 @@
1# proc-nodes
2
3A Blender-Geometry-Nodes-shaped procedural geometry graph for Godot 4, as a
4GDExtension. It generates and manipulates meshes, curves, point clouds,
5volumes and instances inside the Godot editor, compiles GPU-capable subgraphs
6to Vulkan compute, and exports a node graph as a standalone GLSL density
7function for dc-terrain.
8
9**Verified 2026-09-04** against Godot 4.7.beta custom, branch
10`heightmap-erosion-toggle`.
11
12## When to Use
13
14- Working in `/storage/projects/code/godot/proc-nodes/`
15- Adding or changing a node type, socket, field or zone
16- Anything about the **erosion filter** (`src/shared/erosion_filter.inc`) —
17 it is shared with dc-terrain and hyper-tps terrain
18- **Numerical parity with Blender geometry nodes** — that half lives in
19 [[blender-bridge]], read that skill too
20- GLSL export / `compile_to_glsl_function` / GPU batching
21
22## Where things are
23
24| | |
25|---|---|
26| repo | `/storage/projects/code/godot/proc-nodes/` |
27| branch | `heightmap-erosion-toggle` |
28| remotes | `bare` (local), `pear` (punch://…/proc-nodes) |
29| Godot | `/storage/projects/code/godot/nvidia-rtx-godot/bin/godot.linuxbsd.editor.x86_64` |
30| sibling | `/storage/projects/code/godot/blender-bridge/` — the Blender half |
31
32```
33src/core/ ProcMesh, ProcCurves, ProcPointCloud, ProcInstances, ProcVolume,
34 attribute_storage, offset_indices, linear_allocator
35src/graph/ ProcNode, ProcNodeGraph, SocketValue, GraphExecutor,
36 GraphImporter (Blender JSON in), GraphExporter (out)
37src/nodes/ 15 category files — the node implementations
38src/gpu/ GpuCompiler, GpuBufferManager, erosion_filter_glsl.gen.h
39src/shared/ erosion_filter.inc <- THE filter, compiled as BOTH C++ and GLSL
40src/editor/ graph editor plugin
41demo/ headless probe scripts (see Probes)
42```
43
44## Node count: 120 registered, 118 instantiable
45
46`README.md` says **59 node types**; it has been stale for a long time.
47Measured from `src/register_types.cpp` and `ClassDB`:
48
49| file | classes | | file | classes |
50|---|---|---|---|---|
51| rotation_nodes | 18 | | attribute_nodes | 9 |
52| noise_nodes | 13 | | curve_nodes | 8 |
53| math_nodes | 12 | | mesh_primitive_nodes | 7 |
54| mesh_ops_nodes | 12 | | geometry_nodes | 6 |
55| input_nodes | 9 | | zone_nodes | 6 |
56| instance_nodes | 5 | | selection_nodes | 5 |
57| point_nodes | 3 | | volume_nodes | 3 |
58| utility_nodes | 2 | | | |
59
60Do not quote a count from the README or from here — run it:
61
62```bash
63godot --headless --path demo --script res://socket_dump.gd -- /tmp/ps.json
64python3 -c "import json;print(len(json.load(open('/tmp/ps.json'))))"
65```
66
67## Socket types
68
69`SocketType` in `src/graph/socket_value.h`:
70
71```
72Float 0 Int 1 Bool 2 Vector 3 Color 4 String 5 Geometry 6 Object 7
73Rotation 8 (quaternion) Matrix 9 (4x4)
74```
75
76`Rotation` and `Matrix` are first-class because Blender types its whole
77rotation/matrix family on them — euler triples would not round-trip (euler
78loses the winding a quaternion keeps, and there is no vector spelling of a 4x4).
79
80**Trap: `SocketValue::to_float()` takes `vec.x`** from a non-field scalar. Wire
81a vector into a float socket and it reads as a near-constant that LOOKS
82plausible; Blender averages x, y, z per point instead. That asymmetry hid a
83mis-wired test scene for months and reads as "Blender has more detail".
84
85## Fields carry a domain, and producers are LAZY
86
87A field is an array **plus a statement of what it is per**. Without the domain
88an array of 9 booleans over a 3x3 grid could be per-vertex or per-anything of
89that count — which is why nothing could produce or consume a per-face field.
90
91```cpp
92bool is_field; AttrDomain domain; int field_size; // socket_value.h:69
93bool is_lazy; int lazy_node; int lazy_socket; // the PROMISE
94```
95
96Blender's fields are lazy functions evaluated on the CONSUMER's domain —
97`Position` is not an array, it is a function — which is why Blender's Position
98node has no Geometry input. Nodes run in topological order, so a field producer
99runs before any consumer exists to give it context: it leaves a promise, and the
100consumer redeems it with `ProcEvalContext::materialize` once it knows its own
101geometry. Binding and deferral live in the **executor**, so no intermediate node
102needed changing.
103
104`Point` is the default domain so untouched code keeps its old meaning.
105
106Two regressions came out of this work, both caught only by measurement:
107removing `Position`'s input before deferral existed broke every chain through an
108intermediate node, and then deferring on ANY lazy input caught `SetPosition`
109itself.
110
111**Still missing**: nothing PRODUCES a per-face field. `DeleteGeometry` consumes
112one via point->face interpolation, so the Face branch is proven only through
113interpolation. And `Selection` exists on `SetPosition` and `DeleteGeometry` but
114not on `MergeByDistance`, `ResampleCurve`, `TrimCurve`, `ExtrudeMesh`,
115`DuplicateElements`, `ScaleElements` — the largest remaining source of
116index-fallback socket resolution.
117
118**`ProcMesh` has no edge array** — positions, triangles, face offsets only. So
119`MeshLine` outputs a point cloud where Blender outputs a mesh, and the whole
120edge domain is unreachable. Tangents and `face_offsets` are DROPPED whenever
121`DeleteGeometry` changes topology (deliberate — wrong tangents are harder to
122notice than absent ones — but a real loss).
123
124## The erosion filter — one source, three compilations
125
126`src/shared/erosion_filter.inc` is THE filter. It is compiled three ways:
127
128| consumer | how |
129|---|---|
130| proc-nodes / dc-terrain C++ | `src/shared/erosion_cpp_shim.h` — GLSL types as C++ |
131| the GPU | `src/gpu/erosion_filter_glsl.gen.h`, **generated** by `tools/gen_erosion_glsl.py` at build time |
132| the emitted density shader | `src/shared/erosion_wrapper_glsl.h` |
133
134The generated header is not checked in and hand-synced, deliberately: *"a
135checked-in copy is a copy, and copies of this particular algorithm are what the
136whole exercise exists to remove."* If it is stale the build is stale — there is
137no path where the shader and the C++ silently disagree.
138
139Entry points, `erosion_filter.inc`:
140
141```
142phacelle_noise the fade-approach primitive
143erosion_filter the octave loop; returns ErosionResult{delta, magnitude, ridge}
144erosion_node_eval_full:687 the wrapper ErosionNoiseNode calls (46 lines)
145erosion_node_eval_grad same, KEEPING the slope it already computed
146```
147
148`_grad` exists because `edge_intersect.glsl` used to reconstruct the gradient by
149central differences — six more full evaluations per edge, each seven octaves of
150phacelle noise. Measured on the real device the octave loop took the pipeline
151from 491 us to 3795 us per 32³ grid, and `edge_intersect` was 91% of it. It
152returns a `vec4` rather than taking an `out` parameter because `out` is a GLSL
153keyword the C++ shim would have to imitate, and this file is compiled as both.
154
155**The current filter is runevision's FADE approach**: constant stripe width,
156stripes fading toward a target as steepness drops. It replaced the frequency
157approach (2018 clayjohn / 2023 Fewes) where stripe frequency scaled with slope
158magnitude. The `.inc` records that fading erosion at low altitude was a
159WORKAROUND for the frequency approach's bulged valleys — *"we inherited a
160workaround for a defect and shipped it as a feature"*. Blender's
161`ProcNodes_ErosionNoise` is still a port of the OLD one, which is why it
162correlates 0.1848 rather than being merely mistuned.
163
164## Probes — how the other side checks itself against us
165
166`demo/` holds headless scripts so [[blender-bridge]] can compare against the
167code that SHIPS rather than a transcription of it:
168
169```bash
170G=/storage/projects/code/godot/nvidia-rtx-godot/bin/godot.linuxbsd.editor.x86_64
171$G --headless --path demo --script res://socket_dump.gd -- out.json
172$G --headless --path demo --script res://parity_driver.gd -- spec.json out.json
173$G --headless --path demo --script res://phacelle_probe.gd -- in.json out.json
174$G --headless --path demo --script res://erosion_probe.gd -- in.json out.json
175```
176
177Backed by static bindings on `ProcNodeGraph`:
178
179| binding | returns |
180|---|---|
181| `evaluate_socket(node, socket)` | one value node's output, no geometry needed |
182| `debug_phacelle_noise(...)` | `(interp.x, interp.y, side_dir.x, side_dir.y)` |
183| `debug_erosion_filter(...)` | `[delta.x, delta.y, delta.z, magnitude, ridge]` |
184
185**They are declared in `proc_node_graph.h` but DEFINED in `noise_nodes.cpp`** —
186`erosion_cpp_shim.h` defines its functions non-inline, so including it in a
187second translation unit is a multiple-definition link error. Add the next
188`debug_*` there too.
189
190## GPU pipeline
191
1921. `GraphExecutor` finds contiguous runs of GPU-capable geometry nodes
1932. `GpuCompiler` emits GLSL 450 compute from each node's `compile_glsl()`,
194 inserting the noise library when needed
1953. positions + normals upload as vec4-padded SSBOs; push constants carry the
196 element count and up to 7 floats; 64 threads/group
1974. pipelines persist in the `ExecutionPlan` across evaluations
1985. no `RenderingDevice` or a failed compile -> transparent CPU fallback
199
200`ProcNodeGraph::compile_to_glsl_function()` emits a standalone
201`float proc_density(vec3 pos)` for external systems — this is how dc-terrain
202evaluates a node graph inside its own shaders.
203
204⚠ See [[project_dc-gpu-bringup-starves-two-meshers-2026-09-03]]: a second
205`VkDevice` bring-up starves, and chunks meshed meanwhile come from a different
206mesher and STAY. Defer workers, never block.
207
208## Zones
209
210Input/Output pairs sharing a `zone_id`; the executor finds the pair by BFS from
211the input node and collects the body in topological order.
212
213- **Repeat** — body N times (clamped 1000), each iteration fed the last output
214- **ForEach** — once per vertex (clamped 10,000), results joined
215- **Simulation** — frame-persistent state + `delta_time`; `ProcGeometryNode`
216 re-evaluates continuously while one is present
217
218Both zone Input nodes have a `CUSTOM` multi-socket Geometry input that has no
219representable Blender counterpart — 2 of the 6 unresolved sockets, and probably
220permanent.
221
222## Serialization
223
224`ProcNodeGraph` is a `Resource` with custom `_get_property_list`/`_set`/`_get`:
225
226- `nodes` — Array of `ProcNode` sub-resources (class type + `graph_position`)
227- `connections` — flat `PackedInt32Array` of `[from_node, from_sock, to_node, to_sock]`
228- `input_defaults` — Dictionary, **only modified defaults**, keyed `"node:socket"`
229
230`ProcNode` captures its constructor defaults lazily on the first
231`set_input_default()`, and `is_input_default_modified()` compares against that
232snapshot. So a `.tres` holds deltas, not a full dump.
233
234⚠ A node whose sub-resource fails to load becomes `null` while `connections`
235still references it, and the graph then evaluates to **no mesh, silently**.
236`hyper-tps/graphs/terrain.tres` has been in that state since 2026-03-27.
237
238## Blender JSON import
239
240`GraphImporter::import_from_json()` consumes what
241`blender-bridge/blender_addon/geometry_serializer.py` produces:
242
243```json
244{"nodes": [{"id":0, "bl_type":"GeometryNodeMeshGrid", "proc_type":"MeshGrid",
245 "position":[0,200], "defaults":{"0":2.0}, "properties":{}}],
246 "connections": [{"from_node":0,"from_port":0,"to_node":1,"to_port":0}]}
247```
248
249- `proc_type` keys the factory; `null`/empty **skips the node with a warning** —
250 this is how an unmapped node disappears without anyone noticing
251- `defaults` keyed by socket INDEX, with Vector3/Color conversion from arrays
252- `position` converted from Blender's Y-up
253
254## Build
255
256```bash
257cd /storage/projects/code/godot/proc-nodes
258git submodule update --init --recursive # or symlink godot-cpp
259scons target=editor -j$(nproc) # editor plugin + doc data
260scons target=template_release -j$(nproc)
261```
262
263## Adding a node type — five places, and missing one is silent
264
2651. declare in `src/nodes/<category>_nodes.h` (`get_node_type` / `_title` / `_category`)
2662. implement the constructor (`input_defs`/`output_defs`) and `execute()`
2673. `GDREGISTER_CLASS` in `src/register_types.cpp`
2684. the factory in `src/graph/graph_importer.cpp`
2695. `GEOMETRY_NODE_MAP` in **blender-bridge**'s `geometry_serializer.py`, plus
270 its inverse in `geometry_deserializer.py`
271
272Skip 4 or 5 and the node exists but a Blender graph using it loses it without a
273word. Eight long-existing types were unmapped exactly that way.
274
275For GPU: override `supports_gpu()` and `compile_glsl()` operating on `pos` and
276`normal`.
277
278**Name the sockets what Blender names them.** blender-bridge resolves by name
279and falls back to the index only when the type there agrees. Run
280`audit_socket_names.py` (see [[blender-bridge]]) after adding sockets.
281
282## Docs
283
284- `docs/api.md` — ProcNodeGraph, ProcNode, ProcEvalContext, serialization, GPU
285- `docs/nodes.md` — per-node inputs/outputs/defaults
286- `README.md` — **stale on counts**; architecture and extension guide still good
287
288Post-mortems: `hyper-games` #60 and its comment; #44/#58 for terrain and GPU.