207 lines
8.4 KiB
Markdown
207 lines
8.4 KiB
Markdown
# 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
|
|
|
|
CultGeometry CSG is the lean Rust CSG 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 gamecult_geometry_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/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",
|
|
gamecult_geometry_csg::DomeCapZSpec {
|
|
center: Vec3::ZERO,
|
|
radius: 20.0,
|
|
height: 8.0,
|
|
rings: 12,
|
|
segments: 64,
|
|
material: MaterialId(20),
|
|
},
|
|
);
|
|
level.floret_arm(
|
|
"radiator petal",
|
|
gamecult_geometry_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/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
|
|
|
|
CultGeometry 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.
|