From e2888e8f2ed0aeda56f479a3c0ba8ad3654c675b Mon Sep 17 00:00:00 2001 From: Tristam MacDonald Date: Fri, 29 Jan 2021 16:29:44 +0100 Subject: [PATCH] Extended Marching Cubes and Dual Contouring - Bump the minor version as backwards compatibility is not maintained. --- .license_template | 13 + Cargo.toml | 8 +- README.md | 33 +- benches/isosurface.rs | 52 +- examples/common/mod.rs | 10 +- examples/common/sources.rs | 83 +-- examples/common/text.rs | 8 +- examples/deferred_rasterisation.rs | 51 +- examples/{torus.rs => sampler.rs} | 155 +++-- rustfmt.toml | 3 + src/distance.rs | 104 +++ src/dual_contouring.rs | 106 +++ src/extended_marching_cubes.rs | 187 +++++ src/extractor.rs | 127 ++++ src/feature/mod.rs | 149 ++++ src/feature/particle_minimisation.rs | 72 ++ src/feature/qef.rs | 69 ++ src/implicit/csg.rs | 168 +++++ src/implicit/cylinder.rs | 141 ++++ src/implicit/mod.rs | 24 + src/implicit/rectangular_prism.rs | 131 ++++ src/implicit/sphere.rs | 110 +++ src/implicit/torus.rs | 172 +++++ src/index_cache.rs | 115 ++-- src/lib.rs | 68 +- src/linear_hashed_marching_cubes.rs | 300 ++------ src/linear_hashed_octree.rs | 3 +- src/marching_cubes.rs | 184 ++--- src/marching_cubes_impl.rs | 116 +++- src/marching_cubes_tables.rs | 303 +++++++- src/math.rs | 138 ---- src/math/mod.rs | 27 + src/math/svd.rs | 986 +++++++++++++++++++++++++++ src/math/vector.rs | 357 ++++++++++ src/mesh.rs | 260 +++++++ src/morton.rs | 12 +- src/point_cloud.rs | 137 +--- src/sampler.rs | 65 ++ src/source.rs | 87 ++- src/traversal/dual_grid.rs | 96 +++ src/traversal/implicit_octree.rs | 100 +++ src/traversal/mod.rs | 20 + src/traversal/primal_grid.rs | 90 +++ 43 files changed, 4447 insertions(+), 993 deletions(-) create mode 100644 .license_template rename examples/{torus.rs => sampler.rs} (63%) create mode 100644 rustfmt.toml create mode 100644 src/distance.rs create mode 100644 src/dual_contouring.rs create mode 100644 src/extended_marching_cubes.rs create mode 100644 src/extractor.rs create mode 100644 src/feature/mod.rs create mode 100644 src/feature/particle_minimisation.rs create mode 100644 src/feature/qef.rs create mode 100644 src/implicit/csg.rs create mode 100644 src/implicit/cylinder.rs create mode 100644 src/implicit/mod.rs create mode 100644 src/implicit/rectangular_prism.rs create mode 100644 src/implicit/sphere.rs create mode 100644 src/implicit/torus.rs delete mode 100644 src/math.rs create mode 100644 src/math/mod.rs create mode 100644 src/math/svd.rs create mode 100644 src/math/vector.rs create mode 100644 src/mesh.rs create mode 100644 src/sampler.rs create mode 100644 src/traversal/dual_grid.rs create mode 100644 src/traversal/implicit_octree.rs create mode 100644 src/traversal/mod.rs create mode 100644 src/traversal/primal_grid.rs diff --git a/.license_template b/.license_template new file mode 100644 index 0000000..2a8f0b2 --- /dev/null +++ b/.license_template @@ -0,0 +1,13 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 380c685..4566ba2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" name = "isosurface" readme = "README.md" repository = "https://github.com/swiftcoder/isosurface" -version = "0.0.4" +version = "0.1.0-alpha.0" [dev-dependencies] cgmath = "^0.17" @@ -15,12 +15,12 @@ glium = "^0.26" glium_text_rusttype = "^0.3" [profile.dev] -opt-level = 2 +opt-level = 3 [profile.release] -opt-level = 3 lto = true +opt-level = 3 [[bench]] -name = "isosurface" harness = false +name = "isosurface" diff --git a/README.md b/README.md index afada51..98c5501 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,27 @@ # Isosurface -Isosurface extraction algorithms for Rust. Currently only a few techniques are implemented. +Isosurface extraction algorithms implemented in Rust. The classical Marching Cubes and Dual Contouring techniques are included, along with more modern variations on the theme. -This crate intentionally has no dependencies to keep the footprint of the library small. The examples rely on the `glium`, `glium_text_rusttype`, and `cgmath` crates. +In the interest of education, the documentation for each extraction algorithm links to the relevant academic papers. -# Marching Cubes -The Marching Cubes implementation produces perfectly indexed meshes with few duplicate vertices, through the use of a (fairly involved) index caching system. The complexity of the cache could no doubt be reduced through some clever arithmetic, but it is not currently a bottleneck. +## Example programs +`cargo run --example sampler` will execute the sampler, which allows you to compare a variety of algorithms and implicit surfaces. -The implementation has been optimised for performance, with memory use kept as a low as possible considering. For an NxNxN voxel chunk, it will allocate roughly NxN of f32 storage for isosurface values, and Nx(N+1) of u32 storage for the index cache. + `cargo run --example deferred_rasterisation` will execute a demonstration of GPU-side deferred rasterisation from point clouds. This is a technique pioneered by Gavan Woolery, of [Voxel Quest](https://www.voxelquest.com) fame. -Indices are 32-bit because for chunks of 32x32 and larger you'll typically end up with more than 65k vertices. If you are targeting a mobile platform that supports only 16-bit indices, you'll need to use smaller chunk sizes, and truncate on the output side. +## Dependencies +This library intentionally has no dependencies. While that requires some redevelopment of common code (i.e. the Vec3 type), it keeps the footprint of the library small, and compile times low for consuming crates. The examples do however rely on the `glium`, `glium_text_rusttype`, and `cgmath` crates, to avoid reinventing the world. -# Linear Hashed Marching Cubes -A very efficient algorithm using interleaved integer coordinates to represent octree cells, and storing them in a hash table. Results in better mesh quality than regular marching cubes, and is significantly faster. Memory usage is less predictable, but shouldn't be significantly higher than standard marching cubes. - -# Point Clouds and Deferred Rasterisation -Point cloud extraction is typically not all that useful, given that point clouds don't contain any data about the actual surface. However, Gavan Woolery (gavanw@) posted an interesting image of reconstructing surface data in image space on the GPU, so I've added a simple example of that. +## 32-bit indices +For simplicity vertex indices have been fixed at 32-bits, because for chunks of 32x32x32 and larger you'll often end up with more than 65k vertices. If you are targeting a mobile platform that supports only 16-bit indices, you'll need to keep to smaller chunk sizes, or split the mesh on the output side. -# Why are optimisations enabled in debug builds? -Without optimisations enabled, debug builds are 70x slower (1 minute to extract a 256^3 volume, versus ~800 milliseconds). +## Why are optimisations enabled in debug builds? +Without optimisations enabled, debug builds are around 70x slower. The implementation relies on a lot of nested for loops over integer ranges, and the range iterators themselves entirely dominate the CPU profiles in unoptimised builds. -The marching cubes implementation relies on a lot of nested for loops over integer ranges, and the range iterators themselves entirely dominate the CPU profiles in unoptimised builds. While this could likely be worked around by converting the `for 0..8` style of loop to a while loop with manual counter, that seems ugly and distinctly not in the spirit of rust. I'd rather leave optimisations enabled, and wait for the compiler to become better at handling iterators. +While this can be worked around by converting the `for 0..8` style of loop to a while loop with manual counter, the result is quite unpleasant, and distinctly not in the spirit of rust. I'd rather leave optimisations enabled, and wait for the compiler to become better at handling iterators in debug builds. + +If you take a dependency on this crate and run into the same issue, you can tell Cargo to compile just this one crate in release mode, by adding the following to your `Cargo.toml`: + +``` +[profile.dev.package.isosurface] +opt-level = 3 +``` \ No newline at end of file diff --git a/benches/isosurface.rs b/benches/isosurface.rs index 81305da..b67045f 100644 --- a/benches/isosurface.rs +++ b/benches/isosurface.rs @@ -1,41 +1,45 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use criterion::{criterion_group, criterion_main, Criterion}; use isosurface::{ - linear_hashed_marching_cubes::LinearHashedMarchingCubes, marching_cubes::MarchingCubes, - source::Source, + distance::Signed, extractor::IndexedVertices, implicit::Torus, sampler::Sampler, + LinearHashedMarchingCubes, MarchingCubes, }; -fn torus(x: f32, y: f32, z: f32) -> f32 { - const R1: f32 = 1.0 / 4.0; - const R2: f32 = 1.0 / 10.0; - let q_x = ((x * x + y * y).sqrt()).abs() - R1; - let len = (q_x * q_x + z * z).sqrt(); - len - R2 -} - -pub struct Torus {} - -impl Source for Torus { - fn sample(&self, x: f32, y: f32, z: f32) -> f32 { - torus(x - 0.5, y - 0.5, z - 0.5) - } -} - fn marching_cubes() { - let torus = Torus {}; + let torus = Torus::new(0.25, 0.1); + let sampler = Sampler::new(&torus); + let mut vertices = vec![]; let mut indices = vec![]; + let mut extractor = IndexedVertices::new(&mut vertices, &mut indices); - let mut marching_cubes = MarchingCubes::new(256); - marching_cubes.extract(&torus, &mut vertices, &mut indices); + let mut marching_cubes = MarchingCubes::::new(128); + marching_cubes.extract(&sampler, &mut extractor); } fn linear_hashed_marching_cubes() { - let torus = Torus {}; + let torus = Torus::new(0.25, 0.1); + let sampler = Sampler::new(&torus); + let mut vertices = vec![]; let mut indices = vec![]; + let mut extractor = IndexedVertices::new(&mut vertices, &mut indices); - let mut marching_cubes = LinearHashedMarchingCubes::new(8); - marching_cubes.extract(&torus, &mut vertices, &mut indices); + let mut marching_cubes = LinearHashedMarchingCubes::new(7); + marching_cubes.extract(&sampler, &mut extractor); } fn marching_cubes_benchmark(c: &mut Criterion) { diff --git a/examples/common/mod.rs b/examples/common/mod.rs index 87877ab..a954dd3 100644 --- a/examples/common/mod.rs +++ b/examples/common/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ pub mod text; use std::mem; use std::slice; -/// This is used to reinterpret slices of floats as slices of repr(C) structs, without any -/// copying. It is optimal, but it is also punching holes in the type system. I hope that Rust -/// provides safe functionality to handle this in the future. In the meantime, reproduce -/// this workaround at your own risk. +/// This is used to reinterpret slices of floats as slices of repr(C) structs, +/// without any copying. It is optimal, but it is also punching holes in the +/// type system. I hope that Rust provides safe functionality to handle this in +/// the future. In the meantime, reproduce this workaround at your own risk. pub fn reinterpret_cast_slice(input: &[S]) -> &[T] { let length_in_bytes = input.len() * mem::size_of::(); let desired_length = length_in_bytes / mem::size_of::(); diff --git a/examples/common/sources.rs b/examples/common/sources.rs index a3d8a14..0672af8 100644 --- a/examples/common/sources.rs +++ b/examples/common/sources.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,68 +13,45 @@ // limitations under the License. //! Isosurface definitions for use in multiple examples +use isosurface::{ + distance::{Directed, Signed}, + math::Vec3, + source::{HermiteSource, ScalarSource, VectorSource}, +}; -use isosurface::source::Source; +pub trait AllSources: ScalarSource + VectorSource + HermiteSource {} -/// The distance-field equation for a torus -fn torus(x: f32, y: f32, z: f32) -> f32 { - const R1: f32 = 1.0 / 4.0; - const R2: f32 = 1.0 / 10.0; - let q_x = ((x * x + y * y).sqrt()).abs() - R1; - let len = (q_x * q_x + z * z).sqrt(); - len - R2 -} - -pub struct Torus {} - -impl Source for Torus { - fn sample(&self, x: f32, y: f32, z: f32) -> f32 { - torus(x - 0.5, y - 0.5, z - 0.5) - } -} +impl AllSources for S {} -fn abs(x: f32, y: f32, z: f32) -> (f32, f32, f32) { - ( - if x > 0.0 { x } else { -x }, - if y > 0.0 { y } else { -y }, - if z > 0.0 { z } else { -z }, - ) +pub struct DemoSource<'a> { + pub source: Box, } -fn max(px: f32, py: f32, pz: f32, qx: f32, qy: f32, qz: f32) -> (f32, f32, f32) { - ( - if px > qx { px } else { qx }, - if py > qy { py } else { qy }, - if pz > qz { pz } else { qz }, - ) -} - -/// The distance field equation for a cube -fn cube(px: f32, py: f32, pz: f32, bx: f32, by: f32, bz: f32) -> f32 { - let (ax, ay, az) = abs(px, py, pz); - let (dx, dy, dz) = (ax - bx, ay - by, az - bz); - let (mx, my, mz) = max(dx, dy, dz, 0.0, 0.0, 0.0); - let l = (mx * mx + my * my + mz * mz).sqrt(); - dx.max(dz.max(dy)).min(0.0) + l +impl<'a> DemoSource<'a> { + pub fn new(source: S) -> Self { + Self { + source: Box::new(source), + } + } } -/// The distance field equation for a sphere -fn sphere(x: f32, y: f32, z: f32, r: f32) -> f32 { - (x * x + y * y + z * z).sqrt() - r +impl<'a> ScalarSource for DemoSource<'a> { + fn sample_scalar(&self, p: Vec3) -> Signed { + let q = p - Vec3::from_scalar(0.5); + self.source.sample_scalar(q) + } } -/// Subtract one distance field from another (i.e. CSG difference operation) -fn subtract(d1: f32, d2: f32) -> f32 { - d2.max(-d1) +impl<'a> VectorSource for DemoSource<'a> { + fn sample_vector(&self, p: Vec3) -> Directed { + let q = p - Vec3::from_scalar(0.5); + self.source.sample_vector(q) + } } -pub struct CubeSphere {} - -impl Source for CubeSphere { - fn sample(&self, x: f32, y: f32, z: f32) -> f32 { - subtract( - sphere(x - 0.5, y - 0.5, z - 0.5, 0.25), - cube(x - 0.5, y - 0.5, z - 0.5, 0.2, 0.2, 0.2), - ) +impl<'a> HermiteSource for DemoSource<'a> { + fn sample_normal(&self, p: Vec3) -> Vec3 { + let q = p - Vec3::from_scalar(0.5); + self.source.sample_normal(q) } } diff --git a/examples/common/text.rs b/examples/common/text.rs index ef3497d..1de7d7a 100644 --- a/examples/common/text.rs +++ b/examples/common/text.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,11 +13,11 @@ // limitations under the License. //! Conveniences to make the `glium_text` API easier to use in samples. - use cgmath::{Matrix4, Vector3}; -/// Produce a transform matrix that will display text at offset column `x`, row `y`, in a -/// display-filling coordinate space N characters wide and N*aspect rows high. +/// Produce a transform matrix that will display text at offset column `x`, row +/// `y`, in a display-filling coordinate space N characters wide and N*aspect +/// rows high. pub fn layout_text(characters_per_row: f32, aspect: f32, x: f32, y: f32) -> Matrix4 { let inv_scale = 2.0 / characters_per_row; Matrix4::from_translation(Vector3::new(-1.0, -1.0, 0.0)) diff --git a/examples/deferred_rasterisation.rs b/examples/deferred_rasterisation.rs index 887efb1..d029a05 100644 --- a/examples/deferred_rasterisation.rs +++ b/examples/deferred_rasterisation.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,21 +19,22 @@ extern crate isosurface; mod common; -use crate::common::reinterpret_cast_slice; -use crate::common::sources::Torus; +use crate::common::{reinterpret_cast_slice, sources::DemoSource}; use cgmath::{vec3, Matrix4, Point3, SquareMatrix}; -use glium::glutin; -use glium::glutin::{ - dpi::LogicalSize, - event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent}, - Api, GlProfile, GlRequest, +use glium::{ + glutin::{ + self, + dpi::LogicalSize, + event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent}, + Api, GlProfile, GlRequest, + }, + texture::{DepthFormat, DepthTexture2d, MipmapsOption, Texture2d, UncompressedFloatFormat}, + Surface, }; -use glium::texture::{ - DepthFormat, DepthTexture2d, MipmapsOption, Texture2d, UncompressedFloatFormat, +use isosurface::{ + distance::Signed, extractor::OnlyInterleavedNormals, implicit::Torus, sampler::Sampler, + source::CentralDifference, PointCloud, }; -use glium::Surface; -use isosurface::point_cloud::PointCloud; -use isosurface::source::CentralDifference; #[derive(Copy, Clone)] #[repr(C)] @@ -52,9 +53,9 @@ struct VertexWithNormal { implement_vertex!(VertexWithNormal, position, normal); -// This technique is derived from an image tweeted by Gavan Woolery (gavanw@). it needs some -// refinement, but I think I've captured an approximation of his rendering technique. -// https://twitter.com/gavanw/status/717265068086308865 +// This technique is derived from an image tweeted by Gavan Woolery (gavanw@). +// it needs some refinement, but I think I've captured an approximation of his +// rendering technique. https://twitter.com/gavanw/status/717265068086308865 fn main() { let events_loop = glutin::event_loop::EventLoop::new(); @@ -76,13 +77,15 @@ fn main() { let subdivisions = 64; - let torus = Torus {}; + let torus = DemoSource::new(Torus::new(0.25, 0.1)); let central_difference = CentralDifference::new(torus); + let sampler = Sampler::new(¢ral_difference); let mut vertices = vec![]; - let mut marcher = PointCloud::new(subdivisions); + let mut extractor = OnlyInterleavedNormals::new(&mut vertices, &sampler); + let mut marcher = PointCloud::::new(subdivisions); - marcher.extract_midpoints_with_normals(¢ral_difference, &mut vertices); + marcher.extract(&sampler, &mut extractor); let vertex_buffer: glium::VertexBuffer = { glium::VertexBuffer::new(&display, reinterpret_cast_slice(&vertices)) @@ -226,7 +229,8 @@ fn main() { ); let model = Matrix4::identity(); - // We need two textures to ping-pong between, and one of them needs an attached depth buffer for the initial pass + // We need two textures to ping-pong between, and one of them needs an attached + // depth buffer for the initial pass let position1 = Texture2d::empty_with_format( &display, UncompressedFloatFormat::F32F32F32F32, @@ -358,8 +362,8 @@ fn main() { .expect("failed to draw to surface"); } - // pass 1 through N-1, ping-pong render both buffers in turn, spreading the points across - // the faces of their respective cubes + // pass 1 through N-1, ping-pong render both buffers in turn, spreading the + // points across the faces of their respective cubes for i in 0..3 { let framebuffer = if i % 2 == 0 { &mut framebuffer2 @@ -390,7 +394,8 @@ fn main() { .expect("failed to draw to surface"); } - // final pass, composite the last buffer to the screen, performing lighting in the process + // final pass, composite the last buffer to the screen, performing lighting in + // the process { let mut surface = display.draw(); surface.clear_color_and_depth((0.306, 0.267, 0.698, 0.0), 1.0); diff --git a/examples/torus.rs b/examples/sampler.rs similarity index 63% rename from examples/torus.rs rename to examples/sampler.rs index 0c3ccce..fbb4a8d 100644 --- a/examples/torus.rs +++ b/examples/sampler.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - extern crate cgmath; #[macro_use] extern crate glium; @@ -20,23 +19,32 @@ extern crate isosurface; mod common; -use crate::common::reinterpret_cast_slice; -use crate::common::sources::{CubeSphere, Torus}; -use crate::common::text::layout_text; -use cgmath::{vec3, Matrix4, Point3}; -use glium::backend::Facade; -use glium::draw_parameters::PolygonMode; -use glium::glutin; -use glium::glutin::{ - dpi::LogicalSize, - event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent}, - Api, GlProfile, GlRequest, +use crate::{ + common::reinterpret_cast_slice, common::sources::DemoSource, common::text::layout_text, }; +use cgmath::{vec3, Matrix4, Point3}; use glium::index::PrimitiveType; use glium::Surface; -use isosurface::linear_hashed_marching_cubes::LinearHashedMarchingCubes; -use isosurface::marching_cubes::MarchingCubes; -use isosurface::source::CentralDifference; +use glium::{ + backend::Facade, + draw_parameters::PolygonMode, + glutin::{ + self, + dpi::LogicalSize, + event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent}, + Api, GlProfile, GlRequest, + }, +}; +use isosurface::{ + distance::Signed, + extractor::IndexedInterleavedNormals, + feature::ParticleBasedMinimisation, + implicit::{Cylinder, Difference, Intersection, RectangularPrism, Sphere, Torus, Union}, + math::Vec3, + sampler::Sampler, + source::CentralDifference, + DualContouring, ExtendedMarchingCubes, LinearHashedMarchingCubes, MarchingCubes, +}; #[derive(Copy, Clone)] #[repr(C)] @@ -48,50 +56,73 @@ struct Vertex { implement_vertex!(Vertex, position, normal); const HELP_TEXT: &'static str = - "Press [A] to switch Algorithm, [S] to switch Shape, or [W] to toggle Wireframe"; + "Press [A] to change Algorithm, [S] Shape, [C] Complexity, [N] Normals, or [W] Wireframe"; struct GenerateResult(glium::VertexBuffer, glium::IndexBuffer, String); -fn generate(display: &F, shape: usize, algorithm: usize) -> GenerateResult +fn generate(display: &F, shape: usize, algorithm: usize, complexity: usize) -> GenerateResult where F: Facade, { let mut vertices = vec![]; let mut indices = vec![]; - let torus = CentralDifference::new(Torus {}); - let cube_sphere = CentralDifference::new(CubeSphere {}); + let sources = [ + (DemoSource::new(Torus::new(0.25, 0.1)), "Torus"), + (DemoSource::new(Sphere::new(0.3)), "Sphere"), + ( + DemoSource::new(RectangularPrism::new(Vec3::from_scalar(0.2))), + "Box", + ), + (DemoSource::new(Cylinder::new(0.25, 0.2)), "Cylinder"), + ( + DemoSource::new(CentralDifference::new(Union::new( + Difference::new( + Sphere::new(0.25), + RectangularPrism::new(Vec3::from_scalar(0.2)), + ), + Cylinder::new(0.02, 0.25), + ))), + "Sphere subtracted from Cube", + ), + ( + DemoSource::new(CentralDifference::new(Intersection::new( + Sphere::new(0.3), + RectangularPrism::new(Vec3::from_scalar(0.2)), + ))), + "Sphere intersected with Cube", + ), + ]; - let shape_name = match shape % 2 { - 0 => "Torus", - _ => "Cube Sphere", - }; + let (source, shape_name) = &sources[shape % sources.len()]; + let sampler = Sampler::new(source); + + let max_level = 3 + complexity % 5; + let grid_size = 2usize.pow(max_level as u32); - let algorithm_name = match algorithm % 2 { + let mut extractor = IndexedInterleavedNormals::new(&mut vertices, &mut indices, &sampler); + + let algorithm_name = match algorithm % 4 { 0 => { - let mut marching_cubes = MarchingCubes::new(128); - match shape % 2 { - 0 => marching_cubes.extract_with_normals(&torus, &mut vertices, &mut indices), - _ => marching_cubes.extract_with_normals(&cube_sphere, &mut vertices, &mut indices), - } + let mut marching_cubes = MarchingCubes::::new(grid_size); + marching_cubes.extract(&sampler, &mut extractor); "Marching Cubes" } - _ => { - let mut linear_hashed_marching_cubes = LinearHashedMarchingCubes::new(7); - match shape % 2 { - 0 => linear_hashed_marching_cubes.extract_with_normals( - &torus, - &mut vertices, - &mut indices, - ), - _ => linear_hashed_marching_cubes.extract_with_normals( - &cube_sphere, - &mut vertices, - &mut indices, - ), - } + 1 => { + let mut linear_hashed_marching_cubes = LinearHashedMarchingCubes::new(max_level); + linear_hashed_marching_cubes.extract(&sampler, &mut extractor); "Linear Hashed Marching Cubes" } + 2 => { + let mut extended_marching_cubes = ExtendedMarchingCubes::new(grid_size); + extended_marching_cubes.extract(&sampler, &mut extractor); + "Extended Marching Cubes" + } + _ => { + let mut dual_contouring = DualContouring::new(grid_size, ParticleBasedMinimisation {}); + dual_contouring.extract(&sampler, &mut extractor); + "Dual Contouring" + } }; let vertex_buffer: glium::VertexBuffer = @@ -106,9 +137,11 @@ where vertex_buffer, index_buffer, format!( - "{} - {}. {} vertices {} triangles", + "{} - {} with {} octree levels, {} grid size, {} vertices, {} triangles", shape_name, algorithm_name, + max_level, + grid_size, vertices.len() / 6, indices.len() / 3 ), @@ -143,8 +176,10 @@ fn main() { let mut wireframe = false; let mut shape = 0; let mut algorithm = 0; + let mut complexity = 4; + let mut show_normals = false; - let mut generated = generate(&display, shape, algorithm); + let mut generated = generate(&display, shape, algorithm, complexity); let program = program!(&display, 330 => { @@ -162,6 +197,8 @@ fn main() { } ", fragment: "#version 330 + uniform float show_normals; + in vec3 vNormal; layout(location=0) out vec4 color; @@ -169,11 +206,15 @@ fn main() { vec3 hemisphere(vec3 normal) { const vec3 light = vec3(0.1, -1.0, 0.0); float NdotL = dot(normal, light)*0.5 + 0.5; - return mix(vec3(0.886, 0.757, 0.337), vec3(0.518, 0.169, 0.0), NdotL); + return mix(vec3(0.3605, 0.2176, 0.005), vec3(0.7381, 0.531, 0.1003), NdotL); } void main() { - color = vec4(hemisphere(normalize(vNormal)), 1.0); + if (show_normals > 0.5) { + color = vec4(normalize(vNormal)*0.5 + 0.5, 1.0); + } else { + color = vec4(hemisphere(normalize(vNormal)), 1.0); + } } " }, @@ -189,8 +230,8 @@ fn main() { vec3(0.0, 1.0, 0.0), ); - let help_transform = layout_text(50.0, aspect, 1.0, 1.0); - let label_transform = layout_text(50.0, aspect, 1.0, 50.0 / aspect - 2.0); + let help_transform = layout_text(65.0, aspect, 1.0, 1.0); + let label_transform = layout_text(65.0, aspect, 1.0, 65.0 / aspect - 2.0); events_loop.run(move |event, _, control_flow| { match event { @@ -215,11 +256,18 @@ fn main() { } Some(VirtualKeyCode::A) => { algorithm += 1; - generated = generate(&display, shape, algorithm); + generated = generate(&display, shape, algorithm, complexity); } Some(VirtualKeyCode::S) => { shape += 1; - generated = generate(&display, shape, algorithm); + generated = generate(&display, shape, algorithm, complexity); + } + Some(VirtualKeyCode::C) => { + complexity += 1; + generated = generate(&display, shape, algorithm, complexity); + } + Some(VirtualKeyCode::N) => { + show_normals = !show_normals; } _ => (), }, @@ -229,10 +277,11 @@ fn main() { } let mut surface = display.draw(); - surface.clear_color_and_depth((0.024, 0.184, 0.337, 0.0), 1.0); + surface.clear_color_and_depth((0.011, 0.0089, 0.1622, 0.0), 1.0); let uniforms = uniform! { model_view_projection: Into::<[[f32; 4]; 4]>::into(projection * view), + show_normals: if show_normals {1.0f32} else {0.0}, }; let polygon_mode = if wireframe { diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..a66dfe6 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +unstable_features = true +wrap_comments = true +license_template_path = ".license_template" \ No newline at end of file diff --git a/src/distance.rs b/src/distance.rs new file mode 100644 index 0000000..07fb5ad --- /dev/null +++ b/src/distance.rs @@ -0,0 +1,104 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::math::Vec3; + +// Used to compute the diagonal dimension (i.e. 3-dimensional hypotenuse) of a +// cube. +const SQRT_OF_3: f32 = 1.732_050_807_57; + +/// A representation of distance in a specific metric space. +pub trait Distance: Copy + Clone { + /// Create a zero distance. + fn zero() -> Self; + + /// Determine if the given distance is positive. + fn is_positive(&self) -> bool; + + /// Interpolate between two distances. + fn lerp(&self, other: Self, factor: f32) -> Self; + + /// Test if the distance is within a cube of the specified amount. + fn within_extent(&self, extent: f32) -> bool; + + /// Find the point along the line between the given grid points, + /// that lies at the zero-crossing of the associated distances. + fn find_crossing_point(a: Self, b: Self, p_a: Vec3, p_b: Vec3) -> Vec3; +} + +/// A signed scalar distance. +#[derive(Copy, Clone)] +pub struct Signed(pub f32); + +/// A signed distance along all three cardinal axis. +#[derive(Copy, Clone)] +pub struct Directed(pub Vec3); + +impl Distance for Signed { + fn zero() -> Self { + Signed(0.0) + } + + fn is_positive(&self) -> bool { + self.0 > 0.0 + } + + fn lerp(&self, other: Self, f: f32) -> Self { + Signed((1.0 - f) * self.0 + f * other.0) + } + + fn within_extent(&self, extent: f32) -> bool { + self.0.abs() < extent * SQRT_OF_3 + } + + fn find_crossing_point(a: Self, b: Self, p_a: Vec3, p_b: Vec3) -> Vec3 { + let delta = b.0 - a.0; + let t = if delta == 0.0 { 0.5 } else { -a.0 / delta }; + + p_a * (1.0 - t) + p_b * t + } +} + +impl Distance for Directed { + fn zero() -> Self { + Directed(Vec3::zero()) + } + + fn is_positive(&self) -> bool { + // If any component is positive, we're not inside the surface + self.0.x > 0.0 || self.0.y > 0.0 || self.0.z > 0.0 + } + + fn lerp(&self, other: Self, f: f32) -> Self { + Directed(self.0.lerp(other.0, f)) + } + + fn within_extent(&self, extent: f32) -> bool { + self.0.abs().any(|f| f < extent * SQRT_OF_3) + } + + fn find_crossing_point(a: Self, b: Self, p_a: Vec3, p_b: Vec3) -> Vec3 { + // Since we're working on a grid, we only care about distance along the dominant + // axis + let axis = (p_a - p_b).abs().max_component_index(); + + let delta = b.0[axis] - a.0[axis]; + let t = if delta == 0.0 { + 0.5 + } else { + -a.0[axis] / delta + }; + + p_a * (1.0 - t) + p_b * t + } +} diff --git a/src/dual_contouring.rs b/src/dual_contouring.rs new file mode 100644 index 0000000..25e2a43 --- /dev/null +++ b/src/dual_contouring.rs @@ -0,0 +1,106 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + distance::Signed, + extractor::Extractor, + feature::PlaceFeatureInCell, + index_cache::GridKey, + marching_cubes_impl::{ + classify_corners, find_edge_crossings, march_cube, sample_normals_at_corners, + }, + math::Vec3, + mesh::MeshTopologyBuilder, + sampler::Sample, + source::HermiteSource, + traversal::DualGrid, +}; + +#[cfg(doc)] +use crate::feature::{MinimiseQEF, ParticleBasedMinimisation}; + +/// Convert isosurfaces to meshes using dual contouring. +/// +/// If you pass [MinimiseQEF] to the constructor this implements the classic [Dual Contouring of Hermite Data](https://doi.org/10.1145/566570.566586). If you instead pass in [ParticleBasedMinimisation] this becomes the improved version of dual contouring from [Efficient and Quality Contouring Algorithms on the GPU](https://doi.org/10.1111/j.1467-8659.2010.01825.x). +/// +/// Pros: +/// * Decent reproduction of sharp edges even when not grid-aligned. +/// +/// Cons: +/// * Feature placement can be very sensitive to the quality of input data. +pub struct DualContouring { + dual_grid: DualGrid, + place_feature: P, +} + +impl DualContouring

