Add experimental routed CSG surfaces
This commit is contained in:
parent
6bb8726a37
commit
053b244295
7 changed files with 431 additions and 34 deletions
|
|
@ -41,6 +41,7 @@ fn main() {
|
|||
for case in cases {
|
||||
run_case(case, PerfMode::Stable);
|
||||
run_case(case, PerfMode::Dirty);
|
||||
run_case(case, PerfMode::Routed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ struct PerfCase {
|
|||
enum PerfMode {
|
||||
Stable,
|
||||
Dirty,
|
||||
Routed,
|
||||
}
|
||||
|
||||
impl PerfMode {
|
||||
|
|
@ -62,6 +64,7 @@ impl PerfMode {
|
|||
match self {
|
||||
Self::Stable => "stable",
|
||||
Self::Dirty => "dirty",
|
||||
Self::Routed => "routed",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,12 +72,16 @@ impl PerfMode {
|
|||
match self {
|
||||
Self::Stable => assembler.build(),
|
||||
Self::Dirty => assembler.rebuild(),
|
||||
Self::Routed => assembler.rebuild_routed_surfaces(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_case(case: PerfCase, mode: PerfMode) {
|
||||
let assembler = (case.build)();
|
||||
if matches!(mode, PerfMode::Routed) && !assembler.supports_routed_surfaces() {
|
||||
return;
|
||||
}
|
||||
let warmup = mode.build(&assembler);
|
||||
black_box(warmup.mesh.triangle_count());
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::cell::RefCell;
|
|||
use bevy_math::Vec3;
|
||||
|
||||
use crate::{
|
||||
Aabb, Brush, BrushId, BrushOp, ConvexSolid, MaterialId, Primitive, TriangleMesh,
|
||||
Aabb, Brush, BrushId, BrushOp, ConvexPolygon, ConvexSolid, MaterialId, Primitive, TriangleMesh,
|
||||
append_cylinder_z, append_dome_cap_z, append_floret_arm,
|
||||
primitives::{DomeCapZSpec, FloretArmSpec},
|
||||
};
|
||||
|
|
@ -183,6 +183,111 @@ impl Assembler {
|
|||
self.build_uncached()
|
||||
}
|
||||
|
||||
pub fn rebuild_routed_surfaces(&self) -> BuildOutput {
|
||||
self.try_build_routed_surfaces()
|
||||
.unwrap_or_else(|| self.build_uncached())
|
||||
}
|
||||
|
||||
pub fn supports_routed_surfaces(&self) -> bool {
|
||||
self.supports_routed_surface_subset()
|
||||
}
|
||||
|
||||
fn try_build_routed_surfaces(&self) -> Option<BuildOutput> {
|
||||
if !self.supports_routed_surface_subset() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut report = BuildReport {
|
||||
input_brushes: self.brushes.len(),
|
||||
..BuildReport::default()
|
||||
};
|
||||
let mut source = None::<ConvexSolid>;
|
||||
let mut surfaces = Vec::<ConvexPolygon>::new();
|
||||
let mut previous_cutters = Vec::<ConvexSolid>::new();
|
||||
|
||||
for brush in &self.compiled {
|
||||
match brush.op {
|
||||
BrushOp::Add => {
|
||||
if source.is_some() {
|
||||
return None;
|
||||
}
|
||||
let solid = brush.convex_cutter()?;
|
||||
surfaces = solid.polygons.clone();
|
||||
source = Some(solid);
|
||||
}
|
||||
BrushOp::Subtract => {
|
||||
report.operator_brushes += 1;
|
||||
let source_solid = source.as_ref()?;
|
||||
if !source_solid.bounds.intersects(brush.bounds) {
|
||||
report.rejected_pairs += 1;
|
||||
continue;
|
||||
}
|
||||
let cutter = brush.convex_cutter()?;
|
||||
report.candidate_pairs += 1;
|
||||
surfaces = ConvexSolid::route_polygons_outside_of(surfaces, &cutter);
|
||||
|
||||
let mut caps = ConvexSolid::route_polygons_inside_of(
|
||||
cutter.polygons.clone(),
|
||||
source_solid,
|
||||
);
|
||||
for previous in &previous_cutters {
|
||||
caps = ConvexSolid::route_polygons_outside_of(caps, previous);
|
||||
}
|
||||
for cap in &mut caps {
|
||||
cap.material = source_solid.material;
|
||||
cap.reversed = !cap.reversed;
|
||||
}
|
||||
surfaces.extend(caps);
|
||||
previous_cutters.push(cutter);
|
||||
}
|
||||
BrushOp::Intersect => return None,
|
||||
}
|
||||
}
|
||||
|
||||
let mut mesh = TriangleMesh::new();
|
||||
ConvexSolid::append_polygons_to_mesh(&surfaces, &mut mesh);
|
||||
report.emitted_convex_fragments = surfaces.len();
|
||||
|
||||
Some(BuildOutput {
|
||||
mesh,
|
||||
report,
|
||||
generation: self.generation,
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_routed_surface_subset(&self) -> bool {
|
||||
let mut source = None::<Aabb>;
|
||||
let mut candidate_subtracts = 0usize;
|
||||
|
||||
for brush in &self.compiled {
|
||||
match brush.op {
|
||||
BrushOp::Add => {
|
||||
if source.is_some() || brush.convex_cutter().is_none() {
|
||||
return false;
|
||||
}
|
||||
source = Some(brush.bounds);
|
||||
}
|
||||
BrushOp::Subtract => {
|
||||
let Some(source_bounds) = source else {
|
||||
return false;
|
||||
};
|
||||
if brush.convex_cutter().is_none() {
|
||||
return false;
|
||||
}
|
||||
if source_bounds.intersects(brush.bounds) {
|
||||
candidate_subtracts += 1;
|
||||
if candidate_subtracts > 1 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
BrushOp::Intersect => return false,
|
||||
}
|
||||
}
|
||||
|
||||
source.is_some() && candidate_subtracts == 1
|
||||
}
|
||||
|
||||
fn build_uncached(&self) -> BuildOutput {
|
||||
if self
|
||||
.compiled
|
||||
|
|
@ -691,4 +796,118 @@ mod tests {
|
|||
assert_eq!(cached.mesh.positions, stable.mesh.positions);
|
||||
assert_eq!(cached.mesh.indices, stable.mesh.indices);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_surface_rebuild_emits_clean_center_cut_boundary() {
|
||||
let mut asm = Assembler::new();
|
||||
let cutter = Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0));
|
||||
asm.solid_box(
|
||||
"source",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
asm.cut_box("void", cutter);
|
||||
|
||||
let routed = asm.rebuild_routed_surfaces();
|
||||
|
||||
assert_eq!(routed.report.operator_brushes, 1);
|
||||
assert_eq!(routed.report.candidate_pairs, 1);
|
||||
assert_eq!(routed.report.rejected_pairs, 0);
|
||||
assert_eq!(routed.report.emitted_convex_fragments, 24);
|
||||
assert!(routed.mesh.triangle_count() < asm.rebuild().mesh.triangle_count());
|
||||
for tri in routed.mesh.indices.chunks_exact(3) {
|
||||
let center = (routed.mesh.positions[tri[0] as usize]
|
||||
+ routed.mesh.positions[tri[1] as usize]
|
||||
+ routed.mesh.positions[tri[2] as usize])
|
||||
/ 3.0;
|
||||
assert!(!cutter.contains_point_strict(center, 1.0e-4));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_surface_rebuild_falls_back_for_intersections() {
|
||||
let mut asm = Assembler::new();
|
||||
asm.solid_box(
|
||||
"source",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
asm.add_brush(
|
||||
"common",
|
||||
BrushOp::Intersect,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::X, Vec3::splat(4.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
|
||||
let stable = asm.rebuild();
|
||||
let routed = asm.rebuild_routed_surfaces();
|
||||
|
||||
assert_eq!(
|
||||
routed.report.emitted_convex_fragments,
|
||||
stable.report.emitted_convex_fragments
|
||||
);
|
||||
assert_eq!(routed.mesh.triangle_count(), stable.mesh.triangle_count());
|
||||
assert_eq!(routed.mesh.positions, stable.mesh.positions);
|
||||
assert_eq!(routed.mesh.indices, stable.mesh.indices);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_surface_rebuild_falls_back_for_multiple_candidate_cutters() {
|
||||
let mut asm = Assembler::new();
|
||||
asm.solid_box(
|
||||
"source",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
asm.cut_oriented_box(
|
||||
"a",
|
||||
Vec3::new(-0.5, 0.0, 0.0),
|
||||
Vec3::new(1.0, 5.0, 5.0),
|
||||
bevy_math::Quat::from_rotation_z(0.25),
|
||||
);
|
||||
asm.cut_oriented_box(
|
||||
"b",
|
||||
Vec3::new(0.5, 0.0, 0.0),
|
||||
Vec3::new(1.0, 5.0, 5.0),
|
||||
bevy_math::Quat::from_rotation_z(-0.25),
|
||||
);
|
||||
|
||||
let stable = asm.rebuild();
|
||||
let routed = asm.rebuild_routed_surfaces();
|
||||
|
||||
assert_eq!(
|
||||
routed.report.emitted_convex_fragments,
|
||||
stable.report.emitted_convex_fragments
|
||||
);
|
||||
assert_eq!(routed.mesh.triangle_count(), stable.mesh.triangle_count());
|
||||
assert_eq!(routed.mesh.positions, stable.mesh.positions);
|
||||
assert_eq!(routed.mesh.indices, stable.mesh.indices);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_surface_rebuild_falls_back_when_no_cutter_touches_source() {
|
||||
let mut asm = Assembler::new();
|
||||
asm.solid_box(
|
||||
"source",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
asm.cut_box(
|
||||
"far",
|
||||
Aabb::from_center_size(Vec3::splat(20.0), Vec3::splat(2.0)),
|
||||
);
|
||||
|
||||
let stable = asm.rebuild();
|
||||
let routed = asm.rebuild_routed_surfaces();
|
||||
|
||||
assert_eq!(
|
||||
routed.report.emitted_convex_fragments,
|
||||
stable.report.emitted_convex_fragments
|
||||
);
|
||||
assert_eq!(routed.mesh.triangle_count(), stable.mesh.triangle_count());
|
||||
assert_eq!(routed.mesh.positions, stable.mesh.positions);
|
||||
assert_eq!(routed.mesh.indices, stable.mesh.indices);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -256,39 +256,75 @@ impl ConvexSolid {
|
|||
}
|
||||
|
||||
pub fn categorize_polygons_against(&self, cutter: &Self) -> CategorizedPolygons {
|
||||
let mut categorized = CategorizedPolygons::default();
|
||||
categorize_polygon_list_against(&self.polygons, cutter)
|
||||
}
|
||||
|
||||
for polygon in &self.polygons {
|
||||
let mut pieces = vec![CategorizationPiece {
|
||||
polygon: polygon.clone(),
|
||||
aligned_category: None,
|
||||
}];
|
||||
pub fn route_polygons_outside_of(
|
||||
polygons: Vec<ConvexPolygon>,
|
||||
cutter: &Self,
|
||||
) -> Vec<ConvexPolygon> {
|
||||
let categorized = categorize_polygon_list_against(&polygons, cutter);
|
||||
categorized.outside
|
||||
}
|
||||
|
||||
for plane in &cutter.clip_planes {
|
||||
let mut inside_pieces = Vec::new();
|
||||
for piece in pieces {
|
||||
classify_piece_against_plane(
|
||||
piece,
|
||||
*plane,
|
||||
&mut inside_pieces,
|
||||
&mut categorized,
|
||||
);
|
||||
}
|
||||
pieces = inside_pieces;
|
||||
}
|
||||
pub fn route_polygons_inside_of(
|
||||
polygons: Vec<ConvexPolygon>,
|
||||
cutter: &Self,
|
||||
) -> Vec<ConvexPolygon> {
|
||||
let categorized = categorize_polygon_list_against(&polygons, cutter);
|
||||
let mut inside = categorized.inside;
|
||||
inside.extend(categorized.aligned);
|
||||
inside.extend(categorized.reverse_aligned);
|
||||
inside
|
||||
}
|
||||
|
||||
for piece in pieces {
|
||||
match piece.aligned_category {
|
||||
Some(PolygonCategory::Aligned) => categorized.aligned.push(piece.polygon),
|
||||
Some(PolygonCategory::ReverseAligned) => {
|
||||
categorized.reverse_aligned.push(piece.polygon);
|
||||
}
|
||||
_ => categorized.inside.push(piece.polygon),
|
||||
}
|
||||
}
|
||||
pub fn subtract_convex_routed_polygons(&self, cutter: &Self) -> Vec<ConvexPolygon> {
|
||||
let mut polygons = Vec::new();
|
||||
let source = self.categorize_polygons_against(cutter);
|
||||
polygons.extend(source.outside);
|
||||
|
||||
let cutter_inside_source = cutter.categorize_polygons_against(self);
|
||||
for mut polygon in cutter_inside_source.inside {
|
||||
polygon.reversed = !polygon.reversed;
|
||||
polygons.push(polygon);
|
||||
}
|
||||
for mut polygon in cutter_inside_source.aligned {
|
||||
polygon.reversed = !polygon.reversed;
|
||||
polygons.push(polygon);
|
||||
}
|
||||
for mut polygon in cutter_inside_source.reverse_aligned {
|
||||
polygon.reversed = !polygon.reversed;
|
||||
polygons.push(polygon);
|
||||
}
|
||||
|
||||
categorized
|
||||
polygons
|
||||
}
|
||||
|
||||
pub fn append_polygons_to_mesh(polygons: &[ConvexPolygon], mesh: &mut TriangleMesh) {
|
||||
for polygon in polygons {
|
||||
if !polygon.visible || polygon.vertices.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let base = polygon.vertices[0];
|
||||
for i in 1..polygon.vertices.len() - 1 {
|
||||
let triangle = if polygon.reversed {
|
||||
[base, polygon.vertices[i + 1], polygon.vertices[i]]
|
||||
} else {
|
||||
[base, polygon.vertices[i], polygon.vertices[i + 1]]
|
||||
};
|
||||
let normal = if polygon.reversed {
|
||||
-polygon.normal
|
||||
} else {
|
||||
polygon.normal
|
||||
};
|
||||
mesh.append_triangle(
|
||||
triangle,
|
||||
normal,
|
||||
[Vec2::ZERO, Vec2::X, Vec2::Y],
|
||||
polygon.material,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_owned(self, plane: Plane) -> SplitResult {
|
||||
|
|
@ -348,6 +384,40 @@ impl ConvexSolid {
|
|||
}
|
||||
}
|
||||
|
||||
fn categorize_polygon_list_against(
|
||||
polygons: &[ConvexPolygon],
|
||||
cutter: &ConvexSolid,
|
||||
) -> CategorizedPolygons {
|
||||
let mut categorized = CategorizedPolygons::default();
|
||||
|
||||
for polygon in polygons {
|
||||
let mut pieces = vec![CategorizationPiece {
|
||||
polygon: polygon.clone(),
|
||||
aligned_category: None,
|
||||
}];
|
||||
|
||||
for plane in &cutter.clip_planes {
|
||||
let mut inside_pieces = Vec::new();
|
||||
for piece in pieces {
|
||||
classify_piece_against_plane(piece, *plane, &mut inside_pieces, &mut categorized);
|
||||
}
|
||||
pieces = inside_pieces;
|
||||
}
|
||||
|
||||
for piece in pieces {
|
||||
match piece.aligned_category {
|
||||
Some(PolygonCategory::Aligned) => categorized.aligned.push(piece.polygon),
|
||||
Some(PolygonCategory::ReverseAligned) => {
|
||||
categorized.reverse_aligned.push(piece.polygon);
|
||||
}
|
||||
_ => categorized.inside.push(piece.polygon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
categorized
|
||||
}
|
||||
|
||||
enum SplitResult {
|
||||
Front(ConvexSolid),
|
||||
Back(ConvexSolid),
|
||||
|
|
@ -731,4 +801,76 @@ mod tests {
|
|||
.any(|polygon| polygon.normal.x.abs() > 0.01 && polygon.normal.y.abs() > 0.01)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_subtraction_emits_source_shell_and_reversed_cutter_shell() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let routed = source.subtract_convex_routed_polygons(&cutter);
|
||||
|
||||
assert_eq!(routed.len(), 24);
|
||||
assert_eq!(
|
||||
routed
|
||||
.iter()
|
||||
.filter(|polygon| polygon.material == MaterialId(1))
|
||||
.count(),
|
||||
18
|
||||
);
|
||||
assert_eq!(
|
||||
routed
|
||||
.iter()
|
||||
.filter(|polygon| polygon.material == MaterialId(0))
|
||||
.count(),
|
||||
6
|
||||
);
|
||||
assert_eq!(
|
||||
routed
|
||||
.iter()
|
||||
.filter(|polygon| polygon.material == MaterialId(0) && polygon.reversed)
|
||||
.count(),
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_subtraction_clips_crossing_cutter_surfaces_to_source() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::new(1.5, 0.0, 0.0),
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let routed = source.subtract_convex_routed_polygons(&cutter);
|
||||
|
||||
assert!(!routed.is_empty());
|
||||
assert!(
|
||||
routed
|
||||
.iter()
|
||||
.flat_map(|polygon| polygon.vertices.iter())
|
||||
.all(|vertex| source.bounds.contains_point(*vertex))
|
||||
);
|
||||
assert!(
|
||||
routed
|
||||
.iter()
|
||||
.any(|polygon| polygon.material == MaterialId(0) && polygon.reversed)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,17 @@ The script builds `vg_csg` in release mode and writes JSONL to
|
|||
- emitted triangle, fragment, and warning counts
|
||||
- demand-frontier `candidate_pairs` and `rejected_pairs`
|
||||
|
||||
The `vg_csg` fixture measures the stable-tree path after warmup. `Assembler`
|
||||
caches evaluated output by generation, so repeated `build()` calls on an
|
||||
unchanged graph return the cached mesh. Dirty generations still rebuild.
|
||||
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
|
||||
|
||||
|
|
@ -91,6 +99,17 @@ new kernel path; they expose the ordered brush stream as affected source/operato
|
|||
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.
|
||||
|
||||
## Latest Local Baseline
|
||||
|
||||
Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp`:
|
||||
|
|
|
|||
|
|
@ -98,6 +98,15 @@ First implementation slice: `vg_csg` exports `DemandFrontier`/`DemandPair`, 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.
|
||||
|
||||
## Important Invariants
|
||||
|
||||
- Target Blender 5.0+ unless the task says otherwise.
|
||||
|
|
|
|||
|
|
@ -63,3 +63,4 @@
|
|||
{"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."}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ data_flow:
|
|||
|
||||
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 and dirty modes, mesh-array output, stable-generation output caching, tests, clippy, and examples. 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 path, cached compiled brush geometry, owned convex splitting, convex containment shortcuts, stable-tree output caching, and stable/dirty perf records; dirty box-heavy and distant-rejection paths are respectable, but dirty dense rotated subtraction still needs the category-router kernel. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes.
|
||||
next_action: Use the new demand-frontier counters to replace the ordered `vg_csg` dense rotated subtraction path with a category-router kernel; keep cached-vs-dirty output parity tests green, compare against the C++ timing oracle, and only add BVH/grid storage after frontier batching proves it needs one.
|
||||
next_action: Generalize the experimental `vg_csg` routed-surface path from the single-candidate subtract case into a compact dense category-router using frontier batches and scratch storage; keep cached-vs-dirty output parity tests green, compare against the C++ timing oracle, and only add BVH/grid storage after batching proves it needs one.
|
||||
remotes:
|
||||
vibegeometry: https://github.com/GameCult/VibeGeometry
|
||||
geometry_script_fork: https://github.com/GameCult/geometry-script
|
||||
|
|
|
|||
Loading…
Reference in a new issue