Add convex CSG brush splitting
This commit is contained in:
parent
379484f9fd
commit
7cb637edd2
7 changed files with 560 additions and 39 deletions
|
|
@ -17,6 +17,24 @@ The public C# layer still exposes the parts worth stealing cleanly:
|
|||
- 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.
|
||||
|
|
@ -49,10 +67,20 @@ let output = level.assemble();
|
|||
assert!(output.mesh.triangle_count() > 12);
|
||||
```
|
||||
|
||||
The first actual CSG operator is exact AABB subtraction against additive AABB
|
||||
solids. A cutter splits a source box into up to six surviving slabs. This is
|
||||
enough for doors, windows, vents, corridor punches, terraces, anchor trenches,
|
||||
and level blockout loops.
|
||||
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, outside fragments are
|
||||
emitted, and the inside remainder is discarded after the final plane. This
|
||||
supports angled box cuts and gives us the correct shape for a future general
|
||||
convex brush kernel.
|
||||
|
||||
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.
|
||||
|
||||
Procedural non-CSG primitives are additive:
|
||||
|
||||
|
|
@ -117,18 +145,22 @@ or sweep than by carving:
|
|||
- flower-like utility crests and other habitat-specific silhouettes
|
||||
|
||||
The immediate next upgrade is not "more primitives." It is a stronger convex
|
||||
brush kernel: plane classification, polygon clipping, fragment merge policy,
|
||||
and a BVH or spatial hash so cutter cost does not scale like a punishment.
|
||||
brush kernel: plane classification, polygon clipping, category routing,
|
||||
fragment merge policy, and a BVH or spatial hash so cutter cost does not scale
|
||||
like a punishment.
|
||||
|
||||
## Limits
|
||||
|
||||
`vg_csg` is not yet a full RealtimeCSG replacement.
|
||||
|
||||
- only box subtractors affect box solids
|
||||
- subtractive convex support currently covers axis-aligned and oriented boxes
|
||||
- non-box subtractors are reported and ignored
|
||||
- intersect brushes are reported and ignored
|
||||
- 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
|
||||
|
||||
Those limits are explicit because invisible ambition is how you get haunted by
|
||||
your own abstractions. The useful loop exists now: script brushes, assemble,
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ fn main() {
|
|||
|
||||
let output = level.assemble();
|
||||
println!(
|
||||
"brushes={} box_fragments={} vertices={} triangles={} warnings={}",
|
||||
"brushes={} convex_fragments={} vertices={} triangles={} warnings={}",
|
||||
output.report.input_brushes,
|
||||
output.report.emitted_box_fragments,
|
||||
output.report.emitted_convex_fragments,
|
||||
output.mesh.vertex_count(),
|
||||
output.mesh.triangle_count(),
|
||||
output.report.warnings.len()
|
||||
|
|
|
|||
136
src/assembler.rs
136
src/assembler.rs
|
|
@ -1,8 +1,8 @@
|
|||
use bevy_math::Vec3;
|
||||
|
||||
use crate::{
|
||||
Aabb, Brush, BrushId, BrushOp, MaterialId, Primitive, TriangleMesh, append_cylinder_z,
|
||||
append_dome_cap_z, append_floret_arm,
|
||||
Aabb, Brush, BrushId, BrushOp, ConvexSolid, MaterialId, Primitive, TriangleMesh,
|
||||
append_cylinder_z, append_dome_cap_z, append_floret_arm,
|
||||
primitives::{DomeCapZSpec, FloretArmSpec},
|
||||
};
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ pub enum BuildWarning {
|
|||
#[derive(Clone, Debug, Default)]
|
||||
pub struct BuildReport {
|
||||
pub input_brushes: usize,
|
||||
pub emitted_box_fragments: usize,
|
||||
pub emitted_convex_fragments: usize,
|
||||
pub warnings: Vec<BuildWarning>,
|
||||
}
|
||||
|
||||
|
|
@ -26,12 +26,6 @@ pub struct BuildOutput {
|
|||
pub generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BoxSolid {
|
||||
bounds: Aabb,
|
||||
material: MaterialId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Assembler {
|
||||
brushes: Vec<Brush>,
|
||||
|
|
@ -85,21 +79,71 @@ impl Assembler {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn solid_oriented_box(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: bevy_math::Quat,
|
||||
material: MaterialId,
|
||||
) -> BrushId {
|
||||
self.add_brush(
|
||||
name,
|
||||
BrushOp::Add,
|
||||
Primitive::OrientedBox {
|
||||
center,
|
||||
size,
|
||||
rotation,
|
||||
},
|
||||
material,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn cut_oriented_box(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: bevy_math::Quat,
|
||||
) -> BrushId {
|
||||
self.add_brush(
|
||||
name,
|
||||
BrushOp::Subtract,
|
||||
Primitive::OrientedBox {
|
||||
center,
|
||||
size,
|
||||
rotation,
|
||||
},
|
||||
MaterialId::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build(&self) -> BuildOutput {
|
||||
let mut report = BuildReport {
|
||||
input_brushes: self.brushes.len(),
|
||||
..BuildReport::default()
|
||||
};
|
||||
let mut mesh = TriangleMesh::new();
|
||||
let mut boxes: Vec<BoxSolid> = Vec::new();
|
||||
let mut solids: Vec<ConvexSolid> = Vec::new();
|
||||
|
||||
for brush in &self.brushes {
|
||||
match brush.op {
|
||||
BrushOp::Add => match &brush.primitive {
|
||||
Primitive::Box { bounds } => boxes.push(BoxSolid {
|
||||
bounds: *bounds,
|
||||
material: brush.material,
|
||||
}),
|
||||
Primitive::Box { bounds } => {
|
||||
solids.push(ConvexSolid::from_aabb(*bounds, brush.material));
|
||||
}
|
||||
Primitive::OrientedBox {
|
||||
center,
|
||||
size,
|
||||
rotation,
|
||||
} => {
|
||||
solids.push(ConvexSolid::box_from_center_size(
|
||||
*center,
|
||||
*size,
|
||||
*rotation,
|
||||
brush.material,
|
||||
));
|
||||
}
|
||||
Primitive::CylinderZ {
|
||||
center,
|
||||
radius,
|
||||
|
|
@ -153,8 +197,8 @@ impl Assembler {
|
|||
),
|
||||
},
|
||||
BrushOp::Subtract => {
|
||||
if let Some(cutter) = brush.as_box() {
|
||||
boxes = subtract_from_boxes(&boxes, cutter);
|
||||
if let Some(cutter) = convex_cutter_for_brush(brush) {
|
||||
solids = subtract_from_solids(&solids, &cutter);
|
||||
} else {
|
||||
report
|
||||
.warnings
|
||||
|
|
@ -171,9 +215,9 @@ impl Assembler {
|
|||
}
|
||||
}
|
||||
|
||||
report.emitted_box_fragments = boxes.len();
|
||||
for solid in boxes {
|
||||
mesh.append_box(solid.bounds, solid.material);
|
||||
report.emitted_convex_fragments = solids.len();
|
||||
for solid in solids {
|
||||
solid.append_to_mesh(&mut mesh);
|
||||
}
|
||||
|
||||
BuildOutput {
|
||||
|
|
@ -205,15 +249,27 @@ impl Assembler {
|
|||
}
|
||||
}
|
||||
|
||||
fn subtract_from_boxes(boxes: &[BoxSolid], cutter: Aabb) -> Vec<BoxSolid> {
|
||||
let mut out = Vec::with_capacity(boxes.len() + 4);
|
||||
for solid in boxes {
|
||||
for bounds in solid.bounds.subtract_box(cutter) {
|
||||
out.push(BoxSolid {
|
||||
bounds,
|
||||
material: solid.material,
|
||||
});
|
||||
}
|
||||
fn convex_cutter_for_brush(brush: &Brush) -> Option<ConvexSolid> {
|
||||
match &brush.primitive {
|
||||
Primitive::Box { bounds } => Some(ConvexSolid::from_aabb(*bounds, brush.material)),
|
||||
Primitive::OrientedBox {
|
||||
center,
|
||||
size,
|
||||
rotation,
|
||||
} => Some(ConvexSolid::box_from_center_size(
|
||||
*center,
|
||||
*size,
|
||||
*rotation,
|
||||
brush.material,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn subtract_from_solids(solids: &[ConvexSolid], cutter: &ConvexSolid) -> Vec<ConvexSolid> {
|
||||
let mut out = Vec::with_capacity(solids.len() + 4);
|
||||
for solid in solids {
|
||||
out.extend(solid.subtract_convex(cutter));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -240,7 +296,7 @@ mod tests {
|
|||
let output = Assembler::sample_room_with_door().build();
|
||||
let cutter = Aabb::from_center_size(Vec3::new(0.0, 4.0, 1.0), Vec3::new(1.2, 0.5, 2.0));
|
||||
|
||||
assert!(output.report.emitted_box_fragments > 2);
|
||||
assert!(output.report.emitted_convex_fragments > 2);
|
||||
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]
|
||||
|
|
@ -250,6 +306,28 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_door_cut_produces_diagonal_portal_faces() {
|
||||
let mut asm = Assembler::new();
|
||||
asm.solid_box(
|
||||
"wall",
|
||||
Aabb::from_center_size(Vec3::new(0.0, 0.0, 1.5), Vec3::new(5.0, 0.5, 3.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
asm.cut_oriented_box(
|
||||
"angled void",
|
||||
Vec3::new(0.0, 0.0, 1.5),
|
||||
Vec3::new(1.5, 2.0, 2.0),
|
||||
bevy_math::Quat::from_rotation_z(std::f32::consts::FRAC_PI_4),
|
||||
);
|
||||
|
||||
let output = asm.build();
|
||||
assert!(output.report.emitted_convex_fragments > 2);
|
||||
assert!(output.mesh.normals.iter().any(|normal| {
|
||||
normal.x.abs() > 0.2 && normal.y.abs() > 0.2 && normal.z.abs() < 0.1
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_box_emits_twelve_triangles() {
|
||||
let mut mesh = TriangleMesh::new();
|
||||
|
|
|
|||
|
|
@ -149,6 +149,11 @@ pub enum Primitive {
|
|||
Box {
|
||||
bounds: Aabb,
|
||||
},
|
||||
OrientedBox {
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
},
|
||||
CylinderZ {
|
||||
center: Vec3,
|
||||
radius: f32,
|
||||
|
|
|
|||
381
src/convex.rs
Normal file
381
src/convex.rs
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
use bevy_math::{Quat, Vec2, Vec3};
|
||||
|
||||
use crate::{Aabb, MaterialId, TriangleMesh};
|
||||
|
||||
const EPSILON: f32 = 1.0e-5;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Plane {
|
||||
pub normal: Vec3,
|
||||
pub distance: f32,
|
||||
}
|
||||
|
||||
impl Plane {
|
||||
pub fn from_point_normal(point: Vec3, normal: Vec3) -> Self {
|
||||
let normal = normal.normalize_or_zero();
|
||||
Self {
|
||||
normal,
|
||||
distance: -normal.dot(point),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signed_distance(self, point: Vec3) -> f32 {
|
||||
self.normal.dot(point) + self.distance
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConvexPolygon {
|
||||
pub vertices: Vec<Vec3>,
|
||||
pub normal: Vec3,
|
||||
pub material: MaterialId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConvexSolid {
|
||||
pub polygons: Vec<ConvexPolygon>,
|
||||
pub clip_planes: Vec<Plane>,
|
||||
pub material: MaterialId,
|
||||
}
|
||||
|
||||
impl ConvexSolid {
|
||||
pub fn from_aabb(bounds: Aabb, material: MaterialId) -> Self {
|
||||
Self::box_from_center_size(bounds.center(), bounds.size(), Quat::IDENTITY, material)
|
||||
}
|
||||
|
||||
pub fn box_from_center_size(
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
material: MaterialId,
|
||||
) -> Self {
|
||||
let half = size * 0.5;
|
||||
let local_corners = [
|
||||
Vec3::new(-half.x, -half.y, -half.z),
|
||||
Vec3::new(half.x, -half.y, -half.z),
|
||||
Vec3::new(half.x, half.y, -half.z),
|
||||
Vec3::new(-half.x, half.y, -half.z),
|
||||
Vec3::new(-half.x, -half.y, half.z),
|
||||
Vec3::new(half.x, -half.y, half.z),
|
||||
Vec3::new(half.x, half.y, half.z),
|
||||
Vec3::new(-half.x, half.y, half.z),
|
||||
];
|
||||
let corners = local_corners.map(|point| center + rotation * point);
|
||||
let faces = [
|
||||
([0, 3, 2, 1], Vec3::NEG_Z),
|
||||
([4, 5, 6, 7], Vec3::Z),
|
||||
([0, 1, 5, 4], Vec3::NEG_Y),
|
||||
([3, 7, 6, 2], Vec3::Y),
|
||||
([0, 4, 7, 3], Vec3::NEG_X),
|
||||
([1, 2, 6, 5], Vec3::X),
|
||||
];
|
||||
|
||||
let mut polygons = Vec::with_capacity(6);
|
||||
let mut clip_planes = Vec::with_capacity(6);
|
||||
for (indices, local_normal) in faces {
|
||||
let normal = rotation * local_normal;
|
||||
let vertices = indices.map(|index| corners[index]).to_vec();
|
||||
clip_planes.push(Plane::from_point_normal(vertices[0], normal));
|
||||
polygons.push(ConvexPolygon {
|
||||
vertices,
|
||||
normal,
|
||||
material,
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
polygons,
|
||||
clip_planes,
|
||||
material,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subtract_convex(&self, cutter: &Self) -> Vec<Self> {
|
||||
let mut fragments = Vec::new();
|
||||
let mut remainder = Some(self.clone());
|
||||
|
||||
for plane in &cutter.clip_planes {
|
||||
let Some(current) = remainder.take() else {
|
||||
break;
|
||||
};
|
||||
|
||||
match current.split(*plane) {
|
||||
SplitResult::Front(front) => {
|
||||
fragments.push(front);
|
||||
break;
|
||||
}
|
||||
SplitResult::Back(back) => {
|
||||
remainder = Some(back);
|
||||
}
|
||||
SplitResult::Both { front, back } => {
|
||||
fragments.push(front);
|
||||
remainder = Some(back);
|
||||
}
|
||||
SplitResult::Coplanar(solid) => {
|
||||
remainder = Some(solid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragments
|
||||
}
|
||||
|
||||
pub fn append_to_mesh(&self, mesh: &mut TriangleMesh) {
|
||||
for polygon in &self.polygons {
|
||||
if polygon.vertices.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let base = polygon.vertices[0];
|
||||
for i in 1..polygon.vertices.len() - 1 {
|
||||
mesh.append_triangle(
|
||||
[base, polygon.vertices[i], polygon.vertices[i + 1]],
|
||||
polygon.normal,
|
||||
[Vec2::ZERO, Vec2::X, Vec2::Y],
|
||||
polygon.material,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split(&self, plane: Plane) -> SplitResult {
|
||||
let mut saw_front = false;
|
||||
let mut saw_back = false;
|
||||
for polygon in &self.polygons {
|
||||
for vertex in &polygon.vertices {
|
||||
let distance = plane.signed_distance(*vertex);
|
||||
saw_front |= distance > EPSILON;
|
||||
saw_back |= distance < -EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
match (saw_front, saw_back) {
|
||||
(true, false) => return SplitResult::Front(self.clone()),
|
||||
(false, true) => return SplitResult::Back(self.clone()),
|
||||
(false, false) => return SplitResult::Coplanar(self.clone()),
|
||||
(true, true) => {}
|
||||
}
|
||||
|
||||
let mut front_polygons = Vec::new();
|
||||
let mut back_polygons = Vec::new();
|
||||
let mut cap_points = Vec::new();
|
||||
|
||||
for polygon in &self.polygons {
|
||||
let front = clip_polygon(&polygon.vertices, plane, KeepSide::Front, &mut cap_points);
|
||||
if front.len() >= 3 {
|
||||
front_polygons.push(ConvexPolygon {
|
||||
normal: polygon.normal,
|
||||
vertices: front,
|
||||
material: polygon.material,
|
||||
});
|
||||
}
|
||||
|
||||
let back = clip_polygon(&polygon.vertices, plane, KeepSide::Back, &mut cap_points);
|
||||
if back.len() >= 3 {
|
||||
back_polygons.push(ConvexPolygon {
|
||||
normal: polygon.normal,
|
||||
vertices: back,
|
||||
material: polygon.material,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let cap_points = unique_points_on_plane(cap_points);
|
||||
if cap_points.len() >= 3 {
|
||||
front_polygons.push(make_cap_polygon(&cap_points, -plane.normal, self.material));
|
||||
back_polygons.push(make_cap_polygon(&cap_points, plane.normal, self.material));
|
||||
}
|
||||
|
||||
SplitResult::Both {
|
||||
front: Self {
|
||||
clip_planes: planes_from_polygons(&front_polygons),
|
||||
polygons: front_polygons,
|
||||
material: self.material,
|
||||
},
|
||||
back: Self {
|
||||
clip_planes: planes_from_polygons(&back_polygons),
|
||||
polygons: back_polygons,
|
||||
material: self.material,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SplitResult {
|
||||
Front(ConvexSolid),
|
||||
Back(ConvexSolid),
|
||||
Both {
|
||||
front: ConvexSolid,
|
||||
back: ConvexSolid,
|
||||
},
|
||||
Coplanar(ConvexSolid),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum KeepSide {
|
||||
Front,
|
||||
Back,
|
||||
}
|
||||
|
||||
fn clip_polygon(
|
||||
vertices: &[Vec3],
|
||||
plane: Plane,
|
||||
keep: KeepSide,
|
||||
cap_points: &mut Vec<Vec3>,
|
||||
) -> Vec<Vec3> {
|
||||
let mut output = Vec::new();
|
||||
if vertices.is_empty() {
|
||||
return output;
|
||||
}
|
||||
|
||||
let mut previous = *vertices.last().expect("checked non-empty");
|
||||
let mut previous_distance = plane.signed_distance(previous);
|
||||
let mut previous_inside = inside(previous_distance, keep);
|
||||
|
||||
for ¤t in vertices {
|
||||
let current_distance = plane.signed_distance(current);
|
||||
let current_inside = inside(current_distance, keep);
|
||||
|
||||
if current_inside != previous_inside {
|
||||
let t = previous_distance / (previous_distance - current_distance);
|
||||
let intersection = previous + (current - previous) * t;
|
||||
output.push(intersection);
|
||||
cap_points.push(intersection);
|
||||
}
|
||||
|
||||
if current_inside {
|
||||
output.push(current);
|
||||
}
|
||||
|
||||
previous = current;
|
||||
previous_distance = current_distance;
|
||||
previous_inside = current_inside;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn inside(distance: f32, keep: KeepSide) -> bool {
|
||||
match keep {
|
||||
KeepSide::Front => distance >= -EPSILON,
|
||||
KeepSide::Back => distance <= EPSILON,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_cap_polygon(points: &[Vec3], normal: Vec3, material: MaterialId) -> ConvexPolygon {
|
||||
let center = points.iter().copied().sum::<Vec3>() / points.len() as f32;
|
||||
let axis = if normal.z.abs() < 0.9 {
|
||||
Vec3::Z
|
||||
} else {
|
||||
Vec3::Y
|
||||
};
|
||||
let tangent = normal.cross(axis).normalize_or_zero();
|
||||
let bitangent = normal.cross(tangent).normalize_or_zero();
|
||||
let mut vertices = points.to_vec();
|
||||
|
||||
vertices.sort_by(|a, b| {
|
||||
let da = *a - center;
|
||||
let db = *b - center;
|
||||
let aa = da.dot(bitangent).atan2(da.dot(tangent));
|
||||
let ab = db.dot(bitangent).atan2(db.dot(tangent));
|
||||
aa.total_cmp(&ab)
|
||||
});
|
||||
|
||||
if polygon_normal(&vertices).dot(normal) < 0.0 {
|
||||
vertices.reverse();
|
||||
}
|
||||
|
||||
ConvexPolygon {
|
||||
vertices,
|
||||
normal,
|
||||
material,
|
||||
}
|
||||
}
|
||||
|
||||
fn unique_points_on_plane(points: Vec<Vec3>) -> Vec<Vec3> {
|
||||
let mut unique = Vec::new();
|
||||
'outer: for point in points {
|
||||
for seen in &unique {
|
||||
let delta: Vec3 = point - *seen;
|
||||
if delta.length_squared() < EPSILON * EPSILON {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
unique.push(point);
|
||||
}
|
||||
unique
|
||||
}
|
||||
|
||||
fn planes_from_polygons(polygons: &[ConvexPolygon]) -> Vec<Plane> {
|
||||
polygons
|
||||
.iter()
|
||||
.filter_map(|polygon| {
|
||||
polygon
|
||||
.vertices
|
||||
.first()
|
||||
.map(|point| Plane::from_point_normal(*point, polygon.normal))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn polygon_normal(vertices: &[Vec3]) -> Vec3 {
|
||||
if vertices.len() < 3 {
|
||||
return Vec3::ZERO;
|
||||
}
|
||||
(vertices[1] - vertices[0])
|
||||
.cross(vertices[2] - vertices[0])
|
||||
.normalize_or_zero()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn subtract_center_box_emits_six_convex_fragments() {
|
||||
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 fragments = source.subtract_convex(&cutter);
|
||||
assert_eq!(fragments.len(), 6);
|
||||
assert!(
|
||||
fragments
|
||||
.iter()
|
||||
.all(|fragment| !fragment.polygons.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtract_rotated_box_creates_non_axis_aligned_faces() {
|
||||
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::new(2.0, 5.0, 2.0),
|
||||
Quat::from_rotation_z(std::f32::consts::FRAC_PI_4),
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let fragments = source.subtract_convex(&cutter);
|
||||
assert!(fragments.len() > 2);
|
||||
assert!(fragments.iter().any(|fragment| {
|
||||
fragment
|
||||
.polygons
|
||||
.iter()
|
||||
.any(|polygon| polygon.normal.x.abs() > 0.01 && polygon.normal.y.abs() > 0.01)
|
||||
}));
|
||||
}
|
||||
}
|
||||
25
src/dsl.rs
25
src/dsl.rs
|
|
@ -1,4 +1,4 @@
|
|||
use bevy_math::Vec3;
|
||||
use bevy_math::{Quat, Vec3};
|
||||
|
||||
use crate::{
|
||||
Aabb, Assembler, BrushId, BrushOp, BuildOutput, DomeCapZSpec, FloretArmSpec, MaterialId,
|
||||
|
|
@ -31,6 +31,29 @@ impl LevelDsl {
|
|||
.cut_box(name, Aabb::from_center_size(center, size))
|
||||
}
|
||||
|
||||
pub fn solid_oriented_box(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
material: MaterialId,
|
||||
) -> BrushId {
|
||||
self.assembler
|
||||
.solid_oriented_box(name, center, size, rotation, material)
|
||||
}
|
||||
|
||||
pub fn cut_oriented_box(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
) -> BrushId {
|
||||
self.assembler
|
||||
.cut_oriented_box(name, center, size, rotation)
|
||||
}
|
||||
|
||||
pub fn cylinder_z(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@
|
|||
|
||||
mod assembler;
|
||||
mod brush;
|
||||
mod convex;
|
||||
mod dsl;
|
||||
mod mesh;
|
||||
mod primitives;
|
||||
|
||||
pub use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning};
|
||||
pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, Primitive};
|
||||
pub use convex::{ConvexPolygon, ConvexSolid, Plane};
|
||||
pub use dsl::LevelDsl;
|
||||
pub use mesh::TriangleMesh;
|
||||
pub use primitives::{
|
||||
|
|
|
|||
Loading…
Reference in a new issue