{ + /// Create a new DualContouring with the given chunk size. + /// + /// For a given `size`, this will evaluate chunks of `size^3` voxels. + pub fn new(size: usize, place_feature: P) -> Self { + Self { + dual_grid: DualGrid::new(size), + place_feature, + } + } + + /// Extracts a mesh from the given [Sample]. + /// + /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the + /// number of steps determined by the size provided to the constructor. + /// + /// The resulting vertex and face data will be returned via the provided + /// Extractor. + pub fn extract(&mut self, source: &S, extractor: &mut E) + where + S: Sample + HermiteSource, + E: Extractor, + { + let mut mesh_builder = MeshTopologyBuilder::new(extractor); + let mut normals = [Vec3::zero(); 8]; + + let dual_grid = &mut self.dual_grid; + let place_feature = &mut self.place_feature; + + dual_grid.traverse( + source, + Some(|corners: &[Vec3; 8], values: &[Signed; 8]| { + let cube_index = classify_corners(&values); + if cube_index == 0 || cube_index == 255 { + return None; + } + + sample_normals_at_corners(source, &corners, &mut normals); + + Some(place_feature.place_feature_in_cell(corners, &normals)) + }), + |keys, corners, values| { + let cube_index = classify_corners(&values); + + let mut vertices = [Vec3::zero(); 12]; + find_edge_crossings(cube_index, &corners, &values, &mut vertices); + + march_cube(cube_index, |a, b, c| { + let a = mesh_builder.add_vertex(Some(GridKey::new(keys, a)), vertices[a]); + let b = mesh_builder.add_vertex(Some(GridKey::new(keys, b)), vertices[b]); + let c = mesh_builder.add_vertex(Some(GridKey::new(keys, c)), vertices[c]); + + mesh_builder.add_face(a, b, c); + }); + }, + ); + + mesh_builder.build().extract_indices(extractor); + } +} diff --git a/src/extended_marching_cubes.rs b/src/extended_marching_cubes.rs new file mode 100644 index 0000000..95fbe7f --- /dev/null +++ b/src/extended_marching_cubes.rs @@ -0,0 +1,187 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::Directed, + extractor::Extractor, + feature::{LocalTopology, MinimiseQEF, TangentPlanes}, + index_cache::GridKey, + marching_cubes_impl::{ + classify_corners, find_edge_crossings, march_cube, sample_normals_at_edge_crossings, + }, + marching_cubes_tables::EDGE_LOOPS, + math::Vec3, + mesh::{MeshTopology, MeshTopologyBuilder, VertexHandle}, + sampler::Sample, + source::HermiteSource, + traversal::PrimalGrid, +}; +use std::collections::HashSet; + +/// Convert isosurfaces to meshes using extended marching cubes. +/// +/// This is an implementation of the paper [Feature Sensitive Surface Extraction from Volume Data](https://dl.acm.org/doi/abs/10.1145/383259.383265). +/// Pros: +/// +/// * Decent reproduction of sharp edges even when not grid-aligned. +/// +/// Cons: +/// +/// * May produce even more small triangle slivers on sharp edges. +/// * Extraction has dependencies on neighbouring chunks at the final +/// edge-flipping step. +pub struct ExtendedMarchingCubes { + primal_grid: PrimalGrid, +} + +impl ExtendedMarchingCubes { + /// Create a new ExtendedMarchingCubes with the given chunk size. + /// + /// For a given `size`, this will evaluate chunks of `size^3` voxels. + pub fn new(size: usize) -> Self { + Self { + primal_grid: PrimalGrid::new(size), + } + } + + /// Extracts a mesh from the given [Sample]. + /// + /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the + /// number of steps determined by the size provided to the constructor. + /// + /// The resulting vertex and face data will be returned via the provided + /// Extractor. + pub fn extract(&mut self, source: &S, extractor: &mut E) + where + S: Sample + HermiteSource, + E: Extractor, + { + let mut mesh_builder = MeshTopologyBuilder::new(extractor); + let mut features = HashSet::new(); + + self.primal_grid.traverse(source, |keys, corners, values| { + let mut vertices = [Vec3::zero(); 12]; + let mut normals = [Vec3::zero(); 12]; + + let cube_index = classify_corners(&values); + find_edge_crossings(cube_index, &corners, &values, &mut vertices); + sample_normals_at_edge_crossings(cube_index, source, &vertices, &mut normals); + + Self::march_cube_extended( + &mut features, + &mut mesh_builder, + cube_index, + vertices, + normals, + keys, + ); + }); + + let mut mesh = mesh_builder.build(); + Self::flip_feature_edges(features, &mut mesh); + mesh.extract_indices(extractor); + } + + fn march_cube_extended( + features: &mut HashSet, + mesh_builder: &mut MeshTopologyBuilder, + cube_index: usize, + vertices: [Vec3; 12], + normals: [Vec3; 12], + keys: &[(usize, usize, usize); 8], + ) where + E: Extractor, + { + let tangent_planes = TangentPlanes::from_edge_crossings(cube_index, &vertices, &normals); + + if let LocalTopology::Planar = tangent_planes.feature { + // No feature detected, so we can just use traditional marching cubes + march_cube(cube_index, |a, b, c| { + let a = mesh_builder.add_vertex(Some(GridKey::new(keys, a)), vertices[a]); + let b = mesh_builder.add_vertex(Some(GridKey::new(keys, b)), vertices[b]); + let c = mesh_builder.add_vertex(Some(GridKey::new(keys, c)), vertices[c]); + + mesh_builder.add_face(a, b, c); + }); + } else { + // We have a feature, so we need to spawn a new vertex close to the feature + + // Position the feature point by minimising the error in the system of equations + // formed by the tangent planes. Due to numerical inaccuracies the result may + // diverge a little from the expected location. + let feature_point = MinimiseQEF::place_feature_with_tangents(&tangent_planes); + + // Spawn a new vertex at the feature point + let center_index = mesh_builder.add_vertex(None, feature_point); + + // extract_vertex(feature_point); + // let center_index = work.mesh.add_vertex(); + + // Flag the vertex as a feature, since we'll need to flip some edges around it + // later + features.insert(center_index); + + // Form a triangle fan for each edge loop in the marching cubes triangle + // configuration + let loop_count = EDGE_LOOPS[cube_index][0] as usize; + let mut offset = 1 + loop_count; + for loops in 0..loop_count { + let vertex_count = EDGE_LOOPS[cube_index][1 + loops] as usize; + + for i in 0..vertex_count { + let j = (i + 1) % vertex_count; + + let e0 = EDGE_LOOPS[cube_index][offset + i] as usize; + let a = mesh_builder.add_vertex(Some(GridKey::new(keys, e0)), vertices[e0]); + + let e1 = EDGE_LOOPS[cube_index][offset + j] as usize; + let b = mesh_builder.add_vertex(Some(GridKey::new(keys, e1)), vertices[e1]); + + mesh_builder.add_face(a, b, center_index); + } + offset += vertex_count; + } + } + } + + fn flip_feature_edges(features: HashSet, mesh: &mut MeshTopology) { + // We can't modify the mesh while iterating it (it would anger the borrow + // checker), so accumulate edges that need flipping and process them + // afterwards + let mut flips = HashSet::new(); + + for &edge in mesh.edges() { + // If this edge already joins two features, don't flip it + if features.contains(&edge.start()) && features.contains(&edge.end()) { + continue; + } + + let faces = mesh.adjoining_faces(edge); + + // Only try to flip edges that adjoin exactly two faces + if let [face_a, face_b] = faces[..] { + // If flipping this edge will connect two features, add it to the list + if features.contains(&face_a.vertex_opposite(edge)) + && features.contains(&face_b.vertex_opposite(edge)) + { + flips.insert(edge); + } + } + } + + // Now we can flip all the edges we found earlier + for edge in flips { + mesh.rotate_edge(edge); + } + } +} diff --git a/src/extractor.rs b/src/extractor.rs new file mode 100644 index 0000000..e0a35c2 --- /dev/null +++ b/src/extractor.rs @@ -0,0 +1,127 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{math::Vec3, source::HermiteSource}; + +/// Trait for outputting mesh vertices and indices. +pub trait Extractor { + fn extract_vertex(&mut self, vertex: Vec3); + fn extract_index(&mut self, index: usize); +} + +/// Output vertices as a tightly packed array of floats, discarding any face +/// data. +pub struct OnlyVertices<'a> { + vertices: &'a mut Vec, +} + +impl<'a> OnlyVertices<'a> { + pub fn new(vertices: &'a mut Vec) -> Self { + Self { vertices } + } +} + +impl<'a> Extractor for OnlyVertices<'a> { + fn extract_vertex(&mut self, v: Vec3) { + self.vertices.push(v.x); + self.vertices.push(v.y); + self.vertices.push(v.z); + } + + fn extract_index(&mut self, _: usize) {} +} + +/// Output vertices as a tightly packed array of floats, discarding any face +/// data. +pub struct OnlyInterleavedNormals<'a, S: HermiteSource> { + vertices: &'a mut Vec, + source: &'a S, +} + +impl<'a, S: HermiteSource> OnlyInterleavedNormals<'a, S> { + pub fn new(vertices: &'a mut Vec, source: &'a S) -> Self { + Self { vertices, source } + } +} + +impl<'a, S: HermiteSource> Extractor for OnlyInterleavedNormals<'a, S> { + fn extract_vertex(&mut self, v: Vec3) { + let n = self.source.sample_normal(v); + self.vertices.push(v.x); + self.vertices.push(v.y); + self.vertices.push(v.z); + self.vertices.push(n.x); + self.vertices.push(n.y); + self.vertices.push(n.z); + } + + fn extract_index(&mut self, _: usize) {} +} + +/// Output vertices as a tightly packed array of floats. +pub struct IndexedVertices<'a> { + vertices: &'a mut Vec, + indices: &'a mut Vec, +} + +impl<'a> IndexedVertices<'a> { + pub fn new(vertices: &'a mut Vec, indices: &'a mut Vec) -> Self { + Self { vertices, indices } + } +} + +impl<'a> Extractor for IndexedVertices<'a> { + fn extract_vertex(&mut self, v: Vec3) { + self.vertices.push(v.x); + self.vertices.push(v.y); + self.vertices.push(v.z); + } + + fn extract_index(&mut self, index: usize) { + self.indices.push(index as u32); + } +} + +/// Sample normals from an implicit surface and output them interleaved with +/// vertices, as a tightly packed array of floats. +pub struct IndexedInterleavedNormals<'a, S: HermiteSource> { + vertices: &'a mut Vec, + indices: &'a mut Vec, + source: &'a S, +} + +impl<'a, S: HermiteSource> IndexedInterleavedNormals<'a, S> { + pub fn new(vertices: &'a mut Vec, indices: &'a mut Vec, source: &'a S) -> Self { + Self { + vertices, + indices, + source, + } + } +} + +impl<'a, S: HermiteSource> Extractor for IndexedInterleavedNormals<'a, S> { + fn extract_vertex(&mut self, v: Vec3) { + let n = self.source.sample_normal(v); + self.vertices.push(v.x); + self.vertices.push(v.y); + self.vertices.push(v.z); + self.vertices.push(n.x); + self.vertices.push(n.y); + self.vertices.push(n.z); + } + + fn extract_index(&mut self, index: usize) { + self.indices.push(index as u32); + } +} diff --git a/src/feature/mod.rs b/src/feature/mod.rs new file mode 100644 index 0000000..af0458d --- /dev/null +++ b/src/feature/mod.rs @@ -0,0 +1,149 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +mod particle_minimisation; +mod qef; + +pub use particle_minimisation::*; +pub use qef::*; + +use crate::{marching_cubes_tables::EDGE_CROSSING_MASK, math::Vec3}; + +const FEATURE_ANGLE: f32 = 0.8660254037844387; // cos(30º) + +/// Place a mesh vertex at a feature point within a grid cell +pub trait PlaceFeatureInCell { + /// Place a vertex as close as possible to any feature within the specified + /// cell. Requires the corner vertices of the cell, and the normals at each + /// corner. + fn place_feature_in_cell(&self, corners: &[Vec3; 8], normals: &[Vec3; 8]) -> Vec3; +} + +/// Classifies the topology of a surface within a specific region. Note that +/// since topology is evaluated based on discrete samples, it will not take into +/// account topological features that are smaller than the sampling frequency. +pub enum LocalTopology { + /// The surface doesn't have any sharp features in this region. + Planar, + /// The surface has a sharp edge or crease in this region. + Edge, + /// The surface has a sharp corner in this region. + Corner, +} + +pub(crate) struct Plane { + normal: Vec3, + d: f32, +} + +impl Plane { + pub(crate) fn distance(&self, p: Vec3) -> f32 { + self.normal.dot(p) - self.d + } + + pub fn point_closest_to(&self, p: Vec3) -> Vec3 { + let distance = self.distance(p); + p - self.normal * distance + } +} + +/// The set of planes tangent to the surface within a given grid cell. +pub struct TangentPlanes { + pub(crate) planes: Vec, + pub(crate) center_of_mass: Vec3, + pub(crate) feature: LocalTopology, +} + +impl TangentPlanes { + /// Generate a set of tangent planes from the cell corners and the + /// corresponding surface normals. + pub fn from_corners(corners: &[Vec3; 8], normals: &[Vec3; 8]) -> Self { + Self::new(corners, normals) + } + + /// Generate a set of tangent planes from the points where the surface + /// crosses the cell edges and the corresponding surface normals. + pub fn from_edge_crossings( + cube_index: usize, + vertices: &[Vec3; 12], + normals: &[Vec3; 12], + ) -> Self { + let edges = EDGE_CROSSING_MASK[cube_index]; + + let indices: Vec = (0..12).filter(|i| (edges & (1 << i)) != 0).collect(); + + let vertices: Vec = indices.iter().map(|&i| vertices[i]).collect(); + let normals: Vec = indices.iter().map(|&i| normals[i]).collect(); + + Self::new(&vertices, &normals) + } + + fn new(vertices: &[Vec3], normals: &[Vec3]) -> Self { + let mut center_of_mass = Vec3::zero(); + let mut axis = Vec3::zero(); + let mut min_angle = std::f32::MAX; + + let mut count = 0.0; + + for i in 0..vertices.len() { + for j in 0..vertices.len() { + let angle = normals[i].dot(normals[j]); + if angle < min_angle { + axis = normals[i].cross(normals[j]); + min_angle = angle; + } + } + + center_of_mass += vertices[i]; + count += 1.0; + } + + center_of_mass /= count; + + let feature = if min_angle > FEATURE_ANGLE { + LocalTopology::Planar + } else { + axis = axis.normalised().unwrap_or_default(); + let (mut min_c, mut max_c) = (1.0f32, -1.0f32); + for &n in normals { + let c = axis.dot(n); + min_c = min_c.min(c); + max_c = max_c.max(c); + } + let c = min_c.abs().max(max_c.abs()); + let c = (1.0 - c * c).sqrt(); + + if c > FEATURE_ANGLE { + LocalTopology::Edge + } else { + LocalTopology::Corner + } + }; + + let planes = (0..vertices.len()) + .into_iter() + .map(|i| { + let normal = normals[i]; + let d = (vertices[i] - center_of_mass).dot(normal); + + Plane { normal, d } + }) + .collect(); + + Self { + planes, + center_of_mass, + feature, + } + } +} diff --git a/src/feature/particle_minimisation.rs b/src/feature/particle_minimisation.rs new file mode 100644 index 0000000..42ce4ca --- /dev/null +++ b/src/feature/particle_minimisation.rs @@ -0,0 +1,72 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + feature::{PlaceFeatureInCell, TangentPlanes}, + math::Vec3, +}; + +/// The feature placement algorithm from [Efficient and Quality Contouring Algorithms on the GPU](https://doi.org/10.1111/j.1467-8659.2010.01825.x). This is a much simpler, and often faster alternative to the classic QEF minimisation traditionally used in Dual Contouring. +pub struct ParticleBasedMinimisation {} + +const STEP_SIZE: f32 = 0.05; +const THRESHOLD: f32 = 0.02; + +impl PlaceFeatureInCell for ParticleBasedMinimisation { + fn place_feature_in_cell(&self, corners: &[Vec3; 8], normals: &[Vec3; 8]) -> Vec3 { + let t = TangentPlanes::from_corners(corners, normals); + let max_feature_size = corners[6].x - corners[0].x; + + let mut particle = t.center_of_mass; + + let mut forces = [Vec3::zero(); 8]; + for i in 0..8 { + for j in 0..8 { + forces[i] += corners[i] - t.planes[j].point_closest_to(corners[i]); + } + } + + for _ in 0..100 { + let force = Self::trilinear(corners, &forces, particle) * STEP_SIZE * max_feature_size; + + particle += force; + + if force.len_sq() < THRESHOLD * max_feature_size { + break; + } + } + + particle + } +} + +impl ParticleBasedMinimisation { + fn trilinear(corners: &[Vec3; 8], forces: &[Vec3; 8], p: Vec3) -> Vec3 { + // trilinear interpolation factor along each axis + let f = + (p - corners[0]) / (Vec3::new(corners[1].x, corners[3].y, corners[4].z) - corners[0]); + + // interpolate first along the x axis + let c00 = (1.0 - f.x) * forces[0] + f.x * forces[1]; + let c01 = (1.0 - f.x) * forces[4] + f.x * forces[5]; + let c10 = (1.0 - f.x) * forces[3] + f.x * forces[2]; + let c11 = (1.0 - f.x) * forces[7] + f.x * forces[6]; + + // Then along the y axis + let c0 = (1.0 - f.y) * c00 + f.y * c10; + let c1 = (1.0 - f.y) * c01 + f.y * c11; + + // Now finally along the z axis + (1.0 - f.z) * c0 + f.z * c1 + } +} diff --git a/src/feature/qef.rs b/src/feature/qef.rs new file mode 100644 index 0000000..686c3b3 --- /dev/null +++ b/src/feature/qef.rs @@ -0,0 +1,69 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + feature::{LocalTopology, PlaceFeatureInCell, TangentPlanes}, + math::{svd::SVD, Vec3}, +}; + +/// The feature placement algorithm used by Extended Marching Cubes and +/// traditional Dual Contouring. Uses Singular Value Decomposition to minimise +/// the quadratic error function defined by the tangent planes to the implicit +/// surface at the grid edge crossings. +pub struct MinimiseQEF {} + +impl PlaceFeatureInCell for MinimiseQEF { + fn place_feature_in_cell(&self, corners: &[Vec3; 8], normals: &[Vec3; 8]) -> Vec3 { + let t = TangentPlanes::from_corners(corners, normals); + Self::place_feature_with_tangents(&t) + } +} + +impl MinimiseQEF { + /// Place a vertex as close as possible to any feature within the specified + /// cell. Requires the tangent planes to the surface at the grid edge + /// crossings. + pub fn place_feature_with_tangents(t: &TangentPlanes) -> Vec3 { + if let LocalTopology::Planar = t.feature { + return t.center_of_mass; + } + + let a: Vec<[f64; 3]> = t + .planes + .iter() + .map(|p| [p.normal.x as f64, p.normal.y as f64, p.normal.z as f64]) + .collect(); + + let mut svd = SVD::new(&a); + + // The system of equations is underspecified for edges, so + // we zero the minimum singular value to reduce the rank + if let LocalTopology::Edge = t.feature { + let mut s_min = std::f64::MAX; + let mut s_min_id = 0; + + for i in 0..3 { + if svd.diagonal()[i] < s_min { + s_min = svd.diagonal()[i]; + s_min_id = i; + } + } + + svd.diagonal()[s_min_id] = 0.0; + } + + let b: Vec = t.planes.iter().map(|p| p.d as f64).collect(); + + t.center_of_mass + svd.solve(&b) + } +} diff --git a/src/implicit/csg.rs b/src/implicit/csg.rs new file mode 100644 index 0000000..68150b3 --- /dev/null +++ b/src/implicit/csg.rs @@ -0,0 +1,168 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::{Directed, Signed}, + math::Vec3, + source::{HermiteSource, ScalarSource, VectorSource}, +}; + +/// The CSG union operation. An implicit function that is solid where either of +/// the provided implicit functions is solid. +pub struct Union { + /// The first implicit function. + pub a: A, + /// The second implicit function. + pub b: B, +} + +impl Union { + pub fn new(a: A, b: B) -> Self { + Self { a, b } + } +} + +impl ScalarSource for Union { + fn sample_scalar(&self, p: Vec3) -> Signed { + Signed(self.a.sample_scalar(p).0.min(self.b.sample_scalar(p).0)) + } +} + +impl VectorSource for Union { + fn sample_vector(&self, p: Vec3) -> Directed { + Directed(self.a.sample_vector(p).0.min(self.b.sample_vector(p).0)) + } +} + +impl HermiteSource for Union { + fn sample_normal(&self, p: Vec3) -> Vec3 { + self.a.sample_normal(p).min(self.b.sample_normal(p)) + } +} + +/// The CSG intersection operation. An implicit function that is solid only +/// where both of the provided implicit functions are solid. +pub struct Intersection { + /// The first implicit function. + pub a: A, + /// The second implicit function. + pub b: B, +} + +impl Intersection { + pub fn new(a: A, b: B) -> Self { + Self { a, b } + } +} + +impl ScalarSource for Intersection { + fn sample_scalar(&self, p: Vec3) -> Signed { + Signed(self.a.sample_scalar(p).0.max(self.b.sample_scalar(p).0)) + } +} + +impl VectorSource for Intersection { + fn sample_vector(&self, p: Vec3) -> Directed { + Directed(self.a.sample_vector(p).0.max(self.b.sample_vector(p).0)) + } +} + +/// The CSG difference operation. Subtracts the first provided implicit function +/// from the second, i.e. the result is solid where the second +/// function is solid, except where the first is solid. +pub struct Difference { + /// The first implicit function. + pub a: A, + /// The second implicit function. + pub b: B, +} + +impl Difference { + pub fn new(a: A, b: B) -> Self { + Self { a, b } + } +} + +impl ScalarSource for Difference { + fn sample_scalar(&self, p: Vec3) -> Signed { + Signed(self.b.sample_scalar(p).0.max(-self.a.sample_scalar(p).0)) + } +} + +impl VectorSource for Difference { + fn sample_vector(&self, p: Vec3) -> Directed { + Directed(self.b.sample_vector(p).0.max(-self.a.sample_vector(p).0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::implicit::RectangularPrism; + use std::f32::MAX; + + #[test] + fn test_csg() { + let a = RectangularPrism::new(Vec3::new(4.0, 4.0, 1.0)); + let b = RectangularPrism::new(Vec3::new(2.0, 2.0, 4.0)); + + let u = Union::new(a.clone(), b.clone()); + + assert_eq!(u.sample_scalar(Vec3::zero()).0, -2.0); + assert_eq!(u.sample_scalar(Vec3::new(4.0, 4.0, 1.0)).0, 0.0); + assert_eq!(u.sample_scalar(Vec3::new(0.0, 0.0, 8.0)).0, 4.0); + assert_eq!(u.sample_scalar(Vec3::new(8.0, 0.0, 0.0)).0, 4.0); + + assert_eq!(u.sample_vector(Vec3::zero()).0, Vec3::new(-4.0, -4.0, -4.0)); + assert_eq!(u.sample_vector(Vec3::new(4.0, 4.0, 1.0)).0, Vec3::zero()); + assert_eq!( + u.sample_vector(Vec3::new(0.0, 0.0, 8.0)).0, + Vec3::new(MAX, MAX, 4.0) + ); + assert_eq!( + u.sample_vector(Vec3::new(8.0, 0.0, 0.0)).0, + Vec3::new(4.0, MAX, MAX) + ); + + assert_eq!( + u.sample_normal(Vec3::new(0.0, 0.0, 8.0)) + .normalised() + .unwrap(), + Vec3::new(0.0, 0.0, 1.0) + ); + assert_eq!( + u.sample_normal(Vec3::new(8.0, 0.0, 0.0)) + .normalised() + .unwrap(), + Vec3::new(1.0, 0.0, 0.0) + ); + + let i = Intersection::new(a.clone(), b.clone()); + + assert_eq!(i.sample_scalar(Vec3::zero()).0, -1.0); + assert_eq!(i.sample_scalar(Vec3::new(2.0, 2.0, 1.0)).0, 0.0); + assert_eq!(i.sample_scalar(Vec3::new(0.0, 0.0, 8.0)).0, 7.0); + assert_eq!(i.sample_scalar(Vec3::new(8.0, 0.0, 0.0)).0, 6.0); + + assert_eq!(i.sample_vector(Vec3::zero()).0, Vec3::new(-2.0, -2.0, -1.0)); + assert_eq!(i.sample_vector(Vec3::new(2.0, 2.0, 1.0)).0, Vec3::zero()); + assert_eq!( + i.sample_vector(Vec3::new(0.0, 0.0, 8.0)).0, + Vec3::new(MAX, MAX, 7.0) + ); + assert_eq!( + i.sample_vector(Vec3::new(8.0, 0.0, 0.0)).0, + Vec3::new(6.0, MAX, MAX) + ); + } +} diff --git a/src/implicit/cylinder.rs b/src/implicit/cylinder.rs new file mode 100644 index 0000000..a7167e2 --- /dev/null +++ b/src/implicit/cylinder.rs @@ -0,0 +1,141 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::{Directed, Signed}, + math::Vec3, + source::{HermiteSource, ScalarSource, VectorSource}, +}; +use std::f32::MAX; + +/// A capped cylinder. +pub struct Cylinder { + /// The radius of the cylinder. + pub radius: f32, + /// Half the length of the cylinder, the distance from the center point + /// to each capped end. + pub half_length: f32, +} + +impl Cylinder { + /// Create a capped cylinder from the desired radius and half of the desired + /// length. + pub fn new(radius: f32, half_length: f32) -> Self { + Self { + radius, + half_length, + } + } +} + +impl ScalarSource for Cylinder { + fn sample_scalar(&self, p: Vec3) -> Signed { + let q_x = (p.xy().len()).abs() - self.radius; + let q_z = p.z.abs() - self.half_length; + let d = Vec3::new(q_x.max(0.0), q_z.max(0.0), 0.0); + Signed(q_x.max(q_z).min(0.0) + d.len()) + } +} + +impl VectorSource for Cylinder { + fn sample_vector(&self, p: Vec3) -> Directed { + // Flip the point into the positive quadrant + let a = p.abs(); + Directed(Vec3::new( + if a.z > self.half_length || a.y > self.radius { + MAX + } else { + a.x - (self.radius * self.radius - a.y * a.y).sqrt() + }, + if a.z > self.half_length || a.x > self.radius { + MAX + } else { + a.y - (self.radius * self.radius - a.x * a.x).sqrt() + }, + if a.xy().len_sq() > self.radius * self.radius { + MAX + } else { + a.z - self.half_length + }, + )) + } +} + +impl HermiteSource for Cylinder { + fn sample_normal(&self, p: Vec3) -> Vec3 { + let z = p.z.abs() / self.half_length; + let r = (p.x * p.x + p.y * p.y).sqrt() / self.radius; + + if z > r { + Vec3::new(0.0, 0.0, p.z) + } else { + Vec3::new(p.x, p.y, 0.0) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::Vec2; + + #[test] + fn test_cylinder() { + let cylinder = Cylinder::new(2.0, 4.0); + + assert_eq!(cylinder.sample_scalar(Vec3::zero()).0, -2.0); + assert_eq!(cylinder.sample_scalar(Vec3::new(2.0, 0.0, 4.0)).0, 0.0); + assert_eq!(cylinder.sample_scalar(Vec3::new(0.0, 0.0, 8.0)).0, 4.0); + assert_eq!(cylinder.sample_scalar(Vec3::new(8.0, 0.0, 0.0)).0, 6.0); + + assert_eq!( + cylinder.sample_vector(Vec3::zero()).0, + Vec3::new(-2.0, -2.0, -4.0) + ); + assert_eq!( + cylinder.sample_vector(Vec3::new(0.0, 0.0, 1.0)).0, + Vec3::new(-2.0, -2.0, -3.0) + ); + assert_eq!( + cylinder.sample_vector(Vec3::new(1.0, 1.0, 1.0)).0, + Vec2::from_scalar(1.0 - (4.0f32 - 1.0f32).sqrt()).extend(-3.0) + ); + assert_eq!( + cylinder.sample_vector(Vec3::new(2.0, 0.0, 4.0)).0, + Vec3::zero() + ); + assert_eq!( + cylinder.sample_vector(Vec3::new(0.0, 0.0, 8.0)).0, + Vec3::new(MAX, MAX, 4.0) + ); + assert_eq!( + cylinder.sample_vector(Vec3::new(8.0, 0.0, 0.0)).0, + Vec3::new(6.0, MAX, MAX) + ); + + assert_eq!( + cylinder + .sample_normal(Vec3::new(0.0, 0.0, 8.0)) + .normalised() + .unwrap(), + Vec3::new(0.0, 0.0, 1.0) + ); + assert_eq!( + cylinder + .sample_normal(Vec3::new(8.0, 0.0, 0.0)) + .normalised() + .unwrap(), + Vec3::new(1.0, 0.0, 0.0) + ); + } +} diff --git a/src/implicit/mod.rs b/src/implicit/mod.rs new file mode 100644 index 0000000..cde4d6e --- /dev/null +++ b/src/implicit/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +mod csg; +mod cylinder; +mod rectangular_prism; +mod sphere; +mod torus; + +pub use csg::*; +pub use cylinder::*; +pub use rectangular_prism::*; +pub use sphere::*; +pub use torus::*; diff --git a/src/implicit/rectangular_prism.rs b/src/implicit/rectangular_prism.rs new file mode 100644 index 0000000..fe1e2e3 --- /dev/null +++ b/src/implicit/rectangular_prism.rs @@ -0,0 +1,131 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::{Directed, Signed}, + math::Vec3, + source::{HermiteSource, ScalarSource, VectorSource}, +}; +use std::f32::MAX; + +/// A rectangular prism, or box. +#[derive(Copy, Clone)] +pub struct RectangularPrism { + /// Half the extent of the box, the distance along each axis from the center + /// point to the surface of the box. + pub half_extent: Vec3, +} + +impl RectangularPrism { + /// Create a new rectangular prism from half the desired extents. + pub fn new(half_extent: Vec3) -> Self { + Self { half_extent } + } +} + +impl ScalarSource for RectangularPrism { + fn sample_scalar(&self, p: Vec3) -> Signed { + let q = p.abs() - self.half_extent; + Signed(q.max(Vec3::zero()).len() + q.max_component().min(0.0)) + } +} + +impl VectorSource for RectangularPrism { + fn sample_vector(&self, p: Vec3) -> Directed { + // Flip the point into the positive quadrant + let a = p.abs(); + let mask = Vec3::new( + if (a.yz() - self.half_extent.yz()).any(|f| f > 0.0) { + 1.0 + } else { + -1.0 + }, + if (a.xz() - self.half_extent.xz()).any(|f| f > 0.0) { + 1.0 + } else { + -1.0 + }, + if (a.xy() - self.half_extent.xy()).any(|f| f > 0.0) { + 1.0 + } else { + -1.0 + }, + ) * MAX; + // The closest point on a box is just the point clamped to the box bounds + let closest_point_on_cube = + if a.x < self.half_extent.x && a.y < self.half_extent.y && a.z < self.half_extent.z { + a.max(self.half_extent) + } else { + a.min(self.half_extent) + }; + // The distance along each axis is just the distance to the closest point + Directed((a - closest_point_on_cube).max(mask)) + } +} + +impl HermiteSource for RectangularPrism { + fn sample_normal(&self, p: Vec3) -> Vec3 { + p.clamp_to_cardinal_axis() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rectangular_prism() { + let prism = RectangularPrism::new(Vec3::new(1.0, 2.0, 4.0)); + + assert_eq!(prism.sample_scalar(Vec3::zero()).0, -1.0); + assert_eq!(prism.sample_scalar(Vec3::new(1.0, 2.0, 4.0)).0, 0.0); + assert_eq!(prism.sample_scalar(Vec3::new(0.0, 0.0, 8.0)).0, 4.0); + assert_eq!(prism.sample_scalar(Vec3::new(8.0, 0.0, 0.0)).0, 7.0); + + assert_eq!( + prism.sample_vector(Vec3::zero()).0, + Vec3::new(-1.0, -2.0, -4.0) + ); + assert_eq!( + prism.sample_vector(Vec3::new(1.0, 2.0, 4.0)).0, + Vec3::zero() + ); + assert_eq!( + prism.sample_vector(Vec3::new(0.0, 0.0, 8.0)).0, + Vec3::new(MAX, MAX, 4.0) + ); + assert_eq!( + prism.sample_vector(Vec3::new(8.0, 0.0, 0.0)).0, + Vec3::new(7.0, MAX, MAX) + ); + assert_eq!( + prism.sample_vector(Vec3::new(8.0, 8.0, 8.0)).0, + Vec3::from_scalar(MAX) + ); + + assert_eq!( + prism + .sample_normal(Vec3::new(0.0, 0.0, 8.0)) + .normalised() + .unwrap(), + Vec3::new(0.0, 0.0, 1.0) + ); + assert_eq!( + prism + .sample_normal(Vec3::new(8.0, 0.0, 0.0)) + .normalised() + .unwrap(), + Vec3::new(1.0, 0.0, 0.0) + ); + } +} diff --git a/src/implicit/sphere.rs b/src/implicit/sphere.rs new file mode 100644 index 0000000..2a160fc --- /dev/null +++ b/src/implicit/sphere.rs @@ -0,0 +1,110 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::{Directed, Signed}, + math::Vec3, + source::{HermiteSource, ScalarSource, VectorSource}, +}; +use std::f32::MAX; + +/// A sphere. +#[derive(Copy, Clone)] +pub struct Sphere { + /// The radius of the sphere. + pub radius: f32, +} + +impl Sphere { + /// Create a new sphere from the desired radius. + pub fn new(radius: f32) -> Self { + Self { radius } + } +} + +impl ScalarSource for Sphere { + fn sample_scalar(&self, p: Vec3) -> Signed { + Signed(p.len() - self.radius) + } +} + +impl VectorSource for Sphere { + fn sample_vector(&self, p: Vec3) -> Directed { + // Flip the point into the positive quadrant + let a = p.abs(); + + let r2 = self.radius * self.radius; + let l_yz = r2 - a.yz().len_sq(); + let l_xz = r2 - a.xz().len_sq(); + let l_xy = r2 - a.xy().len_sq(); + + Directed(Vec3::new( + if l_yz < 0.0 { MAX } else { a.x - l_yz.sqrt() }, + if l_xz < 0.0 { MAX } else { a.y - l_xz.sqrt() }, + if l_xy < 0.0 { MAX } else { a.z - l_xy.sqrt() }, + )) + } +} + +impl HermiteSource for Sphere { + fn sample_normal(&self, p: Vec3) -> Vec3 { + p + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sphere() { + let sphere = Sphere::new(2.0); + + assert_eq!(sphere.sample_scalar(Vec3::zero()).0, -2.0); + assert_eq!(sphere.sample_scalar(Vec3::new(2.0, 0.0, 0.0)).0, 0.0); + assert_eq!(sphere.sample_scalar(Vec3::new(0.0, 0.0, 8.0)).0, 6.0); + assert_eq!(sphere.sample_scalar(Vec3::new(8.0, 0.0, 0.0)).0, 6.0); + + assert_eq!( + sphere.sample_vector(Vec3::zero()).0, + Vec3::from_scalar(-2.0) + ); + assert_eq!( + sphere.sample_vector(Vec3::new(2.0, 0.0, 0.0)).0, + Vec3::zero() + ); + assert_eq!( + sphere.sample_vector(Vec3::new(0.0, 0.0, 8.0)).0, + Vec3::new(MAX, MAX, 6.0) + ); + assert_eq!( + sphere.sample_vector(Vec3::new(8.0, 0.0, 0.0)).0, + Vec3::new(6.0, MAX, MAX) + ); + + assert_eq!( + sphere + .sample_normal(Vec3::new(0.0, 0.0, 8.0)) + .normalised() + .unwrap(), + Vec3::new(0.0, 0.0, 1.0) + ); + assert_eq!( + sphere + .sample_normal(Vec3::new(8.0, 0.0, 0.0)) + .normalised() + .unwrap(), + Vec3::new(1.0, 0.0, 0.0) + ); + } +} diff --git a/src/implicit/torus.rs b/src/implicit/torus.rs new file mode 100644 index 0000000..289dc93 --- /dev/null +++ b/src/implicit/torus.rs @@ -0,0 +1,172 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::{Directed, Signed}, + math::{Vec2, Vec3}, + source::{HermiteSource, ScalarSource, VectorSource}, +}; +use std::f32::MAX; + +/// A torus, or doughnut-shape. +#[derive(Copy, Clone)] +pub struct Torus { + /// The radius from the center point to the middle of the outer ring. + pub radius: f32, + /// The radius of the outer ring itself. + pub tube_radius: f32, +} + +impl Torus { + /// Create a new torus from the primary radius and radius of the outer ring. + pub fn new(radius: f32, tube_radius: f32) -> Self { + Self { + radius, + tube_radius, + } + } +} + +impl ScalarSource for Torus { + fn sample_scalar(&self, p: Vec3) -> Signed { + let q_x = ((p.x * p.x + p.y * p.y).sqrt()).abs() - self.radius; + let len = (q_x * q_x + p.z * p.z).sqrt(); + Signed(len - self.tube_radius) + } +} + +impl VectorSource for Torus { + fn sample_vector(&self, p: Vec3) -> Directed { + // Flip the point into the positive quadrant + let a = p.abs(); + // // Find the closest point on the major radius of the torus + // let point_in_ring = Vec3::new(a.x, a.y, 0.0) + // .normalised() + // .unwrap_or_else(|| Vec3::new(1.0, 1.0, 0.0)) + // * self.radius; + + // // Transform the input point relative to the new point, and flip it into the + // positive quadrant again let a = (a - point_in_ring).abs(); + // // Find the closest point on the minor radius of the torus + // let closest_point_on_ring = + // a.normalised().unwrap_or(Vec3::new(0.0, 0.0, 1.0)) * self.tube_radius; + // // The distance along each axis is just the distance to the closest point + // Directed(a - closest_point_on_ring); + + let xy = Vec2::new(a.x, a.y); + let l = xy.len(); + let l_xy = l - self.radius; + let tube_radius_at_z = (self.tube_radius * self.tube_radius - a.z * a.z).sqrt(); + let r = Vec2::new( + self.radius + tube_radius_at_z, + self.radius - tube_radius_at_z, + ); + Directed(Vec3::new( + if a.z > self.tube_radius || a.y > self.radius + tube_radius_at_z { + MAX + } else if a.x == 0.0 { + (a.y - self.radius).abs() - self.tube_radius + } else { + (a.x - (r.x * r.x - a.y * a.y).sqrt()).max((r.y * r.y - a.y * a.y).sqrt() - a.x) + }, + if a.z > self.tube_radius || a.x > self.radius + tube_radius_at_z { + MAX + } else if a.y == 0.0 { + (a.x - self.radius).abs() - self.tube_radius + } else { + (a.y - (r.x * r.x - a.x * a.x).sqrt()).max((r.y * r.y - a.x * a.x).sqrt() - a.y) + }, + if l_xy.abs() > self.tube_radius { + MAX + } else { + a.z - (self.tube_radius * self.tube_radius - l_xy * l_xy).sqrt() + }, + )) + } +} + +impl HermiteSource for Torus { + fn sample_normal(&self, p: Vec3) -> Vec3 { + // Find the closest point on the major radius of the torus + let point_in_ring = Vec3::new(p.x, p.y, 0.0) + .normalised() + .unwrap_or(Vec3::new(1.0, 0.0, 0.0)) + * self.radius; + + p - point_in_ring + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_torus() { + let torus = Torus::new(8.0, 2.0); + + assert_eq!(torus.sample_scalar(Vec3::zero()).0, 6.0); + assert_eq!(torus.sample_scalar(Vec3::new(8.0, 0.0, 0.0)).0, -2.0); + assert_eq!(torus.sample_scalar(Vec3::new(10.0, 0.0, 0.0)).0, 0.0); + assert_eq!(torus.sample_scalar(Vec3::new(12.0, 0.0, 0.0)).0, 2.0); + assert_eq!(torus.sample_scalar(Vec3::new(8.0, 0.0, 8.0)).0, 6.0); + + assert_eq!( + torus.sample_vector(Vec3::zero()).0, + Vec3::new(6.0, 6.0, MAX) + ); + assert_eq!( + torus.sample_vector(Vec3::new(8.0, 0.0, 0.0)).0, + Vec3::from_scalar(-2.0), + ); + assert_eq!( + torus.sample_vector(Vec3::new(10.0, 0.0, 0.0)).0, + Vec3::zero() + ); + assert_eq!( + torus.sample_vector(Vec3::new(12.0, 0.0, 0.0)).0, + Vec3::new(2.0, MAX, MAX) + ); + assert_eq!( + torus.sample_vector(Vec3::new(12.0, 12.0, 0.0)).0, + Vec3::from_scalar(MAX) + ); + assert_eq!( + torus.sample_vector(Vec3::new(9.0, 9.0, 0.0)).0, + Vec2::from_scalar(9.0 - (10.0 * 10.0 - 9.0 * 9.0f32).sqrt()).extend(MAX) + ); + assert_eq!( + torus.sample_vector(Vec3::new(2.0, 2.0, 0.0)).0, + Vec2::from_scalar((6.0 * 6.0 - 2.0 * 2.0f32).sqrt() - 2.0).extend(MAX) + ); + assert_eq!( + torus.sample_vector(Vec3::new(8.0, 0.0, 8.0)).0, + Vec3::new(MAX, MAX, 6.0) + ); + + assert_eq!( + torus + .sample_normal(Vec3::new(8.0, 0.0, 8.0)) + .normalised() + .unwrap(), + Vec3::new(0.0, 0.0, 1.0) + ); + assert_eq!( + torus + .sample_normal(Vec3::new(12.0, 0.0, 0.0)) + .normalised() + .unwrap(), + Vec3::new(1.0, 0.0, 0.0) + ); + } +} diff --git a/src/index_cache.rs b/src/index_cache.rs index a42a244..921eb48 100644 --- a/src/index_cache.rs +++ b/src/index_cache.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,91 +11,64 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::{marching_cubes_tables::EDGE_CONNECTION, morton::Morton}; +use std::{cmp::Eq, collections::HashMap, hash::Hash}; -/// Tracks vertex indices to avoid emitting duplicate vertices during marching cubes mesh generation -pub struct IndexCache { - size: usize, - layers: [Vec<[u32; 4]>; 2], - rows: [Vec<[u32; 3]>; 2], - cells: [[u32; 2]; 2], - current_cell: [u32; 12], -} +#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub struct GridKey((usize, usize, usize), (usize, usize, usize)); -impl IndexCache { - /// Create a new IndexCache for the given chunk size - pub fn new(size: usize) -> IndexCache { - IndexCache { - size, - layers: [vec![[0; 4]; size * size], vec![[0; 4]; size * size]], - rows: [vec![[0; 3]; size], vec![[0; 3]; size]], - cells: [[0; 2]; 2], - current_cell: [0; 12], - } - } +#[derive(Debug, Hash, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct MortonKey(Morton, Morton); - /// Put an index in the cache at the given (x, y, edge) coordinate - pub fn put(&mut self, x: usize, y: usize, edge: usize, index: u32) { - if let 4..=7 = edge { - self.layers[1][y * self.size + x][edge - 4] = index; - } +/// Tracks vertex indices to avoid emitting duplicate vertices during marching +/// cubes mesh generation +pub struct IndexCache { + indices: HashMap, +} - match edge { - 6 => self.rows[1][x][0] = index, - 11 => self.rows[1][x][1] = index, - 10 => self.rows[1][x][2] = index, - _ => (), +impl IndexCache { + /// Create a new IndexCache + pub fn new() -> Self { + Self { + indices: HashMap::new(), } + } - match edge { - 5 | 10 => self.cells[1][0] = index, - _ => (), - } + /// Put an index in the cache at the given (x, y, z, edge) coordinate + pub fn put(&mut self, key: K, index: I) { + self.indices.insert(key, index); + } - self.current_cell[edge] = index; + /// Retrieve an index from the cache at the given (x, y, z, edge) coordinate + pub fn get(&self, key: K) -> Option { + self.indices.get(&key).cloned() } +} - /// Retrieve an index from the cache at the given (x, y, edge) coordinate - pub fn get(&mut self, x: usize, y: usize, edge: usize) -> u32 { - let result = match edge { - 0..=3 => self.layers[0][y * self.size + x][edge], - 4 => self.rows[0][x][0], - 8 => self.rows[0][x][1], - 9 => self.rows[0][x][2], - 7 | 11 => self.cells[0][1], - _ => 0, - }; +impl GridKey { + pub fn new(corners: &[(usize, usize, usize); 8], edge: usize) -> Self { + let [u, v] = EDGE_CONNECTION[edge]; - if result > 0 { - result + let a = corners[u]; + let b = corners[v]; + + if a > b { + Self(b, a) } else { - self.current_cell[edge] + Self(a, b) } } +} - /// Update the cache when mesh extraction moves to the next cell - pub fn advance_cell(&mut self) { - self.cells.swap(0, 1); - for i in &mut self.current_cell { - *i = 0; - } - } +impl MortonKey { + pub fn new(corners: &[Morton; 8], edge: usize) -> Self { + let [u, v] = EDGE_CONNECTION[edge]; + let (a, b) = (corners[u], corners[v]); - /// Update the cache when mesh extraction moves to the next row - pub fn advance_row(&mut self) { - self.rows.swap(0, 1); - for i in &mut self.cells[0] { - *i = 0; - } - } - - /// Update the cache when mesh extraction moves to the next layer - pub fn advance_layer(&mut self) { - self.layers.swap(0, 1); - for i in &mut self.cells[0] { - *i = 0; - } - for i in &mut self.rows[0] { - *i = [0; 3]; + if a > b { + Self(b, a) + } else { + Self(a, b) } } } diff --git a/src/lib.rs b/src/lib.rs index 01537f7..f59ebaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Algorithms for extracting meshe data from isosurfaces. +//! Algorithms for extracting mesh data from isosurfaces. /// Common math types pub mod math; @@ -20,48 +20,38 @@ pub mod math; /// Traits for defining isosurface data sources pub mod source; -/// Convert isosurfaces to meshes using marching cubes. -/// -/// Pros: -/// -/// * Pretty fast. -/// * Extraction has no dependencies on neighbouring chunks. -/// -/// Cons: -/// -/// * Produces a lot of small triangle slivers -/// * Can't accurately reproduce sharp corners in the isosurface. -/// * Cracks between chunks of differing levels of detail -pub mod marching_cubes; +/// Types for handling distances in different metric spaces. +pub mod distance; -/// Convert isosurfaces to point clouds -/// -/// Pros: -/// -/// * Blindingly fast. -/// -/// Cons: -/// -/// * Doesn't contain any surface data. Surfaces have to be reconsutructed, probably on the GPU. -pub mod point_cloud; +/// Utilities for outputting mesh data in specific formats. +pub mod extractor; -/// Convert isosurfaces to meshes using marching cubes over a linear hashed octree. -/// -/// This is a loose implementation of the paper [Fast Generation of Pointerless Octree Duals](https://onlinelibrary.wiley.com/doi/full/10.1111/j.1467-8659.2010.01775.x). -/// -/// Pros: -/// -/// * Roughly twice as fast as standard marching cubes. -/// * Accurately reproduce sharp grid-aligned corners in the underlying isosurface. -/// -/// Cons: -/// -/// * Still can't accurately reproduce sharp corners which are not grid-aligned. -/// * Still no level-of-detail for neighbouring chunks. -pub mod linear_hashed_marching_cubes; +/// Primitives for building distance fields from implicit functions. +pub mod implicit; +/// Sampling from distance fields. +pub mod sampler; + +/// Algorithms for traversing bounded regions of distance fields. +pub mod traversal; + +/// Algorithms for accurately placing vertices on features (edges or corners) of +/// an implicit surface. +pub mod feature; + +mod dual_contouring; +mod extended_marching_cubes; mod index_cache; +mod linear_hashed_marching_cubes; mod linear_hashed_octree; +mod marching_cubes; mod marching_cubes_impl; mod marching_cubes_tables; +mod mesh; mod morton; +mod point_cloud; + +pub use self::{ + dual_contouring::*, extended_marching_cubes::*, linear_hashed_marching_cubes::*, + marching_cubes::*, point_cloud::*, +}; diff --git a/src/linear_hashed_marching_cubes.rs b/src/linear_hashed_marching_cubes.rs index 88651f9..cbac1f8 100644 --- a/src/linear_hashed_marching_cubes.rs +++ b/src/linear_hashed_marching_cubes.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,265 +11,75 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -use crate::linear_hashed_octree::LinearHashedOctree; -use crate::marching_cubes_impl::{get_offset, interpolate, march_cube}; -use crate::marching_cubes_tables::EDGE_CONNECTION; -use crate::math::Vec3; -use crate::morton::Morton; -use crate::source::{HermiteSource, Norm, Source}; -use std::collections::HashMap; - -// Morton cube corners are ordered differently to the marching cubes tables, so remap them to match. -const REMAP_CUBE: [usize; 8] = [2, 3, 1, 0, 6, 7, 5, 4]; - -// Used to compute the diagonal dimension (i.e. 3-dimensional hypotenuse) of a cube. -const SQRT_OF_3: f32 = 1.732_050_807_57; - -// Uniquely identifies an edge by its terminal vertices -#[derive(Debug, Hash, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -struct Edge(Morton, Morton); - -impl Edge { - fn new(u: Morton, v: Morton) -> Self { - if u > v { - Edge(v, u) - } else { - Edge(u, v) - } - } -} - -/// Extracts meshes from distance fields using marching cubes over a linear hashed octree. +use crate::{ + distance::Signed, + extractor::Extractor, + index_cache::MortonKey, + marching_cubes_impl::{classify_corners, find_edge_crossings, march_cube}, + math::Vec3, + mesh::MeshTopologyBuilder, + sampler::Sample, + source::ScalarSource, + traversal::ImplicitOctree, +}; + +/// Convert isosurfaces to meshes using marching cubes over a linear hashed +/// octree. +/// +/// This is a loose implementation of the paper [Fast Generation of Pointerless Octree Duals](https://onlinelibrary.wiley.com/doi/full/10.1111/j.1467-8659.2010.01775.x). +/// +/// Pros: +/// +/// * Faster than standard marching cubes. +/// * Accurately reproduces grid-aligned sharp edges in the underlying +/// isosurface. +/// +/// Cons: +/// +/// * Still can't accurately reproduce sharp edges which are not grid-aligned. pub struct LinearHashedMarchingCubes { max_depth: usize, - norm: Norm, } impl LinearHashedMarchingCubes { /// Create a new LinearHashedMarchingCubes. /// - /// The depth of the internal octree will be at most `max_depth`, causing the tree to span the - /// equivalent of a cubic grid at most `2.pow(max_depth)` in either direction. Distances - /// will be evaluated in Euclidean space. + /// The depth of the internal octree will be at most `max_depth`, causing + /// the tree to span the equivalent of a cubic grid at most + /// `2.pow(max_depth)` in either direction. Distances will be evaluated + /// in Euclidean space. pub fn new(max_depth: usize) -> Self { - Self { - max_depth, - norm: Norm::Euclidean, - } - } - - /// Create a new LinearHashedMarchingCubes. - /// - /// The depth of the internal octree will be at most `max_depth`, causing the tree to span the - /// equivalent of a cubic grid at most `2.pow(max_depth)` in either direction. Distances will - /// be evaluated in accordance with the provided Norm. - pub fn with_norm(max_depth: usize, norm: Norm) -> Self { - Self { max_depth, norm } + Self { max_depth } } - /// Extracts a mesh from the given [`Source`](../source/trait.Source.html). + /// Extracts a mesh from the given [Sample]. /// - /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the number of steps - /// determined by the size provided to the constructor. + /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the + /// number of steps determined by the size provided to the constructor. /// - /// Extracted vertices will be appended to `vertices` as triples of (x, y, z) - /// coordinates. Extracted triangles will be appended to `indices` as triples of - /// vertex indices. - pub fn extract(&mut self, source: &S, vertices: &mut Vec, indices: &mut Vec) - where - S: Source, - { - self.extract_impl( - source, - |v: Vec3| { - vertices.push(v.x); - vertices.push(v.y); - vertices.push(v.z); - }, - indices, - ); - } - - /// Extracts a mesh from the given [`HermiteSource`](../source/trait.HermiteSource.html). - /// - /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the number of steps - /// determined by the size provided to the constructor. - /// - /// Extracted vertices will be appended to `vertices` as triples of (x, y, z) - /// coordinates, followed by the surface normals as triples of (x, y, z) dimensions. Extracted - /// triangles will be appended to `indices` as triples of vertex indices. - pub fn extract_with_normals( - &mut self, - source: &S, - vertices: &mut Vec, - indices: &mut Vec, - ) where - S: HermiteSource, - { - self.extract_impl( - source, - |v: Vec3| { - let n = source.sample_normal(v.x, v.y, v.z); - vertices.push(v.x); - vertices.push(v.y); - vertices.push(v.z); - vertices.push(n.x); - vertices.push(n.y); - vertices.push(n.z); - }, - indices, - ); - } - - fn extract_impl(&mut self, source: &S, extract: E, indices: &mut Vec) - where - S: Source, - E: FnMut(Vec3) -> (), - { - let octree = self.build_octree(source); - let primal_vertices = self.compute_primal_vertices(&octree); - let mut base_index = 0; - self.extract_surface(&octree, &primal_vertices, indices, &mut base_index, extract); - } - - fn diagonal(&self, distance: f32) -> f32 { - match self.norm { - Norm::Euclidean => distance * SQRT_OF_3, - Norm::Max => distance, - } - } - - fn build_octree(&mut self, source: &S) -> LinearHashedOctree + /// The resulting vertex and face data will be returned via the provided + /// Extractor. + pub fn extract(&mut self, source: &S, extractor: &mut E) where - S: Source, + S: Sample + ScalarSource, + E: Extractor, { - let max_depth = self.max_depth; - let mut octree = LinearHashedOctree::new(); - - octree.build( - |key: Morton, distance: &f32| { - let level = key.level(); - let size = key.size(); - level < 2 || (level < max_depth && distance.abs() <= self.diagonal(size)) - }, - |key: Morton| { - let p = key.center(); - source.sample(p.x, p.y, p.z) - }, - ); - - octree - } - - fn compute_primal_vertices( - &mut self, - octree: &LinearHashedOctree, - ) -> HashMap { - let mut primal_vertices = HashMap::new(); - - octree.walk_leaves(|key: Morton| { - let level = key.level(); - for i in 0..8 { - let vertex = key.primal_vertex(level, i); - - if vertex != Morton::with_key(0) { - if let Some(&existing_level) = primal_vertices.get(&vertex) { - if level > existing_level { - primal_vertices.insert(vertex, level); - } - } else { - primal_vertices.insert(vertex, level); - } - } - } + let mut implicit_octree = ImplicitOctree::new(self.max_depth); + let mut mesh_builder = MeshTopologyBuilder::new(extractor); + + implicit_octree.traverse(source, |keys, corners, values| { + let cube_index = classify_corners(&values); + + let mut vertices = [Vec3::zero(); 12]; + find_edge_crossings(cube_index, &corners, &values, &mut vertices); + march_cube(cube_index, |a, b, c| { + let a = mesh_builder.add_vertex(Some(MortonKey::new(&keys, a)), vertices[a]); + let b = mesh_builder.add_vertex(Some(MortonKey::new(&keys, b)), vertices[b]); + let c = mesh_builder.add_vertex(Some(MortonKey::new(&keys, c)), vertices[c]); + mesh_builder.add_face(a, b, c); + }); }); - primal_vertices - } - - fn extract_surface( - &mut self, - octree: &LinearHashedOctree, - primal_vertices: &HashMap, - indices: &mut Vec, - base_index: &mut u32, - mut extract: E, - ) where - E: FnMut(Vec3) -> (), - { - let mut index_map = HashMap::new(); - - let mut duals = [Morton::new(); 8]; - let mut dual_distances = [0.0; 8]; - - for (key, &level) in primal_vertices { - for i in 0..8 { - let mut m = key.dual_vertex(level, i); - while m > Morton::new() { - if let Some(&distance) = octree.get_node(&m) { - duals[i] = m; - dual_distances[i] = distance; - break; - } - m = m.parent(); - } - } - - self.march_one_cube( - duals, - dual_distances, - &mut index_map, - indices, - base_index, - &mut extract, - ); - } - } - - fn march_one_cube( - &mut self, - nodes: [Morton; 8], - dual_distances: [f32; 8], - index_map: &mut HashMap, - indices: &mut Vec, - base_index: &mut u32, - extract: &mut E, - ) where - E: FnMut(Vec3) -> (), - { - let mut reordered_nodes = [Morton::with_key(0); 8]; - let mut corners = [Vec3::zero(); 8]; - let mut values = [0f32; 8]; - - for i in 0..8 { - let key = nodes[REMAP_CUBE[i]]; - let distance = dual_distances[REMAP_CUBE[i]]; - - reordered_nodes[i] = key; - corners[i] = key.center(); - values[i] = distance; - } - - march_cube(&values, |edge: usize| { - let u = EDGE_CONNECTION[edge][0]; - let v = EDGE_CONNECTION[edge][1]; - - let edge_key = Edge::new(reordered_nodes[u], reordered_nodes[v]); - - if let Some(&index) = index_map.get(&edge_key) { - indices.push(index); - } else { - let index = *base_index; - *base_index += 1; - - index_map.insert(edge_key, index); - indices.push(index); - - let offset = get_offset(values[u], values[v]); - let vertex = interpolate(corners[u], corners[v], offset); - extract(vertex); - } - }); + mesh_builder.build().extract_indices(extractor); } } diff --git a/src/linear_hashed_octree.rs b/src/linear_hashed_octree.rs index bbcf322..266a8de 100644 --- a/src/linear_hashed_octree.rs +++ b/src/linear_hashed_octree.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - use crate::morton::Morton; use std::collections::{HashMap, VecDeque}; diff --git a/src/marching_cubes.rs b/src/marching_cubes.rs index 1d3df5e..3314668 100644 --- a/src/marching_cubes.rs +++ b/src/marching_cubes.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,155 +11,73 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::{ + distance::Distance, + extractor::Extractor, + index_cache::GridKey, + marching_cubes_impl::{classify_corners, find_edge_crossings, march_cube}, + math::Vec3, + mesh::MeshTopologyBuilder, + sampler::Sample, + traversal::PrimalGrid, +}; -use crate::index_cache::IndexCache; -use crate::marching_cubes_impl::{get_offset, interpolate, march_cube}; -use crate::marching_cubes_tables::{CORNERS, EDGE_CONNECTION}; -use crate::math::Vec3; -use crate::source::{HermiteSource, Source}; - -/// Extracts meshes from distance fields using the marching cubes algorithm. -pub struct MarchingCubes { - size: usize, - layers: [Vec; 2], +/// Convert isosurfaces to meshes using marching cubes. +/// +/// This is the classical isosurface extraction algorithm from [Marching cubes: A high resolution 3D surface construction algorithm](https://doi.org/10.1145/37402.37422). +/// +/// Pros: +/// +/// * Pretty fast. +/// * The classics are timeless. +/// +/// Cons: +/// +/// * Produces a lot of small triangle slivers. +/// * Can't accurately reproduce sharp edges in the isosurface. +pub struct MarchingCubes { + primal_grid: PrimalGrid, } -impl MarchingCubes { +impl MarchingCubes { /// Create a new MarchingCubes with the given chunk size. /// /// For a given `size`, this will evaluate chunks of `size^3` voxels. - pub fn new(size: usize) -> MarchingCubes { - MarchingCubes { - size, - layers: [vec![0f32; size * size], vec![0f32; size * size]], + pub fn new(size: usize) -> Self { + Self { + primal_grid: PrimalGrid::new(size), } } - /// Extracts a mesh from the given [`Source`](../source/trait.Source.html). - /// - /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the number of steps - /// determined by the size provided to the constructor. - /// - /// Extracted vertices will be appended to `vertices` as triples of (x, y, z) - /// coordinates. Extracted triangles will be appended to `indices` as triples of - /// vertex indices. - pub fn extract(&mut self, source: &S, vertices: &mut Vec, indices: &mut Vec) - where - S: Source, - { - self.extract_impl( - source, - |v: Vec3| { - vertices.push(v.x); - vertices.push(v.y); - vertices.push(v.z); - }, - indices, - ); - } - - /// Extracts a mesh from the given [`HermiteSource`](../source/trait.HermiteSource.html). + /// Extracts a mesh from the given [Sample]. /// - /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the number of steps - /// determined by the size provided to the constructor. + /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the + /// number of steps determined by the size provided to the constructor. /// - /// Extracted vertices will be appended to `vertices` as triples of (x, y, z) - /// coordinates, followed by the surface normals as triples of (x, y, z) dimensions. Extracted - /// triangles will be appended to `indices` as triples of vertex indices. - pub fn extract_with_normals( - &mut self, - source: &S, - vertices: &mut Vec, - indices: &mut Vec, - ) where - S: HermiteSource, - { - self.extract_impl( - source, - |v: Vec3| { - let n = source.sample_normal(v.x, v.y, v.z); - vertices.push(v.x); - vertices.push(v.y); - vertices.push(v.z); - vertices.push(n.x); - vertices.push(n.y); - vertices.push(n.z); - }, - indices, - ); - } - - fn extract_impl(&mut self, source: &S, mut extract: E, indices: &mut Vec) + /// The resulting vertex and face data will be returned via the provided + /// Extractor. + pub fn extract(&mut self, source: &S, extractor: &mut E) where - S: Source, - E: FnMut(Vec3) -> (), + S: Sample, + E: Extractor, { - let size_minus_one = self.size - 1; - let one_over_size = 1.0 / (size_minus_one as f32); - - // Cache layer zero of distance field values - for y in 0usize..self.size { - for x in 0..self.size { - self.layers[0][y * self.size + x] = - source.sample(x as f32 * one_over_size, y as f32 * one_over_size, 0.0); - } - } + let mut mesh_builder = MeshTopologyBuilder::new(extractor); - let mut corners = [Vec3::zero(); 8]; - let mut values = [0f32; 8]; + self.primal_grid.traverse(source, |keys, corners, values| { + let cube_index = classify_corners(&values); - let mut index_cache = IndexCache::new(self.size); - let mut index = 0u32; + let mut vertices = [Vec3::zero(); 12]; + find_edge_crossings(cube_index, &corners, &values, &mut vertices); - for z in 0..self.size { - // Cache layer N+1 of isosurface values - for y in 0..self.size { - for x in 0..self.size { - self.layers[1][y * self.size + x] = source.sample( - x as f32 * one_over_size, - y as f32 * one_over_size, - (z + 1) as f32 * one_over_size, - ); - } - } + march_cube(cube_index, |a, b, c| { + let a = mesh_builder.add_vertex(Some(GridKey::new(keys, a)), vertices[a]); + let b = mesh_builder.add_vertex(Some(GridKey::new(keys, b)), vertices[b]); + let c = mesh_builder.add_vertex(Some(GridKey::new(keys, c)), vertices[c]); - // Extract the calls in the current layer - for y in 0..size_minus_one { - for x in 0..size_minus_one { - for i in 0..8 { - corners[i] = Vec3::new( - (x + CORNERS[i][0]) as f32 * one_over_size, - (y + CORNERS[i][1]) as f32 * one_over_size, - (z + CORNERS[i][2]) as f32 * one_over_size, - ); - values[i] = self.layers[CORNERS[i][2]] - [(y + CORNERS[i][1]) * self.size + x + CORNERS[i][0]]; - } + mesh_builder.add_face(a, b, c); + }); + }); - march_cube(&values, |edge: usize| { - let cached_index = index_cache.get(x, y, edge); - if cached_index > 0 { - indices.push(cached_index); - } else { - let u = EDGE_CONNECTION[edge][0]; - let v = EDGE_CONNECTION[edge][1]; - - index_cache.put(x, y, edge, index); - indices.push(index); - index += 1; - - let offset = get_offset(values[u], values[v]); - let vertex = interpolate(corners[u], corners[v], offset); - extract(vertex); - } - }); - index_cache.advance_cell(); - } - index_cache.advance_row(); - } - index_cache.advance_layer(); - - self.layers.swap(0, 1); - } + mesh_builder.build().extract_indices(extractor); } } diff --git a/src/marching_cubes_impl.rs b/src/marching_cubes_impl.rs index 833a43b..ee41ab3 100644 --- a/src/marching_cubes_impl.rs +++ b/src/marching_cubes_impl.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,55 +11,107 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::{ + distance::Distance, + marching_cubes_tables::{EDGE_CONNECTION, EDGE_CROSSING_MASK, TRIANGLE_CONNECTION}, + math::Vec3, + sampler::Sample, + source::HermiteSource, +}; -use crate::marching_cubes_tables::TRIANGLE_CONNECTION; -use std::ops::{Add, Mul}; - -/// March a single cube, given the 8 corner vertices, and the density at each vertex. -/// -/// The `edge_func` will be invoked once for each vertex in the resulting mesh data, with the index -/// of the edge on which the vertex falls. Each triplet of invocations forms one triangle. -/// -/// It would in many ways be simple to output triangles directly, but callers needing to produce -/// indexed geometry will want to deduplicate vertices before forming triangles. -pub fn march_cube(values: &[f32; 8], mut edge_func: E) +/// Given the signed distances at each corner of the cube, classify them as +/// either inside or outside the surface, and return the bitmask of the result +/// (where a bit is set if the corner is outside the surface, and unset if +/// inside). +pub fn classify_corners(values: &[D; 8]) -> usize where - E: FnMut(usize) -> (), + D: Distance, { let mut cube_index = 0; for i in 0..8 { - if values[i] <= 0.0 { + if !values[i].is_positive() { cube_index |= 1 << i; } } + cube_index +} - for i in 0..5 { - if TRIANGLE_CONNECTION[cube_index][3 * i] < 0 { - break; - } +pub fn find_edge_crossings( + cube_index: usize, + corners: &[Vec3; 8], + values: &[D; 8], + vertices: &mut [Vec3; 12], +) where + D: Distance, +{ + let edges = EDGE_CROSSING_MASK[cube_index]; - for j in 0..3 { - let edge = TRIANGLE_CONNECTION[cube_index][3 * i + j] as usize; + for i in 0..12 { + if (edges & (1 << i)) != 0 { + let [u, v] = EDGE_CONNECTION[i]; - edge_func(edge); + vertices[i] = D::find_crossing_point(values[u], values[v], corners[u], corners[v]); } } } -/// Calculate the position of the vertex along an edge, given the density at either end of the edge. -pub fn get_offset(a: f32, b: f32) -> f32 { - let delta = b - a; - if delta == 0.0 { - 0.5 - } else { - -a / delta +pub fn sample_normals_at_corners(source: &S, corners: &[Vec3; 8], normals: &mut [Vec3; 8]) +where + D: Distance, + S: Sample + HermiteSource, +{ + for i in 0..8 { + normals[i] = source + .sample_normal(corners[i]) + .normalised() + .unwrap_or_default(); + } +} + +pub fn sample_normals_at_edge_crossings( + cube_index: usize, + source: &S, + vertices: &[Vec3; 12], + normals: &mut [Vec3; 12], +) where + D: Distance, + S: Sample + HermiteSource, +{ + let edges = EDGE_CROSSING_MASK[cube_index]; + + for i in 0..12 { + if (edges & (1 << i)) != 0 { + normals[i] = source + .sample_normal(vertices[i]) + .normalised() + .unwrap_or_default(); + } } } -/// Linearly Interpolate between two floating point values -pub fn interpolate(a: T, b: T, t: f32) -> T +/// March a single cube, given the 8 corner vertices, and the density at each +/// vertex. +/// +/// The `edge_func` will be invoked once for each vertex in the resulting mesh +/// data, with the index of the edge on which the vertex falls. Each triplet of +/// invocations forms one triangle. +/// +/// It would in many ways be simple to output triangles directly, but callers +/// needing to produce indexed geometry will want to deduplicate vertices before +/// forming triangles. +pub fn march_cube(cube_index: usize, mut face_callback: F) where - T: Add + Mul, + F: FnMut(usize, usize, usize) -> (), { - a * (1.0 - t) + b * t + for i in 0..5 { + if TRIANGLE_CONNECTION[cube_index][3 * i] < 0 { + break; + } + + face_callback( + TRIANGLE_CONNECTION[cube_index][3 * i + 0] as usize, + TRIANGLE_CONNECTION[cube_index][3 * i + 1] as usize, + TRIANGLE_CONNECTION[cube_index][3 * i + 2] as usize, + ); + } } diff --git a/src/marching_cubes_tables.rs b/src/marching_cubes_tables.rs index 7950f62..d3b4744 100644 --- a/src/marching_cubes_tables.rs +++ b/src/marching_cubes_tables.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,6 +24,10 @@ pub const CORNERS: [[usize; 3]; 8] = [ [0, 1, 1], ]; +/// Morton cube corners are ordered differently to the marching cubes tables, so +/// remap them to match. +pub const REMAP_CUBE: [usize; 8] = [2, 3, 1, 0, 6, 7, 5, 4]; + /// The corners used by each edge in a call pub const EDGE_CONNECTION: [[usize; 2]; 12] = [ [0, 1], @@ -40,11 +44,35 @@ pub const EDGE_CONNECTION: [[usize; 2]; 12] = [ [3, 7], ]; +/// A bitmask of the edges which cross the surface in the current cell +/// configuration +pub const EDGE_CROSSING_MASK: [usize; 256] = [ + 0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, + 0xd03, 0xe09, 0xf00, 0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, 0x99c, 0x895, + 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, 0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, + 0x53c, 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, 0x3a0, 0x2a9, 0x1a3, 0x0aa, + 0x7a6, 0x6af, 0x5a5, 0x4ac, 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, 0x460, + 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c, 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, + 0xa69, 0xb60, 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc, 0xdfc, 0xcf5, 0xfff, + 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c, + 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, + 0x2cf, 0x1c5, 0x0cc, 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, 0x8c0, 0x9c9, + 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, 0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, + 0x7c0, 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, 0x15c, 0x055, 0x35f, 0x256, + 0x55a, 0x453, 0x759, 0x650, 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, 0x2fc, + 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, + 0xd65, 0xc6c, 0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460, 0xca0, 0xda9, 0xea3, + 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, 0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0, + 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, 0x53c, 0x435, 0x73f, 0x636, 0x13a, + 0x033, 0x339, 0x230, 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, 0x69c, 0x795, + 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190, 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, + 0x80c, 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000, +]; + /// Maps the signs of each corner in a cell to the set of triangles spanning the active edges +#[rustfmt::skip] pub const TRIANGLE_CONNECTION: [[i8; 16]; 256] = [ - [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - ], + [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], [0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], [0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], [1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], @@ -299,7 +327,268 @@ pub const TRIANGLE_CONNECTION: [[i8; 16]; 256] = [ [1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], [0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], [0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], - [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - ], + [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], +]; + +/// Contains the number of edge loops, followed by the vertex count for +/// each loop, followed by the indices for the vertices in that loop +/// i.e. [loop_count, loop_0_count, ... loop_n_count, index_0, ... index_n] +#[rustfmt::skip] +pub const EDGE_LOOPS: [[i8; 17]; 256] = [ + [0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 1, 9, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 2, 10, 9, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 10, 9, 8, 3, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 0, 8, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 9, 8, 11, 2, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 3, 11, 10, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 8, 11, 10, 1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 11, 10, 9, 0, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 8, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 4, 7, 3, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 7, 3, 1, 9, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 3, 0, 4, 7, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 2, 10, 9, 0, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 7, 3, 2, 10, 9, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 2, 0, 4, 7, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1], + [2, 4, 4, 2, 1, 9, 11, 11, 9, 4, 7, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 3, 11, 10, 1, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 1, 0, 4, 7, 11, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 4, 7, 8, 0, 3, 11, 10, 9, -1, -1, -1, -1, -1, -1], + [1, 5, 4, 7, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 0, 1, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 3, 1, 5, 4, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1], + [1, 5, 4, 0, 2, 10, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 2, 10, 5, 3, 4, 8, 3, 5, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 0, 8, 11, 2, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 0, 1, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 1, 5, 4, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 3, 11, 10, 1, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 4, 9, 5, 1, 0, 8, 11, 10, -1, -1, -1, -1, -1, -1], + [1, 6, 5, 4, 0, 3, 11, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 5, 4, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 7, 8, 9, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 5, 7, 3, 0, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 1, 5, 7, 8, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 3, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 7, 8, 9, 5, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 10, 1, 2, 0, 9, 5, 7, 3, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 10, 5, 7, 8, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 2, 10, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 7, 8, 9, 5, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 0, 9, 5, 7, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 2, 3, 11, 8, 0, 1, 5, 7, -1, -1, -1, -1, -1, -1], + [1, 5, 11, 2, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 3, 11, 10, 1, 5, 7, 8, 9, -1, -1, -1, -1, -1, -1], + [1, 7, 5, 7, 11, 10, 1, 0, 9, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 11, 10, 5, 7, 8, 0, 3, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 5, 7, 11, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 1, 9, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 1, 2, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 1, 2, 6, 5, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 0, 2, 6, 5, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 6, 5, 9, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 0, 8, 11, 2, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1], + [2, 3, 5, 5, 10, 6, 2, 1, 9, 8, 11, -1, -1, -1, -1, -1, -1], + [1, 5, 5, 1, 3, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 5, 1, 0, 8, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 3, 11, 6, 0, 5, 9, 0, 6, -1, -1, -1, -1, -1, -1], + [1, 5, 6, 5, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 4, 7, 3, 0, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1], + [2, 3, 5, 10, 6, 5, 9, 4, 7, 3, 1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 1, 2, 6, 5, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 2, 6, 5, 1, 3, 0, 4, 7, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 8, 4, 7, 5, 9, 0, 2, 6, -1, -1, -1, -1, -1, -1], + [1, 7, 7, 3, 2, 6, 5, 9, 4, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1], + [2, 3, 5, 5, 10, 6, 7, 11, 2, 0, 4, -1, -1, -1, -1, -1, -1], + [4, 3, 3, 3, 3, 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6], + [3, 4, 4, 3, 2, 1, 9, 11, 4, 7, 11, 9, 5, 10, 6, -1, -1], + [2, 3, 5, 8, 4, 7, 11, 6, 5, 1, 3, -1, -1, -1, -1, -1, -1], + [1, 7, 5, 1, 0, 4, 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 4, 4, 3, 0, 6, 5, 9, 3, 11, 6, 0, 8, 4, 7, -1, -1], + [2, 4, 4, 9, 4, 7, 11, 6, 5, 9, 11, -1, -1, -1, -1, -1, -1], + [1, 4, 4, 9, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 4, 9, 10, 6, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 6, 4, 0, 1, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 1, 10, 6, 4, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 2, 6, 4, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 3, 0, 8, 9, 1, 2, 6, 4, -1, -1, -1, -1, -1, -1], + [1, 4, 2, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 8, 3, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 10, 6, 4, 9, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 2, 11, 8, 0, 10, 6, 4, 9, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 3, 11, 2, 1, 10, 6, 4, 0, -1, -1, -1, -1, -1, -1], + [1, 7, 6, 4, 8, 11, 2, 1, 10, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 3, 11, 6, 4, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 8, 11, 6, 4, 9, 1, 0, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 3, 11, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 8, 11, 6, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 8, 9, 10, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 0, 9, 10, 6, 7, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 8, 0, 1, 7, 10, 6, 7, 1, -1, -1, -1, -1, -1, -1], + [1, 5, 10, 6, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 1, 2, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 2, 6, 7, 3, 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 7, 8, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 7, 3, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 2, 3, 11, 6, 7, 8, 9, 10, -1, -1, -1, -1, -1, -1], + [1, 7, 2, 0, 9, 10, 6, 7, 11, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 4, 4, 3, 8, 0, 1, 7, 10, 6, 7, 1, 11, 2, 3, -1, -1], + [2, 4, 4, 11, 2, 1, 7, 1, 10, 6, 7, -1, -1, -1, -1, -1, -1], + [1, 7, 8, 9, 1, 3, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 0, 3, 11, 6, 7, 8, 0, 6, -1, -1, -1, -1, -1, -1], + [1, 3, 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 1, 9, 8, 3, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1], + [2, 4, 3, 2, 10, 9, 0, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 6, 11, 7, 3, 2, 10, 9, 8, -1, -1, -1, -1, -1, -1], + [1, 4, 2, 3, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 6, 2, 0, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 2, 3, 7, 6, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 6, 2, 1, 9, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 1, 3, 7, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 10, 1, 7, 6, 8, 7, 1, 0, -1, -1, -1, -1, -1, -1], + [1, 6, 10, 9, 0, 3, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 7, 6, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 6, 11, 8, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 0, 4, 6, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 6, 11, 8, 4, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 6, 11, 3, 1, 9, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 6, 11, 8, 4, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 1, 2, 10, 11, 3, 0, 4, 6, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 4, 6, 11, 8, 2, 10, 9, 0, -1, -1, -1, -1, -1, -1], + [1, 7, 10, 9, 4, 6, 11, 3, 2, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 4, 6, 2, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 4, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 1, 9, 0, 3, 8, 4, 6, 2, -1, -1, -1, -1, -1, -1], + [1, 5, 1, 9, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 1, 3, 8, 4, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 10, 1, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 4, 6, 10, 9, 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 4, 6, 10, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1], + [2, 4, 3, 0, 1, 5, 4, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 11, 7, 6, 4, 8, 3, 1, 5, -1, -1, -1, -1, -1, -1], + [3, 3, 3, 3, 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1], + [4, 3, 3, 3, 3, 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5], + [2, 3, 5, 7, 6, 11, 10, 5, 4, 0, 2, -1, -1, -1, -1, -1, -1], + [3, 4, 4, 3, 5, 3, 2, 10, 4, 8, 3, 5, 6, 11, 7, 6, -1], + [2, 4, 3, 2, 3, 7, 6, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 9, 5, 4, 8, 7, 6, 2, 0, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 3, 7, 6, 2, 0, 1, 5, 4, -1, -1, -1, -1, -1, -1], + [1, 7, 6, 2, 1, 5, 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 9, 5, 4, 6, 10, 1, 3, 7, -1, -1, -1, -1, -1, -1], + [3, 4, 4, 3, 0, 8, 7, 1, 6, 10, 1, 7, 9, 5, 4, -1, -1], + [1, 7, 4, 0, 3, 7, 6, 10, 5, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 4, 8, 10, 5, 7, 6, 10, 8, -1, -1, -1, -1, -1, -1], + [1, 5, 11, 8, 9, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 0, 9, 5, 6, 6, 11, 3, 0, -1, -1, -1, -1, -1, -1], + [1, 6, 0, 1, 5, 6, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 6, 11, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 1, 2, 10, 5, 6, 11, 8, 9, -1, -1, -1, -1, -1, -1], + [3, 4, 4, 3, 11, 3, 0, 6, 9, 5, 6, 0, 2, 10, 1, 2, 10], + [1, 7, 11, 8, 0, 2, 10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 6, 11, 3, 5, 10, 5, 3, 2, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 3, 8, 9, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 9, 5, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 1, 5, 6, 2, 3, 8, 0, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 1, 5, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 1, 3, 8, 9, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 5, 6, 0, 9, 10, 1, 0, 6, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 5, 10, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 5, 10, 11, 7, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 3, 5, 10, 11, 7, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 10, 11, 7, 5, 1, 9, 8, 3, -1, -1, -1, -1, -1, -1], + [1, 5, 7, 5, 1, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 0, 8, 3, 2, 11, 7, 5, 1, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 11, 7, 5, 9, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 7, 5, 9, 8, 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 3, 7, 5, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 5, 10, 2, 0, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 9, 0, 1, 10, 2, 3, 7, 5, -1, -1, -1, -1, -1, -1], + [1, 7, 9, 8, 7, 5, 10, 2, 1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 3, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 0, 8, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 9, 0, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 7, 5, 9, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 10, 11, 8, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 0, 4, 5, 10, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 0, 1, 9, 4, 5, 10, 11, 8, -1, -1, -1, -1, -1, -1], + [1, 7, 10, 11, 3, 1, 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 11, 8, 4, 5, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 0, 4, 5, 1, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 0, 2, 11, 8, 4, 5, 9, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 2, 3, 5, 10, 4, 5, 3, 8, -1, -1, -1, -1, -1, -1], + [1, 5, 5, 10, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [3, 4, 4, 3, 3, 5, 10, 2, 8, 4, 5, 3, 0, 1, 9, -1, -1], + [1, 6, 10, 2, 1, 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 8, 4, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 0, 4, 5, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 0, 3, 5, 9, 8, 4, 5, 3, -1, -1, -1, -1, -1, -1], + [1, 3, 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 9, 10, 11, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 5, 0, 8, 3, 7, 4, 9, 10, 11, -1, -1, -1, -1, -1, -1], + [1, 6, 1, 10, 11, 7, 4, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 3, 1, 10, 11, 7, 4, 8, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 2, 11, 9, 1, 4, 9, 11, 7, -1, -1, -1, -1, -1, -1], + [3, 4, 4, 3, 1, 2, 11, 9, 7, 4, 9, 11, 8, 3, 0, 8, 3], + [1, 5, 11, 7, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 11, 7, 4, 2, 3, 2, 4, 8, -1, -1, -1, -1, -1, -1], + [1, 6, 2, 3, 7, 4, 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 9, 10, 2, 0, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 7, 3, 7, 4, 0, 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 3, 3, 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 4, 9, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 8, 7, 1, 0, 4, 9, 1, 7, -1, -1, -1, -1, -1, -1], + [1, 4, 3, 7, 4, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 8, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 3, 0, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 0, 1, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 3, 1, 10, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 1, 2, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 2, 11, 9, 1, 3, 0, 9, 11, -1, -1, -1, -1, -1, -1], + [1, 4, 0, 2, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 5, 2, 3, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 2, 0, 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [2, 4, 4, 2, 3, 8, 10, 1, 10, 8, 0, -1, -1, -1, -1, -1, -1], + [1, 3, 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 4, 1, 3, 8, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [1, 3, 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], + [0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] ]; diff --git a/src/math.rs b/src/math.rs deleted file mode 100644 index 42b4ead..0000000 --- a/src/math.rs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2018 Tristam MacDonald -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std; - -/// A 3 dimensional vector -/// -/// Ideally we'd reuse an exiting geometry library, but in the interest both of minimising -/// dependencies, and of compatibility with multiple geometry libraries, we'll define our own. -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] -pub struct Vec3 { - pub x: f32, - pub y: f32, - pub z: f32, -} - -impl Vec3 { - /// Create a vector - pub fn new(x: f32, y: f32, z: f32) -> Self { - Self { x, y, z } - } - - /// Create a vector with all coordinates set to zero - pub fn zero() -> Self { - Self { - x: 0.0, - y: 0.0, - z: 0.0, - } - } - - /// Create a vector with all coordinates set to one - pub fn one() -> Self { - Self { - x: 1.0, - y: 1.0, - z: 1.0, - } - } - - /// Create a vector by taking the absolute value of each component in this vector - pub fn abs(&self) -> Self { - Self { - x: self.x.abs(), - y: self.y.abs(), - z: self.z.abs(), - } - } - - /// Sum all of the components in this vector - pub fn component_sum(&self) -> f32 { - self.x + self.y + self.z - } - - /// Find the maximum value out of all components in this vector - pub fn component_max(&self) -> f32 { - self.x.max(self.y.max(self.z)) - } - - /// Find the minimum value out of all components in this vector - pub fn component_min(&self) -> f32 { - self.x.min(self.y.min(self.z)) - } -} - -impl std::ops::Add for Vec3 { - type Output = Vec3; - - fn add(self, other: Vec3) -> Vec3 { - Vec3::new(self.x + other.x, self.y + other.y, self.z + other.z) - } -} - -impl std::ops::Sub for Vec3 { - type Output = Vec3; - - fn sub(self, other: Vec3) -> Vec3 { - Vec3::new(self.x - other.x, self.y - other.y, self.z - other.z) - } -} - -impl std::ops::Mul for Vec3 { - type Output = Vec3; - - fn mul(self, other: Vec3) -> Vec3 { - Vec3::new(self.x * other.x, self.y * other.y, self.z * other.z) - } -} - -impl std::ops::Mul for Vec3 { - type Output = Vec3; - - fn mul(self, other: f32) -> Vec3 { - Vec3::new(self.x * other, self.y * other, self.z * other) - } -} -impl std::ops::Mul for f32 { - type Output = Vec3; - - fn mul(self, other: Vec3) -> Vec3 { - Vec3::new(self * other.x, self * other.y, self * other.z) - } -} - -impl std::ops::Div for Vec3 { - type Output = Vec3; - - fn div(self, other: Vec3) -> Vec3 { - Vec3::new(self.x / other.x, self.y / other.y, self.z / other.z) - } -} - -impl std::ops::Div for Vec3 { - type Output = Vec3; - - fn div(self, other: f32) -> Vec3 { - Vec3::new(self.x / other, self.y / other, self.z / other) - } -} - -impl std::ops::Div for f32 { - type Output = Vec3; - - fn div(self, other: Vec3) -> Vec3 { - Vec3::new(self / other.x, self / other.y, self / other.z) - } -} diff --git a/src/math/mod.rs b/src/math/mod.rs new file mode 100644 index 0000000..9bef72a --- /dev/null +++ b/src/math/mod.rs @@ -0,0 +1,27 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pub mod svd; +pub mod vector; + +pub use vector::*; + +use std::ops::{Add, Mul}; + +pub fn lerp(a: T, b: T, f: f32) -> T +where + f32: Mul, + T: Add, +{ + (1.0 - f) * a + f * b +} diff --git a/src/math/svd.rs b/src/math/svd.rs new file mode 100644 index 0000000..35d723d --- /dev/null +++ b/src/math/svd.rs @@ -0,0 +1,986 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This is a mostly machine translation of the SVD from Ronen Tzur's dual +//! contouring sample + +#![allow( + dead_code, + mutable_transmutes, + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + unused_assignments, + unused_mut, + clippy::all +)] + +use crate::math::Vec3; + +pub struct SVD { + rows: usize, + u: [[f64; 3]; 12], + v: [[f64; 3]; 3], + d: [f64; 3], +} + +impl SVD { + pub fn new(mat: &[[f64; 3]]) -> Self { + let rows = mat.len(); + + // perform singular value decomposition on matrix mat + // into u, v and d. + // u is a matrix of rows x 3 (same as mat); + // v is a square matrix 3 x 3 (for 3 columns in mat); + // d is vector of 3 values representing the diagonal + // matrix 3 x 3 (for 3 colums in mat). + let mut u: [[f64; 3]; 12] = [[0.; 3]; 12]; + let mut v: [[f64; 3]; 3] = [[0.; 3]; 3]; + let mut d: [f64; 3] = [0.; 3]; + + unsafe { + computeSVD( + mat.as_ptr(), + u.as_mut_ptr(), + v.as_mut_ptr(), + d.as_mut_ptr(), + rows, + ) + }; + + Self { rows, u, v, d } + } + + pub fn diagonal(&mut self) -> &mut [f64] { + &mut self.d + } + + pub fn solve(mut self, vec: &[f64]) -> Vec3 { + let mut point = [0.0; 3]; + let mut v = vec.to_vec(); + + // solve linear system given by mat and vec using the + // singular value decomposition of mat into u, v and d. + if self.d[2 as usize as usize] < 0.1f64 { + self.d[2 as usize as usize] = 0.0f64 + } + if self.d[1 as usize as usize] < 0.1f64 { + self.d[1 as usize as usize] = 0.0f64 + } + if self.d[0 as usize as usize] < 0.1f64 { + self.d[0 as usize as usize] = 0.0f64 + } + + unsafe { + solveSVD( + self.u.as_mut_ptr(), + self.v.as_mut_ptr(), + self.d.as_mut_ptr(), + v.as_mut_ptr(), + point.as_mut_ptr(), + self.rows, + ) + }; + + Vec3::new(point[0] as f32, point[1] as f32, point[2] as f32) + } +} + +//---------------------------------------------------------------------------- +#[no_mangle] +unsafe extern "C" fn evaluateSVD( + mat: *const [f64; 3], + mut vec: *mut f64, + mut rows: usize, + mut point: *mut f64, +) { + // perform singular value decomposition on matrix mat + // into u, v and d. + // u is a matrix of rows x 3 (same as mat); + // v is a square matrix 3 x 3 (for 3 columns in mat); + // d is vector of 3 values representing the diagonal + // matrix 3 x 3 (for 3 colums in mat). + let mut u: [[f64; 3]; 12] = [[0.; 3]; 12]; + let mut v: [[f64; 3]; 3] = [[0.; 3]; 3]; + let mut d: [f64; 3] = [0.; 3]; + computeSVD(mat, u.as_mut_ptr(), v.as_mut_ptr(), d.as_mut_ptr(), rows); + // solve linear system given by mat and vec using the + // singular value decomposition of mat into u, v and d. + if d[2 as usize as usize] < 0.1f64 { + d[2 as usize as usize] = 0.0f64 + } + if d[1 as usize as usize] < 0.1f64 { + d[1 as usize as usize] = 0.0f64 + } + if d[0 as usize as usize] < 0.1f64 { + d[0 as usize as usize] = 0.0f64 + } + let mut x: [f64; 3] = [0.; 3]; + solveSVD( + u.as_mut_ptr(), + v.as_mut_ptr(), + d.as_mut_ptr(), + vec, + x.as_mut_ptr(), + rows, + ); + *point.offset(0) = x[0]; + *point.offset(1) = x[1]; + *point.offset(2) = x[2]; +} +// compute svd +//---------------------------------------------------------------------------- +unsafe extern "C" fn computeSVD( + mat: *const [f64; 3], + mut u: *mut [f64; 3], + mut v: *mut [f64; 3], + mut d: *mut f64, + mut rows: usize, +) { + for i in 0..rows { + *u.offset(i as isize) = *mat.offset(i as isize); + } + let mut tau_u: *mut f64 = d; + let mut tau_v: [f64; 2] = [0.; 2]; + factorize(u, tau_u, tau_v.as_mut_ptr(), rows); + unpack(u, v, tau_u, tau_v.as_mut_ptr(), rows); + diagonalize(u, v, tau_u, tau_v.as_mut_ptr(), rows); + singularize(u, v, tau_u, rows); +} +// factorize +//---------------------------------------------------------------------------- +unsafe extern "C" fn factorize( + mut mat: *mut [f64; 3], + mut tau_u: *mut f64, + mut tau_v: *mut f64, + mut rows: usize, +) { + let mut y: usize = 0; + // bidiagonal factorization of (rows x 3) matrix into :- + // tau_u, a vector of 1x3 (for 3 columns in the matrix) + // tau_v, a vector of 1x2 (one less column than the matrix) + let mut i: usize = 0 as usize; + while i < 3 as usize { + // set up a vector to reference into the matrix + // from mat(i,i) to mat(m,i), that is, from the + // i'th column of the i'th row and down all the way + // through that column + let mut ptrs: [*mut f64; 12] = [0 as *mut f64; 12]; + let mut num_ptrs: usize = rows - i; + let mut q: usize = 0 as usize; + while q < num_ptrs { + ptrs[q as usize] = &mut *(*mat.offset((q + i) as isize)) + .as_mut_ptr() + .offset(i as isize) as *mut f64; + q += 1 + } + // perform householder transformation on this vector + let mut tau: f64 = factorize_hh(ptrs.as_mut_ptr(), num_ptrs); + *tau_u.offset(i as isize) = tau; + // all computations below this point are performed + // only for the first two columns: i=0 or i=1 + if (i + 1 as usize) < 3 as usize { + // perform householder transformation on the matrix + // mat(i,i+1) to mat(m,n), that is, on the sub-matrix + // that begins in the (i+1)'th column of the i'th + // row and extends to the end of the matrix at (m,n) + if tau != 0.0f64 { + let mut x: usize = i + 1 as usize; + while x < 3 as usize { + let mut wx: f64 = (*mat.offset(i as isize))[x as usize]; + y = i + 1 as usize; + while y < rows { + wx += (*mat.offset(y as isize))[x as usize] * *ptrs[(y - i) as usize]; + y += 1 + } + let mut tau_wx: f64 = tau * wx; + (*mat.offset(i as isize))[x as usize] -= tau_wx; + y = i + 1 as usize; + while y < rows { + (*mat.offset(y as isize))[x as usize] -= tau_wx * *ptrs[(y - i) as usize]; + y += 1 + } + x += 1 + } + } + // perform householder transformation on i'th row + // (remember at this point, i is either 0 or 1) + // set up a vector to reference into the matrix + // from mat(i,i+1) to mat(i,n), that is, from the + // (i+1)'th column of the i'th row and all the way + // through to the end of that row + ptrs[0 as usize as usize] = &mut *(*mat.offset(i as isize)) + .as_mut_ptr() + .offset((i + 1 as usize) as isize) + as *mut f64; // i == 1 + if i == 0 as usize { + ptrs[1 as usize as usize] = &mut *(*mat.offset(i as isize)) + .as_mut_ptr() + .offset((i + 2 as usize) as isize) + as *mut f64; + num_ptrs = 2 as usize + } else { + num_ptrs = 1 as usize + } + // perform householder transformation on this vector + tau = factorize_hh(ptrs.as_mut_ptr(), num_ptrs); + *tau_v.offset(i as isize) = tau; + // perform householder transformation on the sub-matrix + // mat(i+1,i+1) to mat(m,n), that is, on the sub-matrix + // that begins in the (i+1)'th column of the (i+1)'th + // row and extends to the end of the matrix at (m,n) + if tau != 0.0f64 { + y = i + 1 as usize; + while y < rows { + let mut wy: f64 = (*mat.offset(y as isize))[(i + 1 as usize) as usize]; + if i == 0 as usize { + wy += (*mat.offset(y as isize))[(i + 2 as usize) as usize] + * *ptrs[1 as usize as usize] + } + let mut tau_wy: f64 = tau * wy; + (*mat.offset(y as isize))[(i + 1 as usize) as usize] -= tau_wy; + if i == 0 as usize { + (*mat.offset(y as isize))[(i + 2 as usize) as usize] -= + tau_wy * *ptrs[1 as usize as usize] + } + y += 1 + } + } + } + i += 1 + } +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn factorize_hh(mut ptrs: *mut *mut f64, mut n: usize) -> f64 { + let mut tau: f64 = 0.0f64; + if n > 1 as usize { + let mut xnorm: f64 = 0.; + if n == 2 as usize { + xnorm = (**ptrs.offset(1 as usize as isize)).abs() + } else { + let mut scl: f64 = 0.0f64; + let mut ssq: f64 = 1.0f64; + let mut i: usize = 1 as usize; + while i < n { + let mut x: f64 = (**ptrs.offset(i as isize)).abs(); + if x != 0.0f64 { + if scl < x { + ssq = 1.0f64 + ssq * (scl / x) * (scl / x); + scl = x + } else { + ssq += x / scl * (x / scl) + } + } + i += 1 + } + xnorm = scl * (ssq).sqrt() + } + if xnorm != 0.0f64 { + let mut alpha: f64 = **ptrs.offset(0 as usize as isize); + let mut beta: f64 = (alpha * alpha + xnorm * xnorm).sqrt(); + if alpha >= 0.0f64 { + beta = -beta + } + tau = (beta - alpha) / beta; + let mut scl_0: f64 = 1.0f64 / (alpha - beta); + **ptrs.offset(0 as usize as isize) = beta; + let mut i_0: usize = 1 as usize; + while i_0 < n { + **ptrs.offset(i_0 as isize) *= scl_0; + i_0 += 1 + } + } + } + return tau; +} +// unpack +//---------------------------------------------------------------------------- +unsafe extern "C" fn unpack( + mut u: *mut [f64; 3], + mut v: *mut [f64; 3], + mut tau_u: *mut f64, + mut tau_v: *mut f64, + mut rows: usize, +) { + let mut i: usize = 0; + let mut y: usize = 0; + // reset v to the identity matrix + let ref mut fresh0 = (*v.offset(2 as usize as isize))[2 as usize as usize]; + *fresh0 = 1.0f64; + let ref mut fresh1 = (*v.offset(1 as usize as isize))[1 as usize as usize]; + *fresh1 = *fresh0; + (*v.offset(0 as usize as isize))[0 as usize as usize] = *fresh1; + let ref mut fresh2 = (*v.offset(2 as usize as isize))[1 as usize as usize]; + *fresh2 = 0.0f64; + let ref mut fresh3 = (*v.offset(2 as usize as isize))[0 as usize as usize]; + *fresh3 = *fresh2; + let ref mut fresh4 = (*v.offset(1 as usize as isize))[2 as usize as usize]; + *fresh4 = *fresh3; + let ref mut fresh5 = (*v.offset(1 as usize as isize))[0 as usize as usize]; + *fresh5 = *fresh4; + let ref mut fresh6 = (*v.offset(0 as usize as isize))[2 as usize as usize]; + *fresh6 = *fresh5; + (*v.offset(0 as usize as isize))[1 as usize as usize] = *fresh6; + + for i in (0..=1).rev() { + let mut tau: f64 = *tau_v.offset(i as isize); + // perform householder transformation on the sub-matrix + // v(i+1,i+1) to v(m,n), that is, on the sub-matrix of v + // that begins in the (i+1)'th column of the (i+1)'th row + // and extends to the end of the matrix at (m,n). the + // householder vector used to perform this is the vector + // from u(i,i+1) to u(i,n) + if tau != 0.0f64 { + let mut x: usize = i + 1 as usize; + while x < 3 as usize { + let mut wx: f64 = (*v.offset((i + 1 as usize) as isize))[x as usize]; + y = i + 1 as usize + 1 as usize; + while y < 3 as usize { + wx += (*v.offset(y as isize))[x as usize] * (*u.offset(i as isize))[y as usize]; + y += 1 + } + let mut tau_wx: f64 = tau * wx; + (*v.offset((i + 1 as usize) as isize))[x as usize] -= tau_wx; + y = i + 1 as usize + 1 as usize; + while y < 3 as usize { + (*v.offset(y as isize))[x as usize] -= + tau_wx * (*u.offset(i as isize))[y as usize]; + y += 1 + } + x += 1 + } + } + } + // copy superdiagonal of u into tau_v + i = 0 as usize; + while i < 2 as usize { + *tau_v.offset(i as isize) = (*u.offset(i as isize))[(i + 1 as usize) as usize]; + i += 1 + } + // below, same idea for u: householder transformations + // and the superdiagonal copy + for i in (0..=2).rev() { + // copy superdiagonal of u into tau_u + let mut tau_0: f64 = *tau_u.offset(i as isize); + *tau_u.offset(i as isize) = (*u.offset(i as isize))[i as usize]; + // perform householder transformation on the sub-matrix + // u(i,i) to u(m,n), that is, on the sub-matrix of u that + // begins in the i'th column of the i'th row and extends + // to the end of the matrix at (m,n). the householder + // vector used to perform this is the i'th column of u, + // that is, u(0,i) to u(m,i) + if tau_0 == 0.0f64 { + (*u.offset(i as isize))[i as usize] = 1.0f64; + if i < 2 as usize { + (*u.offset(i as isize))[2 as usize as usize] = 0.0f64; + if i < 1 as usize { + (*u.offset(i as isize))[1 as usize as usize] = 0.0f64 + } + } + y = i + 1 as usize; + while y < rows { + (*u.offset(y as isize))[i as usize] = 0.0f64; + y += 1 + } + } else { + let mut x_0: usize = i + 1 as usize; + while x_0 < 3 as usize { + let mut wx_0: f64 = 0.0f64; + y = i + 1 as usize; + while y < rows { + wx_0 += + (*u.offset(y as isize))[x_0 as usize] * (*u.offset(y as isize))[i as usize]; + y += 1 + } + let mut tau_wx_0: f64 = tau_0 * wx_0; + (*u.offset(i as isize))[x_0 as usize] = -tau_wx_0; + y = i + 1 as usize; + while y < rows { + (*u.offset(y as isize))[x_0 as usize] -= + tau_wx_0 * (*u.offset(y as isize))[i as usize]; + y += 1 + } + x_0 += 1 + } + y = i + 1 as usize; + while y < rows { + (*u.offset(y as isize))[i as usize] = (*u.offset(y as isize))[i as usize] * -tau_0; + y += 1 + } + (*u.offset(i as isize))[i as usize] = 1.0f64 - tau_0 + } + } +} +// diagonalize +//---------------------------------------------------------------------------- +unsafe extern "C" fn diagonalize( + mut u: *mut [f64; 3], + mut v: *mut [f64; 3], + mut tau_u: *mut f64, + mut tau_v: *mut f64, + mut rows: usize, +) { + let mut i: usize = 0; + let mut j: usize = 0; + chop(tau_u, tau_v, 3 as usize); + // progressively reduce the matrices into diagonal form + let mut b: usize = 3 as usize - 1 as usize; + while b > 0 as usize { + if *tau_v.offset((b - 1 as usize) as isize) == 0.0f64 { + b -= 1 + } else { + let mut a: usize = b - 1 as usize; + while a > 0 as usize && *tau_v.offset((a - 1 as usize) as isize) != 0.0f64 { + a -= 1 + } + let mut n: usize = b - a + 1 as usize; + let mut u1: [[f64; 3]; 12] = [[0.; 3]; 12]; + let mut v1: [[f64; 3]; 3] = [[0.; 3]; 3]; + j = a; + while j <= b { + i = 0 as usize; + while i < rows { + u1[i as usize][(j - a) as usize] = (*u.offset(i as isize))[j as usize]; + i += 1 + } + i = 0 as usize; + while i < 3 as usize { + v1[i as usize][(j - a) as usize] = (*v.offset(i as isize))[j as usize]; + i += 1 + } + j += 1 + } + qrstep( + u1.as_mut_ptr(), + v1.as_mut_ptr(), + &mut *tau_u.offset(a as isize), + &mut *tau_v.offset(a as isize), + rows, + n, + ); + j = a; + while j <= b { + i = 0 as usize; + while i < rows { + (*u.offset(i as isize))[j as usize] = u1[i as usize][(j - a) as usize]; + i += 1 + } + i = 0 as usize; + while i < 3 as usize { + (*v.offset(i as isize))[j as usize] = v1[i as usize][(j - a) as usize]; + i += 1 + } + j += 1 + } + chop( + &mut *tau_u.offset(a as isize), + &mut *tau_v.offset(a as isize), + n, + ); + } + } +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn chop(mut a: *mut f64, mut b: *mut f64, mut n: usize) { + let mut ai: f64 = *a.offset(0 as usize as isize); + let mut i: usize = 0 as usize; + while i < n - 1 as usize { + let mut bi: f64 = *b.offset(i as isize); + let mut ai1: f64 = *a.offset((i + 1 as usize) as isize); + if bi.abs() < 1e-5f64 * (ai.abs() + ai1.abs()) { + *b.offset(i as isize) = 0.0f64 + } + ai = ai1; + i += 1 + } +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn qrstep( + mut u: *mut [f64; 3], + mut v: *mut [f64; 3], + mut tau_u: *mut f64, + mut tau_v: *mut f64, + mut rows: usize, + mut cols: usize, +) { + let mut i: usize = 0; + if cols == 2 as usize { + qrstep_cols2(u, v, tau_u, tau_v, rows); + return; + } + if cols == 1 as usize { + let mut bomb: *mut std::os::raw::c_char = 0 as *mut std::os::raw::c_char; + *bomb = 0 as usize as std::os::raw::c_char + } + // handle zeros on the diagonal or at its end + i = 0 as usize; + while i < cols - 1 as usize { + if *tau_u.offset(i as isize) == 0.0f64 { + qrstep_middle(u, tau_u, tau_v, rows, cols, i); + return; + } + i += 1 + } + if *tau_u.offset((cols - 1 as usize) as isize) == 0.0f64 { + qrstep_end(v, tau_u, tau_v); + return; + } + // perform qr reduction on the diagonal and off-diagonal + let mut mu: f64 = qrstep_eigenvalue(tau_u, tau_v); + let mut y: f64 = *tau_u.offset(0 as usize as isize) * *tau_u.offset(0 as usize as isize) - mu; + let mut z: f64 = *tau_u.offset(0 as usize as isize) * *tau_v.offset(0 as usize as isize); + let mut ak: f64 = 0.0f64; + let mut bk: f64 = 0.0f64; + let mut zk: f64 = 0.; + let mut ap: f64 = *tau_u.offset(0 as usize as isize); + let mut bp: f64 = *tau_v.offset(0 as usize as isize); + let mut aq: f64 = *tau_u.offset(1 as usize as isize); + let mut k: usize = 0 as usize; + while k < cols - 1 as usize { + let mut c: f64 = 0.; + let mut s: f64 = 0.; + // perform Givens rotation on V + computeGivens(y, z, &mut c, &mut s); + i = 0 as usize; + while i < 3 as usize { + let mut vip: f64 = (*v.offset(i as isize))[k as usize]; + let mut viq: f64 = (*v.offset(i as isize))[(k + 1 as usize) as usize]; + (*v.offset(i as isize))[k as usize] = vip * c - viq * s; + (*v.offset(i as isize))[(k + 1 as usize) as usize] = vip * s + viq * c; + i += 1 + } + // perform Givens rotation on B + let mut bk1: f64 = bk * c - z * s; + let mut ap1: f64 = ap * c - bp * s; + let mut bp1: f64 = ap * s + bp * c; + let mut zp1: f64 = aq * -s; + let mut aq1: f64 = aq * c; + if k > 0 as usize { + *tau_v.offset((k - 1 as usize) as isize) = bk1 + } + ak = ap1; + bk = bp1; + zk = zp1; + ap = aq1; + if k < cols - 2 as usize { + bp = *tau_v.offset((k + 1 as usize) as isize) + } else { + bp = 0.0f64 + } + y = ak; + z = zk; + // perform Givens rotation on U + computeGivens(y, z, &mut c, &mut s); + i = 0 as usize; + while i < rows { + let mut uip: f64 = (*u.offset(i as isize))[k as usize]; + let mut uiq: f64 = (*u.offset(i as isize))[(k + 1 as usize) as usize]; + (*u.offset(i as isize))[k as usize] = uip * c - uiq * s; + (*u.offset(i as isize))[(k + 1 as usize) as usize] = uip * s + uiq * c; + i += 1 + } + // perform Givens rotation on B + let mut ak1: f64 = ak * c - zk * s; + bk1 = bk * c - ap * s; + let mut zk1: f64 = bp * -s; + ap1 = bk * s + ap * c; + bp1 = bp * c; + *tau_u.offset(k as isize) = ak1; + ak = ak1; + bk = bk1; + zk = zk1; + ap = ap1; + bp = bp1; + if k < cols - 2 as usize { + aq = *tau_u.offset((k + 2 as usize) as isize) + } else { + aq = 0.0f64 + } + y = bk; + z = zk; + k += 1 + } + *tau_v.offset((cols - 2 as usize) as isize) = bk; + *tau_u.offset((cols - 1 as usize) as isize) = ap; +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn qrstep_middle( + mut u: *mut [f64; 3], + mut tau_u: *mut f64, + mut tau_v: *mut f64, + mut rows: usize, + mut cols: usize, + mut col: usize, +) { + let mut x: f64 = *tau_v.offset(col as isize); + let mut y: f64 = *tau_u.offset((col + 1 as usize) as isize); + let mut j: usize = col; + while j < cols - 1 as usize { + let mut c: f64 = 0.; + let mut s: f64 = 0.; + // perform Givens rotation on U + computeGivens(y, -x, &mut c, &mut s); + let mut i: usize = 0 as usize; + while i < rows { + let mut uip: f64 = (*u.offset(i as isize))[col as usize]; + let mut uiq: f64 = (*u.offset(i as isize))[(j + 1 as usize) as usize]; + (*u.offset(i as isize))[col as usize] = uip * c - uiq * s; + (*u.offset(i as isize))[(j + 1 as usize) as usize] = uip * s + uiq * c; + i += 1 + } + // perform transposed Givens rotation on B + *tau_u.offset((j + 1 as usize) as isize) = x * s + y * c; + if j == col { + *tau_v.offset(j as isize) = x * c - y * s + } + if j < cols - 2 as usize { + let mut z: f64 = *tau_v.offset((j + 1 as usize) as isize); + *tau_v.offset((j + 1 as usize) as isize) *= c; + x = z * -s; + y = *tau_u.offset((j + 2 as usize) as isize) + } + j += 1 + } +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn qrstep_end(mut v: *mut [f64; 3], mut tau_u: *mut f64, mut tau_v: *mut f64) { + let mut x: f64 = *tau_u.offset(1 as usize as isize); + let mut y: f64 = *tau_v.offset(1 as usize as isize); + for k in (0..=1).rev() { + let mut c: f64 = 0.; + let mut s: f64 = 0.; + // perform Givens rotation on V + computeGivens(x, y, &mut c, &mut s); + let mut i: usize = 0 as usize; + while i < 3 as usize { + let mut vip: f64 = (*v.offset(i as isize))[k as usize]; + let mut viq: f64 = (*v.offset(i as isize))[2 as usize as usize]; + (*v.offset(i as isize))[k as usize] = vip * c - viq * s; + (*v.offset(i as isize))[2 as usize as usize] = vip * s + viq * c; + i += 1 + } + // perform Givens rotation on B + *tau_u.offset(k as isize) = x * c - y * s; + if k == 1 as usize { + *tau_v.offset(k as isize) = x * s + y * c + } + if k > 0 as usize { + let mut z: f64 = *tau_v.offset((k - 1 as usize) as isize); + *tau_v.offset((k - 1 as usize) as isize) *= c; + x = *tau_u.offset((k - 1 as usize) as isize); + y = z * s + } + } +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn qrstep_eigenvalue(mut tau_u: *mut f64, mut tau_v: *mut f64) -> f64 { + let mut ta: f64 = *tau_u.offset(1 as usize as isize) * *tau_u.offset(1 as usize as isize) + + *tau_v.offset(0 as usize as isize) * *tau_v.offset(0 as usize as isize); + let mut tb: f64 = *tau_u.offset(2 as usize as isize) * *tau_u.offset(2 as usize as isize) + + *tau_v.offset(1 as usize as isize) * *tau_v.offset(1 as usize as isize); + let mut tab: f64 = *tau_u.offset(1 as usize as isize) * *tau_v.offset(1 as usize as isize); + let mut dt: f64 = (ta - tb) / 2.0f64; + let mut mu: f64 = 0.; + if dt >= 0.0f64 { + mu = tb - tab * tab / (dt + (dt * dt + tab * tab).sqrt()) + } else { + mu = tb + tab * tab / ((dt * dt + tab * tab).sqrt() - dt) + } + return mu; +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn qrstep_cols2( + mut u: *mut [f64; 3], + mut v: *mut [f64; 3], + mut tau_u: *mut f64, + mut tau_v: *mut f64, + mut rows: usize, +) { + let mut i: usize = 0; + let mut tmp: f64 = 0.; + // eliminate off-diagonal element in [ 0 tau_v0 ] + // [ 0 tau_u1 ] + // to make [ tau_u[0] 0 ] + // [ 0 0 ] + if *tau_u.offset(0 as usize as isize) == 0.0f64 { + let mut c: f64 = 0.; + let mut s: f64 = 0.; + // perform transposed Givens rotation on B + // multiplied by X = [ 0 1 ] + // [ 1 0 ] + computeGivens( + *tau_v.offset(0 as usize as isize), + *tau_u.offset(1 as usize as isize), + &mut c, + &mut s, + ); + *tau_u.offset(0 as usize as isize) = + *tau_v.offset(0 as usize as isize) * c - *tau_u.offset(1 as usize as isize) * s; + *tau_v.offset(0 as usize as isize) = + *tau_v.offset(0 as usize as isize) * s + *tau_u.offset(1 as usize as isize) * c; + *tau_u.offset(1 as usize as isize) = 0.0f64; + // perform Givens rotation on U + i = 0 as usize; + while i < rows { + let mut uip: f64 = (*u.offset(i as isize))[0 as usize as usize]; + let mut uiq: f64 = (*u.offset(i as isize))[1 as usize as usize]; + (*u.offset(i as isize))[0 as usize as usize] = uip * c - uiq * s; + (*u.offset(i as isize))[1 as usize as usize] = uip * s + uiq * c; + i += 1 + } + // multiply V by X, effectively swapping first two columns + i = 0 as usize; + while i < 3 as usize { + tmp = (*v.offset(i as isize))[0 as usize as usize]; + (*v.offset(i as isize))[0 as usize as usize] = + (*v.offset(i as isize))[1 as usize as usize]; + (*v.offset(i as isize))[1 as usize as usize] = tmp; + i += 1 + } + } else if *tau_u.offset(1 as usize as isize) == 0.0f64 { + let mut c_0: f64 = 0.; + let mut s_0: f64 = 0.; + // eliminate off-diagonal element in [ tau_u0 tau_v0 ] + // [ 0 0 ] + // perform Givens rotation on B + computeGivens( + *tau_u.offset(0 as usize as isize), + *tau_v.offset(0 as usize as isize), + &mut c_0, + &mut s_0, + ); + *tau_u.offset(0 as usize as isize) = + *tau_u.offset(0 as usize as isize) * c_0 - *tau_v.offset(0 as usize as isize) * s_0; + *tau_v.offset(0 as usize as isize) = 0.0f64; + // perform Givens rotation on V + i = 0 as usize; + while i < 3 as usize { + let mut vip: f64 = (*v.offset(i as isize))[0 as usize as usize]; + let mut viq: f64 = (*v.offset(i as isize))[1 as usize as usize]; + (*v.offset(i as isize))[0 as usize as usize] = vip * c_0 - viq * s_0; + (*v.offset(i as isize))[1 as usize as usize] = vip * s_0 + viq * c_0; + i += 1 + } + } else { + // make colums orthogonal, + let mut c_1: f64 = 0.; + let mut s_1: f64 = 0.; + // perform Schur rotation on B + computeSchur( + *tau_u.offset(0 as usize as isize), + *tau_v.offset(0 as usize as isize), + *tau_u.offset(1 as usize as isize), + &mut c_1, + &mut s_1, + ); + let mut a11: f64 = + *tau_u.offset(0 as usize as isize) * c_1 - *tau_v.offset(0 as usize as isize) * s_1; + let mut a21: f64 = -*tau_u.offset(1 as usize as isize) * s_1; + let mut a12: f64 = + *tau_u.offset(0 as usize as isize) * s_1 + *tau_v.offset(0 as usize as isize) * c_1; + let mut a22: f64 = *tau_u.offset(1 as usize as isize) * c_1; + // perform Schur rotation on V + i = 0 as usize; + while i < 3 as usize { + let mut vip_0: f64 = (*v.offset(i as isize))[0 as usize as usize]; + let mut viq_0: f64 = (*v.offset(i as isize))[1 as usize as usize]; + (*v.offset(i as isize))[0 as usize as usize] = vip_0 * c_1 - viq_0 * s_1; + (*v.offset(i as isize))[1 as usize as usize] = vip_0 * s_1 + viq_0 * c_1; + i += 1 + } + // eliminate off diagonal elements + if a11 * a11 + a21 * a21 < a12 * a12 + a22 * a22 { + // multiply B by X + tmp = a11; + a11 = a12; + a12 = tmp; + tmp = a21; + a21 = a22; + a22 = tmp; + // multiply V by X, effectively swapping first + // two columns + i = 0 as usize; + while i < 3 as usize { + tmp = (*v.offset(i as isize))[0 as usize as usize]; + (*v.offset(i as isize))[0 as usize as usize] = + (*v.offset(i as isize))[1 as usize as usize]; + (*v.offset(i as isize))[1 as usize as usize] = tmp; + i += 1 + } + } + // perform transposed Givens rotation on B + computeGivens(a11, a21, &mut c_1, &mut s_1); + *tau_u.offset(0 as usize as isize) = a11 * c_1 - a21 * s_1; + *tau_v.offset(0 as usize as isize) = a12 * c_1 - a22 * s_1; + *tau_u.offset(1 as usize as isize) = a12 * s_1 + a22 * c_1; + // perform Givens rotation on U + i = 0 as usize; + while i < rows { + let mut uip_0: f64 = (*u.offset(i as isize))[0 as usize as usize]; + let mut uiq_0: f64 = (*u.offset(i as isize))[1 as usize as usize]; + (*u.offset(i as isize))[0 as usize as usize] = uip_0 * c_1 - uiq_0 * s_1; + (*u.offset(i as isize))[1 as usize as usize] = uip_0 * s_1 + uiq_0 * c_1; + i += 1 + } + }; +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn computeGivens(mut a: f64, mut b: f64, mut c: *mut f64, mut s: *mut f64) { + if b == 0.0f64 { + *c = 1.0f64; + *s = 0.0f64 + } else if b.abs() > a.abs() { + let mut t: f64 = -a / b; + let mut s1: f64 = 1.0f64 / (1 as usize as f64 + t * t).sqrt(); + *s = s1; + *c = s1 * t + } else { + let mut t_0: f64 = -b / a; + let mut c1: f64 = 1.0f64 / (1 as usize as f64 + t_0 * t_0).sqrt(); + *c = c1; + *s = c1 * t_0 + }; +} +//---------------------------------------------------------------------------- +unsafe extern "C" fn computeSchur( + mut a1: f64, + mut a2: f64, + mut a3: f64, + mut c: *mut f64, + mut s: *mut f64, +) { + let mut apq: f64 = a1 * a2 * 2.0f64; + if apq == 0.0f64 { + *c = 1.0f64; + *s = 0.0f64 + } else { + let mut t: f64 = 0.; + let mut tau: f64 = (a2 * a2 + (a3 + a1) * (a3 - a1)) / apq; + if tau >= 0.0f64 { + t = 1.0f64 / (tau + (1.0f64 + tau * tau).sqrt()) + } else { + t = -1.0f64 / ((1.0f64 + tau * tau).sqrt() - tau) + } + *c = 1.0f64 / (1.0f64 + t * t).sqrt(); + *s = t * *c + }; +} +// singularize +//---------------------------------------------------------------------------- +unsafe extern "C" fn singularize( + mut u: *mut [f64; 3], + mut v: *mut [f64; 3], + mut d: *mut f64, + mut rows: usize, +) { + let mut i: usize = 0; + let mut j: usize = 0; + let mut y: usize = 0; + // make singularize values positive + j = 0 as usize; + while j < 3 as usize { + if *d.offset(j as isize) < 0.0f64 { + i = 0 as usize; + while i < 3 as usize { + (*v.offset(i as isize))[j as usize] = -(*v.offset(i as isize))[j as usize]; + i += 1 + } + *d.offset(j as isize) = -*d.offset(j as isize) + } + j += 1 + } + // sort singular values in decreasing order + i = 0 as usize; + while i < 3 as usize { + let mut d_max: f64 = *d.offset(i as isize); + let mut i_max: usize = i; + j = i + 1 as usize; + while j < 3 as usize { + if *d.offset(j as isize) > d_max { + d_max = *d.offset(j as isize); + i_max = j + } + j += 1 + } + if i_max != i { + // swap eigenvalues + let mut tmp: f64 = *d.offset(i as isize); + *d.offset(i as isize) = *d.offset(i_max as isize); + *d.offset(i_max as isize) = tmp; + // swap eigenvectors + y = 0 as usize; + while y < rows { + tmp = (*u.offset(y as isize))[i as usize]; + (*u.offset(y as isize))[i as usize] = (*u.offset(y as isize))[i_max as usize]; + (*u.offset(y as isize))[i_max as usize] = tmp; + y += 1 + } + y = 0 as usize; + while y < 3 as usize { + tmp = (*v.offset(y as isize))[i as usize]; + (*v.offset(y as isize))[i as usize] = (*v.offset(y as isize))[i_max as usize]; + (*v.offset(y as isize))[i_max as usize] = tmp; + y += 1 + } + } + i += 1 + } +} +// solve svd +//---------------------------------------------------------------------------- +unsafe extern "C" fn solveSVD( + mut u: *mut [f64; 3], + mut v: *mut [f64; 3], + mut d: *mut f64, + mut b: *mut f64, + mut x: *mut f64, + mut rows: usize, +) { + let mut i: usize = 0; + let mut j: usize = 0; + // compute vector w = U^T * b + let mut w: [f64; 3] = [0.; 3]; + + i = 0 as usize; + while i < rows { + if *b.offset(i as isize) != 0.0f64 { + j = 0 as usize; + while j < 3 as usize { + w[j as usize] += *b.offset(i as isize) * (*u.offset(i as isize))[j as usize]; + j += 1 + } + } + i += 1 + } + // introduce non-zero singular values in d into w + i = 0 as usize; + while i < 3 as usize { + if *d.offset(i as isize) != 0.0f64 { + w[i as usize] /= *d.offset(i as isize) + } + i += 1 + } + // compute result vector x = V * w + i = 0 as usize; + while i < 3 as usize { + let mut tmp: f64 = 0.0f64; + j = 0 as usize; + while j < 3 as usize { + tmp += w[j as usize] * (*v.offset(i as isize))[j as usize]; + j += 1 + } + *x.offset(i as isize) = tmp; + i += 1 + } +} diff --git a/src/math/vector.rs b/src/math/vector.rs new file mode 100644 index 0000000..6f1dfd9 --- /dev/null +++ b/src/math/vector.rs @@ -0,0 +1,357 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///! Ideally we'd reuse an exiting geometry library, but in the interest both +/// of minimising dependencies, and of compatibility with multiple geometry +/// libraries, we'll define our own. +use std; + +/// A 2 dimensional vector +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct Vec2 { + pub x: f32, + pub y: f32, +} + +/// A 3 dimensional vector +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct Vec3 { + pub x: f32, + pub y: f32, + pub z: f32, +} + +pub fn vec2(x: f32, y: f32) -> Vec2 { + Vec2::new(x, y) +} + +pub fn vec3(x: f32, y: f32, z: f32) -> Vec3 { + Vec3::new(x, y, z) +} + +// Plus and multiply operators can't be used as separators in macro repetition, +// so I use a fold operation instead +macro_rules! fold { + ($op:tt, $x:expr, $y:expr) => { + $x $op $y + }; + ($op:tt, $x:expr, $y:expr, $($rest:expr),+) => { + fold!($op, ($x $op $y), $($rest),*) + } +} + +macro_rules! impl_arithmetic_op { + ($name:ident, $op_name:ident, $op_small_name:ident, $op:tt { $($field:ident ),+ }) => { + impl std::ops::$op_name for $name { + type Output = $name; + fn $op_small_name(self, other: $name) -> $name { + $name::new($(self.$field $op other.$field),*) + } + } + impl std::ops::$op_name for &$name { + type Output = $name; + fn $op_small_name(self, other: &$name) -> $name { + $name::new($(self.$field $op other.$field),*) + } + } + impl std::ops::$op_name<&$name> for $name { + type Output = $name; + fn $op_small_name(self, other: &$name) -> $name { + $name::new($(self.$field $op other.$field),*) + } + } + impl std::ops::$op_name for $name { + type Output = $name; + fn $op_small_name(self, other: f32) -> $name { + $name::new($(self.$field $op other),*) + } + } + impl std::ops::$op_name<$name> for f32 { + type Output = $name; + fn $op_small_name(self, other: $name) -> $name { + $name::new($(self $op other.$field),*) + } + } + }; +} +macro_rules! impl_arithmetic_assign_op { + ($name:ident, $op_assign_name:ident, $op_assign_small_name:ident, $op:tt { $($field:ident ),+ }) => { + impl std::ops::$op_assign_name for $name { + fn $op_assign_small_name(&mut self, other: $name) { + $(self.$field $op other.$field);* + } + } + impl std::ops::$op_assign_name for $name { + fn $op_assign_small_name(&mut self, other: f32) { + $(self.$field $op other);* + } + } + }; +} + +macro_rules! impl_vector { + ($name:ident { $($field:ident ),+ }) => { + impl $name { + /// Create a vector + pub fn new($($field : f32),*) -> Self { + Self { $($field),* } + } + /// Create a vector by repeating a single value + pub fn from_scalar(f: f32) -> Self { + Self { $($field: f),* } + } + + /// Create a vector with all coordinates set to zero + pub fn zero() -> Self { + Self{ $($field: 0.0),*} + } + /// Create a vector with all coordinates set to one + pub fn one() -> Self { + Self{ $($field: 1.0),*} + } + + /// Squared Euclidean length of this vector + pub fn len_sq(&self) -> f32 { + fold!(+, $(self.$field * self.$field),*) + } + + /// Euclidean length of this vector + pub fn len(&self) -> f32 { + self.len_sq().sqrt() + } + + /// Normalised copy of this vector + pub fn normalised(&self) -> Option { + let l = self.len(); + if l.abs() < std::f32::EPSILON { + None + } else { + Some(Self { + $($field: self.$field / l),* + }) + } + } + + /// Create a new vector by applying the provided function to each component in this vector + pub fn map f32>(&self, f: F) -> Self { + $name::new( $(f(self.$field)),* ) + } + /// Test if any component matches a predicate + pub fn any bool>(&self, f: F) -> bool { + fold!(||, $(f(self.$field)),* ) + } + /// Test if every component matches a predicate + pub fn all bool>(&self, f: F) -> bool { + fold!(&&, $(f(self.$field)),* ) + } + } + + impl std::default::Default for $name { + fn default() -> Self { + Self::zero() + } + } + + impl std::ops::Neg for $name { + type Output = $name; + fn neg(self) -> $name { + $name::new($(-self.$field),*) + } + } + + impl std::iter::Sum for $name { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), std::ops::Add::add) + } + } + impl<'a> std::iter::Sum<&'a $name> for $name { + fn sum>(iter: I) -> Self { + iter.fold(Self::zero(), std::ops::Add::add) + } + } + + impl std::iter::Product for $name { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), std::ops::Mul::mul) + } + } + impl<'a> std::iter::Product<&'a $name> for $name { + fn product>(iter: I) -> Self { + iter.fold(Self::one(), std::ops::Mul::mul) + } + } + + impl std::ops::Index for $name { + type Output = f32; + fn index(&self, index: usize) -> &f32 { + [$(&self.$field),*][index] + } + } + + impl std::ops::IndexMut for $name { + fn index_mut(&mut self, index: usize) -> &mut f32 { + [$(&mut self.$field),*][index] + } + } + + impl_arithmetic_op!($name, Add, add, + { $($field),* }); + impl_arithmetic_op!($name, Sub, sub, - { $($field),* }); + impl_arithmetic_op!($name, Mul, mul, * { $($field),* }); + impl_arithmetic_op!($name, Div, div, / { $($field),* }); + impl_arithmetic_assign_op!($name, AddAssign, add_assign, += { $($field),* }); + impl_arithmetic_assign_op!($name, SubAssign, sub_assign, -= { $($field),* }); + impl_arithmetic_assign_op!($name, MulAssign, mul_assign, *= { $($field),* }); + impl_arithmetic_assign_op!($name, DivAssign, div_assign, /= { $($field),* }); + }; +} + +impl_vector!(Vec2 { x, y }); +impl_vector!(Vec3 { x, y, z }); + +impl Vec2 { + pub fn extend(&self, z: f32) -> Vec3 { + vec3(self.x, self.y, z) + } +} + +impl Vec3 { + /// Create a vector by taking the absolute value of each component in this + /// vector + pub fn abs(&self) -> Self { + Self { + x: self.x.abs(), + y: self.y.abs(), + z: self.z.abs(), + } + } + + /// Sum all of the components in this vector + pub fn component_sum(&self) -> f32 { + self.x + self.y + self.z + } + + /// Find the maximum value out of all components in this vector + pub fn max_component(&self) -> f32 { + self.x.max(self.y.max(self.z)) + } + + /// Find the minimum value out of all components in this vector + pub fn min_component(&self) -> f32 { + self.x.min(self.y.min(self.z)) + } + + /// Find the index of the maximum value out of all components in this vector + pub fn max_component_index(&self) -> usize { + if self.x > self.y && self.x > self.z { + 0 + } else if self.y > self.z { + 1 + } else { + 2 + } + } + + /// Find the index of the minimum value out of all components in this vector + pub fn min_component_index(&self) -> usize { + if self.x < self.y && self.x < self.z { + 0 + } else if self.y < self.z { + 1 + } else { + 2 + } + } + + /// Take only the component in this vector that lies along the closest + /// cardinal axis + pub fn clamp_to_cardinal_axis(&self) -> Vec3 { + let p = self.abs(); + if p.x > p.y && p.x > p.z { + Vec3::new(self.x, 0.0, 0.0) + } else if p.y > p.z { + Vec3::new(0.0, self.y, 0.0) + } else { + Vec3::new(0.0, 0.0, self.z) + } + } + + /// Calculate the dot product of this vector and another + pub fn dot(&self, other: Self) -> f32 { + (*self * other).component_sum() + } + + /// Compute the cross product of this vector and another + pub fn cross(&self, rhs: Self) -> Self { + Self { + x: self.y * rhs.z - self.z * rhs.y, + y: self.z * rhs.x - self.x * rhs.z, + z: self.x * rhs.y - self.y * rhs.x, + } + } + + /// Create a vector by taking the min value of each component in this vector + /// and another + pub fn min(&self, other: Self) -> Self { + Self { + x: self.x.min(other.x), + y: self.y.min(other.y), + z: self.z.min(other.z), + } + } + + /// Create a vector by taking the max value of each component in this vector + /// and another + pub fn max(&self, other: Self) -> Self { + Self { + x: self.x.max(other.x), + y: self.y.max(other.y), + z: self.z.max(other.z), + } + } + + /// Create a vector by linearly interpolating between this vector and + /// another + pub fn lerp(&self, other: Self, f: f32) -> Self { + let of = 1.0 - f; + + Self { + x: of * self.x + f * other.x, + y: of * self.y + f * other.y, + z: of * self.z + f * other.z, + } + } + + pub fn yz(&self) -> Vec2 { + vec2(self.y, self.z) + } + + pub fn xz(&self) -> Vec2 { + vec2(self.x, self.z) + } + + pub fn xy(&self) -> Vec2 { + vec2(self.x, self.y) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fold_works() { + assert_eq!(vec2(1.0, 2.0).len_sq(), 5.0); + assert_eq!(vec3(1.0, 2.0, 3.0).len_sq(), 14.0); + } +} diff --git a/src/mesh.rs b/src/mesh.rs new file mode 100644 index 0000000..b7347d6 --- /dev/null +++ b/src/mesh.rs @@ -0,0 +1,260 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{extractor::Extractor, index_cache::IndexCache, math::Vec3}; +use std::{ + collections::{hash_set::Iter as HashSetIter, HashMap, HashSet}, + hash::Hash, +}; + +/// A handle to a specific vertex within a vertex array +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct VertexHandle(usize); + +/// A handle to a specific face within a mesh +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct FaceHandle(usize); + +/// A face within a mesh. Faces are required to be triangles (i.e. composed of +/// exactly 3 vertices and 3 edges) +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct Face([VertexHandle; 3]); + +/// An edge within a mesh. Edges are bidirectional (i.e. Edge(u,v) and Edge(v,u) +/// represent the same edge) +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct Edge(VertexHandle, VertexHandle); + +/// Utility class to store and manipulate the topology of meshes +/// +/// Note that this type does not store the mesh vertices themselves. +/// It is expected that the caller will maintain the actual vertex +/// data, and use this class to generate a suitable set of indices +/// for the mesh topology. +pub struct MeshTopology { + next_vertex: usize, + faces: Vec, + edges: HashSet, + edge_to_face: HashMap>, +} + +impl MeshTopology { + /// Create a new empty MeshTopology + pub fn new() -> Self { + Self { + next_vertex: 0, + faces: vec![], + edges: HashSet::new(), + edge_to_face: HashMap::new(), + } + } + + /// Allocate a new vertex handle. The caller is responsible for + /// storing the actual vertex data associated with this handle. + pub fn add_vertex(&mut self) -> VertexHandle { + let handle = VertexHandle(self.next_vertex); + self.next_vertex += 1; + handle + } + + /// Add a new face, given 3 vertices in counter-clockwise order. + pub fn add_face(&mut self, a: VertexHandle, b: VertexHandle, c: VertexHandle) -> FaceHandle { + let face = FaceHandle(self.faces.len()); + self.faces.push(Face([a, b, c])); + + let edge_a = Edge::new(a, b); + self.edges.insert(edge_a); + self.edge_to_face.entry(edge_a).or_default().push(face); + + let edge_b = Edge::new(b, c); + self.edges.insert(edge_b); + self.edge_to_face.entry(edge_b).or_default().push(face); + + let edge_c = Edge::new(c, a); + self.edges.insert(edge_c); + self.edge_to_face.entry(edge_c).or_default().push(face); + + face + } + + /// Build an index buffer from the mesh, suitable for use by rendering APIs + pub fn extract_indices(&self, extractor: &mut E) + where + E: Extractor, + { + for face in &self.faces { + for v in &face.0 { + extractor.extract_index(v.0); + } + } + } + + /// An iterator over the unique edges in the mesh + pub fn edges(&self) -> HashSetIter { + self.edges.iter() + } + + /// The faces that share a given edge. In an ideal world, meshes would be + /// manifold, and at most 2 faces would share a single edge. However + /// isosurface extraction may produce non-manifold meshes with 3 or more + /// faces sharing an edge. + pub fn adjoining_faces(&self, edge: Edge) -> Vec { + self.edge_to_face + .get(&edge) + .cloned() + .unwrap_or_default() + .iter() + .map(|f| self.faces[f.0]) + .collect() + } + + /// Rotate an edge within the mesh. + /// + /// Given a pair of faces which share the specified edge, this will + /// flip the direction of the common edge, like so: + /// ```text + /// *---* *---* + /// |\ | | /| + /// | \ | ==> | / | + /// | \| |/ | + /// *---* *---* + /// ``` + /// This is useful when the two faces are not co-planar (i.e. the edge + /// represents a crease), and the edge currently runs in the opposite + /// direction to the crease. + /// + /// Edge rotation only works for edges that are shared by exactly 2 faces, + /// so we silently ignore requests to rotate other types of edge. + pub fn rotate_edge(&mut self, edge: Edge) { + if let Some(adjoining) = self.edge_to_face.get(&edge) { + // Only rotate if the edge is adjoining exactly 2 faces + if let [handle_a, handle_b] = adjoining[..] { + let face_a = self.faces[handle_a.0]; + let face_b = self.faces[handle_b.0]; + + // Find the two vertices that aren't on the shared edge + let c = face_a.vertex_opposite(edge); + let d = face_b.vertex_opposite(edge); + + // We don't know which way the edge runs, so use the vertex winding + // to determine if we need to flip it + let (u, v) = if face_a.matches_winding_direction(edge) { + (edge.end(), edge.start()) + } else { + (edge.start(), edge.end()) + }; + + // Overwrite the two faces with the two new faces + self.faces[handle_a.0].0 = [c, d, u]; + self.faces[handle_b.0].0 = [c, v, d]; + + // Add our new edge to the auxiliary tables + let e = Edge::new(c, d); + self.edges.insert(e); + self.edge_to_face.insert(e, vec![handle_a, handle_b]); + + // And finally remove the original edge + self.edges.remove(&edge); + self.edge_to_face.remove(&edge); + } + } + } +} + +impl Edge { + /// Construct a new edge from the two vertices it connects. + /// The edge direction will be normalised during construction. + pub fn new(a: VertexHandle, b: VertexHandle) -> Edge { + if a > b { + Edge(b, a) + } else { + Edge(a, b) + } + } +} + +impl Face { + /// Find the vertex in the face that is not on the provided edge. + /// Note that if you pass an edge that is not part of this face, the + /// result will be an arbitrary vertex on this face. + pub fn vertex_opposite(&self, edge: Edge) -> VertexHandle { + for &v in self.0.iter() { + if v != edge.0 && v != edge.1 { + return v; + } + } + unreachable!(); + } + + fn matches_winding_direction(&self, edge: Edge) -> bool { + for i in 0..3 { + let j = (i + 1) % 3; + + if edge.0 == self.0[i] && edge.1 == self.0[j] { + return true; + } else if edge.0 == self.0[j] && edge.1 == self.0[i] { + return false; + } + } + unreachable!(); + } +} + +impl Edge { + /// The start of the edge. Note that edge directions are normalised. + pub fn start(&self) -> VertexHandle { + self.0 + } + + /// The end of the edge. Note that edge directions are normalised. + pub fn end(&self) -> VertexHandle { + self.1 + } +} + +pub struct MeshTopologyBuilder<'a, K: Eq + Hash + Copy, E: Extractor> { + index_cache: IndexCache, + mesh: MeshTopology, + extractor: &'a mut E, +} + +impl<'a, K: Eq + Hash + Copy, E: Extractor> MeshTopologyBuilder<'a, K, E> { + pub fn new(extractor: &'a mut E) -> Self { + Self { + index_cache: IndexCache::new(), + mesh: MeshTopology::new(), + extractor, + } + } + + pub fn add_vertex(&mut self, key: Option, vertex: Vec3) -> VertexHandle { + if let Some(index) = key.and_then(|k| self.index_cache.get(k)) { + index + } else { + let index = self.mesh.add_vertex(); + if let Some(key) = key { + self.index_cache.put(key, index); + } + self.extractor.extract_vertex(vertex); + index + } + } + + pub fn add_face(&mut self, a: VertexHandle, b: VertexHandle, c: VertexHandle) { + self.mesh.add_face(a, b, c); + } + + pub fn build(self) -> MeshTopology { + self.mesh + } +} diff --git a/src/morton.rs b/src/morton.rs index f6cbf5c..c3c8580 100644 --- a/src/morton.rs +++ b/src/morton.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - use crate::math::Vec3; use std; @@ -69,7 +68,8 @@ impl Morton { Morton((self.0 << 3) | u64::from(which)) } - /// The distance from the center of the octree node to the edge (i.e. half the width/height/depth). + /// The distance from the center of the octree node to the edge (i.e. half + /// the width/height/depth). pub fn size(&self) -> f32 { 1.0 / ((2 << self.level()) as f32) } @@ -115,7 +115,8 @@ impl Morton { ) } - /// Assuming that self is a point on the dual mesh, finds the 8 corresponding vertices on the primal mesh. + /// Assuming that self is a point on the dual mesh, finds the 8 + /// corresponding vertices on the primal mesh. pub fn primal_vertex(&self, level: usize, which: usize) -> Morton { let k = 1 << (3 * level); let k_plus_one = k << 1; @@ -134,7 +135,8 @@ impl Morton { } } - /// Assuming that self is a point on the primal mesh, finds the 8 corresponding vertices on the dual mesh. + /// Assuming that self is a point on the primal mesh, finds the 8 + /// corresponding vertices on the dual mesh. pub fn dual_vertex(&self, level: usize, which: usize) -> Morton { let dk = Morton(self.0 >> (3 * (MAX_LEVEL - level))); diff --git a/src/point_cloud.rs b/src/point_cloud.rs index 380d7c9..f6b571a 100644 --- a/src/point_cloud.rs +++ b/src/point_cloud.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,125 +11,54 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::{ + distance::Distance, extractor::Extractor, marching_cubes_impl::classify_corners, + sampler::Sample, traversal::PrimalGrid, +}; -use crate::marching_cubes_tables::CORNERS; -use crate::source::{HermiteSource, Source}; - -/// Extracts point clouds from distance fields. -pub struct PointCloud { - size: usize, - layers: [Vec; 2], +/// Convert isosurfaces to point clouds +/// +/// Pros: +/// +/// * Blindingly fast. +/// +/// Cons: +/// +/// * Doesn't contain any surface data. Surfaces have to be reconstructed, +/// ideally on the GPU itself. +pub struct PointCloud { + primal_grid: PrimalGrid, } -impl PointCloud { +impl PointCloud { /// Create a new PointCloud with the given chunk size. /// /// For a given `size`, this will evaluate chunks of `size^3` voxels. pub fn new(size: usize) -> Self { PointCloud { - size, - layers: [vec![0f32; size * size], vec![0f32; size * size]], + primal_grid: PrimalGrid::new(size), } } - /// Extracts a point cloud from the given [`Source`](../source/trait.Source.html). - /// - /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the number of steps - /// determined by the size provided to the constructor. + /// Extracts a point cloud from the given [Sample]. /// - /// The midpoints of extracted voxels will be appended to `vertices` as triples of (x, y, z) - /// coordinates. - pub fn extract_midpoints(&mut self, source: &S, vertices: &mut Vec) - where - S: Source, - { - self.extract_impl(source, |x: f32, y: f32, z: f32| { - vertices.push(x); - vertices.push(y); - vertices.push(z); - }); - } - - /// Extracts a point cloud with normal data from the given [`HermiteSource`](../source/trait.HermiteSource.html). + /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the + /// number of steps determined by the size provided to the constructor. /// - /// The Source will be sampled in the range (0,0,0) to (1,1,1), with the number of steps - /// determined by the size provided to the constructor. - /// - /// The midpoints of extracted voxels will be appended to `vertices` as triples of (x, y, z) - /// coordinates, followed by the surface normals as triples of (x, y, z) dimensions. - pub fn extract_midpoints_with_normals(&mut self, source: &S, vertices: &mut Vec) - where - S: HermiteSource, - { - self.extract_impl(source, |x: f32, y: f32, z: f32| { - let n = source.sample_normal(x, y, z); - vertices.push(x); - vertices.push(y); - vertices.push(z); - vertices.push(n.x); - vertices.push(n.y); - vertices.push(n.z); - }); - } - - fn extract_impl(&mut self, source: &S, mut extract: E) + /// The resulting vertex data will be returned via the provided + /// Extractor. Note that no face data will be produced. + pub fn extract(&mut self, source: &S, extractor: &mut E) where - S: Source, - E: FnMut(f32, f32, f32) -> (), + S: Sample, + E: Extractor, { - let size_minus_one = self.size - 1; - let one_over_size = 1.0 / (size_minus_one as f32); + self.primal_grid.traverse(source, |_keys, corners, values| { + let cube_index = classify_corners(&values); - // Cache layer zero of distance field values - for y in 0usize..self.size { - for x in 0..self.size { - self.layers[0][y * self.size + x] = - source.sample(x as f32 * one_over_size, y as f32 * one_over_size, 0.0); + if cube_index != 0 && cube_index != 255 { + let p = corners[0].lerp(corners[6], 0.5); + extractor.extract_vertex(p); } - } - - let mut values = [0f32; 8]; - - for z in 0..self.size { - // Cache layer N+1 of isosurface values - for y in 0..self.size { - for x in 0..self.size { - self.layers[1][y * self.size + x] = source.sample( - x as f32 * one_over_size, - y as f32 * one_over_size, - (z + 1) as f32 * one_over_size, - ); - } - } - - // Extract the cells in the current layer - for y in 0..size_minus_one { - for x in 0..size_minus_one { - for i in 0..8 { - values[i] = self.layers[CORNERS[i][2]] - [(y + CORNERS[i][1]) * self.size + x + CORNERS[i][0]]; - } - - let mut cube_index = 0; - for i in 0usize..8 { - if values[i] <= 0.0 { - cube_index |= 1 << i; - } - } - - if cube_index == 0 || cube_index == 255 { - continue; - } - - let px = (x as f32 + 0.5) * one_over_size; - let py = (y as f32 + 0.5) * one_over_size; - let pz = (z as f32 + 0.5) * one_over_size; - - extract(px, py, pz); - } - } - - self.layers.swap(0, 1); - } + }); } } diff --git a/src/sampler.rs b/src/sampler.rs new file mode 100644 index 0000000..fb130e7 --- /dev/null +++ b/src/sampler.rs @@ -0,0 +1,65 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::{Directed, Distance, Signed}, + math::Vec3, + source::{HermiteSource, ScalarSource, VectorSource}, +}; + +/// Sample a distance field defined in terms of a specific [Distance] metric. +pub trait Sample: Sized { + fn sample(&self, p: Vec3) -> D; +} + +/// Samplers abstract sampling across multiple different [Distance] metrics +pub struct Sampler<'a, S> { + pub source: &'a S, +} + +impl<'a, S> Sampler<'a, S> { + /// Create a new sampler from a source. + pub fn new(source: &'a S) -> Self { + Self { source } + } +} + +impl<'a, S: ScalarSource> Sample for Sampler<'a, S> { + fn sample(&self, p: Vec3) -> Signed { + self.source.sample_scalar(p) + } +} + +impl<'a, S: VectorSource> Sample for Sampler<'a, S> { + fn sample(&self, p: Vec3) -> Directed { + self.source.sample_vector(p) + } +} + +impl<'a, S: ScalarSource> ScalarSource for Sampler<'a, S> { + fn sample_scalar(&self, p: Vec3) -> Signed { + self.source.sample_scalar(p) + } +} + +impl<'a, S: VectorSource + ScalarSource> VectorSource for Sampler<'a, S> { + fn sample_vector(&self, p: Vec3) -> Directed { + self.source.sample_vector(p) + } +} + +impl<'a, S: HermiteSource> HermiteSource for Sampler<'a, S> { + fn sample_normal(&self, p: Vec3) -> Vec3 { + self.source.sample_normal(p) + } +} diff --git a/src/source.rs b/src/source.rs index 1560a67..79b0c8a 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Tristam MacDonald +// Copyright 2021 Tristam MacDonald // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,68 +11,81 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::{ + distance::{Directed, Signed}, + math::Vec3, +}; -use crate::math::Vec3; - -/// The context in which signed distance fields should be evaluated -pub enum Norm { - /// The L^2 or Euclidean norm is the one you are used to, i.e. where l = sqrt(x^2 + y^2 + z^2). - Euclidean, - /// The L^∞ or Max norm represents Manhattan/Taxicab distance, i.e. l = max(|x|, |y|, |z|). - Max, +/// A source capable of sampling a signed distance field at discrete +/// coordinates. +pub trait ScalarSource { + /// Samples the distance field at the given (x, y, z) coordinates. + /// + /// Must return the signed distance (i.e. negative for coordinates inside + /// the surface), as our Marching Cubes implementation will evaluate the + /// surface at the zero-crossing. + fn sample_scalar(&self, p: Vec3) -> Signed; } -/// A source capable of sampling a signed distance field at discrete coordinates. -pub trait Source { - /// Samples the distance field at the given (x, y, z) coordinates. +/// A source capable of sampling a directed distance field at discrete +/// coordinates. +pub trait VectorSource { + /// Samples the directed distance field at the given (x, y, z) coordinates. /// - /// Must return the signed distance (i.e. negative for coordinates inside the surface), - /// as our Marching Cubes implementation will evaluate the surface at the zero-crossing. - fn sample(&self, x: f32, y: f32, z: f32) -> f32; + /// Must return the signed distance (i.e. negative for coordinates inside + /// the surface), as our Marching Cubes implementation will evaluate the + /// surface at the zero-crossing. + fn sample_vector(&self, p: Vec3) -> Directed; } -/// A source capable of evaluating the normal vector to a signed distance field at discrete coordinates. -pub trait HermiteSource: Source { +/// A source capable of evaluating the normal vector to a distance field +/// at discrete coordinates. +pub trait HermiteSource: ScalarSource { /// Samples the distance field at the given (x, y, z) coordinates. /// /// Must return a normal vector to the surface. - fn sample_normal(&self, x: f32, y: f32, z: f32) -> Vec3; + fn sample_normal(&self, p: Vec3) -> Vec3; } -/// Adapts a `Source` to a `HermiteSource` by deriving normals from the surface via central differencing -pub struct CentralDifference { +/// Adapts a [ScalarSource] to a [HermiteSource] by deriving normals from the +/// surface via central differencing. +pub struct CentralDifference { pub source: S, epsilon: f32, } -impl CentralDifference { - /// Create an adaptor from a [Source](trait.Source.html) +impl CentralDifference { + /// Create an adaptor from a [ScalarSource]. pub fn new(source: S) -> Self { - Self { - source, - epsilon: 0.0001, - } + Self::new_with_epsilon(source, 0.000001) } - /// Create an adaptor from a [Source](trait.Source.html) and an epsilon value + /// Create an adaptor from a [ScalarSource] and an epsilon + /// value. pub fn new_with_epsilon(source: S, epsilon: f32) -> Self { Self { source, epsilon } } } -impl Source for CentralDifference { - fn sample(&self, x: f32, y: f32, z: f32) -> f32 { - self.source.sample(x, y, z) +impl ScalarSource for CentralDifference { + fn sample_scalar(&self, p: Vec3) -> Signed { + self.source.sample_scalar(p) + } +} + +impl VectorSource for CentralDifference { + fn sample_vector(&self, p: Vec3) -> Directed { + self.source.sample_vector(p) } } -impl HermiteSource for CentralDifference { - fn sample_normal(&self, x: f32, y: f32, z: f32) -> Vec3 { - let v = self.sample(x, y, z); - let vx = self.sample(x + self.epsilon, y, z); - let vy = self.sample(x, y + self.epsilon, z); - let vz = self.sample(x, y, z + self.epsilon); +impl HermiteSource for CentralDifference { + fn sample_normal(&self, p: Vec3) -> Vec3 { + let v = self.sample_scalar(p); + let vx = self.sample_scalar(p + Vec3::new(self.epsilon, 0.0, 0.0)); + let vy = self.sample_scalar(p + Vec3::new(0.0, self.epsilon, 0.0)); + let vz = self.sample_scalar(p + Vec3::new(0.0, 0.0, self.epsilon)); - Vec3::new(vx - v, vy - v, vz - v) + Vec3::new(vx.0 - v.0, vy.0 - v.0, vz.0 - v.0) } } diff --git a/src/traversal/dual_grid.rs b/src/traversal/dual_grid.rs new file mode 100644 index 0000000..4087382 --- /dev/null +++ b/src/traversal/dual_grid.rs @@ -0,0 +1,96 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::Distance, marching_cubes_tables::CORNERS, math::Vec3, sampler::Sample, + traversal::PrimalGrid, +}; + +/// Traverses over cubes in a dual grid. A dual grid is the grid formed by +/// placing a vertex in the center of each cube in a primal grid, and connecting +/// adjacent vertices into cubes. i.e. each 2x2x2 cube in the dual grid spans a +/// 3x3x3 region in the primal grid. +pub struct DualGrid { + size: usize, + primal_grid: PrimalGrid, + duals: [Vec<(Vec3, D)>; 2], +} + +impl DualGrid { + /// Create a dual grid that spans a primal grid with dimensions NxNxN. + /// The dual grid will have dimension (N-1) along each axis. + pub fn new(size: usize) -> Self { + let size_minus_one = size - 1; + + Self { + size, + primal_grid: PrimalGrid::new(size), + duals: [ + vec![(Vec3::zero(), D::zero()); size_minus_one * size_minus_one], + vec![(Vec3::zero(), D::zero()); size_minus_one * size_minus_one], + ], + } + } + + /// Traverse the dual grid, sampling from the provided Sampler at each point + /// in the primal grid. The vertex callback, if provided, will be + /// invoked to adjust the location of each dual vertex, and provided + /// with the vertices and distance samples corresponding to the enclosing + /// primal cube. The cube callback will be invoked for each 2x2x2 set of + /// neighbouring points in the dual grid, and provided the corner grid + /// references, corner points, and the field values at those points. + pub fn traverse( + &mut self, + source: &S, + mut vertex_callback: Option, + mut cube_callback: C, + ) where + S: Sample, + V: FnMut(&[Vec3; 8], &[D; 8]) -> Option, + C: FnMut(&[(usize, usize, usize); 8], &[Vec3; 8], &[D; 8]), + { + let size_minus_one = self.size - 1; + + let mut keys = [(0, 0, 0); 8]; + let mut corners = [Vec3::zero(); 8]; + let mut values = [D::zero(); 8]; + + // two separate borrows, so the borrow checker knows we aren't borrowing self + // twice + let primal_grid = &mut self.primal_grid; + let duals = &mut self.duals; + + primal_grid.traverse(source, |primal_keys, primal_corners, primal_values| { + let vertex = vertex_callback + .as_mut() + .and_then(|f| f(primal_corners, primal_values)) + .unwrap_or(primal_corners[0].lerp(primal_corners[6], 0.5)); + + let (x, y, z) = primal_keys[0]; + duals[z % 2][y * size_minus_one + x] = + (vertex, primal_values[0].lerp(primal_values[6], 0.5)); + + if x > 0 && y > 0 && z > 0 { + let (x, y, z) = (x - 1, y - 1, z - 1); + for i in 0..8 { + keys[i] = (x + CORNERS[i][0], y + CORNERS[i][1], z + CORNERS[i][2]); + let dual = duals[(z + CORNERS[i][2]) % 2] + [(y + CORNERS[i][1]) * size_minus_one + x + CORNERS[i][0]]; + corners[i] = dual.0; + values[i] = dual.1; + } + cube_callback(&keys, &corners, &values); + } + }); + } +} diff --git a/src/traversal/implicit_octree.rs b/src/traversal/implicit_octree.rs new file mode 100644 index 0000000..2517e86 --- /dev/null +++ b/src/traversal/implicit_octree.rs @@ -0,0 +1,100 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{ + distance::Distance, linear_hashed_octree::LinearHashedOctree, + marching_cubes_tables::REMAP_CUBE, math::Vec3, morton::Morton, sampler::Sample, +}; +use std::collections::HashMap; + +/// Traverses over the leaves in a sparse octree that uses morton coordinates to +/// represent nodes in the tree. +pub struct ImplicitOctree { + max_depth: usize, +} + +impl ImplicitOctree { + /// Create a implicit octree with depth N, which is equivalent to a cubic + /// grid with dimensions 2^N along each axis. + pub fn new(max_depth: usize) -> Self { + Self { max_depth } + } + + /// Build an implicit octree by sampling from the provided Sampler to find + /// the surface crossings. Then place a vertex at the center of each + /// leaf node, and traverse the leaf nodes, invoking the callback for + /// each 2x2x2 cube of neighbouring leaf vertices. The callback will be + /// provided the Morton coordinates for each vertex, the vertices + /// themselves, and the field values at those vertices. + pub fn traverse(&mut self, source: &S, mut callback: C) + where + D: Distance, + S: Sample, + C: FnMut(&[Morton; 8], &[Vec3; 8], &[D; 8]), + { + let mut octree = LinearHashedOctree::new(); + + octree.build( + |key: Morton, distance: &D| { + let level = key.level(); + let size = key.size(); + // TODO: figure out how to construct an octree over a directed distance field + level < 2 || (level < self.max_depth && distance.within_extent(size)) + }, + |key: Morton| { + let p = key.center(); + source.sample(p) + }, + ); + + let mut primal_vertices = HashMap::new(); + + octree.walk_leaves(|key: Morton| { + let level = key.level(); + for i in 0..8 { + let vertex = key.primal_vertex(level, i); + + if vertex != Morton::with_key(0) { + if let Some(&existing_level) = primal_vertices.get(&vertex) { + if level > existing_level { + primal_vertices.insert(vertex, level); + } + } else { + primal_vertices.insert(vertex, level); + } + } + } + }); + + let mut keys = [Morton::new(); 8]; + let mut corners = [Vec3::zero(); 8]; + let mut values = [D::zero(); 8]; + + for (key, level) in primal_vertices { + for i in 0..8 { + let mut m = key.dual_vertex(level, REMAP_CUBE[i]); + while m > Morton::new() { + if let Some(&distance) = octree.get_node(&m) { + keys[i] = m; + corners[i] = m.center(); + values[i] = distance; + break; + } + m = m.parent(); + } + } + + callback(&keys, &corners, &values); + } + } +} diff --git a/src/traversal/mod.rs b/src/traversal/mod.rs new file mode 100644 index 0000000..e120f36 --- /dev/null +++ b/src/traversal/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +mod dual_grid; +mod implicit_octree; +mod primal_grid; + +pub use dual_grid::*; +pub use implicit_octree::*; +pub use primal_grid::*; diff --git a/src/traversal/primal_grid.rs b/src/traversal/primal_grid.rs new file mode 100644 index 0000000..873823c --- /dev/null +++ b/src/traversal/primal_grid.rs @@ -0,0 +1,90 @@ +// Copyright 2021 Tristam MacDonald +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{distance::Distance, marching_cubes_tables::CORNERS, math::Vec3, sampler::Sample}; + +/// Traverses over cubes in a primal grid (i.e. cubes formed by adjacent sample +/// points). +pub struct PrimalGrid { + size: usize, + layers: [Vec<(Vec3, D)>; 2], +} + +impl PrimalGrid { + /// Create a cubic grid with dimensions N*N*N + pub fn new(size: usize) -> Self { + Self { + size, + layers: [ + vec![(Vec3::zero(), D::zero()); size * size], + vec![(Vec3::zero(), D::zero()); size * size], + ], + } + } + + /// Traverse the primal grid, sampling from the provided Sampler at each + /// grid point. The callback will be invoked for each 2x2x2 set of + /// neighbouring grid points, and provided the corner grid references, + /// corner points, and the field values at those points. + pub fn traverse(&mut self, source: &S, mut callback: C) + where + S: Sample, + C: FnMut(&[(usize, usize, usize); 8], &[Vec3; 8], &[D; 8]), + { + let size_minus_one = self.size - 1; + let one_over_size = 1.0 / (size_minus_one as f32); + + // Cache layer zero of distance field values + for y in 0usize..self.size { + for x in 0..self.size { + let corner = Vec3::new(x as f32 * one_over_size, y as f32 * one_over_size, 0.0); + self.layers[0][y * self.size + x] = (corner, source.sample(corner)); + } + } + + let mut keys = [(0, 0, 0); 8]; + let mut corners = [Vec3::zero(); 8]; + let mut values = [D::zero(); 8]; + + for z in 0..self.size { + // Cache layer N+1 of isosurface values + for y in 0..self.size { + for x in 0..self.size { + let corner = Vec3::new( + x as f32 * one_over_size, + y as f32 * one_over_size, + (z + 1) as f32 * one_over_size, + ); + self.layers[1][y * self.size + x] = (corner, source.sample(corner)); + } + } + + // Traverse the calls in the current layer + for y in 0..size_minus_one { + for x in 0..size_minus_one { + for i in 0..8 { + keys[i] = (x + CORNERS[i][0], y + CORNERS[i][1], z + CORNERS[i][2]); + let (corner, value) = self.layers[CORNERS[i][2]] + [(y + CORNERS[i][1]) * self.size + x + CORNERS[i][0]]; + corners[i] = corner; + values[i] = value; + } + + callback(&keys, &corners, &values); + } + } + + self.layers.swap(0, 1); + } + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy