Focus VibeGeometry on vg-csg
22
.gitignore
vendored
|
|
@ -1,17 +1,7 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
node_modules/
|
||||
target/
|
||||
bin/
|
||||
obj/
|
||||
external/
|
||||
state/scratch-compaction-*.md
|
||||
experiments/renders/
|
||||
experiments/blender-cache/
|
||||
experiments/source-blends/
|
||||
experiments/source-repos/
|
||||
experiments/inspection/
|
||||
experiments/generated/realtimecsg-cpp-*.jsonl
|
||||
*.blend1
|
||||
*.blend2
|
||||
*.pdb
|
||||
*.obj
|
||||
*.mtl
|
||||
*.svg
|
||||
*.png
|
||||
*.jsonl
|
||||
|
|
|
|||
163
AGENTS.md
|
|
@ -1,144 +1,45 @@
|
|||
# VibeGeometry Instructions
|
||||
|
||||
## Project Purpose
|
||||
## Purpose
|
||||
|
||||
VibeGeometry is a research and implementation home for agent-authored Blender
|
||||
scene generation.
|
||||
VibeGeometry is the repository shell for the `vg-csg` Rust work surface.
|
||||
|
||||
The core problem is not "make a Python script that creates something shiny."
|
||||
The core problem is building a pipeline where an agent can understand a visual
|
||||
target, choose a procedural geometry strategy, construct inspectable Blender
|
||||
5.0+ geometry-node graphs through Python, render the result, and revise from
|
||||
evidence without accumulating clever junk.
|
||||
The previous scene-generation mission has been deleted from the live tree. Do
|
||||
not recreate it unless the user explicitly asks to recover a historical artifact
|
||||
from git history.
|
||||
|
||||
## Canonical State
|
||||
The live mission:
|
||||
|
||||
- Treat `state/map.yaml` as the canonical project map.
|
||||
- Treat `state/scratch.md` as disposable working memory for one bounded
|
||||
subgoal.
|
||||
- Treat `state/evidence.jsonl` as the distilled durable ledger of what was
|
||||
learned, verified, rejected, or accepted.
|
||||
- Treat `notes/vibegeometry-implementation-plan.md` as the current
|
||||
implementation plan.
|
||||
- Update `state/map.yaml` when project understanding changes.
|
||||
- Add evidence after meaningful research, implementation, verification, or
|
||||
rejected paths, but keep it distilled. Routine proof belongs in git history,
|
||||
renders, smoke artifacts, or targeted logs unless it changes what the next
|
||||
session should believe.
|
||||
- Do not turn notes, map, handoff, and evidence into four copies of the same
|
||||
brain. Distinct jobs or bust.
|
||||
|
||||
## Important Paths
|
||||
|
||||
- Project root: `E:\Projects\VibeGeometry`
|
||||
- Canonical map: `E:\Projects\VibeGeometry\state\map.yaml`
|
||||
- Scratch surface: `E:\Projects\VibeGeometry\state\scratch.md`
|
||||
- Distilled evidence ledger: `E:\Projects\VibeGeometry\state\evidence.jsonl`
|
||||
- Branch ledger: `E:\Projects\VibeGeometry\state\branches.json`
|
||||
- Handoff summary: `E:\Projects\VibeGeometry\notes\fresh-workspace-handoff.md`
|
||||
- System map: `E:\Projects\VibeGeometry\notes\vibegeometry-current-system-map.md`
|
||||
- Implementation plan: `E:\Projects\VibeGeometry\notes\vibegeometry-implementation-plan.md`
|
||||
- Architecture rationale: `E:\Projects\VibeGeometry\notes\architecture-rationale.md`
|
||||
- State CLI: `E:\Projects\VibeGeometry\tools\vibegeometry_state.py`
|
||||
- Pre-compaction helper: `E:\Projects\VibeGeometry\tools\vibegeometry_prepare_compaction.py`
|
||||
|
||||
## Useful Commands
|
||||
|
||||
Use the bundled Python runtime if `python` is not on PATH:
|
||||
|
||||
```powershell
|
||||
npm run state:status
|
||||
npm run state:prepare-compaction
|
||||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\vibegeometry_state.py' add-evidence --type research --status ok --note '...'
|
||||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\vibegeometry_state.py' add-branch --id branch-id --hypothesis '...'
|
||||
```text
|
||||
domain/tree intent
|
||||
-> LOD-aware CSG hierarchy
|
||||
-> camera/frustum selected cut
|
||||
-> triangle and collider output
|
||||
```
|
||||
|
||||
## Session Bootstrap And Re-entry Protocol
|
||||
## Ownership
|
||||
|
||||
On fresh session load, do this before implementation:
|
||||
- `crates/vg_csg` owns implementation.
|
||||
- The root workspace owns only wiring, submodule state, and very small guidance.
|
||||
- If a new domain-tree DSL, selected-cut planner, streaming mesh cache, or
|
||||
frustum query API is needed, start inside `vg-csg`.
|
||||
- Do not add root-level helper runtimes, prompt packs, visual translation
|
||||
corpora, or schema scaffolds.
|
||||
|
||||
1. read:
|
||||
- `state/map.yaml`
|
||||
- `notes/fresh-workspace-handoff.md`
|
||||
- `notes/vibegeometry-current-system-map.md`
|
||||
- `notes/vibegeometry-implementation-plan.md`
|
||||
2. run:
|
||||
- `npm run state:status`
|
||||
3. restate the current next action from the persisted state before starting
|
||||
edits
|
||||
4. if the user only asked to rehydrate or reorient, stop after orientation and
|
||||
wait for an explicit continue instruction
|
||||
## Commands
|
||||
|
||||
After compaction, resume, or suspicious continuity loss:
|
||||
```powershell
|
||||
git submodule update --init --recursive
|
||||
cargo test
|
||||
cargo clippy --all-targets --all-features
|
||||
cargo run -p vg_csg --release --example csg_perf_fixture
|
||||
```
|
||||
|
||||
1. rerun `npm run state:status`
|
||||
2. reread `state/map.yaml` and `notes/fresh-workspace-handoff.md`
|
||||
3. treat the persisted next action as authoritative unless fresh repo evidence
|
||||
contradicts it
|
||||
## Discipline
|
||||
|
||||
When context pressure is clearly rising:
|
||||
|
||||
1. stop broad exploration
|
||||
2. narrow the active move to one bounded pipeline organ
|
||||
3. persist map or handoff updates, plus distilled evidence only when the lesson
|
||||
changes future belief, before the blackout lands
|
||||
|
||||
When the user says to prepare for imminent compaction:
|
||||
|
||||
1. immediately write hot live context from memory into a new
|
||||
`state/scratch-compaction-<guid>.md` file before reading files, running
|
||||
status checks, or doing tidy persistence work
|
||||
2. run `tools/vibegeometry_prepare_compaction.py` before editing persistence
|
||||
surfaces
|
||||
3. use its warnings as the checklist for map, handoff, scratch, evidence, and
|
||||
git hygiene
|
||||
4. update only the state that actually needs to change
|
||||
5. after hot context has been folded into canonical persisted state, delete the
|
||||
`state/scratch-compaction-<guid>.md` file so stale emergency memory does not
|
||||
linger
|
||||
6. run `tools/vibegeometry_prepare_compaction.py` again after edits and scratch
|
||||
cleanup
|
||||
7. fix errors, address warnings, and commit the completed persistence pass
|
||||
unless the work is deliberately mid-surgery
|
||||
|
||||
## Operating Discipline
|
||||
|
||||
- Before substantial edits, restate the current mechanism and intended change.
|
||||
- Prefer one clear hypothesis per iteration.
|
||||
- Verify with checks that reflect the visual and procedural goal, not just
|
||||
proxy success.
|
||||
- Revert or discard changes that do not clearly improve the target.
|
||||
- If the diff grows while understanding shrinks, stop implementation and switch
|
||||
to diagnosis.
|
||||
- Keep maps and prose together; do not replace useful maps with prose-only
|
||||
explanations.
|
||||
- Commit completed work before it rots in the worktree unless the task is
|
||||
deliberately mid-surgery or the user asked to leave changes uncommitted.
|
||||
- Push after major completed passes when a remote exists, unless the user asked
|
||||
not to or there is a concrete reason to keep the commit local for a moment.
|
||||
- Before handoff, compaction, or phase boundaries, sync `state/map.yaml`, add
|
||||
distilled evidence when the lesson changes future belief, refresh
|
||||
`notes/fresh-workspace-handoff.md`, and make the next action explicit.
|
||||
|
||||
## Blender And Geometry-Node Doctrine
|
||||
|
||||
- Target Blender 5.0+ unless a task explicitly chooses another version.
|
||||
- Prefer geometry nodes for procedural geometry and Python for construction,
|
||||
orchestration, parameterization, render setup, and validation.
|
||||
- Keep generated node groups named, inspectable, and reusable. A script that
|
||||
creates an opaque graph is just a new kind of mess with sockets.
|
||||
- Preserve attribute flow explicitly. Name important attributes, document where
|
||||
they are created, transformed, consumed, and exposed.
|
||||
- Prefer proven Blender node idioms and current API guidance over invented
|
||||
graph tricks.
|
||||
- Treat reference images as evidence-bearing inputs. Record what visual traits
|
||||
are being targeted: silhouette, repetition, surface breakup, scale language,
|
||||
material response, lighting, camera, and color.
|
||||
- Render review is mandatory for visual claims. A script that runs is not a
|
||||
scene that works.
|
||||
- When a generated graph misses the target, revise one hypothesis at a time:
|
||||
geometry structure, distribution logic, material, lighting, camera, or scale.
|
||||
- Keep failed render attempts only when they teach a durable lesson. Otherwise
|
||||
archive lightly or delete them; the repo is not a museum of almost.
|
||||
- Do not bury visual intent inside Python constants. Scene briefs, graph
|
||||
manifests, and review artifacts should expose what the agent meant to build.
|
||||
- Keep the runtime path legible: input tree, selected cut, brush/CSG lowering,
|
||||
emitted triangles, and timing evidence.
|
||||
- Prefer tests and performance receipts inside `vg-csg` over root-level notes.
|
||||
- Preserve exact parity only for public behavior the crate claims to mirror.
|
||||
- RealtimeCSG is a reference seam, not an idol.
|
||||
- Do not let historical scene-generation work become a live dependency again.
|
||||
|
|
|
|||
8
Cargo.lock
generated
|
|
@ -807,14 +807,6 @@ dependencies = [
|
|||
"bevy_math",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vg_grammar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy_math",
|
||||
"vg_csg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
[workspace]
|
||||
members = ["crates/vg_csg", "crates/vg_grammar"]
|
||||
members = ["crates/vg_csg"]
|
||||
resolver = "3"
|
||||
|
|
|
|||
141
README.md
|
|
@ -1,113 +1,50 @@
|
|||
# VibeGeometry
|
||||
|
||||
VibeGeometry is a research and implementation home for agent-authored Blender
|
||||
scene generation.
|
||||
VibeGeometry is now the working shell around `vg-csg`.
|
||||
|
||||
The project exists to build a pipeline where Codex, or any other sufficiently
|
||||
useful local agent, can take reference images and visual instructions, plan a
|
||||
procedural geometry strategy, generate Blender Python scripts, construct
|
||||
complex geometry-node graphs, render the result, and revise from evidence
|
||||
instead of shrugging confidently at a pile of decorative node spaghetti.
|
||||
The previous scene-generation research era is gone from the live tree. It can
|
||||
stay in git history, where failed experiments are useful without being allowed
|
||||
to hold a steering wheel.
|
||||
|
||||
The target is Blender 5.0 and later. The repo treats geometry nodes as the
|
||||
main modeling surface, Python as the graph construction and orchestration
|
||||
language, and rendered output as the evidence loop.
|
||||
The live mission is narrower and sharper:
|
||||
|
||||
## Core Idea
|
||||
|
||||
VibeGeometry separates five things that are easy to smear together:
|
||||
|
||||
1. **Visual target**
|
||||
What the result should look like, grounded in reference images and explicit
|
||||
instructions.
|
||||
|
||||
2. **Geometry strategy**
|
||||
The procedural plan: primitives, fields, instances, curves, surfaces,
|
||||
simulation zones, attributes, materials, and camera composition.
|
||||
|
||||
3. **Node graph contract**
|
||||
The legible graph structure that should exist in Blender, including named
|
||||
groups, exposed parameters, attribute flow, and reusable node idioms.
|
||||
|
||||
4. **Python builder**
|
||||
The script that creates the scene, builds node groups, links sockets,
|
||||
assigns materials, places cameras and lights, and saves or renders the
|
||||
`.blend` result.
|
||||
|
||||
5. **Render review**
|
||||
What the actual output proves: visual match, graph sanity, missing geometry,
|
||||
material errors, performance pain, and rejected approaches.
|
||||
|
||||
The script is not the design. The render is not the truth. The node graph is
|
||||
the machine, and the repo exists to make that machine inspectable.
|
||||
|
||||
## Current Status
|
||||
|
||||
This is an initial scaffold.
|
||||
|
||||
Currently present:
|
||||
|
||||
- persistence surfaces for re-entry, evidence, planning, and compaction hygiene
|
||||
- starter architecture notes for the Blender scene-generation pipeline
|
||||
- a small `vibegeometry/` helper library for coordinate frames, deterministic
|
||||
fields, batching, Blender object/material/camera helpers, and verifier checks
|
||||
- experimental Rust crates: `vg_csg` for Bevy-math brush assembly and
|
||||
`vg_grammar` for semantic procedural claims feeding the CSG tree
|
||||
- a repo structure for schemas, prompts, examples, experiments, scripts, tools,
|
||||
notes, and durable state
|
||||
- small state inspection and pre-compaction helpers
|
||||
|
||||
Not present yet:
|
||||
|
||||
- scene brief schema
|
||||
- node graph manifest schema
|
||||
- render review schema
|
||||
- reference-image ingestion helpers
|
||||
- automated Blender render runner
|
||||
- visual comparison or reviewer loop
|
||||
|
||||
## Repo Tour
|
||||
|
||||
- `AGENTS.md`: operating discipline for humans and agents
|
||||
- `state/map.yaml`: canonical project map and current next action
|
||||
- `state/scratch.md`: disposable working memory for one bounded subgoal
|
||||
- `state/evidence.jsonl`: distilled belief-changing evidence
|
||||
- `state/branches.json`: active or closed investigation branches
|
||||
- `notes/fresh-workspace-handoff.md`: compact re-entry packet
|
||||
- `notes/vibegeometry-current-system-map.md`: current repo machinery
|
||||
- `notes/vibegeometry-implementation-plan.md`: implementation sequence
|
||||
- `vibegeometry/`: reusable procedural-scene helper library
|
||||
- `notes/architecture-rationale.md`: why the persistence surfaces exist
|
||||
- `docs/architecture/`: pipeline contracts and design notes
|
||||
- `docs/research/`: Blender geometry-node research notes
|
||||
- `schemas/`: future JSON schema contracts
|
||||
- `examples/`: future scene briefs, generated scripts, and review artifacts
|
||||
- `experiments/`: disposable render attempts and captures
|
||||
- `prompts/`: reusable agent prompts for scene planning and review
|
||||
- `scripts/`: utility scripts that are not core persistence machinery
|
||||
- `tools/vibegeometry_state.py`: state inspection and evidence helper
|
||||
- `tools/vibegeometry_prepare_compaction.py`: pre-compaction audit helper
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```powershell
|
||||
npm run state:status
|
||||
npm run state:prepare-compaction
|
||||
```text
|
||||
domain/tree intent
|
||||
-> LOD-aware CSG hierarchy
|
||||
-> camera/frustum selected cut
|
||||
-> triangle and collider output in milliseconds
|
||||
```
|
||||
|
||||
To add distilled evidence:
|
||||
The actual work surface is the Rust crate at:
|
||||
|
||||
- `crates/vg_csg`
|
||||
- upstream: `https://github.com/GameCult/vg-csg`
|
||||
|
||||
## What This Repo Owns
|
||||
|
||||
- The `vg-csg` submodule pointer.
|
||||
- A minimal Rust workspace that builds that crate.
|
||||
- Thin operating notes that keep future work pointed at runtime CSG, not legacy
|
||||
scene-generation scaffolding.
|
||||
|
||||
## What `vg-csg` Owns
|
||||
|
||||
- Brush and CSG tree APIs.
|
||||
- RealtimeCSG-shaped public seams where useful.
|
||||
- Parity fixtures for claimed public behavior.
|
||||
- Dirty rebuild and demand-frontier experiments.
|
||||
- Mesh/triangle output and receipt examples.
|
||||
- The future LOD-aware selected-cut runtime.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\vibegeometry_state.py' add-evidence --type design --status accepted --note '...'
|
||||
git submodule update --init --recursive
|
||||
cargo test
|
||||
cargo clippy --all-targets --all-features
|
||||
cargo run -p vg_csg --release --example csg_perf_fixture
|
||||
```
|
||||
|
||||
## Best Starting Points
|
||||
|
||||
- `AGENTS.md`
|
||||
- `state/map.yaml`
|
||||
- `notes/vibegeometry-current-system-map.md`
|
||||
- `notes/vibegeometry-implementation-plan.md`
|
||||
- `docs/architecture/pipeline-overview.md`
|
||||
|
||||
For Codex-driven work, read `AGENTS.md` before changing the repo. It contains
|
||||
the rules that keep the project from turning into a confident procedural hairball.
|
||||
If a future task needs domain-tree DSL work, put it in `vg-csg` unless there is
|
||||
a proven reason it must live outside the crate. The default answer is simple:
|
||||
the geometry machine belongs where the geometry machine runs.
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
[package]
|
||||
name = "vg_grammar"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Procedural grammar layer that emits semantic CSG claims for VibeGeometry"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
bevy_math = "0.17"
|
||||
vg_csg = { path = "../vg_csg" }
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
use bevy_math::Vec3;
|
||||
use vg_grammar::{ClaimTree, Frame, GalleryChainSpec, GrammarContext, Rule};
|
||||
|
||||
fn main() {
|
||||
let ctx = GrammarContext::new(7);
|
||||
let mut tree = ClaimTree::new();
|
||||
|
||||
GalleryChainSpec::new("dream gallery", 4)
|
||||
.in_frame(Frame::new(
|
||||
Vec3::ZERO,
|
||||
bevy_math::Quat::from_rotation_z(0.2),
|
||||
))
|
||||
.emit(&ctx, &mut tree);
|
||||
|
||||
let compiled = tree.compile();
|
||||
let tree_compiled = tree.compile_csg_tree().expect("example emits claims");
|
||||
let output = compiled.assembler.build();
|
||||
|
||||
println!(
|
||||
"claims={} solids={} voids={} brushes={} tree_nodes={} vertices={} triangles={} warnings={}",
|
||||
tree.claims().len(),
|
||||
compiled.solid_claims,
|
||||
compiled.void_claims,
|
||||
compiled.brush_ids.len(),
|
||||
tree_compiled.arena.nodes().len(),
|
||||
output.mesh.vertex_count(),
|
||||
output.mesh.triangle_count(),
|
||||
output.report.warnings.len()
|
||||
);
|
||||
}
|
||||
|
|
@ -1,355 +0,0 @@
|
|||
use bevy_math::{Quat, Vec3};
|
||||
use vg_csg::{
|
||||
Assembler, BrushId, CsgBranchOp, CsgNodeId, CsgOperationType, CsgTree, CsgTreeArena,
|
||||
CsgTreeBranch, MaterialId, Primitive,
|
||||
};
|
||||
|
||||
use crate::Frame;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ClaimTag(pub String);
|
||||
|
||||
impl ClaimTag {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ClaimKind {
|
||||
Solid,
|
||||
Void,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Claim {
|
||||
pub name: String,
|
||||
pub kind: ClaimKind,
|
||||
pub center: Vec3,
|
||||
pub size: Vec3,
|
||||
pub rotation: Quat,
|
||||
pub material: MaterialId,
|
||||
pub tags: Vec<ClaimTag>,
|
||||
}
|
||||
|
||||
impl Claim {
|
||||
pub fn solid_box(
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
material: MaterialId,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
kind: ClaimKind::Solid,
|
||||
center,
|
||||
size,
|
||||
rotation: Quat::IDENTITY,
|
||||
material,
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn solid_oriented_box(
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
material: MaterialId,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
kind: ClaimKind::Solid,
|
||||
center,
|
||||
size,
|
||||
rotation,
|
||||
material,
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn void_box(name: impl Into<String>, center: Vec3, size: Vec3) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
kind: ClaimKind::Void,
|
||||
center,
|
||||
size,
|
||||
rotation: Quat::IDENTITY,
|
||||
material: MaterialId::default(),
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn void_oriented_box(
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
kind: ClaimKind::Void,
|
||||
center,
|
||||
size,
|
||||
rotation,
|
||||
material: MaterialId::default(),
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tagged(mut self, tag: impl Into<String>) -> Self {
|
||||
self.tags.push(ClaimTag::new(tag));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn in_frame(mut self, frame: Frame) -> Self {
|
||||
self.center = frame.point(self.center);
|
||||
self.rotation = frame.rotation * self.rotation;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_axis_aligned(&self) -> bool {
|
||||
self.rotation.abs_diff_eq(Quat::IDENTITY, 1.0e-6)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ClaimTree {
|
||||
claims: Vec<Claim>,
|
||||
}
|
||||
|
||||
impl ClaimTree {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, claim: Claim) {
|
||||
self.claims.push(claim);
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, claims: impl IntoIterator<Item = Claim>) {
|
||||
self.claims.extend(claims);
|
||||
}
|
||||
|
||||
pub fn claims(&self) -> &[Claim] {
|
||||
&self.claims
|
||||
}
|
||||
|
||||
pub fn count_kind(&self, kind: ClaimKind) -> usize {
|
||||
self.claims
|
||||
.iter()
|
||||
.filter(|claim| claim.kind == kind)
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn compile(&self) -> CompileReport {
|
||||
let mut assembler = Assembler::new();
|
||||
let mut brush_ids = Vec::with_capacity(self.claims.len());
|
||||
for claim in &self.claims {
|
||||
let brush_id = match claim.kind {
|
||||
ClaimKind::Solid if claim.is_axis_aligned() => assembler.solid_box(
|
||||
&claim.name,
|
||||
vg_csg::Aabb::from_center_size(claim.center, claim.size),
|
||||
claim.material,
|
||||
),
|
||||
ClaimKind::Solid => assembler.solid_oriented_box(
|
||||
&claim.name,
|
||||
claim.center,
|
||||
claim.size,
|
||||
claim.rotation,
|
||||
claim.material,
|
||||
),
|
||||
ClaimKind::Void if claim.is_axis_aligned() => assembler.cut_box(
|
||||
&claim.name,
|
||||
vg_csg::Aabb::from_center_size(claim.center, claim.size),
|
||||
),
|
||||
ClaimKind::Void => assembler.cut_oriented_box(
|
||||
&claim.name,
|
||||
claim.center,
|
||||
claim.size,
|
||||
claim.rotation,
|
||||
),
|
||||
};
|
||||
brush_ids.push(brush_id);
|
||||
}
|
||||
|
||||
CompileReport {
|
||||
assembler,
|
||||
brush_ids,
|
||||
solid_claims: self.count_kind(ClaimKind::Solid),
|
||||
void_claims: self.count_kind(ClaimKind::Void),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_csg_tree(&self) -> Option<TreeCompileReport> {
|
||||
if self.claims.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let mut solid_nodes = Vec::new();
|
||||
let mut void_nodes = Vec::new();
|
||||
|
||||
for claim in &self.claims {
|
||||
let primitive = claim.primitive();
|
||||
let brush = arena.generate_brush(
|
||||
&claim.name,
|
||||
CsgOperationType::Additive,
|
||||
primitive,
|
||||
claim.material,
|
||||
);
|
||||
|
||||
match claim.kind {
|
||||
ClaimKind::Solid => solid_nodes.push(brush.node),
|
||||
ClaimKind::Void => void_nodes.push(brush.node),
|
||||
}
|
||||
}
|
||||
|
||||
let solid_root = branch_or_single(&mut arena, "solids", CsgBranchOp::Addition, solid_nodes);
|
||||
let root = if void_nodes.is_empty() {
|
||||
solid_root
|
||||
} else {
|
||||
let void_root =
|
||||
branch_or_single(&mut arena, "voids", CsgBranchOp::Addition, void_nodes);
|
||||
arena
|
||||
.generate_branch(
|
||||
"grammar result",
|
||||
CsgBranchOp::Subtraction,
|
||||
[solid_root, void_root],
|
||||
)
|
||||
.node
|
||||
};
|
||||
|
||||
Some(TreeCompileReport {
|
||||
tree: arena.generate_tree(root),
|
||||
arena,
|
||||
solid_claims: self.count_kind(ClaimKind::Solid),
|
||||
void_claims: self.count_kind(ClaimKind::Void),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompileReport {
|
||||
pub assembler: Assembler,
|
||||
pub brush_ids: Vec<BrushId>,
|
||||
pub solid_claims: usize,
|
||||
pub void_claims: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TreeCompileReport {
|
||||
pub arena: CsgTreeArena,
|
||||
pub tree: CsgTree,
|
||||
pub solid_claims: usize,
|
||||
pub void_claims: usize,
|
||||
}
|
||||
|
||||
impl TreeCompileReport {
|
||||
pub fn compile_to_assembler(&self) -> Assembler {
|
||||
self.arena.compile_tree_to_assembler(self.tree)
|
||||
}
|
||||
}
|
||||
|
||||
fn branch_or_single(
|
||||
arena: &mut CsgTreeArena,
|
||||
name: &str,
|
||||
op: CsgBranchOp,
|
||||
nodes: Vec<CsgNodeId>,
|
||||
) -> CsgNodeId {
|
||||
if nodes.len() == 1 {
|
||||
nodes[0]
|
||||
} else {
|
||||
CsgTreeBranch {
|
||||
node: arena.generate_branch(name, op, nodes).node,
|
||||
}
|
||||
.node
|
||||
}
|
||||
}
|
||||
|
||||
impl Claim {
|
||||
fn primitive(&self) -> Primitive {
|
||||
if self.is_axis_aligned() {
|
||||
Primitive::Box {
|
||||
bounds: vg_csg::Aabb::from_center_size(self.center, self.size),
|
||||
}
|
||||
} else {
|
||||
Primitive::OrientedBox {
|
||||
center: self.center,
|
||||
size: self.size,
|
||||
rotation: self.rotation,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_math::Vec3;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn claim_tree_compiles_solids_and_voids_to_brushes() {
|
||||
let mut tree = ClaimTree::new();
|
||||
tree.push(Claim::solid_box(
|
||||
"wall",
|
||||
Vec3::ZERO,
|
||||
Vec3::new(4.0, 0.25, 3.0),
|
||||
MaterialId(1),
|
||||
));
|
||||
tree.push(Claim::void_box(
|
||||
"door",
|
||||
Vec3::new(0.0, 0.0, -0.25),
|
||||
Vec3::new(1.0, 0.5, 1.5),
|
||||
));
|
||||
|
||||
let report = tree.compile();
|
||||
assert_eq!(report.solid_claims, 1);
|
||||
assert_eq!(report.void_claims, 1);
|
||||
assert_eq!(report.brush_ids.len(), 2);
|
||||
assert_eq!(report.assembler.brushes().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_tree_compiles_nested_csg_tree_intent() {
|
||||
let mut tree = ClaimTree::new();
|
||||
tree.push(Claim::solid_box(
|
||||
"wall",
|
||||
Vec3::ZERO,
|
||||
Vec3::new(4.0, 0.25, 3.0),
|
||||
MaterialId(1),
|
||||
));
|
||||
tree.push(Claim::void_box(
|
||||
"door",
|
||||
Vec3::new(0.0, 0.0, -0.25),
|
||||
Vec3::new(1.0, 0.5, 1.5),
|
||||
));
|
||||
|
||||
let report = tree.compile_csg_tree().expect("non-empty tree");
|
||||
assert_eq!(report.solid_claims, 1);
|
||||
assert_eq!(report.void_claims, 1);
|
||||
assert_eq!(report.compile_to_assembler().brushes().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oriented_claims_compile_to_oriented_brushes() {
|
||||
let mut tree = ClaimTree::new();
|
||||
tree.push(Claim::solid_oriented_box(
|
||||
"angled wall",
|
||||
Vec3::ZERO,
|
||||
Vec3::new(4.0, 0.25, 3.0),
|
||||
Quat::from_rotation_z(0.25),
|
||||
MaterialId(1),
|
||||
));
|
||||
tree.push(Claim::void_oriented_box(
|
||||
"angled door",
|
||||
Vec3::ZERO,
|
||||
Vec3::new(1.0, 0.5, 1.5),
|
||||
Quat::from_rotation_z(0.25),
|
||||
));
|
||||
|
||||
let report = tree.compile();
|
||||
assert_eq!(report.assembler.brushes().len(), 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
use vg_csg::MaterialId;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Materials {
|
||||
pub wall: MaterialId,
|
||||
pub floor: MaterialId,
|
||||
pub ceiling: MaterialId,
|
||||
pub trim: MaterialId,
|
||||
}
|
||||
|
||||
impl Default for Materials {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wall: MaterialId(1),
|
||||
floor: MaterialId(2),
|
||||
ceiling: MaterialId(3),
|
||||
trim: MaterialId(4),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GrammarContext {
|
||||
pub seed: u64,
|
||||
pub materials: Materials,
|
||||
pub world_scale: f32,
|
||||
}
|
||||
|
||||
impl GrammarContext {
|
||||
pub fn new(seed: u64) -> Self {
|
||||
Self {
|
||||
seed,
|
||||
materials: Materials::default(),
|
||||
world_scale: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unit(&self, salt: impl AsRef<str>) -> f32 {
|
||||
let mut hash = self.seed ^ 0x9E37_79B9_7F4A_7C15;
|
||||
for byte in salt.as_ref().bytes() {
|
||||
hash ^= u64::from(byte);
|
||||
hash = hash.wrapping_mul(0x1000_0000_01B3);
|
||||
hash ^= hash >> 32;
|
||||
}
|
||||
let bits = (hash >> 40) as u32;
|
||||
bits as f32 / 0x00FF_FFFFu32 as f32
|
||||
}
|
||||
|
||||
pub fn range(&self, salt: impl AsRef<str>, min: f32, max: f32) -> f32 {
|
||||
min + (max - min) * self.unit(salt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GrammarContext {
|
||||
fn default() -> Self {
|
||||
Self::new(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
use bevy_math::{Quat, Vec3};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Frame {
|
||||
pub origin: Vec3,
|
||||
pub rotation: Quat,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub const IDENTITY: Self = Self {
|
||||
origin: Vec3::ZERO,
|
||||
rotation: Quat::IDENTITY,
|
||||
};
|
||||
|
||||
pub fn new(origin: Vec3, rotation: Quat) -> Self {
|
||||
Self { origin, rotation }
|
||||
}
|
||||
|
||||
pub fn translated(self, local_offset: Vec3) -> Self {
|
||||
Self {
|
||||
origin: self.point(local_offset),
|
||||
rotation: self.rotation,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rotated(self, local_rotation: Quat) -> Self {
|
||||
Self {
|
||||
origin: self.origin,
|
||||
rotation: self.rotation * local_rotation,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn point(self, local: Vec3) -> Vec3 {
|
||||
self.origin + self.rotation * local
|
||||
}
|
||||
|
||||
pub fn vector(self, local: Vec3) -> Vec3 {
|
||||
self.rotation * local
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
//! Procedural grammar for emitting semantic CSG claims.
|
||||
//!
|
||||
//! `vg_grammar` is the authoring layer above `vg_csg`: rules describe why
|
||||
//! space exists, then compile into solid and void brush claims.
|
||||
|
||||
mod claim;
|
||||
mod context;
|
||||
mod frame;
|
||||
mod rules;
|
||||
|
||||
pub use claim::{Claim, ClaimKind, ClaimTag, ClaimTree, CompileReport, TreeCompileReport};
|
||||
pub use context::{GrammarContext, Materials};
|
||||
pub use frame::Frame;
|
||||
pub use rules::{CorridorSpec, DoorSpec, GalleryChainSpec, RoomSpec, Rule, RuleSet};
|
||||
|
|
@ -1,461 +0,0 @@
|
|||
use bevy_math::Vec3;
|
||||
|
||||
use crate::{Claim, ClaimTree, Frame, GrammarContext};
|
||||
|
||||
pub trait Rule {
|
||||
fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RuleSet {
|
||||
rules: Vec<RuleNode>,
|
||||
}
|
||||
|
||||
impl RuleSet {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with(mut self, rule: impl Into<RuleNode>) -> Self {
|
||||
self.rules.push(rule.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for RuleSet {
|
||||
fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) {
|
||||
for rule in &self.rules {
|
||||
rule.emit(ctx, tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RuleNode {
|
||||
Room(RoomSpec),
|
||||
Corridor(CorridorSpec),
|
||||
Door(DoorSpec),
|
||||
GalleryChain(GalleryChainSpec),
|
||||
}
|
||||
|
||||
impl Rule for RuleNode {
|
||||
fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) {
|
||||
match self {
|
||||
Self::Room(rule) => rule.emit(ctx, tree),
|
||||
Self::Corridor(rule) => rule.emit(ctx, tree),
|
||||
Self::Door(rule) => rule.emit(ctx, tree),
|
||||
Self::GalleryChain(rule) => rule.emit(ctx, tree),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomSpec> for RuleNode {
|
||||
fn from(value: RoomSpec) -> Self {
|
||||
Self::Room(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CorridorSpec> for RuleNode {
|
||||
fn from(value: CorridorSpec) -> Self {
|
||||
Self::Corridor(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DoorSpec> for RuleNode {
|
||||
fn from(value: DoorSpec) -> Self {
|
||||
Self::Door(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GalleryChainSpec> for RuleNode {
|
||||
fn from(value: GalleryChainSpec) -> Self {
|
||||
Self::GalleryChain(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoomSpec {
|
||||
pub name: String,
|
||||
pub center: Vec3,
|
||||
pub size: Vec3,
|
||||
pub frame: Frame,
|
||||
pub wall_thickness: f32,
|
||||
}
|
||||
|
||||
impl RoomSpec {
|
||||
pub fn new(name: impl Into<String>, center: Vec3, size: Vec3) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
center,
|
||||
size,
|
||||
frame: Frame::IDENTITY,
|
||||
wall_thickness: 0.25,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = frame;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for RoomSpec {
|
||||
fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) {
|
||||
let half = self.size * 0.5;
|
||||
let t = self.wall_thickness;
|
||||
let wall_height = self.size.z;
|
||||
let wall_z = self.center.z + wall_height * 0.5;
|
||||
let frame = self.frame;
|
||||
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} floor", self.name),
|
||||
self.center + Vec3::new(0.0, 0.0, -t * 0.5),
|
||||
Vec3::new(self.size.x, self.size.y, t),
|
||||
ctx.materials.floor,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("room")
|
||||
.tagged("floor"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} ceiling", self.name),
|
||||
self.center + Vec3::new(0.0, 0.0, wall_height + t * 0.5),
|
||||
Vec3::new(self.size.x, self.size.y, t),
|
||||
ctx.materials.ceiling,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("room")
|
||||
.tagged("ceiling"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} north wall", self.name),
|
||||
Vec3::new(self.center.x, self.center.y + half.y, wall_z),
|
||||
Vec3::new(self.size.x, t, wall_height),
|
||||
ctx.materials.wall,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("room")
|
||||
.tagged("wall"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} south wall", self.name),
|
||||
Vec3::new(self.center.x, self.center.y - half.y, wall_z),
|
||||
Vec3::new(self.size.x, t, wall_height),
|
||||
ctx.materials.wall,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("room")
|
||||
.tagged("wall"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} east wall", self.name),
|
||||
Vec3::new(self.center.x + half.x, self.center.y, wall_z),
|
||||
Vec3::new(t, self.size.y, wall_height),
|
||||
ctx.materials.wall,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("room")
|
||||
.tagged("wall"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} west wall", self.name),
|
||||
Vec3::new(self.center.x - half.x, self.center.y, wall_z),
|
||||
Vec3::new(t, self.size.y, wall_height),
|
||||
ctx.materials.wall,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("room")
|
||||
.tagged("wall"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CorridorSpec {
|
||||
pub name: String,
|
||||
pub center: Vec3,
|
||||
pub length: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub frame: Frame,
|
||||
pub wall_thickness: f32,
|
||||
}
|
||||
|
||||
impl CorridorSpec {
|
||||
pub fn along_x(name: impl Into<String>, center: Vec3, length: f32) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
center,
|
||||
length,
|
||||
width: 2.0,
|
||||
height: 2.6,
|
||||
frame: Frame::IDENTITY,
|
||||
wall_thickness: 0.2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = frame;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for CorridorSpec {
|
||||
fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) {
|
||||
let t = self.wall_thickness;
|
||||
let z = self.center.z + self.height * 0.5;
|
||||
let frame = self.frame;
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} floor", self.name),
|
||||
self.center + Vec3::new(0.0, 0.0, -t * 0.5),
|
||||
Vec3::new(self.length, self.width, t),
|
||||
ctx.materials.floor,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("corridor")
|
||||
.tagged("floor"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} left wall", self.name),
|
||||
Vec3::new(self.center.x, self.center.y + self.width * 0.5, z),
|
||||
Vec3::new(self.length, t, self.height),
|
||||
ctx.materials.wall,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("corridor")
|
||||
.tagged("wall"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} right wall", self.name),
|
||||
Vec3::new(self.center.x, self.center.y - self.width * 0.5, z),
|
||||
Vec3::new(self.length, t, self.height),
|
||||
ctx.materials.wall,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("corridor")
|
||||
.tagged("wall"),
|
||||
);
|
||||
tree.push(
|
||||
Claim::solid_box(
|
||||
format!("{} ceiling", self.name),
|
||||
self.center + Vec3::new(0.0, 0.0, self.height + t * 0.5),
|
||||
Vec3::new(self.length, self.width, t),
|
||||
ctx.materials.ceiling,
|
||||
)
|
||||
.in_frame(frame)
|
||||
.tagged("corridor")
|
||||
.tagged("ceiling"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DoorSpec {
|
||||
pub name: String,
|
||||
pub center: Vec3,
|
||||
pub size: Vec3,
|
||||
pub frame: Frame,
|
||||
}
|
||||
|
||||
impl DoorSpec {
|
||||
pub fn new(name: impl Into<String>, center: Vec3, size: Vec3) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
center,
|
||||
size,
|
||||
frame: Frame::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = frame;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for DoorSpec {
|
||||
fn emit(&self, _ctx: &GrammarContext, tree: &mut ClaimTree) {
|
||||
tree.push(
|
||||
Claim::void_box(&self.name, self.center, self.size)
|
||||
.in_frame(self.frame)
|
||||
.tagged("door")
|
||||
.tagged("void"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GalleryChainSpec {
|
||||
pub name: String,
|
||||
pub frame: Frame,
|
||||
pub rooms: usize,
|
||||
pub base_room_size: Vec3,
|
||||
pub corridor_length: f32,
|
||||
}
|
||||
|
||||
impl GalleryChainSpec {
|
||||
pub fn new(name: impl Into<String>, rooms: usize) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
frame: Frame::IDENTITY,
|
||||
rooms: rooms.max(1),
|
||||
base_room_size: Vec3::new(5.0, 4.0, 3.0),
|
||||
corridor_length: 3.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = frame;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for GalleryChainSpec {
|
||||
fn emit(&self, ctx: &GrammarContext, tree: &mut ClaimTree) {
|
||||
let mut cursor_x = 0.0;
|
||||
for index in 0..self.rooms {
|
||||
let width_jitter = ctx.range(format!("{}:{index}:width", self.name), -0.4, 0.6);
|
||||
let depth_jitter = ctx.range(format!("{}:{index}:depth", self.name), -0.25, 0.5);
|
||||
let room_size = self.base_room_size + Vec3::new(width_jitter, depth_jitter, 0.0);
|
||||
let room_center = Vec3::new(cursor_x, 0.0, 0.0);
|
||||
|
||||
RoomSpec::new(
|
||||
format!("{} room {index}", self.name),
|
||||
room_center,
|
||||
room_size,
|
||||
)
|
||||
.in_frame(self.frame)
|
||||
.emit(ctx, tree);
|
||||
|
||||
if index + 1 < self.rooms {
|
||||
let room_half = room_size.x * 0.5;
|
||||
let next_width = self.base_room_size.x
|
||||
+ ctx.range(format!("{}:{}:width", self.name, index + 1), -0.4, 0.6);
|
||||
let next_half = next_width * 0.5;
|
||||
let corridor_center_x = cursor_x + room_half + self.corridor_length * 0.5;
|
||||
let door_x = cursor_x + room_half;
|
||||
|
||||
DoorSpec::new(
|
||||
format!("{} doorway {index}", self.name),
|
||||
Vec3::new(door_x, 0.0, 1.1),
|
||||
Vec3::new(0.8, 1.5, 2.2),
|
||||
)
|
||||
.in_frame(self.frame)
|
||||
.emit(ctx, tree);
|
||||
|
||||
CorridorSpec::along_x(
|
||||
format!("{} corridor {index}", self.name),
|
||||
Vec3::new(corridor_center_x, 0.0, 0.0),
|
||||
self.corridor_length,
|
||||
)
|
||||
.in_frame(self.frame)
|
||||
.emit(ctx, tree);
|
||||
|
||||
let next_door_x = cursor_x + room_half + self.corridor_length;
|
||||
DoorSpec::new(
|
||||
format!("{} doorway {}b", self.name, index),
|
||||
Vec3::new(next_door_x, 0.0, 1.1),
|
||||
Vec3::new(0.8, 1.5, 2.2),
|
||||
)
|
||||
.in_frame(self.frame)
|
||||
.emit(ctx, tree);
|
||||
|
||||
cursor_x += room_half + self.corridor_length + next_half;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_math::Quat;
|
||||
use vg_csg::Aabb;
|
||||
|
||||
use super::*;
|
||||
use crate::ClaimKind;
|
||||
|
||||
#[test]
|
||||
fn room_corridor_door_grammar_emits_semantic_claims_and_mesh() {
|
||||
let ctx = GrammarContext::new(42);
|
||||
let mut tree = ClaimTree::new();
|
||||
RoomSpec::new("gallery", Vec3::ZERO, Vec3::new(6.0, 5.0, 3.0)).emit(&ctx, &mut tree);
|
||||
CorridorSpec::along_x("artery", Vec3::new(5.0, 0.0, 0.0), 5.0).emit(&ctx, &mut tree);
|
||||
DoorSpec::new(
|
||||
"gallery east doorway",
|
||||
Vec3::new(3.0, 0.0, 1.1),
|
||||
Vec3::new(0.7, 1.4, 2.2),
|
||||
)
|
||||
.emit(&ctx, &mut tree);
|
||||
|
||||
assert_eq!(tree.count_kind(ClaimKind::Solid), 10);
|
||||
assert_eq!(tree.count_kind(ClaimKind::Void), 1);
|
||||
assert!(
|
||||
tree.claims()
|
||||
.iter()
|
||||
.any(|claim| claim.tags.iter().any(|tag| tag.0 == "door"))
|
||||
);
|
||||
|
||||
let report = tree.compile();
|
||||
let tree_report = tree.compile_csg_tree().expect("claims compile to tree");
|
||||
let output = report.assembler.build();
|
||||
let door_void = Aabb::from_center_size(Vec3::new(3.0, 0.0, 1.1), Vec3::new(0.7, 1.4, 2.2));
|
||||
|
||||
assert!(output.mesh.triangle_count() > 60);
|
||||
assert!(output.report.warnings.is_empty());
|
||||
assert!(tree_report.arena.nodes().len() > report.brush_ids.len());
|
||||
for tri in output.mesh.indices.chunks_exact(3) {
|
||||
let center = (output.mesh.positions[tri[0] as usize]
|
||||
+ output.mesh.positions[tri[1] as usize]
|
||||
+ output.mesh.positions[tri[2] as usize])
|
||||
/ 3.0;
|
||||
assert!(!door_void.contains_point_strict(center, 1.0e-4));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gallery_chain_uses_seeded_variation_and_frames() {
|
||||
let ctx = GrammarContext::new(99);
|
||||
let mut tree = ClaimTree::new();
|
||||
let frame = Frame::new(Vec3::new(10.0, 0.0, 0.0), Quat::from_rotation_z(0.4));
|
||||
GalleryChainSpec::new("chain", 3)
|
||||
.in_frame(frame)
|
||||
.emit(&ctx, &mut tree);
|
||||
|
||||
assert_eq!(tree.count_kind(ClaimKind::Solid), 26);
|
||||
assert_eq!(tree.count_kind(ClaimKind::Void), 4);
|
||||
assert!(tree.claims().iter().any(|claim| !claim.is_axis_aligned()));
|
||||
|
||||
let report = tree.compile();
|
||||
let tree_report = tree.compile_csg_tree().expect("claims compile to tree");
|
||||
let output = report.assembler.build();
|
||||
assert!(output.mesh.triangle_count() > 150);
|
||||
assert!(output.report.warnings.is_empty());
|
||||
assert!(tree_report.arena.nodes().len() > report.brush_ids.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruleset_composes_rules_without_losing_claims() {
|
||||
let ctx = GrammarContext::new(5);
|
||||
let mut tree = ClaimTree::new();
|
||||
RuleSet::new()
|
||||
.with(RoomSpec::new("a", Vec3::ZERO, Vec3::new(4.0, 4.0, 3.0)))
|
||||
.with(DoorSpec::new(
|
||||
"a door",
|
||||
Vec3::new(2.0, 0.0, 1.0),
|
||||
Vec3::new(0.7, 1.2, 2.0),
|
||||
))
|
||||
.emit(&ctx, &mut tree);
|
||||
|
||||
assert_eq!(tree.count_kind(ClaimKind::Solid), 6);
|
||||
assert_eq!(tree.count_kind(ClaimKind::Void), 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
# Pipeline Overview
|
||||
|
||||
VibeGeometry's intended pipeline turns visual intent into Blender geometry-node
|
||||
machinery, then uses rendered evidence to decide what actually worked.
|
||||
|
||||
```text
|
||||
reference images + instructions
|
||||
-> scene brief
|
||||
-> visual trait extraction
|
||||
-> geometry strategy
|
||||
-> node graph manifest
|
||||
-> Blender Python builder
|
||||
-> generated scene and render
|
||||
-> render review
|
||||
```
|
||||
|
||||
## Scene Brief
|
||||
|
||||
The scene brief is the target contract. It should say what the scene needs to
|
||||
look like before it says how to build it.
|
||||
|
||||
Expected contents:
|
||||
|
||||
- reference image paths or URLs
|
||||
- user instructions
|
||||
- target visual traits
|
||||
- constraints and exclusions
|
||||
- camera and composition notes
|
||||
- material and lighting expectations
|
||||
- acceptance criteria
|
||||
- open questions
|
||||
|
||||
## Geometry Strategy
|
||||
|
||||
The geometry strategy is plain-language reasoning about how the target can be
|
||||
made procedurally. It should explain primitives, distributions, curves,
|
||||
surfaces, instances, attributes, material variation, and scale relationships.
|
||||
|
||||
This is where the agent proves it knows what machine it is about to build.
|
||||
|
||||
## Node Graph Manifest
|
||||
|
||||
The node graph manifest is the inspectable design for Blender. It should name:
|
||||
|
||||
- geometry node groups
|
||||
- exposed sockets and default values
|
||||
- attribute names and where they flow
|
||||
- modifiers and object targets
|
||||
- materials and shader parameters
|
||||
- reusable graph idioms
|
||||
- performance risks or version-sensitive API assumptions
|
||||
|
||||
## Python Builder
|
||||
|
||||
The Python builder turns the manifest into a Blender scene. Python owns
|
||||
construction, orchestration, render setup, and file output. It should not hide
|
||||
the procedural design in ad hoc mesh mutation unless the scene brief explicitly
|
||||
requires that escape hatch.
|
||||
|
||||
## Render Review
|
||||
|
||||
Render review compares output to the target. It should identify visual hits,
|
||||
misses, graph problems, material issues, camera/lighting errors, and the next
|
||||
single hypothesis to test.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Blender 5 Geometry Nodes Research
|
||||
|
||||
This file is the placeholder for current Blender 5.0+ API and geometry-node
|
||||
best-practice notes.
|
||||
|
||||
Use it for distilled findings only:
|
||||
|
||||
- confirmed API changes
|
||||
- node construction gotchas
|
||||
- geometry-node idioms worth reusing
|
||||
- rejected approaches that looked plausible and failed
|
||||
- links to official Blender documentation or source material
|
||||
|
||||
Do not store generic tutorial mush here. The internet already has enough of
|
||||
that and seems proud of itself.
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
# Aetheria Bloom Habitat Build Study
|
||||
|
||||
## Source Lore Read
|
||||
|
||||
Indexed AetheriaLore sources:
|
||||
|
||||
- `Aetheria/Worldbuilding/Pre-Elysium/Technology/Bloom Habitat Anatomy.md`
|
||||
- `Aetheria/Brainstorming/Technology/Bloom Habitat Engineering Research Notes.md`
|
||||
- `Aetheria/Brainstorming/Technology/Bloom Habitat Systems Walkthroughs.md`
|
||||
- `Aetheria/Brainstorming/Technology/Bloom Habitat Open Questions And Red Team.md`
|
||||
|
||||
External visual/procedural reference:
|
||||
|
||||
- Eric Bruneton, `Modeling and Rendering Rama`, via
|
||||
`http://ebruneton.free.fr/rama3/rama.html` and the mirrored text at
|
||||
`https://doczz.net/doc/3617854/making-of---rama`
|
||||
|
||||
Rama lessons used here:
|
||||
|
||||
- sell the cylinder as a whole world, not just a tube
|
||||
- divide the inner surface into large readable axial/circumferential regions
|
||||
- generate terrain, roads, rivers, cities, fields, forests, clouds, and lighting
|
||||
from layered map logic
|
||||
- adapt detail to the view instead of pretending a hand-modeled megastructure
|
||||
is a sensible authoring target
|
||||
|
||||
The build follows the current Bloom model:
|
||||
|
||||
- rotating manufactured cylinder, not hollowed asteroid rock
|
||||
- broad open pressurized commons
|
||||
- high-radius civic surface with comfortable spin gravity
|
||||
- despun or partially despun axial hub and light/traffic spire
|
||||
- despun hub cap and docking port on the center axis
|
||||
- spun-up spire sheath connected to the rotating spoke frame
|
||||
- shared cylindrical coordinate helpers so shell districts, spokes, surface
|
||||
routes, cloud patches, and endcaps all join in one `x / angle / radius` frame
|
||||
- expensive spoke interfaces where transit, utilities, cargo, air, and authority
|
||||
concentrate
|
||||
- golden-angle spiral spoke layout with tangential passenger/cargo loops from
|
||||
the spun sheath to the civic surface
|
||||
- frame-transfer arteries between the despun core and spun sheath/spoke frame,
|
||||
modeled as curved transfer loops instead of straight shafts
|
||||
- layered endcaps with pressure faces, terrace rings, slum balconies, and beach
|
||||
service structures
|
||||
- outward stack of utility mat, pressure/structural shell, and aggregate shielding
|
||||
- layered civic geography: hubward low-g transition belt, hyper-urban favela
|
||||
belts, luxury spoke districts, urban mixed belts, suburban/industrial areas,
|
||||
industrial/agricultural bands, beach rim, sea band, roads, rivers, cloud
|
||||
patches, and service routes
|
||||
- hubward endcap terrace slums: noisy, near-continuous annular shelf fields
|
||||
around the docking hub office complex, stepping inward toward the Spire as
|
||||
"up" and expanding outward with gravity until they meet the high-radius
|
||||
surface
|
||||
- rickety-to-classy endcap construction gradient: batched poor shacks, patched
|
||||
roofs, ladders, nets, handlines, midrise housing, and a denser urban crown on
|
||||
the outer terraces
|
||||
- elaborated spoke plazas and town fields: plaza rings, kiosks, feeder walks,
|
||||
nested town houses, and lane networks
|
||||
- one small Service Ring Kappa marker among many maintenance systems, not the
|
||||
compositional focus
|
||||
|
||||
## Built Artifact
|
||||
|
||||
Script:
|
||||
`examples/geometry_script/aetheria_bloom_habitat.py`
|
||||
|
||||
Generated artifacts:
|
||||
|
||||
- `experiments/generated/aetheria_bloom/aetheria_bloom_habitat.blend`
|
||||
- `experiments/generated/aetheria_bloom/aetheria_bloom_habitat.png`
|
||||
- `experiments/generated/aetheria_bloom/aetheria_bloom_interior_world.png`
|
||||
- `experiments/generated/aetheria_bloom/aetheria_bloom_hubward_endcap_terraces.png`
|
||||
|
||||
Verification:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\generated\aetheria_bloom\aetheria_bloom_habitat.blend' --python '.\tools\verify_aetheria_bloom_habitat.py'
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
AETHERIA_BLOOM_VERIFY ok
|
||||
AETHERIA_BLOOM_VERIFY group VG Bloom Light Spine 21 31
|
||||
AETHERIA_BLOOM_VERIFY objects 1028
|
||||
```
|
||||
|
||||
## Procedural Translation Notes
|
||||
|
||||
This is intentionally hybrid:
|
||||
|
||||
- Python structures the lore-derived coordinate system, layer tables, material
|
||||
meanings, mapped civic regions, cameras, labels, and render paths.
|
||||
- `vibegeometry/` supplies the reusable Python primitives promoted from this
|
||||
build: coordinate frames, deterministic fields, batched mesh construction,
|
||||
Blender object/material/camera helpers, and verifier checks.
|
||||
- Geometry Script emits `VG Bloom Light Spine`, an inspectable 21-node
|
||||
geometry-node group used by a Nodes modifier in the scene.
|
||||
- bpy builds the full shell surfaces, despun core, hub cap, docking port, spun
|
||||
sheath, spiral spoke loops, endcaps, frame-transfer arteries, wrapped region
|
||||
patches, city blocks, hubward endcap terrace slums, batched town/plaza detail,
|
||||
roads, rivers, clouds, utility lines, service markers, and cameras, including
|
||||
a dedicated hubward-endcap inspection camera.
|
||||
|
||||
The IRCSS pass paid off in three ways:
|
||||
|
||||
- **Edge-first architecture:** the Bloom is built as shell layers, spoke lines,
|
||||
transfer arteries, region patches, utility routes, and service markers before
|
||||
local dressing.
|
||||
- **Curves as attachment rails:** roads, rivers, and utility routes are curves
|
||||
wrapped onto the inner cylinder or service layers; spiral spokes and
|
||||
frame-transfer arteries curve between rotating frames instead of pretending
|
||||
the hub/spire interface is a straight elevator.
|
||||
- **Coordinate space as contract:** `surface_point`, `cyl_point`, radial axes,
|
||||
and tangents let each subsystem attach to the same cylindrical frame. That
|
||||
makes twisted forms useful instead of decorative: the loop can bend, but its
|
||||
endpoints still land on the sheath, shell, cap, or district layer.
|
||||
- **Golden-angle filling:** spoke placement uses the sunflower trick, stepping
|
||||
by the golden angle along the axis so each new spoke avoids the previous
|
||||
radial lanes while still reading as one organic exchange network.
|
||||
- **Batched micro-detail:** rickety visual detail should not mean one Blender
|
||||
object per plank. The favela/town pass batches many boxes into shared meshes
|
||||
and many brace/catwalk paths into shared curve objects, preserving detail
|
||||
without making scene generation stall.
|
||||
- **Lore frame before ornament:** Bloom endcap "up" points inward toward the
|
||||
axial Spire, not along the cylinder wall. Terrace slums therefore belong on
|
||||
hubward/capward endcaps as annular shelves, galleries, ladders, nets, and
|
||||
handlines around the axial hub, growing outward to the surface.
|
||||
- **Noise as settlement pressure:** sparse concentric placement reads as civic
|
||||
planning, not poverty. The current terrace pass uses fractal value noise to
|
||||
warp angles, tier breaks, density, footprints, heights, ladders, and class
|
||||
transition so the slum behaves like accretion over rice-paddy shelves.
|
||||
- **Python as orchestration:** the cylinder layers, region maps, city blocks,
|
||||
social-gradient bands, roads, rivers, forests, clouds, and spokes come from
|
||||
small tables and named helpers rather than copied object placement.
|
||||
|
||||
## Current Limits
|
||||
|
||||
- The build is a spatial systems study, not final art.
|
||||
- The civic surface is still a symbolic map-layer world, but the hubward endcap
|
||||
now has enough nested structure to read as precarious built habitat.
|
||||
- The current whole-Bloom view is less skeletal, but still a study. It needs
|
||||
denser horizon city detail and better art-directed lighting before it earns
|
||||
the word cinematic without fraud.
|
||||
- Only the light spine is emitted through Geometry Script so far. The next pass
|
||||
should move the shared coordinate helpers plus spiral spoke/artery network
|
||||
into reusable Geometry Script groups.
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
# Dream Machine Level Slice
|
||||
|
||||
This is the first visible Rust-side showpiece for the CSG assembler.
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
cargo run -p vg_csg --example dream_machine_level
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
- `experiments/generated/dream_machine_level/dream_machine_level.obj`
|
||||
- `experiments/generated/dream_machine_level/dream_machine_level.mtl`
|
||||
- `experiments/generated/dream_machine_level/dream_machine_level_preview.svg`
|
||||
- `experiments/generated/dream_machine_level/dream_machine_level_preview.png`
|
||||
|
||||
Current build stats:
|
||||
|
||||
- brushes: 197
|
||||
- convex fragments: 177
|
||||
- vertices: 14,516
|
||||
- triangles: 5,924
|
||||
- candidate pairs: 0
|
||||
- rejected pairs: 16
|
||||
- CSG warnings: 0
|
||||
|
||||
## Composition
|
||||
|
||||
The level is a machine-cathedral switchyard built directly through
|
||||
`LevelDsl`. It combines:
|
||||
|
||||
- a round machine-garden hub with a raised inner terrace and radial vent cuts
|
||||
- a central reactor plinth, cylinder throat, and glass dome cap
|
||||
- concentric anchor-city towers fading into lower blocks and park patches
|
||||
- radial plaza strips proving shared polar coordinate intent
|
||||
- alternating segmented solar and radiator ray florets extending far past the
|
||||
city disk
|
||||
- orbiting cargo arteries, hinge spines, and outer piers for large-scale
|
||||
structure language
|
||||
|
||||
The point is not visual finality. The point is a durable artifact that proves
|
||||
the Rust CSG organ can already author a named, inspectable, multi-part spatial
|
||||
idea and emit a portable mesh without waiting for the Bevy renderer loop.
|
||||
|
||||
## Doctrine
|
||||
|
||||
Use `LevelDsl` as the fast blockout brush, not as a secret final art pipe.
|
||||
When the idea needs proof now, the DSL can hold the spatial claim in explicit
|
||||
named primitives, export mesh evidence, and keep the pressure on the CSG kernel.
|
||||
|
||||
Additive habitat primitives are useful as scale-language anchors, but the first
|
||||
preview proved that a bad silhouette will eat all the cleverness. The rectangular
|
||||
nave, roof ribs, and flat floret slabs made the scene read as a blocked-up
|
||||
industrial fan. The corrected pass moves to a circular hub, keeps the dome
|
||||
visible, lowers the city mass, and appends segmented curved ray panels after CSG
|
||||
assembly.
|
||||
|
||||
The CSG cuts still carry the machine grammar: vents and service voids are how
|
||||
solid mass becomes a level instead of a pile. But the landmark shape has to read
|
||||
at thumbnail scale before detail deserves more budget.
|
||||
|
||||
The preview SVG is deliberately dumb. It is a quick inspection surface for
|
||||
shape, density, material partitioning, and catastrophic export failures. It is
|
||||
not a substitute for Blender or Bevy render review.
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# Industrial Maintenance Loop
|
||||
|
||||
Grounded fixture generated from the maintenance-loop reference prompt and image.
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
cargo run -p vg_csg --example industrial_maintenance_loop
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
- `experiments/generated/industrial_maintenance_loop/industrial_maintenance_loop.obj`
|
||||
- `experiments/generated/industrial_maintenance_loop/industrial_maintenance_loop.mtl`
|
||||
- `experiments/generated/industrial_maintenance_loop/industrial_maintenance_loop_preview.svg`
|
||||
- `experiments/generated/industrial_maintenance_loop/industrial_maintenance_loop_preview.png`
|
||||
|
||||
Current build stats:
|
||||
|
||||
- brushes: 266
|
||||
- convex fragments: 266
|
||||
- vertices: 11,448
|
||||
- triangles: 4,128
|
||||
- candidate pairs: 0
|
||||
- rejected pairs: 0
|
||||
- CSG warnings: 0
|
||||
|
||||
## Composition
|
||||
|
||||
The scene is a single-level industrial maintenance loop built into a curved
|
||||
habitat shell. It includes:
|
||||
|
||||
- annular scuffed deck, curved outer shell wall, inner island shroud, and
|
||||
continuous ceiling previewed as x-ray shell material
|
||||
- one floor-to-ceiling central machinery island that blocks the far loop
|
||||
- inner manifold faces with gasket panel proxies, access hatches, amber
|
||||
diagnostic strips, and cable bundles
|
||||
- outer staging zone with anchor rail, sliding rail blocks, lockers, carts,
|
||||
clipped rescue lines, and hazard markings
|
||||
- one raised supervisor gallery segment behind glass with a visible stair
|
||||
- service artery mouths and pipe-tunnel throats entering the machinery island
|
||||
- dry-operation mobility gear rack with folded harness loops, cartridges,
|
||||
oxygenation tube proxies, and charging/detail blocks
|
||||
|
||||
## DSL Lesson
|
||||
|
||||
This fixture is mostly about coordinate systems. The example introduces a local
|
||||
`LoopFrame` helper:
|
||||
|
||||
```rust
|
||||
let frame = LoopFrame::at(angle, OUTER_LOOP_RADIUS - 1.15);
|
||||
loop_box(level, "bolted outer anchor rail", frame, LoopBox {
|
||||
tangent: 0.0,
|
||||
radial: 0.0,
|
||||
z: 0.55,
|
||||
size_tangent: 1.25,
|
||||
size_radial: 0.18,
|
||||
size_z: 0.18,
|
||||
material: HAZARD,
|
||||
});
|
||||
```
|
||||
|
||||
The move is simple and important: author each surface in its own domain. The
|
||||
outer staging zone thinks in tangent/radial/up. The inner manifold face uses
|
||||
the same axes at a different radius. The gallery owns one angular segment. The
|
||||
shell is generated as annular and vertical arc bands. The world-space brush
|
||||
placement becomes a compilation step instead of the authoring language.
|
||||
|
||||
That is the shape the DSL should grow toward: named local coordinate contracts,
|
||||
small repeated detail organs, and late mapping into CSG brushes or direct mesh
|
||||
panels depending on whether the surface needs boolean behavior.
|
||||
|
||||
## Caveat
|
||||
|
||||
The SVG preview is an inspection cutaway, not a physical render. The model
|
||||
contains a continuous ceiling and shell, but the preview makes shell materials
|
||||
transparent so the internal fixture can be judged without a renderer. A Bevy or
|
||||
Blender pass should turn this into a lit interior view with proper occlusion,
|
||||
glass, emissive panels, and worn materials.
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
# Lucent Tether Habitat Build
|
||||
|
||||
## Source Read
|
||||
|
||||
Primary source: `Aetheria/Stories/Lucent Hostage Feed.md` and its interactive
|
||||
Ghostlight scaffold. Supporting faction read:
|
||||
`Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major/Lucent Media.md`.
|
||||
Street-generation priors came from Parish and Muller's SIGGRAPH 2001
|
||||
`Procedural Modeling of Cities` and Chen, Esch, Wonka, Muller, and Zhang's
|
||||
SIGGRAPH 2008 `Interactive Procedural Street Modeling`:
|
||||
|
||||
- https://people.eecs.berkeley.edu/~sequin/CS285/PAPERS/Parish_Muller01.pdf
|
||||
- https://peterwonka.net/Publications/pdfs/2008.SG.Chen.InteractiveProceduralStreetModeling.pdf
|
||||
|
||||
The story describes a Lucent tether station with a transfer lounge at the
|
||||
media-eye end of the tether. The lounge has glass walls, an elevator gate, a
|
||||
waist-high glass rail, a city bubble far below like a sealed aquarium, public
|
||||
surfaces full of moderation overlays, Mara's private control rail, influencer
|
||||
side panels, sponsor-risk colors, breach estimates, audience heat, Lucent-gray
|
||||
security beyond the lounge doors, and a white safe-line two careful steps back
|
||||
from the elevator-side glass rail.
|
||||
|
||||
Lucent's broader faction language matters: its habitats are partially staged
|
||||
media ecologies. Permissiveness sits over a classification engine. The build
|
||||
therefore treats surfaces as active controls, not passive decoration.
|
||||
|
||||
## Coordinate Contract
|
||||
|
||||
- `X`: tether axis, from the city-bubble side to the media-eye lounge.
|
||||
- `YZ`: spinning city ground plane, orthogonal to the tether.
|
||||
- `+X`: outward from the city ground toward the dome/headquarters anchor side;
|
||||
building height runs along this axis.
|
||||
- `Y`: lounge width and city ground-plane axis.
|
||||
- The media-eye lounge is at positive `X`.
|
||||
- The dome city is at negative `X`, boolean-cut flat at the ground plane, and
|
||||
attached to the tether through a prestige anchor plaza.
|
||||
- Crisis geometry lives inside the lounge: elevator gate and glass rail at the
|
||||
threshold, safe-line arc behind it, security beyond the doors.
|
||||
|
||||
This is deliberately room-scale plus habitat-scale in one frame. The tether
|
||||
axis gives the habitat its body; the lounge threshold gives it teeth.
|
||||
|
||||
## Implementation
|
||||
|
||||
Script:
|
||||
`examples/geometry_script/lucent_tether_habitat.py`
|
||||
|
||||
Generated artifacts:
|
||||
|
||||
- `experiments/generated/lucent_tether_habitat/lucent_tether_habitat.blend`
|
||||
- `experiments/generated/lucent_tether_habitat/lucent_tether_overview.png`
|
||||
- `experiments/generated/lucent_tether_habitat/lucent_media_eye_lounge.png`
|
||||
- `experiments/generated/lucent_tether_habitat/lucent_city_bubble_aquarium.png`
|
||||
|
||||
Verifier:
|
||||
`tools/verify_lucent_tether_habitat.py`
|
||||
|
||||
Geometry Script group:
|
||||
`VG Lucent Feed Ribbon`
|
||||
|
||||
The group creates a procedural ribbon of moderation/feed geometry orbiting the
|
||||
media-eye lounge. Python owns the lore table, deterministic city detail,
|
||||
materials, camera setup, render output, and verification. Geometry Script owns
|
||||
the inspectable node graph for the feed ribbon so the scene keeps a live
|
||||
procedural editing surface instead of becoming only a pile of mesh facts.
|
||||
|
||||
The accepted revision uses Cycles, not EEVEE. Shader graph work is part of the
|
||||
build contract: glass gets procedural noise/bump and transmission tuning, metal
|
||||
and floor materials get micro-surface bump, and Lucent overlay materials use
|
||||
noise plus color ramps to read as active feed surfaces rather than flat colored
|
||||
cards.
|
||||
|
||||
## Translation Patterns
|
||||
|
||||
**Habitat First, Room Second**
|
||||
|
||||
The tether and city bubble establish that the lounge is part of a larger
|
||||
orbital media habitat. Without that, the crisis room becomes a generic glass
|
||||
office with neon rectangles, which is how worldbuilding goes to die wearing a
|
||||
lanyard.
|
||||
|
||||
```python
|
||||
add_cylinder_between("lucent_tether_main_cable", TETHER_START, TETHER_END, 0.075, mats["tether"])
|
||||
add_uv_sphere("city_bubble_sealed_aquarium", CITY_BUBBLE_CENTER, CITY_BUBBLE_SCALE, mats["glass"])
|
||||
add_yz_disk("city_bubble_flat_ground_surface_disk", CITY_GROUND_X + 0.004, 1.54, mats["park"], segments=128)
|
||||
```
|
||||
|
||||
**Crisis Geometry As Spatial Contract**
|
||||
|
||||
The white arc is not decoration. It is a negotiated border between hostage,
|
||||
glass rail, security, and feed optics. Its object name and verifier check make
|
||||
that contract explicit.
|
||||
|
||||
```python
|
||||
add_curve_polyline("waist_high_glass_rail", [(5.25, -0.62, -0.08), (5.31, 0.0, -0.035), (5.25, 0.62, -0.08)], 0.027, mats["glass"])
|
||||
add_curve_polyline("safe_line_white_arc", arc_points((4.82, 0.0, -0.36), 0.68, 208, 332, 36, plane="xy"), 0.015, mats["safe"])
|
||||
```
|
||||
|
||||
**Surfaces Are Interfaces**
|
||||
|
||||
Lucent habitat surfaces carry classification pressure: prompts, sponsor risk,
|
||||
audience heat, delayed influencer panels, and public edit trails. The panels
|
||||
are separate named objects because future passes may need to animate or route
|
||||
them independently.
|
||||
|
||||
```python
|
||||
add_panel("feed_ops_prompt_panels", (4.12, -0.88, 0.34), (1.08, 0.028, 0.22), mats["amber"])
|
||||
add_panel("sponsor_risk_color_stack", (4.74, -0.86, 0.34), (0.15, 0.032, 0.62), mats["risk"])
|
||||
add_panel("audience_heat_meter", (3.55, -0.84, 0.30), (0.2, 0.032, 0.58), mats["heat"])
|
||||
```
|
||||
|
||||
**Street Fields Beat Grids**
|
||||
|
||||
The first city pass used a regular grid. The revised pass uses a small
|
||||
procedural street field instead: radial avenues grow from the tether anchor,
|
||||
tangential promenade rings bend around it, and irregular annular parcels fill
|
||||
between them. This is a compact use of two research lessons: global goals plus
|
||||
local constraints from L-system city growth, and smoothly varying direction
|
||||
fields from tensor-field street modeling.
|
||||
|
||||
```python
|
||||
for i in range(16):
|
||||
base_angle = TAU * i / 16 + (hash01(i, 4, 11) - 0.5) * 0.12
|
||||
line = []
|
||||
for step in range(9):
|
||||
u = step / 8
|
||||
r = 0.12 + 1.42 * smoothstep(u)
|
||||
angle = base_angle + 0.08 * pymath.sin(u * TAU + i)
|
||||
line.append((ground_x + 0.025, r * pymath.cos(angle), r * pymath.sin(angle)))
|
||||
roads.append(line)
|
||||
|
||||
for ring_i, radius in enumerate([0.22, 0.42, 0.68, 0.95, 1.2, 1.42]):
|
||||
for arc_i in range(3):
|
||||
start = TAU * arc_i / 3 + ring_i * 0.21
|
||||
end = start + TAU / 3 * (0.72 + 0.12 * hash01(ring_i, arc_i, 23))
|
||||
roads.append(arc_points((ground_x + 0.028, 0.0, 0.0), radius, pymath.degrees(start), pymath.degrees(end), 24, plane="yz"))
|
||||
```
|
||||
|
||||
**Land Use Falls Off From The Anchor**
|
||||
|
||||
The tether anchor plaza is the prestige attractor. Tallest skyscrapers cluster
|
||||
around the plug-in point, the middle city steps down into irregular midrise
|
||||
blocks and parks, and the outer ring fades into green faux-rural cottagecore.
|
||||
This gives the skyline a social gradient instead of a height-map haircut.
|
||||
|
||||
```python
|
||||
if zone < 0.28:
|
||||
verts, faces = prestige_verts, prestige_faces
|
||||
elif zone < 0.72:
|
||||
verts, faces = tower_verts, tower_faces
|
||||
else:
|
||||
verts, faces = cottage_verts, cottage_faces
|
||||
```
|
||||
|
||||
**Sunflower Utility Crest**
|
||||
|
||||
The city disk is the disk-floret region. The external crest is the ray-floret
|
||||
region: alternating solar panels and radiators, Perlin-warped and hinged around
|
||||
the flat dome base. These are Citadel-scale arms, large enough that a defensive
|
||||
folded posture can reach toward the far media-eye end of the tether and dwarf
|
||||
the dome itself.
|
||||
|
||||
```python
|
||||
for petal in range(34):
|
||||
center_a = TAU * petal / petals + 0.025 * pymath.sin(petal * 1.7)
|
||||
target_verts = solar_verts if petal % 2 == 0 else radiator_verts
|
||||
radius = 1.57 + outward_reach * smoothstep(t)
|
||||
x = CITY_GROUND_X + arm_length * smoothstep(t)
|
||||
```
|
||||
|
||||
**Shader Graphs Are Geometry Evidence Too**
|
||||
|
||||
Flat material defaults can make a procedural scene read like a diagram even
|
||||
when the object layout is correct. The revised pass spends actual shader nodes
|
||||
on the surfaces that carry the lore: glass, tether material, and feed panels.
|
||||
|
||||
```python
|
||||
noise = nodes.new("ShaderNodeTexNoise")
|
||||
bump = nodes.new("ShaderNodeBump")
|
||||
links.new(noise.outputs["Fac"], bump.inputs["Height"])
|
||||
links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Command:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\generated\lucent_tether_habitat\lucent_tether_habitat.blend' --python '.\tools\verify_lucent_tether_habitat.py'
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
LUCENT_TETHER_VERIFY ok objects=81
|
||||
```
|
||||
|
||||
Render review rejected the first pass because the city bubble read as detached
|
||||
from the tether and the EEVEE/default-material pass was too shallow. The
|
||||
accepted revision uses Cycles, explicit bubble-to-tether attachment geometry,
|
||||
shader-node work, a non-grid city map, the orthogonal spinning city frame,
|
||||
boolean-cut flat dome base, and an external alternating solar/radiator
|
||||
sunflower ray-floret crest. The verifier now
|
||||
checks the attachment objects, Cycles engine, material shader-node types, named
|
||||
crisis geometry, city coordinate contract, render artifacts, and the `VG Lucent
|
||||
Feed Ribbon` Geometry Script group.
|
||||
|
||||
## Durable Lesson
|
||||
|
||||
For Lucent spaces, avoid thinking "set dressing." The set is the power system.
|
||||
Build public surfaces as rails, rankings, prompt stacks, edits, delayed feeds,
|
||||
and shader behavior. Then put physical bodies and thresholds where those
|
||||
abstractions become dangerous. Also: "far below" is not the coordinate system.
|
||||
For a tether habitat, spin gravity decides the ground plane. Large habitat parts
|
||||
need visible load paths, or the render becomes a polite lie.
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
# CSG Performance Fixtures
|
||||
|
||||
The timing surface is a fixture, not a victory poster.
|
||||
|
||||
Run our current kernel:
|
||||
|
||||
```powershell
|
||||
.\tools\run_csg_perf.ps1
|
||||
```
|
||||
|
||||
The script builds `vg_csg` in release mode and writes JSONL to
|
||||
`experiments/generated/csg-perf-latest.jsonl`. Each record carries:
|
||||
|
||||
- `kernel`
|
||||
- `scenario`
|
||||
- `brushes`
|
||||
- warmup and measured iteration counts
|
||||
- `mean_ns`, `min_ns`, `p50_ns`, `p95_ns`, `max_ns`
|
||||
- emitted triangle, fragment, and warning counts
|
||||
- demand-frontier `candidate_pairs` and `rejected_pairs`
|
||||
|
||||
The `vg_csg` fixture measures:
|
||||
|
||||
- `stable`: cached output after warmup
|
||||
- `dirty`: full rebuild through the current ordered assembler
|
||||
- `routed`: experimental surface-router rebuild, falling back to the ordered
|
||||
assembler outside its current single-source, single-candidate subtractive
|
||||
convex contract. The fixture only emits routed records for supported cases.
|
||||
|
||||
`Assembler` caches evaluated output by generation, so repeated `build()` calls
|
||||
on an unchanged graph return the cached mesh. Dirty and routed generations
|
||||
still rebuild.
|
||||
|
||||
## Scenarios
|
||||
|
||||
- `single_center_cut`: the current exact centered box-subtraction parity seam.
|
||||
- `room_grid_8x8_doors`: repeated additive floors/walls and repeated door
|
||||
voids, matching the level-editing workload we care about.
|
||||
- `rotated_cut_stack_64`: one large slab cut by many oriented boxes.
|
||||
- `common_box_chain_64`: repeated convex intersections, exercising `Common`
|
||||
branch behavior through the ordered assembler.
|
||||
- `distant_cutters_512`: many irrelevant cutters, proving bounds early-outs
|
||||
stay cheap.
|
||||
|
||||
## Reference Kernel Slot
|
||||
|
||||
The same wrapper can append reference timings:
|
||||
|
||||
```powershell
|
||||
$env:VIBEGEOMETRY_REFERENCE_CSG_PERF = "path\to\reference-perf.exe"
|
||||
.\tools\run_csg_perf.ps1
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```powershell
|
||||
.\tools\run_csg_perf.ps1 -ReferenceCommand "path\to\reference-perf.exe"
|
||||
```
|
||||
|
||||
To run the native RealtimeCSG C++ plugin directly through the public P/Invoke
|
||||
surface mirrored by its managed Unity library:
|
||||
|
||||
```powershell
|
||||
.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp
|
||||
```
|
||||
|
||||
To test the native bridge without running the full timing suite:
|
||||
|
||||
```powershell
|
||||
.\tools\run_realtimecsg_cpp_perf.ps1 -OutputPath .\experiments\generated\realtimecsg-cpp-health-latest.jsonl -Health
|
||||
```
|
||||
|
||||
That path builds `tools/realtimecsg_native_bridge`, copies
|
||||
`RealtimeCSG[1_559].dll` beside the bridge executable, calls only exported
|
||||
functions already declared by the plugin's managed P/Invoke layer, and writes
|
||||
native timing records to `experiments/generated/realtimecsg-cpp-perf-latest.jsonl`.
|
||||
It fails closed if the native plugin emits no mesh descriptions; zero-geometry
|
||||
timings are poison, not data.
|
||||
|
||||
Current direct-DLL status: the bridge successfully loads the native plugin,
|
||||
creates brush meshes, brushes, and trees, verifies bounds, outlines, raycasts,
|
||||
and now extracts generated mesh descriptions. The fragile part was construction
|
||||
discipline: build child nodes before the tree, insert them through the public
|
||||
Foundation range-insert path, and avoid eagerly setting default brush operation
|
||||
or flag state.
|
||||
|
||||
The reference executable must emit JSONL with the same scenario names. Until
|
||||
that harness exists, the script appends a `kernel=reference,status=missing`
|
||||
record instead of faking a comparison. Sad, but hygienic.
|
||||
|
||||
The public `LogicalError/Realtime-CSG-demo` source still exposes the relevant
|
||||
algorithmic surface: half-edge control meshes, polygon bounds, visible flags,
|
||||
inside/outside/aligned/reverse-aligned categories, crossing polygon splits, and
|
||||
logical routing tables. Use it as readable doctrine. Use the native bridge as
|
||||
the performance target.
|
||||
|
||||
`vg_csg` now reports the first demand-frontier counters. These are not yet a
|
||||
new kernel path; they expose the ordered brush stream as affected source/operator
|
||||
pairs and rejected bounds pairs so future router/index work can prove it is
|
||||
refusing real work rather than merely rearranging it.
|
||||
|
||||
The first routed surface experiment is intentionally narrow. It cleans a single
|
||||
subtractive convex cut into boundary polygons instead of closed fragment boxes,
|
||||
but dense repeated cutters currently create surface-piece explosion. Keep dense
|
||||
rotated subtraction on the ordered kernel until the category router has compact
|
||||
frontier batching and scratch storage.
|
||||
|
||||
Current routed lesson: for `single_center_cut`, routed output emits 48 boundary
|
||||
triangles instead of the fragment carver's 72 closed-fragment triangles. That is
|
||||
cleaner surface output, not yet faster output. The implementation still moves
|
||||
owned polygon vectors around, so it is a topology proof before it is a hot path.
|
||||
|
||||
Dense-kernel regression guardrail: `rotated_cut_stack_64` is pinned by tests at
|
||||
280 fragments, 3404 triangles, 804 candidate pairs, and 9417 rejected pairs.
|
||||
This protects against "optimizations" that alter ordered CSG semantics while
|
||||
looking faster. Output-contract changes need to be explicit.
|
||||
|
||||
Realtime editing seam: `Assembler` can now mutate brush primitives and
|
||||
operations by `BrushId`, invalidating cached output and incrementing generation.
|
||||
`DirtyDemandFrontier` computes the conservative ordered suffix after the first
|
||||
dirty brush. The prefix before that index is the cacheable region; the suffix is
|
||||
the live region because ordered CSG decisions propagate forward. The first
|
||||
implementation now uses that boundary for box-only and general convex prefix
|
||||
checkpoints. The category-router path still needs the same treatment.
|
||||
|
||||
Incremental edit seam: ordered CSG now stores prefix checkpoints after each
|
||||
brush for both the box and general convex builders. A tail edit can resume from
|
||||
the checkpoint immediately before the dirty brush and replay only the live
|
||||
suffix. The result is parity-checked against a full rebuild. Cache validity is
|
||||
tracked as a prefix boundary so repeated edits do not deep-copy stale
|
||||
checkpoints. Final mesh emission is still whole-output.
|
||||
|
||||
Mesh reuse seam: incremental rebuilds now keep the previous generation's cached
|
||||
mesh and reuse it only when suffix replay proves no geometry was touched. This
|
||||
is intentionally exact and conservative: distant rejected edits can skip mesh
|
||||
emission, while candidate cuts still emit the full output mesh. The report
|
||||
exposes `reused_mesh`, `used_checkpoint`, `checkpoint_rebuild_from`, and
|
||||
`direct_dirty_rebuild` so the fixture and tests do not infer rebuild strategy
|
||||
from timing.
|
||||
|
||||
Benchmark correction: `rebuild()` must bypass checkpoint caches. An earlier
|
||||
fixture pass accidentally let the dirty box baseline travel through cached box
|
||||
state, which made incremental box edits look better than they were. The live
|
||||
policy is stricter: box dirty edits use the direct builder unless a future real
|
||||
mesh patcher proves otherwise; general convex dirty edits keep the valid-prefix
|
||||
checkpoint path.
|
||||
|
||||
## Latest Local Baseline
|
||||
|
||||
Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp`:
|
||||
|
||||
This baseline predates the `candidate_pairs`/`rejected_pairs` fields. New
|
||||
fixture runs include them; keep the old block until a fresh C++ comparison is
|
||||
captured rather than mixing timing environments.
|
||||
|
||||
```jsonl
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":948,"min_ns":800,"p50_ns":900,"p95_ns":1100,"max_ns":1200,"triangles":72,"fragments":6,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":6123,"min_ns":4300,"p50_ns":5500,"p95_ns":6000,"max_ns":44700,"triangles":72,"fragments":6,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":85546,"min_ns":66100,"p50_ns":84900,"p95_ns":103300,"max_ns":114800,"triangles":3072,"fragments":256,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":322628,"min_ns":231400,"p50_ns":301600,"p95_ns":428500,"max_ns":841700,"triangles":3072,"fragments":256,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":162057,"min_ns":107500,"p50_ns":139600,"p95_ns":271700,"max_ns":518200,"triangles":3404,"fragments":280,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":4835134,"min_ns":2799000,"p50_ns":4260700,"p95_ns":7080200,"max_ns":10841300,"triangles":3404,"fragments":280,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":937,"min_ns":700,"p50_ns":900,"p95_ns":1100,"max_ns":9700,"triangles":45,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":700876,"min_ns":396000,"p50_ns":662400,"p95_ns":963600,"max_ns":1582700,"triangles":45,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":278,"min_ns":200,"p50_ns":300,"p95_ns":300,"max_ns":400,"triangles":12,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":4762,"min_ns":4500,"p50_ns":4700,"p95_ns":5000,"max_ns":5200,"triangles":12,"fragments":1,"warnings":0}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":7900,"min_ns":5900,"p50_ns":6300,"p95_ns":12100,"max_ns":16400,"triangles":72,"vertices":144,"mesh_descriptions":3}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":775500,"min_ns":376100,"p50_ns":670500,"p95_ns":1451600,"max_ns":2108000,"triangles":12800,"vertices":20992,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":154300,"min_ns":106500,"p50_ns":132900,"p95_ns":240000,"max_ns":380100,"triangles":3498,"vertices":5102,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":64000,"min_ns":45000,"p50_ns":61400,"p95_ns":90500,"max_ns":224800,"triangles":1536,"vertices":2412,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":740400,"min_ns":340900,"p50_ns":641200,"p95_ns":1403100,"max_ns":2258600,"triangles":6180,"vertices":12360,"mesh_descriptions":4}
|
||||
```
|
||||
|
||||
Read these as smoke timings, not final benchmark gospel. Stable unchanged
|
||||
output is no longer embarrassing. Dirty rebuilds are now explicit: box-heavy
|
||||
dirty paths are respectable, distant bounds rejection is excellent, and dense
|
||||
rotated subtraction is still the open wound. The category-router kernel remains
|
||||
the next architectural move; cached output is a guardrail, not a replacement
|
||||
for the real engine.
|
||||
|
||||
## Incremental Dirty Smoke Run
|
||||
|
||||
Captured on 2026-05-07 with
|
||||
`cargo run -p vg_csg --release --example csg_perf_fixture`. Dirty modes mutate
|
||||
the tail brush each iteration. `incremental_dirty` uses prefix checkpoints and
|
||||
falls back to full replay only when no valid checkpoint exists.
|
||||
|
||||
```jsonl
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":506,"min_ns":500,"p50_ns":500,"p95_ns":500,"max_ns":800,"triangles":72,"fragments":6,"warnings":0,"candidate_pairs":1,"rejected_pairs":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":3937,"min_ns":3700,"p50_ns":3800,"p95_ns":4100,"max_ns":12700,"triangles":72,"fragments":6,"warnings":0,"candidate_pairs":1,"rejected_pairs":0}
|
||||
{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":4204,"min_ns":4000,"p50_ns":4100,"p95_ns":5100,"max_ns":6000,"triangles":72,"fragments":6,"warnings":0,"candidate_pairs":1,"rejected_pairs":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":68625,"min_ns":41300,"p50_ns":62600,"p95_ns":99600,"max_ns":150000,"triangles":3072,"fragments":256,"warnings":0,"candidate_pairs":64,"rejected_pairs":8128}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":817095,"min_ns":428800,"p50_ns":664300,"p95_ns":1392000,"max_ns":3642000,"triangles":3108,"fragments":259,"warnings":0,"candidate_pairs":65,"rejected_pairs":8127}
|
||||
{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":104501,"min_ns":45200,"p50_ns":61400,"p95_ns":283600,"max_ns":374000,"triangles":3108,"fragments":259,"warnings":0,"candidate_pairs":65,"rejected_pairs":8127}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":14776,"min_ns":9500,"p50_ns":10300,"p95_ns":14200,"max_ns":233700,"triangles":3404,"fragments":280,"warnings":0,"candidate_pairs":804,"rejected_pairs":9417}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":3490834,"min_ns":2233100,"p50_ns":3133400,"p95_ns":5120800,"max_ns":6423700,"triangles":3404,"fragments":280,"warnings":0,"candidate_pairs":805,"rejected_pairs":9416}
|
||||
{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":415318,"min_ns":242200,"p50_ns":305100,"p95_ns":874600,"max_ns":1777700,"triangles":3404,"fragments":280,"warnings":0,"candidate_pairs":805,"rejected_pairs":9416}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":651,"min_ns":500,"p50_ns":600,"p95_ns":700,"max_ns":800,"triangles":45,"fragments":1,"warnings":0,"candidate_pairs":63,"rejected_pairs":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":488343,"min_ns":360200,"p50_ns":394100,"p95_ns":832100,"max_ns":1936700,"triangles":45,"fragments":1,"warnings":0,"candidate_pairs":63,"rejected_pairs":0}
|
||||
{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":10150,"min_ns":9500,"p50_ns":9600,"p95_ns":13100,"max_ns":17200,"triangles":45,"fragments":1,"warnings":0,"candidate_pairs":63,"rejected_pairs":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":218,"min_ns":200,"p50_ns":200,"p95_ns":300,"max_ns":400,"triangles":12,"fragments":1,"warnings":0,"candidate_pairs":0,"rejected_pairs":512}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":110825,"min_ns":35700,"p50_ns":52200,"p95_ns":115600,"max_ns":2843900,"triangles":12,"fragments":1,"warnings":0,"candidate_pairs":0,"rejected_pairs":512}
|
||||
{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":2856,"min_ns":2400,"p50_ns":2500,"p95_ns":3600,"max_ns":4100,"triangles":12,"fragments":1,"warnings":0,"candidate_pairs":0,"rejected_pairs":512}
|
||||
```
|
||||
|
||||
Tail-edit lesson: prefix caching cuts the room-grid dirty mean by roughly 7.8x,
|
||||
the rotated-stack dirty mean by roughly 8.4x, the common-box chain by roughly
|
||||
48x, and the distant-cutter case by roughly 39x in this run. A follow-up mesh
|
||||
reuse seam pushed distant rejected edits into low microseconds by reusing the
|
||||
previous mesh when no geometry was touched. The single-cut case is still
|
||||
slightly slower because cache machinery dominates. The next hard target is
|
||||
dirty mesh range patching for candidate edits; replaying less CSG work is not
|
||||
enough when every real cut still serializes the whole result mesh.
|
||||
|
||||
Corrected lesson after separating `build()` from `rebuild()`: box candidate
|
||||
edits are currently better served by the direct box builder, not checkpoint
|
||||
replay. The checkpoint path still wins hard for general convex tail edits:
|
||||
`rotated_cut_stack_64` remains an order-of-magnitude style win in smoke runs,
|
||||
and `common_box_chain_64` stays far below full dirty rebuild. Rejected general
|
||||
convex suffixes can expose `reused_mesh:true`; the fixture includes
|
||||
`distant_oriented_cutters_128` for that case.
|
||||
|
||||
Planning counters now expose rebuild strategy. Treat `direct_dirty_rebuild:true`
|
||||
as a sign the incremental planner deliberately bailed to the direct path.
|
||||
Treat `used_checkpoint:true` and `checkpoint_rebuild_from` as evidence of a live
|
||||
suffix replay boundary. Treat `reused_mesh:true` as proof that suffix replay did
|
||||
not touch geometry and reused the previous output mesh. These fields are part of
|
||||
the benchmark contract because timings alone already lied once.
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# Dream Machine Grammar
|
||||
|
||||
The grammar layer is the authoring mind above CSG.
|
||||
|
||||
```text
|
||||
brief / lore / level intent
|
||||
-> procedural grammar rules
|
||||
-> semantic solid and void claims
|
||||
-> CSG tree classification
|
||||
-> visible polygon set
|
||||
-> Bevy mesh cache
|
||||
-> inspection loop
|
||||
```
|
||||
|
||||
`crates/vg_grammar` starts this layer. It deliberately emits claims before
|
||||
geometry:
|
||||
|
||||
- `Solid` claims say matter exists.
|
||||
- `Void` claims say space must stay empty.
|
||||
- tags preserve why the claim exists: `room`, `corridor`, `door`, `wall`,
|
||||
`floor`, `ceiling`.
|
||||
|
||||
The first rules are small on purpose:
|
||||
|
||||
- `RoomSpec`
|
||||
- `CorridorSpec`
|
||||
- `DoorSpec`
|
||||
- `GalleryChainSpec`
|
||||
|
||||
They prove the shape of the pipeline: grammar emits semantic claims, claims
|
||||
compile into `vg_csg` brushes, and the CSG assembler produces the mesh verdict.
|
||||
|
||||
The first expansion adds local `Frame`s, deterministic seeded variation, and
|
||||
`RuleSet`/`GalleryChainSpec` composition. This is the rule: every richer grammar
|
||||
feature must sharpen spatial intent before it increases mesh complexity.
|
||||
|
||||
The second expansion adds generated CSG tree intent. `ClaimTree` can still
|
||||
compile to the flat assembler stream for the current backend, but it can also
|
||||
emit a `CsgTreeArena` where solids are grouped under addition, voids are grouped
|
||||
under addition, and the result is represented as `solids - voids`. This is the
|
||||
bridge from grammar-as-list to grammar-as-boolean-program.
|
||||
|
||||
Current example output:
|
||||
|
||||
```text
|
||||
claims=42 solids=36 voids=6 brushes=42 tree_nodes=45
|
||||
```
|
||||
|
||||
The rule is simple: future procedural generators should feed CSG with authored
|
||||
space claims instead of bypassing the tree with final triangles. Cities,
|
||||
habitats, corridors, and districts can grow from fields and grammars, but hard
|
||||
architectural legality belongs in the CSG tree.
|
||||
|
||||
Where a grammar feature compiles to a CSG behavior that overlaps public
|
||||
RealtimeCSG semantics, tests should preserve exact parity at that seam: claim
|
||||
counts, brush counts, category vocabulary, split counts, and mesh invariants.
|
||||
Novel grammar can sing. Shared API behavior must stay in tune.
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
# Geometry Script Fork Notes
|
||||
|
||||
VibeGeometry uses a repo-local Geometry Script clone at:
|
||||
|
||||
```text
|
||||
external/geometry-script
|
||||
```
|
||||
|
||||
The clone currently tracks upstream:
|
||||
|
||||
```text
|
||||
https://github.com/carson-katri/geometry-script.git
|
||||
```
|
||||
|
||||
The GameCult fork remote is:
|
||||
|
||||
```text
|
||||
https://github.com/GameCult/geometry-script.git
|
||||
```
|
||||
|
||||
The fork is intentionally kept small. As of 2026-05-04, its live branches are:
|
||||
|
||||
```text
|
||||
main
|
||||
vibegeometry/blender-5-nested-tree-groups
|
||||
vibegeometry/blender-5-nodes-to-script
|
||||
```
|
||||
|
||||
Upstream may still have historical branches such as `gh-pages` or
|
||||
`nodes_to_script`; those are upstream history, not GameCult fork policy.
|
||||
|
||||
## Upstream `nodes_to_script` Branch
|
||||
|
||||
Upstream's historical `nodes_to_script` branch is real and potentially useful,
|
||||
but should be treated as prototype material rather than a drop-in dependency.
|
||||
|
||||
Observed on 2026-05-04:
|
||||
|
||||
- Branch head: `b78ec37` (`Improve dot access handling`, 2023-03-01).
|
||||
- Merge base with current `main`: `5fbc7d1`.
|
||||
- Main has substantial later work not present on the branch, including Blender
|
||||
4 `NodeTree.interface` migration, nested tree checks, repeat-zone support,
|
||||
and Blender 3/4 compatibility fixes.
|
||||
- The branch adds `operators/convert_tree.py` and registers a
|
||||
`geometry_script.convert_tree` node editor operator.
|
||||
|
||||
The operator walks the active node tree, creates one assignment per node, reads
|
||||
enabled input defaults and node-specific properties, follows links into
|
||||
arguments, topologically sorts from `Group Output`, then writes a generated
|
||||
`@tree(...)` script into a Blender text datablock.
|
||||
|
||||
That makes it useful as a translation assistant or bootstrap oracle. It does
|
||||
not replace manual doctrine-building yet: output quality, Blender 5.1
|
||||
compatibility, interface sockets, zones, nested groups, multi-input sockets,
|
||||
and current Geometry Script API drift all need testing before VibeGeometry
|
||||
leans on it.
|
||||
|
||||
Patch work belongs in this clone first, then in a fork branch/PR. Do not patch
|
||||
the installed AppData add-on directly. That path is live installation state, not
|
||||
source control, and it will quietly become a stupid little archaeological layer
|
||||
if indulged.
|
||||
|
||||
## Setup
|
||||
|
||||
Run from the VibeGeometry repo root:
|
||||
|
||||
```powershell
|
||||
.\tools\setup_geometry_script_clone.ps1
|
||||
```
|
||||
|
||||
This clones upstream into `external/geometry-script`, creates or switches to the
|
||||
current VibeGeometry toolchain branch, and applies the patches stored in:
|
||||
|
||||
```text
|
||||
patches/geometry-script/0001-fix-nested-tree-group-references-on-blender-4.patch
|
||||
patches/geometry-script/0002-Port-node-tree-conversion-operator.patch
|
||||
patches/geometry-script/0003-Tolerate-Blender-menu-socket-defaults.patch
|
||||
patches/geometry-script/0004-Avoid-duplicate-converter-keywords-for-socket-backed-properties.patch
|
||||
```
|
||||
|
||||
`external/` is intentionally ignored by the VibeGeometry repository. The clone
|
||||
is source-controlled as its own repo, not smuggled into this repo as a pile of
|
||||
vendor files.
|
||||
|
||||
## Toolchain Branch
|
||||
|
||||
Branch:
|
||||
|
||||
```text
|
||||
vibegeometry/blender-5-nodes-to-script
|
||||
```
|
||||
|
||||
This is the branch VibeGeometry should use locally. It contains:
|
||||
|
||||
- the nested `@tree` group fix from
|
||||
`vibegeometry/blender-5-nested-tree-groups`
|
||||
- a Blender 5.1 port of the historical `nodes_to_script` converter prototype
|
||||
- a Blender 5.1 menu-socket default tolerance fix so group construction does
|
||||
not fail when `NodeSocketMenu` enum items are not populated yet
|
||||
- a converter duplicate-key fix for nodes that expose one control as both an
|
||||
RNA property and a linkable input socket, such as `FunctionNodeAxesToRotation`
|
||||
|
||||
The converter adds:
|
||||
|
||||
- `operators/convert_tree.py`
|
||||
- a callable `convert_node_tree(tree)` function for headless tests
|
||||
- a `geometry_script.convert_tree` node editor operator that writes a generated
|
||||
script into a Blender text datablock
|
||||
|
||||
Smoke test:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-fieldvalue.blend' --python '.\tools\smoke_nodes_to_script_converter.py' -- --groups 'Create Decimal' 'Digit At' 'Next Digit' 'Seven Segments' 'Delete Segments' 'Field Value' --json '.\experiments\inspection\nodes-to-script-smoke.json' --out-dir '.\experiments\inspection\nodes-to-script-output'
|
||||
```
|
||||
|
||||
Current acceptance:
|
||||
|
||||
- Converter runs in Blender 5.1.1.
|
||||
- Generated drafts compile as Python for the tested groups.
|
||||
- The full 176-node `Field Value` source group emits a 179-line draft script.
|
||||
- IRCSS `GenerateArc` and `MakeStairs` now emit compiling drafts instead of
|
||||
duplicate `primary_axis` keyword syntax errors.
|
||||
- Generated text is a bootstrap draft, not doctrine-quality final code or style
|
||||
guidance.
|
||||
|
||||
Known limits:
|
||||
|
||||
- Nested source group calls are emitted by source group name, so dependency
|
||||
groups must be converted or replaced by hand-authored equivalents.
|
||||
- The generated script preserves node-level wiring and therefore can be noisy.
|
||||
- Generated names and ordering reflect extraction mechanics, not spatial intent.
|
||||
- Behavioral equivalence still needs the same evaluated-geometry or scalar
|
||||
harness verification used by the hand translations.
|
||||
- Blender 5 capture items are still a live gap in Geometry Script's generic
|
||||
node builder. The accepted `VG Field Value` translation creates typed capture
|
||||
items explicitly in script instead of relying on the generated
|
||||
`capture_attribute(...)` wrapper.
|
||||
|
||||
## Upstream PR Branch
|
||||
|
||||
Branch:
|
||||
|
||||
```text
|
||||
vibegeometry/blender-5-nested-tree-groups
|
||||
```
|
||||
|
||||
Problem:
|
||||
|
||||
- Geometry Script's `@tree` decorator returns a Python function intended to
|
||||
create a nested node-group node when called inside another `@tree`.
|
||||
- Under Blender 5.1, that returned function tries to call
|
||||
`geometrynodegroup(...)`, but no such generated DSL symbol exists.
|
||||
- Manually calling the generated `group(node_tree=...)` wrapper returned
|
||||
`None` in the nested path, so callers could not access group outputs.
|
||||
|
||||
Patch:
|
||||
|
||||
- In Blender 4/5, create `GeometryNodeGroup` directly.
|
||||
- Assign `result.node_tree = node_group`.
|
||||
- Wire positional and keyword arguments into enabled group inputs with
|
||||
Geometry Script's existing `set_or_create_link(...)` helper.
|
||||
- Return `Type(...)` wrappers for each enabled group output.
|
||||
|
||||
Verification:
|
||||
|
||||
- `examples/geometry_script/shriinivas_cartesian_helper.py` loads
|
||||
`external/geometry-script` through `tools/geometry_script_loader.py`.
|
||||
- `VG Cartesian Circle` calls `vg_cartesian_helper(...)` twice inside its own
|
||||
`@tree` body.
|
||||
- Generated circle graph contains nested `GeometryNodeGroup` nodes.
|
||||
- `tools/verify_cartesian_translations.py` verifies helper, line, parabola, and
|
||||
circle behavior against the source `cartesian.blend` groups in Blender 5.1.1.
|
||||
|
||||
## Fork/PR Todo
|
||||
|
||||
- Upstream PR: https://github.com/carson-katri/geometry-script/pull/69
|
||||
- Keep PR #69 limited to the nested `@tree` fix. Converter work belongs on
|
||||
`vibegeometry/blender-5-nodes-to-script` unless it is intentionally prepared
|
||||
as a separate upstream PR.
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
# Geometry Nodes Translation Corpus
|
||||
|
||||
This note tracks public Geometry Nodes examples that are useful for learning how
|
||||
to translate visual node graphs into Geometry Script. The goal is not to collect
|
||||
cool files. The goal is to build a graded ladder from known node networks to
|
||||
scripts we can generate, inspect, render, and revise.
|
||||
|
||||
## Source Selection Rules
|
||||
|
||||
- Prefer `.blend` files or repositories with real node groups over screenshot
|
||||
and video-only tutorials.
|
||||
- Prefer examples with clear visual outcomes and bounded mechanisms.
|
||||
- Use Blender itself to inspect node groups before recreating anything.
|
||||
- Translate one graph idiom at a time into Geometry Script.
|
||||
- Verify each translation by generating a node group in Blender, inspecting its
|
||||
node/link shape, and rendering when the visual result matters.
|
||||
- Keep video-only sources as secondary explanation, not primary truth.
|
||||
|
||||
## Complexity Ladder
|
||||
|
||||
### Tier 1: Primitive Construction And Simple Fields
|
||||
|
||||
Use these to learn Geometry Script signatures, result objects, chain syntax,
|
||||
modifier sockets, and basic graph inspection.
|
||||
|
||||
- `Shriinivas/geometrynodes`
|
||||
- Source: https://github.com/Shriinivas/geometrynodes
|
||||
- Candidate files: `cartesian.blend`, `paramnpolareq.blend`,
|
||||
`piechart.blend`, `fieldvalue.blend`
|
||||
- Why it matters: small mathematical graphs with clear output shapes.
|
||||
- Translation value: curve generation, mesh primitives, math nodes, exposed
|
||||
parameters, and field/value flow.
|
||||
|
||||
### Tier 2: Scattering, Instancing, And Attribute Flow
|
||||
|
||||
Use these after the primitive layer can be recreated without guessing.
|
||||
|
||||
- Blender public Geometry Nodes demo: instance attributes grass tuft
|
||||
- Source:
|
||||
https://download.blender.org/demo/geometry-nodes/instance_attribtues.blend
|
||||
- Completed entry:
|
||||
`docs/research/translations/blender-instance-attributes.md`
|
||||
- Why it matters: compact scatter graph that captures instance root position
|
||||
before realization and uses it to curl each realized blade.
|
||||
- Translation value: distribute points, instance on points, scale instances,
|
||||
capture attributes on instance domain, realize instances, tangent-axis
|
||||
deformation, and profile-scaled curve-to-mesh output.
|
||||
|
||||
- Blender Guru beginner Geometry Nodes candy tutorial
|
||||
- Source: https://www.blenderguru.com/tutorials/2025/9/10/blender-tutorial-geometry-nodes-for-beginners
|
||||
- Candidate artifact: downloadable final `.blend`
|
||||
- Why it matters: canonical beginner scatter workflow.
|
||||
- Translation value: distribute points, instance on points, random rotation,
|
||||
random scale, join geometry, and exposed controls.
|
||||
|
||||
- Samuel Sullins demo file via BlenderNation
|
||||
- Source: https://www.blendernation.com/2023/04/20/learn-geometry-nodes-with-this-demo-file/
|
||||
- Candidate artifact: free demo `.blend` with node comments and simple
|
||||
scattering setups.
|
||||
- Why it matters: likely built for learning and rebuilding.
|
||||
- Translation value: comments may make node intent easier to map into prose
|
||||
and Geometry Script.
|
||||
|
||||
### Tier 3: Reusable Utility Node Groups
|
||||
|
||||
Use these to learn how artists package node networks into inspectable tools.
|
||||
|
||||
- `quellenform/blender-CurveToMeshUV`
|
||||
- Source: https://github.com/quellenform/blender-CurveToMeshUV
|
||||
- Candidate file: `Curve to Mesh UV v1.7.02.230213-FREE - Blender 3.4+.blend`
|
||||
- Why it matters: a focused utility group with a clear contract.
|
||||
- Translation value: curve-to-mesh workflows, UV attributes, face-corner
|
||||
domain discipline, output attribute handling.
|
||||
|
||||
- `Tams3d/T3D-GN-Presets`
|
||||
- Source: https://github.com/Tams3d/T3D-GN-Presets
|
||||
- Candidate file: `t3d_nodes.blend`
|
||||
- Why it matters: a large preset library for Blender 4.4+ with many utility
|
||||
node groups.
|
||||
- Translation value: reusable group organization, socket naming, matrix and
|
||||
deformation utilities. This is not a first target; it is a quarry.
|
||||
|
||||
### Tier 4: Procedural Systems
|
||||
|
||||
Use these only after the smaller idioms are reliable.
|
||||
|
||||
- Blender public Geometry Nodes demo: Index Of Nearest
|
||||
- Source: https://download.blender.org/demo/geometry-nodes/index_of_nearest.blend
|
||||
- Completed entry:
|
||||
`docs/research/translations/blender-index-of-nearest.md`
|
||||
- Why it matters: compact helper groups around a simulation-zone point
|
||||
system.
|
||||
- Translation value: nearest-neighbor lookup, evaluate-at-index sampling,
|
||||
point-radius collision, velocity-field harnessing, and boundary/collider
|
||||
constraint helpers.
|
||||
|
||||
- Blender public Geometry Nodes demo: shortest-path labyrinth
|
||||
- Source:
|
||||
https://download.blender.org/demo/geometry-nodes/labyrinth-shortest_path-simon_thommes.blend
|
||||
- Completed entry:
|
||||
`docs/research/translations/blender-labyrinth-shortest-path.md`
|
||||
- Why it matters: compact pathfinding system built around shortest edge
|
||||
paths and path-to-curve conversion.
|
||||
- Translation value: topology-first graph structure, edge path selection,
|
||||
path capture before deletion, filleted curve output, and solution reveal
|
||||
control.
|
||||
|
||||
- Blender public Geometry Nodes demo: raycast minigame
|
||||
- Source: https://download.blender.org/demo/geometry-nodes/raycast-minigame.blend
|
||||
- Completed entry:
|
||||
`docs/research/translations/blender-raycast-minigame.md`
|
||||
- Why it matters: small ray-step helper groups inside a larger game-like
|
||||
procedural setup.
|
||||
- Translation value: initial direction vectors, sampled ray lines, raycast
|
||||
hit updates, reflected direction, and curve-output harnessing.
|
||||
|
||||
- Blender Studio Geometry Nodes from Scratch files
|
||||
- Source: https://studio.blender.org/training/geometry-nodes-from-scratch/chapter/files/
|
||||
- Candidate files: rock scattering, tree generator, grass tuft generator,
|
||||
spaceship generator, geometry components demo.
|
||||
- Why it matters: production-adjacent learning files with a broad concept
|
||||
range.
|
||||
- Translation value: progressive systems that combine distribution,
|
||||
attributes, generated geometry, materials, and exposed controls.
|
||||
|
||||
- `IRCSS/Trees-With-Geometry-Nodes-Blender`
|
||||
- Source: https://github.com/IRCSS/Trees-With-Geometry-Nodes-Blender
|
||||
- Candidate artifact: tree-generation node groups.
|
||||
- Why it matters: procedural tree generation based on curves and instancing.
|
||||
- Translation value: recursive-looking branch systems, curve distortion,
|
||||
instance orientation, scale variation, and material variation.
|
||||
|
||||
- `IRCSS/Blender-Geometry-Node-French-Houses`
|
||||
- Source: https://github.com/IRCSS/Blender-Geometry-Node-French-Houses
|
||||
- Completed foothold:
|
||||
`docs/research/translations/ircss-french-houses.md`
|
||||
- Structural maps:
|
||||
`docs/research/translations/ircss-french-houses-system-map.md`,
|
||||
`docs/research/translations/ircss-french-houses-node-atlas.md`, and
|
||||
`docs/research/translations/ircss-french-houses-node-mechanisms.md`
|
||||
- Why it matters: city-scale architectural kit with houses, gates, towers,
|
||||
walls, bridges, church parts, stalls, terrain, roads, vegetation, banners,
|
||||
windows, doors, and roofs.
|
||||
- Translation value: hierarchical procedural city systems, façade organs,
|
||||
topology generators, repeat/foreach/gizmo blockers, and reusable spines.
|
||||
|
||||
## First Translation Target
|
||||
|
||||
Start with `Shriinivas/geometrynodes` because it is public, GitHub-hosted, MIT
|
||||
licensed, and contains several bounded `.blend` examples. The first useful
|
||||
translation is a tiny graph from `cartesian.blend` or `piechart.blend`, chosen
|
||||
after node group inspection.
|
||||
|
||||
First completed translation entry:
|
||||
|
||||
- `docs/research/translations/shriinivas-cartesian-helper.md`
|
||||
- Source group: `NodeGroup.001` from `cartesian.blend`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/shriinivas_cartesian_helper.py`
|
||||
|
||||
Completed Cartesian function graph entry:
|
||||
|
||||
- `docs/research/translations/shriinivas-cartesian-function-graphs.md`
|
||||
- Source groups: `Geometry Nodes Line`, `Geometry Nodes Parabola`,
|
||||
`Geometry Nodes Circle`
|
||||
- Verification: all translated graphs match evaluated source geometry in
|
||||
Blender 5.1.1. Circle matches as a sorted vertex set because node expansion
|
||||
changes vertex order while preserving shape.
|
||||
- Toolchain note: `external/geometry-script` is patched so nested `@tree` calls
|
||||
create `GeometryNodeGroup` nodes under Blender 5.1.
|
||||
|
||||
Completed pie chart entry:
|
||||
|
||||
- `docs/research/translations/shriinivas-piechart.md`
|
||||
- Source groups: `NodeGroup`, `Pie Chart`, `getPie.002` through
|
||||
`getPie.012`, `Extended Pie Chart`
|
||||
- Verification: translated segment and three-slice chart match evaluated source
|
||||
geometry with sorted max vertex delta `0.0` in Blender 5.1.1; extended
|
||||
segment and extended ten-slice chart also match source geometry with sorted
|
||||
max vertex delta `0.0`.
|
||||
|
||||
Completed field value helper entry:
|
||||
|
||||
- `docs/research/translations/shriinivas-fieldvalue-digit-at.md`
|
||||
- Source groups: `Create Decimal`, `Create Segment`, `Seven Segments`,
|
||||
`Delete Segments`, `Next Digit`, `Digit At`
|
||||
- Verification: decimal glyph geometry matches source with sorted max vertex
|
||||
delta `0.0`; segment geometry matches horizontal and vertical source cases
|
||||
with sorted max vertex delta `0.0`; seven-segment chassis geometry matches
|
||||
source with 74 vertices and sorted max vertex delta `0.0`; digit scalar
|
||||
helper matches source behavior across four number/position cases by driving
|
||||
`Mesh Line.Count` with the result; next-digit branch helper matches source
|
||||
across four cases spanning both sides of `Max Precision`; delete-mask scalar
|
||||
helper matches source across 195 digit/segment-position cases with zero
|
||||
mismatches; full `Field Value` default-route geometry matches source with
|
||||
256 vertices and sorted max vertex delta `0.0`.
|
||||
- Toolchain note: full `Field Value` required explicit Blender 5 capture-item
|
||||
creation and a Geometry Script fork fix for menu socket defaults.
|
||||
|
||||
Completed parametric/polar equation entry:
|
||||
|
||||
- `docs/research/translations/shriinivas-paramnpolareq.md`
|
||||
- Source groups: `chooseFloatVal`, `chooseFloatVal.001`, `chooseStrVal`,
|
||||
`chooseStrVal.001`, `NodeGroup`, `NodeGroup.001`, `NodeGroup.002`,
|
||||
`NodeGroup.003`, `getxy.001`, `Archimedes Spiral`, `Epicycloid`,
|
||||
`Geometry Nodes Group`, `Geometry Nodes Group.002`
|
||||
- Verification: scalar equation helpers match by placing outputs into a single
|
||||
vertex position; Archimedes, epicycloid, and mirrored root spiral evaluated
|
||||
vertices match source geometry with max sorted deltas at or near `0.0`.
|
||||
- Translation value: accumulated parameter ramps, parametric equations, polar
|
||||
conversion, mirrored curve streams, and scalar-harness design.
|
||||
|
||||
Completed CurveToMeshUV utility entry:
|
||||
|
||||
- `docs/research/translations/quellenform-curve-to-mesh-uv.md`
|
||||
- Source groups: `Auto Smooth`, `Curve to Mesh UV`, `_258246`, `_Title`
|
||||
- Verification: source and translated `Auto Smooth` preserve cube geometry and
|
||||
match smooth-face flags in Blender 5.1.1; main utility mesh, UV-field
|
||||
harness, caps-mask harness, demo geometry, and title geometry all match
|
||||
source evaluated geometry.
|
||||
|
||||
Completed Blender instance attributes entry:
|
||||
|
||||
- `docs/research/translations/blender-instance-attributes.md`
|
||||
- Source group: `Grass Tuft Generator`
|
||||
- Verification: default and wider/curlier/seeded control cases match source
|
||||
evaluated geometry with sorted max vertex delta `0.0`; edge counts, polygon
|
||||
counts, and smooth-face flags also match.
|
||||
- Translation value: scatter nurseries, reused radial falloff, instance-domain
|
||||
capture before realization, and per-blade curl driven by remembered root
|
||||
position.
|
||||
|
||||
Completed Blender index-of-nearest helper entry:
|
||||
|
||||
- `docs/research/translations/blender-index-of-nearest.md`
|
||||
- Source groups: `boundary_step`, `update_velocity`, `collision_step`,
|
||||
`collider_step`
|
||||
- Verification: helper harnesses match source evaluated geometry with sorted
|
||||
max vertex delta `0.0`.
|
||||
- Translation value: constraint projection, velocity capping, nearest-neighbor
|
||||
collision, and point-output verification harnesses.
|
||||
|
||||
Completed Blender shortest-path labyrinth entry:
|
||||
|
||||
- `docs/research/translations/blender-labyrinth-shortest-path.md`
|
||||
- Source group: `Solvable Labyrinth Generator`
|
||||
- Verification: small unsolved and solved cases match source evaluated
|
||||
geometry with sorted max vertex delta `0.0`; edge and polygon counts also
|
||||
match.
|
||||
- Translation value: shortest edge paths, path capture, topology carving,
|
||||
path-to-curve conversion, and solution reveal.
|
||||
|
||||
Completed Blender raycast minigame foothold:
|
||||
|
||||
- `docs/research/translations/blender-raycast-minigame.md`
|
||||
- Source groups: `Initial Direction`, `Line to be Casted`, `Cast Rays`
|
||||
- Verification: direction, line, and raycast-step harnesses match source
|
||||
evaluated geometry with sorted max vertex delta `0.0`.
|
||||
- Translation value: direction vectors, curve breadcrumbs, raycast hit updates,
|
||||
reflected vectors, and object-target controls.
|
||||
|
||||
Completed IRCSS French Houses foothold:
|
||||
|
||||
- `docs/research/translations/ircss-french-houses.md`
|
||||
- File-level map: `docs/research/translations/ircss-french-houses-system-map.md`
|
||||
- All-node atlas:
|
||||
`docs/research/translations/ircss-french-houses-node-atlas.md`
|
||||
- Node mechanism study:
|
||||
`docs/research/translations/ircss-french-houses-node-mechanisms.md`
|
||||
- Source groups: `MakeSpiral`, `PointyGothicCone`
|
||||
- Verification: spiral output curve converted to points matches source with
|
||||
ordered max vertex delta `0.0`; plain and ornamented gothic cone cases match
|
||||
source evaluated mesh vertices with sorted max vertex delta `0.0`.
|
||||
- Translation value: procedural-city corpus triage, spiral architectural
|
||||
spines, roof-cap ornament grown from structural ribs, captured source points
|
||||
through instancing, and explicit blockers for repeat-zone feedback, repeat
|
||||
`Top` outputs, foreach zones, gizmo nodes, and converter duplicate-key
|
||||
failures.
|
||||
|
||||
Inspected but deferred official demo files:
|
||||
|
||||
- `hexgrid_blender_geometry_nodes_demo.blend`: mostly legacy undefined nodes
|
||||
in Blender 5.1, plus a duplicate `Auto Smooth` helper already covered.
|
||||
- `wavy_procedural_wall.blend`: several legacy/undefined attribute-toolkit
|
||||
groups; useful later as migration quarry, not current doctrine.
|
||||
- `pebble_scattering.blend`: legacy undefined nodes dominate the single group.
|
||||
- `gizmo_array.blend`: focused on Blender 4.3+ geometry-node gizmo nodes.
|
||||
Needs a dedicated gizmo-authoring pass.
|
||||
- `jiggly_pudding.blend`: simulation-zone graph. Needs dedicated simulation
|
||||
zone support and verification discipline.
|
||||
- `repeat_zone_flower_by_MiRA.blend`: repeat-zone flower system. Needs a
|
||||
repeat-zone pass before translation.
|
||||
|
||||
## Extraction Workflow
|
||||
|
||||
1. Download one source `.blend` into an ignored experiment folder.
|
||||
2. Run Blender headlessly to list geometry node groups, group interface sockets,
|
||||
node types, link counts, and exposed modifiers.
|
||||
3. Pick one small node group and write a plain-language graph map:
|
||||
inputs, generated geometry, fields/attributes, materials, outputs, and why
|
||||
each stage belongs.
|
||||
4. Recreate that group with Geometry Script using local generated 5.1 docs for
|
||||
signatures.
|
||||
5. Smoke-test generation in Blender and compare node types/link shape against
|
||||
the source.
|
||||
6. Render only after the graph structure is close enough to make visual review
|
||||
meaningful.
|
||||
|
||||
## Known Tooling Need
|
||||
|
||||
`tools/inspect_blend_node_groups.py` can summarize geometry node groups from an
|
||||
opened `.blend` file into JSON. It records group interfaces, nodes, simple node
|
||||
properties, sockets, links, and modifier users.
|
||||
|
||||
`tools/verify_cartesian_translations.py` verifies the translated Cartesian
|
||||
helper, line, parabola, and circle groups against their source groups by
|
||||
wrapping each graph in the same evaluator graph and comparing evaluated mesh
|
||||
vertices.
|
||||
|
||||
Generated node groups must be referenced by an object/modifier or marked with
|
||||
`use_fake_user = True` before saving a `.blend`; otherwise Blender discards the
|
||||
orphaned group on save. Very normal behavior, if your filing cabinet hates you.
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
# Realtime CSG Bevy Assembler
|
||||
|
||||
## Source Read
|
||||
|
||||
LogicalError's RealtimeCSG is useful as an architectural reference, not as a
|
||||
kernel we can directly port. The public repository describes a Unity editor
|
||||
plugin for CSG level editing and states that runtime editing is not supported
|
||||
through a maintained API. It also states that the native C++ source is not
|
||||
public.
|
||||
|
||||
The public C# layer still exposes the parts worth stealing cleanly:
|
||||
|
||||
- control meshes made from vertices, half edges, polygons, and texgen/surface
|
||||
metadata
|
||||
- tree, branch, and brush handles
|
||||
- explicit brush operation types
|
||||
- dirty generation counters
|
||||
- a rebuild step that turns authored brushes into renderable meshes
|
||||
|
||||
The older public demo is more important than the Unity product for clean
|
||||
algorithm work. Sander van Rossen's 2009-2010 blog series describes the
|
||||
principles directly: consistent epsilon classification, half-edges, polygon
|
||||
cutting, convex brush construction, boolean categorization, and the idea that
|
||||
subtraction can be expressed as composed logical operations. The
|
||||
`LogicalError/Realtime-CSG-demo` repository is public C# code from the article
|
||||
"Real Time Constructive Solid Geometry" in *Game Development Tools*. It is now
|
||||
the preferred research input for algorithmic behavior because it is public
|
||||
source tied to the article rather than a closed Unity native kernel.
|
||||
|
||||
Verified public sources:
|
||||
|
||||
- Sander van Rossen, "Realtime CSG - Part 1"
|
||||
- Sander van Rossen, "Realtime CSG - Part 5"
|
||||
- Sander van Rossen and Matthew Baranowski, "Real-Time Constructive Solid
|
||||
Geometry", *Game Development Tools*
|
||||
- `LogicalError/Realtime-CSG-demo`
|
||||
|
||||
That shape maps well to Bevy: keep authoring data as lightweight brush records,
|
||||
rebuild only dirty worlds, and emit plain mesh buffers that can later be handed
|
||||
to Bevy render assets.
|
||||
|
||||
## Current Port Shape
|
||||
|
||||
`crates/vg_csg` is the first lean Rust organ.
|
||||
|
||||
It uses `bevy_math` for `Vec3`, `Vec2`, and Bevy-compatible spatial types while
|
||||
keeping output as simple arrays:
|
||||
|
||||
```rust
|
||||
use bevy_math::Vec3;
|
||||
use vg_csg::{LevelDsl, MaterialId};
|
||||
|
||||
let mut level = LevelDsl::new();
|
||||
level.solid_box(
|
||||
"wall",
|
||||
Vec3::new(0.0, 0.0, 1.5),
|
||||
Vec3::new(6.0, 0.3, 3.0),
|
||||
MaterialId(1),
|
||||
);
|
||||
level.cut_box(
|
||||
"door",
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::new(1.2, 0.6, 2.0),
|
||||
);
|
||||
|
||||
let output = level.assemble();
|
||||
assert!(output.mesh.triangle_count() > 12);
|
||||
```
|
||||
|
||||
The first actual CSG operator was exact AABB subtraction against additive AABB
|
||||
solids. The current kernel has moved to convex decomposition: an additive
|
||||
convex solid is split against each outward cutter plane, subtraction emits the
|
||||
outside fragments, and intersection keeps the inside remainder. This supports
|
||||
angled box cuts, real `Common` branch output for convex boxes, and gives us the
|
||||
correct shape for a future general convex brush kernel.
|
||||
|
||||
Polygons now carry the first routing metadata the mature kernel needs:
|
||||
category, visibility, reversal, and bounds. Source polygons can be classified
|
||||
as inside, outside, aligned, or reverse-aligned against a convex brush. When a
|
||||
polygon crosses a cutter plane, it is split first and each piece is routed to a
|
||||
category bucket. That matches the public demo's important shape: split crossing
|
||||
polygons before final category decisions.
|
||||
|
||||
That is deliberately not sold as full parity yet. The current assembler still
|
||||
emits meshes through convex fragment decomposition. The next parity step is to
|
||||
make those category buckets drive boolean-tree mesh emission directly.
|
||||
|
||||
The important doctrinal adjustment from the public demo and blog series is that
|
||||
real-time CSG should think in **classification**, not only carving. A polygon is
|
||||
inside, outside, aligned, or reverse-aligned relative to another node. Boolean
|
||||
operations are then mostly routing tables for those categories. Our current
|
||||
fragment-splitting implementation is a stepping stone; the faster mature kernel
|
||||
should preserve brush polygons, classify them through the tree, and emit only
|
||||
the visible categories.
|
||||
|
||||
The expanded doctrine lives in
|
||||
`docs/research/realtime-csg-doctrine.md`; keep this file focused on the current
|
||||
crate shape.
|
||||
|
||||
Procedural non-CSG primitives are additive:
|
||||
|
||||
- `CylinderZ` for tethers, shafts, columns, conduits, and ring anchors
|
||||
- `DomeCapZ` for flat-base pressure bubbles and caps
|
||||
- `FloretArm` for sunflower/Citadel-scale solar and radiator petals
|
||||
|
||||
Those primitives are intentionally in the same brush stream as CSG boxes, so a
|
||||
level script can mix hard architectural cuts with habitat-specific forms:
|
||||
|
||||
```rust
|
||||
level.dome_cap_z(
|
||||
"city dome",
|
||||
vg_csg::DomeCapZSpec {
|
||||
center: Vec3::ZERO,
|
||||
radius: 20.0,
|
||||
height: 8.0,
|
||||
rings: 12,
|
||||
segments: 64,
|
||||
material: MaterialId(20),
|
||||
},
|
||||
);
|
||||
level.floret_arm(
|
||||
"radiator petal",
|
||||
vg_csg::FloretArmSpec {
|
||||
anchor: Vec3::new(20.0, 0.0, 0.2),
|
||||
direction: Vec3::X,
|
||||
length: 120.0,
|
||||
root_width: 6.0,
|
||||
tip_width: 18.0,
|
||||
thickness: 0.12,
|
||||
tip_lift: 3.0,
|
||||
material: MaterialId(21),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Doctrine
|
||||
|
||||
Think of brushes as legal claims on volume.
|
||||
|
||||
An additive brush says: "this matter exists." A subtractive brush says: "this
|
||||
space must remain empty." The assembler is the clerk that resolves claims in
|
||||
order and emits triangles only after the claims stop arguing.
|
||||
|
||||
For interactive work, the brush list is the durable design surface. The mesh is
|
||||
a cache. This matters because agents should revise intent by moving, adding, or
|
||||
renaming brushes, not by poking triangles after the fact.
|
||||
|
||||
Use CSG where boolean space carries meaning:
|
||||
|
||||
- doorways, windows, hatches, service trenches
|
||||
- docking sockets and tether anchor gaps
|
||||
- ring terraces and mechanically repeated cutouts
|
||||
- fast blockouts where the same cutter can punch many solids
|
||||
- common-volume filters where rooms, shafts, or constraint regions overlap
|
||||
|
||||
Use additive procedural primitives where a form is better described by a field
|
||||
or sweep than by carving:
|
||||
|
||||
- domes, rings, tethers, panels, radiators
|
||||
- city-field parcels after street generation
|
||||
- flower-like utility crests and other habitat-specific silhouettes
|
||||
|
||||
The immediate next upgrade is not "more primitives." It is the category-router
|
||||
kernel: split crossing polygons, route classified source polygons through
|
||||
boolean branches, and preserve material/surface metadata.
|
||||
|
||||
The next accelerator should not be a reflexive BVH-shaped apology. The
|
||||
EpiphanyAquarium memory points at a stronger pattern from GigaVoxels, froxels,
|
||||
and Dreams: let the consumer define the query frontier. For CSG, the consumer is
|
||||
not always "the whole scene." It can be the moved brush, the current branch, the
|
||||
editor tile, the requested output mesh, or the visible/view-tested surface set.
|
||||
So the near-term spatial plan is:
|
||||
|
||||
- use branch and brush bounds as semantic gates before cutting;
|
||||
- keep dirty/touched brush discovery separate from polygon classification;
|
||||
- build affected brush-pair or surface-frontier batches from those gates;
|
||||
- measure those batches before adding a generic BVH, loose grid, or spatial
|
||||
hash;
|
||||
- only promote an index if it beats the demand-shaped frontier on dirty rebuilds
|
||||
and preserves exact output parity.
|
||||
|
||||
Sideways version: do not ask every cutter whether it matters. Ask the branch,
|
||||
dirty region, and requested output which pairs deserve to exist this frame.
|
||||
|
||||
Performance fixtures live in `docs/research/csg-performance-fixtures.md` and
|
||||
`tools/run_csg_perf.ps1`. They time our release fixture now and reserve a JSONL
|
||||
slot for a public-demo/reference harness when one is available.
|
||||
|
||||
## Limits
|
||||
|
||||
`vg_csg` is not yet a full RealtimeCSG replacement.
|
||||
|
||||
- subtractive convex support currently covers axis-aligned and oriented boxes
|
||||
- non-box subtractors are reported and ignored
|
||||
- intersect/common support currently covers axis-aligned and oriented boxes
|
||||
- output is Bevy-compatible mesh data, not yet a Bevy `Mesh` asset constructor
|
||||
- there is no editor gizmo, ECS plugin, or incremental spatial index yet
|
||||
- current subtraction emits split fragments with generated cap polygons; the
|
||||
target kernel should move toward article/demo-style category routing over
|
||||
brush polygons for speed and cleaner material behavior
|
||||
- no reference timing executable is wired yet; the perf wrapper records this
|
||||
explicitly instead of inventing comparison numbers
|
||||
|
||||
Those limits are explicit because invisible ambition is how you get haunted by
|
||||
your own abstractions. The useful loop exists now: script brushes, assemble,
|
||||
inspect mesh stats, and replace the kernel under the same DSL when the target
|
||||
demands it.
|
||||
|
|
@ -1,336 +0,0 @@
|
|||
# Realtime CSG Doctrine
|
||||
|
||||
## Provenance
|
||||
|
||||
This note distills public material around Sander van Rossen and Matthew
|
||||
Baranowski's "Real-Time Constructive Solid Geometry" article and the later
|
||||
RealtimeCSG ecosystem. It excludes closed native kernel internals.
|
||||
|
||||
Primary clean inputs:
|
||||
|
||||
- Sander van Rossen, [Realtime CSG - Part 1](https://sandervanrossen.blogspot.com/2009/12/realtime-csg-part-1.html)
|
||||
- Sander van Rossen, [Realtime CSG - Part 5](https://sandervanrossen.blogspot.com/2010/05/realtime-csg-part-5.html)
|
||||
- Sander van Rossen, [Realtime CSG - Optimizations](https://sandervanrossen.blogspot.com/2010/05/realtime-csg-optimizations.html)
|
||||
- [`LogicalError/Realtime-CSG-demo`](https://github.com/LogicalError/Realtime-CSG-demo)
|
||||
- Patrick Kaster, [BSO3](https://patrickkaster.github.io/BSO3/index.html)
|
||||
- [Geometry in Milliseconds - Real-Time Constructive Solid Geometry](https://www.classcentral.com/course/youtube-geometry-in-milliseconds-real-time-constructive-solid-geometry-158033)
|
||||
- [three.js CSG support discussion](https://bleepingcoder.com/three-js/427091361/constructive-solid-geometry-csg-support)
|
||||
|
||||
## Core Model
|
||||
|
||||
Realtime CSG is not primarily a triangle boolean problem. It is a brush polygon
|
||||
visibility problem.
|
||||
|
||||
The article/blog/demo model starts with convex brushes, cuts source brush
|
||||
polygons only when needed, classifies the resulting polygon pieces through a
|
||||
CSG tree, and emits the pieces that remain visible. It does not treat the final
|
||||
mesh as the authority. The brush tree is the authority; the mesh is cache and
|
||||
evidence.
|
||||
|
||||
The highest-value shift for `vg_csg` is therefore:
|
||||
|
||||
```text
|
||||
from: subtract cutter -> produce new convex fragments -> triangulate fragments
|
||||
to: source brush polygons -> split only at crossing planes -> classify through tree -> emit visible polygon pieces
|
||||
```
|
||||
|
||||
Our current convex splitter is useful scaffolding, but it is still a carver.
|
||||
The target kernel is a classifier.
|
||||
|
||||
## Doctrines
|
||||
|
||||
### 1. Consistency Beats Accuracy
|
||||
|
||||
Floating point geometry fails first through inconsistent decisions, not through
|
||||
small absolute error. Every plane test needs one shared epsilon policy:
|
||||
|
||||
- distance `< -epsilon` is inside
|
||||
- distance `> epsilon` is outside
|
||||
- values between are coplanar
|
||||
|
||||
Do not mix inclusive/exclusive comparisons casually. The same point must not be
|
||||
inside for one subsystem and coplanar for another because someone got cute with
|
||||
`>=`.
|
||||
|
||||
For intersections, compute from a canonical direction. If an edge crosses a
|
||||
plane, pick one input order for interpolation and use it everywhere. Otherwise
|
||||
adjacent polygons will receive near-identical but non-identical vertices, and
|
||||
the mesh will die by a thousand decimal-place paper cuts.
|
||||
|
||||
### 2. Brushes Are Convex Plane Sets
|
||||
|
||||
A brush should be authored and cached as a set of planes, not merely as a
|
||||
triangle soup. Brush mesh creation is the act of finding valid intersections
|
||||
between those planes and linking the resulting topology.
|
||||
|
||||
Implication for `vg_csg`: add a `ConvexBrush` authored by planes, then derive
|
||||
polygons, half-edges, bounds, and render buffers from that. Boxes, cylinders,
|
||||
wedges, stairs, panels, and dome approximations should become constructors for
|
||||
plane sets where possible.
|
||||
|
||||
### 3. Half-Edges Are Working Topology, Not Render Data
|
||||
|
||||
Rendering buffers are bad workbenches for CSG. Downstream discussions around
|
||||
three.js make the same point from another angle: render-oriented geometries
|
||||
often lose topology, indexing, material grouping, or edit intent.
|
||||
|
||||
Use half-edge-like topology while cutting and classifying:
|
||||
|
||||
- split one edge and update both adjacent polygons consistently
|
||||
- preserve polygon identity, plane identity, material, and visibility
|
||||
- convert to Bevy mesh buffers only at the final boundary
|
||||
|
||||
This does not require a literal pointer-heavy half-edge graph forever. The
|
||||
important thing is that the working structure carries adjacency and polygon
|
||||
ownership better than a pile of final triangles.
|
||||
|
||||
### 4. Boolean Operations Are Routing Tables
|
||||
|
||||
The elegant part of the public demo is category routing. A polygon piece is
|
||||
classified as:
|
||||
|
||||
- inside
|
||||
- outside
|
||||
- aligned/touching inside
|
||||
- reverse-aligned/touching outside
|
||||
|
||||
Union, intersection, and subtraction can then be expressed through the same
|
||||
logical OR machinery with category parameter reversal:
|
||||
|
||||
- union: `A || B`
|
||||
- intersection: `!(!A || !B)`
|
||||
- subtraction: `!(!A || B)`
|
||||
|
||||
This matters because subtraction stops being a special monster. It becomes
|
||||
category routing plus orientation reversal. Less bespoke logic, fewer places
|
||||
for edge cases to hide and invoice us later.
|
||||
|
||||
### 5. Visible Polygons Are Source Polygons With Rejections
|
||||
|
||||
The target algorithm should avoid inventing new surface area except at cuts.
|
||||
It starts with all brush polygons and removes or hides the pieces that the tree
|
||||
rejects.
|
||||
|
||||
Top-level output policy:
|
||||
|
||||
- pieces inside the final tree are invisible
|
||||
- pieces outside the final tree are invisible
|
||||
- aligned pieces stay visible
|
||||
- reverse-aligned pieces need winding reversal
|
||||
- overlapping coplanar surfaces are resolved by tree/order policy so duplicate
|
||||
faces do not z-fight
|
||||
|
||||
Implication for `vg_csg`: material propagation should be natural if source
|
||||
polygons remain source polygons. Generated cut faces need explicit policy:
|
||||
inherit cutter material, source material, or a domain material such as "raw
|
||||
concrete cut." Do not leave that as accidental array order.
|
||||
|
||||
### 6. Bounds Are Part Of The Algorithm
|
||||
|
||||
Realtime performance comes from refusing work. Branch bounds and brush bounds
|
||||
allow early-outs before cutting:
|
||||
|
||||
- if polygons cannot touch a branch in union, route to the other side or
|
||||
outside immediately
|
||||
- if polygons are outside either side of an intersection, they are outside
|
||||
- if subtraction cannot touch the right/cutter branch, categorize only the left
|
||||
|
||||
The optimization post reports a major speedup from branch-specific bounds
|
||||
checks alone. This is not a garnish. Bounds are semantic gates.
|
||||
|
||||
Implication for `vg_csg`: every brush, polygon batch, and branch needs cheap
|
||||
bounds. Later BVH, hashed grid, or sweep-and-prune should be measured against
|
||||
the built-in tree culling instead of assumed superior by vibes in a lab coat.
|
||||
|
||||
The EpiphanyAquarium research memory adds a sharper rule: treat spatial queries
|
||||
as a demand problem before treating them as an index problem. GigaVoxels uses
|
||||
visible rays as the oracle for which bricks deserve high resolution. Wronski fog
|
||||
uses the camera froxel volume as the field where sources are injected once and
|
||||
sampled many times. Dreams' prototype refined frustum cells, shortened object
|
||||
lists as cells split, then switched to per-pixel sorted lists that could truncate
|
||||
at the first solid hit.
|
||||
|
||||
For `vg_csg`, that translates to consumer-shaped query frontiers:
|
||||
|
||||
- dirty brush movement asks for affected brush pairs, not a world rebuild;
|
||||
- branch evaluation asks for left/right bounds gates before polygon tests;
|
||||
- an editor viewport or tile asks for surfaces that can contribute to the
|
||||
requested output, not every latent overlap in the level;
|
||||
- repeated cutters ask for cached pair classifications or brush-frontier batches,
|
||||
not a thousand independent list walks.
|
||||
|
||||
This does not outlaw BVHs, grids, or sweep-and-prune. It demotes them. They are
|
||||
candidate storage layouts for a demand frontier, not the central idea. The
|
||||
central idea is that the request already contains spatial information; make the
|
||||
kernel exploit that before building a generic little bureaucracy with boxes.
|
||||
|
||||
### 7. Realtime Means Incremental, Not Global
|
||||
|
||||
The public notes frame the algorithm as fast for updating affected brushes, not
|
||||
as a promise to rebuild the whole world every frame.
|
||||
|
||||
Interactive loop doctrine:
|
||||
|
||||
- base brush meshes are cacheable
|
||||
- when a brush moves, reprocess it and the brushes it touches
|
||||
- brush independence creates parallel work units
|
||||
- global rebuild is a debugging tool and benchmark, not the intended editor
|
||||
path
|
||||
|
||||
Implication for Bevy: store authoring brushes as ECS/editor state, maintain a
|
||||
dirty set, find touching brushes through bounds/spatial index, then rebuild
|
||||
only affected render meshes.
|
||||
|
||||
### 8. CPU Cache Is A First-Class Design Constraint
|
||||
|
||||
Sander explicitly calls out cache-aware rewrite potential and half-edges as the
|
||||
largest pain point. The three.js discussion arrives at the same pressure from a
|
||||
different ecosystem: representation conversions, de-indexing, and optimization
|
||||
passes dominate runtime when data layout is wrong.
|
||||
|
||||
Rust doctrine:
|
||||
|
||||
- favor index-based arenas over pointer graphs
|
||||
- store hot plane, vertex, edge, polygon fields in compact arrays
|
||||
- avoid temporary list storms in the inner classifier
|
||||
- use flags/ranges/scratch arenas before heap allocation
|
||||
- keep render buffers as output, not internal truth
|
||||
|
||||
The first correct implementation can be plain. The fast implementation should
|
||||
be built with data layout in mind before the code ossifies into a shapely little
|
||||
performance tax.
|
||||
|
||||
### 9. Generated Mesh Optimization Is A Separate Pass
|
||||
|
||||
The public material notes remaining T-junctions, unmerged polygons, and
|
||||
unoptimized output. Downstream engine discussions agree: CSG and final render
|
||||
mesh optimization are related but distinct jobs.
|
||||
|
||||
Keep the kernel responsible for correct categorized polygon output. Then run
|
||||
separate passes for:
|
||||
|
||||
- polygon merge
|
||||
- T-junction repair
|
||||
- vertex welding
|
||||
- material grouping
|
||||
- Bevy mesh/index-buffer packing
|
||||
- normal/UV generation or preservation
|
||||
|
||||
Do not contaminate the boolean kernel with every render-buffer cleanup trick.
|
||||
That way lies soup with comments.
|
||||
|
||||
### 10. CSG Belongs Beside Procedural Grammars
|
||||
|
||||
BSO3 is useful philosophically: it ports the article algorithm into a growth
|
||||
grammar modeller by making boolean set operations a node type. For VibeGeometry,
|
||||
this means the Rust CSG assembler should not replace procedural generation. It
|
||||
should be one grammar operator among many.
|
||||
|
||||
Use CSG for:
|
||||
|
||||
- hard architectural voids
|
||||
- interlocking modular parts
|
||||
- tunnels, hatches, shafts, sockets, stairs, cut floors
|
||||
- level-design iteration where negative space is authored
|
||||
|
||||
Use procedural fields and meshes for:
|
||||
|
||||
- cities, settlements, organic detail, clouds, fields, decoration
|
||||
- high-frequency surface elaboration
|
||||
- forms whose identity is distribution rather than volume claim
|
||||
|
||||
The level DSL should let these coexist. Boolean nodes cut space; procedural
|
||||
nodes grow matter and detail around that space.
|
||||
|
||||
## Implementation Target For `vg_csg`
|
||||
|
||||
Near-term kernel shape:
|
||||
|
||||
```text
|
||||
ConvexBrush {
|
||||
planes
|
||||
polygons
|
||||
half_edges
|
||||
vertices
|
||||
bounds
|
||||
}
|
||||
|
||||
Branch {
|
||||
op: Union | Intersect | Subtract
|
||||
left
|
||||
right
|
||||
bounds
|
||||
}
|
||||
|
||||
Process brush:
|
||||
clone or borrow cached base polygons
|
||||
classify polygon pieces through root
|
||||
mark visible / invisible / reversed
|
||||
emit visible pieces into render mesh
|
||||
```
|
||||
|
||||
`vg_csg` now exposes the first version of that public tree surface:
|
||||
|
||||
- `CsgOperationType::{Additive, Subtractive, Intersecting}` with parity-tested
|
||||
ordinals matching the public API
|
||||
- `CsgBranchOp::{Addition, Subtraction, Common}` matching the public demo
|
||||
- `CsgTreeArena`, `CsgTree`, `CsgTreeBranch`, `CsgTreeBrush`, and `CsgNodeId`
|
||||
- branch child replacement and operation mutation
|
||||
- brush operation mutation
|
||||
- compilation from tree intent into the current ordered assembler backend
|
||||
|
||||
This is still not the final classifier kernel. It is the API skeleton that lets
|
||||
grammar and editor code speak in RealtimeCSG-shaped boolean trees while the
|
||||
backend grows into the category router.
|
||||
|
||||
Algorithm migration steps:
|
||||
|
||||
1. Add polygon categories and visible/reversed flags to `vg_csg`.
|
||||
2. Add plane-set brush construction independent of `Aabb`.
|
||||
3. Add a topology arena with vertices, directed edges, polygons, and polygon
|
||||
plane/material ids.
|
||||
4. Implement polygon-plane split with one epsilon policy and canonical
|
||||
intersection direction.
|
||||
5. Implement brush classification against convex brush planes.
|
||||
6. Implement boolean branch routing using category list swizzles.
|
||||
7. Add bounds early-outs before every expensive classification.
|
||||
8. Add dirty-set and touching-brush discovery.
|
||||
9. Add a demand-frontier prototype that produces affected brush-pair batches
|
||||
from dirty brushes, branch bounds, and requested output scope.
|
||||
10. Convert categorized visible polygons to Bevy mesh buffers.
|
||||
11. Benchmark global rebuild, affected-brush rebuild, and worst-case overlap
|
||||
scenes before making performance claims.
|
||||
|
||||
## Acceptance Tests To Add
|
||||
|
||||
- Where `vg_csg` exposes the same behavior as public RealtimeCSG/demo APIs,
|
||||
maintain exact observable parity fixtures. These should compare public
|
||||
behavior such as category vocabulary, brush plane/polygon counts, split
|
||||
counts, visible/reversed semantics, bounds early-outs, and mesh output
|
||||
invariants. Do not use vague "looks right" tests where a public seam has a
|
||||
countable contract.
|
||||
- Two overlapping additive boxes produce no duplicate coplanar surface.
|
||||
- Subtraction is implemented as `!(!A || B)` and matches direct cases.
|
||||
- Intersection is implemented as `!(!A || !B)` and matches direct cases.
|
||||
- Reverse-aligned output flips winding exactly once.
|
||||
- A moved cutter only dirties itself and brushes whose bounds overlap it.
|
||||
- Polygon split creates shared vertices for adjacent polygons on the same cut.
|
||||
- Repeated cuts do not create strict-inside triangle centers in the void.
|
||||
- Material ids survive classification on source faces and follow an explicit
|
||||
policy on cut faces.
|
||||
- Global rebuild and affected-brush rebuild produce identical render buffers
|
||||
for a deterministic scene.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should cut faces inherit source material, cutter material, or a DSL-specified
|
||||
cut material?
|
||||
- Should Bevy output preserve large n-gons for later optimization, or triangulate
|
||||
at the boundary immediately?
|
||||
- What is the smallest demand frontier after branch bounds: dirty affected
|
||||
brush pairs, viewport/tile-frontier batches, pair-classification caches, or no
|
||||
extra structure until profiling says tree culling is insufficient?
|
||||
- If a frontier needs storage, which layout wins under dirty rebuild timings:
|
||||
loose grid, BVH, sweep-and-prune, pair cache, or sorted frontier batches?
|
||||
- How much topology should live in reusable library code versus Bevy ECS
|
||||
components?
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Translation: Blender Index Of Nearest Helpers
|
||||
|
||||
## Source
|
||||
|
||||
- Source file: https://download.blender.org/demo/geometry-nodes/index_of_nearest.blend
|
||||
- Source groups translated in this pass:
|
||||
- `boundary_step`
|
||||
- `update_velocity`
|
||||
- `collision_step`
|
||||
- `collider_step`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/blender_index_of_nearest.py`
|
||||
|
||||
## Why This Graph
|
||||
|
||||
The main `Geometry Nodes` group is a simulation-zone point system. The compact
|
||||
helper groups are the useful entry point: each one enforces a small physical
|
||||
rule on point positions or velocity fields.
|
||||
|
||||
## Helper Maps
|
||||
|
||||
`VG Boundary Step`:
|
||||
|
||||
- Compute point position relative to a center vector.
|
||||
- If distance exceeds boundary radius `B`, normalize the direction and place
|
||||
the point on the boundary circle.
|
||||
- Multiply by `(1, 1, 0)` to keep the constraint planar.
|
||||
|
||||
`VG Update Velocity`:
|
||||
|
||||
- Estimate movement from current `Position - Last Position`.
|
||||
- Dampen and flatten the movement vector.
|
||||
- If it exceeds the velocity cap, normalize to the cap.
|
||||
- Otherwise blend previous velocity toward the new planar movement.
|
||||
|
||||
`VG Collision Step`:
|
||||
|
||||
- Find the nearest point with `Index Of Nearest`.
|
||||
- Evaluate that neighbor's position with `Evaluate at Index`.
|
||||
- If the current point is closer than its radius, push it away a small planar
|
||||
amount.
|
||||
|
||||
`VG Collider Step`:
|
||||
|
||||
- Read an object's location and scale.
|
||||
- If a point is inside the object's scaled radius, project it to the collider
|
||||
boundary in XY.
|
||||
- The accepted Geometry Script group exposes the collider as an object input
|
||||
instead of hard-coding the source object's name.
|
||||
|
||||
Metaphor: these groups are bumpers. They do not decide the whole simulation;
|
||||
they make one small rule true each tick: stay inside the table, do not move too
|
||||
fast, stop sitting on another point, stop sitting inside the obstacle.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\blender-index-of-nearest.blend' --python '.\tools\verify_index_of_nearest_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source vertices | Translated vertices | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| boundary_step | 6 | 6 | 0.0 | accepted |
|
||||
| update_velocity harness | 4 | 4 | 0.0 | accepted |
|
||||
| collision_step point harness | 5 | 5 | 0.0 | accepted |
|
||||
| collider_step | 6 | 6 | 0.0 | accepted |
|
||||
|
||||
The velocity output is verified by driving `Set Position.Position` with the
|
||||
output vector. The collision output is verified by converting the resulting
|
||||
points to vertices after the helper group.
|
||||
|
||||
## Lessons
|
||||
|
||||
- Small simulation helpers are better first translation targets than full
|
||||
simulation zones.
|
||||
- Constraint groups read well when scripted as condition, projection, and
|
||||
`Set Position`.
|
||||
- Invisible vector fields need harnesses. `update_velocity` became observable
|
||||
only after using the vector output as point position.
|
||||
- Point-cloud outputs can evaluate as empty mesh summaries. Convert points to
|
||||
vertices in the verifier when point positions are the behavior under test.
|
||||
- Geometry Script's `mix(...)` helper mapped to the wrong Blender node family
|
||||
here. The accepted translation expresses the source vector mix as explicit
|
||||
scale-and-add nodes.
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# Translation: Blender Instance Attributes Grass Tuft
|
||||
|
||||
## Source
|
||||
|
||||
- Source file: https://download.blender.org/demo/geometry-nodes/instance_attribtues.blend
|
||||
- Source group: `Grass Tuft Generator`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/blender_instance_attributes.py`
|
||||
|
||||
## Why This Graph
|
||||
|
||||
This is the first verified scatter/instance/deform sample in the corpus. It is
|
||||
small enough to translate directly, but it uses the important grown-up trick:
|
||||
capture instance state before realization, then use that remembered value to
|
||||
deform the realized geometry.
|
||||
|
||||
## Graph Map
|
||||
|
||||
Source group: `Grass Tuft Generator`
|
||||
|
||||
Geometry Script group: `VG Grass Tuft Generator`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Build a triangular-fan disc, subdivide it, and use it as the scatter nursery.
|
||||
- Compute radial distance from `Position`, remap it through `Map Range`, and
|
||||
reuse that falloff for both point density and instance scale.
|
||||
- Build one blade as a five-point vertical mesh line, convert it to a curve,
|
||||
and taper it with `Set Curve Radius` from `1 - Spline Parameter.Factor`.
|
||||
- Distribute points on the disc with Poisson spacing controlled by thickness
|
||||
and density.
|
||||
- Instance the tapered blade on the points, scale each instance by radial
|
||||
falloff, then capture the instance `Position` on the `INSTANCE` domain.
|
||||
- Realize the instances. The captured root position survives as the center,
|
||||
radius sample, and tangent-axis basis for curl.
|
||||
- Rotate each realized blade position around a tangent axis. Curl grows along
|
||||
blade length and is modulated by distance from the tuft center.
|
||||
- Convert curled curves to mesh with a triangular profile. If a `radius`
|
||||
attribute exists, use it as the profile scale.
|
||||
|
||||
Metaphor: the disc is a nursery tray, the points are planting holes, each
|
||||
instance carries a root tag, and realization turns the tray into individual
|
||||
blades. Curl reads the root tag to bend every blade around its own base instead
|
||||
of around the world origin.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\blender-instance-attributes.blend' --python '.\tools\verify_instance_attributes_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source vertices | Translated vertices | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| default | 45 | 45 | 0.0 | accepted |
|
||||
| wider curlier seeded | 165 | 165 | 0.0 | accepted |
|
||||
|
||||
Both cases also match edge counts, polygon counts, and smooth-face flags.
|
||||
|
||||
## Lessons
|
||||
|
||||
- Scatter graphs often have three separable layers: seed surface, point field,
|
||||
and instance body. Keep them named in script.
|
||||
- A field can be reused across domains intentionally. Here, radial falloff
|
||||
controls both scatter density and final instance scale.
|
||||
- Captured instance attributes must be explicit under Blender 5.1. Geometry
|
||||
Script's generic `capture_attribute(...)` call did not create the required
|
||||
capture item here; the verified script uses the explicit
|
||||
`_capture_attribute_item(...)` helper.
|
||||
- `nodes_to_script` recovered the wiring and defaults well enough to debug the
|
||||
translation, but its output remains a transcript, not accepted style.
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# Translation: Blender Shortest-Path Labyrinth
|
||||
|
||||
## Source
|
||||
|
||||
- Source file:
|
||||
https://download.blender.org/demo/geometry-nodes/labyrinth-shortest_path-simon_thommes.blend
|
||||
- Source group: `Solvable Labyrinth Generator`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/blender_labyrinth_shortest_path.py`
|
||||
|
||||
## Why This Graph
|
||||
|
||||
This graph is the first accepted pathfinding translation. It uses grid topology,
|
||||
randomized edge costs, shortest-edge paths, path selection, path-to-curve
|
||||
conversion, stored UV attributes, curve radius, and endpoint markers.
|
||||
|
||||
## Graph Map
|
||||
|
||||
- Build a square grid and store its UV map on the corner domain.
|
||||
- Mark vertex `0` as the path root.
|
||||
- Use edge-neighbor count and random booleans to create edge costs.
|
||||
- Run `Shortest Edge Paths`, convert the result to an edge selection, and
|
||||
capture that boolean on the edge domain.
|
||||
- Subdivide the mesh and delete selected points to carve the walkable graph.
|
||||
- Run shortest paths again on the carved graph, find the farthest X point on
|
||||
the path, and convert the edge path into curves.
|
||||
- Instance endpoint marker spheres on the path endpoints and capture their
|
||||
instance indices as `Start/End`.
|
||||
- Trim the solution curve by the exposed `Solve` value and capture a `Path`
|
||||
boolean on the curve domain.
|
||||
- Convert maze walls and solution curves into filleted radius curves, then
|
||||
convert them to mesh with a circular profile.
|
||||
- Join endpoint markers, path/wall geometry, and the base grid.
|
||||
|
||||
Metaphor: the grid is a street map, random edge costs close some roads, shortest
|
||||
path draws the route home, and the captured booleans are colored tape left on
|
||||
the roads so later carving and tube-building know what survived.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\blender-labyrinth-shortest-path.blend' --python '.\tools\verify_labyrinth_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source vertices | Translated vertices | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| small unsolved | 22730 | 22730 | 0.0 | accepted |
|
||||
| small solved | 25484 | 25484 | 0.0 | accepted |
|
||||
|
||||
Edge and polygon counts also match for both cases.
|
||||
|
||||
## Lessons
|
||||
|
||||
- Shortest-path graphs can be translated as topology passes first, visual
|
||||
passes second.
|
||||
- Capture path booleans before subdivision/deletion. Once the graph is carved,
|
||||
the original edge selection is no longer obvious.
|
||||
- Anonymous `Value` nodes are easy for converter output to misassign. The
|
||||
source links showed `0.1 / Size` driving radius and `3.0` driving marker
|
||||
scale; trusting the generated variable names produced the right topology at
|
||||
the wrong size.
|
||||
- Path outputs are fields. Geometry comparison catches the visible contract;
|
||||
scalar/boolean path outputs need a harness only when they become the target.
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# Translation: Blender Raycast Minigame Foothold
|
||||
|
||||
## Source
|
||||
|
||||
- Source file:
|
||||
https://download.blender.org/demo/geometry-nodes/raycast-minigame.blend
|
||||
- Source groups translated in this pass:
|
||||
- `Initial Direction`
|
||||
- `Line to be Casted`
|
||||
- `Cast Rays`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/blender_raycast_minigame.py`
|
||||
|
||||
## Why This Graph
|
||||
|
||||
The full minigame graph is larger, but these helpers isolate the ray pipeline:
|
||||
make a direction, make a sampled line, cast one ray step, write the hit point
|
||||
back into the line, and return the reflected direction plus traveled distance.
|
||||
|
||||
## Helper Maps
|
||||
|
||||
`VG Initial Direction`:
|
||||
|
||||
- Start with `(1, 0, 0)` as forward.
|
||||
- Rotate it by the exposed Euler vector.
|
||||
- Output the resulting direction vector.
|
||||
|
||||
`VG Line To Be Casted`:
|
||||
|
||||
- Create a zero-length curve line.
|
||||
- Move it to `Start Pos`.
|
||||
- Resample it to `Line Size` points.
|
||||
|
||||
`VG Cast Rays`:
|
||||
|
||||
- Select the active hit point by `Hit Index`.
|
||||
- Sample the previous line point.
|
||||
- Raycast from that previous point into the target geometry along the current
|
||||
ray direction.
|
||||
- Move the active point to the hit position scaled by `0.999`.
|
||||
- Reflect the ray direction by the hit normal.
|
||||
- Add hit distance to traveled distance and increment the hit index.
|
||||
|
||||
Metaphor: the line is a breadcrumb trail. Each raycast step reads the previous
|
||||
crumb, throws a beam into the boundary object, places the next crumb just short
|
||||
of impact, then turns the beam for the next bounce.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\blender-raycast-minigame.blend' --python '.\tools\verify_raycast_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source vertices | Translated vertices | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| initial direction harness | 1 | 1 | 0.0 | accepted |
|
||||
| line to be casted | 5 | 5 | 0.0 | accepted |
|
||||
| cast rays | 4 | 4 | 0.0 | accepted |
|
||||
|
||||
Vector outputs are verified by geometry harnesses: direction drives a single
|
||||
vertex position, and curve outputs are converted through curve points to
|
||||
vertices before comparison.
|
||||
|
||||
## Lessons
|
||||
|
||||
- A ray pipeline is a baton chain: direction, previous point, hit, reflected
|
||||
direction, next index.
|
||||
- Object-dependent groups are better authored with explicit object inputs than
|
||||
hard-coded source object names.
|
||||
- Curve outputs may need curve-to-points harnesses before evaluated mesh
|
||||
comparison can see their control points.
|
||||
|
|
@ -1,378 +0,0 @@
|
|||
# IRCSS French Houses Node Mechanism Study
|
||||
|
||||
This is the pass below the atlas. The atlas answers "which groups exist?" This note answers "what moves do the nodes use?" It is generated from the Blender 5.1.1 inspection cache and then kept as procedural doctrine for city-scale translation work.
|
||||
|
||||
## Coverage
|
||||
|
||||
- Geometry node groups studied: 108.
|
||||
- Total inspected nodes: 16889.
|
||||
- Unique node types: 138.
|
||||
- Node frames carrying artist intent: 844.
|
||||
- Nested group nodes: 273; distinct dependency edges: 163.
|
||||
|
||||
## What The Town Kit Is Made Of
|
||||
|
||||
- Coordinate math is the dominant substrate. `Math`, `Vector Math`, `Combine XYZ`, `Separate XYZ`, comparisons, rotations, and map ranges turn edge facts into heights, offsets, roof slopes, beam directions, and random variation.
|
||||
- Topology mutation is the architectural engine. Extrude, delete, flip, merge, scale, mesh-to-curve, and curve-to-mesh nodes repeatedly turn boring rectangles and edges into walls, arches, crenellations, rails, and roofs.
|
||||
- Attribute memory is the load-bearing glue. Captures, stored named attributes, field-on-domain, sample-index, and named attributes preserve edge identity, ordering, side masks, curve facts, and scatter state across domain changes.
|
||||
- Instancing is used as a fabrication step, not just decoration. Stones, crops, plants, roof spikes, banners, windows, trees, pebbles, and facade details are usually made as small bodies and attached to points or sampled ribs.
|
||||
- Repeat, foreach, closure, menu, and gizmo nodes are authoring grammar. They encode growth loops, per-element facade passes, reusable calculations, user-selectable variants, and direct viewport controls.
|
||||
|
||||
## Family Counts
|
||||
|
||||
- `authoring_controls`: 5224
|
||||
- `coordinate_math`: 5107
|
||||
- `position_fields`: 1757
|
||||
- `topology_mutation`: 1064
|
||||
- `attribute_memory`: 928
|
||||
- `instances_scatter`: 529
|
||||
- `control_flow`: 504
|
||||
- `curve_rails`: 478
|
||||
- `material_finish`: 372
|
||||
- `random_inputs`: 271
|
||||
- `rotation_matrix_math`: 240
|
||||
- `input_constants`: 148
|
||||
- `mesh_seeds`: 135
|
||||
- `topology_query`: 125
|
||||
- `color_material`: 7
|
||||
|
||||
Role counts: `composer: prop/resource` 32, `utility: field/topology` 26, `composer: architecture` 21, `organ: architectural part` 15, `composer: terrain/scatter` 10, `organ: terrain/scatter` 4.
|
||||
|
||||
## Frame Intent
|
||||
|
||||
The source file has 844 frame nodes with artist labels across 61 groups. Treat
|
||||
them as field notes, not polished docs. The spelling is rough, but the intent
|
||||
is often exact enough to preserve stage boundaries in a translation.
|
||||
|
||||
Frame-label pressure clusters:
|
||||
|
||||
- Roofs, floors, bases, beams, windows, doors, walls, edges, arcs, and supports
|
||||
dominate the architecture vocabulary.
|
||||
- The largest framed systems are `GenerateHouse` (131 frames), `CreateGate`
|
||||
(71), `Generate Tower` (70), `Generate Square Tower` (68), `DoorTileFromArc`
|
||||
(25), `GenerateChurchBuildingB` (24), `TownWall` (24), `Geometry Nodes`
|
||||
(23), `PCAGetLongAndShortAxis` (22), and `TownWall_Climbing` (21).
|
||||
- Frame labels repeatedly separate calculation from fabrication: calculate
|
||||
widths and directions, save edge IDs or random values, filter top/front/side
|
||||
selections, then extrude, tile, spawn, or finish geometry.
|
||||
- The labels confirm the translation ladder: topology utilities first, then
|
||||
organ stages, then full composers. A thousand-node group is usually a stack
|
||||
of named frame stages, not one indivisible spell.
|
||||
|
||||
## Translation Doctrine From This File
|
||||
|
||||
### Start With Edges, Not Buildings
|
||||
|
||||
Most architecture graphs read an input rectangle, curve, edge strip, or generated topology and then ask edge questions: which side, which corner, which loop, which face belongs to the original extrusion, which edge can host a window. In script, the first useful abstraction is usually a named edge-selection helper, not a complete house.
|
||||
|
||||
Planning sketch, not accepted Geometry Script:
|
||||
|
||||
```text
|
||||
base edges -> named side/corner selections -> extruded wall band -> sampled window sites
|
||||
```
|
||||
|
||||
### Treat Curves As Attachment Rails
|
||||
|
||||
Many visible details are built by converting mesh edges to curves, resampling them, and instancing parts along the result. This is how roof ribs, stair spines, trim, and detail spikes become controllable instead of hand-placed.
|
||||
|
||||
Verified excerpt from `examples/geometry_script/ircss_french_houses.py`:
|
||||
|
||||
```python
|
||||
cone_ribs = delete_geometry(mode="ALL", domain="EDGE", geometry=cone_for_detail_edges, selection=cone_edge_flags.edge_trim)
|
||||
rib_curve = mesh_to_curve(mode="EDGES", mesh=cone_ribs)
|
||||
detail_points = resample_curve(
|
||||
keep_last_segment=True,
|
||||
curve=rib_curve,
|
||||
mode="Length",
|
||||
count=5,
|
||||
length=gothic_detail_density,
|
||||
)
|
||||
```
|
||||
|
||||
### Capture Before You Change Domains
|
||||
|
||||
If a graph instances, realizes, extrudes, deletes, or converts between mesh and curve, values needed later must be captured first. The source does this constantly; skipping it is how a plausible script loses the side or edge it meant to dress.
|
||||
|
||||
Verified excerpt from `examples/geometry_script/ircss_french_houses.py`:
|
||||
|
||||
```python
|
||||
point_positions = _capture_attribute_item("VECTOR", "POINT", geometry, position(), "source position")
|
||||
cone_edge_flags = _capture_attribute_item("BOOLEAN", "EDGE", tapered_cone, cap_or_body_edge, "edge trim")
|
||||
```
|
||||
|
||||
### Use Python For Tables And Orchestration
|
||||
|
||||
The file has repeated facade passes, roof variants, banner/crop families, and many near-clone composers. Python should own those tables and call compact Geometry Script groups. The emitted graph should still expose the resulting controls.
|
||||
|
||||
Planning sketch, not accepted Geometry Script:
|
||||
|
||||
```text
|
||||
Python rows/variants -> repeated compact group calls -> one inspectable composer graph
|
||||
```
|
||||
|
||||
### Translate Toolchain Grammar Deliberately
|
||||
|
||||
Repeat and foreach zones are not inconvenient decorations. They are how this file grows rows, walks geometry elements, and accumulates topology. If Geometry Script cannot emit them cleanly yet, the right response is either toolchain support or an explicit Python-side orchestration model with equivalent evaluated behavior.
|
||||
|
||||
## Mechanism Table For Every Group
|
||||
|
||||
| Group | Role | Main mechanisms | Features | Nodes | Calls | Called by |
|
||||
| --- | --- | --- | --- | ---: | ---: | ---: |
|
||||
| `ArcsFromEdges` | organ: architectural part | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `capture`, `store` | 209 | 1 | 3 |
|
||||
| `Banner` | composer: prop/resource | mesh surgery, attribute memory, instancing/scatter, surface finish, primitive seeds | `gizmo`, `capture`, `store`, `asset`, `material` | 51 | 2 | 0 |
|
||||
| `BannerTypeFour` | composer: prop/resource | mesh surgery, attribute memory, instancing/scatter, surface finish, primitive seeds | `capture`, `store`, `asset`, `material` | 70 | 2 | 0 |
|
||||
| `BannerTypeThree` | composer: prop/resource | mesh surgery, attribute memory, instancing/scatter, surface finish, primitive seeds | `capture`, `store`, `asset`, `material` | 58 | 2 | 0 |
|
||||
| `ChurchA-Front` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `foreach`, `gizmo`, `menu`, `capture`, `store`, `material` | 475 | 8 | 1 |
|
||||
| `ChurchFrontTowers` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `foreach`, `gizmo`, `closure`, `menu`, `capture`, `store`, `material` | 792 | 8 | 1 |
|
||||
| `Color in Range` | utility: field/topology | field utility | none | 7 | 0 | 2 |
|
||||
| `ColumeDecoFromEdges` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `capture`, `store`, `material` | 114 | 0 | 2 |
|
||||
| `Corn From Point` | organ: terrain/scatter | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture`, `store`, `material` | 209 | 0 | 1 |
|
||||
| `CreateDiagonalBeams` | organ: architectural part | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture` | 26 | 0 | 1 |
|
||||
| `CreateGate` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `closure`, `capture`, `store`, `material` | 1081 | 7 | 0 |
|
||||
| `Curve Info` | utility: field/topology | route/rail shaping, attribute memory | none | 19 | 2 | 7 |
|
||||
| `Curve Root` | utility: field/topology | route/rail shaping, edge/face queries, attribute memory | none | 10 | 0 | 2 |
|
||||
| `Curve Tip` | utility: field/topology | route/rail shaping, edge/face queries, attribute memory | none | 10 | 0 | 4 |
|
||||
| `CurvedDoorFromEdges` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `capture`, `store`, `material` | 182 | 0 | 1 |
|
||||
| `DistancingType` | utility: field/topology | branching/zones | `menu` | 3 | 0 | 1 |
|
||||
| `Distribute Corn` | composer: terrain/scatter | attribute memory, instancing/scatter, orientation math | `capture`, `asset` | 18 | 2 | 0 |
|
||||
| `Distribute Sunflower` | composer: terrain/scatter | attribute memory, instancing/scatter, orientation math | `gizmo`, `capture`, `asset` | 23 | 2 | 0 |
|
||||
| `Distribute Wheat` | composer: terrain/scatter | attribute memory, instancing/scatter, orientation math | `capture`, `asset` | 19 | 2 | 0 |
|
||||
| `DomaTower` | composer: architecture | route/rail shaping, mesh surgery, attribute memory, branching/zones, surface finish | `repeat`, `gizmo`, `menu`, `capture`, `store`, `material` | 290 | 4 | 1 |
|
||||
| `DoorTileFromArc` | composer: prop/resource | mesh surgery, edge/face queries, attribute memory, instancing/scatter, branching/zones | `repeat`, `store` | 313 | 4 | 3 |
|
||||
| `EdgesToGates` | composer: architecture | route/rail shaping, attribute memory | `store` | 11 | 1 | 0 |
|
||||
| `Fence Type A` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `foreach`, `capture`, `store`, `material` | 167 | 2 | 1 |
|
||||
| `Flag` | composer: prop/resource | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `repeat`, `gizmo`, `capture`, `store`, `material` | 244 | 0 | 3 |
|
||||
| `Float in Range` | utility: field/topology | field utility | none | 4 | 0 | 2 |
|
||||
| `float to Rotation` | utility: field/topology | orientation math | none | 13 | 0 | 1 |
|
||||
| `Generate Cliff Type 2` | composer: terrain/scatter | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `capture`, `store`, `asset`, `material` | 250 | 2 | 0 |
|
||||
| `Generate Extruded DoorShape` | organ: architectural part | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture`, `store`, `material` | 180 | 1 | 3 |
|
||||
| `Generate Gothic Square Tower` | organ: architectural part | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `foreach`, `capture`, `material` | 238 | 3 | 3 |
|
||||
| `Generate Square Tower` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `capture`, `store`, `asset`, `material` | 805 | 6 | 0 |
|
||||
| `Generate Stones` | composer: terrain/scatter | mesh surgery, instancing/scatter, branching/zones, surface finish, orientation math | `repeat`, `material` | 47 | 0 | 0 |
|
||||
| `Generate Tower` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `capture`, `store`, `asset`, `material` | 707 | 7 | 0 |
|
||||
| `Generate Windows` | organ: architectural part | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `menu`, `capture`, `store`, `asset`, `material` | 188 | 2 | 5 |
|
||||
| `GenerateAlcoveWindows` | composer: architecture | mesh surgery, attribute memory, primitive seeds | `store` | 14 | 1 | 0 |
|
||||
| `GenerateArc` | composer: prop/resource | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture`, `store` | 153 | 0 | 2 |
|
||||
| `GenerateBricks` | composer: prop/resource | mesh surgery, branching/zones, surface finish, primitive seeds | `repeat`, `material` | 26 | 0 | 0 |
|
||||
| `GenerateChurchBuildingB` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `menu`, `capture`, `store`, `material` | 893 | 8 | 1 |
|
||||
| `GenerateCover` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `menu`, `capture`, `store`, `material` | 178 | 1 | 0 |
|
||||
| `GenerateDoorsWithAlcove` | composer: prop/resource | attribute memory | `store` | 10 | 1 | 0 |
|
||||
| `GenerateHouse` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `capture`, `store`, `asset`, `material` | 1023 | 7 | 1 |
|
||||
| `GenerateLightShaftExrudes` | utility: field/topology | mesh surgery, attribute memory, instancing/scatter, branching/zones, surface finish | `capture`, `store`, `material` | 112 | 1 | 2 |
|
||||
| `GenerateMedialAxisRectangleTopology` | composer: prop/resource | mesh surgery, edge/face queries, attribute memory, branching/zones, primitive seeds | `repeat`, `capture`, `store` | 119 | 0 | 3 |
|
||||
| `GenerateRockSlow` | composer: prop/resource | mesh surgery, edge/face queries, attribute memory, instancing/scatter, branching/zones | `repeat`, `gizmo`, `menu`, `store`, `material` | 94 | 2 | 1 |
|
||||
| `GenerateStairCase` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory | `capture` | 53 | 3 | 0 |
|
||||
| `GenerateTerrace` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `menu`, `capture`, `store`, `asset`, `material` | 150 | 3 | 1 |
|
||||
| `GenerateUpperWindowWithAlcove` | organ: architectural part | mesh surgery, attribute memory, instancing/scatter, branching/zones, surface finish | `menu`, `capture`, `store`, `material` | 128 | 2 | 4 |
|
||||
| `GenerateWindowsFromEdges` | organ: architectural part | mesh surgery, branching/zones, surface finish | `foreach`, `material` | 29 | 2 | 2 |
|
||||
| `GeneratorBoulders` | composer: prop/resource | mesh surgery, branching/zones | `repeat` | 16 | 1 | 0 |
|
||||
| `Geometry Nodes` | composer: prop/resource | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `repeat`, `gizmo`, `closure`, `capture`, `store`, `material` | 355 | 3 | 0 |
|
||||
| `Geometry Nodes.001` | composer: prop/resource | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, surface finish | `capture`, `store`, `asset`, `material` | 135 | 1 | 0 |
|
||||
| `Geometry Nodes.002` | composer: prop/resource | mesh surgery, attribute memory, instancing/scatter, branching/zones | `gizmo`, `store` | 142 | 3 | 0 |
|
||||
| `GetBoundaryEdgeTangentAndNomral` | utility: field/topology | edge/face queries, attribute memory, branching/zones | `capture` | 45 | 0 | 1 |
|
||||
| `GetOnlySide Edges after Edge Extrude` | utility: field/topology | attribute memory | none | 7 | 1 | 1 |
|
||||
| `GetPositionBasedSeed` | utility: field/topology | instancing/scatter | `asset` | 15 | 0 | 4 |
|
||||
| `GetSteppeddirectionFrom position` | utility: field/topology | field utility | none | 8 | 0 | 1 |
|
||||
| `Gothic SupportColumn From EDge` | organ: architectural part | mesh surgery, edge/face queries, attribute memory, instancing/scatter, branching/zones | `repeat`, `gizmo`, `menu`, `capture` | 107 | 2 | 6 |
|
||||
| `GothicSupportBeaconFromEdgeParent` | composer: prop/resource | attribute memory, surface finish | `gizmo`, `store`, `material` | 13 | 1 | 0 |
|
||||
| `HangingBanners` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `capture`, `store`, `material` | 108 | 0 | 0 |
|
||||
| `HousesOnCurves` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `foreach`, `capture`, `store` | 166 | 4 | 0 |
|
||||
| `Int in Range` | utility: field/topology | field utility | none | 4 | 0 | 1 |
|
||||
| `Is Edge Manifold [1]` | utility: field/topology | edge/face queries, attribute memory | none | 4 | 0 | 9 |
|
||||
| `Is Edge Manifold [2]` | utility: field/topology | edge/face queries, attribute memory | none | 4 | 0 | 9 |
|
||||
| `Make Dirt Road` | composer: prop/resource | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, surface finish | `capture`, `store`, `asset`, `material` | 102 | 0 | 0 |
|
||||
| `Make Pumpkin From Points` | utility: field/topology | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture`, `store`, `material` | 217 | 1 | 1 |
|
||||
| `Make River` | composer: terrain/scatter | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture`, `store`, `asset`, `material` | 136 | 0 | 0 |
|
||||
| `MakeBush` | composer: prop/resource | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `capture`, `store`, `material` | 94 | 1 | 2 |
|
||||
| `MakeGrass` | composer: prop/resource | mesh surgery, attribute memory, instancing/scatter, branching/zones, surface finish | `repeat`, `store`, `material` | 26 | 0 | 0 |
|
||||
| `MakeSpiral` | composer: prop/resource | route/rail shaping, edge/face queries | none | 20 | 0 | 1 |
|
||||
| `MakeStairs` | composer: prop/resource | route/rail shaping, instancing/scatter, branching/zones, surface finish, primitive seeds | `closure`, `menu`, `asset`, `material` | 25 | 0 | 1 |
|
||||
| `MakeStallCover` | composer: architecture | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `repeat`, `gizmo`, `menu`, `capture`, `store`, `material` | 282 | 0 | 0 |
|
||||
| `MakeTree` | composer: terrain/scatter | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `gizmo`, `capture`, `store`, `material` | 271 | 1 | 0 |
|
||||
| `MatrixSubtract` | utility: field/topology | orientation math | none | 23 | 0 | 1 |
|
||||
| `OctaveBasedSpawnStone` | organ: terrain/scatter | mesh surgery, edge/face queries, instancing/scatter | `asset` | 62 | 1 | 1 |
|
||||
| `PCAGetLongAndShortAxis` | utility: field/topology | attribute memory, instancing/scatter, branching/zones, orientation math | `repeat` | 155 | 1 | 1 |
|
||||
| `Points On Surface` | composer: prop/resource | mesh surgery, instancing/scatter | `asset` | 17 | 0 | 0 |
|
||||
| `PointWindowsFromEdges` | organ: architectural part | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `capture`, `material` | 81 | 2 | 1 |
|
||||
| `PointyGothicCone` | organ: architectural part | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, orientation math | `capture` | 58 | 0 | 3 |
|
||||
| `Pole` | composer: prop/resource | mesh surgery, attribute memory, branching/zones, surface finish, primitive seeds | `repeat`, `gizmo`, `store`, `material` | 89 | 0 | 3 |
|
||||
| `ProjectOnCoordinate` | utility: field/topology | field utility | none | 12 | 0 | 1 |
|
||||
| `Pumpkin` | composer: prop/resource | instancing/scatter, orientation math | `asset` | 15 | 1 | 0 |
|
||||
| `RailsFromEdge` | organ: architectural part | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `gizmo`, `capture`, `material` | 191 | 0 | 4 |
|
||||
| `Random Rotation` | utility: field/topology | orientation math | none | 25 | 0 | 4 |
|
||||
| `Reroute` | utility: field/topology | mesh surgery, edge/face queries, attribute memory, instancing/scatter, branching/zones | `repeat`, `gizmo`, `capture`, `store`, `material` | 571 | 3 | 2 |
|
||||
| `ResampleEdgesAndSpawnWindowedWalld` | organ: architectural part | route/rail shaping, mesh surgery, edge/face queries, attribute memory, branching/zones | `foreach`, `capture` | 56 | 2 | 2 |
|
||||
| `Round Bottom` | composer: prop/resource | mesh surgery, branching/zones, surface finish, primitive seeds | `repeat`, `gizmo`, `closure` | 56 | 0 | 2 |
|
||||
| `Select Original Geometry After Edge Extrude` | utility: field/topology | edge/face queries, attribute memory | none | 11 | 0 | 6 |
|
||||
| `SetAutoSmooth` | utility: field/topology | surface finish | none | 5 | 1 | 6 |
|
||||
| `ShouldAutoSmooth` | utility: field/topology | edge/face queries, attribute memory | `capture` | 18 | 0 | 3 |
|
||||
| `Spawn Normals on faces` | utility: field/topology | attribute memory, instancing/scatter, primitive seeds, orientation math | `capture` | 10 | 0 | 1 |
|
||||
| `Stall` | composer: architecture | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `gizmo`, `capture`, `store`, `material` | 116 | 2 | 0 |
|
||||
| `StonesOnSurface` | composer: terrain/scatter | mesh surgery, edge/face queries, attribute memory, instancing/scatter, branching/zones | `closure`, `menu`, `capture`, `store`, `asset`, `material` | 129 | 2 | 6 |
|
||||
| `Sun Flowers From Points` | utility: field/topology | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture`, `store`, `material` | 229 | 0 | 1 |
|
||||
| `Switch On Roof Type` | organ: architectural part | branching/zones | `menu` | 3 | 0 | 9 |
|
||||
| `Tower Gothic Type 2` | composer: architecture | attribute memory | `gizmo`, `store` | 23 | 1 | 0 |
|
||||
| `TownWall` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `closure`, `capture`, `store`, `material` | 342 | 4 | 0 |
|
||||
| `TownWall_Climbing` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `capture`, `store`, `asset`, `material` | 285 | 3 | 0 |
|
||||
| `Tree On Geo Points` | composer: terrain/scatter | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `capture`, `store`, `material` | 282 | 3 | 0 |
|
||||
| `WallTowerround` | composer: architecture | route/rail shaping, mesh surgery, attribute memory, branching/zones, surface finish | `repeat`, `gizmo`, `capture`, `store`, `material` | 265 | 3 | 0 |
|
||||
| `Waterfall` | composer: terrain/scatter | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `gizmo`, `capture`, `store`, `material` | 114 | 0 | 0 |
|
||||
| `Wheat From Point` | organ: terrain/scatter | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `menu`, `capture`, `store`, `material` | 265 | 0 | 1 |
|
||||
| `Wind On Plants` | organ: terrain/scatter | surface finish | none | 23 | 0 | 3 |
|
||||
| `WindMill` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `gizmo`, `capture`, `store`, `material` | 512 | 7 | 0 |
|
||||
| `WindowBeams` | organ: architectural part | route/rail shaping, mesh surgery, attribute memory, instancing/scatter, branching/zones | `capture`, `store`, `material` | 63 | 1 | 1 |
|
||||
| `WindowFromEdges` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `capture`, `store`, `asset`, `material` | 164 | 1 | 1 |
|
||||
| `WindowInnerPattern` | organ: architectural part | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `repeat`, `capture` | 137 | 0 | 1 |
|
||||
| `WindowWalls` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `foreach`, `gizmo`, `capture`, `store`, `material` | 141 | 4 | 4 |
|
||||
| `Wooden Bridge` | composer: architecture | route/rail shaping, mesh surgery, edge/face queries, attribute memory, instancing/scatter | `foreach`, `gizmo`, `capture`, `store`, `material` | 181 | 1 | 0 |
|
||||
| `WrapGeometryToCurve` | composer: prop/resource | route/rail shaping, instancing/scatter, branching/zones, orientation math | `menu`, `asset` | 43 | 0 | 2 |
|
||||
|
||||
## Complete Node-Type Inventory
|
||||
|
||||
| Count | Node type | Family |
|
||||
| ---: | --- | --- |
|
||||
| 2379 | `NodeReroute` | `authoring_controls` |
|
||||
| 1869 | `ShaderNodeMath` | `coordinate_math` |
|
||||
| 1299 | `ShaderNodeVectorMath` | `coordinate_math` |
|
||||
| 1063 | `NodeGroupInput` | `authoring_controls` |
|
||||
| 844 | `NodeFrame` | `authoring_controls` |
|
||||
| 715 | `ShaderNodeCombineXYZ` | `coordinate_math` |
|
||||
| 667 | `GeometryNodeSetPosition` | `position_fields` |
|
||||
| 503 | `GeometryNodeInputPosition` | `position_fields` |
|
||||
| 461 | `FunctionNodeCompare` | `coordinate_math` |
|
||||
| 431 | `GeometryNodeViewer` | `authoring_controls` |
|
||||
| 369 | `FunctionNodeBooleanMath` | `coordinate_math` |
|
||||
| 316 | `GeometryNodeDeleteGeometry` | `topology_mutation` |
|
||||
| 303 | `GeometryNodeJoinGeometry` | `topology_mutation` |
|
||||
| 273 | `GeometryNodeGroup` | `authoring_controls` |
|
||||
| 271 | `FunctionNodeRandomValue` | `random_inputs` |
|
||||
| 263 | `GeometryNodeStoreNamedAttribute` | `attribute_memory` |
|
||||
| 262 | `GeometryNodeInputIndex` | `position_fields` |
|
||||
| 262 | `ShaderNodeSeparateXYZ` | `coordinate_math` |
|
||||
| 259 | `GeometryNodeCaptureAttribute` | `attribute_memory` |
|
||||
| 257 | `GeometryNodeExtrudeMesh` | `topology_mutation` |
|
||||
| 236 | `GeometryNodeSetMaterial` | `material_finish` |
|
||||
| 154 | `GeometryNodeInstanceOnPoints` | `instances_scatter` |
|
||||
| 135 | `GeometryNodeSampleIndex` | `attribute_memory` |
|
||||
| 134 | `GeometryNodeRealizeInstances` | `instances_scatter` |
|
||||
| 130 | `GeometryNodeFieldOnDomain` | `attribute_memory` |
|
||||
| 122 | `ShaderNodeValue` | `input_constants` |
|
||||
| 118 | `GeometryNodeSwitch` | `control_flow` |
|
||||
| 115 | `GeometryNodeGizmoLinear` | `authoring_controls` |
|
||||
| 114 | `GeometryNodeInputNormal` | `position_fields` |
|
||||
| 108 | `NodeGroupOutput` | `authoring_controls` |
|
||||
| 98 | `GeometryNodeCurveToMesh` | `curve_rails` |
|
||||
| 92 | `GeometryNodeInputNamedAttribute` | `attribute_memory` |
|
||||
| 90 | `NodeSeparateBundle` | `control_flow` |
|
||||
| 85 | `GeometryNodeSetShadeSmooth` | `material_finish` |
|
||||
| 83 | `GeometryNodeResampleCurve` | `curve_rails` |
|
||||
| 80 | `GeometryNodeMeshToCurve` | `curve_rails` |
|
||||
| 78 | `GeometryNodeTransform` | `instances_scatter` |
|
||||
| 72 | `GeometryNodeProximity` | `position_fields` |
|
||||
| 68 | `FunctionNodeAxesToRotation` | `rotation_matrix_math` |
|
||||
| 68 | `GeometryNodeFlipFaces` | `topology_mutation` |
|
||||
| 66 | `ShaderNodeMapRange` | `coordinate_math` |
|
||||
| 65 | `GeometryNodeIndexSwitch` | `control_flow` |
|
||||
| 64 | `GeometryNodeMeshCube` | `mesh_seeds` |
|
||||
| 60 | `GeometryNodeRepeatInput` | `control_flow` |
|
||||
| 60 | `GeometryNodeRepeatOutput` | `control_flow` |
|
||||
| 59 | `GeometryNodeMergeByDistance` | `topology_mutation` |
|
||||
| 55 | `GeometryNodeInputMeshEdgeVertices` | `topology_query` |
|
||||
| 53 | `FunctionNodeRotateVector` | `rotation_matrix_math` |
|
||||
| 50 | `GeometryNodeCurvePrimitiveLine` | `curve_rails` |
|
||||
| 49 | `ShaderNodeMix` | `coordinate_math` |
|
||||
| 47 | `GeometryNodeSelfObject` | `position_fields` |
|
||||
| 47 | `GeometryNodeDistributePointsOnFaces` | `instances_scatter` |
|
||||
| 46 | `GeometryNodeCurvePrimitiveCircle` | `curve_rails` |
|
||||
| 45 | `GeometryNodeObjectInfo` | `instances_scatter` |
|
||||
| 45 | `GeometryNodeInputTangent` | `position_fields` |
|
||||
| 42 | `GeometryNodeSplineParameter` | `curve_rails` |
|
||||
| 39 | `FunctionNodeAlignRotationToVector` | `rotation_matrix_math` |
|
||||
| 35 | `GeometryNodeAttributeDomainSize` | `topology_query` |
|
||||
| 30 | `GeometryNodeMeshToPoints` | `instances_scatter` |
|
||||
| 30 | `GeometryNodeBoundBox` | `position_fields` |
|
||||
| 30 | `GeometryNodeCurveToPoints` | `curve_rails` |
|
||||
| 28 | `ShaderNodeTexNoise` | `material_finish` |
|
||||
| 25 | `NodeCombineBundle` | `control_flow` |
|
||||
| 25 | `GeometryNodeMeshCircle` | `mesh_seeds` |
|
||||
| 24 | `GeometryNodeMeshGrid` | `mesh_seeds` |
|
||||
| 22 | `GeometryNodeMeshLine` | `mesh_seeds` |
|
||||
| 21 | `GeometryNodeMenuSwitch` | `control_flow` |
|
||||
| 21 | `GeometryNodeMaterialSelection` | `material_finish` |
|
||||
| 21 | `GeometryNodeAttributeStatistic` | `attribute_memory` |
|
||||
| 19 | `GeometryNodeRemoveAttribute` | `attribute_memory` |
|
||||
| 18 | `GeometryNodeScaleElements` | `topology_mutation` |
|
||||
| 16 | `FunctionNodeRotateRotation` | `rotation_matrix_math` |
|
||||
| 16 | `GeometryNodeCollectionInfo` | `instances_scatter` |
|
||||
| 15 | `GeometryNodeCurveLength` | `curve_rails` |
|
||||
| 14 | `GeometryNodeForeachGeometryElementInput` | `control_flow` |
|
||||
| 14 | `GeometryNodeForeachGeometryElementOutput` | `control_flow` |
|
||||
| 13 | `NodeEvaluateClosure` | `control_flow` |
|
||||
| 13 | `GeometryNodeInputID` | `position_fields` |
|
||||
| 12 | `NodeClosureInput` | `control_flow` |
|
||||
| 12 | `NodeClosureOutput` | `control_flow` |
|
||||
| 12 | `GeometryNodeMeshCylinder` | `topology_mutation` |
|
||||
| 12 | `ShaderNodeVectorRotate` | `rotation_matrix_math` |
|
||||
| 11 | `GeometryNodeGizmoDial` | `authoring_controls` |
|
||||
| 11 | `GeometryNodeSubdivideMesh` | `topology_mutation` |
|
||||
| 11 | `ShaderNodeClamp` | `coordinate_math` |
|
||||
| 10 | `FunctionNodeInputInt` | `input_constants` |
|
||||
| 10 | `FunctionNodeEulerToRotation` | `rotation_matrix_math` |
|
||||
| 10 | `FunctionNodeAxisAngleToRotation` | `rotation_matrix_math` |
|
||||
| 10 | `GeometryNodeRaycast` | `instances_scatter` |
|
||||
| 9 | `GeometryNodeFillCurve` | `curve_rails` |
|
||||
| 9 | `GeometryNodeFieldAtIndex` | `attribute_memory` |
|
||||
| 8 | `GeometryNodeMeshUVSphere` | `topology_mutation` |
|
||||
| 8 | `FunctionNodeInvertRotation` | `rotation_matrix_math` |
|
||||
| 7 | `GeometryNodeCurveArc` | `curve_rails` |
|
||||
| 7 | `FunctionNodeInputVector` | `input_constants` |
|
||||
| 7 | `GeometryNodeSampleNearest` | `instances_scatter` |
|
||||
| 7 | `GeometryNodeEdgesOfVertex` | `topology_query` |
|
||||
| 7 | `GeometryNodeInputMeshEdgeAngle` | `topology_query` |
|
||||
| 6 | `ShaderNodeFloatCurve` | `coordinate_math` |
|
||||
| 6 | `GeometryNodeInputCollection` | `instances_scatter` |
|
||||
| 6 | `FunctionNodeSeparateMatrix` | `rotation_matrix_math` |
|
||||
| 5 | `FunctionNodeCombineTransform` | `rotation_matrix_math` |
|
||||
| 5 | `GeometryNodePointsOfCurve` | `topology_query` |
|
||||
| 5 | `GeometryNodeCornersOfEdge` | `topology_query` |
|
||||
| 5 | `GeometryNodeSampleCurve` | `curve_rails` |
|
||||
| 4 | `FunctionNodeSeparateColor` | `color_material` |
|
||||
| 4 | `GeometryNodeReverseCurve` | `curve_rails` |
|
||||
| 4 | `GeometryNodeCurveEndpointSelection` | `curve_rails` |
|
||||
| 4 | `FunctionNodeIntegerMath` | `input_constants` |
|
||||
| 4 | `FunctionNodeCombineMatrix` | `rotation_matrix_math` |
|
||||
| 3 | `GeometryNodeMeshIcoSphere` | `topology_mutation` |
|
||||
| 3 | `GeometryNodeSubdivisionSurface` | `topology_mutation` |
|
||||
| 3 | `FunctionNodeInputRotation` | `input_constants` |
|
||||
| 3 | `GeometryNodeVertexOfCorner` | `topology_query` |
|
||||
| 3 | `GeometryNodeFaceOfCorner` | `topology_query` |
|
||||
| 3 | `FunctionNodeTransformDirection` | `rotation_matrix_math` |
|
||||
| 2 | `FunctionNodeCombineColor` | `color_material` |
|
||||
| 2 | `GeometryNodeSampleNearestSurface` | `instances_scatter` |
|
||||
| 2 | `GeometryNodeConvexHull` | `topology_mutation` |
|
||||
| 2 | `GeometryNodeMeshBoolean` | `topology_mutation` |
|
||||
| 2 | `FunctionNodeInvertMatrix` | `rotation_matrix_math` |
|
||||
| 2 | `GeometryNodeSetMeshNormal` | `topology_mutation` |
|
||||
| 2 | `GeometryNodeOffsetCornerInFace` | `topology_query` |
|
||||
| 2 | `GeometryNodeInputMeshIsland` | `topology_query` |
|
||||
| 2 | `GeometryNodeUVUnwrap` | `material_finish` |
|
||||
| 2 | `GeometryNodeInputRadius` | `position_fields` |
|
||||
| 2 | `GeometryNodeSetCurveRadius` | `curve_rails` |
|
||||
| 2 | `FunctionNodeInputBool` | `input_constants` |
|
||||
| 2 | `GeometryNodeInputSceneTime` | `position_fields` |
|
||||
| 1 | `GeometryNodeInputMeshVertexNeighbors` | `topology_query` |
|
||||
| 1 | `GeometryNodeSplineLength` | `curve_rails` |
|
||||
| 1 | `ShaderNodeTexVoronoi` | `color_material` |
|
||||
| 1 | `GeometryNodeSetCurveTilt` | `curve_rails` |
|
||||
| 1 | `GeometryNodeSetInstanceTransform` | `rotation_matrix_math` |
|
||||
| 1 | `FunctionNodeTransformPoint` | `rotation_matrix_math` |
|
||||
| 1 | `GeometryNodeInputSplineCyclic` | `curve_rails` |
|
||||
| 1 | `FunctionNodeMatrixMultiply` | `rotation_matrix_math` |
|
||||
| 1 | `FunctionNodeQuaternionToRotation` | `rotation_matrix_math` |
|
||||
|
||||
## Reading Order Now That Every Node Is Accounted For
|
||||
|
||||
1. Utility leaves: `Is Edge Manifold`, `Select Original Geometry After Edge Extrude`, `ShouldAutoSmooth`, `Curve Root`, `Curve Tip`, `Random Rotation`, `GetPositionBasedSeed`, and roof/menu routers.
|
||||
2. Organ chains: `GenerateArc`, `DoorTileFromArc`, `WindowInnerPattern`, `Generate Windows`, `GenerateWindowsFromEdges`, `RailsFromEdge`, `Gothic SupportColumn From EDge`, `GenerateTerrace`, `MakeStairs`, `Fence Type A`, `StonesOnSurface`.
|
||||
3. Composer roots: house, tower, church, gate, wall, bridge, terrain, vegetation, river, and set-dressing groups only after their utilities and organs have behavioral harnesses.
|
||||
4. Toolchain targets: repeat/foreach/gizmo/closure graphs when the goal is to teach Geometry Script the missing grammar instead of dodging it.
|
||||
|
|
@ -1,371 +0,0 @@
|
|||
# IRCSS French Houses System Map
|
||||
|
||||
## Scope
|
||||
|
||||
Source file:
|
||||
`experiments/source-blends/ircss-french-houses.blend`
|
||||
|
||||
Inspection cache:
|
||||
`experiments/inspection/ircss-french-houses-node-groups.json`
|
||||
|
||||
All-node atlas:
|
||||
`docs/research/translations/ircss-french-houses-node-atlas.md`
|
||||
|
||||
Node mechanism study:
|
||||
`docs/research/translations/ircss-french-houses-node-mechanisms.md`
|
||||
|
||||
Generated with Blender 5.1.1 after extending
|
||||
`tools/inspect_blend_node_groups.py` to record nested `GeometryNodeGroup`
|
||||
`node_tree` references.
|
||||
|
||||
## High-Level Shape
|
||||
|
||||
This `.blend` is a procedural town-kit showcase, not one continuous generated
|
||||
city graph. The file contains:
|
||||
|
||||
- 108 geometry node groups, 107 unique names.
|
||||
- One duplicate group name: `Is Edge Manifold`.
|
||||
- 63 distinct modifier root groups used by scene objects.
|
||||
- 62 groups that call at least one nested group.
|
||||
- 163 distinct group dependency edges.
|
||||
|
||||
The useful mental split:
|
||||
|
||||
- **Composers** create recognizable buildings and scene pieces: houses, towers,
|
||||
church fronts, gates, walls, windmills, bridges, stalls, terrain, and roads.
|
||||
- **Organs** make reusable architectural parts: arcs, windows, doors, roof caps,
|
||||
stairs, beams, rails, columns, banners, terraces, and covers.
|
||||
- **Utilities** carry field logic, edge tests, ranges, auto-smoothing, wrapping,
|
||||
curve facts, random seeding, and geometry selection.
|
||||
|
||||
Metaphor in use: the town is not poured from one mold. It is an assembly yard.
|
||||
Composers bolt together organs; organs lean on utilities; utilities are the
|
||||
measuring jigs and clamps.
|
||||
|
||||
## Final Scene Assembly
|
||||
|
||||
The authored scene is a showcase board. Visible collections place generated
|
||||
systems near each other: houses and roads, church/cathedral parts, towers,
|
||||
walls/gate/bridge, farms, terrain/cliffs/river, and set-dressing examples.
|
||||
|
||||
A low-cost Workbench render was generated at:
|
||||
|
||||
```text
|
||||
experiments/inspection/ircss-french-houses-workbench.png
|
||||
```
|
||||
|
||||
The render shows the final composition as a display yard: cathedral and church
|
||||
complex on the right, houses and towers across the middle, gate/walls/bridge
|
||||
and standalone facade examples to the left, terrain/cliffs/trees/river and farm
|
||||
pieces around the front and sides.
|
||||
|
||||
Evaluated visible modifier roots by collection:
|
||||
|
||||
| Collection | Visible modifier roots | Evaluated vertices | Evaluated polygons | Main role |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| `Houses` | 6 | 565006 | 488310 | repeated houses plus house-on-curve system |
|
||||
| `Church` | 6 | 405499 | 288479 | cathedral, church body/front, dome, towers |
|
||||
| `WallsAndBridgets` | 5 | 86803 | 101470 | gate, walls, wall tower, bridge |
|
||||
| `Farms` | 7 | 49005 | 37123 | windmill, crops, bridge, fence |
|
||||
| `SetDressing` | 20 | 48061 | 68502 | stairs, terraces, covers, windows, doors, stalls |
|
||||
| `landscape` | 5 | 45078 | 24745 | tree, bush, scatter tree, rocks, waterfall |
|
||||
| `Towers` | 3 | 33055 | 35141 | standalone round/square towers |
|
||||
| `Cliffs` | 2 | 24685 | 18628 | terrain/cliff systems |
|
||||
| `Resources` | 11 | 24567 | 17353 | reusable examples and source handles |
|
||||
| `RiverBank` | 2 | 9772 | 6982 | riverbank cliffs |
|
||||
| `Paths` | 1 | 1258 | 940 | dirt road |
|
||||
| `Collection` | 1 | 530 | 420 | river |
|
||||
|
||||
Largest evaluated roots:
|
||||
|
||||
| Object | Root group | Collection | Vertices | Polygons |
|
||||
| --- | --- | --- | ---: | ---: |
|
||||
| `HousesOnCurve` | `HousesOnCurves` | `Houses` | 385472 | 360845 |
|
||||
| `Catehdral` | `Geometry Nodes.002` | `Church` | 255516 | 183189 |
|
||||
| `HouseFour` | `GenerateHouse` | `Houses` | 61312 | 44326 |
|
||||
| `Tower-Doma` | `DomaTower` | `Church` | 59960 | 41607 |
|
||||
| `HouseThree` | `GenerateHouse` | `Houses` | 52567 | 38223 |
|
||||
| `Gate` | `CreateGate` | `WallsAndBridgets` | 43642 | 40323 |
|
||||
| `BuildingB-sides` | `GenerateChurchBuildingB` | `Church` | 36136 | 26336 |
|
||||
| `Tree` | `MakeTree` | `landscape` | 32706 | 15081 |
|
||||
| `BuildA-Front` | `ChurchA-Front` | `Church` | 31127 | 22010 |
|
||||
| `HouseOne` | `GenerateHouse` | `Houses` | 27919 | 20003 |
|
||||
|
||||
Interpretation: the final result is built by scene-object composition. Each
|
||||
visible object owns a Geometry Nodes modifier whose root group generates that
|
||||
object's piece. The final town image is the union of those generated objects,
|
||||
not the output of a single master town node group.
|
||||
|
||||
## Modifier Entrypoints
|
||||
|
||||
Most reused scene roots:
|
||||
|
||||
| Modifier root | Scene users | Dependency closure |
|
||||
| --- | ---: | ---: |
|
||||
| `GenerateHouse` | 5 | 9 |
|
||||
| `Generate Cliff Type 2` | 3 | 2 |
|
||||
| `MakeStallCover` | 3 | 0 |
|
||||
| `GenerateDoorsWithAlcove` | 2 | 2 |
|
||||
| `Tree On Geo Points` | 2 | 5 |
|
||||
| `GenerateCover` | 2 | 1 |
|
||||
| `GenerateStairCase` | 2 | 9 |
|
||||
| `Generate Tower` | 2 | 10 |
|
||||
| `GenerateAlcoveWindows` | 2 | 6 |
|
||||
|
||||
Large single-root systems:
|
||||
|
||||
| Modifier root | Dependency closure | Role |
|
||||
| --- | ---: | --- |
|
||||
| `Geometry Nodes.002` | 28 | cathedral root |
|
||||
| `GenerateChurchBuildingB` | 22 | church side/body composer |
|
||||
| `ChurchA-Front` | 20 | church front facade composer |
|
||||
| `CreateGate` | 18 | gate composer |
|
||||
| `HousesOnCurves` | 13 | curve-distributed houses |
|
||||
| `DomaTower` | 12 | domed tower composer |
|
||||
| `ChurchFrontTowers` | 11 | church tower front system |
|
||||
| `Generate Tower` | 10 | tower composer |
|
||||
| `TownWall` | 10 | wall composer |
|
||||
| `WindMill` | 10 | windmill composer |
|
||||
|
||||
## Major Composer Wiring
|
||||
|
||||
`GenerateHouse` depends on:
|
||||
|
||||
- `GetPositionBasedSeed`
|
||||
- `Switch On Roof Type`
|
||||
- `Generate Windows`
|
||||
- `Curve Info`
|
||||
- `CreateDiagonalBeams`
|
||||
- `WindowBeams`
|
||||
- `Is Edge Manifold`
|
||||
|
||||
Interpretation: houses are facade assemblers. They choose roof type, place
|
||||
windows, derive local curve facts, add diagonal beams, and use edge-manifold
|
||||
tests as selection guards.
|
||||
|
||||
`GenerateStairCase` depends on:
|
||||
|
||||
- `MakeSpiral`
|
||||
- `MakeStairs`
|
||||
- `GenerateTerrace`
|
||||
|
||||
Interpretation: this is the cleanest staircase chain. `MakeSpiral` provides the
|
||||
spine, `MakeStairs` hangs tread geometry on the spine, and `GenerateTerrace`
|
||||
builds the surrounding rail/ground system.
|
||||
|
||||
`DomaTower` depends on:
|
||||
|
||||
- `PointWindowsFromEdges`
|
||||
- `PointyGothicCone`
|
||||
- `WindowWalls`
|
||||
- `Generate Gothic Square Tower`
|
||||
|
||||
Interpretation: domed towers are edge/window systems with gothic cone ornament.
|
||||
The accepted `PointyGothicCone` translation is not isolated trivia; it is a
|
||||
reused ornament organ in this tower family.
|
||||
|
||||
`GenerateChurchBuildingB` depends on:
|
||||
|
||||
- `Switch On Roof Type`
|
||||
- `DistancingType`
|
||||
- `ResampleEdgesAndSpawnWindowedWalld`
|
||||
- `Is Edge Manifold`
|
||||
- `Gothic SupportColumn From EDge`
|
||||
- `GenerateWindowsFromEdges`
|
||||
- `ChurchA-Front`
|
||||
- `WindowWalls`
|
||||
|
||||
Interpretation: church bodies are facade/edge pipelines. They resample edges,
|
||||
spawn windowed walls, add gothic supports, and reuse the church front.
|
||||
|
||||
`CreateGate` depends on:
|
||||
|
||||
- `DoorTileFromArc`
|
||||
- `Is Edge Manifold`
|
||||
- `Spawn Normals on faces`
|
||||
- `GenerateUpperWindowWithAlcove`
|
||||
- `GetSteppeddirectionFrom position`
|
||||
- `StonesOnSurface`
|
||||
- `GenerateMedialAxisRectangleTopology`
|
||||
|
||||
Interpretation: gates are topology-first structures. A medial-axis rectangle
|
||||
topology lays out the plan; arcs and upper windows turn that skeleton into
|
||||
visible architecture; stones finish surfaces.
|
||||
|
||||
## Dependency Hubs
|
||||
|
||||
Most reused groups:
|
||||
|
||||
| Group | Callers | Role |
|
||||
| --- | ---: | --- |
|
||||
| `Is Edge Manifold` | 9 | edge-validity guard |
|
||||
| `Switch On Roof Type` | 9 | roof/menu router |
|
||||
| `Curve Info` | 7 | curve endpoint/root/tip facts |
|
||||
| `Gothic SupportColumn From EDge` | 6 | gothic column organ |
|
||||
| `Select Original Geometry After Edge Extrude` | 6 | post-extrude selection utility |
|
||||
| `SetAutoSmooth` | 6 | surface finishing utility |
|
||||
| `StonesOnSurface` | 6 | stone scatter/finish organ |
|
||||
| `Generate Windows` | 5 | reusable window generator |
|
||||
| `Curve Tip` | 4 | curve endpoint helper |
|
||||
| `GenerateUpperWindowWithAlcove` | 4 | upper window organ |
|
||||
| `GetPositionBasedSeed` | 4 | deterministic spatial seed helper |
|
||||
| `RailsFromEdge` | 4 | rail organ |
|
||||
| `Random Rotation` | 4 | randomized orientation helper |
|
||||
| `WindowWalls` | 4 | window-wall organ |
|
||||
|
||||
These hubs define the file's real language. Translating one thousand-node
|
||||
composer before these hubs are mapped would be pure ceremony.
|
||||
|
||||
## Feature Pressure
|
||||
|
||||
Node-feature counts across groups:
|
||||
|
||||
| Feature | Groups | Translation meaning |
|
||||
| --- | ---: | --- |
|
||||
| repeat zones | 35 | major blocker for accumulators and iterative topology |
|
||||
| geometry-node gizmos | 31 | authoring/UI blocker, often mixed into large systems |
|
||||
| foreach zones | 9 | blocker for element-wise geometry workflows |
|
||||
| closures | 7 | Blender 5 closure/menu workflows need careful converter handling |
|
||||
| menu switches | 17 | control-surface and socket-default pressure |
|
||||
| capture attributes | 66 | attribute survival is central |
|
||||
| store named attributes | 61 | named field/attribute contracts are central |
|
||||
|
||||
Captures and stored attributes are not blockers; they are the bloodstream of the
|
||||
file. Repeat, foreach, gizmo, and closure nodes are the hard toolchain pressure.
|
||||
|
||||
## Materials And Assets
|
||||
|
||||
The file has:
|
||||
|
||||
- 502 objects.
|
||||
- 56 materials.
|
||||
- 23 collections.
|
||||
- 16 `Collection Info` nodes.
|
||||
- 45 `Object Info` nodes.
|
||||
- 14 material interface sockets.
|
||||
- 81 color interface sockets.
|
||||
- 257 material assignment or material-selection nodes.
|
||||
|
||||
Material usage is concentrated around a small palette:
|
||||
|
||||
| Material | Node references | Role |
|
||||
| --- | ---: | --- |
|
||||
| `GroundFloorWalls` | 86 | dominant wall/base material |
|
||||
| `Roof` | 42 | roof surfaces |
|
||||
| `Beam` | 30 | timber and rail pieces |
|
||||
| `UpperFloorsPaint` | 13 | painted alcove/window systems |
|
||||
| `Greenery` | 9 | vegetation |
|
||||
| `RoundWindwos` | 7 | round/gothic windows |
|
||||
| `Copper` | 6 | domes and metal roof accents |
|
||||
| `FakeRoof` | 6 | alternate roof material |
|
||||
| `WindowGlass` | 4 | glass |
|
||||
| `Rock` | 4 | stones/terrain |
|
||||
|
||||
Important material sockets:
|
||||
|
||||
- `DomaTower.DomeMaterial -> Copper`
|
||||
- `Geometry Nodes.002.DomeMaterial -> FakeRoof`
|
||||
- `GenerateAlcoveWindows.AlcoveMaterial -> UpperFloorsPaint`
|
||||
- `GenerateDoorsWithAlcove.AlcoveMaterial -> UpperFloorsPaint`
|
||||
- `GenerateUpperWindowWithAlcove.AlcoveMaterial -> UpperFloorsPaint`
|
||||
- `Wooden Bridge.Material -> Beam`
|
||||
- `Fence Type A.Material -> Beam`
|
||||
- `Generate Cliff Type 2.Walls Material -> Cliff2Walls`
|
||||
- `Generate Cliff Type 2.Top Ground Materials -> Cliff2Top`
|
||||
|
||||
Collection dependencies:
|
||||
|
||||
| Collection | Used by | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `Bricks` | `MakeStairs`, `StonesOnSurface` | instanced brick/stone assets |
|
||||
| `BouldersSet` | `Geometry Nodes.001` | landscape scatter assets |
|
||||
| `Paths` | `Geometry Nodes.001` | landscape/path interaction |
|
||||
| `Plants` | `Generate Cliff Type 2` | cliff vegetation scatter |
|
||||
| `RiverBank` | `Make River` | riverbank geometry |
|
||||
|
||||
Most `Object Info` nodes have no default object set. The one explicit object
|
||||
reference observed is `DoorTileFromArc`, used by an object-info node as an
|
||||
asset handle.
|
||||
|
||||
Interpretation: material and collection state is part of the procedural
|
||||
contract. A translation that matches vertices while dropping materials,
|
||||
collections, or color sockets may preserve shape but not the authored city kit.
|
||||
|
||||
## Accepted Footholds
|
||||
|
||||
`MakeSpiral -> VG Make Spiral`
|
||||
|
||||
- Role: architectural spine.
|
||||
- Verified with 200 ordered curve points at max delta `0.0`.
|
||||
- Used by `GenerateStairCase`.
|
||||
|
||||
`PointyGothicCone -> VG Pointy Gothic Cone`
|
||||
|
||||
- Role: roof/gothic ornament grown from an input point host.
|
||||
- Verified with plain and detailed cases at sorted max delta `0.0`.
|
||||
- Used by `DomaTower`, `Gothic SupportColumn From EDge`, and
|
||||
`PointWindowsFromEdges`.
|
||||
|
||||
## Current Toolchain State
|
||||
|
||||
The Geometry Script fork now emits compiling `nodes_to_script` drafts for these
|
||||
IRCSS targets:
|
||||
|
||||
- `MakeSpiral`
|
||||
- `PointyGothicCone`
|
||||
- `GenerateArc`
|
||||
- `MakeStairs`
|
||||
- `GenerateStairCase`
|
||||
- `GenerateBricks`
|
||||
- `GenerateMedialAxisRectangleTopology`
|
||||
- `GenerateWindowsFromEdges`
|
||||
- `Pole`
|
||||
- `Round Bottom`
|
||||
- `WindowBeams`
|
||||
|
||||
This only proves the converter can produce syntactically valid drafts. It does
|
||||
not prove runtime graph construction or behavioral equivalence. The drafts are
|
||||
source probes, not house style.
|
||||
|
||||
## Translation Strategy
|
||||
|
||||
The next useful city-organ translations should come from dependency hubs or
|
||||
clean chains, not top-level composers.
|
||||
|
||||
Best next candidates:
|
||||
|
||||
| Candidate | Why it matters | Risk |
|
||||
| --- | --- | --- |
|
||||
| `GenerateArc` | core arch primitive for gates, doors, and gothic systems | large but non-zone |
|
||||
| `MakeStairs` | direct child of `GenerateStairCase`; now has compiling draft | closures and collection asset dependency |
|
||||
| `GenerateStairCase` | clean chain proving spine -> treads -> terrace | depends on `MakeStairs` and `GenerateTerrace` |
|
||||
| `Switch On Roof Type` | tiny, heavily reused roof router | menu contract must be preserved |
|
||||
| `Is Edge Manifold` | tiny, heavily reused guard | duplicate source names need disambiguation |
|
||||
| `GenerateUpperWindowWithAlcove` | reused by towers, gates, alcove windows | 128 nodes, nested dependencies |
|
||||
| `Generate Windows` | reused facade organ | 188 nodes but no zone blocker |
|
||||
|
||||
Avoid as first next targets:
|
||||
|
||||
- `CreateGate`, `GenerateHouse`, `GenerateChurchBuildingB`, `Generate Tower`,
|
||||
`Generate Square Tower`, `ChurchFrontTowers`, `WindMill`: too much composer
|
||||
surface before their organs are mapped.
|
||||
- Repeat/foreach/gizmo-heavy systems unless the explicit goal is toolchain
|
||||
support.
|
||||
- Empty-output harnesses such as the rejected `WindowBeams` case.
|
||||
|
||||
## Remaining Unknowns
|
||||
|
||||
- Behavioral equivalence for most groups. The atlas accounts for every graph
|
||||
structurally; only accepted translations have evaluated-geometry proof.
|
||||
- Exact visual appearance of every modifier root under render lighting.
|
||||
- Runtime behavior of all converter drafts. Compile success is only the first
|
||||
gate.
|
||||
- Repeat-zone state semantics for topology accumulators.
|
||||
- Foreach-zone semantics for edge/window/wall systems.
|
||||
- Geometry-node gizmo behavior as editable authoring controls.
|
||||
- How unset object/collection inputs are meant to be supplied by modifier users
|
||||
in every root object.
|
||||
|
||||
This is enough structural understanding to choose translation targets without
|
||||
pretending the town has been regenerated from script. The next proof must be
|
||||
behavioral: pick a target, script it, and compare evaluated geometry.
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
# Translation: IRCSS French Houses Footholds
|
||||
|
||||
## Source
|
||||
|
||||
- Repository: https://github.com/IRCSS/Blender-Geometry-Node-French-Houses
|
||||
- Source file:
|
||||
`GeometryNodesFrenchHous.blend`
|
||||
- Source groups translated so far:
|
||||
- `MakeSpiral`
|
||||
- `PointyGothicCone`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/ircss_french_houses.py`
|
||||
- File-level system map:
|
||||
`docs/research/translations/ircss-french-houses-system-map.md`
|
||||
|
||||
## Why This File
|
||||
|
||||
This is the first genuinely city-scale corpus target. The inspected `.blend`
|
||||
contains 108 geometry node groups, including thousand-node architectural
|
||||
systems:
|
||||
|
||||
- `CreateGate`: 1081 nodes / 1234 links
|
||||
- `GenerateHouse`: 1023 nodes / 1075 links
|
||||
- `GenerateChurchBuildingB`: 893 nodes / 1124 links
|
||||
- `Generate Square Tower`: 805 nodes / 877 links
|
||||
- `ChurchFrontTowers`: 792 nodes / 990 links
|
||||
- `Generate Tower`: 707 nodes / 754 links
|
||||
|
||||
The file is a town-building kit: houses, gates, towers, walls, bridges, church
|
||||
parts, stalls, cliffs, roads, rivers, vegetation, banners, windows, doors, and
|
||||
roof systems. This is the right quarry for procedural-city doctrine, but the
|
||||
first accepted bite has to be small enough to verify.
|
||||
|
||||
## Accepted Graph: MakeSpiral
|
||||
|
||||
Source group: `MakeSpiral`
|
||||
|
||||
Geometry Script group: `VG Make Spiral`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Create a vertical curve line from `(0, 0, 0)` to `(0, 0, 1)`.
|
||||
- Resample it to 200 points.
|
||||
- Use `Index / Point Count` as a normalized parameter `t`.
|
||||
- Compute angle as `t * Frequency`.
|
||||
- Compute radius as `Radius + t * HeightRadiusGain`.
|
||||
- Place each point at:
|
||||
- `x = sin(angle) * radius`
|
||||
- `y = cos(angle) * radius`
|
||||
- `z = t * Height`
|
||||
|
||||
Metaphor: the spiral is a screw-thread spine. Stairs, rails, walls, and
|
||||
decorative ribs can hang from it later, but the first job is to prove the spine
|
||||
lands in the same places as the source.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\ircss-french-houses.blend' --python '.\tools\verify_ircss_french_houses_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source vertices | Translated vertices | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| MakeSpiral curve points | 200 | 200 | 0.0 | accepted |
|
||||
| PointyGothicCone plain | 36 | 36 | 0.0 | accepted |
|
||||
| PointyGothicCone with details | 138 | 138 | 0.0 | accepted |
|
||||
|
||||
## Accepted Graph: PointyGothicCone
|
||||
|
||||
Source group: `PointyGothicCone`
|
||||
|
||||
Geometry Script group: `VG Pointy Gothic Cone`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Capture each input point position before instancing starts.
|
||||
- Build a tapered six-sided cone seed from a cylinder.
|
||||
- Capture an edge-domain trim mask that marks cone cap/body edges.
|
||||
- Instance one scaled cone at each input point.
|
||||
- When `SpawnDetails` is enabled, delete the marked cone edges, convert the
|
||||
remaining ribs to curves, resample them into detail stations, and instance
|
||||
small tapered five-sided spikes along those ribs.
|
||||
- Orient each detail spike toward the horizontal direction from the source
|
||||
point to the current rib point.
|
||||
|
||||
Metaphor: the input points are roof pegs. The main cone is the roof cap hung on
|
||||
each peg; the optional details grow from the cap ribs after the cap is already
|
||||
real enough to have edges. Build the big readable silhouette first, then let
|
||||
ornament use that structure.
|
||||
|
||||
The spiral case converts the output curve to evaluated points and compares
|
||||
ordered vertex positions. Cone cases feed identical synthetic point meshes into
|
||||
the source and translation, then compare sorted evaluated vertices.
|
||||
|
||||
## Blockers Exposed
|
||||
|
||||
- `GenerateMedialAxisRectangleTopology` is a strong next target, but it uses a
|
||||
repeat-zone feedback loop and exposes the repeat `Top` boolean as a group
|
||||
output. The current Geometry Script path does not provide a clean linkable
|
||||
socket for that output.
|
||||
- `GenerateBricks` also uses repeat-zone geometry accumulation. Blender's
|
||||
source graph can leave the repeat geometry seed implicit; the current DSL
|
||||
path exposed the previous-geometry state as `None`.
|
||||
- `GenerateWindowsFromEdges` uses `For Each Geometry Element` zones.
|
||||
- `Pole` uses repeat zones and geometry-node gizmos.
|
||||
- `WindowBeams` depends on specific upstream edge/curve inputs. A synthetic
|
||||
harness could verify its secondary base-point output, but the main beam mesh
|
||||
stayed empty for both source and translation. That was rejected as an
|
||||
acceptance target.
|
||||
|
||||
## Toolchain Fixes From This Pass
|
||||
|
||||
- `GenerateArc` and `MakeStairs` originally exposed a `nodes_to_script`
|
||||
converter bug: duplicated keyword arguments for `primary_axis`.
|
||||
- The Geometry Script fork now drops duplicate property keywords when a node
|
||||
exposes the same control as a linkable input socket.
|
||||
- Expanded IRCSS converter smoke now compiles drafts for `GenerateArc`,
|
||||
`MakeStairs`, `GenerateStairCase`, `GenerateBricks`,
|
||||
`GenerateMedialAxisRectangleTopology`, `GenerateWindowsFromEdges`, `Pole`,
|
||||
`Round Bottom`, and `WindowBeams`.
|
||||
- These are still converter drafts, not accepted translations. Repeat zones,
|
||||
foreach zones, closures, gizmos, dependency groups, and behavioral harnesses
|
||||
still decide what can be trusted.
|
||||
|
||||
## Lessons
|
||||
|
||||
- City-scale graphs are hierarchies of organs. Start with reusable spines,
|
||||
tiles, rails, façade parts, and topology generators before translating a
|
||||
thousand-node composer.
|
||||
- Ornament often needs a structural host. `PointyGothicCone` first creates the
|
||||
readable cap silhouette, then derives detail placement from the cap's own
|
||||
realized ribs.
|
||||
- Captures are not optional bookkeeping when instancing starts. The original
|
||||
point position and cone edge mask both have to survive later domain changes.
|
||||
- Converter drafts are still useful for triage: they quickly separate clean
|
||||
helpers, repeat-zone blockers, foreach-zone blockers, gizmo-specific groups,
|
||||
and false symbol names.
|
||||
- Verification harnesses must reject empty success. If both source and
|
||||
translation output zero visible vertices for the main contract, the harness
|
||||
is not proving the graph.
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# Translation: quellenform Curve To Mesh UV Foothold
|
||||
|
||||
## Source
|
||||
|
||||
- Repository: https://github.com/quellenform/blender-CurveToMeshUV
|
||||
- Source file:
|
||||
`Curve to Mesh UV v1.7.02.230213-FREE - Blender 3.4+.blend`
|
||||
- Source group translated in this pass:
|
||||
- `Auto Smooth`
|
||||
- `Curve to Mesh UV`
|
||||
- `_258246`
|
||||
- `_Title`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/quellenform_curve_to_mesh_uv.py`
|
||||
|
||||
## Why This Graph
|
||||
|
||||
The main `Curve to Mesh UV` group is a 126-node utility network with curve
|
||||
resampling, spline parameters, UV fields, capture attributes, endpoint masks,
|
||||
domain-size accounting, and named attribute outputs. That is useful quarry, but
|
||||
not a single-pass snack.
|
||||
|
||||
`Auto Smooth` is the small utility attached to that system. The main
|
||||
`Curve to Mesh UV` group teaches how a utility can output geometry plus fields
|
||||
that only make sense when sampled on that geometry.
|
||||
|
||||
## Auto Smooth Map
|
||||
|
||||
Source group: `Auto Smooth`
|
||||
|
||||
Geometry Script group: `VG Auto Smooth`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Read the unsigned mesh edge angle.
|
||||
- Compare it against the exposed `Angle`.
|
||||
- AND that threshold result with the existing face smooth state.
|
||||
- On the edge domain, set shade smooth for already-smooth edges using that
|
||||
thresholded condition.
|
||||
- On the face domain, set shade smooth true for the full mesh.
|
||||
|
||||
Metaphor: the group is a polishing gate. It does not build the object. It
|
||||
decides where the surface may keep a smooth handshake across edges and then
|
||||
hands the mesh back to the larger utility.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background 'E:\Projects\VibeGeometry\experiments\source-repos\blender-CurveToMeshUV\Curve to Mesh UV v1.7.02.230213-FREE - Blender 3.4+.blend' --python '.\tools\verify_curve_to_mesh_uv_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source | Translated | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Auto Smooth cube vertices | 8 | 8 | 0.0 | accepted |
|
||||
| Auto Smooth smooth faces | 6 | 6 | exact flags | accepted |
|
||||
| Curve To Mesh UV mesh | 192 | 192 | 0.0 | accepted |
|
||||
| Curve To Mesh UV UV-field harness | 192 | 192 | 0.0 | accepted |
|
||||
| Curve To Mesh UV caps-mask harness | 192 | 192 | 0.0 | accepted |
|
||||
| Demo geometry | 512 | 512 | 0.0 | accepted |
|
||||
| Title geometry | 12 | 12 | 0.0 | accepted |
|
||||
|
||||
## Curve To Mesh UV Map
|
||||
|
||||
Source group: `Curve to Mesh UV`
|
||||
|
||||
Geometry Script group: `VG Curve To Mesh UV`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Resample the source curve and profile curve.
|
||||
- Capture point triplets and curve indices before converting the curves to a
|
||||
mesh.
|
||||
- Convert the curve/profile pair with `Curve to Mesh`.
|
||||
- Use corner-domain accumulation to identify cap regions.
|
||||
- Build side UVs from captured curve/profile point positions.
|
||||
- Build cap UVs by remapping profile source positions through a bounding box.
|
||||
- Switch between side UVs and cap UVs with the cap mask.
|
||||
|
||||
Metaphor: the curve and profile are two rulers crossing. The mesh is the cloth
|
||||
stretched across them; the captured indices are the ruler marks that survive
|
||||
after the cloth exists.
|
||||
|
||||
## Demo And Title
|
||||
|
||||
`VG Curve To Mesh UV Demo` recreates `_258246` as a simple closed curve/profile
|
||||
example that calls `VG Curve To Mesh UV`.
|
||||
|
||||
`VG Curve To Mesh UV Title` recreates `_Title` as decorative text, plinth, and
|
||||
backdrop geometry. It is useful mostly because it exercises text curves,
|
||||
stored UV attributes, face flipping, and map-range UV generation.
|
||||
|
||||
## Lessons
|
||||
|
||||
- Not every reusable node group creates form. Some preserve or condition
|
||||
surface state for a larger graph.
|
||||
- Edge-domain and face-domain passes can be stacked deliberately: first decide
|
||||
edge continuity, then stamp final face smoothness.
|
||||
- Utility mega-graphs should be entered through their small helpers first.
|
||||
They expose the local contracts the main group relies on.
|
||||
- Field outputs need field-shaped verification. The UV output was checked by
|
||||
driving vertex position with the UV vector; the caps mask was checked by
|
||||
using it as a deletion selection.
|
||||
- Utility graphs often return several contracts at once. Verify geometry,
|
||||
fields, and masks separately so one passing output does not launder the rest.
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
# Translation: Shriinivas Cartesian Function Graphs
|
||||
|
||||
## Source
|
||||
|
||||
- Repository: https://github.com/Shriinivas/geometrynodes
|
||||
- Source file: `cartesian.blend`
|
||||
- Source groups:
|
||||
- `Geometry Nodes Line`
|
||||
- `Geometry Nodes Parabola`
|
||||
- `Geometry Nodes Circle`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/shriinivas_cartesian_helper.py`
|
||||
|
||||
## Graph Family
|
||||
|
||||
These groups are three versions of the same procedural sentence:
|
||||
|
||||
```text
|
||||
make a simple path
|
||||
read each point's position
|
||||
compute a new Y value from X
|
||||
move the path into that shape
|
||||
give the curve physical thickness
|
||||
```
|
||||
|
||||
The source file uses this sentence to draw Cartesian function curves. The line
|
||||
uses `y = m*x + value`. The parabola uses `y = a*x^2 + b*x + c`. The circle
|
||||
uses the helper graph twice:
|
||||
|
||||
```text
|
||||
y = sqrt(r^2 - x^2)
|
||||
y = -sqrt(r^2 - x^2)
|
||||
```
|
||||
|
||||
then joins the upper and lower arcs before giving them thickness.
|
||||
|
||||
## Translation Summary
|
||||
|
||||
### Line
|
||||
|
||||
Source group: `Geometry Nodes Line`
|
||||
|
||||
Geometry Script group: `VG Cartesian Line`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Build a two-point `Curve Line` from origin to `(Length, 0, 0)`.
|
||||
- Read the current field `Position`.
|
||||
- Split position into X/Y/Z.
|
||||
- Compute `y = m*x + Value`.
|
||||
- Recombine `(x, y, z)`.
|
||||
- Feed the vector into `Set Position`.
|
||||
- Convert the resulting curve to mesh using a circular profile.
|
||||
|
||||
Metaphor: the curve is a straight rail, and the position field is a set of
|
||||
chalk marks along it. The math node chain tells each chalk mark how high to
|
||||
stand. `Set Position` is the hand lifting those marks into place.
|
||||
|
||||
### Parabola
|
||||
|
||||
Source group: `Geometry Nodes Parabola`
|
||||
|
||||
Geometry Script group: `VG Cartesian Parabola`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Build a line from `(Start, 0, 0)` to `(End, 0, 0)`.
|
||||
- Resample it to `Sample Resolution` points.
|
||||
- Read each point's X coordinate.
|
||||
- Compute `y = a*x^2 + b*x + c`.
|
||||
- Move the resampled curve with `Set Position`.
|
||||
- Tube the curve with `Curve to Mesh`.
|
||||
|
||||
Metaphor: resampling makes evenly spaced beads on a string. The parabola
|
||||
formula is the jig that bends each bead upward or downward based on its X
|
||||
address.
|
||||
|
||||
The source group has two input sockets named `Resolution`; only the later one
|
||||
drives `Resample Curve.Count`. The translation keeps the unused compatibility
|
||||
input as `Resolution` and names the active control `Sample Resolution`. This is
|
||||
one place where perfect socket-name mimicry would make the generated graph less
|
||||
legible.
|
||||
|
||||
### Circle
|
||||
|
||||
Source group: `Geometry Nodes Circle`
|
||||
|
||||
Geometry Script group: `VG Cartesian Circle`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Compute left and right X endpoints from `-r` and `r`.
|
||||
- Build an upper arc with `sqrt(r^2 - x^2)`.
|
||||
- Build a lower arc with the same helper and `Value = -1`.
|
||||
- Move both arcs with `Set Position`.
|
||||
- Join the two curves.
|
||||
- Tube the joined curve with `Curve to Mesh`.
|
||||
|
||||
Metaphor: the helper graph is a compass arm. Run it once above the axis and
|
||||
once below the axis, then clamp the two strokes together into a full circle.
|
||||
|
||||
The source graph reuses `NodeGroup.001` as nested group nodes. The installed
|
||||
Geometry Script code initially failed in this path under Blender 5.1 because
|
||||
its `@tree` returned function called a missing `geometrynodegroup(...)` symbol.
|
||||
We patched the repo-local Geometry Script clone so nested `@tree` calls create
|
||||
`GeometryNodeGroup` nodes directly, wire inputs, and expose typed outputs.
|
||||
|
||||
The translation now calls `vg_cartesian_helper(...)` twice inside
|
||||
`VG Cartesian Circle`, preserving the repeated motif as nested graph organs
|
||||
instead of expanding it inline.
|
||||
|
||||
## Control Surfaces
|
||||
|
||||
These graphs expose the useful joints:
|
||||
|
||||
- `Length`: extent of the line along X.
|
||||
- `m`: line slope.
|
||||
- `Value`: line Y offset, and helper polarity/amplitude.
|
||||
- `Start` / `End`: sampled X range for the parabola.
|
||||
- `a`, `b`, `c`: parabola coefficients.
|
||||
- `r`: circle radius.
|
||||
- `Thickness`: curve profile radius.
|
||||
- `Sample Resolution`: number of samples before bending a curve.
|
||||
|
||||
The important pattern is that controls should match geometric intent, not node
|
||||
incidental detail. A user wants radius, slope, coefficient, and thickness; they
|
||||
do not want to manage the third socket of a math node like a cursed vending
|
||||
machine.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-cartesian.blend' --python '.\tools\verify_cartesian_translations.py'
|
||||
```
|
||||
|
||||
Verification wraps each source and translated group in the same evaluator,
|
||||
evaluates the resulting mesh, and compares vertices.
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Vertices | Direct max delta | Sorted max delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Helper | 200 | 0.0 | 0.0 | accepted |
|
||||
| Line | 64 | 0.0 | 0.0 | accepted |
|
||||
| Parabola | 992 | 0.0 | 0.0 | accepted |
|
||||
| Circle | 2368 | 1.2999999523162842 | 0.0 | accepted |
|
||||
|
||||
Circle has identical geometry but different vertex order. For rendered geometry
|
||||
and shape behavior, sorted vertex equality is the relevant check. For
|
||||
simulations or downstream topology-sensitive processing, direct order would
|
||||
matter and should be preserved deliberately.
|
||||
|
||||
## Lessons
|
||||
|
||||
- A field is weather over geometry: it has a value at every point, not one
|
||||
value globally. `Position` is the weather station every point carries.
|
||||
- `Set Position` is the hinge where an abstract formula becomes visible
|
||||
geometry.
|
||||
- `Curve to Mesh` is a lathe/extruder: it turns a weightless path into a tube by
|
||||
dragging a profile along it.
|
||||
- `Named Attribute` plus `Switch` is a graceful override hook: if a `radius`
|
||||
attribute exists, use it; otherwise fall back to scale `1.0`.
|
||||
- Nested Geometry Script `@tree` calls are now viable after the patch branch in
|
||||
`external/geometry-script`. Use them when a repeated motif deserves to remain
|
||||
a named, inspectable organ in the generated Blender graph.
|
||||
- Python helper functions are still useful for repeated authoring idioms that do
|
||||
not need to survive as named node groups.
|
||||
- Socket names are part of the control contract. Geometry Script title-cases
|
||||
one-letter parameter names, so finalization currently renames `R`, `M`, `A`,
|
||||
`B`, and `C` back to `r`, `m`, `a`, `b`, and `c`.
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
# Translation: Shriinivas Cartesian Helper
|
||||
|
||||
## Source
|
||||
|
||||
- Repository: https://github.com/Shriinivas/geometrynodes
|
||||
- Source file: `cartesian.blend`
|
||||
- Local ignored copy:
|
||||
`experiments/source-blends/shriinivas-cartesian.blend`
|
||||
- Source node group: `NodeGroup.001`
|
||||
- License: MIT, per source repository.
|
||||
|
||||
## Why This Graph
|
||||
|
||||
This was the smallest geometry node group in `cartesian.blend` after inspection.
|
||||
It is a useful first translation because it combines:
|
||||
|
||||
- group inputs and multiple outputs
|
||||
- a generated curve primitive
|
||||
- curve resampling
|
||||
- field-driven position reads
|
||||
- scalar math
|
||||
- vector recomposition
|
||||
|
||||
It does not include materials, object modifiers, or final scene polish. That is
|
||||
good. First learn the tendon before drawing the whole hand.
|
||||
|
||||
## Source Inspection
|
||||
|
||||
Inspected with:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-cartesian.blend' --python '.\tools\inspect_blend_node_groups.py' -- --json '.\experiments\inspection\shriinivas-cartesian-node-groups.json'
|
||||
```
|
||||
|
||||
Source group summary:
|
||||
|
||||
- Node group: `NodeGroup.001`
|
||||
- Nodes: 15
|
||||
- Links: 20
|
||||
- Inputs: `Count`, `Start`, `End`, `r`, `Value`
|
||||
- Outputs: `Curve`, `Vector`
|
||||
- Node types:
|
||||
- `GeometryNodeCurvePrimitiveLine`
|
||||
- `GeometryNodeInputPosition`
|
||||
- `GeometryNodeResampleCurve`
|
||||
- `NodeGroupInput`
|
||||
- `NodeGroupOutput`
|
||||
- `NodeReroute`
|
||||
- `ShaderNodeCombineXYZ`
|
||||
- `ShaderNodeMath`
|
||||
- `ShaderNodeSeparateXYZ`
|
||||
|
||||
## Plain-Language Graph Map
|
||||
|
||||
The group builds a straight curve from `Start` to `End`, then resamples it to
|
||||
`Count` points. That output is the `Curve`.
|
||||
|
||||
Separately, it reads the current `Position` field and splits it into X, Y, and
|
||||
Z. It keeps X and Z as-is. For Y, it computes:
|
||||
|
||||
```text
|
||||
sqrt(r^2 - x^2) * Value
|
||||
```
|
||||
|
||||
Then it combines:
|
||||
|
||||
```text
|
||||
(x, sqrt(r^2 - x^2) * Value, z)
|
||||
```
|
||||
|
||||
That output is the `Vector`.
|
||||
|
||||
The group is a helper for bending or lifting a Cartesian curve into a circular
|
||||
arc-like profile. It does not move geometry by itself; it emits the curve and
|
||||
the vector field that another graph can feed into `Set Position`.
|
||||
|
||||
## Geometry Script Translation
|
||||
|
||||
Script:
|
||||
|
||||
```text
|
||||
examples/geometry_script/shriinivas_cartesian_helper.py
|
||||
```
|
||||
|
||||
The translation intentionally omits source reroute nodes. Reroutes clarify the
|
||||
visual editor layout but do not change graph semantics.
|
||||
|
||||
One small compatibility adjustment is made after generation: the auto-created
|
||||
Geometry Script input socket `R` is renamed to `r` to match the source group.
|
||||
|
||||
## Verification
|
||||
|
||||
Smoke-tested with Blender 5.1.1:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background --python '.\examples\geometry_script\shriinivas_cartesian_helper.py'
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
VG_CARTESIAN_HELPER True 12 19
|
||||
```
|
||||
|
||||
Generated node types:
|
||||
|
||||
- `GeometryNodeCurvePrimitiveLine`
|
||||
- `GeometryNodeInputPosition`
|
||||
- `GeometryNodeResampleCurve`
|
||||
- `NodeGroupInput`
|
||||
- `NodeGroupOutput`
|
||||
- `ShaderNodeCombineXYZ`
|
||||
- `ShaderNodeMath`
|
||||
- `ShaderNodeSeparateXYZ`
|
||||
|
||||
Saved-and-reinspected generated `.blend`:
|
||||
|
||||
```text
|
||||
VG Cartesian Helper: nodes=12 links=19
|
||||
```
|
||||
|
||||
The generated group has fewer nodes than the source because it omits three
|
||||
`NodeReroute` layout nodes and one reroute link. The computational nodes match
|
||||
the source mechanism.
|
||||
|
||||
Behavior verification:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-cartesian.blend' --python '.\tools\verify_cartesian_translations.py'
|
||||
```
|
||||
|
||||
The verification harness wraps both the source helper and the Geometry Script
|
||||
helper in the same graph:
|
||||
|
||||
```text
|
||||
helper Curve + helper Vector -> Set Position -> Curve to Mesh
|
||||
```
|
||||
|
||||
It evaluates both generated meshes with:
|
||||
|
||||
```text
|
||||
Count = 25
|
||||
Start = (-0.45, 0.0, 0.0)
|
||||
End = (0.45, 0.0, 0.0)
|
||||
r = 0.6
|
||||
Value = 1.25
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
source_vertex_count = 200
|
||||
translated_vertex_count = 200
|
||||
max_vertex_delta = 0.0
|
||||
mean_vertex_delta = 0.0
|
||||
```
|
||||
|
||||
This verifies that the translated helper retained the source helper's evaluated
|
||||
behavior for the tested parameter set, not merely its rough node vocabulary.
|
||||
|
||||
## Lessons
|
||||
|
||||
- The local generated Geometry Script docs should be checked before writing each
|
||||
node call. Function signatures and result-object shapes matter.
|
||||
- Geometry Script is good at semantic graph construction, but it will not
|
||||
preserve visual-editor layout details such as reroutes unless we explicitly
|
||||
add them.
|
||||
- Generated node groups need `use_fake_user = True` or a real modifier/object
|
||||
user before saving, or Blender will discard them as orphan data.
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
# Translation: Shriinivas Field Value Helpers
|
||||
|
||||
## Source
|
||||
|
||||
- Repository: https://github.com/Shriinivas/geometrynodes
|
||||
- Source file: `fieldvalue.blend`
|
||||
- Source groups:
|
||||
- `Create Decimal`
|
||||
- `Create Segment`
|
||||
- `Seven Segments`
|
||||
- `Delete Segments`
|
||||
- `Next Digit`
|
||||
- `Digit At`
|
||||
- `Field Value`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/shriinivas_fieldvalue.py`
|
||||
|
||||
## Why This Graph
|
||||
|
||||
`fieldvalue.blend` contains a large seven-segment field display system. The full
|
||||
`Field Value` graph has 176 nodes, so the first honest targets are helpers that
|
||||
build the digit chassis before the full display logic is attempted.
|
||||
|
||||
Together they show the first display-system layers: tiny glyph geometry,
|
||||
field-sculpted segment geometry, segment composition, segment visibility masks,
|
||||
and value-only digit extraction.
|
||||
|
||||
## Create Decimal Map
|
||||
|
||||
Source group: `Create Decimal`
|
||||
|
||||
Geometry Script group: `VG Create Decimal`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Create a filled 32-vertex `Mesh Circle`.
|
||||
- Use `radius` as the circle radius.
|
||||
- Offset it downward by `Vertical Segment Size * -1`.
|
||||
- Return the dot geometry.
|
||||
|
||||
Metaphor: the decimal is a loose bead placed under the seven-segment digit
|
||||
frame. It is not a digit; it is punctuation geometry with one placement rule.
|
||||
|
||||
## Digit At Map
|
||||
|
||||
Inputs:
|
||||
|
||||
- `Number`
|
||||
- `Position`
|
||||
|
||||
Output:
|
||||
|
||||
- `Result`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Shift `Position` by one.
|
||||
- Divide `Number` by `10^(Position + 1)`.
|
||||
- Take the fractional part.
|
||||
- Multiply by 10 to isolate the digit window.
|
||||
- Scale by `10^Position`, round, then divide back down.
|
||||
- If `Position == 0`, round the result.
|
||||
- Otherwise, floor the result.
|
||||
|
||||
Metaphor: the group is a little inspection window sliding over a number. It
|
||||
moves to the requested decimal place, crops the surrounding digits away, then
|
||||
decides whether to round or floor the exposed digit.
|
||||
|
||||
## Create Segment Map
|
||||
|
||||
Source group: `Create Segment`
|
||||
|
||||
Geometry Script group: `VG Create Segment`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Build a `Grid` with either `2x3` or `3x2` vertices depending on `X Major`.
|
||||
- Read each vertex `Position`.
|
||||
- Split the position into major-axis and secondary-axis coordinates.
|
||||
- Build a tip vector along the major axis:
|
||||
|
||||
```text
|
||||
tip = 0.5 * Sharpness * Secondary Axis Segment Thickness
|
||||
```
|
||||
|
||||
- Detect vertices on the secondary axis and at the positive/negative segment
|
||||
ends.
|
||||
- Push only those gated vertices inward/outward to make pointed segment tips.
|
||||
- Add the user `offset`.
|
||||
- Move the grid vertices with `Set Position`.
|
||||
|
||||
Metaphor: the segment starts as a plain rectangular tile. The position field is
|
||||
a stencil laid over its vertices. The end-corner vertices get pinched into tips,
|
||||
while the middle vertices stay put. It is a chisel, not a sculpting spree.
|
||||
|
||||
## Seven Segments Map
|
||||
|
||||
Source group: `Seven Segments`
|
||||
|
||||
Geometry Script group: `VG Seven Segments`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Divide horizontal and vertical segment sizes by two to get left/right and
|
||||
upper/lower placement extents.
|
||||
- Subtract `X Separation` from horizontal length and `Y Seperation` from
|
||||
vertical length so the bars leave small gaps.
|
||||
- Create four vertical bars with `X Major = False`:
|
||||
lower left, upper left, lower right, and upper right.
|
||||
- Create three horizontal bars with `X Major = True`:
|
||||
bottom, middle, and top.
|
||||
- Create the decimal dot with radius `Horizontal Segment Thickness / 2`.
|
||||
- Join dot plus seven bars into one `bars` output.
|
||||
|
||||
Metaphor: `Create Segment` is a single machined stroke; `Seven Segments` is the
|
||||
digit chassis. It bolts seven strokes and one punctuation bead onto a shared
|
||||
frame so later graph layers can decide which pieces stay visible.
|
||||
|
||||
## Delete Segments Map
|
||||
|
||||
Source group: `Delete Segments`
|
||||
|
||||
Geometry Script group: `VG Delete Segments`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `Digit`
|
||||
- `Segment Position`
|
||||
|
||||
Output:
|
||||
|
||||
- `result`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Shift `Digit` down by one.
|
||||
- Group `Segment Position` into repeated pair masks:
|
||||
`0/1`, `2/3`, `4/5`, `6/7`, `8/9`, `10/11`, `12/13`, plus singleton `14`.
|
||||
- Compare the shifted digit against the source's digit cases.
|
||||
- For each digit case, return true when that segment-position mask should be
|
||||
deleted.
|
||||
- OR all digit-specific delete clauses into the final `result`.
|
||||
|
||||
Metaphor: `Delete Segments` is not a sculptor. It is a stencil card. Given a
|
||||
digit and a slot on the chassis, it punches out the slots that should go dark.
|
||||
|
||||
## Next Digit Map
|
||||
|
||||
Source group: `Next Digit`
|
||||
|
||||
Geometry Script group: `VG Next Digit`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `Whole Part`
|
||||
- `Fraction Part`
|
||||
- `Max Precision`
|
||||
- `Position`
|
||||
|
||||
Output:
|
||||
|
||||
- `Result`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Compute `Position - Max Precision` for whole-part indexing.
|
||||
- Compare `Position < Max Precision`.
|
||||
- If true, read from `Fraction Part` at `Position`.
|
||||
- If false, read from `Whole Part` at `Position - Max Precision`.
|
||||
|
||||
Metaphor: `Next Digit` is the read head for the display tape. Before the decimal
|
||||
boundary it reads the fractional reel; after that boundary it reads the whole
|
||||
number reel.
|
||||
|
||||
## Field Value Map
|
||||
|
||||
Source group: `Field Value`
|
||||
|
||||
Geometry Script group: `VG Field Value`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Use menu switches to route three control choices:
|
||||
input type (`Float` or `Vector`), source domain (`Point`, `Edge`, or `Face`),
|
||||
and text alignment (`Center`, `Right`, or `Left`).
|
||||
- Capture the requested float or vector field on point, edge, and face domains,
|
||||
then choose the captured geometry and captured value stream for the active
|
||||
domain.
|
||||
- Convert edge or face domains to points so the display can instance one value
|
||||
readout per source element.
|
||||
- Build one seven-segment glyph, capture each glyph face index, then instance
|
||||
that glyph across digit slots, vector component slots, and source elements.
|
||||
- Sample source position and normal by domain index so each readout can be
|
||||
placed and oriented back onto the element that produced it.
|
||||
- Compute the active scalar value for the current component, split it into
|
||||
whole/fractional parts, decide visible slots, negative sign, decimal point,
|
||||
overflow, and leading-zero behavior.
|
||||
- Call `VG Next Digit` for digit extraction and `VG Delete Segments` to delete
|
||||
the dark faces from each glyph.
|
||||
- Realize instances and assign the requested material.
|
||||
|
||||
Metaphor: the full graph is a label printer mounted on the mesh. It reads a
|
||||
value from each element, assembles a small digit rack, swings the rack onto the
|
||||
element normal, then punches out the unlit segments.
|
||||
|
||||
Implementation note: Blender 5 capture items and menu sockets exposed two
|
||||
Geometry Script drift points. `VG Field Value` uses a local
|
||||
`_capture_attribute_item(...)` helper to create typed capture items explicitly,
|
||||
and the VibeGeometry Geometry Script fork tolerates menu socket defaults whose
|
||||
enum items do not exist at group-interface construction time.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-fieldvalue.blend' --python '.\tools\verify_fieldvalue_translations.py'
|
||||
```
|
||||
|
||||
The verifier wraps both source and translated `Digit At` groups in a mesh-line
|
||||
graph where the digit output drives vertex count. This turns a scalar output
|
||||
into geometry Blender can evaluate and compare.
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source | Translated | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Create Decimal vertices | 32 | 32 | 0.0 | accepted |
|
||||
| Create Segment horizontal vertices | 6 | 6 | 0.0 | accepted |
|
||||
| Create Segment vertical vertices | 6 | 6 | 0.0 | accepted |
|
||||
| Seven Segments vertices | 74 | 74 | 0.0 | accepted |
|
||||
| Digit At `1234.0`, position `0` | 4 | 4 | 0 | accepted |
|
||||
| Digit At `1234.0`, position `1` | 3 | 3 | 0 | accepted |
|
||||
| Digit At `98765.0`, position `2` | 7 | 7 | 0 | accepted |
|
||||
| Digit At `120305.0`, position `3` | 0 | 0 | 0 | accepted |
|
||||
| Field Value default-route vertices | 256 | 256 | 0.0 | accepted |
|
||||
| Next Digit branch cases | 4 cases | 4 cases | 0 mismatches | accepted |
|
||||
| Delete Segments grid | 195 cases | 195 cases | 0 mismatches | accepted |
|
||||
|
||||
## Lessons
|
||||
|
||||
- Not every useful node group emits final geometry. Some are value transducers.
|
||||
- Value-only graph behavior can still be verified in Blender by embedding the
|
||||
value output inside a small geometry-producing harness.
|
||||
- Digit extraction is windowing: scale, crop with fraction/floor/round, then
|
||||
normalize.
|
||||
- Large procedural display systems should be built from tiny tested scalar
|
||||
helpers before composing visible glyph geometry.
|
||||
- Tiny glyph marks are still worth isolating as named groups. A decimal point is
|
||||
just a dot, but a named dot with scale and placement controls is a reusable
|
||||
punctuation part.
|
||||
- Field masks can act as chisels. In `Create Segment`, comparisons over
|
||||
position decide which vertices are allowed to move, creating pointed tips
|
||||
without hand-authoring mesh coordinates.
|
||||
- Composition groups should preserve part boundaries in the script even when
|
||||
the output is one joined geometry stream. The source `Seven Segments` graph is
|
||||
easier to reason about as four vertical bars, three horizontal bars, and one
|
||||
dot than as a pile of joined anonymous geometry.
|
||||
- Dense boolean graphs often hide lookup tables. Translate them into named
|
||||
clauses and verify over the full input grid instead of preserving every source
|
||||
`OR` node as ceremony.
|
||||
- A branch helper deserves tests on both sides of the branch. `Next Digit`
|
||||
verifies positions before and after `Max Precision`, because the interesting
|
||||
behavior is not digit extraction itself but choosing which number part feeds
|
||||
extraction.
|
||||
- Capture attributes are a loading dock for fields. Once a graph crosses into
|
||||
instancing, capture the values and indices that later stages must still know.
|
||||
- Full display graphs are stacked assemblies: source element points carry
|
||||
value racks; value racks carry component stacks; component stacks carry digit
|
||||
slots; digit slots carry segment faces. Preserve each layer's index baton.
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
# Translation: Shriinivas Parametric And Polar Equations
|
||||
|
||||
## Source
|
||||
|
||||
- Repository: https://github.com/Shriinivas/geometrynodes
|
||||
- Source file: `paramnpolareq.blend`
|
||||
- Source groups:
|
||||
- `chooseFloatVal`
|
||||
- `chooseFloatVal.001`
|
||||
- `chooseStrVal`
|
||||
- `chooseStrVal.001`
|
||||
- `NodeGroup`
|
||||
- `NodeGroup.001`
|
||||
- `NodeGroup.002`
|
||||
- `NodeGroup.003`
|
||||
- `getxy.001`
|
||||
- `Archimedes Spiral`
|
||||
- `Epicycloid`
|
||||
- `Geometry Nodes Group`
|
||||
- `Geometry Nodes Group.002`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/shriinivas_paramnpolareq.py`
|
||||
|
||||
## Why This Graph
|
||||
|
||||
This sample teaches parametric curve construction without hiding behind final
|
||||
rendering polish. The source graphs make a line of points, accumulate a
|
||||
parameter over that line, turn the parameter into `x/y` fields, and then move
|
||||
the points into curve shapes.
|
||||
|
||||
## Equation Helpers
|
||||
|
||||
`VG Param Epicycloid X` and `VG Param Epicycloid Y` recreate the source
|
||||
epicycloid scalar helpers:
|
||||
|
||||
```text
|
||||
x = (a + b) * cos(t) - b * cos((a / b + 1) * t)
|
||||
y = (a + b) * sin(t) - b * sin((a / b + 1) * t)
|
||||
```
|
||||
|
||||
`VG Polar Spiral X` and `VG Polar Spiral Y` recreate the polar helper pair:
|
||||
|
||||
```text
|
||||
r = a + b * theta
|
||||
x = cos(theta) * r
|
||||
y = sin(theta) * r
|
||||
```
|
||||
|
||||
Metaphor: these helpers are plotter pens without paper. They answer "where
|
||||
should the point be for this parameter?" but they do not decide how many points
|
||||
exist or how the curve is sampled.
|
||||
|
||||
## Parameter Conveyor
|
||||
|
||||
`VG Archimedes Spiral`, `VG Epicycloid`, and `VG Mirrored Root Spiral` share a
|
||||
sampling spine:
|
||||
|
||||
- Create a `Mesh Line` with `Resolution` points.
|
||||
- Compute `step = max_value / (Resolution - 1)`.
|
||||
- Feed `step` into `Accumulate Field`.
|
||||
- Use the trailing accumulated value as `theta` or `t`.
|
||||
- Compute `x/y`, combine them into a position, and move the mesh line with
|
||||
`Set Position`.
|
||||
|
||||
Metaphor: the mesh line is a conveyor belt. `Accumulate Field` stamps each
|
||||
station with a progressively larger parameter, and the equation helper tells
|
||||
that station where to stand.
|
||||
|
||||
## Mirrored Root Spiral
|
||||
|
||||
Source group `Geometry Nodes Group.002` is a two-arm spiral:
|
||||
|
||||
```text
|
||||
theta = accumulated(2 * pi * Round Count)
|
||||
r = a * sqrt(theta)
|
||||
x = r * cos(theta)
|
||||
y = r * sin(theta)
|
||||
```
|
||||
|
||||
The source builds a forward point stream and a mirrored stream at `(-x, -y)`,
|
||||
then joins them.
|
||||
|
||||
Metaphor: the first arm is a chalk line unwinding from the center; the second
|
||||
arm is the same chalk line reflected through the origin.
|
||||
|
||||
## Selector And Wrapper
|
||||
|
||||
Source groups: `chooseFloatVal`, `chooseStrVal`, `getxy.001`,
|
||||
`Geometry Nodes Group`
|
||||
|
||||
Geometry Script groups: `VG Choose Float Val`, `VG Choose Str Val`,
|
||||
`VG Param XY`, `VG Param Curve`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Normalize `Curve Type` with absolute value and modulo.
|
||||
- Compute five candidate curve formulas:
|
||||
epicycloid, epitrochoid, hypocycloid, hypotrochoid, and Archimedes spiral.
|
||||
- Use switch chains to select the active `x`, `y`, and equation label.
|
||||
- Build a mesh-line parameter conveyor for `t`.
|
||||
- Convert the parameterized point stream to a curve, give it a circular
|
||||
profile, and optionally add a text label.
|
||||
|
||||
Metaphor: `getxy.001` is a rotary selector on a plotting machine. The machine
|
||||
prepares five pens, then the selector lets exactly one touch the paper.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-paramnpolareq.blend' --python '.\tools\verify_paramnpolareq_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Source | Translated | Delta | Status |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Epicycloid X scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
|
||||
| Epicycloid Y scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
|
||||
| Polar X scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
|
||||
| Polar Y scalar vertex | 1 vertex | 1 vertex | 0.0 | accepted |
|
||||
| Archimedes Spiral vertices | 64 | 64 | 4.47e-8 | accepted |
|
||||
| Epicycloid vertices | 80 | 80 | 0.0 | accepted |
|
||||
| Mirrored Root Spiral vertices | 192 | 192 | 0.0 | accepted |
|
||||
| Param XY curve types | 5 cases | 5 cases | 0.0 | accepted |
|
||||
| Param Curve wrapper vertices | 2048 | 2048 | 0.0 | accepted |
|
||||
|
||||
The scalar helpers are verified by putting the scalar into a single vertex's X
|
||||
position. The curve groups are verified by sorted evaluated vertex comparison.
|
||||
|
||||
## Lessons
|
||||
|
||||
- `Accumulate Field` is the natural way to build a parameter ramp inside a
|
||||
node graph. Python should describe the ramp; the graph should evaluate it.
|
||||
- Equation helpers are worth naming when several curve generators need the same
|
||||
scalar law.
|
||||
- Verifiers should normalize socket names when comparing source groups to
|
||||
generated groups. `maxT` and `Max T` are the same control for behavior even
|
||||
when the UI spelling drifts.
|
||||
- A scalar harness should preserve numeric value, not just count geometry.
|
||||
Driving a vertex coordinate is often cleaner than driving `Mesh Line.Count`.
|
||||
- Selector graphs are better translated as named candidate formulas plus one
|
||||
switchboard than as copied switch-node ceremony.
|
||||
- Optional label geometry should be verified separately from the curve when
|
||||
possible. The wrapper geometry path was accepted with labels off so curve
|
||||
behavior could be isolated first.
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
# Translation: Shriinivas Pie Chart
|
||||
|
||||
## Source
|
||||
|
||||
- Repository: https://github.com/Shriinivas/geometrynodes
|
||||
- Source file: `piechart.blend`
|
||||
- Source groups:
|
||||
- `NodeGroup`
|
||||
- `Pie Chart`
|
||||
- `getPie.002` through `getPie.012`
|
||||
- `Extended Pie Chart`
|
||||
- Geometry Script recreation:
|
||||
`examples/geometry_script/shriinivas_piechart.py`
|
||||
|
||||
## Graph Family
|
||||
|
||||
The small pie chart is a nested state-passing graph. Each segment receives:
|
||||
|
||||
```text
|
||||
start angle
|
||||
value
|
||||
total
|
||||
shift distance
|
||||
materials
|
||||
```
|
||||
|
||||
It outputs:
|
||||
|
||||
```text
|
||||
segment geometry
|
||||
end angle
|
||||
```
|
||||
|
||||
The chart group chains three segment calls:
|
||||
|
||||
```text
|
||||
A starts at 0
|
||||
B starts at A.end
|
||||
C starts at B.end
|
||||
```
|
||||
|
||||
This is the new abstraction lesson: a node group can return geometry and a
|
||||
control value. The geometry is the visible slice; the control value is the baton
|
||||
passed to the next runner.
|
||||
|
||||
## Segment Map
|
||||
|
||||
Source group: `NodeGroup`
|
||||
|
||||
Geometry Script group: `VG Pie Segment`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Compute `fraction = Value / Total`.
|
||||
- Compute `sweep = fraction * tau`.
|
||||
- Compute `End = Start + sweep`.
|
||||
- Build a connected-center `Arc` from `Start` through `sweep`.
|
||||
- Fill the arc into a mesh face.
|
||||
- Extrude the face downward for pie thickness.
|
||||
- Convert `fraction * 100` to a string and append `%`.
|
||||
- Convert that string to curves, fill it, and extrude it lightly into text.
|
||||
- Use `Attribute Statistic` over the slice face's `Position` to find a center
|
||||
offset for the text.
|
||||
- Compute a shift vector at the segment midpoint:
|
||||
|
||||
```text
|
||||
mid = Start + sweep / 2
|
||||
offset = (cos(mid) * Shift, sin(mid) * Shift, 0)
|
||||
```
|
||||
|
||||
- Join text, extruded slice, and cap face.
|
||||
- Offset the joined geometry by the shift vector.
|
||||
- Return geometry plus `End`.
|
||||
|
||||
Metaphor: the segment group is a pastry cutter with a receipt printer attached.
|
||||
It cuts one wedge, stamps the percentage label near its middle, nudges the whole
|
||||
wedge outward if requested, and hands the next cutter the angle where it stopped.
|
||||
|
||||
## Chart Map
|
||||
|
||||
Source group: `Pie Chart`
|
||||
|
||||
Geometry Script group: `VG Pie Chart`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Compute `Total = A + B + C`.
|
||||
- Call `VG Pie Segment` for A with `Start = 0`.
|
||||
- Call `VG Pie Segment` for B with `Start = A.End`.
|
||||
- Call `VG Pie Segment` for C with `Start = B.End`.
|
||||
- Join the three segment geometries.
|
||||
|
||||
Metaphor: the chart group is not a shape maker. It is an accountant-conductor:
|
||||
it totals the values, cues each segment in order, and makes sure every slice
|
||||
starts where the previous one ended.
|
||||
|
||||
## Extended Chart Map
|
||||
|
||||
Source groups: `getPie.002` through `getPie.012`, `Extended Pie Chart`
|
||||
|
||||
Geometry Script groups: `VG Extended Pie Segment`, `VG Extended Pie Chart`
|
||||
|
||||
Mechanism:
|
||||
|
||||
- Collapse the numbered `getPie.*` clones into one parameterized extended
|
||||
segment helper.
|
||||
- Add radius, pie height, text height, text size, text offset, label text, and
|
||||
percentage toggle controls.
|
||||
- Generate the segment label as either `Text` or percentage-plus-text.
|
||||
- Place label geometry along the segment midpoint at `Text Offset * Radius`.
|
||||
- Build a filled/extruded wedge at `Radius` and `Pie Height`.
|
||||
- Compose up to ten slices by passing each segment's `End Angle` into the next
|
||||
segment.
|
||||
- Gate slices 2-10 with `Segment Count`.
|
||||
- Add title text below the chart.
|
||||
|
||||
Metaphor: the small chart is a three-slice accountant. The extended chart is a
|
||||
ten-slot serving line: each station can be open or closed, but the serving
|
||||
angle still passes down the line.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe' --background '.\experiments\source-blends\shriinivas-piechart.blend' --python '.\tools\verify_piechart_translations.py'
|
||||
```
|
||||
|
||||
Results from Blender 5.1.1:
|
||||
|
||||
| Case | Vertices | Sorted max delta | Status |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Segment | 319 | 0.0 | accepted |
|
||||
| Chart | 957 | 0.0 | accepted |
|
||||
| Extended Segment | 319 | 0.0 | accepted |
|
||||
| Extended Chart | 3190 | 0.0 | accepted |
|
||||
|
||||
Generated groups saved and reinspected:
|
||||
|
||||
- `VG Pie Segment`
|
||||
- `VG Pie Chart`
|
||||
- `VG Extended Pie Segment`
|
||||
- `VG Extended Pie Chart`
|
||||
|
||||
## Lessons
|
||||
|
||||
- A reusable node group can output both visible geometry and invisible state.
|
||||
That state can drive later graph calls.
|
||||
- `Arc` with `connect_center=True` is a wedge skeleton. `Fill Curve` gives it a
|
||||
face; `Extrude Mesh` gives it thickness.
|
||||
- `Attribute Statistic` can summarize geometry to produce placement handles.
|
||||
Here, the mean position of the wedge face becomes a label anchor.
|
||||
- Text is geometry too: string operations feed `String to Curves`, then the same
|
||||
fill/extrude/material pipeline applies.
|
||||
- Charts and other sequential structures often need cumulative scalar flow.
|
||||
In Geometry Script, nested `@tree` calls make that flow readable as ordinary
|
||||
Python variable passing.
|
||||
- Numbered source clones are not doctrine. Translate the stable contract once,
|
||||
then use Python lists and loops to express the repeated composition.
|
||||
- Output order matters when a group returns both state and geometry. The
|
||||
extended segment preserves the source order `End Angle`, then `Pie`; grabbing
|
||||
those in the wrong order made the whole chart disappear until verification
|
||||
caught it.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Examples
|
||||
|
||||
Reviewed example artifacts live here.
|
||||
|
||||
Expected subfolders:
|
||||
|
||||
- `scene-briefs/`: target descriptions and acceptance criteria
|
||||
- `blender/`: generated scripts, manifests, and small `.blend` receipts when useful
|
||||
|
||||
Keep disposable runs in `experiments/` until they teach something worth
|
||||
promoting.
|
||||
|
|
@ -1,827 +0,0 @@
|
|||
"""Build an Aetheria Bloom habitat study scene.
|
||||
|
||||
This is a hybrid authoring test:
|
||||
|
||||
- Python owns the lore-derived scene brief, tables, materials, labels, camera,
|
||||
and Blender object orchestration.
|
||||
- Geometry Script emits reusable inspectable node groups for procedural rails.
|
||||
- bpy builds the cutaway habitat mesh and render evidence.
|
||||
"""
|
||||
|
||||
import math as pymath
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from vibegeometry import (
|
||||
CylinderFrame,
|
||||
add_box,
|
||||
add_curve_polyline,
|
||||
add_cylinder_between,
|
||||
add_material,
|
||||
add_mesh_parts,
|
||||
add_multi_polyline_curve,
|
||||
aim_camera,
|
||||
append_box_parts,
|
||||
append_cylindrical_ring_band,
|
||||
clear_scene,
|
||||
fbm_2d,
|
||||
smoothstep,
|
||||
)
|
||||
|
||||
|
||||
TAU = pymath.tau
|
||||
AXIS_LENGTH = 18.0
|
||||
CUT_START = pymath.radians(-132.0)
|
||||
CUT_END = pymath.radians(132.0)
|
||||
FULL_START = -pymath.pi
|
||||
FULL_END = pymath.pi
|
||||
INNER_RADIUS = 5.0
|
||||
FRAME = CylinderFrame(INNER_RADIUS)
|
||||
radial = FRAME.radial
|
||||
tangent = FRAME.tangent
|
||||
cyl_point = FRAME.cyl_point
|
||||
surface_point = FRAME.surface_point
|
||||
mat = add_material
|
||||
|
||||
|
||||
@tree("VG Bloom Light Spine")
|
||||
def vg_bloom_light_spine(
|
||||
length: Float = 18.0,
|
||||
helix_radius: Float = 0.18,
|
||||
turns: Float = 7.5,
|
||||
thickness: Float = 0.035,
|
||||
):
|
||||
"""A thin procedural light/distribution rail around the axial spire."""
|
||||
|
||||
rail = curve_line(mode=CurveLine.Mode.POINTS, start=(-9.0, 0.0, 0.0), end=(9.0, 0.0, 0.0))
|
||||
samples = resample_curve(keep_last_segment=True, curve=rail, mode="Count", count=420, length=0.1)
|
||||
point_count = domain_size(component="CURVE", geometry=samples).point_count
|
||||
t = math(operation=Math.Operation.DIVIDE, value=(index(), point_count))
|
||||
theta = math(operation=Math.Operation.MULTIPLY, value=(t, math(operation=Math.Operation.MULTIPLY, value=(turns, TAU))))
|
||||
x = math(operation=Math.Operation.SUBTRACT, value=(math(operation=Math.Operation.MULTIPLY, value=(t, length)), math(operation=Math.Operation.DIVIDE, value=(length, 2.0))))
|
||||
y = math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SINE, value=theta), helix_radius))
|
||||
z = math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.COSINE, value=theta), helix_radius))
|
||||
positioned = set_position(geometry=samples, position=combine_xyz(x=x, y=y, z=z), offset=(0.0, 0.0, 0.0))
|
||||
profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=8, radius=thickness)
|
||||
return {"Geometry": curve_to_mesh(curve=positioned, profile_curve=profile, fill_caps=True)}
|
||||
|
||||
|
||||
def add_shell_segment(name, radius, half_length, thickness, material, angle_steps=144, x_steps=8, start_angle=FULL_START, end_angle=FULL_END):
|
||||
import bpy
|
||||
|
||||
verts = []
|
||||
faces = []
|
||||
for ix in range(x_steps + 1):
|
||||
x = -half_length + (2.0 * half_length * ix / x_steps)
|
||||
for ia in range(angle_steps + 1):
|
||||
a = start_angle + (end_angle - start_angle) * ia / angle_steps
|
||||
verts.append(cyl_point(x, a, radius))
|
||||
for ix in range(x_steps):
|
||||
for ia in range(angle_steps):
|
||||
row = angle_steps + 1
|
||||
faces.append((ix * row + ia, (ix + 1) * row + ia, (ix + 1) * row + ia + 1, ix * row + ia + 1))
|
||||
mesh = bpy.data.meshes.new(name + "Mesh")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
obj.show_transparent = True
|
||||
solid = obj.modifiers.new("visible thickness", "SOLIDIFY")
|
||||
solid.thickness = thickness
|
||||
solid.offset = 0.0
|
||||
return obj
|
||||
|
||||
|
||||
def add_surface_patch(name, x0, x1, a0, a1, radius, material, lift=-0.045, angle_steps=8, x_steps=3):
|
||||
import bpy
|
||||
|
||||
verts = []
|
||||
faces = []
|
||||
for ix in range(x_steps + 1):
|
||||
x = x0 + (x1 - x0) * ix / x_steps
|
||||
for ia in range(angle_steps + 1):
|
||||
a = a0 + (a1 - a0) * ia / angle_steps
|
||||
verts.append(surface_point(x, a, radius, lift=lift))
|
||||
row = angle_steps + 1
|
||||
for ix in range(x_steps):
|
||||
for ia in range(angle_steps):
|
||||
faces.append((ix * row + ia, (ix + 1) * row + ia, (ix + 1) * row + ia + 1, ix * row + ia + 1))
|
||||
mesh = bpy.data.meshes.new(name + "Mesh")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_surface_curve(name, samples, radius, bevel, material, lift=-0.11):
|
||||
points = [surface_point(x, angle, radius, lift=lift) for x, angle in samples]
|
||||
return add_curve_polyline(name, points, bevel, material)
|
||||
|
||||
|
||||
def add_surface_box(name, x, angle, material, size=(0.22, 0.12, 0.08), radial_lift=-0.18):
|
||||
r = radial(angle)
|
||||
t = tangent(angle)
|
||||
loc = surface_point(x, angle, INNER_RADIUS, lift=radial_lift)
|
||||
return add_box(name, loc, axes=((1, 0, 0), t, r), size=size, material=material)
|
||||
|
||||
|
||||
def add_surface_block_field(name, x_values, angles, material, size, radial_lift=-0.22, jitter=0.0):
|
||||
for ix, x in enumerate(x_values):
|
||||
for ia, angle in enumerate(angles):
|
||||
if jitter:
|
||||
x_offset = ((ix * 17 + ia * 7) % 9 - 4) * jitter
|
||||
angle_offset = ((ix * 11 + ia * 5) % 7 - 3) * jitter * 0.05
|
||||
else:
|
||||
x_offset = 0.0
|
||||
angle_offset = 0.0
|
||||
add_surface_box(
|
||||
f"{name}_{ix:02d}_{ia:02d}",
|
||||
x + x_offset,
|
||||
angle + angle_offset,
|
||||
material,
|
||||
size=size,
|
||||
radial_lift=radial_lift,
|
||||
)
|
||||
|
||||
|
||||
def add_surface_stilt(name, x, angle, inner_lift, outer_lift, material, radius=0.009):
|
||||
start = surface_point(x, angle, INNER_RADIUS, lift=inner_lift)
|
||||
end = surface_point(x, angle, INNER_RADIUS, lift=outer_lift)
|
||||
return add_cylinder_between(name, start, end, radius, material, vertices=6)
|
||||
|
||||
|
||||
def add_surface_brace(name, x0, angle0, x1, angle1, lift0, lift1, material, bevel=0.006):
|
||||
return add_curve_polyline(
|
||||
name,
|
||||
[
|
||||
surface_point(x0, angle0, INNER_RADIUS, lift=lift0),
|
||||
surface_point((x0 + x1) * 0.5, (angle0 + angle1) * 0.5, INNER_RADIUS, lift=(lift0 + lift1) * 0.5 - 0.03),
|
||||
surface_point(x1, angle1, INNER_RADIUS, lift=lift1),
|
||||
],
|
||||
bevel,
|
||||
material,
|
||||
resolution=3,
|
||||
)
|
||||
|
||||
|
||||
def add_favela_fractal_cluster(prefix, x_values, angles, mats):
|
||||
shack_verts, shack_faces = [], []
|
||||
roof_verts, roof_faces = [], []
|
||||
brace_lines = []
|
||||
for ix, x in enumerate(x_values):
|
||||
for ia, angle in enumerate(angles):
|
||||
lean = ((ix * 5 + ia * 3) % 5 - 2) * 0.018
|
||||
root = f"{prefix}_{ix:02d}_{ia:02d}"
|
||||
axes = ((1, 0, 0), tangent(angle + lean), radial(angle + lean))
|
||||
append_box_parts(shack_verts, shack_faces, surface_point(x, angle + lean, INNER_RADIUS, lift=-0.38), axes, (0.2, 0.12, 0.24))
|
||||
for level in range(2):
|
||||
lift = -0.55 - level * 0.14
|
||||
width = 0.13 - level * 0.015
|
||||
for wing in (-1, 1):
|
||||
dx = wing * (0.09 + 0.035 * level)
|
||||
da = wing * (0.018 + 0.012 * level) + lean
|
||||
local_axes = ((1, 0, 0), tangent(angle + da), radial(angle + da))
|
||||
append_box_parts(
|
||||
shack_verts,
|
||||
shack_faces,
|
||||
surface_point(x + dx, angle + da, INNER_RADIUS, lift=lift),
|
||||
local_axes,
|
||||
(width, 0.075, 0.11),
|
||||
)
|
||||
roof_axes = ((1, 0, 0), tangent(angle + da + 0.006 * wing), radial(angle + da + 0.006 * wing))
|
||||
append_box_parts(
|
||||
roof_verts,
|
||||
roof_faces,
|
||||
surface_point(x + dx + 0.012 * wing, angle + da + 0.006 * wing, INNER_RADIUS, lift=lift - 0.07),
|
||||
roof_axes,
|
||||
(width * 1.22, 0.095, 0.018),
|
||||
)
|
||||
brace_lines.append(
|
||||
[
|
||||
surface_point(x + dx - width * 0.25, angle + da - 0.006, INNER_RADIUS, lift=lift + 0.05),
|
||||
surface_point(x + dx - width * 0.25, angle + da - 0.006, INNER_RADIUS, lift=-0.25),
|
||||
]
|
||||
)
|
||||
brace_lines.append(
|
||||
[
|
||||
surface_point(x + dx - width * 0.42, angle + da - 0.012, INNER_RADIUS, lift=lift + 0.02),
|
||||
surface_point(x + dx, angle + da, INNER_RADIUS, lift=lift - 0.04),
|
||||
surface_point(x + dx + width * 0.42, angle + da + 0.012, INNER_RADIUS, lift=lift - 0.09),
|
||||
]
|
||||
)
|
||||
for step in range(2):
|
||||
brace_lines.append(
|
||||
[
|
||||
surface_point(x - 0.18, angle + lean + step * 0.028, INNER_RADIUS, lift=-0.62 - step * 0.08),
|
||||
surface_point(x, angle + lean + 0.035 + step * 0.025, INNER_RADIUS, lift=-0.62 - step * 0.08),
|
||||
surface_point(x + 0.2, angle + lean + step * 0.02, INNER_RADIUS, lift=-0.62 - step * 0.08),
|
||||
]
|
||||
)
|
||||
add_mesh_parts(prefix + "_shack_mesh", shack_verts, shack_faces, mats["favela"])
|
||||
add_mesh_parts(prefix + "_roof_mesh", roof_verts, roof_faces, mats["shack_roof"])
|
||||
add_multi_polyline_curve(prefix + "_brace_and_catwalks", brace_lines, 0.005, mats["brace"], resolution=2)
|
||||
|
||||
|
||||
def add_hubward_endcap_terraced_slums(prefix, x, mats):
|
||||
# Bloom lore frame: on an endcap, "up" is inward toward the axial Spire.
|
||||
# These terraces are not polar graph paper. Fractal noise makes the shelf
|
||||
# edges wobble, the settlement thicken, and the class gradient accrete
|
||||
# outward until it meets the high-gravity surface city.
|
||||
shelf_verts, shelf_faces = [], []
|
||||
poor_verts, poor_faces = [], []
|
||||
middle_verts, middle_faces = [], []
|
||||
city_verts, city_faces = [], []
|
||||
roof_verts, roof_faces = [], []
|
||||
ladder_lines = []
|
||||
rings = [0.72, 0.98, 1.24, 1.52, 1.82, 2.14, 2.48, 2.84, 3.22, 3.62, 4.04, 4.48, 4.9, 5.18]
|
||||
for tier, radius in enumerate(rings[:-1]):
|
||||
inner_r = radius
|
||||
outer_r = rings[tier + 1]
|
||||
tier_t = tier / (len(rings) - 2)
|
||||
face_x = x + 0.035 + tier * 0.018
|
||||
append_cylindrical_ring_band(shelf_verts, shelf_faces, face_x, inner_r, outer_r, segments=160)
|
||||
count = int(30 + tier * 8 + 18 * fbm_2d(tier * 0.71, 4.0, seed=12))
|
||||
angular_step = TAU / count
|
||||
run = 0
|
||||
for i in range(count):
|
||||
u = i / count
|
||||
n = fbm_2d(u * 8.0 + tier * 0.37, tier * 0.91, seed=31)
|
||||
n2 = fbm_2d(u * 15.0, tier * 1.7 + 3.0, seed=53)
|
||||
density = 0.58 + tier_t * 0.34 + 0.23 * n
|
||||
if n2 > density and run < 4:
|
||||
run += 1
|
||||
continue
|
||||
run = 0
|
||||
angle_warp = (n - 0.5) * angular_step * 1.6 + 0.12 * pymath.sin(tier * 1.37 + u * TAU * 2.0)
|
||||
a = FULL_START + TAU * u + angle_warp
|
||||
radial_t = 0.24 + 0.56 * fbm_2d(u * 5.0, tier * 0.8, seed=74)
|
||||
lane_r = inner_r + (outer_r - inner_r) * radial_t + (n - 0.5) * (outer_r - inner_r) * 0.55
|
||||
lane_r = max(inner_r + 0.04, min(outer_r - 0.035, lane_r))
|
||||
local_x = face_x + 0.035 * ((i + tier) % 3) + 0.08 * (n - 0.5)
|
||||
local_axes = (tangent(a), radial(a), (1, 0, 0))
|
||||
tangential = max(0.055, TAU * lane_r / count * (0.72 + 0.5 * n))
|
||||
radial_depth = max(0.08, (outer_r - inner_r) * (0.58 + 0.45 * n2))
|
||||
height = (0.1 + 0.24 * tier_t) * (0.75 + 0.7 * n)
|
||||
if tier_t < 0.55:
|
||||
mesh_verts, mesh_faces = poor_verts, poor_faces
|
||||
material_bias = 0.0
|
||||
elif tier_t < 0.82:
|
||||
mesh_verts, mesh_faces = middle_verts, middle_faces
|
||||
material_bias = 0.12
|
||||
else:
|
||||
mesh_verts, mesh_faces = city_verts, city_faces
|
||||
material_bias = 0.24
|
||||
append_box_parts(
|
||||
mesh_verts,
|
||||
mesh_faces,
|
||||
cyl_point(local_x + height * (0.34 + material_bias), a, lane_r),
|
||||
local_axes,
|
||||
(tangential, radial_depth, height),
|
||||
)
|
||||
if tier_t < 0.78 or n > 0.62:
|
||||
roof_axes = (tangent(a + 0.006), radial(a + 0.006), (1, 0, 0))
|
||||
append_box_parts(
|
||||
roof_verts,
|
||||
roof_faces,
|
||||
cyl_point(local_x + height * 0.78, a + 0.004, lane_r - 0.012),
|
||||
roof_axes,
|
||||
(tangential * 1.2, radial_depth * 1.06, 0.018),
|
||||
)
|
||||
if n > 0.38:
|
||||
ladder_lines.append([cyl_point(local_x + 0.12, a, min(outer_r - 0.03, lane_r + radial_depth * 0.45)), cyl_point(local_x + 0.18, a + 0.012, max(inner_r + 0.03, lane_r - radial_depth * 0.48))])
|
||||
if n2 > 0.34:
|
||||
ladder_lines.append(
|
||||
[
|
||||
cyl_point(local_x + 0.08, a - angular_step * 0.35, lane_r + radial_depth * 0.35),
|
||||
cyl_point(local_x + 0.11, a + (n - 0.5) * angular_step, lane_r),
|
||||
cyl_point(local_x + 0.08, a + angular_step * 0.42, lane_r - radial_depth * 0.38),
|
||||
]
|
||||
)
|
||||
add_mesh_parts(prefix + "_annular_shelf_mesh", shelf_verts, shelf_faces, mats["favela"])
|
||||
add_mesh_parts(prefix + "_rickety_shack_mesh", poor_verts, poor_faces, mats["favela"])
|
||||
add_mesh_parts(prefix + "_midrise_housing_mesh", middle_verts, middle_faces, mats["city"])
|
||||
add_mesh_parts(prefix + "_surface_urban_crown_mesh", city_verts, city_faces, mats["luxury"])
|
||||
add_mesh_parts(prefix + "_patched_roof_mesh", roof_verts, roof_faces, mats["shack_roof"])
|
||||
add_multi_polyline_curve(prefix + "_ladders_nets_handlines", ladder_lines, 0.0055, mats["brace"], resolution=2)
|
||||
|
||||
|
||||
def add_hubward_office_complex(prefix, x, mats):
|
||||
office_verts, office_faces = [], []
|
||||
for i, radius in enumerate((0.42, 0.68, 0.92)):
|
||||
append_cylindrical_ring_band(office_verts, office_faces, x + 0.18 + i * 0.06, radius, radius + 0.16, segments=96)
|
||||
add_mesh_parts(prefix + "_ring_office_mesh", office_verts, office_faces, mats["hub"])
|
||||
for i, angle in enumerate([pymath.radians(a) for a in range(-150, 181, 30)]):
|
||||
add_cylinder_between(
|
||||
f"{prefix}_office_spoke_bridge_{i:02d}",
|
||||
cyl_point(x + 0.28, angle, 0.9),
|
||||
cyl_point(x + 0.18, angle, 1.22),
|
||||
0.012,
|
||||
mats["pressure_line"],
|
||||
vertices=6,
|
||||
)
|
||||
|
||||
|
||||
def add_town_detail_cluster(prefix, x_values, angles, material, road_material):
|
||||
verts, faces = [], []
|
||||
lanes = []
|
||||
for ix, x in enumerate(x_values):
|
||||
for ia, angle in enumerate(angles):
|
||||
root = f"{prefix}_{ix:02d}_{ia:02d}"
|
||||
for lane in (-1, 0, 1):
|
||||
local_angle = angle + lane * 0.024
|
||||
lanes.append(
|
||||
[
|
||||
surface_point(x - 0.32, local_angle - 0.014, INNER_RADIUS, lift=-0.37),
|
||||
surface_point(x, local_angle + 0.008, INNER_RADIUS, lift=-0.37),
|
||||
surface_point(x + 0.32, local_angle - 0.006, INNER_RADIUS, lift=-0.37),
|
||||
]
|
||||
)
|
||||
for b in range(3):
|
||||
dx = -0.22 + b * 0.22 + ((ix + ia + b) % 3 - 1) * 0.018
|
||||
da = lane * 0.026 + ((ix * 7 + ia * 5 + b) % 5 - 2) * 0.004
|
||||
height = 0.12 + 0.035 * ((ix + ia + b) % 4)
|
||||
append_box_parts(
|
||||
verts,
|
||||
faces,
|
||||
surface_point(x + dx, angle + da, INNER_RADIUS, lift=-0.3 - height * 0.5),
|
||||
((1, 0, 0), tangent(angle + da), radial(angle + da)),
|
||||
(0.12, 0.07, height),
|
||||
)
|
||||
add_mesh_parts(prefix + "_nested_house_mesh", verts, faces, material)
|
||||
add_multi_polyline_curve(prefix + "_lane_network", lanes, 0.006, road_material, resolution=2)
|
||||
|
||||
|
||||
def add_spoke_plaza_detail(prefix, x, angle, mats, seed):
|
||||
add_surface_curve(
|
||||
f"{prefix}_plaza_ring",
|
||||
[(x + 0.34 * pymath.cos(t), angle + 0.065 * pymath.sin(t)) for t in [TAU * i / 28 for i in range(29)]],
|
||||
INNER_RADIUS,
|
||||
0.01,
|
||||
mats["road"],
|
||||
lift=-0.42,
|
||||
)
|
||||
for i in range(10):
|
||||
t = TAU * i / 10 + seed * 0.13
|
||||
dx = 0.28 * pymath.cos(t)
|
||||
da = 0.052 * pymath.sin(t)
|
||||
add_surface_box(
|
||||
f"{prefix}_kiosk_{i:02d}",
|
||||
x + dx,
|
||||
angle + da,
|
||||
mats["luxury"],
|
||||
size=(0.09, 0.055, 0.1 + 0.025 * (i % 3)),
|
||||
radial_lift=-0.37,
|
||||
)
|
||||
for i, spoke_da in enumerate((-0.11, -0.055, 0.055, 0.11)):
|
||||
add_surface_curve(
|
||||
f"{prefix}_feeder_walk_{i:02d}",
|
||||
[(x - 0.45, angle + spoke_da), (x, angle + spoke_da * 0.3), (x + 0.45, angle - spoke_da * 0.6)],
|
||||
INNER_RADIUS,
|
||||
0.008,
|
||||
mats["pressure_line"],
|
||||
lift=-0.43,
|
||||
)
|
||||
|
||||
|
||||
def add_rotating_spoke(name, x, angle, inner_radius, outer_radius, material, radius=0.045, vertices=10):
|
||||
r = radial(angle)
|
||||
return add_cylinder_between(
|
||||
name,
|
||||
(x, r[1] * inner_radius, r[2] * inner_radius),
|
||||
(x, r[1] * outer_radius, r[2] * outer_radius),
|
||||
radius,
|
||||
material,
|
||||
vertices=vertices,
|
||||
)
|
||||
|
||||
|
||||
def add_frame_transfer_artery(name, x, angle, start_radius, end_radius, material, sweep=0.42, bevel=0.025):
|
||||
samples = []
|
||||
for i in range(9):
|
||||
t = i / 8
|
||||
a = angle + (t - 0.5) * sweep
|
||||
radius = start_radius + (end_radius - start_radius) * t
|
||||
samples.append((x + (t - 0.5) * 0.65, radius * pymath.cos(a), radius * pymath.sin(a)))
|
||||
return add_curve_polyline(name, samples, bevel, material)
|
||||
|
||||
|
||||
def add_spiral_spoke_loop(name, x, angle, start_radius, end_radius, material, bevel=0.04, cargo=False):
|
||||
"""Shared cylinder frame: x is axial, angle is shell azimuth, radius is spin radius."""
|
||||
samples = []
|
||||
handed = 1.0 if pymath.sin(angle * 2.0 + x * 0.37) >= 0 else -1.0
|
||||
tangent_bias = 0.68 if cargo else 0.42
|
||||
loop_gain = 0.58 if cargo else 0.34
|
||||
for i in range(22):
|
||||
u = i / 21
|
||||
e = smoothstep(u)
|
||||
radius = start_radius + (end_radius - start_radius) * e
|
||||
a = angle + handed * (tangent_bias * (1.0 - u) + loop_gain * pymath.sin(u * pymath.pi))
|
||||
x2 = x + handed * 0.62 * pymath.sin(u * pymath.pi) + 0.15 * pymath.sin(u * TAU * 2.0)
|
||||
samples.append(cyl_point(x2, a, radius))
|
||||
return add_curve_polyline(name, samples, bevel, material, resolution=4)
|
||||
|
||||
|
||||
def add_endcap_face(name, x, radius, material, segments=144):
|
||||
import bpy
|
||||
|
||||
mesh = bpy.data.meshes.new(name + "Mesh")
|
||||
verts = [(x, 0.0, 0.0)]
|
||||
verts.extend(cyl_point(x, FULL_START + TAU * i / segments, radius) for i in range(segments))
|
||||
faces = [(0, i + 1, 1 + ((i + 1) % segments)) for i in range(segments)]
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_endcap_ring(name, x, radius, material, minor_radius=0.025):
|
||||
import bpy
|
||||
|
||||
bpy.ops.mesh.primitive_torus_add(
|
||||
major_radius=radius,
|
||||
minor_radius=minor_radius,
|
||||
major_segments=128,
|
||||
minor_segments=8,
|
||||
location=(x, 0, 0),
|
||||
)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.data.name = name + "Mesh"
|
||||
obj.rotation_euler[1] = pymath.radians(90)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_endcap_terrace_blocks(prefix, x, radii, angles, material, height=0.18):
|
||||
for ir, radius in enumerate(radii):
|
||||
for ia, angle in enumerate(angles):
|
||||
loc = cyl_point(x, angle, radius)
|
||||
add_box(
|
||||
f"{prefix}_{ir:02d}_{ia:02d}",
|
||||
loc,
|
||||
axes=((1, 0, 0), tangent(angle), radial(angle)),
|
||||
size=(0.09, 0.16, height),
|
||||
material=material,
|
||||
)
|
||||
|
||||
|
||||
def add_label(name, text, loc, size, material):
|
||||
import bpy
|
||||
|
||||
bpy.ops.object.text_add(location=loc, rotation=(pymath.radians(70), 0.0, pymath.radians(0)))
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.data.body = text
|
||||
obj.data.align_x = "CENTER"
|
||||
obj.data.align_y = "CENTER"
|
||||
obj.data.size = size
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_gn_light_spine(material):
|
||||
import bpy
|
||||
|
||||
group = bpy.data.node_groups.get("VG Bloom Light Spine")
|
||||
mesh = bpy.data.meshes.new("BloomLightSpineCarrierMesh")
|
||||
obj = bpy.data.objects.new("GN_Light_Spine_Modifier_Carrier", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
mod = obj.modifiers.new("VG Bloom Light Spine", "NODES")
|
||||
mod.node_group = group
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def build_scene():
|
||||
import bpy
|
||||
|
||||
clear_scene()
|
||||
|
||||
mats = {
|
||||
"inner": mat("civic inner surface - open commons", (0.28, 0.30, 0.32), alpha=0.78),
|
||||
"utility": mat("utility mat amber", (0.95, 0.57, 0.18), alpha=0.42),
|
||||
"pressure": mat("pressure laminate blue", (0.18, 0.45, 0.86), alpha=0.2),
|
||||
"shield": mat("aggregate shielding graphite", (0.08, 0.075, 0.07), alpha=0.38),
|
||||
"hub": mat("despun hub white", (0.82, 0.86, 0.88), alpha=0.88),
|
||||
"endcap": mat("layered endcap pressure face", (0.62, 0.66, 0.7), alpha=0.62),
|
||||
"spire_sheath": mat("spun spire sheath", (0.38, 0.42, 0.46), alpha=0.34),
|
||||
"light": mat("light spine gold", (1.0, 0.78, 0.25), emission=True, strength=1.6),
|
||||
"farm": mat("TCS Root farms", (0.15, 0.55, 0.24), alpha=0.92),
|
||||
"district": mat("surface districts", (0.55, 0.58, 0.62), alpha=0.95),
|
||||
"favela": mat("hubcap terrace slums", (0.74, 0.48, 0.26), alpha=0.98),
|
||||
"shack_roof": mat("patched favela sheet metal", (0.46, 0.39, 0.32), alpha=0.98),
|
||||
"brace": mat("rickety brace dark steel", (0.12, 0.10, 0.085), alpha=0.98),
|
||||
"luxury": mat("luxury spoke districts", (0.82, 0.78, 0.62), alpha=0.98),
|
||||
"industrial": mat("industrial works", (0.32, 0.34, 0.36), alpha=0.98),
|
||||
"suburban": mat("suburban mixed belt", (0.5, 0.64, 0.5), alpha=0.96),
|
||||
"beach": mat("beach rim", (0.86, 0.76, 0.48), alpha=0.96),
|
||||
"water": mat("cylindrical sea", (0.05, 0.28, 0.55), alpha=0.82),
|
||||
"forest": mat("forest canopy", (0.04, 0.32, 0.1), alpha=0.92),
|
||||
"road": mat("road graph", (0.72, 0.66, 0.52), alpha=0.96),
|
||||
"city": mat("city clusters", (0.62, 0.64, 0.67), alpha=0.98),
|
||||
"cloud": mat("interior cloud layer", (0.8, 0.86, 0.9), alpha=0.24),
|
||||
"air": mat("air return cyan", (0.1, 0.85, 1.0), emission=True, strength=0.25),
|
||||
"coolant": mat("coolant blue", (0.08, 0.25, 1.0), emission=True, strength=0.2),
|
||||
"condensate": mat("condensate green", (0.15, 0.95, 0.55), emission=True, strength=0.15),
|
||||
"pressure_line": mat("pressure amber", (1.0, 0.65, 0.08), emission=True, strength=0.35),
|
||||
"hazard": mat("emergency red", (1.0, 0.12, 0.07), emission=True, strength=0.45),
|
||||
"label": mat("label white", (0.92, 0.95, 1.0), emission=True, strength=0.4),
|
||||
"glass": mat("operations glass", (0.45, 0.8, 1.0), alpha=0.28),
|
||||
}
|
||||
|
||||
add_shell_segment("01_open_civic_inner_surface_full_bloom", 5.0, AXIS_LENGTH / 2, 0.045, mats["inner"])
|
||||
add_shell_segment("02_utility_mat_service_interlayer_full_bloom", 5.35, AXIS_LENGTH / 2, 0.075, mats["utility"])
|
||||
add_shell_segment("03_pressure_structural_shell_full_bloom", 5.75, AXIS_LENGTH / 2, 0.11, mats["pressure"])
|
||||
add_shell_segment("04_outer_aggregate_shielding_full_bloom", 6.35, AXIS_LENGTH / 2, 0.28, mats["shield"])
|
||||
|
||||
add_cylinder_between("despun_center_axis_core", (-10.2, 0, 0), (10.2, 0, 0), 0.28, mats["hub"], vertices=32)
|
||||
add_cylinder_between("despun_hub_cap_docking_axis", (-10.85, 0, 0), (-9.25, 0, 0), 0.72, mats["hub"], vertices=48)
|
||||
add_cylinder_between("docking_port_on_despun_hub_cap", (-11.55, 0, 0), (-10.75, 0, 0), 0.36, mats["pressure_line"], vertices=32)
|
||||
add_cylinder_between("traffic_light_spire_core", (-9.2, 0, 0), (9.2, 0, 0), 0.13, mats["light"], vertices=16)
|
||||
add_cylinder_between("spun_spire_sheath_outer_frame", (-8.9, 0, 0), (8.9, 0, 0), 0.82, mats["spire_sheath"], vertices=48)
|
||||
add_gn_light_spine(mats["light"])
|
||||
|
||||
# The Bloom lives in one cylindrical coordinate space. Spokes, endcaps,
|
||||
# surface districts, roads, clouds, and transfer arteries all speak x/angle/radius.
|
||||
for x, prefix in [(-9.05, "hubward"), (9.05, "capward")]:
|
||||
add_endcap_face(f"{prefix}_endcap_pressure_face", x, 5.72, mats["endcap"])
|
||||
add_endcap_face(f"{prefix}_endcap_inner_face", x + (0.035 if x < 0 else -0.035), INNER_RADIUS, mats["inner"], segments=128)
|
||||
for ir, rr in enumerate((0.82, 1.6, 2.45, 3.25, 4.05, 4.75, 5.35)):
|
||||
mat_key = "favela" if x < 0 and ir >= 2 else "beach" if x > 0 and ir >= 4 else "pressure_line"
|
||||
add_endcap_ring(f"{prefix}_endcap_terrace_ring_{ir:02d}", x, rr, mats[mat_key], minor_radius=0.018 + ir * 0.002)
|
||||
add_hubward_office_complex("hubward_docking_hub_office_complex", -9.24, mats)
|
||||
add_hubward_endcap_terraced_slums("hubward_endcap_rice_paddy_slums", -9.18, mats)
|
||||
add_endcap_terrace_blocks(
|
||||
"capward_endcap_beach_service",
|
||||
9.12,
|
||||
[3.5, 4.1, 4.65],
|
||||
[pymath.radians(a) for a in range(-160, 181, 40)],
|
||||
mats["beach"],
|
||||
height=0.1,
|
||||
)
|
||||
|
||||
# Golden-angle spokes fill the cylinder like seeds on a head: each new
|
||||
# artery avoids the previous one while staying in the same global frame.
|
||||
golden_angle = pymath.pi * (3.0 - pymath.sqrt(5.0))
|
||||
spoke_count = 46
|
||||
prestige_angles = []
|
||||
for i in range(spoke_count):
|
||||
t = i / (spoke_count - 1)
|
||||
x = -8.25 + 16.5 * t
|
||||
a = ((i * golden_angle + 0.48 * pymath.sin(t * TAU * 3.0)) % TAU) - pymath.pi
|
||||
cargo = i % 3 == 0
|
||||
material = mats["pressure_line"] if cargo else mats["hub"]
|
||||
bevel = 0.052 if cargo else 0.036
|
||||
name = "spiral_cargo_spoke" if cargo else "spiral_passenger_spoke"
|
||||
add_spiral_spoke_loop(f"{name}_{i:02d}", x, a, 0.82, 5.18, material, bevel=bevel, cargo=cargo)
|
||||
add_cylinder_between(
|
||||
f"spiral_spoke_shell_collar_{i:02d}",
|
||||
cyl_point(x - 0.2, a, 5.05),
|
||||
cyl_point(x + 0.2, a, 5.05),
|
||||
0.12 if cargo else 0.085,
|
||||
mats["pressure_line"],
|
||||
vertices=18,
|
||||
)
|
||||
add_frame_transfer_artery(
|
||||
f"spun_to_despun_transfer_loop_{i:02d}",
|
||||
x,
|
||||
a + pymath.pi / 2,
|
||||
0.28,
|
||||
0.82,
|
||||
mats["pressure_line"],
|
||||
sweep=0.95 if cargo else 0.62,
|
||||
bevel=0.017,
|
||||
)
|
||||
if i % 5 == 0:
|
||||
prestige_angles.append(a)
|
||||
for i in range(64):
|
||||
t = i / 63
|
||||
x = -8.65 + 17.3 * t
|
||||
a = ((i * golden_angle * 1.618 + 0.72 * pymath.sin(t * TAU * 2.0)) % TAU) - pymath.pi
|
||||
add_spiral_spoke_loop(f"spiral_atmospheric_utility_rib_{i:02d}", x, a, 0.95, 5.55, mats["air"], bevel=0.015, cargo=False)
|
||||
|
||||
# Whole-cylinder civic gradient. Axial bands move from hubward transition
|
||||
# through dense and luxury urban districts, mixed suburbs, industrial/farm
|
||||
# belts, and finally a beach/sea edge.
|
||||
add_surface_patch("hubward_low_g_transition_band", -8.9, -7.35, FULL_START, FULL_END, INNER_RADIUS, mats["favela"], lift=-0.20, angle_steps=72, x_steps=3)
|
||||
add_surface_patch("hyperurban_favela_city_belt", -7.35, -5.6, FULL_START, FULL_END, INNER_RADIUS, mats["city"], lift=-0.18, angle_steps=72, x_steps=3)
|
||||
add_surface_patch("prestige_urban_luxury_spoke_belt", -5.6, -2.7, FULL_START, FULL_END, INNER_RADIUS, mats["luxury"], lift=-0.17, angle_steps=72, x_steps=4)
|
||||
add_surface_patch("urban_mixed_suburban_industrial_belt", -2.7, 0.9, FULL_START, FULL_END, INNER_RADIUS, mats["suburban"], lift=-0.18, angle_steps=72, x_steps=4)
|
||||
add_surface_patch("industrial_agricultural_belt", 0.9, 5.8, FULL_START, FULL_END, INNER_RADIUS, mats["farm"], lift=-0.16, angle_steps=72, x_steps=5)
|
||||
add_surface_patch("beach_resort_rim", 5.8, 7.05, FULL_START, FULL_END, INNER_RADIUS, mats["beach"], lift=-0.23, angle_steps=72, x_steps=3)
|
||||
add_surface_patch("cylindrical_sea_after_beach", 7.05, 8.85, FULL_START, FULL_END, INNER_RADIUS, mats["water"], lift=-0.25, angle_steps=72, x_steps=3)
|
||||
|
||||
dense_angles = [pymath.radians(a) for a in range(-165, 180, 15)]
|
||||
add_surface_block_field("hubward_spoke_access_market_stack", [-8.65, -8.25, -7.85, -7.45], dense_angles, mats["favela"], size=(0.16, 0.08, 0.18), radial_lift=-0.29, jitter=0.035)
|
||||
add_surface_block_field("hyperurban_microtower", [-7.0, -6.55, -6.1, -5.7], dense_angles[::2], mats["city"], size=(0.2, 0.1, 0.36), radial_lift=-0.42, jitter=0.03)
|
||||
add_town_detail_cluster(
|
||||
"hyperurban_nested_town",
|
||||
[-7.1, -6.65, -6.2, -5.75],
|
||||
[pymath.radians(a) for a in range(-150, 181, 45)],
|
||||
mats["city"],
|
||||
mats["road"],
|
||||
)
|
||||
for x in [-5.0, -4.2, -3.4]:
|
||||
for ia, a in enumerate(prestige_angles[:8]):
|
||||
add_surface_box(f"luxury_spoke_plaza_{x:.1f}_{a:.2f}", x, a, mats["luxury"], size=(0.55, 0.34, 0.14), radial_lift=-0.31)
|
||||
add_spoke_plaza_detail(f"luxury_spoke_plaza_detail_{x:.1f}_{ia:02d}", x, a, mats, ia)
|
||||
for da in (-0.16, 0.16):
|
||||
add_surface_box(f"luxury_spoke_tower_{x:.1f}_{a:.2f}_{da:.1f}", x + da, a + da, mats["city"], size=(0.18, 0.1, 0.42), radial_lift=-0.44)
|
||||
add_surface_block_field("mixed_suburban_industrial_block", [-2.2, -1.4, -0.6, 0.2], [pymath.radians(a) for a in range(-150, 181, 30)], mats["suburban"], size=(0.42, 0.18, 0.12), radial_lift=-0.27, jitter=0.04)
|
||||
add_town_detail_cluster(
|
||||
"mixed_belt_nested_town",
|
||||
[-2.35, -1.65, -0.95, -0.25, 0.45],
|
||||
[pymath.radians(a) for a in range(-150, 181, 60)],
|
||||
mats["suburban"],
|
||||
mats["road"],
|
||||
)
|
||||
add_surface_block_field("factory_yard_block", [1.2, 2.0, 2.8, 3.6], [pymath.radians(a) for a in range(-150, 181, 45)], mats["industrial"], size=(0.62, 0.22, 0.16), radial_lift=-0.29, jitter=0.035)
|
||||
add_surface_block_field("farm_service_shed", [4.2, 5.0, 5.6], [pymath.radians(a) for a in range(-165, 181, 45)], mats["farm"], size=(0.52, 0.16, 0.08), radial_lift=-0.24, jitter=0.04)
|
||||
for i, deg in enumerate(range(-170, 181, 20)):
|
||||
a = pymath.radians(deg + (i % 3) * 3)
|
||||
add_surface_curve(f"axial_surface_service_lane_{i:02d}", [(-8.7, a), (-5.5, a + 0.08), (-1.4, a - 0.04), (2.9, a + 0.06), (8.6, a - 0.02)], INNER_RADIUS, 0.007, mats["pressure_line"], lift=-0.43)
|
||||
for i, x in enumerate([-8.65, -7.25, -5.85, -4.45, -3.05, -1.65, -0.25, 1.15, 2.55, 3.95, 5.35, 6.75, 8.15]):
|
||||
add_surface_curve(f"circumferential_market_seam_{i:02d}", [(x, pymath.radians(a)) for a in range(-180, 181, 18)], INNER_RADIUS, 0.006, mats["road"], lift=-0.45)
|
||||
|
||||
road_specs = [
|
||||
[(-8.7, -160), (-7.2, -130), (-5.6, -94), (-3.0, -58), (-1.0, -34), (1.6, -14), (4.4, 8), (6.7, 26), (8.4, 44)],
|
||||
[(-8.4, 140), (-6.7, 118), (-4.9, 92), (-2.0, 72), (0.8, 62), (3.2, 74), (5.4, 94), (8.1, 122)],
|
||||
[(-8.2, -26), (-6.0, -8), (-3.6, 18), (-1.0, 42), (1.8, 50), (4.2, 38), (6.4, 20), (8.4, -2)],
|
||||
[(-8.6, 12), (-6.2, 32), (-4.2, 48), (-1.8, 38), (0.4, 12), (2.8, -22), (5.5, -52), (8.2, -84)],
|
||||
]
|
||||
for i, spec in enumerate(road_specs):
|
||||
add_surface_curve(f"wrapped_road_graph_{i}", [(x, pymath.radians(a)) for x, a in spec], INNER_RADIUS, 0.016, mats["road"], lift=-0.39)
|
||||
river_specs = [
|
||||
[(-8.6, 82), (-6.5, 68), (-4.6, 42), (-2.2, 22), (0.8, 8), (3.8, -2), (6.1, -12), (8.6, -24)],
|
||||
[(-8.2, -112), (-5.6, -92), (-3.0, -72), (-0.2, -58), (2.6, -46), (5.2, -38), (8.4, -34)],
|
||||
]
|
||||
for i, spec in enumerate(river_specs):
|
||||
add_surface_curve(f"wrapped_river_{i}", [(x, pymath.radians(a)) for x, a in spec], INNER_RADIUS, 0.035, mats["water"], lift=-0.34)
|
||||
for i, (x, a, sx) in enumerate([(-6.9, -120, 0.8), (-5.2, 75, 0.7), (-2.4, 15, 0.9), (0.9, -54, 0.8), (3.4, 88, 0.9), (5.8, -12, 1.0)]):
|
||||
add_surface_patch(f"airflow_cloud_patch_{i}", x - sx, x + sx, pymath.radians(a - 12), pymath.radians(a + 12), 3.25, mats["cloud"], lift=0.0, angle_steps=10, x_steps=3)
|
||||
|
||||
# Service and heat/air/water flows running below the public surface.
|
||||
for deg, material, label in [
|
||||
(-112, mats["air"], "inward warm return"),
|
||||
(-105, mats["coolant"], "coolant trunk"),
|
||||
(-98, mats["condensate"], "condensate recovery"),
|
||||
(105, mats["pressure_line"], "pressure reserve"),
|
||||
(112, mats["hazard"], "emergency isolation"),
|
||||
]:
|
||||
a = pymath.radians(deg)
|
||||
r = radial(a)
|
||||
add_curve_polyline(
|
||||
label.replace(" ", "_"),
|
||||
[(-8.6, r[1] * 5.42, r[2] * 5.42), (8.6, r[1] * 5.42, r[2] * 5.42)],
|
||||
0.035,
|
||||
material,
|
||||
)
|
||||
|
||||
# Kappa service ring: kept as one small local maintenance marker, not the point of the scene.
|
||||
kappa_x = -2.7
|
||||
kappa_angle = pymath.radians(-45)
|
||||
kr = radial(kappa_angle)
|
||||
kt = tangent(kappa_angle)
|
||||
service_center = (kappa_x, kr[1] * 5.22, kr[2] * 5.22)
|
||||
add_box("service_ring_kappa_human_gallery", service_center, axes=((1, 0, 0), kt, kr), size=(2.4, 0.34, 0.16), material=mats["utility"])
|
||||
add_box(
|
||||
"kappa_operations_gallery_badge_route",
|
||||
(kappa_x - 0.25, kr[1] * 4.72, kr[2] * 4.72 + 0.28),
|
||||
axes=((1, 0, 0), kt, kr),
|
||||
size=(1.7, 0.24, 0.18),
|
||||
material=mats["glass"],
|
||||
)
|
||||
add_box(
|
||||
"octopoid_support_rig_prep_station",
|
||||
(kappa_x + 1.35, kr[1] * 5.05, kr[2] * 5.05 - 0.12),
|
||||
axes=((1, 0, 0), kt, kr),
|
||||
size=(0.7, 0.32, 0.2),
|
||||
material=mats["hazard"],
|
||||
)
|
||||
|
||||
# Crawl throats branch outward into the shell. Seal lungs live beyond them.
|
||||
for i, offset in enumerate([-0.62, 0.0, 0.62], start=1):
|
||||
base = (kappa_x + offset, kr[1] * 5.36, kr[2] * 5.36)
|
||||
end = (kappa_x + offset, kr[1] * 6.14, kr[2] * 6.14)
|
||||
add_cylinder_between(f"kappa_crawl_throat_{i}", base, end, 0.055, mats["hazard"], vertices=10)
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=12, radius=0.23, location=(kappa_x + offset, kr[1] * 6.0, kr[2] * 6.0))
|
||||
lung = bpy.context.object
|
||||
lung.name = f"kappa_seal_lung_{i}"
|
||||
lung.scale = (0.8, 1.0, 0.45)
|
||||
lung.data.materials.append(mats["pressure_line"])
|
||||
|
||||
manifold_base = (kappa_x - 0.05, kr[1] * 5.48, kr[2] * 5.48)
|
||||
for dz, material, name in [
|
||||
(-0.12, mats["air"], "air_return_to_filters"),
|
||||
(0.0, mats["coolant"], "coolant_heat_exchanger"),
|
||||
(0.12, mats["condensate"], "condensate_drain"),
|
||||
]:
|
||||
add_curve_polyline(
|
||||
"kappa_" + name,
|
||||
[
|
||||
(manifold_base[0] - 1.0, manifold_base[1], manifold_base[2] + dz),
|
||||
(manifold_base[0], manifold_base[1] + kt[1] * 0.55, manifold_base[2] + kt[2] * 0.55 + dz),
|
||||
(manifold_base[0] + 1.0, manifold_base[1], manifold_base[2] + dz),
|
||||
],
|
||||
0.025,
|
||||
material,
|
||||
)
|
||||
|
||||
bpy.ops.object.light_add(type="AREA", location=(-1.6, 6.7, -2.0))
|
||||
kappa_light = bpy.context.object
|
||||
kappa_light.name = "Kappa colored service inspection light"
|
||||
kappa_light.data.energy = 360
|
||||
kappa_light.data.size = 4.0
|
||||
|
||||
# Labels are sparse: only the relationships the lore says a blueprint must show.
|
||||
add_label("label_inward", "INWARD: open air / light spine / hub", (5.8, 1.2, 1.0), 0.23, mats["label"])
|
||||
add_label("label_outward", "OUTWARD: utility mat -> pressure shell -> shielding", (-6.0, -3.8, -4.1), 0.21, mats["label"])
|
||||
add_label("label_regions", "mapped regions: sea, plains, cities, fields, forests, roads, rivers", (-3.4, 3.8, 3.5), 0.16, mats["label"])
|
||||
add_label("label_kappa", "one service-ring marker among many", (kappa_x, -4.2, -4.0), 0.14, mats["label"])
|
||||
add_label("label_spokes", "spoke interfaces: transit, utilities, authority", (1.8, 2.2, 3.2), 0.18, mats["label"])
|
||||
|
||||
# Lighting and camera.
|
||||
bpy.ops.object.light_add(type="AREA", location=(1.5, -6.5, 7.0))
|
||||
key = bpy.context.object
|
||||
key.name = "large soft inspection light"
|
||||
key.data.energy = 650
|
||||
key.data.size = 6.5
|
||||
bpy.ops.object.camera_add(location=(15.5, -19.0, 12.5))
|
||||
bpy.context.scene.camera = bpy.context.object
|
||||
bpy.context.object.name = "Camera_Bloom_Cutaway"
|
||||
aim_camera(bpy.context.object, (0.0, 0.0, 0.0))
|
||||
bpy.context.object.data.type = "ORTHO"
|
||||
bpy.context.object.data.ortho_scale = 20.5
|
||||
|
||||
bpy.ops.object.camera_add(location=(-0.8, 9.5, -7.5))
|
||||
detail_camera = bpy.context.object
|
||||
detail_camera.name = "Camera_Interior_World"
|
||||
aim_camera(detail_camera, (0.0, 0.0, 0.0))
|
||||
detail_camera.data.type = "ORTHO"
|
||||
detail_camera.data.ortho_scale = 9.0
|
||||
|
||||
bpy.ops.object.camera_add(location=(-13.5, 0.45, 0.35))
|
||||
cap_camera = bpy.context.object
|
||||
cap_camera.name = "Camera_Hubward_Endcap_Terraces"
|
||||
aim_camera(cap_camera, (-9.05, 0.0, 0.0))
|
||||
cap_camera.data.type = "ORTHO"
|
||||
cap_camera.data.ortho_scale = 6.6
|
||||
|
||||
bpy.context.scene.render.engine = "BLENDER_EEVEE"
|
||||
if hasattr(bpy.context.scene, "eevee"):
|
||||
bpy.context.scene.eevee.taa_render_samples = 24
|
||||
bpy.context.scene.render.resolution_x = 1400
|
||||
bpy.context.scene.render.resolution_y = 1000
|
||||
bpy.context.scene.view_settings.view_transform = "Filmic"
|
||||
bpy.context.scene.view_settings.look = "Medium High Contrast"
|
||||
|
||||
|
||||
def main():
|
||||
import bpy
|
||||
|
||||
# Force Geometry Script group creation before scene objects reference it.
|
||||
vg_bloom_light_spine()
|
||||
build_scene()
|
||||
|
||||
out_dir = Path("experiments/generated/aetheria_bloom")
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
blend_path = out_dir / "aetheria_bloom_habitat.blend"
|
||||
render_path = out_dir / "aetheria_bloom_habitat.png"
|
||||
detail_render_path = out_dir / "aetheria_bloom_interior_world.png"
|
||||
cap_render_path = out_dir / "aetheria_bloom_hubward_endcap_terraces.png"
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path.resolve()))
|
||||
bpy.context.scene.camera = bpy.data.objects["Camera_Bloom_Cutaway"]
|
||||
bpy.context.scene.render.filepath = str(render_path.resolve())
|
||||
bpy.ops.render.render(write_still=True)
|
||||
bpy.context.scene.camera = bpy.data.objects["Camera_Interior_World"]
|
||||
bpy.context.scene.render.filepath = str(detail_render_path.resolve())
|
||||
bpy.ops.render.render(write_still=True)
|
||||
bpy.context.scene.camera = bpy.data.objects["Camera_Hubward_Endcap_Terraces"]
|
||||
bpy.context.scene.render.filepath = str(cap_render_path.resolve())
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print("AETHERIA_BLOOM_BLEND", blend_path.resolve())
|
||||
print("AETHERIA_BLOOM_RENDER", render_path.resolve())
|
||||
print("AETHERIA_BLOOM_INTERIOR_RENDER", detail_render_path.resolve())
|
||||
print("AETHERIA_BLOOM_HUBWARD_ENDCAP_RENDER", cap_render_path.resolve())
|
||||
group = bpy.data.node_groups.get("VG Bloom Light Spine")
|
||||
if group:
|
||||
print("AETHERIA_BLOOM_GN_GROUP", group.name, len(group.nodes), len(group.links))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
"""Geometry Script footholds from Blender's index_of_nearest demo."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
|
||||
|
||||
@tree("VG Boundary Step")
|
||||
def vg_boundary_step(
|
||||
geometry: Geometry,
|
||||
vector: Vector = (0.0, 0.0, 0.0),
|
||||
b: Float = 0.05000000074505806,
|
||||
):
|
||||
from_center = vector_math(operation=VectorMath.Operation.SUBTRACT, vector=(position(), vector))
|
||||
distance = vector_math(operation=VectorMath.Operation.LENGTH, vector=from_center)
|
||||
outside = compare(
|
||||
operation=Compare.Operation.GREATER_THAN,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=distance,
|
||||
b=b,
|
||||
)
|
||||
clamped_xy = vector_math(
|
||||
operation=VectorMath.Operation.MULTIPLY,
|
||||
vector=(vector_math(operation=VectorMath.Operation.SCALE, vector=vector_math(operation=VectorMath.Operation.NORMALIZE, vector=from_center), scale=b), (1.0, 1.0, 0.0)),
|
||||
)
|
||||
return {"Geometry": set_position(geometry=geometry, selection=outside, position=clamped_xy, offset=(0.0, 0.0, 0.0))}
|
||||
|
||||
|
||||
@tree("VG Update Velocity")
|
||||
def vg_update_velocity(
|
||||
current_velocity: Vector = (0.0, 0.0, 0.0),
|
||||
last_position: Vector = (0.0, 0.0, 0.0),
|
||||
):
|
||||
movement = vector_math(operation=VectorMath.Operation.SUBTRACT, vector=(position(), last_position))
|
||||
damped_movement = vector_math(operation=VectorMath.Operation.SCALE, vector=movement, scale=0.9599999785423279)
|
||||
planar_movement = vector_math(operation=VectorMath.Operation.MULTIPLY, vector=(damped_movement, (1.0, 1.0, 0.0)))
|
||||
fast_enough = compare(
|
||||
operation=Compare.Operation.GREATER_THAN,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=vector_math(operation=VectorMath.Operation.LENGTH, vector=planar_movement),
|
||||
b=0.009999999776482582,
|
||||
)
|
||||
blended = vector_math(
|
||||
operation=VectorMath.Operation.ADD,
|
||||
vector=(
|
||||
vector_math(operation=VectorMath.Operation.SCALE, vector=current_velocity, scale=0.75),
|
||||
vector_math(operation=VectorMath.Operation.SCALE, vector=planar_movement, scale=0.25),
|
||||
),
|
||||
)
|
||||
capped = vector_math(
|
||||
operation=VectorMath.Operation.SCALE,
|
||||
vector=vector_math(operation=VectorMath.Operation.NORMALIZE, vector=damped_movement),
|
||||
scale=0.009999999776482582,
|
||||
)
|
||||
return {"velocity": switch(input_type=Switch.InputType.VECTOR, switch=fast_enough, false=blended, true=capped)}
|
||||
|
||||
|
||||
@tree("VG Collision Step")
|
||||
def vg_collision_step(geometry: Geometry):
|
||||
current_position = position()
|
||||
neighbor = index_of_nearest(position=position())
|
||||
neighbor_position = evaluate_at_index(
|
||||
domain="POINT",
|
||||
data_type="FLOAT_VECTOR",
|
||||
value=position(),
|
||||
index=neighbor.index,
|
||||
)
|
||||
from_neighbor = vector_math(operation=VectorMath.Operation.SUBTRACT, vector=(current_position, neighbor_position))
|
||||
too_close = compare(
|
||||
operation=Compare.Operation.LESS_THAN,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=vector_math(operation=VectorMath.Operation.LENGTH, vector=from_neighbor),
|
||||
b=radius(),
|
||||
)
|
||||
push_strength = math(operation=Math.Operation.MULTIPLY, value=(too_close, 0.02500000037252903))
|
||||
push = vector_math(
|
||||
operation=VectorMath.Operation.MULTIPLY,
|
||||
vector=(vector_math(operation=VectorMath.Operation.SCALE, vector=from_neighbor, scale=push_strength), (1.0, 1.0, 0.0)),
|
||||
)
|
||||
return {"Geometry": set_position(geometry=geometry, offset=push)}
|
||||
|
||||
|
||||
@tree("VG Collider Step")
|
||||
def vg_collider_step(geometry: Geometry, collider: Object = None):
|
||||
collider_info = object_info(transform_space="ORIGINAL", object=collider, as_instance=False)
|
||||
from_collider = vector_math(operation=VectorMath.Operation.SUBTRACT, vector=(position(), collider_info.location))
|
||||
collider_radius = vector_math(operation=VectorMath.Operation.ABSOLUTE, vector=collider_info.scale)
|
||||
inside = compare(
|
||||
operation=Compare.Operation.LESS_THAN,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=vector_math(operation=VectorMath.Operation.LENGTH, vector=from_collider),
|
||||
b=collider_radius,
|
||||
)
|
||||
clamped = vector_math(
|
||||
operation=VectorMath.Operation.ADD,
|
||||
vector=(
|
||||
vector_math(
|
||||
operation=VectorMath.Operation.SCALE,
|
||||
vector=vector_math(operation=VectorMath.Operation.NORMALIZE, vector=from_collider),
|
||||
scale=collider_radius,
|
||||
),
|
||||
collider_info.location,
|
||||
),
|
||||
)
|
||||
planar_clamped = vector_math(operation=VectorMath.Operation.MULTIPLY, vector=(clamped, (1.0, 1.0, 0.0)))
|
||||
return {"Geometry": set_position(geometry=geometry, selection=inside, position=planar_clamped, offset=(0.0, 0.0, 0.0))}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in (
|
||||
"VG Boundary Step",
|
||||
"VG Update Velocity",
|
||||
"VG Collision Step",
|
||||
"VG Collider Step",
|
||||
):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if group:
|
||||
group.use_fake_user = True
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_INDEX_OF_NEAREST_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
"""Geometry Script recreation of Blender's instance_attribtues.blend demo graph."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from geometry_script.api.node_mapper import OutputsList, set_or_create_link
|
||||
from geometry_script.api.state import State
|
||||
from geometry_script.api.types import Type
|
||||
|
||||
|
||||
def _capture_attribute_item(data_type: str, domain: str, geometry, value, name: str):
|
||||
node = State.current_node_tree.nodes.new("GeometryNodeCaptureAttribute")
|
||||
node.capture_items.new(data_type, name)
|
||||
node.active_index = 0
|
||||
node.domain = domain
|
||||
set_or_create_link(geometry, node.inputs[0])
|
||||
set_or_create_link(value, node.inputs[1])
|
||||
return OutputsList({"geometry": Type(node.outputs[0]), name.lower().replace(" ", "_"): Type(node.outputs[1])})
|
||||
|
||||
|
||||
@tree("VG Grass Tuft Generator")
|
||||
def vg_grass_tuft_generator(
|
||||
geometry: Geometry,
|
||||
height: Float = 0.20000000298023224,
|
||||
radius: Float = 0.05000000074505806,
|
||||
density: Float = 1.0,
|
||||
thickness: Float = 0.009999999776482582,
|
||||
curl: Float = 0.5,
|
||||
seed: Int = 0,
|
||||
):
|
||||
del geometry
|
||||
|
||||
base_disc = mesh_circle(fill_type=MeshCircle.FillType.TRIANGLE_FAN, vertices=12, radius=radius)
|
||||
scattered_surface = subdivide_mesh(mesh=base_disc, level=3)
|
||||
radial_distance = vector_math(operation=VectorMath.Operation.LENGTH, vector=position())
|
||||
radial_falloff = map_range(
|
||||
clamp=True,
|
||||
interpolation_type="SMOOTHSTEP",
|
||||
data_type="FLOAT",
|
||||
value=radial_distance,
|
||||
from_min=0.0,
|
||||
from_max=radius,
|
||||
to_min=1.0,
|
||||
to_max=0.38999998569488525,
|
||||
)
|
||||
points = distribute_points_on_faces(
|
||||
distribute_method="POISSON",
|
||||
use_legacy_normal=True,
|
||||
mesh=scattered_surface,
|
||||
distance_min=math(
|
||||
operation=Math.Operation.DIVIDE,
|
||||
value=(math(operation=Math.Operation.MULTIPLY, value=(thickness, 2.0)), density),
|
||||
),
|
||||
density_max=math(operation=Math.Operation.MULTIPLY, value=(density, 5000.0)),
|
||||
density_factor=radial_falloff,
|
||||
seed=seed,
|
||||
)
|
||||
|
||||
blade_spine = mesh_line(
|
||||
mode=MeshLine.Mode.END_POINTS,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=5,
|
||||
start_location=(0.0, 0.0, 0.0),
|
||||
offset=combine_xyz(x=0.0, y=0.0, z=height),
|
||||
)
|
||||
blade_curve = mesh_to_curve(mode="EDGES", mesh=blade_spine)
|
||||
taper = math(operation=Math.Operation.SUBTRACT, value=(1.0, spline_parameter().factor))
|
||||
tapered_blade = set_curve_radius(curve=blade_curve, radius=taper)
|
||||
|
||||
instances = instance_on_points(
|
||||
points=points.points,
|
||||
instance=tapered_blade,
|
||||
pick_instance=False,
|
||||
rotation=(0.0, 0.0, 0.0),
|
||||
scale=(1.0, 1.0, 1.0),
|
||||
)
|
||||
scaled_instances = scale_instances(
|
||||
instances=instances,
|
||||
scale=radial_falloff,
|
||||
center=(0.0, 0.0, 0.0),
|
||||
local_space=True,
|
||||
)
|
||||
roots = _capture_attribute_item("VECTOR", "INSTANCE", scaled_instances, position(), "Value")
|
||||
realized_blades = realize_instances(
|
||||
realize_to_point_domain=True,
|
||||
geometry=roots.geometry,
|
||||
realize_all=True,
|
||||
depth=0,
|
||||
)
|
||||
|
||||
root_position = roots.value
|
||||
tangent_axis = vector_rotate(
|
||||
rotation_type="Z_AXIS",
|
||||
invert=False,
|
||||
vector=vector_math(operation=VectorMath.Operation.NORMALIZE, vector=root_position),
|
||||
center=(0.0, 0.0, 0.0),
|
||||
angle=1.5707963705062866,
|
||||
)
|
||||
root_distance = vector_math(operation=VectorMath.Operation.LENGTH, vector=root_position)
|
||||
curl_falloff = map_range(
|
||||
clamp=True,
|
||||
interpolation_type="SMOOTHSTEP",
|
||||
data_type="FLOAT",
|
||||
value=root_distance,
|
||||
from_min=0.0,
|
||||
from_max=radius,
|
||||
to_min=0.20000000298023224,
|
||||
to_max=2.0,
|
||||
)
|
||||
curl_angle = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.MULTIPLY, value=(spline_parameter().factor, curl)), curl_falloff),
|
||||
)
|
||||
curled_position = vector_rotate(
|
||||
rotation_type="AXIS_ANGLE",
|
||||
invert=False,
|
||||
vector=position(),
|
||||
center=root_position,
|
||||
axis=tangent_axis,
|
||||
angle=curl_angle,
|
||||
)
|
||||
curled_blades = set_position(geometry=realized_blades, position=curled_position, offset=(0.0, 0.0, 0.0))
|
||||
|
||||
profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=3, radius=thickness)
|
||||
radius_attribute = named_attribute(data_type=NamedAttribute.DataType.FLOAT, name="radius")
|
||||
radius_scale = switch(input_type=Switch.InputType.FLOAT, switch=radius_attribute.exists, false=1.0, true=radius_attribute.attribute)
|
||||
mesh = curve_to_mesh(curve=curled_blades, profile_curve=profile, scale=radius_scale, fill_caps=False)
|
||||
smooth_mesh = set_shade_smooth(domain=SetShadeSmooth.Domain.FACE, mesh=mesh, shade_smooth=False)
|
||||
return {"Geometry": set_material(geometry=smooth_mesh, material=None)}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in ("VG Grass Tuft Generator",):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if group:
|
||||
group.use_fake_user = True
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_INSTANCE_ATTRIBUTES_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
"""Geometry Script recreation of Blender's shortest-path labyrinth demo."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from geometry_script.api.node_mapper import OutputsList, set_or_create_link
|
||||
from geometry_script.api.state import State
|
||||
from geometry_script.api.types import Type
|
||||
|
||||
|
||||
def _capture_attribute_item(data_type: str, domain: str, geometry, value, name: str):
|
||||
node = State.current_node_tree.nodes.new("GeometryNodeCaptureAttribute")
|
||||
node.capture_items.new(data_type, name)
|
||||
node.active_index = 0
|
||||
node.domain = domain
|
||||
set_or_create_link(geometry, node.inputs[0])
|
||||
set_or_create_link(value, node.inputs[1])
|
||||
return OutputsList({"geometry": Type(node.outputs[0]), name.lower().replace(" ", "_").replace("/", "_"): Type(node.outputs[1])})
|
||||
|
||||
|
||||
@tree("VG Solvable Labyrinth Generator")
|
||||
def vg_solvable_labyrinth_generator(
|
||||
geometry: Geometry,
|
||||
size: Int = 20,
|
||||
seed: Int = 247,
|
||||
solve: Float = 1.0,
|
||||
):
|
||||
del geometry
|
||||
|
||||
base = grid(size_x=1.0, size_y=1.0, vertices_x=size, vertices_y=size)
|
||||
base_with_uv = store_named_attribute(
|
||||
data_type=StoreNamedAttribute.DataType.FLOAT_VECTOR,
|
||||
domain=StoreNamedAttribute.Domain.CORNER,
|
||||
geometry=base.mesh,
|
||||
name="uv_map",
|
||||
value=base.uv_map,
|
||||
)
|
||||
|
||||
start_vertex = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=index(),
|
||||
b=0,
|
||||
)
|
||||
sparse_edges = compare(
|
||||
operation=Compare.Operation.LESS_THAN,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=edge_neighbors(),
|
||||
b=2,
|
||||
)
|
||||
random_cost = random_value(data_type=RandomValue.DataType.BOOLEAN, probability=0.5, seed=seed)
|
||||
edge_cost = switch(input_type=Switch.InputType.FLOAT, switch=sparse_edges, false=random_cost, true=0.0)
|
||||
random_paths = shortest_edge_paths(end_vertex=start_vertex, edge_cost=edge_cost)
|
||||
random_path_selection = edge_paths_to_selection(next_vertex_index=random_paths.next_vertex_index)
|
||||
captured_maze_edges = _capture_attribute_item("BOOLEAN", "EDGE", base_with_uv, random_path_selection, "Path")
|
||||
|
||||
subdivided = subdivide_mesh(mesh=captured_maze_edges.geometry, level=1)
|
||||
carved_walkable = delete_geometry(
|
||||
mode=DeleteGeometry.Mode.ALL,
|
||||
domain=DeleteGeometry.Domain.POINT,
|
||||
geometry=subdivided,
|
||||
selection=captured_maze_edges.path,
|
||||
)
|
||||
|
||||
parts = separate_xyz(vector=position())
|
||||
direct_paths = shortest_edge_paths(end_vertex=start_vertex)
|
||||
direct_selection = edge_paths_to_selection(next_vertex_index=direct_paths.next_vertex_index)
|
||||
x_stats_on_path = attribute_statistic(
|
||||
data_type=AttributeStatistic.DataType.FLOAT,
|
||||
domain=AttributeStatistic.Domain.POINT,
|
||||
geometry=carved_walkable,
|
||||
selection=direct_selection,
|
||||
attribute=parts.x,
|
||||
)
|
||||
far_x_vertices = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=parts.x,
|
||||
b=x_stats_on_path.max,
|
||||
epsilon=0.0010000000474974513,
|
||||
)
|
||||
solution_curves = edge_paths_to_curves(
|
||||
mesh=carved_walkable,
|
||||
start_vertices=far_x_vertices,
|
||||
next_vertex_index=direct_paths.next_vertex_index,
|
||||
)
|
||||
|
||||
marker_radius = math(operation=Math.Operation.DIVIDE, value=(0.10000000149011612, size))
|
||||
marker_mesh = ico_sphere(radius=marker_radius, subdivisions=3)
|
||||
marker_with_uv = store_named_attribute(
|
||||
data_type=StoreNamedAttribute.DataType.FLOAT_VECTOR,
|
||||
domain=StoreNamedAttribute.Domain.CORNER,
|
||||
geometry=marker_mesh.mesh,
|
||||
name="UVMap",
|
||||
value=marker_mesh.uv_map,
|
||||
)
|
||||
endpoint_markers = instance_on_points(
|
||||
points=solution_curves,
|
||||
selection=endpoint_selection(start_size=1, end_size=1),
|
||||
instance=marker_with_uv,
|
||||
pick_instance=False,
|
||||
rotation=(0.0, 0.0, 0.0),
|
||||
scale=3.0,
|
||||
)
|
||||
marker_indices = _capture_attribute_item("INT", "INSTANCE", endpoint_markers, index(), "Start End")
|
||||
realized_markers = realize_instances(
|
||||
realize_to_point_domain=True,
|
||||
geometry=marker_indices.geometry,
|
||||
realize_all=True,
|
||||
depth=0,
|
||||
)
|
||||
marker_geometry = set_material(geometry=realized_markers, material=None)
|
||||
|
||||
trimmed_solution = trim_curve(mode=TrimCurve.Mode.FACTOR, curve=solution_curves, start=0.0, end=solve)
|
||||
captured_solution = _capture_attribute_item("BOOLEAN", "CURVE", trimmed_solution, True, "Path")
|
||||
maze_walls_source = delete_geometry(
|
||||
mode=DeleteGeometry.Mode.ALL,
|
||||
domain=DeleteGeometry.Domain.POINT,
|
||||
geometry=subdivided,
|
||||
selection=boolean_math(operation=BooleanMath.Operation.NOT, boolean=captured_maze_edges.path),
|
||||
)
|
||||
wall_curves = mesh_to_curve(mode="EDGES", mesh=maze_walls_source)
|
||||
all_curves = join_geometry(geometry=[captured_solution.geometry, wall_curves])
|
||||
filleted = fillet_curve(curve=all_curves, radius=0.004999999888241291, limit_radius=True, mode="Poly", count=6)
|
||||
curve_radius = switch(input_type=Switch.InputType.FLOAT, switch=captured_solution.path, false=2.0, true=1.0)
|
||||
radius_curves = set_curve_radius(curve=filleted, radius=curve_radius)
|
||||
|
||||
smoothed_marker = set_shade_smooth(domain=SetShadeSmooth.Domain.FACE, mesh=marker_with_uv, shade_smooth=True)
|
||||
curve_endpoint_markers = instance_on_points(
|
||||
points=radius_curves,
|
||||
selection=endpoint_selection(start_size=1, end_size=1),
|
||||
instance=smoothed_marker,
|
||||
pick_instance=False,
|
||||
rotation=(0.0, 0.0, 0.0),
|
||||
scale=radius(),
|
||||
)
|
||||
realized_curve_markers = realize_instances(
|
||||
realize_to_point_domain=True,
|
||||
geometry=curve_endpoint_markers,
|
||||
realize_all=True,
|
||||
depth=0,
|
||||
)
|
||||
profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=16, radius=marker_radius)
|
||||
radius_attribute = named_attribute(data_type=NamedAttribute.DataType.FLOAT, name="radius")
|
||||
profile_scale = switch(input_type=Switch.InputType.FLOAT, switch=radius_attribute.exists, false=1.0, true=radius_attribute.attribute)
|
||||
path_mesh = curve_to_mesh(curve=radius_curves, profile_curve=profile, scale=profile_scale, fill_caps=False)
|
||||
curve_geometry = set_material(geometry=join_geometry(geometry=[realized_curve_markers, path_mesh]), material=None)
|
||||
base_geometry = set_material(geometry=base_with_uv, material=None)
|
||||
return {
|
||||
"Geometry": join_geometry(geometry=[marker_geometry, curve_geometry, base_geometry]),
|
||||
"Path": captured_solution.path,
|
||||
"Start/End": marker_indices.start_end,
|
||||
}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in ("VG Solvable Labyrinth Generator",):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if group:
|
||||
group.use_fake_user = True
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_LABYRINTH_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
"""Geometry Script footholds from Blender's raycast minigame demo."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
|
||||
|
||||
@tree("VG Initial Direction")
|
||||
def vg_initial_direction(rotation: Vector = (0.0, 0.0, 0.0)):
|
||||
forward = combine_xyz(x=1.0, y=0.0, z=0.0)
|
||||
return {
|
||||
"Vector": vector_rotate(
|
||||
rotation_type="EULER_XYZ",
|
||||
invert=False,
|
||||
vector=forward,
|
||||
center=(0.0, 0.0, 0.0),
|
||||
rotation=rotation,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@tree("VG Line To Be Casted")
|
||||
def vg_line_to_be_casted(
|
||||
start_pos: Vector = (0.0, 0.0, 0.0),
|
||||
line_size: Int = 3,
|
||||
):
|
||||
seed_line = curve_line(mode=CurveLine.Mode.POINTS, start=(0.0, 0.0, 0.0), end=(0.0, 0.0, 0.0))
|
||||
placed = set_position(geometry=seed_line, position=start_pos, offset=(0.0, 0.0, 0.0))
|
||||
return {
|
||||
"Line": resample_curve(
|
||||
keep_last_segment=False,
|
||||
curve=placed,
|
||||
mode="Count",
|
||||
count=line_size,
|
||||
length=0.10000000149011612,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@tree("VG Cast Rays")
|
||||
def vg_cast_rays(
|
||||
line: Geometry,
|
||||
ray_direction: Vector = (0.0, 0.0, 0.0),
|
||||
traveled_distance: Float = 0.0,
|
||||
hit_index: Int = 1,
|
||||
target: Object = None,
|
||||
):
|
||||
active_point = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=index(),
|
||||
b=hit_index,
|
||||
)
|
||||
target_info = object_info(transform_space="RELATIVE", object=target, as_instance=False)
|
||||
previous_index = math(operation=Math.Operation.SUBTRACT, value=(hit_index, integer(integer=1)))
|
||||
previous_position = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.POINT,
|
||||
clamp=True,
|
||||
geometry=line,
|
||||
value=position(),
|
||||
index=previous_index,
|
||||
)
|
||||
hit = raycast(
|
||||
data_type="FLOAT_VECTOR",
|
||||
target_geometry=target_info.geometry,
|
||||
interpolation="Nearest",
|
||||
source_position=previous_position,
|
||||
ray_direction=ray_direction,
|
||||
ray_length=20.0,
|
||||
)
|
||||
hit_position = vector_math(operation=VectorMath.Operation.SCALE, vector=hit.hit_position, scale=0.9990000128746033)
|
||||
updated_line = set_position(geometry=line, selection=active_point, position=hit_position, offset=(0.0, 0.0, 0.0))
|
||||
return {
|
||||
"Line": updated_line,
|
||||
"Reflected Direction": vector_math(operation=VectorMath.Operation.REFLECT, vector=(ray_direction, hit.hit_normal)),
|
||||
"Traveled Distance": math(operation=Math.Operation.ADD, value=(hit.hit_distance, traveled_distance)),
|
||||
"Next Hit Index": math(operation=Math.Operation.ADD, value=(hit_index, integer(integer=1))),
|
||||
}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in ("VG Initial Direction", "VG Line To Be Casted", "VG Cast Rays"):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if group:
|
||||
group.use_fake_user = True
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_RAYCAST_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
"""Geometry Script footholds from IRCSS/Blender-Geometry-Node-French-Houses."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from geometry_script.api.node_mapper import OutputsList, set_or_create_link
|
||||
from geometry_script.api.state import State
|
||||
from geometry_script.api.types import Type
|
||||
|
||||
|
||||
def _capture_attribute_item(data_type: str, domain: str, geometry, value, name: str):
|
||||
node = State.current_node_tree.nodes.new("GeometryNodeCaptureAttribute")
|
||||
node.capture_items.new(data_type, name)
|
||||
node.active_index = 0
|
||||
node.domain = domain
|
||||
set_or_create_link(geometry, node.inputs[0])
|
||||
set_or_create_link(value, node.inputs[1])
|
||||
return OutputsList({"geometry": Type(node.outputs[0]), name.lower().replace(" ", "_"): Type(node.outputs[1])})
|
||||
|
||||
|
||||
@tree("VG Make Spiral")
|
||||
def vg_make_spiral(
|
||||
frequency: Float = 12.0,
|
||||
radius: Float = 1.0,
|
||||
height: Float = 3.0,
|
||||
height_radius_gain: Float = 0.0,
|
||||
):
|
||||
rail = curve_line(mode=CurveLine.Mode.POINTS, start=(0.0, 0.0, 0.0), end=(0.0, 0.0, 1.0))
|
||||
samples = resample_curve(keep_last_segment=True, curve=rail, mode="Count", count=200, length=0.10000000149011612)
|
||||
point_count = domain_size(component="CURVE", geometry=samples).point_count
|
||||
t = math(operation=Math.Operation.DIVIDE, value=(index(), point_count))
|
||||
theta = math(operation=Math.Operation.MULTIPLY, value=(t, frequency))
|
||||
expanding_radius = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(radius, math(operation=Math.Operation.MULTIPLY, value=(t, height_radius_gain))),
|
||||
)
|
||||
x = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.SINE, value=theta), expanding_radius),
|
||||
)
|
||||
y = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.COSINE, value=theta), expanding_radius),
|
||||
)
|
||||
z = math(operation=Math.Operation.MULTIPLY, value=(t, height))
|
||||
return {"Geometry": set_position(geometry=samples, position=combine_xyz(x=x, y=y, z=z), offset=(0.0, 0.0, 0.0))}
|
||||
|
||||
|
||||
@tree("VG Pointy Gothic Cone")
|
||||
def vg_pointy_gothic_cone(
|
||||
geometry: Geometry,
|
||||
column_radius: Float = 0.3,
|
||||
peak_height: Float = 1.0,
|
||||
gothic_detail_density: Float = 0.6,
|
||||
detail_scale: Float = 0.1,
|
||||
spawn_details: Bool = True,
|
||||
rotation: Rotation = (0.0, 0.0, 0.0),
|
||||
):
|
||||
point_positions = _capture_attribute_item("VECTOR", "POINT", geometry, position(), "source position")
|
||||
|
||||
cone_seed = cylinder(fill_type="NGON", vertices=6, side_segments=1, fill_segments=1, radius=1.0, depth=1.0)
|
||||
raised_cone = set_position(geometry=cone_seed.mesh, offset=(0.0, 0.0, 0.5))
|
||||
tapered_cone = scale_elements(
|
||||
domain="FACE",
|
||||
geometry=raised_cone,
|
||||
selection=cone_seed.top,
|
||||
scale=0.10000002384185791,
|
||||
scale_mode="Uniform",
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
|
||||
not_side = boolean_math(operation="NOT", boolean=cone_seed.side)
|
||||
cap_or_body_edge = boolean_math(
|
||||
operation="OR",
|
||||
boolean=(boolean_math(operation="OR", boolean=(not_side, cone_seed.top)), cone_seed.bottom),
|
||||
)
|
||||
cone_edge_flags = _capture_attribute_item("BOOLEAN", "EDGE", tapered_cone, cap_or_body_edge, "edge trim")
|
||||
|
||||
scaled_cone_position = vector_math(
|
||||
operation="MULTIPLY",
|
||||
vector=(position(), combine_xyz(x=1.0, y=1.0, z=peak_height)),
|
||||
)
|
||||
shaped_cone = set_position(geometry=cone_edge_flags.geometry, position=scaled_cone_position, offset=(0.0, 0.0, 0.0))
|
||||
|
||||
skirt_depth = math(
|
||||
operation="MINIMUM",
|
||||
value=(math(operation="MULTIPLY", value=(peak_height, 0.20000000298023224)), 1.0),
|
||||
)
|
||||
cone_with_skirt = extrude_mesh(
|
||||
mode="FACES",
|
||||
mesh=shaped_cone,
|
||||
selection=cone_seed.bottom,
|
||||
offset_scale=skirt_depth,
|
||||
individual=False,
|
||||
)
|
||||
|
||||
cone_scale = combine_xyz(x=column_radius, y=column_radius, z=1.0)
|
||||
cone_instances = instance_on_points(
|
||||
points=point_positions.geometry,
|
||||
instance=cone_with_skirt.mesh,
|
||||
pick_instance=False,
|
||||
rotation=rotation,
|
||||
scale=cone_scale,
|
||||
)
|
||||
realized_cones = realize_instances(
|
||||
realize_to_point_domain=True,
|
||||
geometry=cone_instances,
|
||||
realize_all=True,
|
||||
depth=0,
|
||||
)
|
||||
|
||||
details_disabled = boolean_math(operation="NOT", boolean=spawn_details)
|
||||
cone_for_detail_edges = delete_geometry(mode="ALL", domain="POINT", geometry=realized_cones, selection=details_disabled)
|
||||
cone_ribs = delete_geometry(mode="ALL", domain="EDGE", geometry=cone_for_detail_edges, selection=cone_edge_flags.edge_trim)
|
||||
rib_curve = mesh_to_curve(mode="EDGES", mesh=cone_ribs)
|
||||
detail_points = resample_curve(
|
||||
keep_last_segment=True,
|
||||
curve=rib_curve,
|
||||
mode="Length",
|
||||
count=5,
|
||||
length=gothic_detail_density,
|
||||
)
|
||||
|
||||
from_source_point = vector_math(operation="SUBTRACT", vector=(position(), point_positions.source_position))
|
||||
z_from_source = separate_xyz(vector=from_source_point)
|
||||
below_peak = compare(
|
||||
operation="LESS_THAN",
|
||||
data_type="FLOAT",
|
||||
mode="ELEMENT",
|
||||
a=math(operation="SUBTRACT", value=(math(operation="ABSOLUTE", value=z_from_source.z), peak_height)),
|
||||
b=-0.09999924898147583,
|
||||
)
|
||||
|
||||
detail_seed = cylinder(fill_type="NGON", vertices=5, side_segments=1, fill_segments=1, radius=1.0, depth=1.0)
|
||||
detail_taper = scale_elements(
|
||||
domain="FACE",
|
||||
geometry=detail_seed.mesh,
|
||||
selection=detail_seed.top,
|
||||
scale=0.20000001788139343,
|
||||
scale_mode="Uniform",
|
||||
axis=(1.0, 0.0, 0.0),
|
||||
)
|
||||
raised_detail = set_position(geometry=detail_taper, offset=(0.0, 0.0, 0.30000001192092896))
|
||||
stretched_detail = set_position(
|
||||
geometry=raised_detail,
|
||||
position=vector_math(operation="MULTIPLY", vector=(position(), (1.0, 1.0, 2.0))),
|
||||
offset=(0.0, 0.0, 0.0),
|
||||
)
|
||||
horizontal_rib_direction = vector_math(operation="MULTIPLY", vector=(from_source_point, (1.0, 1.0, 0.0)))
|
||||
detail_rotation = align_rotation_to_vector(axis="Z", pivot_axis="AUTO", factor=1.0, vector=horizontal_rib_direction)
|
||||
detail_instances = instance_on_points(
|
||||
points=detail_points,
|
||||
selection=below_peak,
|
||||
instance=stretched_detail,
|
||||
pick_instance=False,
|
||||
rotation=detail_rotation,
|
||||
scale=detail_scale,
|
||||
)
|
||||
realized_details = realize_instances(
|
||||
realize_to_point_domain=True,
|
||||
geometry=detail_instances,
|
||||
realize_all=True,
|
||||
depth=0,
|
||||
)
|
||||
return {"Geometry": join_geometry(geometry=[realized_cones, realized_details])}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in ("VG Make Spiral", "VG Pointy Gothic Cone"):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if group:
|
||||
group.use_fake_user = True
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_IRCSS_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,699 +0,0 @@
|
|||
"""Build the Lucent tether habitat from the Ghostlight hostage-feed scene.
|
||||
|
||||
Spatial contract from the story:
|
||||
|
||||
- The tether is the long station spine.
|
||||
- The transfer lounge sits at the media-eye end of the tether.
|
||||
- The city bubble is far below, readable as a sealed aquarium.
|
||||
- The lounge threshold carries an elevator gate, waist-high glass rail, and
|
||||
a white safe-line arc two careful steps back from the rail.
|
||||
- Lucent surfaces are never only surfaces: overlays, rails, prompts, status
|
||||
edits, influencer side panels, and audience heat are part of the room.
|
||||
"""
|
||||
|
||||
import math as pymath
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from mathutils import Vector
|
||||
from vibegeometry import (
|
||||
add_box,
|
||||
add_curve_polyline,
|
||||
add_cylinder_between,
|
||||
add_material,
|
||||
add_mesh_parts,
|
||||
add_multi_polyline_curve,
|
||||
aim_camera,
|
||||
append_box_parts,
|
||||
clear_scene,
|
||||
fbm_2d,
|
||||
hash01,
|
||||
smoothstep,
|
||||
)
|
||||
|
||||
|
||||
TAU = pymath.tau
|
||||
OUT_DIR = Path("experiments/generated/lucent_tether_habitat")
|
||||
CITY_GROUND_X = -5.45
|
||||
CITY_BUBBLE_CENTER = (-4.95, 0.0, 0.0)
|
||||
CITY_BUBBLE_SCALE = (1.0, 1.95, 1.95)
|
||||
CITY_TETHER_JUNCTION = (CITY_GROUND_X, 0.0, 0.0)
|
||||
TETHER_START = CITY_TETHER_JUNCTION
|
||||
TETHER_END = (6.8, 0.0, 0.0)
|
||||
MEDIA_EYE_CENTER = (4.6, 0.0, 0.0)
|
||||
|
||||
|
||||
@tree("VG Lucent Feed Ribbon")
|
||||
def vg_lucent_feed_ribbon(
|
||||
length: Float = 11.5,
|
||||
radius: Float = 1.25,
|
||||
turns: Float = 2.25,
|
||||
wave: Float = 0.22,
|
||||
thickness: Float = 0.025,
|
||||
):
|
||||
"""Procedural moderation/feed rail orbiting the media-eye lounge."""
|
||||
|
||||
rail = curve_line(mode=CurveLine.Mode.POINTS, start=(-5.75, 0.0, 0.0), end=(5.75, 0.0, 0.0))
|
||||
samples = resample_curve(keep_last_segment=True, curve=rail, mode="Count", count=360, length=0.1)
|
||||
point_count = domain_size(component="CURVE", geometry=samples).point_count
|
||||
t = math(operation=Math.Operation.DIVIDE, value=(index(), point_count))
|
||||
theta = math(operation=Math.Operation.MULTIPLY, value=(t, math(operation=Math.Operation.MULTIPLY, value=(turns, TAU))))
|
||||
x = math(operation=Math.Operation.SUBTRACT, value=(math(operation=Math.Operation.MULTIPLY, value=(t, length)), math(operation=Math.Operation.DIVIDE, value=(length, 2.0))))
|
||||
pulse = math(operation=Math.Operation.SINE, value=math(operation=Math.Operation.MULTIPLY, value=(theta, 4.0)))
|
||||
animated_radius = math(operation=Math.Operation.ADD, value=(radius, math(operation=Math.Operation.MULTIPLY, value=(pulse, wave))))
|
||||
y = math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.COSINE, value=theta), animated_radius))
|
||||
z = math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SINE, value=theta), animated_radius))
|
||||
positioned = set_position(geometry=samples, position=combine_xyz(x=x, y=y, z=z), offset=(0.0, 0.0, 0.0))
|
||||
profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=8, radius=thickness)
|
||||
return {"Geometry": curve_to_mesh(curve=positioned, profile_curve=profile, fill_caps=True)}
|
||||
|
||||
|
||||
def mat(name, color, emission=False, strength=0.0, alpha=1.0):
|
||||
return add_material(name, color, emission=emission, strength=strength, alpha=alpha)
|
||||
|
||||
|
||||
def socket_set(node, socket_name, value):
|
||||
if socket_name in node.inputs:
|
||||
node.inputs[socket_name].default_value = value
|
||||
|
||||
|
||||
def add_shader_bump(material, scale=38.0, detail=7.0, strength=0.05, distance=0.08):
|
||||
nodes = material.node_tree.nodes
|
||||
links = material.node_tree.links
|
||||
bsdf = nodes.get("Principled BSDF")
|
||||
if not bsdf:
|
||||
return
|
||||
noise = nodes.new("ShaderNodeTexNoise")
|
||||
noise.name = f"{material.name} procedural micro-noise"
|
||||
noise.inputs["Scale"].default_value = scale
|
||||
noise.inputs["Detail"].default_value = detail
|
||||
noise.inputs["Roughness"].default_value = 0.58
|
||||
bump = nodes.new("ShaderNodeBump")
|
||||
bump.name = f"{material.name} shader bump"
|
||||
bump.inputs["Strength"].default_value = strength
|
||||
bump.inputs["Distance"].default_value = distance
|
||||
links.new(noise.outputs["Fac"], bump.inputs["Height"])
|
||||
links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
|
||||
|
||||
|
||||
def add_panel_shader_graph(material, base_color, emission_color, pulse_color):
|
||||
nodes = material.node_tree.nodes
|
||||
links = material.node_tree.links
|
||||
bsdf = nodes.get("Principled BSDF")
|
||||
if not bsdf:
|
||||
return
|
||||
noise = nodes.new("ShaderNodeTexNoise")
|
||||
noise.name = f"{material.name} scanline noise"
|
||||
noise.inputs["Scale"].default_value = 55.0
|
||||
noise.inputs["Detail"].default_value = 10.0
|
||||
ramp = nodes.new("ShaderNodeValToRGB")
|
||||
ramp.name = f"{material.name} risk-color ramp"
|
||||
ramp.color_ramp.elements[0].position = 0.28
|
||||
ramp.color_ramp.elements[0].color = base_color
|
||||
ramp.color_ramp.elements[1].position = 1.0
|
||||
ramp.color_ramp.elements[1].color = pulse_color
|
||||
links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
|
||||
links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
|
||||
if "Emission Color" in bsdf.inputs:
|
||||
bsdf.inputs["Emission Color"].default_value = emission_color
|
||||
if "Emission Strength" in bsdf.inputs:
|
||||
bsdf.inputs["Emission Strength"].default_value = 0.65
|
||||
|
||||
|
||||
def make_glass_shader(material, tint=(0.38, 0.72, 1.0, 0.28)):
|
||||
material.use_nodes = True
|
||||
material.blend_method = "BLEND"
|
||||
material.use_screen_refraction = True
|
||||
material.show_transparent_back = True
|
||||
nodes = material.node_tree.nodes
|
||||
bsdf = nodes.get("Principled BSDF")
|
||||
if not bsdf:
|
||||
return
|
||||
socket_set(bsdf, "Base Color", tint)
|
||||
socket_set(bsdf, "Alpha", tint[3])
|
||||
socket_set(bsdf, "Roughness", 0.05)
|
||||
socket_set(bsdf, "Metallic", 0.0)
|
||||
socket_set(bsdf, "Transmission Weight", 0.55)
|
||||
socket_set(bsdf, "IOR", 1.47)
|
||||
add_shader_bump(material, scale=96.0, detail=8.0, strength=0.018, distance=0.045)
|
||||
|
||||
|
||||
def enrich_shader_graphs(mats):
|
||||
make_glass_shader(mats["glass"])
|
||||
add_shader_bump(mats["tether"], scale=70.0, detail=9.0, strength=0.075, distance=0.035)
|
||||
add_shader_bump(mats["lucent_gray"], scale=45.0, detail=6.0, strength=0.045, distance=0.035)
|
||||
add_shader_bump(mats["floor"], scale=24.0, detail=7.0, strength=0.035, distance=0.025)
|
||||
add_shader_bump(mats["city"], scale=82.0, detail=4.0, strength=0.03, distance=0.02)
|
||||
add_shader_bump(mats["prestige"], scale=64.0, detail=7.0, strength=0.025, distance=0.018)
|
||||
add_shader_bump(mats["park"], scale=18.0, detail=6.0, strength=0.045, distance=0.028)
|
||||
add_shader_bump(mats["cottage"], scale=31.0, detail=5.0, strength=0.055, distance=0.03)
|
||||
add_shader_bump(mats["plaza"], scale=36.0, detail=6.0, strength=0.028, distance=0.02)
|
||||
add_shader_bump(mats["solar_panel"], scale=42.0, detail=8.0, strength=0.028, distance=0.018)
|
||||
add_shader_bump(mats["radiator"], scale=58.0, detail=9.0, strength=0.035, distance=0.02)
|
||||
for key, pulse in [
|
||||
("cyan", (0.65, 1.0, 1.0, 1.0)),
|
||||
("amber", (1.0, 0.88, 0.32, 1.0)),
|
||||
("risk", (1.0, 0.45, 0.86, 1.0)),
|
||||
("heat", (1.0, 0.18, 0.08, 1.0)),
|
||||
("overlay_blue", (0.7, 0.86, 1.0, 1.0)),
|
||||
]:
|
||||
add_panel_shader_graph(mats[key], mats[key].diffuse_color, mats[key].diffuse_color, pulse)
|
||||
|
||||
|
||||
def add_uv_sphere(name, loc, scale, material, segments=48, rings=24):
|
||||
import bpy
|
||||
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, radius=1.0, location=loc)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.data.name = name + "Mesh"
|
||||
obj.scale = scale
|
||||
obj.data.materials.append(material)
|
||||
obj.show_transparent = True
|
||||
return obj
|
||||
|
||||
|
||||
def add_torus(name, loc, major_radius, minor_radius, material, rotation=(0.0, 0.0, 0.0), segments=96):
|
||||
import bpy
|
||||
|
||||
bpy.ops.mesh.primitive_torus_add(
|
||||
major_segments=segments,
|
||||
minor_segments=10,
|
||||
major_radius=major_radius,
|
||||
minor_radius=minor_radius,
|
||||
location=loc,
|
||||
rotation=rotation,
|
||||
)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.data.name = name + "Mesh"
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_label(name, text, loc, size, material, rotation=(pymath.radians(65), 0.0, pymath.radians(0))):
|
||||
import bpy
|
||||
|
||||
bpy.ops.object.text_add(location=loc, rotation=rotation)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.data.body = text
|
||||
obj.data.align_x = "CENTER"
|
||||
obj.data.align_y = "CENTER"
|
||||
obj.data.size = size
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def arc_points(center, radius, start_deg, end_deg, steps, plane="xy", wobble=0.0):
|
||||
points = []
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
a = pymath.radians(start_deg + (end_deg - start_deg) * t)
|
||||
w = wobble * pymath.sin(t * TAU * 2.0)
|
||||
r = radius + w
|
||||
if plane == "xy":
|
||||
points.append((center[0] + r * pymath.cos(a), center[1] + r * pymath.sin(a), center[2]))
|
||||
elif plane == "yz":
|
||||
points.append((center[0], center[1] + r * pymath.cos(a), center[2] + r * pymath.sin(a)))
|
||||
else:
|
||||
points.append((center[0] + r * pymath.cos(a), center[1], center[2] + r * pymath.sin(a)))
|
||||
return points
|
||||
|
||||
|
||||
def annular_sector_points(center, inner_radius, outer_radius, start_angle, end_angle, plane="xy"):
|
||||
if plane == "yz":
|
||||
x, y, z = center
|
||||
return [
|
||||
(x, y + inner_radius * pymath.cos(start_angle), z + inner_radius * pymath.sin(start_angle)),
|
||||
(x, y + outer_radius * pymath.cos(start_angle), z + outer_radius * pymath.sin(start_angle)),
|
||||
(x, y + outer_radius * pymath.cos(end_angle), z + outer_radius * pymath.sin(end_angle)),
|
||||
(x, y + inner_radius * pymath.cos(end_angle), z + inner_radius * pymath.sin(end_angle)),
|
||||
]
|
||||
return [
|
||||
(center[0] + inner_radius * pymath.cos(start_angle), center[1] + inner_radius * pymath.sin(start_angle), center[2]),
|
||||
(center[0] + outer_radius * pymath.cos(start_angle), center[1] + outer_radius * pymath.sin(start_angle), center[2]),
|
||||
(center[0] + outer_radius * pymath.cos(end_angle), center[1] + outer_radius * pymath.sin(end_angle), center[2]),
|
||||
(center[0] + inner_radius * pymath.cos(end_angle), center[1] + inner_radius * pymath.sin(end_angle), center[2]),
|
||||
]
|
||||
|
||||
|
||||
def add_flat_polygon(name, verts, material):
|
||||
return add_mesh_parts(name, verts, [(0, 1, 2, 3)], material)
|
||||
|
||||
|
||||
def add_yz_disk(name, x, radius, material, segments=96):
|
||||
verts = [(x, 0.0, 0.0)]
|
||||
faces = []
|
||||
for i in range(segments):
|
||||
a = TAU * i / segments
|
||||
verts.append((x, radius * pymath.cos(a), radius * pymath.sin(a)))
|
||||
for i in range(segments):
|
||||
faces.append((0, i + 1, 1 + ((i + 1) % segments)))
|
||||
return add_mesh_parts(name, verts, faces, material)
|
||||
|
||||
|
||||
def add_dome_base_crest(mats):
|
||||
solar_verts, solar_faces = [], []
|
||||
radiator_verts, radiator_faces = [], []
|
||||
petals = 18
|
||||
radial_steps = 8
|
||||
for petal in range(petals):
|
||||
center_a = TAU * petal / petals + 0.025 * pymath.sin(petal * 1.7)
|
||||
width = TAU / petals * (0.72 + 0.24 * hash01(petal, 0, 251))
|
||||
outward_reach = 0.8 + 0.42 * fbm_2d(pymath.cos(center_a) * 2.1, pymath.sin(center_a) * 2.1, seed=177)
|
||||
arm_length = (TETHER_END[0] - CITY_GROUND_X) * (0.88 + 0.08 * hash01(petal, 12, 277))
|
||||
twist = 0.18 * (hash01(petal, 4, 271) - 0.5)
|
||||
target_verts = solar_verts if petal % 2 == 0 else radiator_verts
|
||||
target_faces = solar_faces if petal % 2 == 0 else radiator_faces
|
||||
petal_base = len(target_verts)
|
||||
for side in (-1.0, 1.0):
|
||||
for step in range(radial_steps + 1):
|
||||
t = step / radial_steps
|
||||
edge = (0.55 + 0.45 * smoothstep(1.0 - abs(t - 0.55) * 2.0))
|
||||
noise = fbm_2d(petal * 0.41, step * 0.73 + side, seed=303) - 0.5
|
||||
local_width = width * (1.0 - 0.42 * smoothstep(t)) * edge + noise * 0.018
|
||||
a = center_a + side * local_width * 0.5 + twist * t
|
||||
radius = 1.62 + outward_reach * smoothstep(t) + 0.05 * noise
|
||||
# External defensive arms stow by folding along the tether
|
||||
# toward the far media-eye end; deployed they read as Citadel
|
||||
# scale ray florets rather than petals glued to the dome.
|
||||
x = CITY_GROUND_X + arm_length * smoothstep(t)
|
||||
y = radius * pymath.cos(a)
|
||||
z = radius * pymath.sin(a)
|
||||
target_verts.append((x, y, z))
|
||||
for step in range(radial_steps):
|
||||
target_faces.append((petal_base + step, petal_base + step + 1, petal_base + radial_steps + 2 + step + 1, petal_base + radial_steps + 2 + step))
|
||||
solar = add_mesh_parts("city_bubble_solar_panel_ray_florets", solar_verts, solar_faces, mats["solar_panel"])
|
||||
radiator = add_mesh_parts("city_bubble_radiator_ray_florets", radiator_verts, radiator_faces, mats["radiator"])
|
||||
solar.show_transparent = True
|
||||
radiator.show_transparent = True
|
||||
return solar, radiator
|
||||
|
||||
|
||||
def add_floor_patch(name, center, width, depth, material, z=0.0):
|
||||
return add_box(name, (center[0], center[1], z), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (width, depth, 0.035), material)
|
||||
|
||||
|
||||
def add_panel(name, loc, size, material, angle=0.0, lean=0.0):
|
||||
x_axis = (pymath.cos(angle), pymath.sin(angle), 0)
|
||||
y_axis = (-pymath.sin(angle), pymath.cos(angle), lean)
|
||||
z_axis = (0, -lean, 1)
|
||||
return add_box(name, loc, (x_axis, y_axis, z_axis), size, material)
|
||||
|
||||
|
||||
def add_habitat_cable_bundle(mats):
|
||||
add_cylinder_between("lucent_tether_main_cable", TETHER_START, TETHER_END, 0.075, mats["tether"], vertices=24)
|
||||
add_cylinder_between("lucent_tether_spin_reference_core", (-6.25, 0.18, 0.16), (6.25, 0.18, 0.16), 0.023, mats["cyan"], vertices=10)
|
||||
add_cylinder_between("lucent_tether_service_darkline", (-6.25, -0.16, -0.12), (6.25, -0.16, -0.12), 0.018, mats["dark"], vertices=8)
|
||||
for i, x in enumerate([-4.9, -3.2, -1.4, 0.4, 2.1, 3.9, 5.4]):
|
||||
add_torus(
|
||||
f"slow_rotation_reference_ring_{i:02d}",
|
||||
(x, 0.0, 0.0),
|
||||
0.42 + 0.04 * (i % 3),
|
||||
0.008,
|
||||
mats["soft_white"],
|
||||
rotation=(0.0, pymath.radians(90), 0.0),
|
||||
segments=64,
|
||||
)
|
||||
|
||||
|
||||
def add_city_bubble(mats):
|
||||
import bpy
|
||||
|
||||
bubble = add_uv_sphere("city_bubble_sealed_aquarium", CITY_BUBBLE_CENTER, CITY_BUBBLE_SCALE, mats["glass"], 64, 32)
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(CITY_GROUND_X - 2.0, 0.0, 0.0))
|
||||
cutter = bpy.context.object
|
||||
cutter.name = "city_bubble_flattening_boolean_cutter"
|
||||
cutter.dimensions = (4.0, 5.0, 5.0)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
boolean = bubble.modifiers.new("boolean cut flat ground base", "BOOLEAN")
|
||||
boolean.operation = "DIFFERENCE"
|
||||
boolean.object = cutter
|
||||
bpy.context.view_layer.objects.active = bubble
|
||||
bubble.select_set(True)
|
||||
bpy.ops.object.modifier_apply(modifier=boolean.name)
|
||||
cutter.hide_viewport = True
|
||||
cutter.hide_render = True
|
||||
add_torus(
|
||||
"city_bubble_pressure_equator",
|
||||
(CITY_GROUND_X + 0.02, 0.0, 0.0),
|
||||
1.95,
|
||||
0.022,
|
||||
mats["cyan"],
|
||||
rotation=(0.0, pymath.radians(90), 0.0),
|
||||
segments=128,
|
||||
)
|
||||
add_torus(
|
||||
"city_bubble_flat_ground_rim",
|
||||
(CITY_GROUND_X + 0.01, 0.0, 0.0),
|
||||
1.42,
|
||||
0.018,
|
||||
mats["aquarium"],
|
||||
rotation=(0.0, pymath.radians(90), 0.0),
|
||||
segments=96,
|
||||
)
|
||||
add_yz_disk("city_bubble_flat_ground_surface_disk", CITY_GROUND_X + 0.004, 1.54, mats["park"], segments=128)
|
||||
|
||||
ground_x = CITY_GROUND_X + 0.018
|
||||
tower_verts, tower_faces = [], []
|
||||
prestige_verts, prestige_faces = [], []
|
||||
cottage_verts, cottage_faces = [], []
|
||||
park_faces = []
|
||||
roads = []
|
||||
|
||||
# Street field: major avenues radiate from the tether anchor, then bend
|
||||
# toward a tangential promenade field. This borrows the tensor-field idea
|
||||
# without pretending this tiny bubble needs a whole GIS stack.
|
||||
for i in range(16):
|
||||
base_angle = TAU * i / 16 + (hash01(i, 4, 11) - 0.5) * 0.12
|
||||
line = []
|
||||
for step in range(9):
|
||||
u = step / 8
|
||||
r = 0.12 + 1.42 * smoothstep(u)
|
||||
drift = 0.22 * pymath.sin(u * pymath.pi) * (hash01(i, step, 17) - 0.5)
|
||||
angle = base_angle + drift + 0.08 * pymath.sin(u * TAU + i)
|
||||
line.append((ground_x + 0.025, r * pymath.cos(angle), r * pymath.sin(angle)))
|
||||
roads.append(line)
|
||||
for ring_i, radius in enumerate([0.22, 0.42, 0.68, 0.95, 1.2, 1.42]):
|
||||
for arc_i in range(3):
|
||||
start = TAU * arc_i / 3 + ring_i * 0.21
|
||||
end = start + TAU / 3 * (0.72 + 0.12 * hash01(ring_i, arc_i, 23))
|
||||
roads.append(arc_points((ground_x + 0.028, 0.0, 0.0), radius, pymath.degrees(start), pymath.degrees(end), 24, plane="yz", wobble=0.018))
|
||||
|
||||
# Anchor plaza: the tether physically lands on the ground surface. The
|
||||
# skyline knows where power plugs in.
|
||||
add_yz_disk("city_bubble_tether_anchor_plaza_core", ground_x + 0.012, 0.24, mats["plaza"], segments=48)
|
||||
add_torus("city_bubble_anchor_plaza_ring", (ground_x + 0.025, 0.0, 0.0), 0.32, 0.011, mats["soft_white"], rotation=(0.0, pymath.radians(90), 0.0), segments=96)
|
||||
add_dome_base_crest(mats)
|
||||
|
||||
rings = [0.24, 0.42, 0.62, 0.84, 1.08, 1.31, 1.52]
|
||||
for ring in range(len(rings) - 1):
|
||||
inner = rings[ring]
|
||||
outer = rings[ring + 1]
|
||||
zone = ring / (len(rings) - 2)
|
||||
cells = int(18 + ring * 7)
|
||||
for cell in range(cells):
|
||||
u0 = cell / cells
|
||||
u1 = (cell + 0.78 + 0.34 * hash01(ring, cell, 33)) / cells
|
||||
a0 = TAU * u0 + 0.1 * pymath.sin(ring * 1.7)
|
||||
a1 = TAU * u1 + 0.07 * (hash01(cell, ring, 35) - 0.5)
|
||||
n = fbm_2d(ring * 0.61, cell * 0.29, seed=91)
|
||||
park_bias = fbm_2d(pymath.cos((a0 + a1) * 0.5) * 2.2 + ring, pymath.sin((a0 + a1) * 0.5) * 2.2, seed=121)
|
||||
if zone > 0.32 and park_bias > 0.68:
|
||||
park_faces.extend(annular_sector_points((ground_x + 0.012, 0.0, 0.0), inner + 0.025, outer - 0.02, a0 + 0.025, a1 - 0.018, plane="yz"))
|
||||
continue
|
||||
r = inner + (outer - inner) * (0.45 + 0.22 * (n - 0.5))
|
||||
angle = (a0 + a1) * 0.5
|
||||
radial_axis = (0.0, pymath.cos(angle), pymath.sin(angle))
|
||||
tangent_axis = (0.0, -pymath.sin(angle), pymath.cos(angle))
|
||||
radial_width = max(0.04, (outer - inner) * (0.36 + 0.28 * n))
|
||||
tangential_width = max(0.045, r * (a1 - a0) * (0.55 + 0.3 * hash01(cell, ring, 37)))
|
||||
if zone < 0.28:
|
||||
height = 0.5 + 0.62 * n
|
||||
verts, faces = prestige_verts, prestige_faces
|
||||
size = (tangential_width * 0.72, radial_width * 0.8, height)
|
||||
loc = (ground_x + height * 0.5, r * pymath.cos(angle), r * pymath.sin(angle))
|
||||
elif zone < 0.72:
|
||||
height = 0.16 + 0.36 * n * (1.0 - zone * 0.35)
|
||||
verts, faces = tower_verts, tower_faces
|
||||
size = (tangential_width * 0.82, radial_width * 0.9, height)
|
||||
loc = (ground_x + height * 0.5, r * pymath.cos(angle), r * pymath.sin(angle))
|
||||
else:
|
||||
if n < 0.38:
|
||||
park_faces.extend(annular_sector_points((ground_x + 0.012, 0.0, 0.0), inner + 0.02, outer - 0.02, a0 + 0.02, a1 - 0.02, plane="yz"))
|
||||
continue
|
||||
height = 0.045 + 0.07 * n
|
||||
verts, faces = cottage_verts, cottage_faces
|
||||
size = (tangential_width * 0.55, radial_width * 0.6, height)
|
||||
loc = (ground_x + height * 0.5, r * pymath.cos(angle), r * pymath.sin(angle))
|
||||
append_box_parts(verts, faces, loc, (tangent_axis, radial_axis, (1, 0, 0)), size)
|
||||
|
||||
if park_faces:
|
||||
verts = park_faces
|
||||
faces = [(i, i + 1, i + 2, i + 3) for i in range(0, len(verts), 4)]
|
||||
add_mesh_parts("city_bubble_interspersed_park_cells", verts, faces, mats["park"])
|
||||
add_mesh_parts("city_bubble_prestige_anchor_skyscrapers", prestige_verts, prestige_faces, mats["prestige"])
|
||||
add_mesh_parts("city_bubble_midcity_irregular_blocks", tower_verts, tower_faces, mats["city"])
|
||||
add_mesh_parts("city_bubble_faux_rural_cottagecore_edge", cottage_verts, cottage_faces, mats["cottage"])
|
||||
add_multi_polyline_curve("city_bubble_tensor_field_street_network", roads, 0.006, mats["soft_white"], resolution=3)
|
||||
add_multi_polyline_curve(
|
||||
"city_bubble_prestige_plaza_spokes",
|
||||
[[(ground_x + 0.04, 0.0, 0.0), road[-1]] for road in roads[:16:2]],
|
||||
0.012,
|
||||
mats["cyan"],
|
||||
resolution=3,
|
||||
)
|
||||
|
||||
|
||||
def add_city_bubble_tether_attachment(mats):
|
||||
x, y, z = CITY_TETHER_JUNCTION
|
||||
top = (x, y, z)
|
||||
collar = (CITY_GROUND_X + 0.08, 0.0, 0.0)
|
||||
plaza = (CITY_GROUND_X + 0.08, 0.0, 0.0)
|
||||
keel = (CITY_GROUND_X - 0.12, 0.0, 0.0)
|
||||
add_cylinder_between("city_bubble_tether_load_spine", top, plaza, 0.045, mats["tether"], vertices=16)
|
||||
add_cylinder_between("city_bubble_elevator_umbilical", (x + 0.14, 0.0, -0.05), (plaza[0] + 0.14, 0.0, -0.05), 0.022, mats["cyan"], vertices=10)
|
||||
add_cylinder_between("city_bubble_elevator_drop_to_anchor_plaza", (plaza[0] + 0.14, 0.0, -0.05), (plaza[0] + 0.14, 0.0, 0.18), 0.018, mats["cyan"], vertices=10)
|
||||
add_torus(
|
||||
"city_bubble_upper_tether_collar",
|
||||
collar,
|
||||
0.52,
|
||||
0.025,
|
||||
mats["soft_white"],
|
||||
rotation=(0.0, pymath.radians(90), 0.0),
|
||||
segments=96,
|
||||
)
|
||||
add_box("city_bubble_tether_transfer_node", (x + 0.08, 0.0, 0.0), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.42, 0.34, 0.34), mats["lucent_gray"])
|
||||
for i, angle in enumerate([0.0, pymath.pi / 2, pymath.pi, pymath.pi * 1.5]):
|
||||
y2 = pymath.cos(angle) * 1.35
|
||||
z2 = pymath.sin(angle) * 1.35
|
||||
x2 = CITY_GROUND_X + 0.18 + 0.22 * pymath.sin(angle * 2.0)
|
||||
add_cylinder_between(f"city_bubble_suspension_stay_{i:02d}", top, (x2, y2, z2), 0.012, mats["soft_white"], vertices=6)
|
||||
add_curve_polyline(
|
||||
f"city_bubble_tension_trace_{i:02d}",
|
||||
[top, ((top[0] + x2) * 0.5, y2 * 0.35, z2 * 0.35), (x2, y2, z2)],
|
||||
0.004,
|
||||
mats["cyan"],
|
||||
resolution=3,
|
||||
)
|
||||
add_cylinder_between("city_bubble_under_keel_counterweight", keel, (keel[0] - 0.75, keel[1], keel[2]), 0.028, mats["dark"], vertices=10)
|
||||
|
||||
|
||||
def add_media_eye_lounge(mats):
|
||||
add_uv_sphere("media_eye_transfer_lounge_glass_shell", MEDIA_EYE_CENTER, (1.75, 1.05, 0.78), mats["glass"], 64, 32)
|
||||
add_torus(
|
||||
"media_eye_lounge_structural_lash_ring",
|
||||
(4.6, 0.0, 0.0),
|
||||
1.07,
|
||||
0.025,
|
||||
mats["soft_white"],
|
||||
rotation=(0.0, pymath.radians(90), 0.0),
|
||||
segments=128,
|
||||
)
|
||||
add_torus(
|
||||
"media_eye_public_camera_eye",
|
||||
(5.58, 0.0, 0.08),
|
||||
0.42,
|
||||
0.022,
|
||||
mats["cyan"],
|
||||
rotation=(0.0, pymath.radians(90), 0.0),
|
||||
segments=96,
|
||||
)
|
||||
add_floor_patch("transfer_lounge_curved_floor_slab", (4.58, 0.0), 2.55, 1.36, mats["floor"], z=-0.42)
|
||||
for i, y in enumerate([-0.56, -0.28, 0.0, 0.28, 0.56]):
|
||||
add_curve_polyline(
|
||||
f"floor_keeps_remembering_physics_arc_{i:02d}",
|
||||
arc_points((4.58, y, -0.385), 0.34 + abs(y) * 0.15, 190, 350, 18, plane="xz"),
|
||||
0.004,
|
||||
mats["soft_white"],
|
||||
resolution=2,
|
||||
)
|
||||
|
||||
# The crisis geometry: elevator glass rail at the threshold, and the safe
|
||||
# line behind it. This is the room's moral diagram, unfortunately built
|
||||
# out of real materials and several departments' bad incentives.
|
||||
add_box("elevator_gate_threshold", (5.42, 0.0, -0.12), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.11, 1.36, 0.82), mats["lucent_gray"])
|
||||
add_curve_polyline("waist_high_glass_rail", [(5.25, -0.62, -0.08), (5.31, 0.0, -0.035), (5.25, 0.62, -0.08)], 0.027, mats["glass"], resolution=3)
|
||||
add_curve_polyline("safe_line_white_arc", arc_points((4.82, 0.0, -0.36), 0.68, 208, 332, 36, plane="xy"), 0.015, mats["safe"], resolution=3)
|
||||
add_box("elevator_side_glass_warning_strip", (5.18, 0.0, -0.31), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.025, 1.2, 0.03), mats["amber"])
|
||||
|
||||
add_panel("moderation_overlay_wall_public_status", (4.2, -0.82, 0.12), (1.18, 0.026, 0.46), mats["overlay_blue"], angle=0.0, lean=0.18)
|
||||
add_panel("feed_ops_prompt_panels", (4.12, -0.88, 0.34), (1.08, 0.028, 0.22), mats["amber"], angle=0.0, lean=0.18)
|
||||
add_panel("sponsor_risk_color_stack", (4.74, -0.86, 0.34), (0.15, 0.032, 0.62), mats["risk"], angle=0.0, lean=0.18)
|
||||
add_panel("audience_heat_meter", (3.55, -0.84, 0.30), (0.2, 0.032, 0.58), mats["heat"], angle=0.0, lean=0.18)
|
||||
add_panel("pippa_delayed_side_panel", (3.55, 0.87, 0.26), (0.54, 0.03, 0.38), mats["pippa"], angle=0.0, lean=-0.16)
|
||||
add_panel("brant_delayed_side_panel", (4.34, 0.9, 0.28), (0.58, 0.03, 0.42), mats["brant"], angle=0.0, lean=-0.16)
|
||||
add_panel("public_edit_trail_checksum_panel", (4.92, -0.84, -0.12), (0.6, 0.025, 0.16), mats["cyan"], angle=0.0, lean=0.08)
|
||||
|
||||
add_curve_polyline("mara_private_control_rail", [(3.32, -0.63, -0.22), (4.1, -0.69, -0.12), (5.05, -0.56, -0.2)], 0.018, mats["private"], resolution=3)
|
||||
for i in range(9):
|
||||
x = 3.42 + i * 0.18
|
||||
z = -0.18 + 0.08 * pymath.sin(i * 0.9)
|
||||
material = mats["risk"] if i in {2, 5, 8} else mats["cyan"] if i % 2 else mats["amber"]
|
||||
add_box(f"mara_private_rail_control_chip_{i:02d}", (x, -0.68, z), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.09, 0.022, 0.045), material)
|
||||
|
||||
add_box("security_lounge_doors", (3.12, 0.0, -0.08), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.11, 1.12, 0.72), mats["lucent_gray"])
|
||||
for i, y in enumerate([-0.42, -0.14, 0.14, 0.42]):
|
||||
add_box(f"lucent_gray_security_waiting_marker_{i:02d}", (2.83, y, -0.2), ((1, 0, 0), (0, 1, 0), (0, 0, 1)), (0.08, 0.08, 0.36), mats["security"])
|
||||
|
||||
|
||||
def add_overlay_ribbons_and_signage(mats):
|
||||
import bpy
|
||||
|
||||
group = bpy.data.node_groups.get("VG Lucent Feed Ribbon")
|
||||
mesh = bpy.data.meshes.new("LucentFeedRibbonCarrierMesh")
|
||||
carrier = bpy.data.objects.new("GN_Lucent_Feed_Ribbon_Carrier", mesh)
|
||||
bpy.context.collection.objects.link(carrier)
|
||||
carrier.location = (4.45, 0.0, 0.0)
|
||||
carrier.rotation_euler[1] = pymath.radians(90)
|
||||
carrier.scale = (0.32, 0.32, 0.32)
|
||||
carrier.data.materials.append(mats["cyan"])
|
||||
mod = carrier.modifiers.new("VG Lucent Feed Ribbon", "NODES")
|
||||
mod.node_group = group
|
||||
|
||||
for i, radius in enumerate([0.92, 1.12, 1.34]):
|
||||
points = []
|
||||
for j in range(72):
|
||||
t = j / 71
|
||||
a = TAU * t + i * 0.7
|
||||
x = 4.55 + (t - 0.5) * 1.8
|
||||
y = radius * pymath.cos(a) * 0.82
|
||||
z = 0.12 + radius * pymath.sin(a) * 0.46
|
||||
points.append((x, y, z))
|
||||
add_curve_polyline(f"manual_status_ribbon_orbit_{i:02d}", points, 0.006 + i * 0.002, mats["overlay_blue"], resolution=4)
|
||||
|
||||
labels = [
|
||||
("label_media_eye", "media-eye transfer lounge", (4.65, -1.52, 0.7), 0.082),
|
||||
("label_safe_line", "safe-line / glass rail", (5.18, 1.2, -0.34), 0.066),
|
||||
("label_city_bubble", "orthogonal dome city / sunflower crest", (-5.38, -1.82, -1.72), 0.095),
|
||||
("label_lucent_surfaces", "surfaces as controls, rankings, edits", (1.6, -0.82, 0.5), 0.082),
|
||||
]
|
||||
for name, text, loc, size in labels:
|
||||
add_label(name, text, loc, size, mats["label"])
|
||||
|
||||
|
||||
def build_scene():
|
||||
import bpy
|
||||
|
||||
clear_scene()
|
||||
mats = {
|
||||
"tether": mat("Lucent tether graphite", (0.11, 0.12, 0.14), alpha=0.98),
|
||||
"dark": mat("dark service cable", (0.035, 0.04, 0.05), alpha=1.0),
|
||||
"glass": mat("media-eye blue glass", (0.38, 0.72, 1.0), alpha=0.26),
|
||||
"floor": mat("polished lounge floor", (0.47, 0.49, 0.51), alpha=0.9),
|
||||
"lucent_gray": mat("Lucent security gray", (0.54, 0.56, 0.58), alpha=0.95),
|
||||
"security": mat("Lucent gray security figures", (0.23, 0.25, 0.27), alpha=0.98),
|
||||
"soft_white": mat("soft white public line", (0.9, 0.94, 1.0), emission=True, strength=0.35),
|
||||
"safe": mat("safe-line hard white", (1.0, 1.0, 0.94), emission=True, strength=0.8),
|
||||
"cyan": mat("Lucent cyan feed glow", (0.1, 0.86, 1.0), emission=True, strength=0.65),
|
||||
"amber": mat("Feed Ops amber", (1.0, 0.58, 0.08), emission=True, strength=0.45),
|
||||
"risk": mat("sponsor risk magenta", (1.0, 0.16, 0.42), emission=True, strength=0.55),
|
||||
"heat": mat("audience heat red", (1.0, 0.08, 0.06), emission=True, strength=0.6),
|
||||
"pippa": mat("Pippa soft-focus side panel", (0.95, 0.52, 0.78), emission=True, strength=0.35, alpha=0.72),
|
||||
"brant": mat("Brant outrage side panel", (1.0, 0.34, 0.08), emission=True, strength=0.38, alpha=0.72),
|
||||
"overlay_blue": mat("moderation overlay blue", (0.24, 0.48, 1.0), emission=True, strength=0.2, alpha=0.36),
|
||||
"private": mat("Mara private rail green", (0.32, 1.0, 0.62), emission=True, strength=0.38),
|
||||
"aquarium": mat("aquarium blue", (0.02, 0.28, 0.55), alpha=0.58),
|
||||
"city": mat("sealed miniature city", (0.58, 0.62, 0.66), alpha=0.95),
|
||||
"prestige": mat("prestige anchor skyscraper glass", (0.72, 0.82, 0.9), emission=True, strength=0.08, alpha=0.95),
|
||||
"park": mat("interspersed park green", (0.12, 0.48, 0.22), alpha=0.92),
|
||||
"cottage": mat("verdant faux rural cottagecore", (0.46, 0.64, 0.39), alpha=0.96),
|
||||
"plaza": mat("tether anchor plaza stone", (0.68, 0.7, 0.66), alpha=0.96),
|
||||
"solar_panel": mat("sunward blue solar ray florets", (0.08, 0.42, 0.88), emission=True, strength=0.08, alpha=0.58),
|
||||
"radiator": mat("silver radiator ray florets", (0.78, 0.88, 0.86), emission=True, strength=0.04, alpha=0.52),
|
||||
"label": mat("label white", (0.92, 0.95, 1.0), emission=True, strength=0.45),
|
||||
}
|
||||
enrich_shader_graphs(mats)
|
||||
add_habitat_cable_bundle(mats)
|
||||
add_city_bubble(mats)
|
||||
add_city_bubble_tether_attachment(mats)
|
||||
add_media_eye_lounge(mats)
|
||||
add_overlay_ribbons_and_signage(mats)
|
||||
|
||||
bpy.ops.object.light_add(type="AREA", location=(3.8, -4.8, 4.2))
|
||||
key = bpy.context.object
|
||||
key.name = "Lucent soft inspection key"
|
||||
key.data.energy = 620
|
||||
key.data.size = 5.5
|
||||
bpy.ops.object.light_add(type="POINT", location=(4.8, 0.0, 0.25))
|
||||
eye = bpy.context.object
|
||||
eye.name = "media eye internal glow"
|
||||
eye.data.energy = 180
|
||||
eye.data.color = (0.45, 0.85, 1.0)
|
||||
|
||||
bpy.context.scene.render.engine = "CYCLES"
|
||||
bpy.context.scene.cycles.samples = 36
|
||||
bpy.context.scene.cycles.preview_samples = 12
|
||||
bpy.context.scene.cycles.use_denoising = True
|
||||
bpy.context.scene.render.resolution_x = 1200
|
||||
bpy.context.scene.render.resolution_y = 820
|
||||
bpy.context.scene.view_settings.view_transform = "Filmic"
|
||||
bpy.context.scene.view_settings.look = "Medium High Contrast"
|
||||
|
||||
bpy.ops.object.camera_add(location=(6.6, -7.6, 4.9))
|
||||
overview = bpy.context.object
|
||||
overview.name = "Camera_Lucent_Tether_Overview"
|
||||
aim_camera(overview, (0.15, 0.0, 0.0))
|
||||
overview.data.type = "ORTHO"
|
||||
overview.data.ortho_scale = 7.6
|
||||
bpy.context.scene.camera = overview
|
||||
|
||||
bpy.ops.object.camera_add(location=(5.85, -2.95, 1.15))
|
||||
lounge = bpy.context.object
|
||||
lounge.name = "Camera_Media_Eye_Lounge"
|
||||
aim_camera(lounge, (4.58, 0.02, -0.12))
|
||||
lounge.data.type = "ORTHO"
|
||||
lounge.data.ortho_scale = 2.5
|
||||
|
||||
bpy.ops.object.camera_add(location=(-2.75, -3.25, 1.75))
|
||||
city = bpy.context.object
|
||||
city.name = "Camera_City_Bubble_Aquarium"
|
||||
aim_camera(city, CITY_BUBBLE_CENTER)
|
||||
city.data.type = "ORTHO"
|
||||
city.data.ortho_scale = 4.05
|
||||
|
||||
|
||||
def render_camera(camera_name, path):
|
||||
import bpy
|
||||
|
||||
bpy.context.scene.camera = bpy.data.objects[camera_name]
|
||||
bpy.context.scene.render.filepath = str(path.resolve())
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
|
||||
def main():
|
||||
import bpy
|
||||
|
||||
vg_lucent_feed_ribbon()
|
||||
build_scene()
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
blend_path = OUT_DIR / "lucent_tether_habitat.blend"
|
||||
overview_path = OUT_DIR / "lucent_tether_overview.png"
|
||||
lounge_path = OUT_DIR / "lucent_media_eye_lounge.png"
|
||||
city_path = OUT_DIR / "lucent_city_bubble_aquarium.png"
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path.resolve()))
|
||||
render_camera("Camera_Lucent_Tether_Overview", overview_path)
|
||||
render_camera("Camera_Media_Eye_Lounge", lounge_path)
|
||||
render_camera("Camera_City_Bubble_Aquarium", city_path)
|
||||
print("LUCENT_TETHER_BLEND", blend_path.resolve())
|
||||
print("LUCENT_TETHER_OVERVIEW_RENDER", overview_path.resolve())
|
||||
print("LUCENT_TETHER_LOUNGE_RENDER", lounge_path.resolve())
|
||||
print("LUCENT_TETHER_CITY_RENDER", city_path.resolve())
|
||||
group = bpy.data.node_groups.get("VG Lucent Feed Ribbon")
|
||||
if group:
|
||||
print("LUCENT_TETHER_GN_GROUP", group.name, len(group.nodes), len(group.links))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,393 +0,0 @@
|
|||
"""Geometry Script footholds from quellenform/blender-CurveToMeshUV."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from geometry_script.api.node_mapper import OutputsList, set_or_create_link
|
||||
from geometry_script.api.state import State
|
||||
from geometry_script.api.types import Type
|
||||
|
||||
|
||||
def _capture_attribute_item(data_type: str, domain: str, geometry, value, name: str):
|
||||
node = State.current_node_tree.nodes.new("GeometryNodeCaptureAttribute")
|
||||
node.capture_items.new(data_type, name)
|
||||
node.active_index = 0
|
||||
node.domain = domain
|
||||
set_or_create_link(geometry, node.inputs[0])
|
||||
set_or_create_link(value, node.inputs[1])
|
||||
return OutputsList({"geometry": Type(node.outputs[0]), name.lower().replace(" ", "_"): Type(node.outputs[1])})
|
||||
|
||||
|
||||
@tree("VG Auto Smooth")
|
||||
def vg_auto_smooth(geometry: Geometry, angle: Float = 0.0):
|
||||
shallow_enough = compare(
|
||||
operation=Compare.Operation.LESS_EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=edge_angle().unsigned_angle,
|
||||
b=angle,
|
||||
)
|
||||
preserve_smooth_face = boolean_math(
|
||||
operation=BooleanMath.Operation.AND,
|
||||
boolean=(shallow_enough, is_face_smooth()),
|
||||
)
|
||||
edge_pass = set_shade_smooth(
|
||||
domain=SetShadeSmooth.Domain.EDGE,
|
||||
mesh=geometry,
|
||||
selection=is_edge_smooth(),
|
||||
shade_smooth=preserve_smooth_face,
|
||||
)
|
||||
face_pass = set_shade_smooth(
|
||||
domain=SetShadeSmooth.Domain.FACE,
|
||||
mesh=edge_pass,
|
||||
selection=True,
|
||||
shade_smooth=True,
|
||||
)
|
||||
return {"Geometry": face_pass}
|
||||
|
||||
|
||||
@tree("VG Curve To Mesh UV")
|
||||
def vg_curve_to_mesh_uv(
|
||||
curve: Geometry,
|
||||
profile_curve: Geometry,
|
||||
fill_caps: Bool = False,
|
||||
lock_caps_uv_scaling: Bool = False,
|
||||
hole_tolerant: Bool = False,
|
||||
smooth_angle: Float = 3.1415927410125732,
|
||||
free_u_space: Bool = False,
|
||||
lock_u_scaling: Bool = False,
|
||||
free_v_space: Bool = False,
|
||||
lock_v_scaling: Bool = False,
|
||||
sides_uv_scale: Vector = (1.0, 1.0, 0.0),
|
||||
sides_uv_offset: Vector = (0.0, 0.0, 0.0),
|
||||
sides_uv_z_rotation: Float = 0.0,
|
||||
caps_uv_scale: Vector = (1.0, 1.0, 0.0),
|
||||
caps_uv_offset: Vector = (0.0, 0.0, 0.0),
|
||||
caps_uv_z_rotation: Float = 0.0,
|
||||
store_attributes: Bool = False,
|
||||
):
|
||||
del lock_caps_uv_scaling
|
||||
del hole_tolerant
|
||||
del smooth_angle
|
||||
del free_u_space
|
||||
del lock_u_scaling
|
||||
del free_v_space
|
||||
del lock_v_scaling
|
||||
del sides_uv_scale
|
||||
del sides_uv_offset
|
||||
del sides_uv_z_rotation
|
||||
del caps_uv_scale
|
||||
del caps_uv_offset
|
||||
del caps_uv_z_rotation
|
||||
del store_attributes
|
||||
|
||||
resampled_curve = resample_curve(
|
||||
curve=curve,
|
||||
mode="Evaluated",
|
||||
keep_last_segment=False,
|
||||
)
|
||||
cyclic_start = boolean_math(
|
||||
operation=BooleanMath.Operation.AND,
|
||||
boolean=(endpoint_selection(start_size=1, end_size=0), is_spline_cyclic()),
|
||||
)
|
||||
curve_parameter = spline_parameter()
|
||||
curve_length = spline_length()
|
||||
curve_point_index = switch(
|
||||
input_type=Switch.InputType.INT,
|
||||
switch=cyclic_start,
|
||||
false=curve_parameter.index,
|
||||
true=curve_length.point_count,
|
||||
)
|
||||
curve_point_triplet = combine_xyz(x=curve_point_index, y=curve_parameter.index, z=curve_length.point_count)
|
||||
curve_points = _capture_attribute_item("VECTOR", "POINT", resampled_curve, curve_point_triplet, "curve point triplet")
|
||||
curve_indices = _capture_attribute_item("INT", "CURVE", curve_points.geometry, index(), "curve index")
|
||||
|
||||
resampled_profile = resample_curve(
|
||||
curve=profile_curve,
|
||||
mode="Evaluated",
|
||||
keep_last_segment=False,
|
||||
)
|
||||
profile_points = _capture_attribute_item("VECTOR", "POINT", resampled_profile, curve_point_triplet, "profile point triplet")
|
||||
profile_curve_indices = _capture_attribute_item("INT", "CURVE", profile_points.geometry, index(), "profile curve index")
|
||||
profile_point_indices = _capture_attribute_item("INT", "POINT", profile_curve_indices.geometry, index(), "profile point index")
|
||||
|
||||
radius_attribute = named_attribute(data_type=NamedAttribute.DataType.FLOAT, name="radius")
|
||||
radius_scale = switch(input_type=Switch.InputType.FLOAT, switch=radius_attribute.exists, false=1.0, true=radius_attribute.attribute)
|
||||
mesh = curve_to_mesh(
|
||||
curve=curve_indices.geometry,
|
||||
profile_curve=profile_point_indices.geometry,
|
||||
scale=radius_scale,
|
||||
fill_caps=fill_caps,
|
||||
)
|
||||
|
||||
profile_curve_count = domain_size(component="CURVE", geometry=profile_curve_indices.geometry)
|
||||
corner_group = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(curve_indices.curve_index, profile_curve_count.spline_count)),
|
||||
profile_curve_indices.profile_curve_index,
|
||||
),
|
||||
)
|
||||
corner_counter = accumulate_field(
|
||||
data_type=AccumulateField.DataType.INT,
|
||||
domain=AccumulateField.Domain.CORNER,
|
||||
value=1,
|
||||
group_id=corner_group,
|
||||
).trailing
|
||||
|
||||
curve_cyclic = sample_index(
|
||||
data_type=SampleIndex.DataType.BOOLEAN,
|
||||
domain=SampleIndex.Domain.CURVE,
|
||||
clamp=True,
|
||||
geometry=curve_indices.geometry,
|
||||
value=is_spline_cyclic(),
|
||||
index=curve_indices.curve_index,
|
||||
)
|
||||
source_curve_length = spline_length()
|
||||
source_curve_point_count = switch(
|
||||
input_type=Switch.InputType.INT,
|
||||
switch=curve_cyclic,
|
||||
false=math(operation=Math.Operation.SUBTRACT, value=(source_curve_length.point_count, 1.0)),
|
||||
true=source_curve_length.point_count,
|
||||
)
|
||||
source_curve_points = sample_index(
|
||||
data_type=SampleIndex.DataType.INT,
|
||||
domain=SampleIndex.Domain.CURVE,
|
||||
clamp=True,
|
||||
geometry=curve_indices.geometry,
|
||||
value=source_curve_point_count,
|
||||
index=curve_indices.curve_index,
|
||||
)
|
||||
|
||||
profile_cyclic = sample_index(
|
||||
data_type=SampleIndex.DataType.BOOLEAN,
|
||||
domain=SampleIndex.Domain.CURVE,
|
||||
clamp=True,
|
||||
geometry=profile_curve_indices.geometry,
|
||||
value=is_spline_cyclic(),
|
||||
index=profile_curve_indices.profile_curve_index,
|
||||
)
|
||||
source_profile_length = spline_length()
|
||||
source_profile_point_count = switch(
|
||||
input_type=Switch.InputType.INT,
|
||||
switch=profile_cyclic,
|
||||
false=math(operation=Math.Operation.SUBTRACT, value=(source_profile_length.point_count, 1.0)),
|
||||
true=source_profile_length.point_count,
|
||||
)
|
||||
source_profile_points = sample_index(
|
||||
data_type=SampleIndex.DataType.INT,
|
||||
domain=SampleIndex.Domain.CURVE,
|
||||
clamp=True,
|
||||
geometry=profile_curve_indices.geometry,
|
||||
value=source_profile_point_count,
|
||||
index=profile_curve_indices.profile_curve_index,
|
||||
)
|
||||
cap_start = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(source_curve_points, math(operation=Math.Operation.MULTIPLY, value=(source_profile_points, 4.0))),
|
||||
)
|
||||
caps_mask = compare(
|
||||
operation=Compare.Operation.GREATER_EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=corner_counter,
|
||||
b=cap_start,
|
||||
)
|
||||
|
||||
corner_mod = math(operation=Math.Operation.MODULO, value=(index(), 4.0))
|
||||
use_curve_y = compare(
|
||||
operation=Compare.Operation.LESS_EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=corner_mod,
|
||||
b=1,
|
||||
)
|
||||
curve_triplet = separate_xyz(vector=curve_points.curve_point_triplet)
|
||||
side_u = switch(input_type=Switch.InputType.FLOAT, switch=use_curve_y, false=curve_triplet.x, true=curve_triplet.y)
|
||||
|
||||
profile_mod = math(operation=Math.Operation.MODULO, value=(index(), 4.0))
|
||||
use_profile_y = compare(
|
||||
operation=Compare.Operation.NOT_EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=profile_mod,
|
||||
b=1.5,
|
||||
epsilon=0.5,
|
||||
)
|
||||
profile_triplet = separate_xyz(vector=profile_points.profile_point_triplet)
|
||||
side_v = switch(input_type=Switch.InputType.FLOAT, switch=use_profile_y, false=profile_triplet.x, true=profile_triplet.y)
|
||||
side_uv = combine_xyz(x=side_u, y=side_v, z=0.0)
|
||||
|
||||
source_position = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.POINT,
|
||||
clamp=False,
|
||||
geometry=profile_point_indices.geometry,
|
||||
value=position(),
|
||||
index=profile_point_indices.profile_point_index,
|
||||
)
|
||||
profile_bounds = bounding_box(geometry=profile_point_indices.geometry, use_radius=True)
|
||||
cap_uv = map_range(
|
||||
clamp=False,
|
||||
interpolation_type="LINEAR",
|
||||
data_type="FLOAT_VECTOR",
|
||||
vector=source_position,
|
||||
from_min=profile_bounds.min,
|
||||
from_max=profile_bounds.max,
|
||||
to_min=(0.0, 0.0, 0.0),
|
||||
to_max=(1.0, 1.0, 1.0),
|
||||
)
|
||||
uv_map = switch(input_type=Switch.InputType.VECTOR, switch=caps_mask, false=side_uv, true=switch(input_type=Switch.InputType.VECTOR, switch=fill_caps, false=(0.0, 0.0, 0.0), true=cap_uv))
|
||||
return {"Mesh": mesh, "UV Map": uv_map, "Caps Mask": caps_mask}
|
||||
|
||||
|
||||
@tree("VG Curve To Mesh UV Demo")
|
||||
def vg_curve_to_mesh_uv_demo():
|
||||
curve = transform_geometry(
|
||||
geometry=curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=32, radius=1.899999976158142),
|
||||
translation=(0.0, 0.0, 0.0),
|
||||
)
|
||||
profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=16, radius=0.6269999742507935)
|
||||
mesh, uv_map, caps_mask = vg_curve_to_mesh_uv(
|
||||
curve=curve,
|
||||
profile_curve=profile,
|
||||
fill_caps=False,
|
||||
lock_caps_uv_scaling=True,
|
||||
hole_tolerant=True,
|
||||
smooth_angle=3.1415927410125732,
|
||||
free_u_space=True,
|
||||
lock_u_scaling=True,
|
||||
free_v_space=True,
|
||||
lock_v_scaling=True,
|
||||
sides_uv_scale=(1.0, 1.0, 0.0),
|
||||
sides_uv_offset=(0.0, 0.0, 0.0),
|
||||
sides_uv_z_rotation=0.0,
|
||||
caps_uv_scale=(1.0, 1.0, 0.0),
|
||||
caps_uv_offset=(0.0, 0.0, 0.0),
|
||||
caps_uv_z_rotation=0.0,
|
||||
store_attributes=True,
|
||||
)
|
||||
transformed = transform_geometry(
|
||||
geometry=mesh,
|
||||
translation=(2.4099998474121094, 0.0, 3.3399999141693115),
|
||||
rotation=(0.9615018963813782, 0.0, -0.5942845940589905),
|
||||
).set_material(material=None)
|
||||
return {"Geometry": transformed, "UV Map": uv_map, "Caps Mask": caps_mask}
|
||||
|
||||
|
||||
@tree("VG Curve To Mesh UV Title")
|
||||
def vg_curve_to_mesh_uv_title():
|
||||
title = string_to_curves(
|
||||
string=string(string="Curve to Mesh UV"),
|
||||
size=0.6000000238418579,
|
||||
align_x="Center",
|
||||
align_y="Top",
|
||||
pivot_point="Midpoint",
|
||||
).curve_instances
|
||||
subtitle_text = join_strings(
|
||||
delimiter=special_characters().line_break,
|
||||
strings=[
|
||||
string(string="v1.7.02.230210"),
|
||||
string(string="for Blender 3.4+"),
|
||||
string(string="[ Free|Github Edition]"),
|
||||
],
|
||||
)
|
||||
subtitle = transform_geometry(
|
||||
geometry=string_to_curves(
|
||||
string=subtitle_text,
|
||||
size=0.4000000059604645,
|
||||
align_x="Center",
|
||||
align_y="Top",
|
||||
pivot_point="Bottom Left",
|
||||
).curve_instances,
|
||||
translation=(0.0, -0.699999988079071, 0.0),
|
||||
)
|
||||
text = transform_geometry(
|
||||
geometry=join_geometry(geometry=[title, subtitle]),
|
||||
translation=(0.0, 0.0, -1.6699999570846558),
|
||||
rotation=(1.5707963705062866, 0.0, 0.0),
|
||||
)
|
||||
text_face = fill_curve(curve=text, mode="N-gons", fill_rule="Even-Odd").set_material(material=None)
|
||||
|
||||
plinth = cube(size=(4.400000095367432, 0.20000000298023224, 10.0), vertices_x=2, vertices_y=2, vertices_z=2)
|
||||
plinth_uv = store_named_attribute(
|
||||
data_type=StoreNamedAttribute.DataType.FLOAT_VECTOR,
|
||||
domain=StoreNamedAttribute.Domain.CORNER,
|
||||
geometry=plinth.mesh,
|
||||
name="uv_map",
|
||||
value=plinth.uv_map,
|
||||
)
|
||||
plinth_geo = transform_geometry(geometry=plinth_uv, translation=(0.0, 0.17000000178813934, 0.0)).set_material(material=None)
|
||||
|
||||
backdrop = grid(size_x=2.0, size_y=2.0, vertices_x=2, vertices_y=2)
|
||||
backdrop_uv = store_named_attribute(
|
||||
data_type=StoreNamedAttribute.DataType.FLOAT_VECTOR,
|
||||
domain=StoreNamedAttribute.Domain.CORNER,
|
||||
geometry=backdrop.mesh,
|
||||
name="uv_map",
|
||||
value=backdrop.uv_map,
|
||||
)
|
||||
backdrop_plane = transform_geometry(
|
||||
geometry=backdrop_uv,
|
||||
translation=(0.0, -0.019999999552965164, 0.0),
|
||||
rotation=(-1.5707963705062866, 0.0, 0.0),
|
||||
)
|
||||
bounds = bounding_box(geometry=backdrop_plane, use_radius=True)
|
||||
uv = map_range(
|
||||
clamp=False,
|
||||
interpolation_type="LINEAR",
|
||||
data_type="FLOAT_VECTOR",
|
||||
vector=position(),
|
||||
from_min=bounds.min,
|
||||
from_max=bounds.max,
|
||||
to_min=(-1.0, -1.0, -1.0),
|
||||
to_max=(1.0, 1.0, 1.0),
|
||||
)
|
||||
backdrop_geo = store_named_attribute(
|
||||
data_type=StoreNamedAttribute.DataType.FLOAT_VECTOR,
|
||||
domain=StoreNamedAttribute.Domain.CORNER,
|
||||
geometry=backdrop_plane,
|
||||
name="UV",
|
||||
value=uv,
|
||||
).set_material(material=None)
|
||||
return {"Geometry": join_geometry(geometry=[text_face, plinth_geo, flip_faces(mesh=backdrop_geo)])}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in (
|
||||
"VG Auto Smooth",
|
||||
"VG Curve To Mesh UV",
|
||||
"VG Curve To Mesh UV Demo",
|
||||
"VG Curve To Mesh UV Title",
|
||||
):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if group:
|
||||
group.use_fake_user = True
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_CURVE_TO_MESH_UV_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
"""Geometry Script recreation of a helper group from Shriinivas/cartesian.blend.
|
||||
|
||||
Source:
|
||||
https://github.com/Shriinivas/geometrynodes/blob/main/cartesian.blend
|
||||
|
||||
The first source group is named ``NodeGroup.001``. It creates a resampled curve
|
||||
line and a vector field where Y is lifted by ``sqrt(r^2 - x^2) * value`` while
|
||||
X and Z are preserved from the current position. The other groups in this file
|
||||
translate the scene-facing line, parabola, and circle node groups from the same
|
||||
source file.
|
||||
"""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
|
||||
|
||||
def _cartesian_helper_nodes(count: Int, start: Vector, end: Vector, r: Float, value: Float):
|
||||
curve = curve_line(mode=CurveLine.Mode.POINTS, start=start, end=end)
|
||||
sampled_curve = curve.resample_curve(count=count)
|
||||
|
||||
position_parts = separate_xyz(vector=position())
|
||||
radius_squared = math(operation=Math.Operation.POWER, value=(r, 2.0))
|
||||
x_squared = math(operation=Math.Operation.POWER, value=(position_parts.x, 2.0))
|
||||
y = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
math(
|
||||
operation=Math.Operation.SQRT,
|
||||
value=math(operation=Math.Operation.SUBTRACT, value=(radius_squared, x_squared)),
|
||||
),
|
||||
value,
|
||||
),
|
||||
)
|
||||
vector = combine_xyz(x=position_parts.x, y=y, z=position_parts.z)
|
||||
|
||||
return sampled_curve, vector
|
||||
|
||||
|
||||
@tree("VG Cartesian Helper")
|
||||
def vg_cartesian_helper(
|
||||
count: Int = 10,
|
||||
start: Vector = (0.0, 0.0, 0.0),
|
||||
end: Vector = (0.0, 0.0, 1.0),
|
||||
r: Float = 1.0,
|
||||
value: Float = 1.0,
|
||||
):
|
||||
sampled_curve, vector = _cartesian_helper_nodes(count=count, start=start, end=end, r=r, value=value)
|
||||
return {"Curve": sampled_curve, "Vector": vector}
|
||||
|
||||
|
||||
def _radius_scale():
|
||||
radius = named_attribute(data_type=NamedAttribute.DataType.FLOAT, name="radius")
|
||||
return switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=radius.exists,
|
||||
false=1.0,
|
||||
true=radius.attribute,
|
||||
)
|
||||
|
||||
|
||||
def _tube_curve(curve: Geometry, thickness: Float):
|
||||
profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=32, radius=thickness)
|
||||
return curve.curve_to_mesh(profile_curve=profile, scale=_radius_scale())
|
||||
|
||||
|
||||
@tree("VG Cartesian Line")
|
||||
def vg_cartesian_line(
|
||||
geometry: Geometry,
|
||||
resolution: Int = 10,
|
||||
length: Float = 0.5,
|
||||
m: Float = 0.5,
|
||||
value: Float = 0.5,
|
||||
thickness: Float = 1.0,
|
||||
):
|
||||
del geometry, resolution
|
||||
|
||||
end = combine_xyz(x=length, y=0.0, z=0.0)
|
||||
curve = curve_line(mode=CurveLine.Mode.POINTS, start=(0.0, 0.0, 0.0), end=end)
|
||||
parts = separate_xyz(vector=position())
|
||||
y = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(math(operation=Math.Operation.MULTIPLY, value=(parts.x, m)), value),
|
||||
)
|
||||
vector = combine_xyz(x=parts.x, y=y, z=parts.z)
|
||||
positioned = curve.set_position(position=vector)
|
||||
return _tube_curve(positioned, thickness)
|
||||
|
||||
|
||||
@tree("VG Cartesian Parabola")
|
||||
def vg_cartesian_parabola(
|
||||
geometry: Geometry,
|
||||
resolution: Int = 10,
|
||||
sample_resolution: Int = 100,
|
||||
start: Float = 0.0,
|
||||
end: Float = 0.0,
|
||||
a: Float = 2.0,
|
||||
b: Float = 2.0,
|
||||
c: Float = 0.5,
|
||||
thickness: Float = 1.0,
|
||||
):
|
||||
del geometry, resolution
|
||||
|
||||
curve_start = combine_xyz(x=start, y=0.0, z=0.0)
|
||||
curve_end = combine_xyz(x=end, y=0.0, z=0.0)
|
||||
curve = curve_line(mode=CurveLine.Mode.POINTS, start=curve_start, end=curve_end)
|
||||
sampled = curve.resample_curve(count=sample_resolution)
|
||||
|
||||
parts = separate_xyz(vector=position())
|
||||
x_squared = math(operation=Math.Operation.POWER, value=(parts.x, 2.0))
|
||||
ax_squared = math(operation=Math.Operation.MULTIPLY, value=(x_squared, a))
|
||||
bx = math(operation=Math.Operation.MULTIPLY, value=(parts.x, b))
|
||||
y = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(math(operation=Math.Operation.ADD, value=(ax_squared, bx)), c),
|
||||
)
|
||||
vector = combine_xyz(x=parts.x, y=y, z=parts.z)
|
||||
positioned = sampled.set_position(position=vector)
|
||||
return _tube_curve(positioned, thickness)
|
||||
|
||||
|
||||
@tree("VG Cartesian Circle")
|
||||
def vg_cartesian_circle(
|
||||
geometry: Geometry,
|
||||
resolution: Int = 10,
|
||||
r: Float = 0.5,
|
||||
thickness: Float = 1.0,
|
||||
):
|
||||
del geometry
|
||||
|
||||
left = combine_xyz(x=math(operation=Math.Operation.MULTIPLY, value=(r, -1.0)), y=0.0, z=0.0)
|
||||
right = combine_xyz(x=r, y=0.0, z=0.0)
|
||||
upper_curve, upper_vector = vg_cartesian_helper(
|
||||
count=resolution,
|
||||
start=left,
|
||||
end=right,
|
||||
r=r,
|
||||
value=1.0,
|
||||
)
|
||||
lower_curve, lower_vector = vg_cartesian_helper(
|
||||
count=resolution,
|
||||
start=left,
|
||||
end=right,
|
||||
r=r,
|
||||
value=-1.0,
|
||||
)
|
||||
|
||||
upper = upper_curve.set_position(position=upper_vector)
|
||||
lower = lower_curve.set_position(position=lower_vector)
|
||||
return _tube_curve(join_geometry(geometry=[upper, lower]), thickness)
|
||||
|
||||
|
||||
def _finalize_group():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in ("VG Cartesian Helper", "VG Cartesian Line", "VG Cartesian Parabola", "VG Cartesian Circle"):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if not group:
|
||||
continue
|
||||
group.use_fake_user = True
|
||||
if hasattr(group, "interface"):
|
||||
for item in group.interface.items_tree:
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name == "R":
|
||||
item.name = "r"
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name in {"M", "A", "B", "C"}:
|
||||
item.name = item.name.lower()
|
||||
if (
|
||||
item.item_type == "SOCKET"
|
||||
and item.in_out == "OUTPUT"
|
||||
and item.name == "Result"
|
||||
and name != "VG Cartesian Helper"
|
||||
):
|
||||
item.name = "Geometry"
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_group()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_group():
|
||||
print(
|
||||
"VG_CARTESIAN_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,911 +0,0 @@
|
|||
"""Geometry Script recreation of a scalar helper from Shriinivas/fieldvalue.blend."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from geometry_script.api.node_mapper import OutputsList, set_or_create_link
|
||||
from geometry_script.api.state import State
|
||||
from geometry_script.api.types import Type
|
||||
|
||||
|
||||
def _capture_attribute_item(data_type: str, domain: str, geometry, value, name: str):
|
||||
node = State.current_node_tree.nodes.new("GeometryNodeCaptureAttribute")
|
||||
node.capture_items.new(data_type, name)
|
||||
node.active_index = 0
|
||||
node.domain = domain
|
||||
set_or_create_link(geometry, node.inputs[0])
|
||||
set_or_create_link(value, node.inputs[1])
|
||||
|
||||
key = name.lower().replace(" ", "_")
|
||||
return OutputsList({"geometry": Type(node.outputs[0]), key: Type(node.outputs[1])})
|
||||
|
||||
|
||||
def _int_equal(value, target: int):
|
||||
return compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=value,
|
||||
b=target,
|
||||
)
|
||||
|
||||
|
||||
def _float_equal(value, target: float):
|
||||
return compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=value,
|
||||
b=target,
|
||||
)
|
||||
|
||||
|
||||
@tree("VG Create Decimal")
|
||||
def vg_create_decimal(vertical_segment_size: Float = 2.0, radius: Float = 0.0):
|
||||
dot = mesh_circle(fill_type=MeshCircle.FillType.NGON, vertices=32, radius=radius)
|
||||
offset = combine_xyz(
|
||||
x=0.0,
|
||||
y=math(operation=Math.Operation.MULTIPLY, value=(-1.0, vertical_segment_size)),
|
||||
z=0.0,
|
||||
)
|
||||
return {"Dot": dot.set_position(offset=offset)}
|
||||
|
||||
|
||||
@tree("VG Digit At")
|
||||
def vg_digit_at(number: Float = 0.0, position: Int = 0):
|
||||
shifted_position = math(operation=Math.Operation.ADD, value=(position, 1.0))
|
||||
upper_scale = math(operation=Math.Operation.POWER, value=(10.0, shifted_position))
|
||||
shifted_number = math(operation=Math.Operation.DIVIDE, value=(number, upper_scale))
|
||||
fractional_window = math(operation=Math.Operation.FRACT, value=shifted_number)
|
||||
digit_window = math(operation=Math.Operation.MULTIPLY, value=(10.0, fractional_window))
|
||||
|
||||
position_scale = math(operation=Math.Operation.POWER, value=(10.0, position))
|
||||
rounded_scaled_window = math(
|
||||
operation=Math.Operation.ROUND,
|
||||
value=math(operation=Math.Operation.MULTIPLY, value=(digit_window, position_scale)),
|
||||
)
|
||||
digit_value = math(operation=Math.Operation.DIVIDE, value=(rounded_scaled_window, position_scale))
|
||||
|
||||
is_ones_position = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=position,
|
||||
b=0,
|
||||
)
|
||||
return switch(
|
||||
input_type=Switch.InputType.INT,
|
||||
switch=is_ones_position,
|
||||
false=math(operation=Math.Operation.FLOOR, value=digit_value),
|
||||
true=math(operation=Math.Operation.ROUND, value=digit_value),
|
||||
)
|
||||
|
||||
|
||||
@tree("VG Next Digit")
|
||||
def vg_next_digit(
|
||||
whole_part: Float = 0.0,
|
||||
fraction_part: Float = 0.0,
|
||||
max_precision: Int = 0,
|
||||
position: Int = 0,
|
||||
):
|
||||
whole_position = math(operation=Math.Operation.SUBTRACT, value=(position, max_precision))
|
||||
use_fraction = math(operation=Math.Operation.LESS_THAN, value=(position, max_precision))
|
||||
whole_digit = vg_digit_at(number=whole_part, position=whole_position)
|
||||
fraction_digit = vg_digit_at(number=fraction_part, position=position)
|
||||
return switch(
|
||||
input_type=Switch.InputType.INT,
|
||||
switch=use_fraction,
|
||||
false=whole_digit,
|
||||
true=fraction_digit,
|
||||
)
|
||||
|
||||
|
||||
@tree("VG Create Segment")
|
||||
def vg_create_segment(
|
||||
x_major: Bool = False,
|
||||
segment_length_along_x: Float = 0.0,
|
||||
segment_length_along_y: Float = 0.0,
|
||||
secondary_axis_segment_thickness: Float = 0.0,
|
||||
sharpness: Float = 0.0,
|
||||
offset: Vector = (0.0, 0.0, 0.0),
|
||||
):
|
||||
position_parts = separate_xyz(vector=position())
|
||||
major_axis = switch(input_type=Switch.InputType.FLOAT, switch=x_major, false=position_parts.y, true=position_parts.x)
|
||||
secondary_axis = switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=x_major,
|
||||
false=position_parts.x,
|
||||
true=position_parts.y,
|
||||
)
|
||||
|
||||
not_x_major = math(operation=Math.Operation.SUBTRACT, value=(1.0, x_major))
|
||||
vertices_x = math(operation=Math.Operation.ADD, value=(2.0, not_x_major))
|
||||
vertices_y = math(operation=Math.Operation.ADD, value=(2.0, x_major))
|
||||
|
||||
tip_amount = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(0.5, sharpness)),
|
||||
secondary_axis_segment_thickness,
|
||||
),
|
||||
)
|
||||
tip_vector = combine_xyz(
|
||||
x=math(operation=Math.Operation.MULTIPLY, value=(x_major, tip_amount)),
|
||||
y=math(operation=Math.Operation.MULTIPLY, value=(not_x_major, tip_amount)),
|
||||
z=0.0,
|
||||
)
|
||||
|
||||
has_secondary_axis = compare(
|
||||
operation=Compare.Operation.NOT_EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=secondary_axis,
|
||||
b=0.0,
|
||||
)
|
||||
negative_tip_gate = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.LESS_THAN, value=(major_axis, 0.0)), has_secondary_axis),
|
||||
)
|
||||
positive_tip_gate = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.GREATER_THAN, value=(major_axis, 0.0)), has_secondary_axis),
|
||||
)
|
||||
negative_tip = vector_math(operation=VectorMath.Operation.MULTIPLY, vector=(-1.0, tip_vector))
|
||||
positive_gate_vector = combine_xyz(x=positive_tip_gate, y=positive_tip_gate, z=positive_tip_gate)
|
||||
negative_gate_vector = combine_xyz(x=negative_tip_gate, y=negative_tip_gate, z=negative_tip_gate)
|
||||
positive_end_offset = vector_math(operation=VectorMath.Operation.MULTIPLY, vector=(negative_tip, positive_gate_vector))
|
||||
negative_end_offset = vector_math(operation=VectorMath.Operation.MULTIPLY, vector=(tip_vector, negative_gate_vector))
|
||||
total_offset = vector_math(
|
||||
operation=VectorMath.Operation.ADD,
|
||||
vector=(
|
||||
vector_math(operation=VectorMath.Operation.ADD, vector=(offset, positive_end_offset)),
|
||||
negative_end_offset,
|
||||
),
|
||||
)
|
||||
|
||||
segment = grid(
|
||||
size_x=segment_length_along_x,
|
||||
size_y=segment_length_along_y,
|
||||
vertices_x=vertices_x,
|
||||
vertices_y=vertices_y,
|
||||
).mesh
|
||||
return {"Segment": segment.set_position(offset=total_offset)}
|
||||
|
||||
|
||||
@tree("VG Seven Segments")
|
||||
def vg_seven_segments(
|
||||
horizontal_segment_size: Float = 2.0,
|
||||
horizontal_segment_thickness: Float = 0.5,
|
||||
vertical_segment_size: Float = 2.0,
|
||||
vertical_segment_thickness: Float = 0.5,
|
||||
x_separation: Float = 0.1,
|
||||
y_seperation: Float = 0.1,
|
||||
tip_sharpness: Float = 1.0,
|
||||
):
|
||||
half_horizontal = math(operation=Math.Operation.DIVIDE, value=(horizontal_segment_size, 2.0))
|
||||
half_vertical = math(operation=Math.Operation.DIVIDE, value=(vertical_segment_size, 2.0))
|
||||
negative_half_horizontal = math(operation=Math.Operation.MULTIPLY, value=(-1.0, half_horizontal))
|
||||
negative_half_vertical = math(operation=Math.Operation.MULTIPLY, value=(-1.0, half_vertical))
|
||||
|
||||
vertical_bar_length = math(operation=Math.Operation.SUBTRACT, value=(vertical_segment_size, y_seperation))
|
||||
horizontal_bar_length = math(operation=Math.Operation.SUBTRACT, value=(horizontal_segment_size, x_separation))
|
||||
|
||||
lower_left = vg_create_segment(
|
||||
x_major=False,
|
||||
segment_length_along_x=vertical_segment_thickness,
|
||||
segment_length_along_y=vertical_bar_length,
|
||||
secondary_axis_segment_thickness=horizontal_segment_thickness,
|
||||
sharpness=tip_sharpness,
|
||||
offset=combine_xyz(x=negative_half_horizontal, y=negative_half_vertical, z=0.0),
|
||||
)
|
||||
upper_left = vg_create_segment(
|
||||
x_major=False,
|
||||
segment_length_along_x=vertical_segment_thickness,
|
||||
segment_length_along_y=vertical_bar_length,
|
||||
secondary_axis_segment_thickness=horizontal_segment_thickness,
|
||||
sharpness=tip_sharpness,
|
||||
offset=combine_xyz(x=negative_half_horizontal, y=half_vertical, z=0.0),
|
||||
)
|
||||
lower_right = vg_create_segment(
|
||||
x_major=False,
|
||||
segment_length_along_x=vertical_segment_thickness,
|
||||
segment_length_along_y=vertical_bar_length,
|
||||
secondary_axis_segment_thickness=horizontal_segment_thickness,
|
||||
sharpness=tip_sharpness,
|
||||
offset=combine_xyz(x=half_horizontal, y=negative_half_vertical, z=0.0),
|
||||
)
|
||||
upper_right = vg_create_segment(
|
||||
x_major=False,
|
||||
segment_length_along_x=vertical_segment_thickness,
|
||||
segment_length_along_y=vertical_bar_length,
|
||||
secondary_axis_segment_thickness=horizontal_segment_thickness,
|
||||
sharpness=tip_sharpness,
|
||||
offset=combine_xyz(x=half_horizontal, y=half_vertical, z=0.0),
|
||||
)
|
||||
|
||||
bottom = vg_create_segment(
|
||||
x_major=True,
|
||||
segment_length_along_x=horizontal_bar_length,
|
||||
segment_length_along_y=horizontal_segment_thickness,
|
||||
secondary_axis_segment_thickness=vertical_segment_thickness,
|
||||
sharpness=tip_sharpness,
|
||||
offset=combine_xyz(x=0.0, y=math(operation=Math.Operation.MULTIPLY, value=(-1.0, vertical_segment_size)), z=0.0),
|
||||
)
|
||||
middle = vg_create_segment(
|
||||
x_major=True,
|
||||
segment_length_along_x=horizontal_bar_length,
|
||||
segment_length_along_y=horizontal_segment_thickness,
|
||||
secondary_axis_segment_thickness=vertical_segment_thickness,
|
||||
sharpness=tip_sharpness,
|
||||
offset=(0.0, 0.0, 0.0),
|
||||
)
|
||||
top = vg_create_segment(
|
||||
x_major=True,
|
||||
segment_length_along_x=horizontal_bar_length,
|
||||
segment_length_along_y=horizontal_segment_thickness,
|
||||
secondary_axis_segment_thickness=vertical_segment_thickness,
|
||||
sharpness=tip_sharpness,
|
||||
offset=combine_xyz(x=0.0, y=vertical_segment_size, z=0.0),
|
||||
)
|
||||
dot = vg_create_decimal(
|
||||
vertical_segment_size=vertical_segment_size,
|
||||
radius=math(operation=Math.Operation.DIVIDE, value=(horizontal_segment_thickness, 2.0)),
|
||||
)
|
||||
|
||||
return {
|
||||
"bars": join_geometry(
|
||||
geometry=[
|
||||
dot,
|
||||
top,
|
||||
middle,
|
||||
bottom,
|
||||
upper_right,
|
||||
lower_right,
|
||||
upper_left,
|
||||
lower_left,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@tree("VG Delete Segments")
|
||||
def vg_delete_segments(digit: Int = 0, segment_position: Int = 0):
|
||||
digit_minus_one = math(operation=Math.Operation.SUBTRACT, value=(digit, 1.0))
|
||||
|
||||
p01 = _int_equal(segment_position, 0) | _int_equal(segment_position, 1)
|
||||
p23 = _int_equal(segment_position, 2) | _int_equal(segment_position, 3)
|
||||
p45 = _int_equal(segment_position, 4) | _int_equal(segment_position, 5)
|
||||
p67 = _int_equal(segment_position, 6) | _int_equal(segment_position, 7)
|
||||
p89 = _int_equal(segment_position, 8) | _int_equal(segment_position, 9)
|
||||
p1011 = _int_equal(segment_position, 10) | _int_equal(segment_position, 11)
|
||||
p1213 = _int_equal(segment_position, 12) | _int_equal(segment_position, 13)
|
||||
p14 = _int_equal(segment_position, 14)
|
||||
|
||||
d_minus_one = _float_equal(digit_minus_one, -1.0)
|
||||
d0 = _float_equal(digit_minus_one, 0.0)
|
||||
d1 = _float_equal(digit_minus_one, 1.0)
|
||||
d2 = _float_equal(digit_minus_one, 2.0)
|
||||
d3 = _float_equal(digit_minus_one, 3.0)
|
||||
d4 = _float_equal(digit_minus_one, 4.0)
|
||||
d5 = _float_equal(digit_minus_one, 5.0)
|
||||
d6 = _float_equal(digit_minus_one, 6.0)
|
||||
d7 = _float_equal(digit_minus_one, 7.0)
|
||||
d9 = _float_equal(digit_minus_one, 9.0)
|
||||
d10 = _float_equal(digit_minus_one, 10.0)
|
||||
d11 = _float_equal(digit_minus_one, 11.0)
|
||||
|
||||
delete_for_minus_one = d_minus_one
|
||||
delete_for_zero = d0 & p1011
|
||||
delete_for_one = d1 & ~(p45 | p67)
|
||||
delete_for_two = d2 & (p23 | p45 | p14)
|
||||
delete_for_three = d3 & (p01 | p23 | p14)
|
||||
delete_for_four = d4 & (p89 | p1213 | p01 | p14)
|
||||
delete_for_five = d5 & (p01 | p67 | p14)
|
||||
delete_for_six = d6 & (p67 | p14)
|
||||
delete_for_seven = d7 & ~(p1213 | p45 | p67)
|
||||
delete_for_nine = d9 & (p01 | p14)
|
||||
delete_for_ten = d10 & ~p1011
|
||||
delete_for_eleven = d11 & ~p14
|
||||
delete_for_decimal = p14 & ~d11
|
||||
|
||||
return (
|
||||
delete_for_eleven
|
||||
| delete_for_ten
|
||||
| delete_for_minus_one
|
||||
| delete_for_zero
|
||||
| delete_for_one
|
||||
| delete_for_two
|
||||
| delete_for_three
|
||||
| delete_for_four
|
||||
| delete_for_five
|
||||
| delete_for_six
|
||||
| delete_for_seven
|
||||
| delete_for_decimal
|
||||
| delete_for_nine
|
||||
)
|
||||
|
||||
|
||||
@tree("VG Field Value")
|
||||
def vg_field_value(
|
||||
geometry: Geometry,
|
||||
input_type: Menu = "Float",
|
||||
float_value: Float = 0.0,
|
||||
vector_value: Vector = (0.0, 0.0, 0.0),
|
||||
digits_before_decimal: Int = 4,
|
||||
precision: Int = 2,
|
||||
domain: Menu = "Point",
|
||||
alignment: Menu = "Center",
|
||||
align_to_global: Bool = True,
|
||||
scale: Float = 0.019999999552965164,
|
||||
digit_separation: Float = 0.30000001192092896,
|
||||
element_separation: Float = 1.0,
|
||||
x_offset: Float = 0.0,
|
||||
y_offset: Float = 0.0,
|
||||
z_offset: Float = 0.0010000000474974513,
|
||||
rotation: Int = 0,
|
||||
segment_width_factor: Float = 0.5,
|
||||
segment_separation_factor: Float = 0.10000000149011612,
|
||||
tip_sharpness: Float = 1.0,
|
||||
leading_zeros: Bool = False,
|
||||
material: Material = None,
|
||||
):
|
||||
domain_name = menu_switch(
|
||||
active_index=2,
|
||||
data_type="STRING",
|
||||
menu=domain,
|
||||
point="Point",
|
||||
face="Face",
|
||||
edge="Edge",
|
||||
)
|
||||
use_face_domain = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.STRING,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=domain_name.output,
|
||||
b="Face",
|
||||
)
|
||||
use_edge_domain = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.STRING,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=domain_name.output,
|
||||
b="Edge",
|
||||
)
|
||||
value_kind = menu_switch(
|
||||
active_index=1,
|
||||
data_type="STRING",
|
||||
menu=input_type,
|
||||
float="Float",
|
||||
vector="Vector",
|
||||
)
|
||||
use_vector_value = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.STRING,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=value_kind.output,
|
||||
b="Vector",
|
||||
)
|
||||
|
||||
point_float = _capture_attribute_item("FLOAT", "POINT", geometry, float_value, "float field")
|
||||
edge_float = _capture_attribute_item("FLOAT", "EDGE", geometry, float_value, "float field")
|
||||
face_float = _capture_attribute_item("FLOAT", "FACE", geometry, float_value, "float field")
|
||||
point_vector = _capture_attribute_item("VECTOR", "POINT", geometry, vector_value, "vector field")
|
||||
edge_vector = _capture_attribute_item("VECTOR", "EDGE", geometry, vector_value, "vector field")
|
||||
face_vector = _capture_attribute_item("VECTOR", "FACE", geometry, vector_value, "vector field")
|
||||
|
||||
captured_float_geometry = switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=use_face_domain,
|
||||
false=switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=use_edge_domain,
|
||||
false=point_float.geometry,
|
||||
true=edge_float.geometry,
|
||||
),
|
||||
true=face_float.geometry,
|
||||
)
|
||||
captured_vector_geometry = switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=use_face_domain,
|
||||
false=switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=use_edge_domain,
|
||||
false=point_vector.geometry,
|
||||
true=edge_vector.geometry,
|
||||
),
|
||||
true=face_vector.geometry,
|
||||
)
|
||||
sampled_source = switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=use_vector_value,
|
||||
false=captured_float_geometry,
|
||||
true=captured_vector_geometry,
|
||||
)
|
||||
source_points = switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=use_face_domain,
|
||||
false=switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=use_edge_domain,
|
||||
false=sampled_source,
|
||||
true=mesh_to_points(mode="EDGES", mesh=sampled_source, radius=0.05000000074505806),
|
||||
),
|
||||
true=mesh_to_points(mode="FACES", mesh=sampled_source, radius=0.05000000074505806),
|
||||
)
|
||||
|
||||
component_count = switch(input_type=Switch.InputType.INT, switch=use_vector_value, false=1, true=3)
|
||||
|
||||
segment_glyph = vg_seven_segments(
|
||||
horizontal_segment_size=2.0,
|
||||
horizontal_segment_thickness=segment_width_factor,
|
||||
vertical_segment_size=2.0,
|
||||
vertical_segment_thickness=segment_width_factor,
|
||||
x_separation=segment_separation_factor,
|
||||
y_seperation=segment_separation_factor,
|
||||
tip_sharpness=tip_sharpness,
|
||||
)
|
||||
glyph_faces = _capture_attribute_item("INT", "FACE", segment_glyph, index(), "int field")
|
||||
glyph_position = separate_xyz(vector=position())
|
||||
glyph_y_stats = attribute_statistic(
|
||||
data_type=AttributeStatistic.DataType.FLOAT,
|
||||
domain=AttributeStatistic.Domain.POINT,
|
||||
geometry=glyph_faces.geometry,
|
||||
attribute=glyph_position.y,
|
||||
)
|
||||
glyph_height = math(operation=Math.Operation.SUBTRACT, value=(glyph_y_stats.max, glyph_y_stats.min))
|
||||
component_spacing = math(operation=Math.Operation.ADD, value=(glyph_height, element_separation))
|
||||
vector_component_step = switch(input_type=Switch.InputType.FLOAT, switch=use_vector_value, false=0.0, true=component_spacing)
|
||||
component_points = mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=component_count,
|
||||
start_location=combine_xyz(x=0.0, y=vector_component_step, z=0.0),
|
||||
offset=combine_xyz(
|
||||
x=0.0,
|
||||
y=math(operation=Math.Operation.MULTIPLY, value=(-1.0, vector_component_step)),
|
||||
z=0.0,
|
||||
),
|
||||
)
|
||||
|
||||
digit_count_without_decimal = math(operation=Math.Operation.ADD, value=(digits_before_decimal, precision))
|
||||
digit_count_with_decimal = math(operation=Math.Operation.ADD, value=(digit_count_without_decimal, 1.0))
|
||||
has_precision = math(operation=Math.Operation.GREATER_THAN, value=(precision, 0.0))
|
||||
display_digit_slots = math(operation=Math.Operation.ADD, value=(digit_count_with_decimal, has_precision))
|
||||
|
||||
glyph_x_stats = attribute_statistic(
|
||||
data_type=AttributeStatistic.DataType.FLOAT,
|
||||
domain=AttributeStatistic.Domain.POINT,
|
||||
geometry=glyph_faces.geometry,
|
||||
attribute=glyph_position.x,
|
||||
)
|
||||
glyph_width = math(operation=Math.Operation.SUBTRACT, value=(glyph_x_stats.max, glyph_x_stats.min))
|
||||
digit_pitch = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.ADD, value=(1.0, digit_separation)), glyph_width),
|
||||
)
|
||||
negative_digit_pitch = math(operation=Math.Operation.MULTIPLY, value=(-1.0, digit_pitch))
|
||||
digit_step = combine_xyz(x=negative_digit_pitch, y=0.0, z=0.0)
|
||||
digit_points = mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=display_digit_slots,
|
||||
start_location=vector_math(operation=VectorMath.Operation.DIVIDE, vector=(digit_step, (2.0, 0.0, 0.0))),
|
||||
offset=digit_step,
|
||||
)
|
||||
digit_instances = instance_on_points(
|
||||
points=digit_points,
|
||||
instance=glyph_faces.geometry,
|
||||
pick_instance=False,
|
||||
rotation=(0.0, 0.0, 0.0),
|
||||
scale=(1.0, 1.0, 1.0),
|
||||
)
|
||||
decimal_slot = math(
|
||||
operation=Math.Operation.SUBTRACT,
|
||||
value=(math(operation=Math.Operation.SUBTRACT, value=(display_digit_slots, precision)), 0.5),
|
||||
)
|
||||
decimal_offset = math(operation=Math.Operation.MULTIPLY, value=(decimal_slot, digit_pitch))
|
||||
final_digit_index = math(operation=Math.Operation.SUBTRACT, value=(display_digit_slots, 1.0))
|
||||
is_final_digit_slot = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=index(),
|
||||
b=final_digit_index,
|
||||
epsilon=0.0010000000474974513,
|
||||
)
|
||||
decimal_shifted_digits = set_position(
|
||||
geometry=digit_instances,
|
||||
offset=combine_xyz(
|
||||
x=math(operation=Math.Operation.MULTIPLY, value=(decimal_offset, is_final_digit_slot)),
|
||||
y=0.0,
|
||||
z=math(operation=Math.Operation.MULTIPLY, value=(-0.0010000000474974513, is_final_digit_slot)),
|
||||
),
|
||||
)
|
||||
digit_indexed = _capture_attribute_item("INT", "INSTANCE", decimal_shifted_digits, index(), "int field")
|
||||
component_instances = instance_on_points(
|
||||
points=component_points,
|
||||
instance=digit_indexed.geometry,
|
||||
pick_instance=False,
|
||||
rotation=(0.0, 0.0, 0.0),
|
||||
scale=(1.0, 1.0, 1.0),
|
||||
)
|
||||
component_indexed = _capture_attribute_item("INT", "INSTANCE", component_instances, index(), "int field")
|
||||
value_instances = instance_on_points(
|
||||
points=source_points,
|
||||
instance=component_indexed.geometry,
|
||||
pick_instance=False,
|
||||
rotation=(0.0, 0.0, 0.0),
|
||||
scale=(1.0, 1.0, 1.0),
|
||||
)
|
||||
|
||||
point_position = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.POINT,
|
||||
clamp=False,
|
||||
geometry=sampled_source,
|
||||
value=position(),
|
||||
index=index(),
|
||||
)
|
||||
edge_position = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.EDGE,
|
||||
clamp=False,
|
||||
geometry=sampled_source,
|
||||
value=position(),
|
||||
index=index(),
|
||||
)
|
||||
face_position = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.FACE,
|
||||
clamp=False,
|
||||
geometry=sampled_source,
|
||||
value=position(),
|
||||
index=index(),
|
||||
)
|
||||
source_position = switch(
|
||||
input_type=Switch.InputType.VECTOR,
|
||||
switch=use_face_domain,
|
||||
false=switch(input_type=Switch.InputType.VECTOR, switch=use_edge_domain, false=point_position, true=edge_position),
|
||||
true=face_position,
|
||||
)
|
||||
|
||||
alignment_name = menu_switch(
|
||||
active_index=2,
|
||||
data_type="STRING",
|
||||
menu=alignment,
|
||||
center="Center",
|
||||
right="Right",
|
||||
left="Left",
|
||||
)
|
||||
align_left = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.STRING,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=alignment_name.output,
|
||||
b="Left",
|
||||
)
|
||||
align_right = compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.STRING,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=alignment_name.output,
|
||||
b="Right",
|
||||
)
|
||||
scaled_digit_pitch = math(operation=Math.Operation.MULTIPLY, value=(scale, digit_pitch))
|
||||
|
||||
captured_float = switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=use_face_domain,
|
||||
false=switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=use_edge_domain,
|
||||
false=point_float.float_field,
|
||||
true=edge_float.float_field,
|
||||
),
|
||||
true=face_float.float_field,
|
||||
)
|
||||
captured_vector = switch(
|
||||
input_type=Switch.InputType.VECTOR,
|
||||
switch=use_face_domain,
|
||||
false=switch(
|
||||
input_type=Switch.InputType.VECTOR,
|
||||
switch=use_edge_domain,
|
||||
false=point_vector.vector_field,
|
||||
true=edge_vector.vector_field,
|
||||
),
|
||||
true=face_vector.vector_field,
|
||||
)
|
||||
vector_parts = separate_xyz(vector=captured_vector)
|
||||
component_value = index_switch(
|
||||
data_type=IndexSwitch.DataType.FLOAT,
|
||||
index=component_indexed.int_field,
|
||||
_0=vector_parts.x,
|
||||
_1=vector_parts.y,
|
||||
_2=vector_parts.z,
|
||||
)
|
||||
displayed_value = switch(input_type=Switch.InputType.FLOAT, switch=use_vector_value, false=captured_float, true=component_value)
|
||||
absolute_value = math(operation=Math.Operation.ABSOLUTE, value=displayed_value)
|
||||
whole_part = math(operation=Math.Operation.TRUNC, value=absolute_value)
|
||||
whole_part_digits = math(
|
||||
operation=Math.Operation.MAXIMUM,
|
||||
value=(
|
||||
math(
|
||||
operation=Math.Operation.FLOOR,
|
||||
value=math(
|
||||
operation=Math.Operation.LOGARITHM,
|
||||
value=(math(operation=Math.Operation.MULTIPLY, value=(whole_part, 10.0)), 10.0),
|
||||
),
|
||||
),
|
||||
1.0,
|
||||
),
|
||||
)
|
||||
natural_digit_count = math(operation=Math.Operation.ADD, value=(whole_part_digits, precision))
|
||||
is_negative = math(operation=Math.Operation.LESS_THAN, value=(displayed_value, 0.0))
|
||||
natural_slot_count = math(operation=Math.Operation.ADD, value=(natural_digit_count, is_negative))
|
||||
half_text_width = math(
|
||||
operation=Math.Operation.DIVIDE,
|
||||
value=(math(operation=Math.Operation.MULTIPLY, value=(scaled_digit_pitch, natural_slot_count)), 2.0),
|
||||
)
|
||||
alignment_offset = switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=align_left,
|
||||
false=switch(input_type=Switch.InputType.FLOAT, switch=align_right, false=half_text_width, true=0),
|
||||
true=math(operation=Math.Operation.MULTIPLY, value=(half_text_width, 2.0)),
|
||||
)
|
||||
placed_position = vector_math(
|
||||
operation=VectorMath.Operation.ADD,
|
||||
vector=(source_position, combine_xyz(x=math(operation=Math.Operation.ADD, value=(x_offset, alignment_offset)), y=y_offset, z=z_offset)),
|
||||
)
|
||||
|
||||
point_normal = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.POINT,
|
||||
clamp=False,
|
||||
geometry=sampled_source,
|
||||
value=normal(legacy_corner_normals=True).normal,
|
||||
index=index(),
|
||||
)
|
||||
edge_normal = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.EDGE,
|
||||
clamp=False,
|
||||
geometry=sampled_source,
|
||||
value=normal(legacy_corner_normals=True).normal,
|
||||
index=index(),
|
||||
)
|
||||
face_normal = sample_index(
|
||||
data_type=SampleIndex.DataType.FLOAT_VECTOR,
|
||||
domain=SampleIndex.Domain.FACE,
|
||||
clamp=False,
|
||||
geometry=sampled_source,
|
||||
value=normal(legacy_corner_normals=True).normal,
|
||||
index=index(),
|
||||
)
|
||||
source_normal = switch(
|
||||
input_type=Switch.InputType.VECTOR,
|
||||
switch=use_face_domain,
|
||||
false=switch(input_type=Switch.InputType.VECTOR, switch=use_edge_domain, false=point_normal, true=edge_normal),
|
||||
true=face_normal,
|
||||
)
|
||||
normal_aligned_rotation = align_rotation_to_vector(axis="Z", pivot_axis="AUTO", factor=1.0, vector=source_normal)
|
||||
normal_parts = separate_xyz(vector=source_normal)
|
||||
global_reference_nudge = switch(
|
||||
input_type=Switch.InputType.VECTOR,
|
||||
switch=math(operation=Math.Operation.GREATER_THAN, value=(normal_parts.y, 0.5)),
|
||||
false=(0.0, 0.0, 0.0),
|
||||
true=(9.999999747378752e-06, 0.0, 0.0),
|
||||
)
|
||||
global_reference = combine_xyz(x=global_reference_nudge, y=0.0, z=-1.0)
|
||||
self_info = object_info(transform_space="ORIGINAL", object=self_object(), as_instance=False)
|
||||
object_space_reference = transform_point(vector=global_reference, transform=invert_matrix(matrix=self_info.transform).matrix)
|
||||
reference_vector = switch(input_type=Switch.InputType.VECTOR, switch=align_to_global, false=global_reference, true=object_space_reference)
|
||||
tangent_vector = vector_math(operation=VectorMath.Operation.CROSS_PRODUCT, vector=(source_normal, reference_vector))
|
||||
tangent_aligned_rotation = align_rotation_to_vector(
|
||||
axis="X",
|
||||
pivot_axis="AUTO",
|
||||
rotation=normal_aligned_rotation,
|
||||
factor=1.0,
|
||||
vector=tangent_vector,
|
||||
)
|
||||
local_spin = combine_xyz(x=0.0, y=0.0, z=math(operation=Math.Operation.RADIANS, value=rotation))
|
||||
instance_rotation = rotate_rotation(rotation_space="LOCAL", rotation=tangent_aligned_rotation, rotate_by=local_spin)
|
||||
rotated_position = vector_rotate(
|
||||
rotation_type="EULER_XYZ",
|
||||
invert=False,
|
||||
vector=placed_position,
|
||||
center=source_position,
|
||||
rotation=instance_rotation,
|
||||
)
|
||||
transformed_instances = set_instance_transform(
|
||||
instances=value_instances,
|
||||
transform=combine_transform(
|
||||
translation=rotated_position,
|
||||
rotation=instance_rotation,
|
||||
scale=combine_xyz(x=scale, y=scale, z=scale),
|
||||
),
|
||||
)
|
||||
realized_digits = realize_instances(realize_to_point_domain=True, geometry=transformed_instances, realize_all=True, depth=0)
|
||||
|
||||
last_display_slot = math(operation=Math.Operation.SUBTRACT, value=(display_digit_slots, 1.0))
|
||||
is_decimal_slot = boolean_math(
|
||||
operation=BooleanMath.Operation.AND,
|
||||
boolean=(
|
||||
compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=digit_indexed.int_field,
|
||||
b=last_display_slot,
|
||||
epsilon=0.0010000000474974513,
|
||||
),
|
||||
math(operation=Math.Operation.GREATER_THAN, value=(precision, 0.0)),
|
||||
),
|
||||
)
|
||||
is_negative_slot = boolean_math(
|
||||
operation=BooleanMath.Operation.AND,
|
||||
boolean=(
|
||||
is_negative,
|
||||
compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=digit_indexed.int_field,
|
||||
b=natural_digit_count,
|
||||
epsilon=0.0010000000474974513,
|
||||
),
|
||||
),
|
||||
)
|
||||
forced_visible_digit = boolean_math(
|
||||
operation=BooleanMath.Operation.OR,
|
||||
boolean=(
|
||||
boolean_math(
|
||||
operation=BooleanMath.Operation.OR,
|
||||
boolean=(
|
||||
math(operation=Math.Operation.LESS_THAN, value=(digit_indexed.int_field, precision)),
|
||||
math(
|
||||
operation=Math.Operation.GREATER_THAN,
|
||||
value=(
|
||||
whole_part,
|
||||
math(
|
||||
operation=Math.Operation.SUBTRACT,
|
||||
value=(
|
||||
math(
|
||||
operation=Math.Operation.POWER,
|
||||
value=(10.0, math(operation=Math.Operation.SUBTRACT, value=(digit_indexed.int_field, precision))),
|
||||
),
|
||||
1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
boolean_math(
|
||||
operation=BooleanMath.Operation.AND,
|
||||
boolean=(
|
||||
compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.FLOAT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=whole_part,
|
||||
b=0.0,
|
||||
epsilon=0.0010000000474974513,
|
||||
),
|
||||
compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=digit_indexed.int_field,
|
||||
b=precision,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
leading_zero_slot = boolean_math(
|
||||
operation=BooleanMath.Operation.AND,
|
||||
boolean=(
|
||||
boolean_math(
|
||||
operation=BooleanMath.Operation.AND,
|
||||
boolean=(boolean_math(operation=BooleanMath.Operation.NOT, boolean=is_negative), leading_zeros),
|
||||
),
|
||||
math(
|
||||
operation=Math.Operation.LESS_THAN,
|
||||
value=(math(operation=Math.Operation.SUBTRACT, value=(digit_indexed.int_field, precision)), digits_before_decimal),
|
||||
),
|
||||
),
|
||||
)
|
||||
digit_visible = boolean_math(operation=BooleanMath.Operation.OR, boolean=(forced_visible_digit, leading_zero_slot))
|
||||
display_capacity_exceeded = math(
|
||||
operation=Math.Operation.GREATER_THAN,
|
||||
value=(natural_digit_count, math(operation=Math.Operation.ADD, value=(digits_before_decimal, precision))),
|
||||
)
|
||||
fraction_part = math(
|
||||
operation=Math.Operation.ROUND,
|
||||
value=(
|
||||
math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
math(operation=Math.Operation.FRACT, value=absolute_value),
|
||||
math(operation=Math.Operation.POWER, value=(10.0, precision)),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
next_digit = vg_next_digit(
|
||||
whole_part=whole_part,
|
||||
fraction_part=fraction_part,
|
||||
max_precision=precision,
|
||||
position=digit_indexed.int_field,
|
||||
)
|
||||
digit_code = switch(input_type=Switch.InputType.FLOAT, switch=display_capacity_exceeded, false=next_digit, true=10.0)
|
||||
visible_digit_code = switch(
|
||||
input_type=Switch.InputType.INT,
|
||||
switch=digit_visible,
|
||||
false=0,
|
||||
true=math(operation=Math.Operation.ADD, value=(digit_code, 1.0)),
|
||||
)
|
||||
negative_digit_code = switch(input_type=Switch.InputType.INT, switch=is_negative_slot, false=visible_digit_code, true=11)
|
||||
final_digit_code = switch(input_type=Switch.InputType.INT, switch=is_decimal_slot, false=negative_digit_code, true=12)
|
||||
hidden_segments = vg_delete_segments(digit=final_digit_code, segment_position=glyph_faces.int_field)
|
||||
visible_segments = delete_geometry(mode=DeleteGeometry.Mode.ALL, domain=DeleteGeometry.Domain.FACE, geometry=realized_digits, selection=hidden_segments)
|
||||
return {"Value": set_material(geometry=visible_segments, material=material)}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in (
|
||||
"VG Create Decimal",
|
||||
"VG Digit At",
|
||||
"VG Next Digit",
|
||||
"VG Create Segment",
|
||||
"VG Seven Segments",
|
||||
"VG Delete Segments",
|
||||
"VG Field Value",
|
||||
):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if not group:
|
||||
continue
|
||||
group.use_fake_user = True
|
||||
if hasattr(group, "interface"):
|
||||
for item in group.interface.items_tree:
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name == "X Major":
|
||||
item.name = "X Major"
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name in {"Radius", "Offset"}:
|
||||
item.name = item.name.lower()
|
||||
if item.item_type == "SOCKET" and item.in_out == "OUTPUT" and item.name == "Bars":
|
||||
item.name = "bars"
|
||||
if (
|
||||
name == "VG Delete Segments"
|
||||
and item.item_type == "SOCKET"
|
||||
and item.in_out == "OUTPUT"
|
||||
and item.name == "Result"
|
||||
):
|
||||
item.name = "result"
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_FIELDVALUE_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,509 +0,0 @@
|
|||
"""Geometry Script recreations from Shriinivas/paramnpolareq.blend."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
|
||||
|
||||
TAU = 6.2831854820251465
|
||||
|
||||
|
||||
def _accumulated_parameter(resolution, max_value):
|
||||
step = math(
|
||||
operation=Math.Operation.DIVIDE,
|
||||
value=(max_value, math(operation=Math.Operation.SUBTRACT, value=(resolution, 1.0))),
|
||||
)
|
||||
return accumulate_field(
|
||||
data_type=AccumulateField.DataType.FLOAT,
|
||||
domain=AccumulateField.Domain.POINT,
|
||||
value=step,
|
||||
).trailing
|
||||
|
||||
|
||||
def _choose_float(curve_type, val0, val1, val2, val3, val4, val5=0.0):
|
||||
selected = switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=curve_type,
|
||||
b=4,
|
||||
),
|
||||
false=val5,
|
||||
true=val4,
|
||||
)
|
||||
for index, value in reversed(list(enumerate([val0, val1, val2, val3]))):
|
||||
selected = switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=curve_type,
|
||||
b=index,
|
||||
),
|
||||
false=selected,
|
||||
true=value,
|
||||
)
|
||||
return selected
|
||||
|
||||
|
||||
def _choose_string(curve_type, val0, val1, val2, val3, val4, val5=""):
|
||||
selected = switch(
|
||||
input_type=Switch.InputType.STRING,
|
||||
switch=compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=curve_type,
|
||||
b=4,
|
||||
),
|
||||
false=val5,
|
||||
true=val4,
|
||||
)
|
||||
for index, value in reversed(list(enumerate([val0, val1, val2, val3]))):
|
||||
selected = switch(
|
||||
input_type=Switch.InputType.STRING,
|
||||
switch=compare(
|
||||
operation=Compare.Operation.EQUAL,
|
||||
data_type=Compare.DataType.INT,
|
||||
mode=Compare.Mode.ELEMENT,
|
||||
a=curve_type,
|
||||
b=index,
|
||||
),
|
||||
false=selected,
|
||||
true=value,
|
||||
)
|
||||
return selected
|
||||
|
||||
|
||||
@tree("VG Choose Float Val")
|
||||
def vg_choose_float_val(
|
||||
curve_type: Int = 0,
|
||||
val0: Float = 0.0,
|
||||
val1: Float = 0.0,
|
||||
val2: Float = 0.0,
|
||||
val3: Float = 0.0,
|
||||
val4: Float = 0.0,
|
||||
val5: Float = 0.0,
|
||||
):
|
||||
return {"val": _choose_float(curve_type, val0, val1, val2, val3, val4, val5)}
|
||||
|
||||
|
||||
@tree("VG Choose Str Val")
|
||||
def vg_choose_str_val(
|
||||
curve_type: Int = 0,
|
||||
val0: String = "",
|
||||
val1: String = "",
|
||||
val2: String = "",
|
||||
val3: String = "",
|
||||
val4: String = "",
|
||||
val5: String = "",
|
||||
):
|
||||
return {"val": _choose_string(curve_type, val0, val1, val2, val3, val4, val5)}
|
||||
|
||||
|
||||
@tree("VG Param Epicycloid X")
|
||||
def vg_param_epicycloid_x(a: Float = 0.5, b: Float = 0.5, t: Float = 1.0):
|
||||
ratio_plus_one = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0),
|
||||
)
|
||||
inner = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(b, math(operation=Math.Operation.COSINE, value=math(operation=Math.Operation.MULTIPLY, value=(ratio_plus_one, t)))),
|
||||
)
|
||||
outer = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.ADD, value=(a, b)), math(operation=Math.Operation.COSINE, value=t)),
|
||||
)
|
||||
return {"x": math(operation=Math.Operation.SUBTRACT, value=(outer, inner))}
|
||||
|
||||
|
||||
@tree("VG Param Epicycloid Y")
|
||||
def vg_param_epicycloid_y(a: Float = 0.5, b: Float = 0.5, t: Float = 1.0):
|
||||
ratio_plus_one = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0),
|
||||
)
|
||||
inner = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(b, math(operation=Math.Operation.SINE, value=math(operation=Math.Operation.MULTIPLY, value=(ratio_plus_one, t)))),
|
||||
)
|
||||
outer = math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.ADD, value=(a, b)), math(operation=Math.Operation.SINE, value=t)),
|
||||
)
|
||||
return {"y": math(operation=Math.Operation.SUBTRACT, value=(outer, inner))}
|
||||
|
||||
|
||||
@tree("VG Polar Spiral X")
|
||||
def vg_polar_spiral_x(a: Float = 0.5, b: Float = 0.5, theta: Float = 1.0):
|
||||
radius = math(operation=Math.Operation.ADD, value=(a, math(operation=Math.Operation.MULTIPLY, value=(b, theta))))
|
||||
return {"x": math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.COSINE, value=theta), radius))}
|
||||
|
||||
|
||||
@tree("VG Polar Spiral Y")
|
||||
def vg_polar_spiral_y(a: Float = 0.5, b: Float = 0.5, theta: Float = 1.0):
|
||||
radius = math(operation=Math.Operation.ADD, value=(a, math(operation=Math.Operation.MULTIPLY, value=(b, theta))))
|
||||
return {"y": math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SINE, value=theta), radius))}
|
||||
|
||||
|
||||
@tree("VG Param XY")
|
||||
def vg_param_xy(a: Float = 0.0, b: Float = 0.0, c: Float = 0.0, t: Float = 0.0, curve_type: Int = 0):
|
||||
normalized_curve_type = math(
|
||||
operation=Math.Operation.MODULO,
|
||||
value=(math(operation=Math.Operation.ABSOLUTE, value=curve_type), 5.0),
|
||||
)
|
||||
|
||||
epi_x = vg_param_epicycloid_x(a=a, b=b, t=t)
|
||||
epi_y = vg_param_epicycloid_y(a=a, b=b, t=t)
|
||||
epitrochoid_x = math(
|
||||
operation=Math.Operation.SUBTRACT,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.ADD, value=(a, b)), math(operation=Math.Operation.COSINE, value=t))),
|
||||
math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
c,
|
||||
math(
|
||||
operation=Math.Operation.COSINE,
|
||||
value=math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.ADD, value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0)), t),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
epitrochoid_y = math(
|
||||
operation=Math.Operation.SUBTRACT,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.ADD, value=(a, b)), math(operation=Math.Operation.SINE, value=t))),
|
||||
math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
c,
|
||||
math(
|
||||
operation=Math.Operation.SINE,
|
||||
value=math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.ADD, value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0)), t),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
hypo_x = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SUBTRACT, value=(a, b)), math(operation=Math.Operation.COSINE, value=t))),
|
||||
math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
b,
|
||||
math(
|
||||
operation=Math.Operation.COSINE,
|
||||
value=math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.SUBTRACT, value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0)), t),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
hypo_y = math(
|
||||
operation=Math.Operation.SUBTRACT,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SUBTRACT, value=(a, b)), math(operation=Math.Operation.SINE, value=t))),
|
||||
math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
b,
|
||||
math(
|
||||
operation=Math.Operation.SINE,
|
||||
value=math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.SUBTRACT, value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0)), t),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
hypotrochoid_x = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SUBTRACT, value=(a, b)), math(operation=Math.Operation.COSINE, value=t))),
|
||||
math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
c,
|
||||
math(
|
||||
operation=Math.Operation.COSINE,
|
||||
value=math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.SUBTRACT, value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0)), t),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
hypotrochoid_y = math(
|
||||
operation=Math.Operation.SUBTRACT,
|
||||
value=(
|
||||
math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SUBTRACT, value=(a, b)), math(operation=Math.Operation.SINE, value=t))),
|
||||
math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(
|
||||
c,
|
||||
math(
|
||||
operation=Math.Operation.SINE,
|
||||
value=math(
|
||||
operation=Math.Operation.MULTIPLY,
|
||||
value=(math(operation=Math.Operation.SUBTRACT, value=(math(operation=Math.Operation.DIVIDE, value=(a, b)), 1.0)), t),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
arch_radius = math(operation=Math.Operation.ADD, value=(a, math(operation=Math.Operation.MULTIPLY, value=(b, t))))
|
||||
arch_x = math(operation=Math.Operation.MULTIPLY, value=(arch_radius, math(operation=Math.Operation.COSINE, value=t)))
|
||||
arch_y = math(operation=Math.Operation.MULTIPLY, value=(arch_radius, math(operation=Math.Operation.SINE, value=t)))
|
||||
|
||||
linebreak = special_characters().line_break
|
||||
eq = _choose_string(
|
||||
normalized_curve_type,
|
||||
join_strings(
|
||||
delimiter=linebreak,
|
||||
strings=[
|
||||
string(string="y = ((a + b) * sin(t)) - (b * sin((a/b + 1) * t))"),
|
||||
string(string="x = ((a + b) * cos(t)) - (b * cos((a/b + 1) * t))"),
|
||||
string(string="Epicycloid"),
|
||||
],
|
||||
),
|
||||
join_strings(
|
||||
delimiter=linebreak,
|
||||
strings=[
|
||||
string(string="y = ((a + b) * sin(t)) - (c * sin((a/b + 1) * t))"),
|
||||
string(string="x = ((a + b) * cos(t)) - (c * cos((a/b + 1) * t))"),
|
||||
string(string="Epitrochoid"),
|
||||
],
|
||||
),
|
||||
join_strings(
|
||||
delimiter=linebreak,
|
||||
strings=[
|
||||
string(string="y = ((a - b) * sin(t)) + (b * sin((a/b - 1) * t))"),
|
||||
string(string="x = ((a - b) * cos(t)) + (b * cos((a/b - 1) * t))"),
|
||||
string(string="Hypocycloid"),
|
||||
],
|
||||
),
|
||||
join_strings(
|
||||
delimiter=linebreak,
|
||||
strings=[
|
||||
string(string="y = ((a - b) * sin(t)) + (c * sin((a/b - 1) * t))"),
|
||||
string(string="x = ((a - b) * cos(t)) + (c * cos((a/b - 1) * t))"),
|
||||
string(string="Hypotrochoid"),
|
||||
],
|
||||
),
|
||||
join_strings(
|
||||
delimiter=linebreak,
|
||||
strings=[
|
||||
string(string="r = a + b theta"),
|
||||
string(string="Archimedes Spiral"),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
"x": _choose_float(normalized_curve_type, epi_x, epitrochoid_x, hypo_x, hypotrochoid_x, arch_x),
|
||||
"y": _choose_float(normalized_curve_type, epi_y, epitrochoid_y, hypo_y, hypotrochoid_y, arch_y),
|
||||
"eq": eq,
|
||||
}
|
||||
|
||||
|
||||
@tree("VG Archimedes Spiral")
|
||||
def vg_archimedes_spiral(
|
||||
resolution: Int = 10,
|
||||
no_of_rounds: Float = 0.5,
|
||||
a: Float = 0.5,
|
||||
b: Float = 0.5,
|
||||
):
|
||||
mesh = mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=resolution,
|
||||
start_location=(0.0, 0.0, 0.0),
|
||||
offset=(0.0, 0.0, 1.0),
|
||||
)
|
||||
theta = _accumulated_parameter(
|
||||
resolution=resolution,
|
||||
max_value=math(operation=Math.Operation.MULTIPLY, value=(no_of_rounds, TAU)),
|
||||
)
|
||||
positioned = mesh.set_position(
|
||||
position=combine_xyz(
|
||||
x=vg_polar_spiral_x(a=a, b=b, theta=theta),
|
||||
y=vg_polar_spiral_y(a=a, b=b, theta=theta),
|
||||
z=0.0,
|
||||
)
|
||||
)
|
||||
return {"Geometry": positioned}
|
||||
|
||||
|
||||
@tree("VG Epicycloid")
|
||||
def vg_epicycloid(
|
||||
resolution: Int = 10,
|
||||
max_t: Float = 0.5,
|
||||
a: Float = 0.5,
|
||||
b: Float = 0.5,
|
||||
):
|
||||
mesh = mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=resolution,
|
||||
start_location=(0.0, 0.0, 0.0),
|
||||
offset=(0.0, 0.0, 1.0),
|
||||
)
|
||||
t = _accumulated_parameter(resolution=resolution, max_value=max_t)
|
||||
positioned = mesh.set_position(
|
||||
position=combine_xyz(
|
||||
x=vg_param_epicycloid_x(a=a, b=b, t=t),
|
||||
y=vg_param_epicycloid_y(a=a, b=b, t=t),
|
||||
z=0.0,
|
||||
)
|
||||
)
|
||||
return {"Geometry": positioned}
|
||||
|
||||
|
||||
@tree("VG Mirrored Root Spiral")
|
||||
def vg_mirrored_root_spiral(a: Float = 0.30000001192092896, round_count: Float = 5.0, resolution: Float = 500.0):
|
||||
mesh = mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=resolution,
|
||||
start_location=(0.0, 0.0, 0.0),
|
||||
offset=(0.0, 0.0, 1.0),
|
||||
)
|
||||
theta = _accumulated_parameter(
|
||||
resolution=resolution,
|
||||
max_value=math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.MULTIPLY, value=(round_count, 3.1415927410125732)), 2.0)),
|
||||
)
|
||||
radius = math(operation=Math.Operation.MULTIPLY, value=(a, math(operation=Math.Operation.POWER, value=(theta, 0.5))))
|
||||
x = math(operation=Math.Operation.MULTIPLY, value=(radius, math(operation=Math.Operation.COSINE, value=theta)))
|
||||
y = math(operation=Math.Operation.MULTIPLY, value=(radius, math(operation=Math.Operation.SINE, value=theta)))
|
||||
forward = mesh.set_position(position=combine_xyz(x=x, y=y, z=0.0))
|
||||
mirrored = mesh.set_position(
|
||||
position=combine_xyz(
|
||||
x=math(operation=Math.Operation.MULTIPLY, value=(-1.0, x)),
|
||||
y=math(operation=Math.Operation.MULTIPLY, value=(-1.0, y)),
|
||||
z=0.0,
|
||||
)
|
||||
)
|
||||
return {"Geometry": join_geometry(geometry=[mirrored, forward])}
|
||||
|
||||
|
||||
def _empty_curve_geometry():
|
||||
return mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=0,
|
||||
offset=(0.0, 0.0, 1.0),
|
||||
)
|
||||
|
||||
|
||||
@tree("VG Param Curve")
|
||||
def vg_param_curve(
|
||||
curve_type: Int = 0,
|
||||
a: Float = 0.800000011920929,
|
||||
b: Float = 0.5,
|
||||
c: Float = 1.0,
|
||||
z: Float = 0.0,
|
||||
max_t: Float = 32.5,
|
||||
resolution: Float = 500.0,
|
||||
showlabel: Bool = True,
|
||||
thickness: Float = 0.009999999776482582,
|
||||
curve_material: Material = None,
|
||||
text_material: Material = None,
|
||||
):
|
||||
t = _accumulated_parameter(resolution=resolution, max_value=max_t)
|
||||
x, y, equation = vg_param_xy(a=a, b=b, c=c, t=t, curve_type=curve_type)
|
||||
|
||||
text_curve = string_to_curves(
|
||||
string=equation,
|
||||
size=2.0,
|
||||
align_x="Center",
|
||||
align_y="Middle",
|
||||
pivot_point="Bottom Left",
|
||||
).curve_instances
|
||||
label = transform_geometry(
|
||||
geometry=fill_curve(curve=text_curve, mode="Triangles", fill_rule="Even-Odd").set_material(material=text_material),
|
||||
translation=(0.0, 0.0, 0.30000001192092896),
|
||||
)
|
||||
label_or_empty = switch(input_type=Switch.InputType.GEOMETRY, switch=showlabel, false=_empty_curve_geometry(), true=label)
|
||||
|
||||
rail_seed = mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=resolution,
|
||||
start_location=(0.0, 0.0, 0.0),
|
||||
offset=(0.0, 0.0, 1.0),
|
||||
)
|
||||
rail = mesh_to_curve(mode=MeshToCurve.Mode.EDGES, mesh=rail_seed).set_position(position=combine_xyz(x=x, y=y, z=z))
|
||||
profile = curve_circle(mode=CurveCircle.Mode.RADIUS, resolution=32, radius=thickness)
|
||||
radius_attribute = named_attribute(data_type=NamedAttribute.DataType.FLOAT, name="radius")
|
||||
radius_scale = switch(input_type=Switch.InputType.FLOAT, switch=radius_attribute.exists, false=1.0, true=radius_attribute.attribute)
|
||||
curve_mesh = rail.curve_to_mesh(profile_curve=profile, scale=radius_scale, fill_caps=False).set_material(material=curve_material)
|
||||
return {"Geometry": join_geometry(geometry=[label_or_empty, curve_mesh])}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in (
|
||||
"VG Param Epicycloid X",
|
||||
"VG Param Epicycloid Y",
|
||||
"VG Polar Spiral X",
|
||||
"VG Polar Spiral Y",
|
||||
"VG Choose Float Val",
|
||||
"VG Choose Str Val",
|
||||
"VG Param XY",
|
||||
"VG Archimedes Spiral",
|
||||
"VG Epicycloid",
|
||||
"VG Mirrored Root Spiral",
|
||||
"VG Param Curve",
|
||||
):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if group:
|
||||
group.use_fake_user = True
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_PARAMNPOLAREQ_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,390 +0,0 @@
|
|||
"""Geometry Script recreation of the small pie chart from Shriinivas/piechart.blend."""
|
||||
|
||||
try:
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
|
||||
TAU = 6.2831854820251465
|
||||
|
||||
|
||||
@tree("VG Pie Segment")
|
||||
def vg_pie_segment(
|
||||
start: Float = TAU,
|
||||
value: Float = 0.5,
|
||||
total: Float = 0.5,
|
||||
shift: Float = 0.5,
|
||||
segment_material: Material = None,
|
||||
text_material: Material = None,
|
||||
):
|
||||
fraction = math(operation=Math.Operation.DIVIDE, value=(value, total))
|
||||
sweep = math(operation=Math.Operation.MULTIPLY, value=(fraction, TAU))
|
||||
end = math(operation=Math.Operation.ADD, value=(start, sweep))
|
||||
|
||||
percentage = math(operation=Math.Operation.MULTIPLY, value=(fraction, 100.0))
|
||||
percentage_string = value_to_string(data_type=ValueToString.DataType.FLOAT, value=percentage, decimals=2)
|
||||
label = join_strings(delimiter="", strings=[percentage_string, string(string="%")])
|
||||
|
||||
arc_curve = arc(
|
||||
mode=Arc.Mode.RADIUS,
|
||||
resolution=64,
|
||||
radius=1.0,
|
||||
start_angle=start,
|
||||
sweep_angle=sweep,
|
||||
connect_center=True,
|
||||
)
|
||||
face = arc_curve.fill_curve()
|
||||
extruded_face = face.extrude_mesh(mode=ExtrudeMesh.Mode.FACES, offset_scale=-0.1)
|
||||
|
||||
text_curve = string_to_curves(string=label, size=0.2, align_x="Center", align_y="Middle").curve_instances
|
||||
text_face = text_curve.fill_curve()
|
||||
text_mesh = text_face.extrude_mesh(mode=ExtrudeMesh.Mode.FACES, offset_scale=0.025).mesh
|
||||
face_center = attribute_statistic(
|
||||
data_type=AttributeStatistic.DataType.FLOAT_VECTOR,
|
||||
domain=AttributeStatistic.Domain.FACE,
|
||||
geometry=face,
|
||||
attribute=position(),
|
||||
).mean
|
||||
positioned_text = text_mesh.set_position(offset=face_center).set_material(material=text_material)
|
||||
|
||||
mid_angle = math(operation=Math.Operation.ADD, value=(start, math(operation=Math.Operation.DIVIDE, value=(sweep, 2.0))))
|
||||
shift_vector = combine_xyz(
|
||||
x=math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.COSINE, value=mid_angle), shift)),
|
||||
y=math(operation=Math.Operation.MULTIPLY, value=(math(operation=Math.Operation.SINE, value=mid_angle), shift)),
|
||||
z=0.0,
|
||||
)
|
||||
|
||||
segment = extruded_face.mesh.set_material(material=segment_material)
|
||||
cap = face.set_material(material=segment_material)
|
||||
geometry = join_geometry(geometry=[positioned_text, segment, cap]).set_position(offset=shift_vector)
|
||||
return {"Curve": geometry, "End": end}
|
||||
|
||||
|
||||
@tree("VG Pie Chart")
|
||||
def vg_pie_chart(
|
||||
geometry: Geometry,
|
||||
a: Float = 0.5,
|
||||
a_shift: Float = 0.5,
|
||||
a_material: Material = None,
|
||||
b: Float = 0.5,
|
||||
b_shift: Float = 0.0,
|
||||
b_material: Material = None,
|
||||
c: Float = 0.5,
|
||||
c_shift: Float = 0.0,
|
||||
c_material: Material = None,
|
||||
text_material: Material = None,
|
||||
):
|
||||
del geometry
|
||||
|
||||
total = math(operation=Math.Operation.ADD, value=(math(operation=Math.Operation.ADD, value=(a, b)), c))
|
||||
a_geometry, a_end = vg_pie_segment(
|
||||
start=0.0,
|
||||
value=a,
|
||||
total=total,
|
||||
shift=a_shift,
|
||||
segment_material=a_material,
|
||||
text_material=text_material,
|
||||
)
|
||||
b_geometry, b_end = vg_pie_segment(
|
||||
start=a_end,
|
||||
value=b,
|
||||
total=total,
|
||||
shift=b_shift,
|
||||
segment_material=b_material,
|
||||
text_material=text_material,
|
||||
)
|
||||
c_geometry, _c_end = vg_pie_segment(
|
||||
start=b_end,
|
||||
value=c,
|
||||
total=total,
|
||||
shift=c_shift,
|
||||
segment_material=c_material,
|
||||
text_material=text_material,
|
||||
)
|
||||
return join_geometry(geometry=[a_geometry, b_geometry, c_geometry])
|
||||
|
||||
|
||||
def _empty_geometry():
|
||||
return mesh_line(
|
||||
mode=MeshLine.Mode.OFFSET,
|
||||
count_mode=MeshLine.CountMode.TOTAL,
|
||||
count=0,
|
||||
offset=(0.0, 0.0, 1.0),
|
||||
)
|
||||
|
||||
|
||||
@tree("VG Extended Pie Segment")
|
||||
def vg_extended_pie_segment(
|
||||
radius: Float = 0.0,
|
||||
pie_height: Float = 0.0,
|
||||
text_height: Float = 0.0,
|
||||
text_size: Float = 0.0,
|
||||
text_material: Material = None,
|
||||
text_offset: Float = 0.0,
|
||||
pie_material: Material = None,
|
||||
include_percentage: Bool = True,
|
||||
value: Float = 0.0,
|
||||
total: Float = 0.0,
|
||||
start_angle: Float = 0.0,
|
||||
shift: Float = 0.0,
|
||||
text: String = "",
|
||||
):
|
||||
fraction = math(operation=Math.Operation.DIVIDE, value=(value, total))
|
||||
sweep = math(operation=Math.Operation.MULTIPLY, value=(fraction, TAU))
|
||||
end_angle = math(operation=Math.Operation.ADD, value=(start_angle, sweep))
|
||||
mid_angle = math(operation=Math.Operation.ADD, value=(start_angle, math(operation=Math.Operation.DIVIDE, value=(sweep, 2.0))))
|
||||
|
||||
percentage = value_to_string(
|
||||
data_type=ValueToString.DataType.FLOAT,
|
||||
value=math(operation=Math.Operation.MULTIPLY, value=(fraction, 100.0)),
|
||||
decimals=2,
|
||||
)
|
||||
percentage_line = join_strings(delimiter="", strings=[string(string="%"), percentage])
|
||||
label_with_percentage = join_strings(delimiter=special_characters().line_break, strings=[percentage_line, text])
|
||||
label = switch(input_type=Switch.InputType.STRING, switch=include_percentage, false=text, true=label_with_percentage)
|
||||
|
||||
shift_vector = combine_xyz(
|
||||
x=math(operation=Math.Operation.MULTIPLY, value=(shift, math(operation=Math.Operation.COSINE, value=mid_angle))),
|
||||
y=math(operation=Math.Operation.MULTIPLY, value=(shift, math(operation=Math.Operation.SINE, value=mid_angle))),
|
||||
z=0.0,
|
||||
)
|
||||
label_radius = math(operation=Math.Operation.MULTIPLY, value=(text_offset, radius))
|
||||
label_offset = vector_math(
|
||||
operation=VectorMath.Operation.ADD,
|
||||
vector=(
|
||||
shift_vector,
|
||||
combine_xyz(
|
||||
x=math(operation=Math.Operation.MULTIPLY, value=(label_radius, math(operation=Math.Operation.COSINE, value=mid_angle))),
|
||||
y=math(operation=Math.Operation.MULTIPLY, value=(label_radius, math(operation=Math.Operation.SINE, value=mid_angle))),
|
||||
z=0.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
text_curves = string_to_curves(
|
||||
string=label,
|
||||
size=text_size,
|
||||
align_x="Center",
|
||||
align_y="Middle",
|
||||
pivot_point="Bottom Left",
|
||||
).curve_instances
|
||||
text_face = fill_curve(curve=text_curves, mode="Triangles", fill_rule="Even-Odd")
|
||||
placed_text = set_position(geometry=text_face.set_material(material=text_material), offset=label_offset)
|
||||
extruded_text = extrude_mesh(
|
||||
mode=ExtrudeMesh.Mode.FACES,
|
||||
mesh=placed_text,
|
||||
offset=combine_xyz(x=0.0, y=0.0, z=math(operation=Math.Operation.ADD, value=(text_height, pie_height))),
|
||||
offset_scale=1.0,
|
||||
individual=True,
|
||||
).mesh.set_material(material=text_material)
|
||||
|
||||
arc_curve = arc(
|
||||
mode=Arc.Mode.RADIUS,
|
||||
resolution=64,
|
||||
radius=radius,
|
||||
start_angle=start_angle,
|
||||
sweep_angle=sweep,
|
||||
connect_center=True,
|
||||
)
|
||||
shifted_arc = transform_geometry(geometry=arc_curve, translation=shift_vector)
|
||||
pie_face = fill_curve(curve=shifted_arc, mode="Triangles", fill_rule="Even-Odd").set_material(
|
||||
material=pie_material
|
||||
)
|
||||
extruded_pie = extrude_mesh(
|
||||
mode=ExtrudeMesh.Mode.FACES,
|
||||
mesh=pie_face,
|
||||
offset=combine_xyz(x=0.0, y=0.0, z=pie_height),
|
||||
offset_scale=1.0,
|
||||
individual=True,
|
||||
).mesh.set_material(material=pie_material)
|
||||
|
||||
return {"End Angle": end_angle, "Pie": join_geometry(geometry=[extruded_text, extruded_pie, placed_text, pie_face])}
|
||||
|
||||
|
||||
@tree("VG Extended Pie Chart")
|
||||
def vg_extended_pie_chart(
|
||||
title: String = "Extended Pie Chart",
|
||||
title_size: Float = 0.20000000298023224,
|
||||
title_material: Material = None,
|
||||
title_offset: Float = 0.18000000715255737,
|
||||
segment_count: Int = 10,
|
||||
radius: Float = 1.0,
|
||||
pie_height: Float = 0.10000000149011612,
|
||||
text_height: Float = 0.004999999888241291,
|
||||
text_size: Float = 0.07999999821186066,
|
||||
text_material: Material = None,
|
||||
text_offset: Float = 0.699999988079071,
|
||||
include_percentage: Bool = True,
|
||||
value_1: Float = 20.0,
|
||||
label_1: String = "Lorem",
|
||||
shift_1: Float = 0.20000000298023224,
|
||||
material_1: Material = None,
|
||||
value_2: Float = 30.0,
|
||||
label_2: String = "Ipsum",
|
||||
shift_2: Float = 0.0,
|
||||
material_2: Material = None,
|
||||
value_3: Float = 27.0,
|
||||
label_3: String = "Dolor",
|
||||
shift_3: Float = 0.0,
|
||||
material_3: Material = None,
|
||||
value_4: Float = 25.0,
|
||||
label_4: String = "Sit",
|
||||
shift_4: Float = 0.0,
|
||||
material_4: Material = None,
|
||||
value_5: Float = 55.0,
|
||||
label_5: String = "Amet",
|
||||
shift_5: Float = 0.0,
|
||||
material_5: Material = None,
|
||||
value_6: Float = 45.0,
|
||||
label_6: String = "Consectetur",
|
||||
shift_6: Float = 0.0,
|
||||
material_6: Material = None,
|
||||
value_7: Float = 25.0,
|
||||
label_7: String = "Adipiscing",
|
||||
shift_7: Float = 0.0,
|
||||
material_7: Material = None,
|
||||
value_8: Float = 35.0,
|
||||
label_8: String = "Elit",
|
||||
shift_8: Float = 0.0,
|
||||
material_8: Material = None,
|
||||
value_9: Float = 58.0,
|
||||
label_9: String = "Sed",
|
||||
shift_9: Float = 0.0,
|
||||
material_9: Material = None,
|
||||
value_10: Float = 32.0,
|
||||
label_10: String = "Do",
|
||||
shift_10: Float = 0.0,
|
||||
material_10: Material = None,
|
||||
):
|
||||
values = [value_1, value_2, value_3, value_4, value_5, value_6, value_7, value_8, value_9, value_10]
|
||||
labels = [label_1, label_2, label_3, label_4, label_5, label_6, label_7, label_8, label_9, label_10]
|
||||
shifts = [shift_1, shift_2, shift_3, shift_4, shift_5, shift_6, shift_7, shift_8, shift_9, shift_10]
|
||||
materials = [
|
||||
material_1,
|
||||
material_2,
|
||||
material_3,
|
||||
material_4,
|
||||
material_5,
|
||||
material_6,
|
||||
material_7,
|
||||
material_8,
|
||||
material_9,
|
||||
material_10,
|
||||
]
|
||||
|
||||
total = value_1
|
||||
for index, segment_value in enumerate(values[1:], start=2):
|
||||
total = math(
|
||||
operation=Math.Operation.ADD,
|
||||
value=(
|
||||
total,
|
||||
switch(
|
||||
input_type=Switch.InputType.FLOAT,
|
||||
switch=math(operation=Math.Operation.GREATER_THAN, value=(segment_count, index - 1.0)),
|
||||
false=0.0,
|
||||
true=segment_value,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
empty = _empty_geometry()
|
||||
start_angle = 0.0
|
||||
segments = []
|
||||
for index, (segment_value, label, segment_shift, segment_material) in enumerate(
|
||||
zip(values, labels, shifts, materials),
|
||||
start=1,
|
||||
):
|
||||
end_angle, segment_geometry = vg_extended_pie_segment(
|
||||
radius=radius,
|
||||
pie_height=pie_height,
|
||||
text_height=text_height,
|
||||
text_size=text_size,
|
||||
text_material=text_material,
|
||||
text_offset=text_offset,
|
||||
pie_material=segment_material,
|
||||
include_percentage=include_percentage,
|
||||
value=segment_value,
|
||||
total=total,
|
||||
start_angle=start_angle,
|
||||
shift=segment_shift,
|
||||
text=label,
|
||||
)
|
||||
if index == 1:
|
||||
segments.append(segment_geometry)
|
||||
else:
|
||||
segments.append(
|
||||
switch(
|
||||
input_type=Switch.InputType.GEOMETRY,
|
||||
switch=math(operation=Math.Operation.GREATER_THAN, value=(segment_count, index - 1.0)),
|
||||
false=empty,
|
||||
true=segment_geometry,
|
||||
)
|
||||
)
|
||||
start_angle = end_angle
|
||||
|
||||
title_curves = string_to_curves(
|
||||
string=title,
|
||||
size=title_size,
|
||||
align_x="Center",
|
||||
align_y="Middle",
|
||||
pivot_point="Bottom Left",
|
||||
).curve_instances
|
||||
title_face = fill_curve(curve=title_curves, mode="Triangles", fill_rule="Even-Odd")
|
||||
title_position = combine_xyz(
|
||||
x=0.0,
|
||||
y=math(
|
||||
operation=Math.Operation.SUBTRACT,
|
||||
value=(math(operation=Math.Operation.MULTIPLY, value=(-1.0, math(operation=Math.Operation.ADD, value=(radius, title_size)))), title_offset),
|
||||
),
|
||||
z=0.0,
|
||||
)
|
||||
placed_title = transform_geometry(geometry=title_face, translation=title_position).set_material(material=title_material)
|
||||
extruded_title = extrude_mesh(
|
||||
mode=ExtrudeMesh.Mode.FACES,
|
||||
mesh=placed_title,
|
||||
offset=combine_xyz(x=0.0, y=0.0, z=text_height),
|
||||
offset_scale=1.0,
|
||||
individual=True,
|
||||
).mesh.set_material(material=title_material)
|
||||
return {"Geometry": join_geometry(geometry=[*segments, placed_title, extruded_title])}
|
||||
|
||||
|
||||
def _finalize_groups():
|
||||
import bpy
|
||||
|
||||
groups = []
|
||||
for name in ("VG Pie Segment", "VG Pie Chart", "VG Extended Pie Segment", "VG Extended Pie Chart"):
|
||||
group = bpy.data.node_groups.get(name)
|
||||
if not group:
|
||||
continue
|
||||
group.use_fake_user = True
|
||||
if hasattr(group, "interface"):
|
||||
for item in group.interface.items_tree:
|
||||
if item.item_type == "SOCKET" and item.in_out == "OUTPUT" and item.name == "Result":
|
||||
item.name = "Geometry"
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name in {"A", "B", "C"}:
|
||||
item.name = item.name.lower()
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
|
||||
_finalize_groups()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for group in _finalize_groups():
|
||||
print(
|
||||
"VG_PIE_GROUP",
|
||||
group.name,
|
||||
len(group.nodes),
|
||||
len(group.links),
|
||||
sorted(set(node.bl_idname for node in group.nodes)),
|
||||
)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Experiments
|
||||
|
||||
Disposable render attempts, captures, and test scenes live here.
|
||||
|
||||
Promote only reviewed examples or durable lessons. Failed visual attempts are
|
||||
useful when they change future belief; otherwise they are just ballast with a
|
||||
timestamp.
|
||||
|
Before Width: | Height: | Size: 2 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
|
@ -1,15 +0,0 @@
|
|||
{"kernel":"vg_csg","mode":"stable","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":948,"min_ns":800,"p50_ns":900,"p95_ns":1100,"max_ns":1200,"triangles":72,"fragments":6,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":6123,"min_ns":4300,"p50_ns":5500,"p95_ns":6000,"max_ns":44700,"triangles":72,"fragments":6,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":85546,"min_ns":66100,"p50_ns":84900,"p95_ns":103300,"max_ns":114800,"triangles":3072,"fragments":256,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":322628,"min_ns":231400,"p50_ns":301600,"p95_ns":428500,"max_ns":841700,"triangles":3072,"fragments":256,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":162057,"min_ns":107500,"p50_ns":139600,"p95_ns":271700,"max_ns":518200,"triangles":3404,"fragments":280,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":4835134,"min_ns":2799000,"p50_ns":4260700,"p95_ns":7080200,"max_ns":10841300,"triangles":3404,"fragments":280,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":937,"min_ns":700,"p50_ns":800,"p95_ns":900,"max_ns":9700,"triangles":45,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":700876,"min_ns":396000,"p50_ns":662400,"p95_ns":963600,"max_ns":1582700,"triangles":45,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":278,"min_ns":200,"p50_ns":300,"p95_ns":300,"max_ns":400,"triangles":12,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":4762,"min_ns":4500,"p50_ns":4700,"p95_ns":5000,"max_ns":5200,"triangles":12,"fragments":1,"warnings":0}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":7900,"min_ns":5900,"p50_ns":6300,"p95_ns":12100,"max_ns":16400,"triangles":72,"vertices":144,"mesh_descriptions":3}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":775500,"min_ns":376100,"p50_ns":670500,"p95_ns":1451600,"max_ns":2108000,"triangles":12800,"vertices":20992,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":154300,"min_ns":106500,"p50_ns":132900,"p95_ns":240000,"max_ns":380100,"triangles":3498,"vertices":5102,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":64000,"min_ns":45000,"p50_ns":61400,"p95_ns":90500,"max_ns":224800,"triangles":1536,"vertices":2412,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":740400,"min_ns":340900,"p50_ns":641200,"p95_ns":1403100,"max_ns":2258600,"triangles":6180,"vertices":12360,"mesh_descriptions":4}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
newmtl mat_floor
|
||||
Kd 0.36 0.35 0.31
|
||||
Ke 0.05 0.05 0.05
|
||||
Ns 80
|
||||
|
||||
newmtl mat_wall
|
||||
Kd 0.42 0.43 0.42
|
||||
Ke 0.04 0.04 0.04
|
||||
Ns 80
|
||||
|
||||
newmtl mat_glass
|
||||
Kd 0.30 0.80 1.00
|
||||
Ke 0.20 0.55 0.70
|
||||
Ns 80
|
||||
|
||||
newmtl mat_solar
|
||||
Kd 0.05 0.11 0.20
|
||||
Ke 0.00 0.18 0.28
|
||||
Ns 80
|
||||
|
||||
newmtl mat_radiator
|
||||
Kd 0.70 0.58 0.44
|
||||
Ke 0.28 0.12 0.04
|
||||
Ns 80
|
||||
|
||||
newmtl mat_machine
|
||||
Kd 0.16 0.17 0.18
|
||||
Ke 0.06 0.07 0.08
|
||||
Ns 80
|
||||
|
||||
newmtl mat_tower
|
||||
Kd 0.72 0.76 0.78
|
||||
Ke 0.12 0.16 0.18
|
||||
Ns 80
|
||||
|
||||
newmtl mat_park
|
||||
Kd 0.22 0.46 0.25
|
||||
Ke 0.02 0.08 0.02
|
||||
Ns 80
|
||||
|
||||
newmtl mat_light
|
||||
Kd 0.08 0.75 0.90
|
||||
Ke 0.00 0.65 0.85
|
||||
Ns 80
|
||||
|
Before Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 532 KiB |
|
|
@ -1,49 +0,0 @@
|
|||
newmtl mat_scuffed_deck
|
||||
Kd 0.21 0.20 0.18
|
||||
Ke 0.015 0.012 0.010
|
||||
Ns 65
|
||||
|
||||
newmtl mat_curved_shell
|
||||
Kd 0.28 0.26 0.22
|
||||
Ke 0.015 0.012 0.010
|
||||
Ns 65
|
||||
|
||||
newmtl mat_dark_substrate
|
||||
Kd 0.045 0.043 0.040
|
||||
Ke 0.0 0.0 0.0
|
||||
Ns 65
|
||||
|
||||
newmtl mat_machinery
|
||||
Kd 0.18 0.17 0.15
|
||||
Ke 0.018 0.015 0.012
|
||||
Ns 65
|
||||
|
||||
newmtl mat_pipe_trunks
|
||||
Kd 0.09 0.085 0.075
|
||||
Ke 0.01 0.008 0.006
|
||||
Ns 65
|
||||
|
||||
newmtl mat_amber_screens
|
||||
Kd 0.95 0.48 0.12
|
||||
Ke 0.70 0.26 0.035
|
||||
Ns 65
|
||||
|
||||
newmtl mat_blue_diagnostics
|
||||
Kd 0.08 0.55 0.95
|
||||
Ke 0.03 0.28 0.60
|
||||
Ns 65
|
||||
|
||||
newmtl mat_gallery_glass
|
||||
Kd 0.30 0.65 0.90
|
||||
Ke 0.04 0.20 0.36
|
||||
Ns 65
|
||||
|
||||
newmtl mat_anchor_hazard
|
||||
Kd 0.85 0.58 0.12
|
||||
Ke 0.20 0.09 0.02
|
||||
Ns 65
|
||||
|
||||
newmtl mat_mobility_gear
|
||||
Kd 0.18 0.22 0.24
|
||||
Ke 0.02 0.025 0.03
|
||||
Ns 65
|
||||
|
Before Width: | Height: | Size: 693 KiB |
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 767 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
|
@ -1,86 +0,0 @@
|
|||
# Architecture Rationale
|
||||
|
||||
This note explains why VibeGeometry uses explicit state surfaces. It is not the
|
||||
current implementation map and not the roadmap. For those, read:
|
||||
|
||||
- `state/map.yaml`
|
||||
- `notes/fresh-workspace-handoff.md`
|
||||
- `notes/vibegeometry-implementation-plan.md`
|
||||
- `notes/vibegeometry-current-system-map.md`
|
||||
|
||||
## Core Failure
|
||||
|
||||
The central failure mode is local plausibility without global coherence.
|
||||
|
||||
An agent can write plausible Blender Python, create a large node graph, and
|
||||
still lose the visual machine. Geometry-node work is especially good at this:
|
||||
the script runs, the graph exists, the render has objects in it, and somehow
|
||||
the target has been replaced by decorative procedural fog with a confident
|
||||
variable name.
|
||||
|
||||
The answer is explicit structure:
|
||||
|
||||
- slow-changing map
|
||||
- disposable scratch
|
||||
- distilled evidence
|
||||
- compact handoff
|
||||
- implementation plan that is not pretending to be a second map
|
||||
- ruthless deletion when code, graph, or memory stops serving the target
|
||||
|
||||
## State Split
|
||||
|
||||
VibeGeometry treats different kinds of cognition as different artifacts:
|
||||
|
||||
- `map`: canonical system model, invariants, accepted design, rejected paths
|
||||
- `scratch`: temporary local reasoning for one bounded subgoal
|
||||
- `evidence`: distilled proof, decisions, rejected paths, and scars that change future belief
|
||||
- `system map`: source-grounded explanation of the live repo machinery
|
||||
- `handoff`: compact re-entry packet
|
||||
- `plan`: distilled forward implementation direction
|
||||
- `output`: user-visible replies, code edits, commits, renders, and verification artifacts
|
||||
|
||||
The split matters because Blender scene generation has many seductive proxies:
|
||||
valid Python, non-empty renders, high node counts, pretty materials, and
|
||||
parameter sliders. None of those prove that the graph still serves the visual
|
||||
target.
|
||||
|
||||
## Pipeline Principle
|
||||
|
||||
The working loop should be:
|
||||
|
||||
```text
|
||||
reference images and instructions
|
||||
-> scene brief
|
||||
-> visual trait extraction
|
||||
-> geometry strategy
|
||||
-> node graph manifest
|
||||
-> Blender Python builder
|
||||
-> render evidence
|
||||
-> review against target
|
||||
-> revise one hypothesis
|
||||
```
|
||||
|
||||
The graph is allowed to be complex. The intent is not. If the agent cannot
|
||||
describe what geometry flows through each stage and why that stage belongs,
|
||||
the next move is explanation or simplification, not another compensating node
|
||||
group with a tragic little name.
|
||||
|
||||
## Compact-Rehydrate-Reorient
|
||||
|
||||
Compaction is a real state transition, not a cosmetic cleanup.
|
||||
|
||||
Before compaction, VibeGeometry should preserve:
|
||||
|
||||
- current objective and active subgoal
|
||||
- latest stable map
|
||||
- distilled evidence or rejected paths that change future belief
|
||||
- open questions, blockers, and next action
|
||||
|
||||
After rehydrating, VibeGeometry should:
|
||||
|
||||
- reread canonical state instead of trusting prompt residue
|
||||
- restate the next action from persisted state
|
||||
- continue only when instructed or when the task explicitly calls for it
|
||||
- avoid broad implementation until the current pipeline organ is understood
|
||||
|
||||
The aim is simple: the next waking thing should find coals instead of ash.
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
# Fresh Workspace Handoff
|
||||
|
||||
## Immediate Re-entry Instruction
|
||||
|
||||
Read `state/map.yaml`, this file, `notes/vibegeometry-current-system-map.md`,
|
||||
and `notes/vibegeometry-implementation-plan.md`, then run:
|
||||
|
||||
```powershell
|
||||
npm run state:status
|
||||
```
|
||||
|
||||
Restate the persisted next action before editing.
|
||||
|
||||
Do not continue implementation automatically from a rehydrate-only request.
|
||||
|
||||
Do not trust this file for the exact live HEAD. Use git for volatile truth:
|
||||
|
||||
```powershell
|
||||
git status --short --branch
|
||||
git log --oneline -5
|
||||
```
|
||||
|
||||
## Current State
|
||||
|
||||
VibeGeometry now has three live implementation organs:
|
||||
|
||||
- `vibegeometry/`: Python helpers for Blender coordinate frames, deterministic
|
||||
fields, batching, object/material/camera setup, and verifier checks.
|
||||
- `crates/vg_csg`: Rust/Bevy-math CSG brush assembler.
|
||||
- `crates/vg_grammar`: Dream Machine grammar layer emitting semantic solid/void
|
||||
claims into `vg_csg`.
|
||||
|
||||
Corpus maps, story-bound builds, translation notes, RealtimeCSG doctrine, and
|
||||
procedural doctrine live under `docs/research/`; this handoff is not the ledger.
|
||||
Artifact schemas are still pending.
|
||||
|
||||
Current authoring prior: use the full Python stack. Python structures intent,
|
||||
tables, loops, variants, cleanup, validation, docs, and scene/render
|
||||
orchestration; Geometry Script emits inspectable Geometry Nodes groups; `bpy`
|
||||
handles Blender scene machinery and evaluated evidence.
|
||||
|
||||
`crates/vg_csg` is the first Rust/Bevy-side interactive geometry organ. In
|
||||
VibeGeometry it is now a git submodule pointing at
|
||||
`https://github.com/GameCult/vg-csg`, kept at the same path so workspace and
|
||||
`vg_grammar` path dependencies still resolve.
|
||||
That repo's `main` was rewritten from filtered VibeGeometry history rather than
|
||||
a synthetic import, preserving the CSG crate, docs, native bridge, and perf
|
||||
harness commit chain with one standalone packaging commit on top.
|
||||
RealtimeCSG's public Unity repo exposes a useful brush/tree/operation/generation
|
||||
model and public P/Invoke declarations for the optimized native C++ plugin.
|
||||
The standalone direct-DLL bridge under `tools/realtimecsg_native_bridge` mirrors
|
||||
that managed wrapper surface and intentionally fails closed if the plugin emits
|
||||
zero mesh descriptions. Current bridge status: the DLL loads, exported calls
|
||||
create brush meshes, brushes, and trees, the health probe verifies bounds,
|
||||
outlines, raycasts, and mesh-description extraction, and the combined perf
|
||||
fixture appends native C++ timing records. The key construction lesson: build
|
||||
brush nodes before the tree, create the tree, insert children through the public
|
||||
Foundation range-insert path, and avoid eagerly setting default brush operation
|
||||
or flag state.
|
||||
Do not use those zero outputs as timings. Sander van Rossen's older public
|
||||
`LogicalError/Realtime-CSG-demo` and blog series remain the preferred clean
|
||||
research inputs. Current `vg_csg`: public CSG operation/branch/tree/brush handle
|
||||
surface, child and operation mutation, ordered assembler backend, exact AABB
|
||||
and oriented-box subtraction and intersection via convex plane splitting,
|
||||
crossing polygon classification into inside/outside/aligned/reverse-aligned
|
||||
buckets, polygon category/visibility/reversal metadata, primitive/solid/tree
|
||||
bounds, additive `CylinderZ`, `DomeCapZ`, and `FloretArm` primitives,
|
||||
axis-aligned box fast path, compiled brush geometry cache, owned convex
|
||||
splitting, convex containment shortcuts, stable-generation output cache, stable
|
||||
and dirty release perf fixture modes, Bevy-math mesh buffers, tests, clippy,
|
||||
and examples.
|
||||
Research note: `docs/research/realtime-csg-bevy-assembler.md`.
|
||||
Where `vg_csg` overlaps public RealtimeCSG/demo behavior, add exact observable
|
||||
parity fixtures instead of approximate plausibility tests.
|
||||
|
||||
Current visible Rust-side showpiece: `cargo run -p vg_csg --example
|
||||
dream_machine_level` emits `experiments/generated/dream_machine_level/` with
|
||||
OBJ, MTL, and SVG preview artifacts. It is a machine-cathedral switchyard:
|
||||
cut foundation nave, central glass dome, anchor-city towers, giant alternating
|
||||
solar/radiator florets, looping cargo arteries, and material partitions. Build
|
||||
note: `docs/research/builds/dream-machine-level.md`.
|
||||
|
||||
Grounded fixture example: `cargo run -p vg_csg --example
|
||||
industrial_maintenance_loop` emits
|
||||
`experiments/generated/industrial_maintenance_loop/` with OBJ, MTL, SVG, and PNG
|
||||
preview artifacts. It builds a curved habitat-shell maintenance loop from local
|
||||
tangent/radial/up frames: central floor-to-ceiling machinery island, inner
|
||||
manifold faces, outer staging rail/tools, one supervisor gallery segment,
|
||||
service artery mouths, and dry-operation mobility gear. Build note:
|
||||
`docs/research/builds/industrial-maintenance-loop.md`.
|
||||
|
||||
`crates/vg_grammar` is the Dream Machine grammar layer above CSG. Rules emit
|
||||
semantic solid/void claims with tags, then compile those claims into `vg_csg`
|
||||
brushes. Current features: local `Frame`s, oriented claims, seeded variation,
|
||||
`RuleSet` composition, `RoomSpec`, `CorridorSpec`, `DoorSpec`, and
|
||||
`GalleryChainSpec`.
|
||||
Example: `cargo run -p vg_grammar --example dream_room`. Research note:
|
||||
`docs/research/dream-machine-grammar.md`.
|
||||
|
||||
Repo-local Geometry Script clone notes live in
|
||||
`docs/research/geometry-script-fork-notes.md`. Run
|
||||
`.\tools\setup_geometry_script_clone.ps1` if `external/geometry-script` is
|
||||
missing.
|
||||
|
||||
The current next action is to build the first Bevy inspection shell around the
|
||||
Rust-side CSG artifact, using the Dream Machine level as the load/generate
|
||||
target. Keep orbit camera, material groups, and dirty edit hooks visible so
|
||||
future mesh-patching work is judged against an actual interactive object rather
|
||||
than isolated timing fixtures.
|
||||
|
||||
Spatial-query correction from EpiphanyAquarium memory: GigaVoxels, froxels, and
|
||||
Dreams all point away from static "better tree" thinking. Let the consumer
|
||||
shape the query frontier first: dirty brush, branch bounds, requested output
|
||||
tile/view, or visible surface set. BVHs, grids, sweep-and-prune, and pair caches
|
||||
are candidate layouts only after that frontier exists and timing evidence says
|
||||
they help.
|
||||
|
||||
First implementation slice: `vg_csg` exports `DemandFrontier`/`DemandPair`, and
|
||||
`BuildReport` plus the JSONL perf fixture now expose `candidate_pairs` and
|
||||
`rejected_pairs`. These counters are currently observational and follow existing
|
||||
bounds gates; they are not yet a replacement router.
|
||||
|
||||
Second implementation slice: `Assembler::rebuild_routed_surfaces()` now exposes
|
||||
an experimental surface-router path for exactly one convex source and one
|
||||
candidate subtractive convex cutter. It emits surviving source boundary polygons
|
||||
plus reversed cutter cap polygons, so the centered cut drops from 72 closed
|
||||
fragment-carver triangles to 48 boundary triangles. The perf fixture only emits
|
||||
`mode:"routed"` for supported cases. Dense repeated cutters must stay on the
|
||||
ordered kernel until the router has compact frontier batching and scratch
|
||||
storage; naive surface-list routing exploded polygon counts.
|
||||
|
||||
Latest dense-kernel guardrail: `PolygonRouteScratch` now reuses routing scratch
|
||||
vectors for polygon classification. An attempted in-place fragment frontier
|
||||
changed `rotated_cut_stack_64` output shape, so it was reverted. A regression
|
||||
now pins that fixture at 280 fragments, 3404 triangles, 804 candidate pairs, and
|
||||
9417 rejected pairs. Future speed passes must either preserve that ordered
|
||||
output exactly or make an explicit, reviewed output-contract transition.
|
||||
|
||||
Current realtime-editing seam: `Assembler` can mutate brush primitives and
|
||||
operations by `BrushId`, invalidating cached output and incrementing generation.
|
||||
`DirtyDemandFrontier` computes the conservative ordered suffix after the first
|
||||
dirty brush. The prefix before that index is the future cache boundary; the
|
||||
suffix is live because ordered CSG decisions propagate forward. Box-only ordered
|
||||
CSG and the general convex builder now have prefix checkpoint caches, and
|
||||
`build_incremental()` resumes from the checkpoint before the first dirty brush.
|
||||
Output parity is tested against full rebuilds. Checkpoint validity is tracked as
|
||||
a prefix boundary to avoid deep-copying stale history after tail edits. Mesh
|
||||
emission is still whole-output for candidate edits; rejected dirty suffixes can
|
||||
reuse the previous mesh when replay proves no geometry was touched. The report
|
||||
flag is `reused_mesh`. `rebuild()` intentionally bypasses checkpoint caches;
|
||||
box dirty edits currently use the direct builder because the honest baseline is
|
||||
faster than checkpoint replay until real mesh patching exists. The
|
||||
category-router path is still separate. Perf records also expose
|
||||
`used_checkpoint`, `checkpoint_rebuild_from`, and `direct_dirty_rebuild` so
|
||||
future timing comparisons show which path actually ran.
|
||||
|
||||
## Important Invariants
|
||||
|
||||
- Target Blender 5.0+ unless the task says otherwise.
|
||||
- Geometry nodes are the primary procedural geometry surface.
|
||||
- Python is the authoring/orchestration layer, not a fallback after Geometry
|
||||
Script runs out of rope.
|
||||
- Python constructs and orchestrates graphs; it should not bury the design in
|
||||
opaque imperative mesh generation.
|
||||
- Visual target evidence must remain attached to scene briefs and render
|
||||
review.
|
||||
- A graph is not accepted just because it runs. Render evidence decides.
|
||||
- Node groups, exposed sockets, and attributes need names clear enough for
|
||||
future agents to inspect and modify.
|
||||
- Add evidence only when it changes future belief.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What exact JSON shape should the scene brief use for references,
|
||||
instructions, target traits, and acceptance criteria?
|
||||
- What should the first node graph manifest schema require versus merely allow?
|
||||
- What node-group inspection JSON shape is most useful for translating public
|
||||
`.blend` examples into Geometry Script?
|
||||
- Which Blender 5.0+ APIs and node construction helpers should become the
|
||||
first stable builder layer?
|
||||
- What is the smallest useful render-review artifact?
|
||||
|
||||
## Compaction Notes
|
||||
|
||||
For imminent compaction, write hot memory first to
|
||||
`state/scratch-compaction-<guid>.md`, then run:
|
||||
|
||||
```powershell
|
||||
npm run state:prepare-compaction
|
||||
```
|
||||
|
||||
Fold only durable lessons into canonical state and delete the temporary
|
||||
scratch-compaction file after it has served its purpose.
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# VibeGeometry Current System Map
|
||||
|
||||
VibeGeometry does not have a Blender runtime yet. The live system is a
|
||||
persistence scaffold plus a planned artifact pipeline for agent-authored
|
||||
geometry-node scenes.
|
||||
|
||||
## Control Flow
|
||||
|
||||
1. Rehydrate from `state/map.yaml`, `notes/fresh-workspace-handoff.md`, this
|
||||
file, and `notes/vibegeometry-implementation-plan.md`.
|
||||
2. Run `npm run state:status`.
|
||||
3. Work one bounded pipeline organ.
|
||||
4. Validate the seam that matters.
|
||||
5. Persist only belief-changing evidence.
|
||||
6. Commit completed work.
|
||||
|
||||
## Core Surfaces
|
||||
|
||||
- `state/map.yaml`: canonical mission, boundaries, live architecture, next action.
|
||||
- `state/evidence.jsonl`: distilled belief-changing evidence only.
|
||||
- `state/branches.json`: active and closed research or implementation branches.
|
||||
- `notes/fresh-workspace-handoff.md`: compact re-entry packet.
|
||||
- `notes/vibegeometry-implementation-plan.md`: near-term implementation sequence.
|
||||
- `docs/architecture/`: detailed contracts and rationale.
|
||||
|
||||
## Planned Artifact Pipeline
|
||||
|
||||
```text
|
||||
reference images + user instructions
|
||||
-> scene brief
|
||||
-> target visual trait extraction
|
||||
-> geometry strategy
|
||||
-> node graph manifest
|
||||
-> Blender Python builder script
|
||||
-> generated .blend scene
|
||||
-> render output
|
||||
-> render review and graph audit
|
||||
-> accepted example or revised hypothesis
|
||||
```
|
||||
|
||||
## Missing Or Incomplete Organs
|
||||
|
||||
- Scene brief schema
|
||||
- Node graph manifest schema
|
||||
- Render review schema
|
||||
- Blender Python builder helpers
|
||||
- Geometry-node idiom library
|
||||
- Headless Blender runner
|
||||
- Reference-image storage and provenance rules
|
||||
- Render comparison and review workflow
|
||||
- Example scene fixtures
|
||||
|
||||
## Current North Star
|
||||
|
||||
Build the smallest reliable loop where an agent can describe a target, plan
|
||||
procedural geometry, generate a legible Blender node graph through Python,
|
||||
render it, and use review evidence to improve the next iteration. The loop
|
||||
should reward coherent geometry flow, not just scripts that successfully make
|
||||
Blender emit pixels.
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
# VibeGeometry Implementation Plan
|
||||
|
||||
## Current Phase
|
||||
|
||||
Turn the verified translation corpus into a small set of durable contracts and
|
||||
builder seams for agent-authored Blender geometry-node scenes.
|
||||
|
||||
The goal is still not a procedural art factory. The goal is a clean artifact
|
||||
loop where agents can state a visual target, choose a geometry strategy, emit
|
||||
inspectable node graphs, render evidence, and revise one hypothesis at a time
|
||||
without piling clever junk into the machine.
|
||||
|
||||
Planning belongs in this file and the handoff/map state surfaces. Architecture
|
||||
docs should describe durable contracts, boundaries, and data shapes; they should
|
||||
not carry live next-action clutter.
|
||||
|
||||
## Corpus Lessons
|
||||
|
||||
- Python is the planning and orchestration layer: loops, tables, variants,
|
||||
validation, cleanup, documentation, and Blender scene/render control.
|
||||
- Geometry Script is strongest as an inspectable graph emitter, especially when
|
||||
the script keeps names, sockets, attribute flow, and reusable group boundaries
|
||||
legible.
|
||||
- `bpy` remains necessary for Blender objects, modifiers, materials, cameras,
|
||||
lights, files, renders, and evaluated-geometry comparison.
|
||||
- `nodes_to_script` is useful as a converter draft and source-graph probe. Its
|
||||
output should inform the translation, not define good house style.
|
||||
- Accepted graph translations need behavioral verification, not just successful
|
||||
script execution.
|
||||
- City-scale graphs expose the next toolchain pressure: repeat zones, foreach
|
||||
zones, gizmo nodes, repeat `Top` outputs, and converter edge cases.
|
||||
- Source-bound scenes need a written coordinate contract before ornament.
|
||||
Define basis axes, up/down, attachment surfaces, and frame transitions in the
|
||||
script and verifier before adding detail.
|
||||
- Organic distribution needs a field, not tidy placement. Use deterministic
|
||||
noise to drive density, offsets, sizes, heights, material/class bands, and
|
||||
gaps when the visual target is accretion, poverty, erosion, cloud, vegetation,
|
||||
or informal growth.
|
||||
- Dense procedural detail must be batched by material/class and line family.
|
||||
Thousands of standalone Blender objects are the dumb kind of expensive.
|
||||
- Render evidence must include cameras that face the domain being judged. A
|
||||
beauty camera can hide the exact spatial mistake that matters.
|
||||
|
||||
## Near-Term Sequence
|
||||
|
||||
0. Make the Rust CSG machine meaner and leaner.
|
||||
- Replace the dense rotated subtraction carver with a category-router path
|
||||
that preserves source polygons, splits only crossings, routes categories,
|
||||
and keeps cached-vs-dirty output parity green before performance claims.
|
||||
- Add a demand-frontier prototype before any generic BVH/grid index. The
|
||||
frontier should emit affected brush pairs from dirty brushes, branch
|
||||
bounds, and requested output scope; indexes are candidate storage layouts,
|
||||
not the idea.
|
||||
- Add counters and fixtures for global rebuild, affected-brush rebuild, and
|
||||
request/tile-scoped rebuild so timings explain what work was refused.
|
||||
- Move hot polygon/category work toward reusable scratch arenas and compact
|
||||
arrays only after the router proves exact output parity.
|
||||
|
||||
1. Promote the Bloom coordinate/noise lessons into reusable builder seams.
|
||||
- Build on the new `vibegeometry/` helper library rather than adding more
|
||||
one-off example functions.
|
||||
- Preserve the split between coordinate contract, density field, batched
|
||||
realization, domain camera, and verifier checks.
|
||||
- Keep the first extraction small: endcap terrace slums or spiral transfer
|
||||
arteries, not the whole Bloom at once.
|
||||
|
||||
2. Define the scene brief contract.
|
||||
- Capture references, text instructions, target visual traits, constraints,
|
||||
camera/composition requirements, acceptance criteria, and known unknowns.
|
||||
- Preserve source paths or URLs for every reference image.
|
||||
- Require a source-coordinate section for lore/reference-bound builds:
|
||||
basis axes, local up/down, attachment surfaces, and known frame changes.
|
||||
|
||||
3. Define the node graph manifest contract.
|
||||
- Describe node groups, sockets, exposed controls, modifiers, attribute
|
||||
names, field flow, materials, dependencies, and verification expectations.
|
||||
- Require enough naming and rationale for future agents to inspect the graph.
|
||||
- Keep graph contracts Blender 5.0+ oriented.
|
||||
|
||||
4. Build a minimal Python builder seam.
|
||||
- Start with one tiny scene that proves graph construction, material setup,
|
||||
camera, lighting, save, and render orchestration.
|
||||
- Prefer small composable helpers for node group creation and socket linking.
|
||||
- Avoid a giant builder abstraction until repeated graph idioms demand it.
|
||||
|
||||
5. Build render review artifacts.
|
||||
- Store render path, scene brief id, manifest id, script path, Blender
|
||||
version, review status, visual misses, graph issues, and next hypothesis.
|
||||
- Treat rendered output as the acceptance surface for visual claims.
|
||||
- Require at least one domain-facing inspection render for every major
|
||||
spatial claim, not just a composed overview.
|
||||
|
||||
6. Promote doctrine only after it survives use.
|
||||
- Keep examples small enough to inspect.
|
||||
- Put sample-specific graph maps in `docs/research/translations/`.
|
||||
- Keep `docs/research/procedural-doctrine.md` tight enough to fit in working
|
||||
context when building a new scene from scratch. Nobody needs a sacred
|
||||
haystack.
|
||||
|
||||
7. Generalize after the loop works.
|
||||
- Add reference-image ingestion helpers.
|
||||
- Add headless batch rendering.
|
||||
- Add visual comparison or reviewer prompts.
|
||||
- Add richer examples only after the contracts stop sliding.
|
||||
|
||||
8. Return to IRCSS graph blockers after the scene loop has these contracts.
|
||||
- Either fix one repeat-zone/foreach/gizmo blocker in the Geometry Script
|
||||
fork or select a non-zone architectural helper with a non-empty behavioral
|
||||
harness.
|
||||
- Keep the chosen target small enough to verify against evaluated geometry.
|
||||
- Record rejected paths only when they change future decisions.
|
||||
|
||||
## Deferred
|
||||
|
||||
- Large procedural asset library
|
||||
- Automated visual scoring beyond basic review artifacts
|
||||
- Fine-tuned graph-generation models
|
||||
- Blender add-on packaging
|
||||
- Complex simulation-node workflows before the basic graph loop is stable
|
||||
|
||||
## Discipline
|
||||
|
||||
- Work one bounded organ at a time.
|
||||
- Explain target-to-graph data flow before broad refactors.
|
||||
- Prefer explicit manifests and render evidence over implicit agent memory.
|
||||
- If the diff grows while understanding shrinks, stop and simplify.
|
||||
- Keep history in git; keep persisted state focused on what future sessions
|
||||
need to believe.
|
||||
10
package.json
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"name": "vibegeometry",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "Persistent research repo for agent-authored Blender geometry-node scene generation.",
|
||||
"scripts": {
|
||||
"state:status": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\vibegeometry_state.py status",
|
||||
"state:prepare-compaction": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\vibegeometry_prepare_compaction.py"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
From 55085d0a4b7fed62fdbbb7a3375bf71d9874268f Mon Sep 17 00:00:00 2001
|
||||
From: Meta Vens <rwvens@hotmail.com>
|
||||
Date: Mon, 4 May 2026 13:41:12 +0100
|
||||
Subject: [PATCH] Fix nested tree group references on Blender 4+
|
||||
|
||||
---
|
||||
api/tree.py | 17 +++++++++++++----
|
||||
1 file changed, 13 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/api/tree.py b/api/tree.py
|
||||
index d34dc81..b2250a9 100644
|
||||
--- a/api/tree.py
|
||||
+++ b/api/tree.py
|
||||
@@ -163,12 +163,21 @@ def tree(name):
|
||||
# This lets @trees be used in other @trees via simple function calls.
|
||||
def group_reference(*args, **kwargs):
|
||||
if IS_BLENDER_4:
|
||||
- result = geometrynodegroup(node_tree=node_group, *args, **kwargs)
|
||||
+ result = State.current_node_tree.nodes.new('GeometryNodeGroup')
|
||||
+ result.node_tree = node_group
|
||||
+ enabled_inputs = [node_input for node_input in result.inputs if node_input.enabled]
|
||||
+ for value, node_input in zip(args, enabled_inputs):
|
||||
+ set_or_create_link(value, node_input)
|
||||
+ for node_input in enabled_inputs[len(args):]:
|
||||
+ argname = node_input.name.lower().replace(' ', '_')
|
||||
+ if argname in kwargs:
|
||||
+ set_or_create_link(kwargs[argname], node_input)
|
||||
else:
|
||||
result = group(node_tree=node_group, *args, **kwargs)
|
||||
group_outputs = []
|
||||
- for group_output in result._socket.node.outputs:
|
||||
- group_outputs.append(Type(group_output))
|
||||
+ for group_output in result.outputs:
|
||||
+ if group_output.enabled:
|
||||
+ group_outputs.append(Type(group_output))
|
||||
if len(group_outputs) == 1:
|
||||
return group_outputs[0]
|
||||
else:
|
||||
@@ -178,4 +187,4 @@ def tree(name):
|
||||
return build_tree
|
||||
else:
|
||||
tree_name = name.__name__
|
||||
- return build_tree(name)
|
||||
\ No newline at end of file
|
||||
+ return build_tree(name)
|
||||
--
|
||||
2.53.0.windows.3
|
||||
|
||||
|
|
@ -1,389 +0,0 @@
|
|||
From 277d8b01284fba5fe07c759483963859bf35c781 Mon Sep 17 00:00:00 2001
|
||||
From: Meta Vens <rwvens@hotmail.com>
|
||||
Date: Mon, 4 May 2026 15:06:48 +0100
|
||||
Subject: [PATCH] Port node tree conversion operator
|
||||
|
||||
---
|
||||
__init__.py | 9 ++
|
||||
operators/__init__.py | 1 +
|
||||
operators/convert_tree.py | 316 ++++++++++++++++++++++++++++++++++++++
|
||||
3 files changed, 326 insertions(+)
|
||||
create mode 100644 operators/__init__.py
|
||||
create mode 100644 operators/convert_tree.py
|
||||
|
||||
diff --git a/__init__.py b/__init__.py
|
||||
index 9e428e7..df8432a 100644
|
||||
--- a/__init__.py
|
||||
+++ b/__init__.py
|
||||
@@ -22,6 +22,7 @@ import webbrowser
|
||||
from .api.tree import *
|
||||
from .preferences import GeometryScriptPreferences
|
||||
from .absolute_path import absolute_path
|
||||
+from .operators.convert_tree import ConvertTree
|
||||
|
||||
bl_info = {
|
||||
"name" : "Geometry Script",
|
||||
@@ -74,6 +75,10 @@ def templates_menu_draw(self, context):
|
||||
def editor_header_draw(self, context):
|
||||
self.layout.menu(GeometryScriptMenu.bl_idname)
|
||||
|
||||
+def node_header_draw(self, context):
|
||||
+ if context.space_data.tree_type == 'GeometryNodeTree':
|
||||
+ self.layout.operator(ConvertTree.bl_idname)
|
||||
+
|
||||
def auto_resolve():
|
||||
if bpy.context.scene.geometry_script_settings.auto_resolve:
|
||||
try:
|
||||
@@ -97,8 +102,10 @@ def register():
|
||||
bpy.utils.register_class(GeometryScriptPreferences)
|
||||
bpy.utils.register_class(OpenDocumentation)
|
||||
bpy.utils.register_class(GeometryScriptMenu)
|
||||
+ bpy.utils.register_class(ConvertTree)
|
||||
|
||||
bpy.types.TEXT_HT_header.append(editor_header_draw)
|
||||
+ bpy.types.NODE_HT_header.append(node_header_draw)
|
||||
|
||||
bpy.types.Scene.geometry_script_settings = bpy.props.PointerProperty(type=GeometryScriptSettings)
|
||||
|
||||
@@ -111,7 +118,9 @@ def unregister():
|
||||
bpy.utils.unregister_class(GeometryScriptPreferences)
|
||||
bpy.utils.unregister_class(OpenDocumentation)
|
||||
bpy.utils.unregister_class(GeometryScriptMenu)
|
||||
+ bpy.utils.unregister_class(ConvertTree)
|
||||
bpy.types.TEXT_HT_header.remove(editor_header_draw)
|
||||
+ bpy.types.NODE_HT_header.remove(node_header_draw)
|
||||
try:
|
||||
bpy.app.timers.unregister(auto_resolve)
|
||||
except:
|
||||
diff --git a/operators/__init__.py b/operators/__init__.py
|
||||
new file mode 100644
|
||||
index 0000000..ea7f7d1
|
||||
--- /dev/null
|
||||
+++ b/operators/__init__.py
|
||||
@@ -0,0 +1 @@
|
||||
+"""Geometry Script Blender operators."""
|
||||
diff --git a/operators/convert_tree.py b/operators/convert_tree.py
|
||||
new file mode 100644
|
||||
index 0000000..8485ea2
|
||||
--- /dev/null
|
||||
+++ b/operators/convert_tree.py
|
||||
@@ -0,0 +1,316 @@
|
||||
+import collections.abc
|
||||
+import keyword
|
||||
+import re
|
||||
+
|
||||
+import bpy
|
||||
+import mathutils
|
||||
+
|
||||
+
|
||||
+_SOCKET_TYPE_NAMES = {
|
||||
+ "BOOLEAN": "Bool",
|
||||
+ "COLLECTION": "Collection",
|
||||
+ "GEOMETRY": "Geometry",
|
||||
+ "IMAGE": "Image",
|
||||
+ "INT": "Int",
|
||||
+ "MATERIAL": "Material",
|
||||
+ "MENU": "Menu",
|
||||
+ "OBJECT": "Object",
|
||||
+ "RGBA": "Color",
|
||||
+ "ROTATION": "Rotation",
|
||||
+ "STRING": "String",
|
||||
+ "TEXTURE": "Texture",
|
||||
+ "VALUE": "Float",
|
||||
+ "VECTOR": "Vector",
|
||||
+}
|
||||
+
|
||||
+_PROP_DENYLIST = {
|
||||
+ "bl_idname",
|
||||
+ "bl_label",
|
||||
+ "bl_static_type",
|
||||
+ "color",
|
||||
+ "dimensions",
|
||||
+ "height",
|
||||
+ "hide",
|
||||
+ "inputs",
|
||||
+ "internal_links",
|
||||
+ "label",
|
||||
+ "location",
|
||||
+ "mute",
|
||||
+ "name",
|
||||
+ "outputs",
|
||||
+ "parent",
|
||||
+ "rna_type",
|
||||
+ "select",
|
||||
+ "show_options",
|
||||
+ "show_preview",
|
||||
+ "show_texture",
|
||||
+ "type",
|
||||
+ "use_custom_color",
|
||||
+ "warning_propagation",
|
||||
+ "width",
|
||||
+}
|
||||
+
|
||||
+
|
||||
+def _snake_case(value):
|
||||
+ value = re.sub(r"[^0-9A-Za-z_]+", "_", value.strip())
|
||||
+ value = re.sub(r"_+", "_", value).strip("_").lower()
|
||||
+ if not value:
|
||||
+ value = "value"
|
||||
+ if value[0].isdigit():
|
||||
+ value = "_" + value
|
||||
+ if keyword.iskeyword(value):
|
||||
+ value += "_"
|
||||
+ return value
|
||||
+
|
||||
+
|
||||
+def _node_function_name(node):
|
||||
+ if node.bl_idname == "GeometryNodeGroup" and getattr(node, "node_tree", None):
|
||||
+ return _snake_case(node.node_tree.name)
|
||||
+ return _snake_case(node.bl_rna.name)
|
||||
+
|
||||
+
|
||||
+def _socket_name(socket):
|
||||
+ return _snake_case(socket.name or socket.identifier)
|
||||
+
|
||||
+
|
||||
+def _enabled_sockets(sockets):
|
||||
+ return [socket for socket in sockets if socket.enabled and socket.type != "CUSTOM"]
|
||||
+
|
||||
+
|
||||
+def _convert_value(value):
|
||||
+ if isinstance(value, mathutils.Vector):
|
||||
+ return tuple(value)
|
||||
+ if isinstance(value, mathutils.Euler):
|
||||
+ return tuple(value)
|
||||
+ if isinstance(value, mathutils.Color):
|
||||
+ return tuple(value)
|
||||
+ if isinstance(value, bpy.types.bpy_prop_array):
|
||||
+ return tuple(value)
|
||||
+ if isinstance(value, collections.abc.Iterable) and not isinstance(value, (str, bytes)):
|
||||
+ try:
|
||||
+ return tuple(value)
|
||||
+ except TypeError:
|
||||
+ return value
|
||||
+ return value
|
||||
+
|
||||
+
|
||||
+def _literal(value):
|
||||
+ value = _convert_value(value)
|
||||
+ if isinstance(value, bpy.types.ID):
|
||||
+ return "None"
|
||||
+ return repr(value)
|
||||
+
|
||||
+
|
||||
+def _socket_type_annotation(socket):
|
||||
+ return _SOCKET_TYPE_NAMES.get(socket.type, "Type")
|
||||
+
|
||||
+
|
||||
+class _Assignment:
|
||||
+ def __init__(self, name, node):
|
||||
+ self.name = name
|
||||
+ self.node = node
|
||||
+ self.props = {}
|
||||
+ self.arguments = {}
|
||||
+ self.argument_outputs = {}
|
||||
+ self.argument_is_multi_input = {}
|
||||
+
|
||||
+ def flattened_arguments(self):
|
||||
+ result = []
|
||||
+ for value in self.arguments.values():
|
||||
+ if isinstance(value, _Assignment):
|
||||
+ result.append(value)
|
||||
+ elif isinstance(value, list):
|
||||
+ result.extend(item for item in value if isinstance(item, _Assignment))
|
||||
+ return result
|
||||
+
|
||||
+ def _value_expr(self, key, value, index=None):
|
||||
+ if isinstance(value, list):
|
||||
+ values = [self._value_expr(key, item, index=i) for i, item in enumerate(value)]
|
||||
+ if self.argument_is_multi_input.get(key, False):
|
||||
+ return "[" + ", ".join(values) + "]"
|
||||
+ if len(values) == 1:
|
||||
+ return values[0]
|
||||
+ return "(" + ", ".join(values) + ")"
|
||||
+ if not isinstance(value, _Assignment):
|
||||
+ return _literal(value)
|
||||
+ if value.node.type == "GROUP_INPUT":
|
||||
+ output_name = self.argument_outputs[key] if index is None else self.argument_outputs[key][index]
|
||||
+ return output_name
|
||||
+ output_name = self.argument_outputs[key] if index is None else self.argument_outputs[key][index]
|
||||
+ enabled_outputs = _enabled_sockets(value.node.outputs)
|
||||
+ if len(enabled_outputs) > 1:
|
||||
+ return f"{value.name}.{output_name}"
|
||||
+ return value.name
|
||||
+
|
||||
+ def _argument_expr(self, key, value):
|
||||
+ return f"{key}={self._value_expr(key, value)}"
|
||||
+
|
||||
+ def result_targets(self):
|
||||
+ return self.name
|
||||
+
|
||||
+ def to_script(self):
|
||||
+ args = []
|
||||
+ for key, value in self.props.items():
|
||||
+ args.append(f"{key}={_literal(value)}")
|
||||
+ for key, value in self.arguments.items():
|
||||
+ args.append(self._argument_expr(key, value))
|
||||
+ return f"{self.result_targets()} = {_node_function_name(self.node)}({', '.join(args)})"
|
||||
+
|
||||
+
|
||||
+def _collect_node_props(node):
|
||||
+ props = {}
|
||||
+ parent_props = set()
|
||||
+ if getattr(node.bl_rna, "base", None) is not None:
|
||||
+ parent_props.update(prop.identifier for prop in node.bl_rna.base.bl_rna.properties)
|
||||
+ for prop in node.bl_rna.properties:
|
||||
+ if prop.identifier in parent_props or prop.identifier in _PROP_DENYLIST:
|
||||
+ continue
|
||||
+ if prop.is_readonly and prop.type not in {"ENUM", "BOOLEAN", "INT", "FLOAT", "STRING"}:
|
||||
+ continue
|
||||
+ try:
|
||||
+ value = getattr(node, prop.identifier)
|
||||
+ except Exception:
|
||||
+ continue
|
||||
+ if prop.type == "POINTER":
|
||||
+ continue
|
||||
+ if prop.type == "COLLECTION":
|
||||
+ continue
|
||||
+ props[_snake_case(prop.identifier)] = _convert_value(value)
|
||||
+ return props
|
||||
+
|
||||
+
|
||||
+def _collect_input_defaults(node):
|
||||
+ props = {}
|
||||
+ for socket in _enabled_sockets(node.inputs):
|
||||
+ if not hasattr(socket, "default_value") or socket.hide_value:
|
||||
+ continue
|
||||
+ props[_socket_name(socket)] = _convert_value(socket.default_value)
|
||||
+ return props
|
||||
+
|
||||
+
|
||||
+def _topological_sort(root):
|
||||
+ seen = set()
|
||||
+ order = []
|
||||
+
|
||||
+ def visit(assignment):
|
||||
+ if assignment in seen:
|
||||
+ return
|
||||
+ seen.add(assignment)
|
||||
+ for dependency in assignment.flattened_arguments():
|
||||
+ visit(dependency)
|
||||
+ order.append(assignment)
|
||||
+
|
||||
+ visit(root)
|
||||
+ return order
|
||||
+
|
||||
+
|
||||
+def _function_arguments(group_input):
|
||||
+ if group_input is None:
|
||||
+ return []
|
||||
+ arguments = []
|
||||
+ for output in _enabled_sockets(group_input.outputs):
|
||||
+ arguments.append(f"{_socket_name(output)}: {_socket_type_annotation(output)}")
|
||||
+ return arguments
|
||||
+
|
||||
+
|
||||
+def _return_entries(group_output, output_assignment):
|
||||
+ entries = []
|
||||
+ for socket in _enabled_sockets(group_output.node.inputs):
|
||||
+ key = _socket_name(socket)
|
||||
+ if key not in group_output.arguments:
|
||||
+ continue
|
||||
+ value = group_output.arguments[key]
|
||||
+ expr = group_output._argument_expr(key, value).split("=", 1)[1]
|
||||
+ entries.append(f'"{socket.name}": {expr}')
|
||||
+ if entries:
|
||||
+ return "{ " + ", ".join(entries) + " }"
|
||||
+ return output_assignment.name
|
||||
+
|
||||
+
|
||||
+def convert_node_tree(tree):
|
||||
+ """Return a best-effort Geometry Script draft for a Blender node tree."""
|
||||
+ assignments = []
|
||||
+ node_to_assignment = {}
|
||||
+ node_type_counts = {}
|
||||
+ incoming_links = {}
|
||||
+
|
||||
+ for node in tree.nodes:
|
||||
+ prefix = _snake_case(node.type[:1] or "node")
|
||||
+ count = node_type_counts.get(prefix, 0) + 1
|
||||
+ node_type_counts[prefix] = count
|
||||
+ assignment = _Assignment(f"{prefix}{count}", node)
|
||||
+ assignment.props.update(_collect_node_props(node))
|
||||
+ assignments.append(assignment)
|
||||
+ node_to_assignment[node] = assignment
|
||||
+
|
||||
+ for link in tree.links:
|
||||
+ incoming_links.setdefault(link.to_socket, []).append((node_to_assignment[link.from_node], _socket_name(link.from_socket)))
|
||||
+
|
||||
+ for assignment in assignments:
|
||||
+ grouped_inputs = {}
|
||||
+ for socket in _enabled_sockets(assignment.node.inputs):
|
||||
+ key = _socket_name(socket)
|
||||
+ grouped_inputs.setdefault(key, []).append(socket)
|
||||
+ for key, sockets in grouped_inputs.items():
|
||||
+ values = []
|
||||
+ output_names = {}
|
||||
+ is_multi_input = any(socket.is_multi_input for socket in sockets)
|
||||
+ for socket in sockets:
|
||||
+ links = incoming_links.get(socket, [])
|
||||
+ if links:
|
||||
+ for from_assignment, output_name in links:
|
||||
+ output_names[len(values)] = output_name
|
||||
+ values.append(from_assignment)
|
||||
+ elif hasattr(socket, "default_value") and not socket.hide_value:
|
||||
+ values.append(_convert_value(socket.default_value))
|
||||
+ if not values:
|
||||
+ continue
|
||||
+ if len(values) == 1:
|
||||
+ assignment.arguments[key] = values[0]
|
||||
+ if output_names:
|
||||
+ assignment.argument_outputs[key] = output_names[0]
|
||||
+ else:
|
||||
+ assignment.arguments[key] = values
|
||||
+ assignment.argument_outputs[key] = output_names
|
||||
+ assignment.argument_is_multi_input[key] = is_multi_input
|
||||
+
|
||||
+ group_output = next((assignment for assignment in assignments if assignment.node.type == "GROUP_OUTPUT"), None)
|
||||
+ if group_output is None:
|
||||
+ raise ValueError(f"Node tree {tree.name!r} has no Group Output node")
|
||||
+ group_input = next((node for node in tree.nodes if node.type == "GROUP_INPUT"), None)
|
||||
+
|
||||
+ sorted_assignments = [
|
||||
+ assignment
|
||||
+ for assignment in _topological_sort(group_output)
|
||||
+ if assignment.node.type not in {"GROUP_INPUT", "GROUP_OUTPUT"}
|
||||
+ ]
|
||||
+ body = "\n ".join(assignment.to_script() for assignment in sorted_assignments)
|
||||
+ if not body:
|
||||
+ body = "pass"
|
||||
+
|
||||
+ arguments = ", ".join(_function_arguments(group_input))
|
||||
+ function_name = _snake_case(tree.name)
|
||||
+ return_expr = _return_entries(group_output, sorted_assignments[-1] if sorted_assignments else group_output)
|
||||
+
|
||||
+ return (
|
||||
+ "from geometry_script import *\n\n"
|
||||
+ f'@tree("{tree.name}")\n'
|
||||
+ f"def {function_name}({arguments}):\n"
|
||||
+ f" {body}\n"
|
||||
+ f" return {return_expr}\n"
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+class ConvertTree(bpy.types.Operator):
|
||||
+ bl_idname = "geometry_script.convert_tree"
|
||||
+ bl_label = "Convert to Geometry Script"
|
||||
+
|
||||
+ @classmethod
|
||||
+ def poll(cls, context):
|
||||
+ return bool(getattr(getattr(context, "space_data", None), "node_tree", None))
|
||||
+
|
||||
+ def execute(self, context):
|
||||
+ script = convert_node_tree(context.space_data.node_tree)
|
||||
+ script_datablock = bpy.data.texts.new(context.space_data.node_tree.name + ".py")
|
||||
+ script_datablock.write(script)
|
||||
+ return {"FINISHED"}
|
||||
--
|
||||
2.53.0.windows.3
|
||||
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
From 6fbebb8ff2ae13a97b7b87c7a25cf14d72825c07 Mon Sep 17 00:00:00 2001
|
||||
From: Meta Vens <rwvens@hotmail.com>
|
||||
Date: Mon, 4 May 2026 15:51:38 +0100
|
||||
Subject: [PATCH] Tolerate Blender menu socket defaults
|
||||
|
||||
---
|
||||
api/tree.py | 8 ++++++--
|
||||
1 file changed, 6 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/api/tree.py b/api/tree.py
|
||||
index b2250a9..a787091 100644
|
||||
--- a/api/tree.py
|
||||
+++ b/api/tree.py
|
||||
@@ -115,8 +115,12 @@ def tree(name):
|
||||
node_input = node_group.interface.new_socket(socket_type=arg[1][0].socket_type, name=input_name, in_out='INPUT')
|
||||
else:
|
||||
node_input = node_group.inputs.new(arg[1][0].socket_type, input_name)
|
||||
- if arg[1][1] != inspect.Parameter.empty:
|
||||
- node_input.default_value = arg[1][1]
|
||||
+ if arg[1][1] != inspect.Parameter.empty and arg[1][1] is not None:
|
||||
+ try:
|
||||
+ node_input.default_value = arg[1][1]
|
||||
+ except TypeError:
|
||||
+ if getattr(node_input, "bl_socket_idname", "") != "NodeSocketMenu":
|
||||
+ raise
|
||||
if arg[1][2] is not None:
|
||||
if arg[1][2] not in builder_inputs:
|
||||
builder_inputs[arg[1][2]] = signature.parameters[arg[1][2]].annotation()
|
||||
--
|
||||
2.53.0.windows.3
|
||||
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
From ebac332fe79a608525b223b88f1a89fb2fe80eb9 Mon Sep 17 00:00:00 2001
|
||||
From: Meta Vens <rwvens@hotmail.com>
|
||||
Date: Mon, 4 May 2026 20:04:50 +0100
|
||||
Subject: [PATCH] Avoid duplicate converter keywords for socket-backed
|
||||
properties
|
||||
|
||||
---
|
||||
operators/convert_tree.py | 5 +++++
|
||||
1 file changed, 5 insertions(+)
|
||||
|
||||
diff --git a/operators/convert_tree.py b/operators/convert_tree.py
|
||||
index 8485ea2..fa239ab 100644
|
||||
--- a/operators/convert_tree.py
|
||||
+++ b/operators/convert_tree.py
|
||||
@@ -265,6 +265,11 @@ def convert_node_tree(tree):
|
||||
values.append(_convert_value(socket.default_value))
|
||||
if not values:
|
||||
continue
|
||||
+ # Some Blender nodes expose the same conceptual control as both an
|
||||
+ # RNA enum property and an input socket. Geometry Script maps that
|
||||
+ # collapsed control to one Python keyword, so prefer the socket
|
||||
+ # value/link and drop the duplicate property keyword.
|
||||
+ assignment.props.pop(key, None)
|
||||
if len(values) == 1:
|
||||
assignment.arguments[key] = values[0]
|
||||
if output_names:
|
||||
--
|
||||
2.53.0.windows.3
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# Prompts
|
||||
|
||||
Reusable prompts for VibeGeometry agents live here.
|
||||
|
||||
Expected prompt families:
|
||||
|
||||
- scene brief extraction from reference images and instructions
|
||||
- geometry strategy planning
|
||||
- node graph manifest drafting
|
||||
- Blender Python generation
|
||||
- render review and graph audit
|
||||
|
||||
Keep prompts tied to explicit artifacts. A prompt that only says "make it
|
||||
better" is a small bureaucracy for producing fog.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Schemas
|
||||
|
||||
Future JSON schemas live here.
|
||||
|
||||
Planned contracts:
|
||||
|
||||
- `scene-brief.schema.json`
|
||||
- `node-graph-manifest.schema.json`
|
||||
- `render-review.schema.json`
|
||||
|
||||
Add schemas when the first concrete example needs validation.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Scripts
|
||||
|
||||
Utility scripts live here.
|
||||
|
||||
Persistence machinery belongs in `tools/`. Blender runners, render helpers,
|
||||
image prep utilities, and batch experiment scripts can live here once they
|
||||
exist.
|
||||
|
|
@ -1,3 +1 @@
|
|||
{
|
||||
"branches": []
|
||||
}
|
||||
[]
|
||||
|
|
|
|||
|
|
@ -1,79 +1 @@
|
|||
{"ts":"2026-05-04T00:00:00+00:00","type":"scaffold","status":"accepted","note":"Initialized VibeGeometry with Epiphany-style persistence surfaces adapted to Blender 5.0+ geometry-node scene generation."}
|
||||
{"ts": "2026-05-04T11:52:15+00:00", "type": "research", "status": "ok", "note": "Geometry Script add-on is installed under Blender 5.1 and imports headlessly. A trivial cube node group built successfully in Blender 5.1.1; the documented grid().mesh_to_points() chain failed with NoneType callable, so Geometry Script should be treated as promising but each idiom must be smoke-tested against the installed Blender/add-on pair."}
|
||||
{"ts": "2026-05-04T11:55:00+00:00", "type": "research", "status": "ok", "note": "Local Geometry Script 5.1 generated docs are the authoritative node-signature source for this workspace. The prior grid().mesh_to_points() probe failed because grid() returns a result object with mesh and uv_map; the valid chain is grid(...).mesh.mesh_to_points(...)."}
|
||||
{"ts": "2026-05-04T12:11:47+00:00", "type": "research", "status": "accepted", "note": "Public Geometry Nodes learning material is mostly screenshots, videos, and downloadable .blend files. VibeGeometry should build durable Geometry Script knowledge by inspecting source .blend node groups, mapping one graph at a time in prose, then recreating and smoke-testing with local Geometry Script 5.1 signatures."}
|
||||
{"ts": "2026-05-04T12:21:06+00:00", "type": "implementation", "status": "accepted", "note": "Completed first Geometry Nodes translation corpus entry: inspected Shriinivas/cartesian.blend NodeGroup.001, mapped its curve/vector field flow, recreated it as examples/geometry_script/shriinivas_cartesian_helper.py, and smoke-tested generation/save/reinspection in Blender 5.1.1. Generated node groups need fake users or real users before saving."}
|
||||
{"ts": "2026-05-04T12:24:11+00:00", "type": "verification", "status": "accepted", "note": "Verified translated Shriinivas cartesian helper behavior against source NodeGroup.001 by wrapping both helpers as Curve+Vector -> Set Position -> Curve to Mesh and comparing evaluated mesh vertices. Test used Count=25, Start=(-0.45,0,0), End=(0.45,0,0), r=0.6, Value=1.25; source and translation both produced 200 vertices with max delta 0.0."}
|
||||
{"ts": "2026-05-04T12:42:20+00:00", "type": "implementation", "status": "accepted", "note": "Translated and verified all four inspected Shriinivas/cartesian.blend graph groups: helper, line, parabola, and circle. Verification compares evaluated meshes in Blender 5.1.1; helper/line/parabola match direct vertex order with max delta 0.0, circle matches sorted vertex set with max sorted delta 0.0."}
|
||||
{"ts": "2026-05-04T12:42:20+00:00", "type": "tooling", "status": "accepted", "note": "Reverted direct AppData Geometry Script surgery. Cloned upstream Geometry Script into ignored external/geometry-script, committed local branch vibegeometry/blender-5-nested-tree-groups with nested @tree group fix, stored patch under patches/geometry-script, and added setup_geometry_script_clone.ps1 plus loader so VibeGeometry uses the repo-local patched clone."}
|
||||
{"ts": "2026-05-04T12:48:18+00:00", "type": "implementation", "status": "accepted", "note": "Translated and verified Shriinivas/piechart.blend small graph family: NodeGroup -> VG Pie Segment and Pie Chart -> VG Pie Chart. Segment and chart evaluated mesh comparisons both passed in Blender 5.1.1 with sorted max vertex delta 0.0. Doctrine added: reusable groups can pass invisible scalar state such as End angle alongside visible geometry."}
|
||||
{"ts": "2026-05-04T12:50:55+00:00", "type": "implementation", "status": "accepted", "note": "Translated and verified Shriinivas/fieldvalue.blend Digit At scalar helper as VG Digit At. Verification wraps source and translation by driving Mesh Line.Count and matches four number/position cases, including zero output. Doctrine added: value-only node groups need geometry-producing verification harnesses."}
|
||||
{"ts": "2026-05-04T12:53:00+00:00", "type": "implementation", "status": "accepted", "note": "Extended fieldvalue translation with Create Decimal -> VG Create Decimal. Verified against source by comparing evaluated dot geometry: both source and translation produce 32 vertices with sorted max vertex delta 0.0. Doctrine added: tiny glyph marks such as decimal points should be isolated as named reusable parts when they carry stable display intent."}
|
||||
{"ts": "2026-05-04T12:56:02+00:00", "type": "implementation", "status": "accepted", "note": "Extended fieldvalue translation with Create Segment -> VG Create Segment. Verified horizontal and vertical segment cases against source with 6 vertices each and sorted max vertex delta 0.0. Doctrine added: field masks can act as chisels over regular seed meshes, gating vertex offsets by local position conditions."}
|
||||
{"ts": "2026-05-04T13:11:35+00:00", "type": "tooling", "status": "accepted", "note": "Created and pushed GameCult/VibeGeometry remote. Forked Geometry Script to GameCult/geometry-script, pushed branch vibegeometry/blender-5-nested-tree-groups, and opened upstream PR https://github.com/carson-katri/geometry-script/pull/69 for the nested @tree group reference fix."}
|
||||
{"ts": "2026-05-04T13:22:50+00:00", "type": "tooling", "status": "accepted", "note": "Pruned inherited upstream-only branches gh-pages and nodes_to_script from GameCult/geometry-script. The fork branch surface is intentionally main plus vibegeometry/blender-5-nested-tree-groups while upstream may retain its own historical branch soup."}
|
||||
{"ts": "2026-05-04T13:24:38+00:00", "type": "research", "status": "accepted", "note": "Upstream Geometry Script nodes_to_script branch is a real node-tree-to-script prototype from 2023-03-01. It adds operators/convert_tree.py, which walks a node tree and emits a @tree script text datablock, but it predates Blender 4 interface migration and later main-branch fixes, so VibeGeometry should treat it as a translation assistant candidate to port/test rather than a current drop-in dependency."}
|
||||
{"ts": "2026-05-04T13:30:24+00:00", "type": "implementation", "status": "accepted", "note": "Extended fieldvalue translation with Seven Segments -> VG Seven Segments. Verified against source with 74 vertices and sorted max vertex delta 0.0 in Blender 5.1.1. Doctrine added: chassis groups compose tested parts into a joined assembly while preserving part intent in script."}
|
||||
{"ts": "2026-05-04T13:34:19+00:00", "type": "implementation", "status": "accepted", "note": "Extended fieldvalue translation with Delete Segments -> VG Delete Segments. Verified source and translation across 195 digit/segment-position cases with zero mismatches by driving the boolean output into a Mesh Line count harness. Doctrine added: dense boolean node regions often encode lookup tables and should be translated into named clauses plus full-grid verification."}
|
||||
{"ts": "2026-05-04T13:37:51+00:00", "type": "implementation", "status": "accepted", "note": "Extended fieldvalue translation with Next Digit -> VG Next Digit. Verified four branch cases spanning both sides of Max Precision by driving Result into Mesh Line.Count; all source and translated counts matched."}
|
||||
{"ts": "2026-05-04T13:37:51+00:00", "type": "research", "status": "accepted", "note": "Full fieldvalue Field Value graph is a 176-node composition with menu switches, capture attributes across domains, sample-index reads, instancing, transforms, delete geometry, and material assignment. Manual one-pass translation would be high-risk; next action should test or port upstream Geometry Script nodes_to_script as a generated first-draft tool before attempting the full graph."}
|
||||
{"ts": "2026-05-04T13:47:26+00:00", "type": "documentation", "status": "accepted", "note": "Reframed procedural doctrine as a spatial reasoning playbook rather than abstraction glossary. Each doctrine pattern should describe the visual cue, mental move, Geometry Script code shape, and verification cue so metaphors help an agent choose tools while reasoning about form."}
|
||||
{"ts": "2026-05-04T13:51:58+00:00", "type": "documentation", "status": "accepted", "note": "Audited procedural doctrine snippets against the verified Geometry Script translations and removed conceptual shorthand. Doctrine snippets should use the same explicit Geometry Script node-call style as the scripts that passed Blender verification, with excerpts clearly behaving as excerpts rather than pseudocode."}
|
||||
{"ts": "2026-05-04T14:10:09+00:00", "type": "tooling", "status": "accepted", "note": "Ported upstream Geometry Script nodes_to_script prototype onto GameCult fork branch vibegeometry/blender-5-nodes-to-script. Added callable convert_node_tree(tree), UI operator registration, patch persistence, and a Blender smoke harness. In Blender 5.1.1 the converter emits compiling draft scripts for Create Decimal, Digit At, Next Digit, Seven Segments, Delete Segments, and the 176-node Field Value group; generated output is a bootstrap draft requiring cleanup and behavioral verification."}
|
||||
{"ts": "2026-05-04T14:32:43+00:00", "type": "doctrine", "status": "accepted", "note": "nodes_to_script output is evidence and scaffolding, not style guidance. Use it to recover wiring/defaults/socket order and hidden graph structure, then rewrite into doctrine-quality Geometry Script with spatial names, intentional decomposition, and behavioral verification."}
|
||||
{"ts": "2026-05-04T14:35:46+00:00", "type": "doctrine", "status": "accepted", "note": "Updated VibeGeometry priors: Python is the authoring and orchestration layer, Geometry Script is the inspectable node-graph emission surface, bpy handles Blender scene/render/evaluation machinery, and external Python tools handle inspection, converter drafts, verification, docs, and corpus evidence. Do not reason as if the Geometry Script DSL is the whole toolchain."}
|
||||
{"ts": "2026-05-04T14:54:04+00:00", "type": "implementation", "status": "ok", "note": "Completed and verified full VG Field Value translation, including Blender 5 capture-item helper and Geometry Script menu-socket default tolerance patch. Added and verified paramnpolareq translations for equation helpers, Archimedes spiral, epicycloid, and mirrored root spiral."}
|
||||
{"ts": "2026-05-04T14:59:36+00:00", "type": "implementation", "status": "ok", "note": "Started CurveToMeshUV utility sample by translating and verifying Auto Smooth; main Curve to Mesh UV remains a staged utility-quarry target rather than a one-pass translation."}
|
||||
{"ts": "2026-05-04T15:57:11+00:00", "type": "implementation", "status": "ok", "note": "Finished remaining local sample graph families: extended pie clones/compositor, param/polar selector and wrapper, and full CurveToMeshUV utility/demo/title with mesh, UV-field, and caps-mask harness verification."}
|
||||
{"ts": "2026-05-04T16:56:12+00:00", "type": "implementation", "status": "accepted", "note": "Expanded the verified Geometry Nodes translation corpus with official Blender demo samples: instance attributes grass tuft, index-of-nearest helpers, shortest-path labyrinth, and raycast minigame helpers. All new verifiers pass in Blender 5.1.1 with sorted max vertex delta 0.0 across their harness cases. Deferred official samples are classified as legacy-node, gizmo-node, simulation-zone, or repeat-zone targets rather than padded into doctrine."}
|
||||
{"ts": "2026-05-04T17:15:09+00:00", "type": "research", "status": "accepted", "note": "Started IRCSS French Houses city-scale corpus target. Inspected 108 geometry node groups and verified first foothold MakeSpiral -> VG Make Spiral with 200 ordered curve points matching source at max delta 0.0. Recorded blockers from larger city graphs: repeat-zone feedback state, repeat Top output sockets, foreach zones, gizmo nodes, converter duplicate-key failures, and empty-output harness rejection."}
|
||||
{"ts": "2026-05-04T18:01:03+00:00", "type": "maintenance", "status": "accepted", "note": "Distilled persistent state: handoff now points to corpus docs instead of duplicating completed graph lists; implementation plan reflects post-corpus contract and IRCSS frontier; compaction helper warns on bloated handoff and stale first-corpus planning."}
|
||||
{"ts": "2026-05-04T19:01:27+00:00", "type": "verification", "status": "accepted", "note": "Added IRCSS PointyGothicCone translation as VG Pointy Gothic Cone; plain and ornamented harness cases match source evaluated geometry with sorted max vertex delta 0.0, proving ornament can be grown from captured structural ribs after instancing."}
|
||||
{"ts": "2026-05-04T19:05:41+00:00", "type": "tooling", "status": "accepted", "note": "Patched Geometry Script nodes_to_script on branch vibegeometry/blender-5-nodes-to-script to drop duplicate property keywords when a same-named input socket exists; GenerateArc and MakeStairs now compile from converter drafts, and expanded IRCSS smoke passes for 11 target groups."}
|
||||
{"ts": "2026-05-04T22:03:08+00:00", "type": "research", "status": "accepted", "note": "Mapped IRCSS French Houses at file level: 108 node groups, 63 modifier roots, 163 dependency edges, composer/organ/utility hierarchy, dependency hubs, blocker feature counts, and next target candidates. Inspector now records nested GeometryNodeGroup node_tree references."}
|
||||
{"ts": "2026-05-04T22:09:04+00:00", "type": "research", "status": "accepted", "note": "IRCSS final result is object-level scene composition, not a single master town graph: visible collections combine generated houses, church/cathedral parts, walls/gate/bridge, farms, terrain, river, and set-dressing roots; top evaluated roots are HousesOnCurves and Geometry Nodes.002 cathedral."}
|
||||
{"ts": "2026-05-04T22:19:41+00:00", "type": "research", "status": "accepted", "note": "Accounted for all 108 IRCSS French Houses geometry node group datablocks in a structural node atlas, including modifier use, dependencies, feature pressure, asset bindings, and translation posture. This upgrades corpus knowledge from scene-level map to per-graph coverage, while preserving the boundary that only accepted translations have behavioral equivalence proof."}
|
||||
{"ts": "2026-05-04T22:31:47+00:00", "type": "research", "status": "accepted", "note": "Studied IRCSS French Houses below the group level: 16,889 inspected nodes, 138 unique node types, 844 artist frame labels, 273 nested group nodes, and complete mechanism tables for all 108 groups. Distilled the file into reusable translation doctrine: edge-first architecture, curves as attachment rails, capture before domain changes, Python tables for orchestration, and repeat/foreach/gizmo/closure as toolchain grammar rather than incidental noise."}
|
||||
{"ts": "2026-05-04T23:10:37+00:00", "type": "implementation", "status": "accepted", "note": "Built a from-scratch Aetheria Bloom habitat study after reading Bloom lore and Bruneton Rama making-of references. Refocused from Kappa detail to whole-cylinder spaces: layered shell, axial hub/light spine, spokes, central water band, plains/farms, forests, city clusters, wrapped roads/rivers, cloud patches, utility routes, and one small service-ring marker. Blender verification passes with VG Bloom Light Spine at 21 nodes/31 links and 98 geometry/text objects."}
|
||||
{"ts": "2026-05-04T23:38:17+00:00", "type": "implementation", "status": "accepted", "note": "Reworked the Aetheria Bloom build from a cutout/map-layer view into a full 360-degree whole-habitat composition: full shell layers, despun center-axis core and docking hub cap, spun spire sheath, multi-station major spokes, thin utility spokes, frame-transfer arteries, hub-cap terrace slums, favela/hyperurban belts, luxury spoke districts, mixed urban/suburban/industrial belts, industrial/agricultural areas, beach and sea bands, roads/rivers, and airflow cloud patches. Verification now passes with 481 geometry/text objects and VG Bloom Light Spine still 21 nodes/31 links."}
|
||||
{"ts": "2026-05-04T23:59:24+00:00", "type": "implementation", "status": "accepted", "note": "Aetheria Bloom whole-habitat pass now uses shared cylindrical coordinate helpers, golden-angle spiral spoke/transfer loops, and explicit layered endcaps; Blender verifier passed with 735 geometry/text objects and light-spine Geometry Script group at 21 nodes/31 links."}
|
||||
{"ts": "2026-05-05T00:21:08+00:00", "type": "implementation", "status": "accepted", "note": "Bloom detail pass added batched fractal favela wall detail, elaborated spoke plazas, and nested town fields; initial per-object version timed out, batching preserved visual detail and verifier passed with 1102 geometry/text objects."}
|
||||
{"ts": "2026-05-05T00:33:45+00:00", "type": "correction", "status": "accepted", "note": "Corrected Bloom terrace-slum orientation against lore: on hubward endcaps, up is inward toward the Spire, so slums are annular shelves around the docking hub office complex growing outward to the surface; removed the misleading cylinder-wall favela framing and verifier now checks hubward_endcap_rice_paddy_slums objects."}
|
||||
{"ts": "2026-05-05T00:55:36+00:00", "type": "correction", "status": "accepted", "note": "Bloom endcap terrace slums now use fractal value-noise occupancy rather than neat concentric box rings; verified dedicated hubward-endcap render and mesh-density checks for poor shacks, midrise housing, and outer urban crown."}
|
||||
{"ts": "2026-05-05T01:12:46+00:00", "type": "maintenance", "status": "accepted", "note": "Polished persistent state after Bloom corrections: map, handoff, plan, and doctrine now prioritize source coordinate contracts, deterministic noise for organic occupancy, batched dense detail, domain-facing inspection renders, and verifier checks as reusable machine behavior rather than one-off scene scars."}
|
||||
{"ts": "2026-05-05T01:25:43+00:00", "type": "implementation", "status": "accepted", "note": "Created the first reusable vibegeometry Python helper library for coordinate frames, deterministic fields, dense mesh batching, Blender scene primitives, and verifier helpers; refactored the Bloom build and verifier to use it, then rebuilt and verified the scene in Blender with 1028 geometry/text objects."}
|
||||
{"ts": "2026-05-05T01:40:49+00:00", "type": "implementation", "status": "accepted", "note": "Built and verified the Lucent tether habitat from Ghostlight source: tether axis, city bubble below, media-eye transfer lounge, glass rail/safe-line threshold, Lucent overlay/control surfaces, and an inspectable VG Lucent Feed Ribbon Geometry Script group. Render review accepted overview, lounge, and city-bubble inspection artifacts."}
|
||||
{"ts": "2026-05-05T01:59:41+00:00", "type": "implementation", "status": "accepted", "note": "Revised Lucent tether habitat after visual review: city bubble now has explicit tether load spine, elevator umbilical, collar, stays, and transfer node; renders now use Cycles; shader graphs add procedural noise/bump/ramp nodes to glass, tether, and Lucent overlay surfaces; verifier checks these contracts."}
|
||||
{"ts": "2026-05-05T02:20:23+00:00", "type": "implementation", "status": "accepted", "note": "Replaced Lucent city-bubble grid with street-generation-inspired radial/tangential field roads, irregular annular parcel cells, anchor-plaza prestige skyscrapers, park voids, midcity blocks, and faux-rural cottagecore edge falloff; tether now visibly descends to the city ground-surface plaza."}
|
||||
{"ts": "2026-05-05T03:01:59+00:00", "type": "implementation", "status": "accepted", "note": "Corrected Lucent dome city coordinate frame: ground is now a YZ disk orthogonal to the tether/spin axis, buildings extrude along +X, dome is boolean-cut flat at the base, city fills to the rim, and a Perlin-edged sunflower ray-floret crest rises from the base partway up the dome."}
|
||||
{"ts": "2026-05-05T03:16:04+00:00", "type": "implementation", "status": "accepted", "note": "Moved Lucent sunflower ray florets outside the dome: alternating solar panel and radiator meshes now radiate sunward from the flat base rim, with verifier checks that their geometry remains outside the dome cut."}
|
||||
{"ts": "2026-05-05T03:31:22+00:00", "type": "implementation", "status": "accepted", "note": "Scaled Lucent external ray florets to Citadel-like utility arms: alternating solar/radiator panels now dwarf the dome and span far along the tether, so a folded defensive posture can plausibly reach toward the media-eye end."}
|
||||
{"ts": "2026-05-06T23:06:51+00:00", "type": "implementation", "status": "accepted", "note": "Added vg_csg, a Rust workspace crate for the new interactive Bevy-compatible CSG path: RealtimeCSG-inspired brush ordering and generations, exact AABB box subtraction, additive cylinder/dome/floret primitives, mesh-array output, DSL, tests, clippy, example, and research doctrine. Public RealtimeCSG source informed the shape but not the hidden native kernel."}
|
||||
{"ts": "2026-05-06T23:20:20+00:00", "type": "implementation", "status": "accepted", "note": "Upgraded vg_csg from AABB-only subtraction to a convex-brush stepping-stone kernel: oriented-box brushes, plane splitting, convex fragment emission, angled-cut tests, and updated RealtimeCSG doctrine. Public LogicalError Realtime-CSG-demo and Sander van Rossen blog/article material are now preferred clean research inputs for the faster target model: polygon classification through boolean trees, not decompiled native internals."}
|
||||
{"ts": "2026-05-06T23:31:47+00:00", "type": "research", "status": "accepted", "note": "Distilled public Realtime CSG ecosystem doctrine from Sander van Rossen blog posts, LogicalError/Realtime-CSG-demo, BSO3, GDC talk listing, and downstream engine discussions: target vg_csg should migrate from fragment carving toward polygon classification through boolean trees with explicit categories, half-edge/topology working data, bounds early-outs, dirty affected-brush rebuilds, separate mesh optimization, and CSG as one procedural grammar operator."}
|
||||
{"ts": "2026-05-07T00:12:44+00:00", "type": "implementation", "status": "accepted", "note": "Built the first Dream Machine grammar layer: new vg_grammar crate with semantic solid/void ClaimTree, GrammarContext materials, RoomSpec/CorridorSpec/DoorSpec rules, a dream_room example, and tests proving grammar emits claims that compile into vg_csg brushes and leave doorway voids empty. The authoring split is now grammar writes spatial claims, CSG adjudicates matter, mesh is cache."}
|
||||
{"ts": "2026-05-07T00:19:35+00:00", "type": "implementation", "status": "accepted", "note": "Extended the Dream Machine grammar with local frames, oriented claims, deterministic seeded variation, RuleSet composition, and GalleryChainSpec. The dream_room example now emits a rotated four-room chain as 42 semantic claims into vg_csg; workspace tests and clippy prove the grammar still preserves void claims through CSG assembly."}
|
||||
{"ts": "2026-05-07T00:21:11+00:00", "type": "implementation", "status": "accepted", "note": "Added exact parity testing doctrine and first vg_csg parity fixtures for public RealtimeCSG-overlapping seams: polygon category vocabulary, six-plane/six-polygon box brush counts, and six surviving regions for centered box subtraction. Future matching API surface should get countable parity fixtures, not approximate plausibility checks."}
|
||||
{"ts": "2026-05-07T00:31:59+00:00", "type": "implementation", "status": "accepted", "note": "Expanded the Rust API toward RealtimeCSG shape: added CsgTreeArena, CsgTree/CsgTreeBranch/CsgTreeBrush handles, CsgOperationType ordinals, CsgBranchOp vocabulary, branch child/operation mutation, brush operation mutation, tree-to-assembler compilation, grammar ClaimTree CSG tree emission, and parity fixtures for operation ordinals, branch vocabulary, and handle topology."}
|
||||
{"ts": "2026-05-07T00:41:39+00:00", "type": "implementation", "status": "accepted", "note": "Advanced vg_csg toward the category-router kernel: ConvexPolygon now carries category/visibility/reversal/bounds metadata, primitives/solids/tree nodes expose bounds for early rejection, whole non-crossing polygons classify as inside/outside/aligned/reverse-aligned against convex cutters, convex intersection now powers Intersect brushes and Common branches, and parity/workspace tests keep the RealtimeCSG-shaped API seams honest."}
|
||||
{"ts": "2026-05-07T00:50:13+00:00", "type": "implementation", "status": "accepted", "note": "Added CSG performance fixtures and advanced parity surface: vg_csg now has a release JSONL timing fixture with room-grid, rotated-cut, common-chain, distant-cutter, and centered-cut scenarios; tools/run_csg_perf.ps1 records our timings and an explicit missing reference slot; ConvexSolid can now split crossing polygons into inside/outside/aligned/reverse-aligned category buckets, matching the public demo shape before boolean routing."}
|
||||
{"ts": "2026-05-07T01:35:18+00:00", "type": "implementation", "status": "blocked", "note": "Added a standalone RealtimeCSG C++ P/Invoke bridge that mirrors the managed Unity wrapper surface and fails closed for zero mesh descriptions. The native DLL loads and creates brush meshes, brushes, and trees, but outside Unity's hosted lifecycle GenerateMeshDescriptions currently returns success with count 0 even for an additive cube, so native timings are not yet valid."}
|
||||
{"ts": "2026-05-07T02:27:49+00:00", "type": "implementation", "status": "ok", "note": "RealtimeCSG native bridge now reaches mesh extraction and timing: matching the public Foundation path (brushes first, tree second, InsertChildNodeRange, no redundant default brush setters) converts the direct DLL bridge from zero mesh descriptions to health cube mesh descriptions and full perf fixture records."}
|
||||
{"ts": "2026-05-07T02:49:43+00:00", "type": "implementation", "status": "ok", "note": "CSG speed pass kept parity tests green while adding an axis-aligned box fast path, compiled brush geometry cache, owned convex splitting, clipping allocation reserves, and stable-generation output caching. Stable-tree fixture now beats or matches the direct C++ bridge on sampled scenarios, but dirty dense rotated subtraction still needs category-router work."}
|
||||
{"ts": "2026-05-07T03:31:28+00:00", "type": "implementation", "status": "ok", "note": "Added explicit stable and dirty vg_csg perf modes, public rebuild() bypassing stable output cache, cached-vs-dirty output parity test, and convex containment shortcuts. Latest fixture shows dirty box-heavy and distant-rejection paths are respectable; dirty dense rotated subtraction remains the next category-router target."}
|
||||
{"ts": "2026-05-07T06:57:45+00:00", "type": "research", "status": "accepted", "note": "EpiphanyAquarium persisted memory changes the vg_csg acceleration prior: before adding a generic BVH/grid/sweep index, prototype demand-shaped spatial frontiers driven by dirty brushes, branch bounds, requested output scope, and visible/tile surfaces. GigaVoxels/froxel/Dreams doctrine treats rays or consumers as the query oracle; indexes are storage layouts for that frontier, not the central idea."}
|
||||
{"ts": "2026-05-07T07:19:04+00:00", "type": "implementation", "status": "ok", "note": "Started the meaner vg_csg pass: persisted the CSG roadmap, added exported DemandFrontier/DemandPair for ordered brush affected-pair planning, and added BuildReport/perf JSONL candidate_pairs and rejected_pairs counters along existing bounds gates. Tests/clippy pass; perf counters show rotated_cut_stack_64 still burns many rejected fragment/operator checks, so category-router plus frontier batching remains the next code target."}
|
||||
{"ts": "2026-05-07T07:30:39+00:00", "type": "implementation", "status": "ok", "note": "Added the first experimental vg_csg routed-surface path: single convex source plus exactly one candidate subtractive convex cutter emits surviving source boundary polygons plus reversed cutter cap polygons, reducing centered-cut triangles from 72 fragment-carver triangles to 48 boundary triangles. Dense repeated cutters are deliberately not routed yet: naive surface-list routing caused surface-piece explosion, so the next dense-kernel move must use compact category/frontier batching and scratch storage rather than unbounded polygon Vec churn."}
|
||||
{"ts": "2026-05-07T07:37:47+00:00", "type": "implementation", "status": "ok", "note": "Added PolygonRouteScratch for routed polygon classification and wired it into rebuild_routed_surfaces. An attempted in-place fragment frontier sped nothing safely: it changed rotated_cut_stack_64 output shape, so it was reverted and a regression now pins that scenario at 280 fragments, 3404 triangles, 804 candidate pairs, and 9417 rejected pairs. Future dense speed work must preserve ordered output or introduce an explicit accepted-output transition, not silently mutate semantics."}
|
||||
{"ts": "2026-05-07T07:47:54+00:00", "type": "implementation", "status": "ok", "note": "Added the first realtime-editing seam to vg_csg: brushes can now mutate primitive/op by BrushId, invalidating cached output and incrementing generation; DirtyDemandFrontier computes the conservative ordered suffix after the first dirty brush, identifying the future prefix-cache boundary. This is a planning/cache seam, not yet incremental mesh rebuild."}
|
||||
{"ts": "2026-05-07T14:05:07+00:00", "type": "implementation", "status": "ok", "note": "Added box-only dirty prefix checkpoints to vg_csg. build_incremental resumes from the checkpoint before the first dirty brush and parity tests compare incremental output against full rebuilds. Perf fixture now includes incremental_dirty; tail edits improve in large box-only scenarios, but final mesh emission remains whole-output and non-box paths fall back."}
|
||||
{"ts": "2026-05-07T14:14:21+00:00", "type": "implementation", "status": "ok", "note": "Extended vg_csg incremental dirty rebuilds from box-only state to the general convex builder. The cache now tracks a valid prefix boundary so tail edits reuse the checkpoint before the dirty brush without deep-copying stale checkpoint history. Latest smoke fixture shows large tail-edit wins, including rotated_cut_stack_64 dirty mean dropping from about 3.49 ms full rebuild to about 0.42 ms incremental, with parity tests still green."}
|
||||
{"ts": "2026-05-07T14:39:10+00:00", "type": "implementation", "status": "ok", "note": "Added conservative mesh reuse for vg_csg incremental dirty rebuilds. Mutations no longer discard the previous BuildOutput; suffix replay tracks whether geometry was touched, and rejected dirty suffixes reuse the prior mesh exactly. BuildReport exposes reused_mesh and tests cover distant tail edits. Candidate edits still emit the whole mesh."}
|
||||
{"ts": "2026-05-07T14:50:34+00:00", "type": "implementation", "status": "ok", "note": "Corrected vg_csg dirty perf semantics: rebuild now bypasses checkpoint caches, while build populates them. The honest fixture showed box candidate edits are faster through the direct box builder than checkpoint replay, so incremental box dirty edits now fall back to direct unless future mesh patching changes that. Added distant_oriented_cutters_128 to exercise conservative reused_mesh on the general convex path."}
|
||||
{"ts": "2026-05-07T14:55:58+00:00", "type": "implementation", "status": "ok", "note": "Added explicit rebuild-strategy fields to vg_csg BuildReport and perf JSON: used_checkpoint, checkpoint_rebuild_from, and direct_dirty_rebuild. Tests now pin direct box fallback, general convex checkpoint replay, and reused_mesh for rejected general convex edits so future timings cannot hide which path ran."}
|
||||
{"ts": "2026-05-07T15:08:41+00:00", "type": "implementation", "status": "ok", "note": "Rust CSG vertical slice added: dream_machine_level example emits a named multi-part machine-cathedral OBJ/MTL/SVG artifact, proving LevelDsl can carry visible showpiece blockouts before Bevy renderer integration."}
|
||||
{"ts": "2026-05-07T15:42:22+00:00", "type": "review", "status": "revised", "note": "Dream Machine preview rejected the first CSG showpiece silhouette: rectangular nave, roof ribs, and flat petals overpowered the dome. Revised toward circular hub, visible dome, lowered city mass, orbiting arteries, and segmented curved ray panels; thumbnail silhouette must read before detail budget matters."}
|
||||
{"ts": "2026-05-07T17:36:47+00:00", "type": "implementation", "status": "ok", "note": "Added industrial_maintenance_loop Rust CSG fixture from grounded reference prompt. Durable DSL lesson: curved habitat room details should be authored in local tangent/radial/up frames per domain, then compiled into world brushes or direct mesh bands."}
|
||||
{"ts": "2026-05-07T21:41:02+00:00", "type": "infrastructure", "status": "ok", "note": "Extracted crates/vg_csg into public standalone repo GameCult/vg-csg using gh. Standalone crate includes source, tests, examples, CSG docs, README, MIT license, and Cargo.lock; cargo test and clippy passed before publication."}
|
||||
{"ts": "2026-05-07T21:53:22+00:00", "type": "infrastructure", "status": "revised", "note": "Rewrote GameCult/vg-csg public history from a filtered VibeGeometry CSG extraction instead of the first synthetic import. History now preserves crate, CSG docs, native bridge, and benchmark harness commits, with one standalone packaging commit on top; pushed with --force-with-lease."}
|
||||
{"ts": "2026-05-07T21:58:31+00:00", "type": "infrastructure", "status": "ok", "note": "Replaced embedded crates/vg_csg source in VibeGeometry with a git submodule at the same path pointing to https://github.com/GameCult/vg-csg commit 1adeadc. Workspace cargo test and clippy pass with the submodule."}
|
||||
{"type":"rebuild","status":"accepted","note":"VibeGeometry live tree was reduced to the vg-csg Rust work surface. Historical scene-generation research surfaces were removed from live authority; history remains in git."}
|
||||
|
|
|
|||
138
state/map.yaml
|
|
@ -1,121 +1,49 @@
|
|||
experiment:
|
||||
name: VibeGeometry
|
||||
status: incubation
|
||||
phase: phase_0_scaffold
|
||||
status: runtime_csg_shell
|
||||
phase: vg_csg_focus
|
||||
premise: >
|
||||
Build a durable research and implementation home for agent-authored
|
||||
Blender 5.0+ scenes that use Python to construct complex geometry-node
|
||||
graphs for procedural geometry matching reference images and instructions.
|
||||
VibeGeometry is now the thin repository shell around the `vg-csg` Rust
|
||||
work surface. Historical scene-generation research machinery has been
|
||||
removed from the live tree.
|
||||
|
||||
mission:
|
||||
primary: >
|
||||
Enable Codex and other agents to generate inspectable, reusable Blender
|
||||
geometry-node graphs that produce specific visual results from reference
|
||||
images, text instructions, and iterative render review.
|
||||
near_term: >
|
||||
Define the scene brief, node graph manifest, Python/Geometry Script builder,
|
||||
Rust/Bevy CSG assembler, render runner, and review loop before building a
|
||||
large library of graph idioms.
|
||||
Build a fast runtime CSG machine capable of lowering domain/tree intent into
|
||||
an LOD-aware CSG hierarchy and emitting triangle/collider output for a
|
||||
camera or frustum query in milliseconds.
|
||||
work_surface: crates/vg_csg
|
||||
|
||||
hard_boundaries:
|
||||
- Blender 5.0+ is the target runtime unless a task explicitly chooses another version.
|
||||
- Geometry nodes are the primary procedural modeling surface.
|
||||
- Python scripts should construct and orchestrate node graphs, not hide the design in uninspectable imperative mesh code.
|
||||
- Python is the authoring and orchestration layer; Geometry Script is the inspectable node-graph emission surface, not the whole toolchain.
|
||||
- Visual targets must preserve reference-image evidence and explicit instructions.
|
||||
- A graph that runs is not accepted until rendered output is inspected against the target.
|
||||
- Node groups must be named, parameterized, and legible enough for future agents to modify.
|
||||
- Attribute creation, transformation, and consumption must be explicit.
|
||||
- Prefer established Blender node idioms and current API guidance over bespoke graph inventions.
|
||||
- Keep failed experiments only when they change future belief.
|
||||
- Do not let proxy checks replace visual review.
|
||||
- Where VibeGeometry exposes behavior matching public RealtimeCSG/demo seams, tests must maintain exact observable parity rather than approximate plausibility.
|
||||
- Do not rebuild the historical scene-generation pipeline in this repo.
|
||||
- Put implementation in `crates/vg_csg` unless a task proves a root-level
|
||||
surface is necessary.
|
||||
- Preserve exact parity only for public RealtimeCSG-style behavior the crate
|
||||
explicitly claims to mirror.
|
||||
- Runtime evidence must include selected-cut, dirty rebuild, emitted geometry,
|
||||
and timing data where performance claims matter.
|
||||
|
||||
live_architecture:
|
||||
pipeline:
|
||||
- Ingest reference images and instructions into a scene brief.
|
||||
- Extract target visual traits: silhouette, repetition, distribution, material response, surface breakup, camera, lighting, palette, and scale cues.
|
||||
- Define the source coordinate contract: basis axes, local up/down, attachment surfaces, and frame transitions.
|
||||
- Plan a geometry strategy in plain language before scripting.
|
||||
- Emit a node graph manifest describing groups, sockets, attributes, fields, modifiers, materials, and exposed controls.
|
||||
- Generate Blender Python that builds the scene and node graphs from the manifest.
|
||||
- Run Blender headlessly or interactively to produce `.blend` files, overview renders, and domain-facing inspection renders.
|
||||
- Review renders against the visual target and record accepted, rejected, or revised hypotheses.
|
||||
authoring_stack:
|
||||
- Python structures intent, repetition, data tables, variants, cleanup, validation, documentation, and scene/render orchestration.
|
||||
- `vibegeometry/` provides reusable Python helpers for coordinate contracts, deterministic fields, batched geometry, Blender object/material/camera setup, and verifier checks.
|
||||
- Geometry Script emits inspectable reusable Geometry Nodes groups.
|
||||
- bpy handles Blender objects, modifiers, materials, cameras, lights, files, renders, and evaluated-geometry checks.
|
||||
- Rust/Bevy-side CSG tooling lives in the `crates/vg_csg` submodule, which points at `https://github.com/GameCult/vg-csg`, and provides an interactive brush-and-boolean assembler path for fast level geometry, starting with box CSG and habitat-specific additive primitives.
|
||||
- `vg_grammar` provides the authoring grammar above CSG: procedural rules emit semantic solid/void claims, then `vg_csg` adjudicates the space.
|
||||
- External Python tools inspect source blends, generate converter drafts, compare outputs, and maintain corpus evidence.
|
||||
persistence:
|
||||
- `state/map.yaml` stores canonical mission, boundaries, architecture, artifacts, and next action.
|
||||
- `state/scratch.md` stores one bounded active thought surface.
|
||||
- `state/evidence.jsonl` stores distilled belief-changing records.
|
||||
- `notes/fresh-workspace-handoff.md` stores compact re-entry instructions.
|
||||
- `notes/vibegeometry-current-system-map.md` explains the live repo machinery.
|
||||
- `notes/vibegeometry-implementation-plan.md` stores the near-term build sequence.
|
||||
|
||||
active_subgoals:
|
||||
- Keep the pipeline legible from visual target to graph strategy to Python builder to render review.
|
||||
- Build a graded Geometry Nodes translation corpus before hardening schemas or builder abstractions.
|
||||
- Define artifact schemas after enough translated examples expose the real contract pressure.
|
||||
- Build a minimal Blender runner only after the scene brief and graph manifest contracts are clear.
|
||||
- Collect Blender 5.0+ geometry-node API and best-practice evidence before hardening graph builders.
|
||||
- Preserve one-hypothesis iteration when visual results miss the target.
|
||||
|
||||
current_artifacts:
|
||||
canonical_docs:
|
||||
- notes/architecture-rationale.md
|
||||
- notes/fresh-workspace-handoff.md
|
||||
- notes/vibegeometry-current-system-map.md
|
||||
- notes/vibegeometry-implementation-plan.md
|
||||
- docs/architecture/pipeline-overview.md
|
||||
- docs/research/procedural-doctrine.md
|
||||
- docs/research/realtime-csg-doctrine.md
|
||||
- docs/research/dream-machine-grammar.md
|
||||
- docs/research/geometry-script-fork-notes.md
|
||||
- docs/research/translations/ircss-french-houses-system-map.md
|
||||
- docs/research/translations/ircss-french-houses-node-atlas.md
|
||||
- docs/research/translations/ircss-french-houses-node-mechanisms.md
|
||||
- docs/research/builds/aetheria-bloom-habitat.md
|
||||
- docs/research/builds/lucent-tether-habitat.md
|
||||
- docs/research/builds/dream-machine-level.md
|
||||
- docs/research/builds/industrial-maintenance-loop.md
|
||||
- docs/research/realtime-csg-bevy-assembler.md
|
||||
- crates/vg_csg/ submodule -> https://github.com/GameCult/vg-csg
|
||||
- crates/vg_grammar/
|
||||
- vibegeometry/
|
||||
tools:
|
||||
- tools/vibegeometry_state.py
|
||||
- tools/vibegeometry_prepare_compaction.py
|
||||
- tools/inspect_blend_node_groups.py
|
||||
- tools/verify_cartesian_translations.py
|
||||
- tools/verify_piechart_translations.py
|
||||
- tools/verify_fieldvalue_translations.py
|
||||
- tools/geometry_script_loader.py
|
||||
- tools/smoke_nodes_to_script_converter.py
|
||||
- tools/verify_aetheria_bloom_habitat.py
|
||||
- tools/verify_lucent_tether_habitat.py
|
||||
pending_contracts:
|
||||
- schemas/scene-brief.schema.json
|
||||
- schemas/node-graph-manifest.schema.json
|
||||
- schemas/render-review.schema.json
|
||||
|
||||
data_flow:
|
||||
- Rehydrate from map, handoff, system map, implementation plan, and `npm run state:status`.
|
||||
- Work one bounded pipeline organ.
|
||||
- Verify the seam that matters, including coordinate-frame objects, density checks, and inspection cameras when visual claims depend on them.
|
||||
- Add distilled evidence only when future belief changes.
|
||||
- Commit completed work.
|
||||
- If state grows faster than understanding, cut it back.
|
||||
- Domain or tree intent enters `vg-csg`.
|
||||
- The runtime chooses an LOD-aware selected cut for the requested view,
|
||||
budget, dirty state, or output scope.
|
||||
- The selected cut lowers to CSG/brush operations.
|
||||
- The crate emits triangle and collider output plus receipt/timing evidence.
|
||||
root:
|
||||
- Minimal Cargo workspace.
|
||||
- `crates/vg_csg` git submodule.
|
||||
- Minimal README/AGENTS/state pointers.
|
||||
|
||||
current_status:
|
||||
summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; spin-gravity habitats need ground planes orthogonal to their tether/rotation axis; utility petals, radiators, and solar arrays belong outside pressure glass and may need Citadel-scale presence; large habitat parts need visible load paths across coordinate frames; organic city maps need street fields, irregular parcels, parks, and land-use gradients rather than grids; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, shader behavior, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. The Rust crate `vg_csg` now has a RealtimeCSG-shaped API skeleton plus a convex-brush stepping-stone kernel: CSG operation ordinals, branch vocabulary, tree/branch/brush handles, child and operation mutation, exact parity fixtures, ordered assembler compilation, AABB/oriented-box subtraction/intersection via convex plane splitting, crossing polygon category buckets, primitive/solid/tree bounds, additive cylinder/dome/floret primitives, JSONL performance fixtures with stable, dirty, and incremental dirty modes, mesh-array output, stable-generation output caching, dirty prefix checkpoints for general convex builders, conservative mesh reuse for rejected dirty suffixes, explicit rebuild-strategy report fields, tests, clippy, and examples. The `dream_machine_level` and `industrial_maintenance_loop` examples prove the Rust side can emit named, inspectable scene fixtures as OBJ/MTL/SVG/PNG artifacts; the maintenance loop adds local tangent/radial/up authoring frames for curved habitat rooms, with manifold, staging, gallery, and service-artery subdomains compiled into world brushes. The `vg_grammar` crate is the Dream Machine authoring layer: semantic solid/void claims with tags compile into flat `vg_csg` brushes or generated CSG tree intent; it supports local frames, oriented claims, deterministic seeded variation, rule composition, and generated gallery chains. Public RealtimeCSG demo/source research points the target away from fragment carving and toward faster polygon classification through boolean trees. A standalone RealtimeCSG C++ P/Invoke bridge now mirrors the managed wrapper surface, verifies a native health cube, extracts generated mesh descriptions, and appends direct native timing records. Current perf pass: `vg_csg` has a fast axis-aligned direct path, cached compiled brush geometry, owned convex splitting, convex containment shortcuts, stable-tree output caching, valid-prefix incremental dirty caching for general convex edits, and report flags for `reused_mesh`, `used_checkpoint`, `checkpoint_rebuild_from`, and `direct_dirty_rebuild`; box candidate edits use the direct builder after an honest dirty baseline showed checkpoint replay is slower without real mesh patching. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes.
|
||||
next_action: Build the first Bevy inspection shell for Rust-side CSG artifacts: load or generate the Dream Machine level mesh, show orbit camera/material groups, and keep dirty edit hooks visible so later mesh-patching work is judged against an actual interactive object rather than isolated timing fixtures.
|
||||
summary: >
|
||||
Root-level scene-generation surfaces have been deleted. `vg-csg` is the live
|
||||
work surface for future domain-tree, LOD CSG, selected-cut, and triangle
|
||||
emission work.
|
||||
next_action: >
|
||||
Continue inside `crates/vg_csg`: define the first LOD/query contract for
|
||||
selected CSG output rather than adding new root scaffolding.
|
||||
|
||||
remotes:
|
||||
vibegeometry: https://github.com/GameCult/VibeGeometry
|
||||
vg_csg: https://github.com/GameCult/vg-csg
|
||||
geometry_script_fork: https://github.com/GameCult/geometry-script
|
||||
geometry_script_upstream_pr: https://github.com/carson-katri/geometry-script/pull/69
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
# Scratch
|
||||
|
||||
## Current Subgoal
|
||||
Current live cut: `vg-csg` owns the work surface.
|
||||
|
||||
No active scratch subgoal.
|
||||
|
||||
## Notes
|
||||
|
||||
Use this file for temporary reasoning during one bounded task. Fold durable
|
||||
lessons into `state/map.yaml`, `state/evidence.jsonl`, or the relevant note,
|
||||
then clear this surface.
|
||||
Do not rehydrate old scene-generation plans from memory. The live tree has been
|
||||
intentionally reduced to a Rust CSG workspace shell.
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
"""Load the repo-local Geometry Script clone for Blender scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GEOMETRY_SCRIPT_DIR = ROOT / "external" / "geometry-script"
|
||||
GEOMETRY_SCRIPT_INIT = GEOMETRY_SCRIPT_DIR / "__init__.py"
|
||||
|
||||
|
||||
def load_repo_geometry_script():
|
||||
"""Import the repo-local Geometry Script clone as ``geometry_script``."""
|
||||
if not GEOMETRY_SCRIPT_INIT.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Geometry Script clone not found: {GEOMETRY_SCRIPT_INIT}. "
|
||||
"Run tools/setup_geometry_script_clone.ps1 from the repo root."
|
||||
)
|
||||
|
||||
existing = sys.modules.get("geometry_script")
|
||||
if existing is not None and getattr(existing, "__file__", None) == str(GEOMETRY_SCRIPT_INIT):
|
||||
return existing
|
||||
|
||||
for name in list(sys.modules):
|
||||
if name == "geometry_script" or name.startswith("geometry_script."):
|
||||
del sys.modules[name]
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"geometry_script",
|
||||
GEOMETRY_SCRIPT_INIT,
|
||||
submodule_search_locations=[str(GEOMETRY_SCRIPT_DIR)],
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load Geometry Script from {GEOMETRY_SCRIPT_INIT}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["geometry_script"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
"""Inspect Geometry Nodes node groups in the currently opened Blender file.
|
||||
|
||||
Run with Blender:
|
||||
|
||||
blender --background source.blend --python tools/inspect_blend_node_groups.py -- --json out.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def _jsonable(value: Any) -> Any:
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if hasattr(value, "to_tuple"):
|
||||
return list(value.to_tuple())
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [_jsonable(item) for item in value]
|
||||
return repr(value)
|
||||
|
||||
|
||||
def _socket_summary(socket: Any) -> dict[str, Any]:
|
||||
summary = {
|
||||
"name": getattr(socket, "name", ""),
|
||||
"identifier": getattr(socket, "identifier", ""),
|
||||
"type": getattr(socket, "type", ""),
|
||||
"bl_socket_idname": getattr(socket, "bl_socket_idname", ""),
|
||||
"enabled": getattr(socket, "enabled", None),
|
||||
"is_linked": getattr(socket, "is_linked", None),
|
||||
"is_multi_input": getattr(socket, "is_multi_input", None),
|
||||
}
|
||||
if hasattr(socket, "default_value"):
|
||||
try:
|
||||
summary["default_value"] = _jsonable(socket.default_value)
|
||||
except Exception as exc: # pragma: no cover - Blender API guardrail
|
||||
summary["default_value_error"] = str(exc)
|
||||
return summary
|
||||
|
||||
|
||||
def _interface_item_summary(item: Any) -> dict[str, Any]:
|
||||
summary = {
|
||||
"name": getattr(item, "name", ""),
|
||||
"item_type": getattr(item, "item_type", ""),
|
||||
"in_out": getattr(item, "in_out", ""),
|
||||
"socket_type": getattr(item, "socket_type", ""),
|
||||
"identifier": getattr(item, "identifier", ""),
|
||||
"description": getattr(item, "description", ""),
|
||||
}
|
||||
for attr in ("default_value", "min_value", "max_value", "hide_value", "hide_in_modifier"):
|
||||
if hasattr(item, attr):
|
||||
try:
|
||||
summary[attr] = _jsonable(getattr(item, attr))
|
||||
except Exception as exc: # pragma: no cover - Blender API guardrail
|
||||
summary[f"{attr}_error"] = str(exc)
|
||||
return summary
|
||||
|
||||
|
||||
def _node_summary(node: Any) -> dict[str, Any]:
|
||||
ignored_props = {
|
||||
"rna_type",
|
||||
"name",
|
||||
"label",
|
||||
"location",
|
||||
"width",
|
||||
"height",
|
||||
"dimensions",
|
||||
"inputs",
|
||||
"outputs",
|
||||
"internal_links",
|
||||
"parent",
|
||||
"select",
|
||||
"show_options",
|
||||
"show_preview",
|
||||
"show_texture",
|
||||
"hide",
|
||||
"mute",
|
||||
"use_custom_color",
|
||||
"warning_propagation",
|
||||
}
|
||||
properties: dict[str, Any] = {}
|
||||
for prop in node.bl_rna.properties:
|
||||
if prop.identifier in ignored_props or prop.is_readonly:
|
||||
continue
|
||||
if prop.identifier.startswith("bl_"):
|
||||
continue
|
||||
try:
|
||||
value = getattr(node, prop.identifier)
|
||||
except Exception:
|
||||
continue
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
properties[prop.identifier] = value
|
||||
summary = {
|
||||
"name": node.name,
|
||||
"label": node.label,
|
||||
"bl_idname": node.bl_idname,
|
||||
"type": node.type,
|
||||
"properties": properties,
|
||||
"location": _jsonable(node.location),
|
||||
"inputs": [_socket_summary(socket) for socket in node.inputs],
|
||||
"outputs": [_socket_summary(socket) for socket in node.outputs],
|
||||
}
|
||||
node_tree = getattr(node, "node_tree", None)
|
||||
if node_tree is not None:
|
||||
summary["node_tree"] = getattr(node_tree, "name", "")
|
||||
return summary
|
||||
|
||||
|
||||
def _link_summary(link: Any) -> dict[str, str]:
|
||||
return {
|
||||
"from_node": link.from_node.name,
|
||||
"from_socket": link.from_socket.name,
|
||||
"from_socket_identifier": getattr(link.from_socket, "identifier", ""),
|
||||
"to_node": link.to_node.name,
|
||||
"to_socket": link.to_socket.name,
|
||||
"to_socket_identifier": getattr(link.to_socket, "identifier", ""),
|
||||
}
|
||||
|
||||
|
||||
def _modifier_users() -> list[dict[str, str]]:
|
||||
users: list[dict[str, str]] = []
|
||||
for obj in bpy.data.objects:
|
||||
for modifier in obj.modifiers:
|
||||
if modifier.type == "NODES" and getattr(modifier, "node_group", None):
|
||||
users.append(
|
||||
{
|
||||
"object": obj.name,
|
||||
"modifier": modifier.name,
|
||||
"node_group": modifier.node_group.name,
|
||||
}
|
||||
)
|
||||
return users
|
||||
|
||||
|
||||
def inspect_file() -> dict[str, Any]:
|
||||
node_groups = []
|
||||
for group in bpy.data.node_groups:
|
||||
if group.bl_idname != "GeometryNodeTree":
|
||||
continue
|
||||
interface_items = []
|
||||
if hasattr(group, "interface") and hasattr(group.interface, "items_tree"):
|
||||
interface_items = [_interface_item_summary(item) for item in group.interface.items_tree]
|
||||
node_groups.append(
|
||||
{
|
||||
"name": group.name,
|
||||
"bl_idname": group.bl_idname,
|
||||
"is_modifier": getattr(group, "is_modifier", None),
|
||||
"is_tool": getattr(group, "is_tool", None),
|
||||
"interface": interface_items,
|
||||
"node_count": len(group.nodes),
|
||||
"link_count": len(group.links),
|
||||
"nodes": [_node_summary(node) for node in group.nodes],
|
||||
"links": [_link_summary(link) for link in group.links],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"blend_file": bpy.data.filepath,
|
||||
"blender_version": bpy.app.version_string,
|
||||
"node_groups": sorted(node_groups, key=lambda item: item["name"].lower()),
|
||||
"modifier_users": _modifier_users(),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--json", required=True, help="Path for the inspection JSON output.")
|
||||
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [])
|
||||
|
||||
data = inspect_file()
|
||||
with open(args.json, "w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, indent=2)
|
||||
handle.write("\n")
|
||||
print(f"Inspected {len(data['node_groups'])} geometry node groups -> {args.json}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,593 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
const int WarmupIterations = 8;
|
||||
const int MeasureIterations = 64;
|
||||
var debugNative = args.Any(arg => arg == "--debug-native");
|
||||
var healthOnly = args.Any(arg => arg == "--health");
|
||||
|
||||
var outputArg = args.FirstOrDefault(arg => arg != "--debug-native" && arg != "--health");
|
||||
var output = outputArg is not null
|
||||
? outputArg
|
||||
: Path.GetFullPath(Path.Combine("experiments", "generated", "realtimecsg-cpp-perf-latest.jsonl"));
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
|
||||
Native.RegisterNoopUnityMethods();
|
||||
if (healthOnly)
|
||||
{
|
||||
using var healthWriter = new StreamWriter(output, false);
|
||||
healthWriter.WriteLine(RunHealthProbe());
|
||||
return;
|
||||
}
|
||||
|
||||
var cases = new PerfCase[]
|
||||
{
|
||||
new("single_center_cut", 2, SingleCenterCut),
|
||||
new("room_grid_8x8_doors", 192, RoomGrid8x8Doors),
|
||||
new("rotated_cut_stack_64", 65, RotatedCutStack64),
|
||||
new("common_box_chain_64", 64, CommonBoxChain64),
|
||||
new("distant_cutters_512", 513, DistantCutters512),
|
||||
};
|
||||
|
||||
using var writer = new StreamWriter(output, false);
|
||||
foreach (var perfCase in cases)
|
||||
{
|
||||
writer.WriteLine(RunCase(perfCase));
|
||||
}
|
||||
|
||||
string RunCase(PerfCase perfCase)
|
||||
{
|
||||
var timings = new List<long>(MeasureIterations);
|
||||
var triangles = 0;
|
||||
var vertices = 0;
|
||||
var meshDescriptions = 0;
|
||||
|
||||
Native.ClearAllNodes();
|
||||
var tree = perfCase.Build();
|
||||
if (tree == 0)
|
||||
throw new InvalidOperationException($"Failed to build native RealtimeCSG tree for {perfCase.Name}");
|
||||
|
||||
Native.RebuildAll();
|
||||
if (debugNative)
|
||||
DumpTree(perfCase.Name, tree);
|
||||
if (Native.GetBrushCount() != perfCase.Brushes)
|
||||
throw new InvalidOperationException($"Native brush count mismatch for {perfCase.Name}: expected {perfCase.Brushes}, got {Native.GetBrushCount()}, nodes={Native.GetNodeCount()}, trees={Native.GetTreeCount()}, brushMeshes={Native.GetBrushMeshCount()}");
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
MeasureOnce(tree, out triangles, out vertices, out meshDescriptions);
|
||||
if (meshDescriptions == 0)
|
||||
{
|
||||
DumpTree(perfCase.Name, tree);
|
||||
throw new InvalidOperationException($"Native generated no mesh descriptions for {perfCase.Name}: nodes={Native.GetNodeCount()}, brushes={Native.GetBrushCount()}, trees={Native.GetTreeCount()}, brushMeshes={Native.GetBrushMeshCount()}, treeBrushes={Native.GetNumberOfBrushesInTree(tree)}, treeChildren={Native.GetChildNodeCount(tree)}, treeDirty={Native.IsNodeDirty(tree)}");
|
||||
}
|
||||
|
||||
for (var i = 0; i < MeasureIterations; i++)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
MeasureOnce(tree, out triangles, out vertices, out meshDescriptions);
|
||||
stopwatch.Stop();
|
||||
timings.Add(stopwatch.ElapsedTicks);
|
||||
}
|
||||
Native.ClearAllNodes();
|
||||
|
||||
timings.Sort();
|
||||
var mean = timings.Sum() / timings.Count;
|
||||
return
|
||||
$"{{\"kernel\":\"realtime_csg_cpp\",\"scenario\":\"{perfCase.Name}\",\"brushes\":{perfCase.Brushes},\"iterations\":{MeasureIterations},\"warmup_iterations\":{WarmupIterations},\"mean_ns\":{TicksToNs(mean)},\"min_ns\":{TicksToNs(timings[0])},\"p50_ns\":{TicksToNs(timings[(timings.Count - 1) * 50 / 100])},\"p95_ns\":{TicksToNs(timings[(timings.Count - 1) * 95 / 100])},\"max_ns\":{TicksToNs(timings[^1])},\"triangles\":{triangles},\"vertices\":{vertices},\"mesh_descriptions\":{meshDescriptions}}}";
|
||||
}
|
||||
|
||||
static string RunHealthProbe()
|
||||
{
|
||||
Native.ClearAllNodes();
|
||||
var tree = TreeFrom(() => new[]
|
||||
{
|
||||
Brush("native_health_cube", Vec3.Zero, Vec3.Splat(4), Mat4.Identity, Operation.Additive),
|
||||
});
|
||||
Native.RebuildAll();
|
||||
var updateOk = Native.UpdateAllTreeMeshes();
|
||||
var child = Native.GetChildNodeAtIndex(tree, 0);
|
||||
var bounds = new Aabb();
|
||||
var boundsOk = Native.GetBrushBounds(child, ref bounds);
|
||||
var outlineOk = Native.GetBrushOutlineSizes(child, out var outlineVertices, out var visibleOuter, out var visibleInner, out var invisibleOuter, out var invisibleInner, out var invalid);
|
||||
var rayStart = new Vec3(-8, 0, 0);
|
||||
var rayEnd = new Vec3(8, 0, 0);
|
||||
var matrix = Mat4.Identity;
|
||||
var rayHits = Native.RayCastIntoTreeMultiCount(tree, ref rayStart, ref rayEnd, ref matrix, 0, false, IntPtr.Zero, 0);
|
||||
var meshTypes = MeshQuery.RenderOnlyTypes;
|
||||
var meshTypesHandle = GCHandle.Alloc(meshTypes, GCHandleType.Pinned);
|
||||
var generated = Native.GenerateMeshDescriptions(tree, meshTypes.Length, meshTypesHandle.AddrOfPinnedObject(), VertexChannelFlags.All, out var meshDescriptions);
|
||||
meshTypesHandle.Free();
|
||||
var heartbeat = updateOk && Native.GetTreeEnabled(tree) && Native.GetChildNodeCount(tree) == 1 && boundsOk && outlineOk && rayHits > 0;
|
||||
var meshExtractionReady = generated && meshDescriptions > 0;
|
||||
var json =
|
||||
$"{{\"kernel\":\"realtime_csg_cpp\",\"scenario\":\"native_health_cube\",\"heartbeat\":{JsonBool(heartbeat)},\"mesh_extraction_ready\":{JsonBool(meshExtractionReady)},\"update_ok\":{JsonBool(updateOk)},\"tree\":{tree},\"tree_enabled\":{JsonBool(Native.GetTreeEnabled(tree))},\"nodes\":{Native.GetNodeCount()},\"brushes\":{Native.GetBrushCount()},\"trees\":{Native.GetTreeCount()},\"brush_meshes\":{Native.GetBrushMeshCount()},\"tree_children\":{Native.GetChildNodeCount(tree)},\"tree_brushes\":{Native.GetNumberOfBrushesInTree(tree)},\"tree_dirty\":{JsonBool(Native.IsNodeDirty(tree))},\"child_type\":{Native.GetTypeOfNode(child)},\"child_tree\":{Native.GetTreeOfNode(child)},\"child_mesh\":{Native.GetBrushMeshID(child)},\"child_operation\":{Native.GetBrushOperationType(child)},\"bounds_ok\":{JsonBool(boundsOk)},\"bounds_min\":[{bounds.MinX},{bounds.MinY},{bounds.MinZ}],\"bounds_max\":[{bounds.MaxX},{bounds.MaxY},{bounds.MaxZ}],\"outline_ok\":{JsonBool(outlineOk)},\"outline_vertices\":{outlineVertices},\"visible_outer_lines\":{visibleOuter},\"visible_inner_lines\":{visibleInner},\"invisible_outer_lines\":{invisibleOuter},\"invisible_inner_lines\":{invisibleInner},\"invalid_lines\":{invalid},\"raycast_hits\":{rayHits},\"mesh_descriptions_generated\":{JsonBool(generated)},\"mesh_descriptions\":{meshDescriptions}}}";
|
||||
Native.ClearAllNodes();
|
||||
return json;
|
||||
}
|
||||
|
||||
static void DumpTree(string name, int tree)
|
||||
{
|
||||
Native.RebuildAll();
|
||||
var updated = Native.UpdateAllTreeMeshes();
|
||||
Console.Error.WriteLine($"native debug {name}: updated={updated} tree={tree} type={Native.GetTypeOfNode(tree)} enabled={Native.GetTreeEnabled(tree)} children={Native.GetChildNodeCount(tree)} dirty={Native.IsNodeDirty(tree)} nodes={Native.GetNodeCount()} brushes={Native.GetBrushCount()} trees={Native.GetTreeCount()} brushMeshes={Native.GetBrushMeshCount()}");
|
||||
for (var i = 0; i < Native.GetChildNodeCount(tree); i++)
|
||||
{
|
||||
var child = Native.GetChildNodeAtIndex(tree, i);
|
||||
var bounds = new Aabb();
|
||||
var boundsOk = Native.GetBrushBounds(child, ref bounds);
|
||||
var outlineOk = Native.GetBrushOutlineSizes(child, out var outlineVertices, out var visibleOuter, out var visibleInner, out var invisibleOuter, out var invisibleInner, out var invalid);
|
||||
Console.Error.WriteLine($" child[{i}] id={child} type={Native.GetTypeOfNode(child)} tree={Native.GetTreeOfNode(child)} mesh={Native.GetBrushMeshID(child)} op={Native.GetBrushOperationType(child)} dirty={Native.IsNodeDirty(child)} boundsOk={boundsOk} bounds=({bounds.MinX},{bounds.MinY},{bounds.MinZ})..({bounds.MaxX},{bounds.MaxY},{bounds.MaxZ}) outlineOk={outlineOk} outlineVertices={outlineVertices} visibleOuter={visibleOuter} visibleInner={visibleInner} invisibleOuter={invisibleOuter} invisibleInner={invisibleInner} invalid={invalid}");
|
||||
}
|
||||
var rayStart = new Vec3(-8, 0, 0);
|
||||
var rayEnd = new Vec3(8, 0, 0);
|
||||
var matrix = Mat4.Identity;
|
||||
var rayHits = Native.RayCastIntoTreeMultiCount(tree, ref rayStart, ref rayEnd, ref matrix, 0, false, IntPtr.Zero, 0);
|
||||
Console.Error.WriteLine($" raycast hits={rayHits}");
|
||||
ProbeGeneratedMesh(tree);
|
||||
}
|
||||
|
||||
static void ProbeGeneratedMesh(int tree)
|
||||
{
|
||||
var indices = new int[8192];
|
||||
var positions = new Vec3[8192];
|
||||
var indexHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
|
||||
var positionHandle = GCHandle.Alloc(positions, GCHandleType.Pinned);
|
||||
var ok = Native.GetGeneratedMesh(tree, 0, 0, indices.Length, indexHandle.AddrOfPinnedObject(), positions.Length, positionHandle.AddrOfPinnedObject(), IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out var center, out var size);
|
||||
positionHandle.Free();
|
||||
indexHandle.Free();
|
||||
Console.Error.WriteLine($" direct mesh probe ok={ok} boundsCenter=({center.X},{center.Y},{center.Z}) boundsSize=({size.X},{size.Y},{size.Z}) firstIndex={indices[0]} firstPos=({positions[0].X},{positions[0].Y},{positions[0].Z})");
|
||||
}
|
||||
|
||||
static long TicksToNs(long ticks) => (long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency));
|
||||
static string JsonBool(bool value) => value ? "true" : "false";
|
||||
|
||||
static void MeasureOnce(int tree, out int triangles, out int vertices, out int meshDescriptions)
|
||||
{
|
||||
Native.UpdateAllTreeMeshes();
|
||||
|
||||
var meshTypes = MeshQuery.RenderOnlyTypes;
|
||||
var meshTypesHandle = GCHandle.Alloc(meshTypes, GCHandleType.Pinned);
|
||||
var generated = Native.GenerateMeshDescriptions(tree, meshTypes.Length, meshTypesHandle.AddrOfPinnedObject(), VertexChannelFlags.All, out meshDescriptions);
|
||||
if (!generated || meshDescriptions == 0)
|
||||
{
|
||||
meshTypesHandle.Free();
|
||||
triangles = 0;
|
||||
vertices = 0;
|
||||
return;
|
||||
}
|
||||
meshTypesHandle.Free();
|
||||
|
||||
var descriptions = new GeneratedMeshDescription[meshDescriptions];
|
||||
var descriptionsHandle = GCHandle.Alloc(descriptions, GCHandleType.Pinned);
|
||||
var ok = Native.GetMeshDescriptions(tree, meshDescriptions, descriptionsHandle.AddrOfPinnedObject());
|
||||
descriptionsHandle.Free();
|
||||
if (!ok)
|
||||
{
|
||||
triangles = 0;
|
||||
vertices = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
triangles = 0;
|
||||
vertices = 0;
|
||||
foreach (var description in descriptions)
|
||||
{
|
||||
if (description.VertexCount <= 0 || description.IndexCount <= 0)
|
||||
continue;
|
||||
|
||||
var indices = new int[description.IndexCount];
|
||||
var positions = new Vec3[description.VertexCount];
|
||||
var indexHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
|
||||
var positionHandle = GCHandle.Alloc(positions, GCHandleType.Pinned);
|
||||
ok = Native.GetGeneratedMesh(
|
||||
tree,
|
||||
description.MeshQueryIndex,
|
||||
description.SubMeshQueryIndex,
|
||||
description.IndexCount,
|
||||
indexHandle.AddrOfPinnedObject(),
|
||||
description.VertexCount,
|
||||
positionHandle.AddrOfPinnedObject(),
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
out _,
|
||||
out _);
|
||||
positionHandle.Free();
|
||||
indexHandle.Free();
|
||||
if (ok)
|
||||
{
|
||||
triangles += description.IndexCount / 3;
|
||||
vertices += description.VertexCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int SingleCenterCut()
|
||||
{
|
||||
return TreeFrom(() => new[]
|
||||
{
|
||||
Brush("source", Vec3.Zero, Vec3.Splat(4), Mat4.Identity, Operation.Additive),
|
||||
Brush("void", Vec3.Zero, Vec3.Splat(2), Mat4.Identity, Operation.Subtractive),
|
||||
});
|
||||
}
|
||||
|
||||
static int RoomGrid8x8Doors()
|
||||
{
|
||||
var nodes = new List<int>();
|
||||
const float cell = 6.0f;
|
||||
for (var y = 0; y < 8; y++)
|
||||
for (var x = 0; x < 8; x++)
|
||||
{
|
||||
var center = new Vec3((x - 3.5f) * cell, (y - 3.5f) * cell, 0);
|
||||
nodes.Add(Brush("floor", center + new Vec3(0, 0, -0.1f), new Vec3(5.6f, 5.6f, 0.2f), Mat4.Identity, Operation.Additive));
|
||||
nodes.Add(Brush("north_wall", center + new Vec3(0, 2.8f, 1.5f), new Vec3(5.6f, 0.25f, 3.0f), Mat4.Identity, Operation.Additive));
|
||||
nodes.Add(Brush("door", center + new Vec3(0, 2.8f, 1.0f), new Vec3(1.2f, 0.5f, 2.0f), Mat4.Identity, Operation.Subtractive));
|
||||
}
|
||||
return Tree(nodes.ToArray());
|
||||
}
|
||||
|
||||
static int RotatedCutStack64()
|
||||
{
|
||||
var nodes = new List<int> { Brush("slab", Vec3.Zero, new Vec3(32, 32, 4), Mat4.Identity, Operation.Additive) };
|
||||
for (var index = 0; index < 64; index++)
|
||||
{
|
||||
var angle = index * 0.173f;
|
||||
var radius = 11.0f + (index % 7) * 0.35f;
|
||||
var center = new Vec3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0);
|
||||
nodes.Add(Brush("rotated_void", center, new Vec3(1.0f + (index % 3) * 0.3f, 8.0f, 5.0f), Mat4.RotationZ(angle), Operation.Subtractive));
|
||||
}
|
||||
return Tree(nodes.ToArray());
|
||||
}
|
||||
|
||||
static int CommonBoxChain64()
|
||||
{
|
||||
var nodes = new List<int> { Brush("source", Vec3.Zero, new Vec3(32, 16, 8), Mat4.Identity, Operation.Additive) };
|
||||
for (var index = 1; index < 64; index++)
|
||||
{
|
||||
var t = index / 63.0f;
|
||||
var center = new Vec3((t - 0.5f) * 6.0f, MathF.Sin(t * MathF.Tau) * 1.5f, 0);
|
||||
nodes.Add(Brush("common", center, new Vec3(30.0f - t * 8.0f, 14.0f, 7.0f), Mat4.RotationZ(t * 0.2f), Operation.Intersecting));
|
||||
}
|
||||
return Tree(nodes.ToArray());
|
||||
}
|
||||
|
||||
static int DistantCutters512()
|
||||
{
|
||||
var nodes = new List<int> { Brush("source", Vec3.Zero, Vec3.Splat(8), Mat4.Identity, Operation.Additive) };
|
||||
for (var index = 0; index < 512; index++)
|
||||
{
|
||||
var row = index / 32;
|
||||
var col = index % 32;
|
||||
nodes.Add(Brush("far_void", new Vec3(1000.0f + col * 4.0f, 1000.0f + row * 4.0f, 0), Vec3.Splat(1), Mat4.Identity, Operation.Subtractive));
|
||||
}
|
||||
return Tree(nodes.ToArray());
|
||||
}
|
||||
|
||||
static int Tree(params int[] children)
|
||||
{
|
||||
return TreeFrom(() => children);
|
||||
}
|
||||
|
||||
static int TreeFrom(Func<int[]> buildChildren)
|
||||
{
|
||||
var children = buildChildren();
|
||||
if (!Native.GenerateTree(0, out var tree))
|
||||
return 0;
|
||||
if (!Native.SetTreeEnabled(tree, true) || !Native.GetTreeEnabled(tree))
|
||||
throw new InvalidOperationException($"SetTreeEnabled failed for tree {tree}");
|
||||
if (!InsertChildNodes(tree, children))
|
||||
throw new InvalidOperationException($"InsertChildNodes failed for tree {tree} with {children.Length} children");
|
||||
foreach (var child in children)
|
||||
Native.SetDirty(child);
|
||||
Native.SetDirty(tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
static int Brush(string name, Vec3 center, Vec3 size, Mat4 rotation, Operation operation)
|
||||
{
|
||||
var mesh = Cube(size);
|
||||
var meshId = CreateBrushMesh(mesh, name.GetHashCode());
|
||||
if (meshId == 0 || !Native.GenerateBrush(name.GetHashCode(), out var brush))
|
||||
throw new InvalidOperationException($"Failed to create brush {name}: meshId={meshId}");
|
||||
if (!Native.IsBrushMeshIDValid(meshId))
|
||||
throw new InvalidOperationException($"Native rejected brush mesh {name}/{meshId}");
|
||||
var transform = rotation.WithTranslation(center);
|
||||
if (!Native.SetNodeLocalTransformation(brush, ref transform))
|
||||
throw new InvalidOperationException($"SetNodeLocalTransformation failed for brush {name}/{brush}");
|
||||
if (!Native.SetBrushMeshID(brush, meshId))
|
||||
throw new InvalidOperationException($"SetBrushMeshID failed for brush {name}/{brush}, mesh={meshId}");
|
||||
if (operation != Operation.Additive && !Native.SetBrushOperationType(brush, operation))
|
||||
throw new InvalidOperationException($"SetBrushOperationType failed for brush {name}/{brush}, op={operation}");
|
||||
if (Native.GetBrushMeshID(brush) != meshId)
|
||||
throw new InvalidOperationException($"Brush {name}/{brush} did not retain mesh id {meshId}");
|
||||
Native.SetDirty(brush);
|
||||
return brush;
|
||||
}
|
||||
|
||||
static bool InsertChildNodes(int node, int[] children)
|
||||
{
|
||||
var handle = GCHandle.Alloc(children, GCHandleType.Pinned);
|
||||
var ok = Native.InsertChildNodeRange(node, 0, children.Length, handle.AddrOfPinnedObject());
|
||||
handle.Free();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int CreateBrushMesh(BrushMesh mesh, int userId)
|
||||
{
|
||||
var vh = GCHandle.Alloc(mesh.Vertices, GCHandleType.Pinned);
|
||||
var eh = GCHandle.Alloc(mesh.HalfEdges, GCHandleType.Pinned);
|
||||
var ph = GCHandle.Alloc(mesh.Polygons, GCHandleType.Pinned);
|
||||
var id = Native.CreateBrushMesh(userId, mesh.Vertices.Length, vh.AddrOfPinnedObject(), mesh.HalfEdges.Length, eh.AddrOfPinnedObject(), mesh.Polygons.Length, ph.AddrOfPinnedObject());
|
||||
ph.Free();
|
||||
eh.Free();
|
||||
vh.Free();
|
||||
return id;
|
||||
}
|
||||
|
||||
static BrushMesh Cube(Vec3 size)
|
||||
{
|
||||
var min = size * -0.5f;
|
||||
var max = size * 0.5f;
|
||||
var layers = new SurfaceLayers { LayerUsage = LayerUsageFlags.RenderReceiveCastShadows | LayerUsageFlags.Collidable };
|
||||
var surfaces = new[]
|
||||
{
|
||||
new SurfaceDescription { UV0 = new UvMatrix(new Vec4(-1, 0, 0, -min.X), new Vec4(0, 1, 0, min.Y)) },
|
||||
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 1, 0, 0, min.X), new Vec4(0, 1, 0, min.Y)) },
|
||||
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 0, 0, 1, min.Z), new Vec4(0, 1, 0, min.Y)) },
|
||||
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 0, 0, -1, -min.Z), new Vec4(0, 1, 0, min.Y)) },
|
||||
new SurfaceDescription { UV0 = new UvMatrix(new Vec4(-1, 0, 0, -min.X), new Vec4(0, 0, -1, -min.Z)) },
|
||||
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 1, 0, 0, min.X), new Vec4(0, 0, -1, -min.Z)) },
|
||||
};
|
||||
return new BrushMesh(
|
||||
new[]
|
||||
{
|
||||
new Vec3(min.X, min.Y, min.Z), new Vec3(min.X, max.Y, min.Z),
|
||||
new Vec3(max.X, max.Y, min.Z), new Vec3(max.X, min.Y, min.Z),
|
||||
new Vec3(min.X, min.Y, max.Z), new Vec3(max.X, min.Y, max.Z),
|
||||
new Vec3(max.X, max.Y, max.Z), new Vec3(min.X, max.Y, max.Z),
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new HalfEdge(0,17), new HalfEdge(1,8), new HalfEdge(2,20), new HalfEdge(3,13),
|
||||
new HalfEdge(4,10), new HalfEdge(5,19), new HalfEdge(6,15), new HalfEdge(7,22),
|
||||
new HalfEdge(0,1), new HalfEdge(4,16), new HalfEdge(7,4), new HalfEdge(1,21),
|
||||
new HalfEdge(3,18), new HalfEdge(2,3), new HalfEdge(6,23), new HalfEdge(5,6),
|
||||
new HalfEdge(0,9), new HalfEdge(3,0), new HalfEdge(5,12), new HalfEdge(4,5),
|
||||
new HalfEdge(1,2), new HalfEdge(7,11), new HalfEdge(6,7), new HalfEdge(2,14),
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new Polygon(0,4,0,surfaces[0],layers), new Polygon(4,4,1,surfaces[1],layers),
|
||||
new Polygon(8,4,2,surfaces[2],layers), new Polygon(12,4,3,surfaces[3],layers),
|
||||
new Polygon(16,4,4,surfaces[4],layers), new Polygon(20,4,5,surfaces[5],layers),
|
||||
});
|
||||
}
|
||||
|
||||
record PerfCase(string Name, int Brushes, Func<int> Build);
|
||||
record BrushMesh(Vec3[] Vertices, HalfEdge[] HalfEdges, Polygon[] Polygons);
|
||||
|
||||
enum Operation : byte { Additive = 0, Subtractive = 1, Intersecting = 2 }
|
||||
enum BrushFlags : int { Default = 0, Infinite = 1 }
|
||||
enum VertexChannelFlags : byte { Position = 0, Tangent = 2, Normal = 4, UV0 = 8, All = 14 }
|
||||
enum LayerUsageFlags : int
|
||||
{
|
||||
None = 0,
|
||||
Renderable = 1,
|
||||
CastShadows = 2,
|
||||
ReceiveShadows = 4,
|
||||
RenderCastShadows = Renderable | CastShadows,
|
||||
RenderReceiveShadows = Renderable | ReceiveShadows,
|
||||
RenderReceiveCastShadows = Renderable | CastShadows | ReceiveShadows,
|
||||
Collidable = 8,
|
||||
Culled = 1 << 23,
|
||||
}
|
||||
enum LayerParameterIndex : byte { None = 0, LayerParameter1 = 1, LayerParameter2 = 2, LayerParameter3 = 3 }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct Vec2 { public float X, Y; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct Vec3(float x, float y, float z)
|
||||
{
|
||||
public float X = x;
|
||||
public float Y = y;
|
||||
public float Z = z;
|
||||
public static Vec3 Zero => new(0, 0, 0);
|
||||
public static Vec3 Splat(float value) => new(value, value, value);
|
||||
public static Vec3 operator +(Vec3 a, Vec3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
|
||||
public static Vec3 operator *(Vec3 a, float s) => new(a.X * s, a.Y * s, a.Z * s);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct Vec4(float x, float y, float z, float w) { public float X = x, Y = y, Z = z, W = w; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct Mat4
|
||||
{
|
||||
public float M00, M10, M20, M30;
|
||||
public float M01, M11, M21, M31;
|
||||
public float M02, M12, M22, M32;
|
||||
public float M03, M13, M23, M33;
|
||||
public static Mat4 Identity => new() { M00 = 1, M11 = 1, M22 = 1, M33 = 1 };
|
||||
public static Mat4 RotationZ(float radians)
|
||||
{
|
||||
var c = MathF.Cos(radians);
|
||||
var s = MathF.Sin(radians);
|
||||
return new Mat4 { M00 = c, M10 = s, M01 = -s, M11 = c, M22 = 1, M33 = 1 };
|
||||
}
|
||||
public Mat4 WithTranslation(Vec3 t)
|
||||
{
|
||||
var m = this;
|
||||
m.M03 = t.X; m.M13 = t.Y; m.M23 = t.Z; m.M33 = 1;
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct UvMatrix(Vec4 u, Vec4 v) { public Vec4 U = u; public Vec4 V = v; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct SurfaceDescription
|
||||
{
|
||||
public uint SmoothingGroup;
|
||||
public int SurfaceFlags;
|
||||
public UvMatrix UV0;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct SurfaceLayers
|
||||
{
|
||||
public LayerUsageFlags LayerUsage;
|
||||
public int LayerParameter1;
|
||||
public int LayerParameter2;
|
||||
public int LayerParameter3;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct Aabb
|
||||
{
|
||||
public float MinX, MaxX;
|
||||
public float MinY, MaxY;
|
||||
public float MinZ, MaxZ;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct Polygon(int firstEdge, int edgeCount, int polygonId, SurfaceDescription surface, SurfaceLayers layers)
|
||||
{
|
||||
public int FirstEdge = firstEdge;
|
||||
public int EdgeCount = edgeCount;
|
||||
public int PolygonId = polygonId;
|
||||
public SurfaceDescription Surface = surface;
|
||||
public SurfaceLayers Layers = layers;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct HalfEdge(int vertexIndex, int twinIndex)
|
||||
{
|
||||
public int VertexIndex = vertexIndex;
|
||||
public int TwinIndex = twinIndex;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
struct MeshQuery
|
||||
{
|
||||
const int BitShift = 24;
|
||||
public uint Layers;
|
||||
public uint MaskAndChannels;
|
||||
public static MeshQuery RenderableAll => new(LayerUsageFlags.Renderable, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All);
|
||||
public static MeshQuery[] SimpleTypes => new[]
|
||||
{
|
||||
new MeshQuery(LayerUsageFlags.Renderable, LayerUsageFlags.Renderable, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
new MeshQuery(LayerUsageFlags.RenderReceiveCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
new MeshQuery(LayerUsageFlags.Renderable, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.None, VertexChannelFlags.All),
|
||||
new MeshQuery(LayerUsageFlags.RenderReceiveCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
|
||||
new MeshQuery(LayerUsageFlags.None, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
new MeshQuery(LayerUsageFlags.Culled, LayerUsageFlags.Culled, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
};
|
||||
public static MeshQuery[] RenderOnlyTypes => new[]
|
||||
{
|
||||
new MeshQuery(LayerUsageFlags.CastShadows, LayerUsageFlags.RenderCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
|
||||
new MeshQuery(LayerUsageFlags.Renderable, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
|
||||
new MeshQuery(LayerUsageFlags.RenderCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
|
||||
new MeshQuery(LayerUsageFlags.RenderReceiveShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
|
||||
new MeshQuery(LayerUsageFlags.RenderReceiveCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
|
||||
new MeshQuery(LayerUsageFlags.None, LayerUsageFlags.Renderable, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
new MeshQuery(LayerUsageFlags.CastShadows, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
new MeshQuery(LayerUsageFlags.ReceiveShadows, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
new MeshQuery(LayerUsageFlags.Culled, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
|
||||
};
|
||||
|
||||
public MeshQuery(LayerUsageFlags query, LayerUsageFlags mask, LayerParameterIndex parameterIndex, VertexChannelFlags vertexChannels)
|
||||
{
|
||||
if (mask == LayerUsageFlags.None)
|
||||
mask = query;
|
||||
Layers = (uint)query;
|
||||
Layers |= (uint)parameterIndex << BitShift;
|
||||
MaskAndChannels = (uint)mask | ((uint)vertexChannels << BitShift);
|
||||
}
|
||||
public int UsedVertexChannels => (int)(MaskAndChannels >> BitShift);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||
struct GeneratedMeshDescription
|
||||
{
|
||||
public MeshQuery MeshQuery;
|
||||
public int SurfaceParameter;
|
||||
public int MeshQueryIndex;
|
||||
public int SubMeshQueryIndex;
|
||||
public long GeometryHashValue;
|
||||
public long SurfaceHashValue;
|
||||
public int VertexCount;
|
||||
public int IndexCount;
|
||||
}
|
||||
|
||||
static partial class Native
|
||||
{
|
||||
const string Dll = "RealtimeCSG[1_559]";
|
||||
static readonly StringLog Log = (message, userId) => Console.Error.WriteLine($"native[{userId}]: {message}");
|
||||
static readonly ReturnString NameForUserId = _ => "<native-bridge>";
|
||||
|
||||
public static void RegisterNoopUnityMethods()
|
||||
{
|
||||
var methods = new UnityMethods
|
||||
{
|
||||
DebugLog = Log,
|
||||
DebugLogError = Log,
|
||||
DebugLogWarning = Log,
|
||||
NameForUserID = NameForUserId,
|
||||
};
|
||||
RegisterMethods(ref methods);
|
||||
}
|
||||
|
||||
public delegate void StringLog([MarshalAs(UnmanagedType.LPStr)] string text, int uniqueObjectID);
|
||||
[return: MarshalAs(UnmanagedType.LPStr)]
|
||||
public delegate string ReturnString(int uniqueObjectID);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)]
|
||||
struct UnityMethods
|
||||
{
|
||||
public StringLog DebugLog;
|
||||
public StringLog DebugLogError;
|
||||
public StringLog DebugLogWarning;
|
||||
public ReturnString NameForUserID;
|
||||
}
|
||||
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] static extern void RegisterMethods(ref UnityMethods unityMethods);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern void ClearAllNodes();
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool UpdateAllTreeMeshes();
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern void RebuildAll();
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetNodeCount();
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushCount();
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetTreeCount();
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushMeshCount();
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateTree(int userID, out int generatedTreeNodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetNumberOfBrushesInTree(int nodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetDirty(int nodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool ClearDirty(int nodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool IsNodeDirty(int nodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChildNodeCount(int nodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChildNodeAtIndex(int nodeID, int index);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetTreeOfNode(int nodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern byte GetTypeOfNode(int nodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetTreeEnabled(int modelNodeID, bool isEnabled);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetTreeEnabled(int modelNodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateBrush(int userID, out int generatedNodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateBranch(int userID, out int generatedOperationNodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBranchOperationType(int branchNodeID, Operation operation);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBrushOperationType(int brushNodeID, Operation operation);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBrushFlags(int brushNodeID, BrushFlags flags);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushOperationType(int brushNodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBrushMeshID(int brushNodeID, int brushMeshID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushMeshID(int brushNodeID);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetBrushBounds(int brushNodeID, ref Aabb bounds);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetBrushOutlineSizes(int brushNodeID, out int vertexCount, out int visibleOuterLineCount, out int visibleInnerLineCount, out int invisibleOuterLineCount, out int invisibleInnerLineCount, out int invalidLineCount);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetNodeLocalTransformation(int brushNodeID, ref Mat4 brushToTreeSpace);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChildNodes(int nodeID, int childCount, IntPtr childrenNodeIDs);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool InsertChildNodeRange(int nodeID, int index, int childCount, IntPtr childrenNodeIDs);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int CreateBrushMesh(int userID, int vertexCount, IntPtr vertices, int halfEdgeCount, IntPtr halfEdges, int polygonCount, IntPtr polygons);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool UpdateBrushMesh(int brushMeshIndex, int vertexCount, IntPtr vertices, int halfEdgeCount, IntPtr halfEdges, int polygonCount, IntPtr polygons);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool IsBrushMeshIDValid(int brushMeshIndex);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateMeshDescriptions(int treeNodeID, int meshTypeCount, IntPtr meshTypes, VertexChannelFlags vertexChannelMask, out int meshDescriptionCount);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetMeshDescriptions(int treeNodeID, int meshDescriptionCount, IntPtr meshDescriptions);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetGeneratedMesh(int treeNodeID, int meshIndex, int subMeshIndex, int indexCount, IntPtr indices, int vertexCount, IntPtr positions, IntPtr tangents, IntPtr normals, IntPtr uvs, out Vec3 boundsCenter, out Vec3 boundsSize);
|
||||
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int RayCastIntoTreeMultiCount(int modelNodeID, ref Vec3 worldRayStart, ref Vec3 worldRayEnd, ref Mat4 modelLocalToWorldMatrix, int inFilterLayerParameter0, bool ignoreInvisiblePolygons, IntPtr ignoreNodeIds, int ignoreNodeIdCount);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>RealtimeCsgNativeBridge</AssemblyName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
param(
|
||||
[string]$OutputPath = ".\experiments\generated\csg-perf-latest.jsonl",
|
||||
[string]$ReferenceCommand = $env:VIBEGEOMETRY_REFERENCE_CSG_PERF,
|
||||
[switch]$UseRealtimeCsgCpp
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$resolvedOutput = Join-Path $root $OutputPath
|
||||
$outputDir = Split-Path -Parent $resolvedOutput
|
||||
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
|
||||
|
||||
Push-Location $root
|
||||
try {
|
||||
cargo build --release -p vg_csg --example csg_perf_fixture | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "vg_csg perf fixture build failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$exe = Join-Path $root "target\release\examples\csg_perf_fixture.exe"
|
||||
if (!(Test-Path $exe)) {
|
||||
throw "Missing vg_csg perf fixture executable: $exe"
|
||||
}
|
||||
|
||||
& $exe | Set-Content -Path $resolvedOutput -Encoding utf8
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "vg_csg perf fixture failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
if ($UseRealtimeCsgCpp) {
|
||||
$nativeOutput = ".\experiments\generated\realtimecsg-cpp-perf-latest.jsonl"
|
||||
& (Join-Path $root ".\tools\run_realtimecsg_cpp_perf.ps1") -OutputPath $nativeOutput | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "RealtimeCSG C++ perf fixture failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Get-Content -Path $nativeOutput | Add-Content -Path $resolvedOutput -Encoding utf8
|
||||
} elseif ($ReferenceCommand) {
|
||||
$referenceOutput = & cmd /c $ReferenceCommand
|
||||
foreach ($line in $referenceOutput) {
|
||||
Add-Content -Path $resolvedOutput -Value $line -Encoding utf8
|
||||
}
|
||||
} else {
|
||||
$missing = @{
|
||||
kernel = "reference"
|
||||
status = "missing"
|
||||
reason = "Set VIBEGEOMETRY_REFERENCE_CSG_PERF or pass -ReferenceCommand with an executable that emits the same JSONL scenario records."
|
||||
} | ConvertTo-Json -Compress
|
||||
Add-Content -Path $resolvedOutput -Value $missing -Encoding utf8
|
||||
}
|
||||
|
||||
Get-Content -Path $resolvedOutput
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
param(
|
||||
[string]$OutputPath = ".\experiments\generated\realtimecsg-cpp-perf-latest.jsonl",
|
||||
[string]$PluginDll = ".\experiments\source-repos\realtime-CSG-for-unity\Plugins\Editor\External\x64\RealtimeCSG[1_559].dll",
|
||||
[switch]$Health,
|
||||
[switch]$DebugNative
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$project = Join-Path $root "tools\realtimecsg_native_bridge"
|
||||
$resolvedOutput = Join-Path $root $OutputPath
|
||||
$resolvedDll = Join-Path $root $PluginDll
|
||||
$releaseDir = Join-Path $project "bin\Release\net10.0"
|
||||
|
||||
if (!(Test-Path -LiteralPath $resolvedDll)) {
|
||||
throw "RealtimeCSG native DLL not found: $resolvedDll"
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $resolvedOutput) | Out-Null
|
||||
dotnet build $project -c Release | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "RealtimeCSG native bridge build failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Copy-Item -LiteralPath $resolvedDll -Destination (Join-Path $releaseDir "RealtimeCSG[1_559].dll") -Force
|
||||
$bridgeArgs = @($resolvedOutput)
|
||||
if ($Health) {
|
||||
$bridgeArgs += "--health"
|
||||
}
|
||||
if ($DebugNative) {
|
||||
$bridgeArgs += "--debug-native"
|
||||
}
|
||||
& (Join-Path $releaseDir "RealtimeCsgNativeBridge.exe") @bridgeArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Remove-Item -LiteralPath $resolvedOutput -ErrorAction SilentlyContinue
|
||||
throw "RealtimeCSG native bridge failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Get-Content -Path $resolvedOutput
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
param(
|
||||
[string]$RepoUrl = "https://github.com/carson-katri/geometry-script.git",
|
||||
[string]$Branch = "vibegeometry/blender-5-nodes-to-script"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) {
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
}
|
||||
|
||||
$root = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$target = Join-Path $root "external\geometry-script"
|
||||
$patchDir = Join-Path $root "patches\geometry-script"
|
||||
|
||||
function Test-GitPatch {
|
||||
param(
|
||||
[string]$Mode,
|
||||
[string]$PatchPath
|
||||
)
|
||||
|
||||
$previous = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
if ($Mode -eq "Reverse") {
|
||||
& git -C $target apply --reverse --check $PatchPath 2>$null
|
||||
} else {
|
||||
& git -C $target apply --check $PatchPath 2>$null
|
||||
}
|
||||
return $LASTEXITCODE -eq 0
|
||||
} finally {
|
||||
$ErrorActionPreference = $previous
|
||||
}
|
||||
}
|
||||
|
||||
if (!(Test-Path $target)) {
|
||||
New-Item -ItemType Directory -Force (Split-Path $target) | Out-Null
|
||||
git clone $RepoUrl $target
|
||||
}
|
||||
|
||||
git -C $target fetch origin
|
||||
|
||||
$branches = git -C $target branch --list $Branch
|
||||
if ($branches) {
|
||||
git -C $target switch $Branch
|
||||
} else {
|
||||
git -C $target switch -c $Branch origin/main
|
||||
}
|
||||
|
||||
$patches = Get-ChildItem -Path $patchDir -Filter "*.patch" | Sort-Object Name
|
||||
foreach ($patch in $patches) {
|
||||
if (Test-GitPatch -Mode "Forward" -PatchPath $patch.FullName) {
|
||||
git -C $target am $patch.FullName
|
||||
continue
|
||||
}
|
||||
|
||||
if (!(Test-GitPatch -Mode "Reverse" -PatchPath $patch.FullName)) {
|
||||
throw "Patch is neither applicable nor already applied: $($patch.FullName)"
|
||||
}
|
||||
}
|
||||
|
||||
git -C $target status --short --branch
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"""Smoke-test Geometry Script's node-tree-to-script converter in Blender."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GEOMETRY_SCRIPT = ROOT / "external" / "geometry-script"
|
||||
|
||||
|
||||
def _load_converter():
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from tools.geometry_script_loader import load_repo_geometry_script
|
||||
|
||||
load_repo_geometry_script()
|
||||
from geometry_script.operators.convert_tree import convert_node_tree
|
||||
|
||||
return convert_node_tree
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import bpy
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--groups", nargs="+", required=True)
|
||||
parser.add_argument("--json", type=Path)
|
||||
parser.add_argument("--out-dir", type=Path)
|
||||
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [])
|
||||
|
||||
convert_node_tree = _load_converter()
|
||||
results = []
|
||||
ok = True
|
||||
for name in args.groups:
|
||||
tree = bpy.data.node_groups[name]
|
||||
try:
|
||||
script = convert_node_tree(tree)
|
||||
compile(script, f"<nodes_to_script:{name}>", "exec")
|
||||
output_path = None
|
||||
if args.out_dir:
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = args.out_dir / f"{name.replace(' ', '_').lower()}.py"
|
||||
output_path.write_text(script, encoding="utf-8")
|
||||
result = {
|
||||
"group": name,
|
||||
"ok": True,
|
||||
"script_length": len(script),
|
||||
"line_count": len(script.splitlines()),
|
||||
"header": script.splitlines()[:8],
|
||||
"output_path": str(output_path) if output_path else None,
|
||||
}
|
||||
except Exception as exc:
|
||||
ok = False
|
||||
result = {
|
||||
"group": name,
|
||||
"ok": False,
|
||||
"error_type": type(exc).__name__,
|
||||
"error": str(exc),
|
||||
}
|
||||
results.append(result)
|
||||
|
||||
payload = {"ok": ok, "results": results}
|
||||
if args.json:
|
||||
args.json.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
print("VG_NODES_TO_SCRIPT_SMOKE " + json.dumps(payload, sort_keys=True))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
"""Verify the generated Aetheria Bloom habitat study scene."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from vibegeometry import assert_objects_exist, assert_render_artifacts, assert_scene_density
|
||||
|
||||
ARTIFACT_DIR = ROOT / "experiments" / "generated" / "aetheria_bloom"
|
||||
|
||||
|
||||
REQUIRED_OBJECTS = {
|
||||
"01_open_civic_inner_surface_full_bloom",
|
||||
"02_utility_mat_service_interlayer_full_bloom",
|
||||
"03_pressure_structural_shell_full_bloom",
|
||||
"04_outer_aggregate_shielding_full_bloom",
|
||||
"despun_center_axis_core",
|
||||
"despun_hub_cap_docking_axis",
|
||||
"docking_port_on_despun_hub_cap",
|
||||
"spun_spire_sheath_outer_frame",
|
||||
"traffic_light_spire_core",
|
||||
"GN_Light_Spine_Modifier_Carrier",
|
||||
"hubward_endcap_pressure_face",
|
||||
"capward_endcap_pressure_face",
|
||||
"hubward_endcap_terrace_ring_00",
|
||||
"capward_endcap_terrace_ring_00",
|
||||
"hubward_docking_hub_office_complex_ring_office_mesh",
|
||||
"hubward_docking_hub_office_complex_office_spoke_bridge_00",
|
||||
"hubward_endcap_rice_paddy_slums_annular_shelf_mesh",
|
||||
"hubward_endcap_rice_paddy_slums_rickety_shack_mesh",
|
||||
"hubward_endcap_rice_paddy_slums_midrise_housing_mesh",
|
||||
"hubward_endcap_rice_paddy_slums_surface_urban_crown_mesh",
|
||||
"hubward_endcap_rice_paddy_slums_patched_roof_mesh",
|
||||
"hubward_endcap_rice_paddy_slums_ladders_nets_handlines",
|
||||
"capward_endcap_beach_service_00_00",
|
||||
"spiral_cargo_spoke_00",
|
||||
"spiral_passenger_spoke_01",
|
||||
"spiral_atmospheric_utility_rib_00",
|
||||
"spun_to_despun_transfer_loop_00",
|
||||
"spiral_spoke_shell_collar_00",
|
||||
"service_ring_kappa_human_gallery",
|
||||
"kappa_operations_gallery_badge_route",
|
||||
"octopoid_support_rig_prep_station",
|
||||
"kappa_crawl_throat_1",
|
||||
"kappa_crawl_throat_2",
|
||||
"kappa_crawl_throat_3",
|
||||
"kappa_seal_lung_1",
|
||||
"kappa_air_return_to_filters",
|
||||
"kappa_coolant_heat_exchanger",
|
||||
"kappa_condensate_drain",
|
||||
"hubward_low_g_transition_band",
|
||||
"hyperurban_favela_city_belt",
|
||||
"prestige_urban_luxury_spoke_belt",
|
||||
"urban_mixed_suburban_industrial_belt",
|
||||
"industrial_agricultural_belt",
|
||||
"beach_resort_rim",
|
||||
"cylindrical_sea_after_beach",
|
||||
"wrapped_road_graph_0",
|
||||
"wrapped_river_0",
|
||||
"airflow_cloud_patch_0",
|
||||
"hubward_spoke_access_market_stack_00_00",
|
||||
"hyperurban_nested_town_nested_house_mesh",
|
||||
"hyperurban_nested_town_lane_network",
|
||||
"luxury_spoke_plaza_-5.0_-3.14",
|
||||
"luxury_spoke_plaza_detail_-5.0_00_plaza_ring",
|
||||
"luxury_spoke_plaza_detail_-5.0_00_kiosk_00",
|
||||
"luxury_spoke_plaza_detail_-5.0_00_feeder_walk_00",
|
||||
"mixed_belt_nested_town_nested_house_mesh",
|
||||
"factory_yard_block_00_00",
|
||||
"axial_surface_service_lane_00",
|
||||
"circumferential_market_seam_00",
|
||||
"Camera_Bloom_Cutaway",
|
||||
"Camera_Interior_World",
|
||||
"Camera_Hubward_Endcap_Terraces",
|
||||
}
|
||||
|
||||
|
||||
def assert_true(condition: bool, message: str):
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def verify_scene():
|
||||
assert_objects_exist(bpy, REQUIRED_OBJECTS)
|
||||
|
||||
group = bpy.data.node_groups.get("VG Bloom Light Spine")
|
||||
assert_true(group is not None, "Missing Geometry Script node group VG Bloom Light Spine")
|
||||
assert_true(len(group.nodes) >= 20, f"Unexpectedly small light-spine graph: {len(group.nodes)} nodes")
|
||||
assert_true(len(group.links) >= 25, f"Unexpectedly sparse light-spine graph: {len(group.links)} links")
|
||||
|
||||
carrier = bpy.data.objects["GN_Light_Spine_Modifier_Carrier"]
|
||||
assert_true(any(mod.type == "NODES" and mod.node_group == group for mod in carrier.modifiers), "Light-spine carrier lacks expected geometry-nodes modifier")
|
||||
|
||||
assert_render_artifacts(
|
||||
ARTIFACT_DIR / filename
|
||||
for filename in (
|
||||
"aetheria_bloom_habitat.png",
|
||||
"aetheria_bloom_interior_world.png",
|
||||
"aetheria_bloom_hubward_endcap_terraces.png",
|
||||
"aetheria_bloom_habitat.blend",
|
||||
)
|
||||
)
|
||||
|
||||
mesh_objects = assert_scene_density(bpy, 930)
|
||||
|
||||
slum_mesh = bpy.data.objects["hubward_endcap_rice_paddy_slums_rickety_shack_mesh"].data
|
||||
mid_mesh = bpy.data.objects["hubward_endcap_rice_paddy_slums_midrise_housing_mesh"].data
|
||||
crown_mesh = bpy.data.objects["hubward_endcap_rice_paddy_slums_surface_urban_crown_mesh"].data
|
||||
assert_true(len(slum_mesh.polygons) >= 1200, f"Endcap slum mesh is too sparse: {len(slum_mesh.polygons)} faces")
|
||||
assert_true(len(mid_mesh.polygons) >= 600, f"Endcap midrise mesh is too sparse: {len(mid_mesh.polygons)} faces")
|
||||
assert_true(len(crown_mesh.polygons) >= 250, f"Endcap urban crown mesh is too sparse: {len(crown_mesh.polygons)} faces")
|
||||
|
||||
print("AETHERIA_BLOOM_VERIFY ok")
|
||||
print("AETHERIA_BLOOM_VERIFY group", group.name, len(group.nodes), len(group.links))
|
||||
print("AETHERIA_BLOOM_VERIFY objects", len(mesh_objects))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify_scene()
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
"""Compare Shriinivas cartesian source graphs against Geometry Script translations.
|
||||
|
||||
Run with Blender from the repository root:
|
||||
|
||||
blender --background experiments/source-blends/shriinivas-cartesian.blend --python tools/verify_cartesian_translations.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TRANSLATION = ROOT / "examples" / "geometry_script" / "shriinivas_cartesian_helper.py"
|
||||
|
||||
|
||||
HELPER_INPUTS = {
|
||||
"Count": 25,
|
||||
"Start": (-0.45, 0.0, 0.0),
|
||||
"End": (0.45, 0.0, 0.0),
|
||||
"r": 0.6,
|
||||
"Value": 1.25,
|
||||
}
|
||||
|
||||
GRAPH_CASES = [
|
||||
{
|
||||
"case": "line",
|
||||
"source": "Geometry Nodes Line",
|
||||
"translated": "VG Cartesian Line",
|
||||
"inputs": {
|
||||
"Resolution": 17,
|
||||
"Length": 1.15,
|
||||
"m": -0.35,
|
||||
"Value": 0.25,
|
||||
"Thickness": 0.035,
|
||||
},
|
||||
},
|
||||
{
|
||||
"case": "parabola",
|
||||
"source": "Geometry Nodes Parabola",
|
||||
"translated": "VG Cartesian Parabola",
|
||||
"inputs": {
|
||||
"Resolution": 31,
|
||||
"Sample Resolution": 31,
|
||||
"Start": -0.7,
|
||||
"End": 0.8,
|
||||
"a": 0.55,
|
||||
"b": -0.25,
|
||||
"c": 0.1,
|
||||
"Thickness": 0.025,
|
||||
},
|
||||
},
|
||||
{
|
||||
"case": "circle",
|
||||
"source": "Geometry Nodes Circle",
|
||||
"translated": "VG Cartesian Circle",
|
||||
"inputs": {
|
||||
"Resolution": 37,
|
||||
"r": 0.65,
|
||||
"Thickness": 0.02,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _load_translation() -> None:
|
||||
spec = importlib.util.spec_from_file_location("vg_cartesian_translation", TRANSLATION)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load translation script: {TRANSLATION}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
def _new_geometry_group(name: str) -> bpy.types.GeometryNodeTree:
|
||||
if name in bpy.data.node_groups:
|
||||
bpy.data.node_groups.remove(bpy.data.node_groups[name])
|
||||
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
return group
|
||||
|
||||
|
||||
def _socket_by_name(sockets: Any, name: str):
|
||||
for socket in sockets:
|
||||
if socket.name == name:
|
||||
return socket
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def _set_inputs(group_node: bpy.types.GeometryNode, values: dict[str, object]) -> None:
|
||||
for socket in group_node.inputs:
|
||||
if socket.name not in values:
|
||||
continue
|
||||
value = values[socket.name]
|
||||
if isinstance(value, list):
|
||||
socket.default_value = value.pop(0)
|
||||
else:
|
||||
socket.default_value = value
|
||||
|
||||
|
||||
def _build_helper_eval_wrapper(name: str, helper_group: bpy.types.GeometryNodeTree) -> bpy.types.GeometryNodeTree:
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
helper = nodes.new("GeometryNodeGroup")
|
||||
helper.node_tree = helper_group
|
||||
_set_inputs(helper, HELPER_INPUTS)
|
||||
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
profile = nodes.new("GeometryNodeCurvePrimitiveCircle")
|
||||
_socket_by_name(profile.inputs, "Resolution").default_value = 8
|
||||
_socket_by_name(profile.inputs, "Radius").default_value = 0.01
|
||||
curve_to_mesh = nodes.new("GeometryNodeCurveToMesh")
|
||||
|
||||
links.new(_socket_by_name(helper.outputs, "Curve"), _socket_by_name(set_position.inputs, "Geometry"))
|
||||
links.new(_socket_by_name(helper.outputs, "Vector"), _socket_by_name(set_position.inputs, "Position"))
|
||||
links.new(_socket_by_name(set_position.outputs, "Geometry"), _socket_by_name(curve_to_mesh.inputs, "Curve"))
|
||||
links.new(_socket_by_name(profile.outputs, "Curve"), _socket_by_name(curve_to_mesh.inputs, "Profile Curve"))
|
||||
links.new(_socket_by_name(curve_to_mesh.outputs, "Mesh"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_group_eval_wrapper(
|
||||
name: str,
|
||||
graph_group: bpy.types.GeometryNodeTree,
|
||||
inputs: dict[str, object],
|
||||
) -> bpy.types.GeometryNodeTree:
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
|
||||
group_input = nodes.new("NodeGroupInput")
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
graph = nodes.new("GeometryNodeGroup")
|
||||
graph.node_tree = graph_group
|
||||
_set_inputs(graph, inputs)
|
||||
|
||||
if any(socket.name == "Geometry" for socket in graph.inputs):
|
||||
links.new(_socket_by_name(group_input.outputs, "Geometry"), _socket_by_name(graph.inputs, "Geometry"))
|
||||
links.new(_socket_by_name(graph.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _evaluated_vertices(group: bpy.types.GeometryNodeTree) -> list[tuple[float, float, float]]:
|
||||
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
|
||||
obj = bpy.data.objects.new(group.name + " Object", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
|
||||
modifier.node_group = group
|
||||
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated = obj.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
return [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def _bounds(vertices: list[tuple[float, float, float]]) -> dict[str, list[float]]:
|
||||
return {
|
||||
"min": [min(vertex[i] for vertex in vertices) for i in range(3)],
|
||||
"max": [max(vertex[i] for vertex in vertices) for i in range(3)],
|
||||
}
|
||||
|
||||
|
||||
def _compare(case: str, source_vertices, translated_vertices) -> dict[str, object]:
|
||||
if len(source_vertices) != len(translated_vertices):
|
||||
return {
|
||||
"case": case,
|
||||
"ok": False,
|
||||
"reason": "vertex_count_mismatch",
|
||||
"source_vertex_count": len(source_vertices),
|
||||
"translated_vertex_count": len(translated_vertices),
|
||||
"source_bounds": _bounds(source_vertices) if source_vertices else None,
|
||||
"translated_bounds": _bounds(translated_vertices) if translated_vertices else None,
|
||||
}
|
||||
max_delta = 0.0
|
||||
total_delta = 0.0
|
||||
for source, translated in zip(source_vertices, translated_vertices):
|
||||
delta = math.dist(source, translated)
|
||||
max_delta = max(max_delta, delta)
|
||||
total_delta += delta
|
||||
sorted_source = sorted(source_vertices)
|
||||
sorted_translated = sorted(translated_vertices)
|
||||
max_sorted_delta = 0.0
|
||||
total_sorted_delta = 0.0
|
||||
for source, translated in zip(sorted_source, sorted_translated):
|
||||
delta = math.dist(source, translated)
|
||||
max_sorted_delta = max(max_sorted_delta, delta)
|
||||
total_sorted_delta += delta
|
||||
return {
|
||||
"case": case,
|
||||
"ok": max_delta <= 1e-6 or max_sorted_delta <= 1e-6,
|
||||
"source_vertex_count": len(source_vertices),
|
||||
"translated_vertex_count": len(translated_vertices),
|
||||
"max_vertex_delta": max_delta,
|
||||
"mean_vertex_delta": total_delta / len(source_vertices) if source_vertices else 0.0,
|
||||
"max_sorted_vertex_delta": max_sorted_delta,
|
||||
"mean_sorted_vertex_delta": total_sorted_delta / len(source_vertices) if source_vertices else 0.0,
|
||||
"source_bounds": _bounds(source_vertices),
|
||||
"translated_bounds": _bounds(translated_vertices),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_translation()
|
||||
results = []
|
||||
|
||||
source_helper = bpy.data.node_groups.get("NodeGroup.001")
|
||||
translated_helper = bpy.data.node_groups.get("VG Cartesian Helper")
|
||||
if source_helper is None or translated_helper is None:
|
||||
raise RuntimeError("Source or translated helper group was not found.")
|
||||
results.append(
|
||||
_compare(
|
||||
"helper",
|
||||
_evaluated_vertices(_build_helper_eval_wrapper("VG Verify Source Helper", source_helper)),
|
||||
_evaluated_vertices(_build_helper_eval_wrapper("VG Verify Translated Helper", translated_helper)),
|
||||
)
|
||||
)
|
||||
|
||||
for case in GRAPH_CASES:
|
||||
source = bpy.data.node_groups.get(case["source"])
|
||||
translated = bpy.data.node_groups.get(case["translated"])
|
||||
if source is None or translated is None:
|
||||
raise RuntimeError(f"Missing group for case {case['case']}: {case['source']} / {case['translated']}")
|
||||
results.append(
|
||||
_compare(
|
||||
case["case"],
|
||||
_evaluated_vertices(
|
||||
_build_group_eval_wrapper("VG Verify Source " + case["case"], source, case["inputs"])
|
||||
),
|
||||
_evaluated_vertices(
|
||||
_build_group_eval_wrapper("VG Verify Translated " + case["case"], translated, case["inputs"])
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
ok = all(result["ok"] for result in results)
|
||||
print("VG_CARTESIAN_TRANSLATION_BEHAVIOR " + json.dumps({"ok": ok, "results": results}, sort_keys=True))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
"""Compare small CurveToMeshUV utility groups against Geometry Script translations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TRANSLATION = ROOT / "examples" / "geometry_script" / "quellenform_curve_to_mesh_uv.py"
|
||||
AUTO_SMOOTH_CASE = {"Angle": 0.75}
|
||||
CURVE_TO_MESH_UV_CASE = {
|
||||
"Fill Caps": True,
|
||||
"• Lock Caps UV-Scaling": False,
|
||||
"Hole Tolerant": False,
|
||||
"• Smooth Angle": 3.1415927410125732,
|
||||
"Free U-Space": False,
|
||||
"• Lock U-Scaling": False,
|
||||
"Free V-Space": False,
|
||||
"• Lock V-Scaling": False,
|
||||
"Sides UV Scale": (1.0, 1.0, 0.0),
|
||||
"Sides UV Offset": (0.0, 0.0, 0.0),
|
||||
"Sides UV Z-Rotation": 0.0,
|
||||
"Caps UV Scale": (1.0, 1.0, 0.0),
|
||||
"Caps UV Offset": (0.0, 0.0, 0.0),
|
||||
"Caps UV Z-Rotation": 0.0,
|
||||
"Store Attributes": False,
|
||||
}
|
||||
|
||||
|
||||
def _load_translation() -> None:
|
||||
spec = importlib.util.spec_from_file_location("vg_curve_to_mesh_uv_translation", TRANSLATION)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load translation script: {TRANSLATION}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
def _socket_by_name(sockets, name: str):
|
||||
for socket in sockets:
|
||||
if socket.name == name:
|
||||
return socket
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def _new_geometry_group(name: str) -> bpy.types.GeometryNodeTree:
|
||||
if name in bpy.data.node_groups:
|
||||
bpy.data.node_groups.remove(bpy.data.node_groups[name])
|
||||
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
return group
|
||||
|
||||
|
||||
def _build_auto_smooth_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
|
||||
cube = nodes.new("GeometryNodeMeshCube")
|
||||
_socket_by_name(cube.inputs, "Size").default_value = (1.0, 1.0, 1.0)
|
||||
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
links.new(_socket_by_name(cube.outputs, "Mesh"), _socket_by_name(group_node.inputs, "Geometry"))
|
||||
for socket in group_node.inputs:
|
||||
if socket.name in inputs:
|
||||
socket.default_value = inputs[socket.name]
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(group_node.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _normalized_key(name: str) -> str:
|
||||
return name.lower().replace(" ", "").replace("•", "").replace("-", "")
|
||||
|
||||
|
||||
def _set_inputs(group_node: bpy.types.GeometryNode, inputs: dict[str, object]):
|
||||
normalized = {_normalized_key(key): value for key, value in inputs.items()}
|
||||
for socket in group_node.inputs:
|
||||
key = _normalized_key(socket.name)
|
||||
if socket.name in inputs:
|
||||
socket.default_value = inputs[socket.name]
|
||||
elif key in normalized:
|
||||
socket.default_value = normalized[key]
|
||||
|
||||
|
||||
def _build_curve_to_mesh_uv_wrapper(
|
||||
name: str,
|
||||
source_group: bpy.types.GeometryNodeTree,
|
||||
inputs: dict[str, object],
|
||||
output_mode: str = "mesh",
|
||||
):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
|
||||
curve = nodes.new("GeometryNodeCurvePrimitiveCircle")
|
||||
curve.mode = "RADIUS"
|
||||
_socket_by_name(curve.inputs, "Resolution").default_value = 24
|
||||
_socket_by_name(curve.inputs, "Radius").default_value = 1.2
|
||||
|
||||
profile = nodes.new("GeometryNodeCurvePrimitiveCircle")
|
||||
profile.mode = "RADIUS"
|
||||
_socket_by_name(profile.inputs, "Resolution").default_value = 8
|
||||
_socket_by_name(profile.inputs, "Radius").default_value = 0.2
|
||||
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
links.new(_socket_by_name(curve.outputs, "Curve"), _socket_by_name(group_node.inputs, "Curve"))
|
||||
links.new(_socket_by_name(profile.outputs, "Curve"), _socket_by_name(group_node.inputs, "Profile Curve"))
|
||||
_set_inputs(group_node, inputs)
|
||||
|
||||
output_geometry = _socket_by_name(group_node.outputs, "Mesh")
|
||||
if output_mode == "uv":
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(_socket_by_name(group_node.outputs, "Mesh"), _socket_by_name(set_position.inputs, "Geometry"))
|
||||
links.new(_socket_by_name(group_node.outputs, "UV Map"), _socket_by_name(set_position.inputs, "Position"))
|
||||
output_geometry = _socket_by_name(set_position.outputs, "Geometry")
|
||||
elif output_mode == "caps":
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.mode = "ALL"
|
||||
delete.domain = "FACE"
|
||||
links.new(_socket_by_name(group_node.outputs, "Mesh"), _socket_by_name(delete.inputs, "Geometry"))
|
||||
links.new(_socket_by_name(group_node.outputs, "Caps Mask"), _socket_by_name(delete.inputs, "Selection"))
|
||||
output_geometry = _socket_by_name(delete.outputs, "Geometry")
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(output_geometry, _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_no_input_geometry_wrapper(name: str, source_group: bpy.types.GeometryNodeTree):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(group_node.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _evaluated_mesh_summary(group: bpy.types.GeometryNodeTree):
|
||||
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
|
||||
obj = bpy.data.objects.new(group.name + " Object", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
|
||||
modifier.node_group = group
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated = obj.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
vertices = [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
|
||||
polygons = [polygon.use_smooth for polygon in evaluated_mesh.polygons]
|
||||
return {
|
||||
"vertices": vertices,
|
||||
"polygon_smooth": polygons,
|
||||
}
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_translation()
|
||||
source = _evaluated_mesh_summary(
|
||||
_build_auto_smooth_wrapper("VG Verify Source Auto Smooth", bpy.data.node_groups["Auto Smooth"], AUTO_SMOOTH_CASE)
|
||||
)
|
||||
translated = _evaluated_mesh_summary(
|
||||
_build_auto_smooth_wrapper("VG Verify Translated Auto Smooth", bpy.data.node_groups["VG Auto Smooth"], AUTO_SMOOTH_CASE)
|
||||
)
|
||||
max_delta = max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source["vertices"]), sorted(translated["vertices"]))),
|
||||
default=0.0,
|
||||
)
|
||||
auto_smooth_result = {
|
||||
"case": "auto_smooth_cube",
|
||||
"source_vertex_count": len(source["vertices"]),
|
||||
"translated_vertex_count": len(translated["vertices"]),
|
||||
"max_sorted_vertex_delta": max_delta,
|
||||
"source_smooth_faces": sum(source["polygon_smooth"]),
|
||||
"translated_smooth_faces": sum(translated["polygon_smooth"]),
|
||||
}
|
||||
auto_smooth_result["ok"] = (
|
||||
auto_smooth_result["source_vertex_count"] == auto_smooth_result["translated_vertex_count"]
|
||||
and max_delta <= 1e-6
|
||||
and source["polygon_smooth"] == translated["polygon_smooth"]
|
||||
)
|
||||
results = [auto_smooth_result]
|
||||
for mode in ("mesh", "uv", "caps"):
|
||||
source_uv = _evaluated_mesh_summary(
|
||||
_build_curve_to_mesh_uv_wrapper(
|
||||
f"VG Verify Source Curve To Mesh UV {mode}",
|
||||
bpy.data.node_groups["Curve to Mesh UV"],
|
||||
CURVE_TO_MESH_UV_CASE,
|
||||
output_mode=mode,
|
||||
)
|
||||
)
|
||||
translated_uv = _evaluated_mesh_summary(
|
||||
_build_curve_to_mesh_uv_wrapper(
|
||||
f"VG Verify Translated Curve To Mesh UV {mode}",
|
||||
bpy.data.node_groups["VG Curve To Mesh UV"],
|
||||
CURVE_TO_MESH_UV_CASE,
|
||||
output_mode=mode,
|
||||
)
|
||||
)
|
||||
uv_delta = max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source_uv["vertices"]), sorted(translated_uv["vertices"]))),
|
||||
default=0.0,
|
||||
)
|
||||
uv_result = {
|
||||
"case": f"curve_to_mesh_uv_{mode}",
|
||||
"source_vertex_count": len(source_uv["vertices"]),
|
||||
"translated_vertex_count": len(translated_uv["vertices"]),
|
||||
"max_sorted_vertex_delta": uv_delta,
|
||||
}
|
||||
uv_result["ok"] = uv_result["source_vertex_count"] == uv_result["translated_vertex_count"] and uv_delta <= 1e-6
|
||||
results.append(uv_result)
|
||||
for case, source_name, translated_name in (
|
||||
("curve_to_mesh_uv_demo", "_258246", "VG Curve To Mesh UV Demo"),
|
||||
("curve_to_mesh_uv_title", "_Title", "VG Curve To Mesh UV Title"),
|
||||
):
|
||||
source_demo = _evaluated_mesh_summary(_build_no_input_geometry_wrapper(f"VG Verify Source {case}", bpy.data.node_groups[source_name]))
|
||||
translated_demo = _evaluated_mesh_summary(
|
||||
_build_no_input_geometry_wrapper(f"VG Verify Translated {case}", bpy.data.node_groups[translated_name])
|
||||
)
|
||||
demo_delta = max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source_demo["vertices"]), sorted(translated_demo["vertices"]))),
|
||||
default=0.0,
|
||||
)
|
||||
demo_result = {
|
||||
"case": case,
|
||||
"source_vertex_count": len(source_demo["vertices"]),
|
||||
"translated_vertex_count": len(translated_demo["vertices"]),
|
||||
"max_sorted_vertex_delta": demo_delta,
|
||||
}
|
||||
demo_result["ok"] = (
|
||||
demo_result["source_vertex_count"] == demo_result["translated_vertex_count"] and demo_delta <= 1e-6
|
||||
)
|
||||
results.append(demo_result)
|
||||
payload = {"ok": all(result["ok"] for result in results), "results": results}
|
||||
print("VG_CURVE_TO_MESH_UV_TRANSLATION_BEHAVIOR " + json.dumps(payload, sort_keys=True))
|
||||
return 0 if payload["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,404 +0,0 @@
|
|||
"""Compare Shriinivas fieldvalue scalar helpers against Geometry Script translations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TRANSLATION = ROOT / "examples" / "geometry_script" / "shriinivas_fieldvalue.py"
|
||||
|
||||
CASES = [
|
||||
{"Number": 1234.0, "Position": 0},
|
||||
{"Number": 1234.0, "Position": 1},
|
||||
{"Number": 98765.0, "Position": 2},
|
||||
{"Number": 120305.0, "Position": 3},
|
||||
]
|
||||
NEXT_DIGIT_CASES = [
|
||||
{"Whole Part": 1234.0, "Fraction Part": 5678.0, "Max Precision": 2, "Position": 0},
|
||||
{"Whole Part": 1234.0, "Fraction Part": 5678.0, "Max Precision": 2, "Position": 1},
|
||||
{"Whole Part": 1234.0, "Fraction Part": 5678.0, "Max Precision": 2, "Position": 2},
|
||||
{"Whole Part": 98765.0, "Fraction Part": 4321.0, "Max Precision": 3, "Position": 4},
|
||||
]
|
||||
|
||||
DECIMAL_CASE = {"Vertical Segment Size": 2.4, "radius": 0.18}
|
||||
SEGMENT_CASES = [
|
||||
{
|
||||
"X Major": True,
|
||||
"Segment Length Along X": 2.0,
|
||||
"Segment Length Along Y": 0.45,
|
||||
"Secondary Axis Segment Thickness": 0.45,
|
||||
"Sharpness": 0.75,
|
||||
"offset": (0.2, -0.1, 0.0),
|
||||
},
|
||||
{
|
||||
"X Major": False,
|
||||
"Segment Length Along X": 0.4,
|
||||
"Segment Length Along Y": 1.8,
|
||||
"Secondary Axis Segment Thickness": 0.4,
|
||||
"Sharpness": 1.0,
|
||||
"offset": (-0.3, 0.25, 0.0),
|
||||
},
|
||||
]
|
||||
SEVEN_SEGMENTS_CASE = {
|
||||
"Horizontal Segment Size": 2.2,
|
||||
"Horizontal Segment Thickness": 0.42,
|
||||
"Vertical Segment Size": 1.7,
|
||||
"Vertical Segment Thickness": 0.36,
|
||||
"X Separation": 0.13,
|
||||
"Y Seperation": 0.17,
|
||||
"Tip Sharpness": 0.8,
|
||||
}
|
||||
FIELD_VALUE_CASE = {
|
||||
"Float Value": 12.34,
|
||||
"Digits Before Decimal": 3,
|
||||
"Precision": 2,
|
||||
"Scale": 0.05,
|
||||
"Digit Separation": 0.2,
|
||||
"Element Separation": 1.0,
|
||||
"X Offset": 0.0,
|
||||
"Y Offset": 0.0,
|
||||
"Z Offset": 0.001,
|
||||
"Rotation": 0,
|
||||
"Segment Width Factor": 0.5,
|
||||
"Segment Separation Factor": 0.1,
|
||||
"Tip Sharpness": 1.0,
|
||||
"Leading Zeros": False,
|
||||
}
|
||||
DELETE_DIGITS = range(0, 13)
|
||||
DELETE_SEGMENT_POSITIONS = range(0, 15)
|
||||
|
||||
|
||||
def _load_translation() -> None:
|
||||
spec = importlib.util.spec_from_file_location("vg_fieldvalue_translation", TRANSLATION)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load translation script: {TRANSLATION}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
def _socket_by_name(sockets, name: str):
|
||||
for socket in sockets:
|
||||
if socket.name == name:
|
||||
return socket
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def _new_geometry_group(name: str) -> bpy.types.GeometryNodeTree:
|
||||
if name in bpy.data.node_groups:
|
||||
bpy.data.node_groups.remove(bpy.data.node_groups[name])
|
||||
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
return group
|
||||
|
||||
|
||||
def _build_count_wrapper(name: str, digit_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
digit = nodes.new("GeometryNodeGroup")
|
||||
digit.node_tree = digit_group
|
||||
for socket in digit.inputs:
|
||||
if socket.name in inputs:
|
||||
socket.default_value = inputs[socket.name]
|
||||
|
||||
line = nodes.new("GeometryNodeMeshLine")
|
||||
line.mode = "OFFSET"
|
||||
line.count_mode = "TOTAL"
|
||||
_socket_by_name(line.inputs, "Offset").default_value = (1.0, 0.0, 0.0)
|
||||
links.new(_socket_by_name(digit.outputs, "Result"), _socket_by_name(line.inputs, "Count"))
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_bool_count_wrapper(name: str, bool_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
bool_node = nodes.new("GeometryNodeGroup")
|
||||
bool_node.node_tree = bool_group
|
||||
for socket in bool_node.inputs:
|
||||
if socket.name in inputs:
|
||||
socket.default_value = inputs[socket.name]
|
||||
|
||||
switch_node = nodes.new("GeometryNodeSwitch")
|
||||
switch_node.input_type = "INT"
|
||||
_socket_by_name(switch_node.inputs, "False").default_value = 0
|
||||
_socket_by_name(switch_node.inputs, "True").default_value = 1
|
||||
links.new(_socket_by_name(bool_node.outputs, "result"), _socket_by_name(switch_node.inputs, "Switch"))
|
||||
|
||||
line = nodes.new("GeometryNodeMeshLine")
|
||||
line.mode = "OFFSET"
|
||||
line.count_mode = "TOTAL"
|
||||
_socket_by_name(line.inputs, "Offset").default_value = (1.0, 0.0, 0.0)
|
||||
links.new(_socket_by_name(switch_node.outputs, "Output"), _socket_by_name(line.inputs, "Count"))
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_geometry_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object], output: str):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
lower_inputs = {key.lower(): value for key, value in inputs.items()}
|
||||
for socket in group_node.inputs:
|
||||
if socket.name in inputs:
|
||||
socket.default_value = inputs[socket.name]
|
||||
elif socket.name.lower() in lower_inputs:
|
||||
socket.default_value = lower_inputs[socket.name.lower()]
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(group_node.outputs, output), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_field_value_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
|
||||
line = nodes.new("GeometryNodeMeshLine")
|
||||
line.mode = "OFFSET"
|
||||
line.count_mode = "TOTAL"
|
||||
_socket_by_name(line.inputs, "Count").default_value = 2
|
||||
_socket_by_name(line.inputs, "Offset").default_value = (1.0, 0.0, 0.0)
|
||||
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_node.inputs, "Geometry"))
|
||||
for socket in group_node.inputs:
|
||||
if socket.name not in inputs:
|
||||
continue
|
||||
try:
|
||||
socket.default_value = inputs[socket.name]
|
||||
except TypeError:
|
||||
# Menu sockets without copied enum metadata can still use the
|
||||
# source graph's default route for this baseline comparison.
|
||||
pass
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(group_node.outputs, "Value"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _evaluated_vertex_count(group: bpy.types.GeometryNodeTree) -> int:
|
||||
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
|
||||
obj = bpy.data.objects.new(group.name + " Object", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
|
||||
modifier.node_group = group
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated = obj.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
return len(evaluated_mesh.vertices)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def _evaluated_vertices(group: bpy.types.GeometryNodeTree) -> list[tuple[float, float, float]]:
|
||||
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
|
||||
obj = bpy.data.objects.new(group.name + " Object", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
|
||||
modifier.node_group = group
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated = obj.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
return [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_translation()
|
||||
source = bpy.data.node_groups["Digit At"]
|
||||
translated = bpy.data.node_groups["VG Digit At"]
|
||||
results = []
|
||||
source_decimal = _evaluated_vertices(
|
||||
_build_geometry_wrapper("VG Verify Source Decimal", bpy.data.node_groups["Create Decimal"], DECIMAL_CASE, "Dot")
|
||||
)
|
||||
translated_decimal = _evaluated_vertices(
|
||||
_build_geometry_wrapper("VG Verify Translated Decimal", bpy.data.node_groups["VG Create Decimal"], DECIMAL_CASE, "Dot")
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"case": "create_decimal",
|
||||
"source_vertex_count": len(source_decimal),
|
||||
"translated_vertex_count": len(translated_decimal),
|
||||
"max_sorted_vertex_delta": max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source_decimal), sorted(translated_decimal))),
|
||||
default=0.0,
|
||||
),
|
||||
}
|
||||
)
|
||||
results[-1]["ok"] = (
|
||||
results[-1]["source_vertex_count"] == results[-1]["translated_vertex_count"]
|
||||
and results[-1]["max_sorted_vertex_delta"] <= 1e-6
|
||||
)
|
||||
for index, inputs in enumerate(SEGMENT_CASES):
|
||||
source_segment = _evaluated_vertices(
|
||||
_build_geometry_wrapper(
|
||||
f"VG Verify Source Segment {index}",
|
||||
bpy.data.node_groups["Create Segment"],
|
||||
inputs,
|
||||
"Segment",
|
||||
)
|
||||
)
|
||||
translated_segment = _evaluated_vertices(
|
||||
_build_geometry_wrapper(
|
||||
f"VG Verify Translated Segment {index}",
|
||||
bpy.data.node_groups["VG Create Segment"],
|
||||
inputs,
|
||||
"Segment",
|
||||
)
|
||||
)
|
||||
max_delta = max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source_segment), sorted(translated_segment))),
|
||||
default=0.0,
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"case": {"create_segment": index, **inputs},
|
||||
"source_vertex_count": len(source_segment),
|
||||
"translated_vertex_count": len(translated_segment),
|
||||
"max_sorted_vertex_delta": max_delta,
|
||||
"ok": len(source_segment) == len(translated_segment) and max_delta <= 1e-6,
|
||||
}
|
||||
)
|
||||
source_seven_segments = _evaluated_vertices(
|
||||
_build_geometry_wrapper(
|
||||
"VG Verify Source Seven Segments",
|
||||
bpy.data.node_groups["Seven Segments"],
|
||||
SEVEN_SEGMENTS_CASE,
|
||||
"bars",
|
||||
)
|
||||
)
|
||||
translated_seven_segments = _evaluated_vertices(
|
||||
_build_geometry_wrapper(
|
||||
"VG Verify Translated Seven Segments",
|
||||
bpy.data.node_groups["VG Seven Segments"],
|
||||
SEVEN_SEGMENTS_CASE,
|
||||
"bars",
|
||||
)
|
||||
)
|
||||
seven_segments_delta = max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source_seven_segments), sorted(translated_seven_segments))),
|
||||
default=0.0,
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"case": {"seven_segments": SEVEN_SEGMENTS_CASE},
|
||||
"source_vertex_count": len(source_seven_segments),
|
||||
"translated_vertex_count": len(translated_seven_segments),
|
||||
"max_sorted_vertex_delta": seven_segments_delta,
|
||||
"ok": len(source_seven_segments) == len(translated_seven_segments) and seven_segments_delta <= 1e-6,
|
||||
}
|
||||
)
|
||||
source_field_value = _evaluated_vertices(
|
||||
_build_field_value_wrapper("VG Verify Source Field Value", bpy.data.node_groups["Field Value"], FIELD_VALUE_CASE)
|
||||
)
|
||||
translated_field_value = _evaluated_vertices(
|
||||
_build_field_value_wrapper("VG Verify Translated Field Value", bpy.data.node_groups["VG Field Value"], FIELD_VALUE_CASE)
|
||||
)
|
||||
field_value_delta = max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source_field_value), sorted(translated_field_value))),
|
||||
default=0.0,
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"case": {"field_value": FIELD_VALUE_CASE},
|
||||
"source_vertex_count": len(source_field_value),
|
||||
"translated_vertex_count": len(translated_field_value),
|
||||
"max_sorted_vertex_delta": field_value_delta,
|
||||
"ok": len(source_field_value) == len(translated_field_value) and field_value_delta <= 1e-6,
|
||||
}
|
||||
)
|
||||
for index, inputs in enumerate(CASES):
|
||||
source_count = _evaluated_vertex_count(_build_count_wrapper(f"VG Verify Source Digit {index}", source, inputs))
|
||||
translated_count = _evaluated_vertex_count(
|
||||
_build_count_wrapper(f"VG Verify Translated Digit {index}", translated, inputs)
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"case": inputs,
|
||||
"source_count": source_count,
|
||||
"translated_count": translated_count,
|
||||
"ok": source_count == translated_count,
|
||||
}
|
||||
)
|
||||
source_next_digit = bpy.data.node_groups["Next Digit"]
|
||||
translated_next_digit = bpy.data.node_groups["VG Next Digit"]
|
||||
for index, inputs in enumerate(NEXT_DIGIT_CASES):
|
||||
source_count = _evaluated_vertex_count(
|
||||
_build_count_wrapper(f"VG Verify Source Next Digit {index}", source_next_digit, inputs)
|
||||
)
|
||||
translated_count = _evaluated_vertex_count(
|
||||
_build_count_wrapper(f"VG Verify Translated Next Digit {index}", translated_next_digit, inputs)
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"case": {"next_digit": inputs},
|
||||
"source_count": source_count,
|
||||
"translated_count": translated_count,
|
||||
"ok": source_count == translated_count,
|
||||
}
|
||||
)
|
||||
delete_mismatches = []
|
||||
source_delete = bpy.data.node_groups["Delete Segments"]
|
||||
translated_delete = bpy.data.node_groups["VG Delete Segments"]
|
||||
checked = 0
|
||||
for digit in DELETE_DIGITS:
|
||||
for segment_position in DELETE_SEGMENT_POSITIONS:
|
||||
inputs = {"Digit": digit, "Segment Position": segment_position}
|
||||
source_count = _evaluated_vertex_count(
|
||||
_build_bool_count_wrapper(f"VG Verify Source Delete {digit}-{segment_position}", source_delete, inputs)
|
||||
)
|
||||
translated_count = _evaluated_vertex_count(
|
||||
_build_bool_count_wrapper(
|
||||
f"VG Verify Translated Delete {digit}-{segment_position}",
|
||||
translated_delete,
|
||||
inputs,
|
||||
)
|
||||
)
|
||||
checked += 1
|
||||
if source_count != translated_count:
|
||||
delete_mismatches.append(
|
||||
{
|
||||
"digit": digit,
|
||||
"segment_position": segment_position,
|
||||
"source_count": source_count,
|
||||
"translated_count": translated_count,
|
||||
}
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"case": "delete_segments_grid",
|
||||
"checked": checked,
|
||||
"mismatches": delete_mismatches[:10],
|
||||
"mismatch_count": len(delete_mismatches),
|
||||
"ok": not delete_mismatches,
|
||||
}
|
||||
)
|
||||
ok = all(result["ok"] for result in results)
|
||||
print("VG_FIELDVALUE_TRANSLATION_BEHAVIOR " + json.dumps({"ok": ok, "results": results}, sort_keys=True))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
"""Compare compact index_of_nearest demo helpers against Geometry Script translations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TRANSLATION = ROOT / "examples" / "geometry_script" / "blender_index_of_nearest.py"
|
||||
|
||||
|
||||
def _load_translation() -> None:
|
||||
spec = importlib.util.spec_from_file_location("vg_index_of_nearest_translation", TRANSLATION)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load translation script: {TRANSLATION}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
def _socket_by_name(sockets, name: str):
|
||||
for socket in sockets:
|
||||
if socket.name == name:
|
||||
return socket
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def _socket_key(name: str) -> str:
|
||||
return name.lower().replace(" ", "").replace("_", "")
|
||||
|
||||
|
||||
def _set_inputs(group_node: bpy.types.GeometryNode, inputs: dict[str, object]) -> None:
|
||||
normalized = {_socket_key(key): value for key, value in inputs.items()}
|
||||
for socket in group_node.inputs:
|
||||
key = _socket_key(socket.name)
|
||||
if key in normalized:
|
||||
socket.default_value = normalized[key]
|
||||
|
||||
|
||||
def _new_geometry_group(name: str) -> bpy.types.GeometryNodeTree:
|
||||
if name in bpy.data.node_groups:
|
||||
bpy.data.node_groups.remove(bpy.data.node_groups[name])
|
||||
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
return group
|
||||
|
||||
|
||||
def _add_line(nodes, *, count: int = 6, offset=(0.024000000208616257, 0.017999999225139618, 0.0)):
|
||||
line = nodes.new("GeometryNodeMeshLine")
|
||||
line.mode = "OFFSET"
|
||||
line.count_mode = "TOTAL"
|
||||
_socket_by_name(line.inputs, "Count").default_value = count
|
||||
_socket_by_name(line.inputs, "Offset").default_value = offset
|
||||
return line
|
||||
|
||||
|
||||
def _build_geometry_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object] | None = None):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
line = _add_line(nodes)
|
||||
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(group_node.inputs, "Geometry"))
|
||||
_set_inputs(group_node, inputs or {})
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(group_node.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_velocity_wrapper(name: str, source_group: bpy.types.GeometryNodeTree):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
line = _add_line(nodes, count=4, offset=(0.020000000298023224, 0.0, 0.0))
|
||||
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
_set_inputs(
|
||||
group_node,
|
||||
{
|
||||
"Current Velocity": (0.0020000000949949026, 0.004000000189989805, 0.0),
|
||||
"Last Position": (0.009999999776482582, 0.0, 0.0),
|
||||
},
|
||||
)
|
||||
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(set_position.inputs, "Geometry"))
|
||||
links.new(_socket_by_name(group_node.outputs, "velocity"), _socket_by_name(set_position.inputs, "Position"))
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(set_position.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_collision_wrapper(name: str, source_group: bpy.types.GeometryNodeTree):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
line = _add_line(nodes, count=5, offset=(0.003000000026077032, 0.0, 0.0))
|
||||
points = nodes.new("GeometryNodeMeshToPoints")
|
||||
points.mode = "VERTICES"
|
||||
links.new(_socket_by_name(line.outputs, "Mesh"), _socket_by_name(points.inputs, "Mesh"))
|
||||
radius = nodes.new("GeometryNodeSetPointRadius")
|
||||
_socket_by_name(radius.inputs, "Radius").default_value = 0.02500000037252903
|
||||
links.new(_socket_by_name(points.outputs, "Points"), _socket_by_name(radius.inputs, "Points"))
|
||||
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
links.new(_socket_by_name(radius.outputs, "Points"), _socket_by_name(group_node.inputs, "Geometry"))
|
||||
|
||||
vertices = nodes.new("GeometryNodePointsToVertices")
|
||||
links.new(_socket_by_name(group_node.outputs, "Geometry"), _socket_by_name(vertices.inputs, "Points"))
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(vertices.outputs, "Mesh"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _build_collider_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object] | None = None):
|
||||
return _build_geometry_wrapper(name, source_group, inputs)
|
||||
|
||||
|
||||
def _evaluated_vertices(group: bpy.types.GeometryNodeTree):
|
||||
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
|
||||
obj = bpy.data.objects.new(group.name + " Object", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
|
||||
modifier.node_group = group
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated = obj.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
return [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def _compare(case: str, source_group, translated_group, builder, source_inputs=None, translated_inputs=None):
|
||||
source = _evaluated_vertices(builder(f"VG Verify Source {case}", source_group, source_inputs or {}) if builder != _build_velocity_wrapper and builder != _build_collision_wrapper else builder(f"VG Verify Source {case}", source_group))
|
||||
translated = _evaluated_vertices(
|
||||
builder(f"VG Verify Translated {case}", translated_group, translated_inputs or {})
|
||||
if builder != _build_velocity_wrapper and builder != _build_collision_wrapper
|
||||
else builder(f"VG Verify Translated {case}", translated_group)
|
||||
)
|
||||
max_delta = (
|
||||
max((math.dist(a, b) for a, b in zip(sorted(source), sorted(translated))), default=0.0)
|
||||
if len(source) == len(translated)
|
||||
else math.inf
|
||||
)
|
||||
return {
|
||||
"case": case,
|
||||
"source_vertex_count": len(source),
|
||||
"translated_vertex_count": len(translated),
|
||||
"max_sorted_vertex_delta": max_delta,
|
||||
"ok": len(source) == len(translated) and max_delta <= 1e-6,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_translation()
|
||||
collider = bpy.data.objects.get("Icosphere")
|
||||
results = [
|
||||
_compare(
|
||||
"boundary_step",
|
||||
bpy.data.node_groups["boundary_step"],
|
||||
bpy.data.node_groups["VG Boundary Step"],
|
||||
_build_geometry_wrapper,
|
||||
{"Vector": (0.0, 0.0, 0.0), "B": 0.05000000074505806},
|
||||
{"Vector": (0.0, 0.0, 0.0), "B": 0.05000000074505806},
|
||||
),
|
||||
_compare("update_velocity", bpy.data.node_groups["update_velocity"], bpy.data.node_groups["VG Update Velocity"], _build_velocity_wrapper),
|
||||
_compare("collision_step", bpy.data.node_groups["collision_step"], bpy.data.node_groups["VG Collision Step"], _build_collision_wrapper),
|
||||
_compare(
|
||||
"collider_step",
|
||||
bpy.data.node_groups["collider_step"],
|
||||
bpy.data.node_groups["VG Collider Step"],
|
||||
_build_collider_wrapper,
|
||||
{},
|
||||
{"Collider": collider},
|
||||
),
|
||||
]
|
||||
payload = {"ok": all(result["ok"] for result in results), "results": results}
|
||||
print("VG_INDEX_OF_NEAREST_TRANSLATION_BEHAVIOR " + json.dumps(payload, sort_keys=True))
|
||||
return 0 if payload["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
"""Compare Blender's instance attributes demo against the Geometry Script translation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TRANSLATION = ROOT / "examples" / "geometry_script" / "blender_instance_attributes.py"
|
||||
CASES = {
|
||||
"default": {
|
||||
"Height": 0.20000000298023224,
|
||||
"Radius": 0.05000000074505806,
|
||||
"Density": 1.0,
|
||||
"Thickness": 0.009999999776482582,
|
||||
"Curl": 0.5,
|
||||
"Seed": 0,
|
||||
},
|
||||
"wider_curlier_seeded": {
|
||||
"Height": 0.24000000953674316,
|
||||
"Radius": 0.07000000029802322,
|
||||
"Density": 0.75,
|
||||
"Thickness": 0.00800000037997961,
|
||||
"Curl": 0.8500000238418579,
|
||||
"Seed": 11,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _load_translation() -> None:
|
||||
spec = importlib.util.spec_from_file_location("vg_instance_attributes_translation", TRANSLATION)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load translation script: {TRANSLATION}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
def _socket_by_name(sockets, name: str):
|
||||
for socket in sockets:
|
||||
if socket.name == name:
|
||||
return socket
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def _new_geometry_group(name: str) -> bpy.types.GeometryNodeTree:
|
||||
if name in bpy.data.node_groups:
|
||||
bpy.data.node_groups.remove(bpy.data.node_groups[name])
|
||||
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
|
||||
group.is_modifier = True
|
||||
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
return group
|
||||
|
||||
|
||||
def _build_wrapper(name: str, source_group: bpy.types.GeometryNodeTree, inputs: dict[str, object]):
|
||||
wrapper = _new_geometry_group(name)
|
||||
nodes = wrapper.nodes
|
||||
links = wrapper.links
|
||||
|
||||
group_node = nodes.new("GeometryNodeGroup")
|
||||
group_node.node_tree = source_group
|
||||
for socket in group_node.inputs:
|
||||
if socket.name in inputs:
|
||||
socket.default_value = inputs[socket.name]
|
||||
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
links.new(_socket_by_name(group_node.outputs, "Geometry"), _socket_by_name(group_output.inputs, "Geometry"))
|
||||
return wrapper
|
||||
|
||||
|
||||
def _evaluated_mesh_summary(group: bpy.types.GeometryNodeTree):
|
||||
mesh = bpy.data.meshes.new(group.name + " Input Mesh")
|
||||
obj = bpy.data.objects.new(group.name + " Object", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new(group.name + " Modifier", "NODES")
|
||||
modifier.node_group = group
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated = obj.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
vertices = [tuple(vertex.co) for vertex in evaluated_mesh.vertices]
|
||||
edges = [tuple(edge.vertices) for edge in evaluated_mesh.edges]
|
||||
polygons = [tuple(polygon.vertices) for polygon in evaluated_mesh.polygons]
|
||||
return {
|
||||
"vertices": vertices,
|
||||
"edges": edges,
|
||||
"polygons": polygons,
|
||||
"polygon_smooth": [polygon.use_smooth for polygon in evaluated_mesh.polygons],
|
||||
}
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def _max_sorted_vertex_delta(source_vertices, translated_vertices) -> float:
|
||||
if len(source_vertices) != len(translated_vertices):
|
||||
return math.inf
|
||||
return max(
|
||||
(math.dist(a, b) for a, b in zip(sorted(source_vertices), sorted(translated_vertices))),
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_load_translation()
|
||||
results = []
|
||||
for case_name, inputs in CASES.items():
|
||||
source = _evaluated_mesh_summary(
|
||||
_build_wrapper(f"VG Verify Source Grass Tuft {case_name}", bpy.data.node_groups["Grass Tuft Generator"], inputs)
|
||||
)
|
||||
translated = _evaluated_mesh_summary(
|
||||
_build_wrapper(
|
||||
f"VG Verify Translated Grass Tuft {case_name}",
|
||||
bpy.data.node_groups["VG Grass Tuft Generator"],
|
||||
inputs,
|
||||
)
|
||||
)
|
||||
max_delta = _max_sorted_vertex_delta(source["vertices"], translated["vertices"])
|
||||
result = {
|
||||
"case": f"grass_tuft_generator_{case_name}",
|
||||
"source_vertex_count": len(source["vertices"]),
|
||||
"translated_vertex_count": len(translated["vertices"]),
|
||||
"source_edge_count": len(source["edges"]),
|
||||
"translated_edge_count": len(translated["edges"]),
|
||||
"source_polygon_count": len(source["polygons"]),
|
||||
"translated_polygon_count": len(translated["polygons"]),
|
||||
"source_smooth_faces": sum(source["polygon_smooth"]),
|
||||
"translated_smooth_faces": sum(translated["polygon_smooth"]),
|
||||
"max_sorted_vertex_delta": max_delta,
|
||||
}
|
||||
result["ok"] = (
|
||||
result["source_vertex_count"] == result["translated_vertex_count"]
|
||||
and result["source_edge_count"] == result["translated_edge_count"]
|
||||
and result["source_polygon_count"] == result["translated_polygon_count"]
|
||||
and source["polygon_smooth"] == translated["polygon_smooth"]
|
||||
and max_delta <= 1e-6
|
||||
)
|
||||
results.append(result)
|
||||
payload = {"ok": all(result["ok"] for result in results), "results": results}
|
||||
print("VG_INSTANCE_ATTRIBUTES_TRANSLATION_BEHAVIOR " + json.dumps(payload, sort_keys=True))
|
||||
return 0 if payload["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||