Skip to content

Commit b4b95b3

Browse files
committed
Merge pull request #10 from swiftcoder/0.1-alpha.0
Extended Marching Cubes and Dual Contouring
2 parents f9f4750 + e2888e8 commit b4b95b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4423
-993
lines changed

Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ license = "Apache-2.0"
66
name = "isosurface"
77
readme = "README.md"
88
repository = "https://github.com/swiftcoder/isosurface"
9-
version = "0.0.4"
9+
version = "0.1.0-alpha.0"
1010

1111
[dev-dependencies]
1212
cgmath = "^0.17"
@@ -15,12 +15,12 @@ glium = "^0.26"
1515
glium_text_rusttype = "^0.3"
1616

1717
[profile.dev]
18-
opt-level = 2
18+
opt-level = 3
1919

2020
[profile.release]
21-
opt-level = 3
2221
lto = true
22+
opt-level = 3
2323

2424
[[bench]]
25-
name = "isosurface"
2625
harness = false
26+
name = "isosurface"

README.md

+23-14
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
1+
[![Crates.io](https://img.shields.io/crates/v/isosurface.svg)](https://crates.io/crates/isosurface)
2+
[![Docs.rs](https://docs.rs/isosurface/badge.svg)](https://docs.rs/isosurface)
3+
[![Github license](https://img.shields.io/github/license/swiftcoder/isosurface.svg)](https://github.com/swiftcoder/isosurface/blob/trunk/LICENSE.md)
4+
15
# Isosurface
2-
Isosurface extraction algorithms for Rust. Currently only a few techniques are implemented.
6+
Isosurface extraction algorithms implemented in Rust. The classical Marching Cubes and Dual Contouring techniques are included, along with more modern variations on the theme.
7+
8+
In the interest of education, the documentation for each extraction algorithm links to the relevant academic papers.
9+
10+
## Example programs
11+
`cargo run --example sampler` will execute the sampler, which allows you to compare a variety of algorithms and implicit surfaces.
312

4-
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.
13+
`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.
514

6-
# Marching Cubes
7-
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.
15+
## Dependencies
16+
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.
817

9-
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.
18+
## 32-bit indices
19+
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.
1020

11-
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.
21+
## Why are optimisations enabled in debug builds?
22+
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.
1223

13-
# Linear Hashed Marching Cubes
14-
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.
15-
16-
# Point Clouds and Deferred Rasterisation
17-
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.
24+
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.
1825

19-
# Why are optimisations enabled in debug builds?
20-
Without optimisations enabled, debug builds are 70x slower (1 minute to extract a 256^3 volume, versus ~800 milliseconds).
26+
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`:
2127

22-
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.
28+
```
29+
[profile.dev.package.isosurface]
30+
opt-level = 3
31+
```

benches/isosurface.rs

+28-24
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,45 @@
1+
// Copyright 2021 Tristam MacDonald
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
115
use criterion::{criterion_group, criterion_main, Criterion};
216
use isosurface::{
3-
linear_hashed_marching_cubes::LinearHashedMarchingCubes, marching_cubes::MarchingCubes,
4-
source::Source,
17+
distance::Signed, extractor::IndexedVertices, implicit::Torus, sampler::Sampler,
18+
LinearHashedMarchingCubes, MarchingCubes,
519
};
620

7-
fn torus(x: f32, y: f32, z: f32) -> f32 {
8-
const R1: f32 = 1.0 / 4.0;
9-
const R2: f32 = 1.0 / 10.0;
10-
let q_x = ((x * x + y * y).sqrt()).abs() - R1;
11-
let len = (q_x * q_x + z * z).sqrt();
12-
len - R2
13-
}
14-
15-
pub struct Torus {}
16-
17-
impl Source for Torus {
18-
fn sample(&self, x: f32, y: f32, z: f32) -> f32 {
19-
torus(x - 0.5, y - 0.5, z - 0.5)
20-
}
21-
}
22-
2321
fn marching_cubes() {
24-
let torus = Torus {};
22+
let torus = Torus::new(0.25, 0.1);
23+
let sampler = Sampler::new(&torus);
24+
2525
let mut vertices = vec![];
2626
let mut indices = vec![];
27+
let mut extractor = IndexedVertices::new(&mut vertices, &mut indices);
2728

28-
let mut marching_cubes = MarchingCubes::new(256);
29-
marching_cubes.extract(&torus, &mut vertices, &mut indices);
29+
let mut marching_cubes = MarchingCubes::<Signed>::new(128);
30+
marching_cubes.extract(&sampler, &mut extractor);
3031
}
3132

3233
fn linear_hashed_marching_cubes() {
33-
let torus = Torus {};
34+
let torus = Torus::new(0.25, 0.1);
35+
let sampler = Sampler::new(&torus);
36+
3437
let mut vertices = vec![];
3538
let mut indices = vec![];
39+
let mut extractor = IndexedVertices::new(&mut vertices, &mut indices);
3640

37-
let mut marching_cubes = LinearHashedMarchingCubes::new(8);
38-
marching_cubes.extract(&torus, &mut vertices, &mut indices);
41+
let mut marching_cubes = LinearHashedMarchingCubes::new(7);
42+
marching_cubes.extract(&sampler, &mut extractor);
3943
}
4044

4145
fn marching_cubes_benchmark(c: &mut Criterion) {

examples/common/mod.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 Tristam MacDonald
1+
// Copyright 2021 Tristam MacDonald
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -18,10 +18,10 @@ pub mod text;
1818
use std::mem;
1919
use std::slice;
2020

21-
/// This is used to reinterpret slices of floats as slices of repr(C) structs, without any
22-
/// copying. It is optimal, but it is also punching holes in the type system. I hope that Rust
23-
/// provides safe functionality to handle this in the future. In the meantime, reproduce
24-
/// this workaround at your own risk.
21+
/// This is used to reinterpret slices of floats as slices of repr(C) structs,
22+
/// without any copying. It is optimal, but it is also punching holes in the
23+
/// type system. I hope that Rust provides safe functionality to handle this in
24+
/// the future. In the meantime, reproduce this workaround at your own risk.
2525
pub fn reinterpret_cast_slice<S, T>(input: &[S]) -> &[T] {
2626
let length_in_bytes = input.len() * mem::size_of::<S>();
2727
let desired_length = length_in_bytes / mem::size_of::<T>();

examples/common/sources.rs

+30-53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 Tristam MacDonald
1+
// Copyright 2021 Tristam MacDonald
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -13,68 +13,45 @@
1313
// limitations under the License.
1414

1515
//! Isosurface definitions for use in multiple examples
16+
use isosurface::{
17+
distance::{Directed, Signed},
18+
math::Vec3,
19+
source::{HermiteSource, ScalarSource, VectorSource},
20+
};
1621

17-
use isosurface::source::Source;
22+
pub trait AllSources: ScalarSource + VectorSource + HermiteSource {}
1823

19-
/// The distance-field equation for a torus
20-
fn torus(x: f32, y: f32, z: f32) -> f32 {
21-
const R1: f32 = 1.0 / 4.0;
22-
const R2: f32 = 1.0 / 10.0;
23-
let q_x = ((x * x + y * y).sqrt()).abs() - R1;
24-
let len = (q_x * q_x + z * z).sqrt();
25-
len - R2
26-
}
27-
28-
pub struct Torus {}
29-
30-
impl Source for Torus {
31-
fn sample(&self, x: f32, y: f32, z: f32) -> f32 {
32-
torus(x - 0.5, y - 0.5, z - 0.5)
33-
}
34-
}
24+
impl<S: ScalarSource + VectorSource + HermiteSource> AllSources for S {}
3525

36-
fn abs(x: f32, y: f32, z: f32) -> (f32, f32, f32) {
37-
(
38-
if x > 0.0 { x } else { -x },
39-
if y > 0.0 { y } else { -y },
40-
if z > 0.0 { z } else { -z },
41-
)
26+
pub struct DemoSource<'a> {
27+
pub source: Box<dyn 'a + AllSources>,
4228
}
4329

44-
fn max(px: f32, py: f32, pz: f32, qx: f32, qy: f32, qz: f32) -> (f32, f32, f32) {
45-
(
46-
if px > qx { px } else { qx },
47-
if py > qy { py } else { qy },
48-
if pz > qz { pz } else { qz },
49-
)
50-
}
51-
52-
/// The distance field equation for a cube
53-
fn cube(px: f32, py: f32, pz: f32, bx: f32, by: f32, bz: f32) -> f32 {
54-
let (ax, ay, az) = abs(px, py, pz);
55-
let (dx, dy, dz) = (ax - bx, ay - by, az - bz);
56-
let (mx, my, mz) = max(dx, dy, dz, 0.0, 0.0, 0.0);
57-
let l = (mx * mx + my * my + mz * mz).sqrt();
58-
dx.max(dz.max(dy)).min(0.0) + l
30+
impl<'a> DemoSource<'a> {
31+
pub fn new<S: 'a + AllSources>(source: S) -> Self {
32+
Self {
33+
source: Box::new(source),
34+
}
35+
}
5936
}
6037

61-
/// The distance field equation for a sphere
62-
fn sphere(x: f32, y: f32, z: f32, r: f32) -> f32 {
63-
(x * x + y * y + z * z).sqrt() - r
38+
impl<'a> ScalarSource for DemoSource<'a> {
39+
fn sample_scalar(&self, p: Vec3) -> Signed {
40+
let q = p - Vec3::from_scalar(0.5);
41+
self.source.sample_scalar(q)
42+
}
6443
}
6544

66-
/// Subtract one distance field from another (i.e. CSG difference operation)
67-
fn subtract(d1: f32, d2: f32) -> f32 {
68-
d2.max(-d1)
45+
impl<'a> VectorSource for DemoSource<'a> {
46+
fn sample_vector(&self, p: Vec3) -> Directed {
47+
let q = p - Vec3::from_scalar(0.5);
48+
self.source.sample_vector(q)
49+
}
6950
}
7051

71-
pub struct CubeSphere {}
72-
73-
impl Source for CubeSphere {
74-
fn sample(&self, x: f32, y: f32, z: f32) -> f32 {
75-
subtract(
76-
sphere(x - 0.5, y - 0.5, z - 0.5, 0.25),
77-
cube(x - 0.5, y - 0.5, z - 0.5, 0.2, 0.2, 0.2),
78-
)
52+
impl<'a> HermiteSource for DemoSource<'a> {
53+
fn sample_normal(&self, p: Vec3) -> Vec3 {
54+
let q = p - Vec3::from_scalar(0.5);
55+
self.source.sample_normal(q)
7956
}
8057
}

examples/common/text.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 Tristam MacDonald
1+
// Copyright 2021 Tristam MacDonald
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -13,11 +13,11 @@
1313
// limitations under the License.
1414

1515
//! Conveniences to make the `glium_text` API easier to use in samples.
16-
1716
use cgmath::{Matrix4, Vector3};
1817

19-
/// Produce a transform matrix that will display text at offset column `x`, row `y`, in a
20-
/// display-filling coordinate space N characters wide and N*aspect rows high.
18+
/// Produce a transform matrix that will display text at offset column `x`, row
19+
/// `y`, in a display-filling coordinate space N characters wide and N*aspect
20+
/// rows high.
2121
pub fn layout_text(characters_per_row: f32, aspect: f32, x: f32, y: f32) -> Matrix4<f32> {
2222
let inv_scale = 2.0 / characters_per_row;
2323
Matrix4::from_translation(Vector3::new(-1.0, -1.0, 0.0))

examples/deferred_rasterisation.rs

+28-23
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 Tristam MacDonald
1+
// Copyright 2021 Tristam MacDonald
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -19,21 +19,22 @@ extern crate isosurface;
1919

2020
mod common;
2121

22-
use crate::common::reinterpret_cast_slice;
23-
use crate::common::sources::Torus;
22+
use crate::common::{reinterpret_cast_slice, sources::DemoSource};
2423
use cgmath::{vec3, Matrix4, Point3, SquareMatrix};
25-
use glium::glutin;
26-
use glium::glutin::{
27-
dpi::LogicalSize,
28-
event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent},
29-
Api, GlProfile, GlRequest,
24+
use glium::{
25+
glutin::{
26+
self,
27+
dpi::LogicalSize,
28+
event::{ElementState, KeyboardInput, VirtualKeyCode, WindowEvent},
29+
Api, GlProfile, GlRequest,
30+
},
31+
texture::{DepthFormat, DepthTexture2d, MipmapsOption, Texture2d, UncompressedFloatFormat},
32+
Surface,
3033
};
31-
use glium::texture::{
32-
DepthFormat, DepthTexture2d, MipmapsOption, Texture2d, UncompressedFloatFormat,
34+
use isosurface::{
35+
distance::Signed, extractor::OnlyInterleavedNormals, implicit::Torus, sampler::Sampler,
36+
source::CentralDifference, PointCloud,
3337
};
34-
use glium::Surface;
35-
use isosurface::point_cloud::PointCloud;
36-
use isosurface::source::CentralDifference;
3738

3839
#[derive(Copy, Clone)]
3940
#[repr(C)]
@@ -52,9 +53,9 @@ struct VertexWithNormal {
5253

5354
implement_vertex!(VertexWithNormal, position, normal);
5455

55-
// This technique is derived from an image tweeted by Gavan Woolery (gavanw@). it needs some
56-
// refinement, but I think I've captured an approximation of his rendering technique.
57-
// https://twitter.com/gavanw/status/717265068086308865
56+
// This technique is derived from an image tweeted by Gavan Woolery (gavanw@).
57+
// it needs some refinement, but I think I've captured an approximation of his
58+
// rendering technique. https://twitter.com/gavanw/status/717265068086308865
5859

5960
fn main() {
6061
let events_loop = glutin::event_loop::EventLoop::new();
@@ -76,13 +77,15 @@ fn main() {
7677

7778
let subdivisions = 64;
7879

79-
let torus = Torus {};
80+
let torus = DemoSource::new(Torus::new(0.25, 0.1));
8081
let central_difference = CentralDifference::new(torus);
82+
let sampler = Sampler::new(&central_difference);
8183

8284
let mut vertices = vec![];
83-
let mut marcher = PointCloud::new(subdivisions);
85+
let mut extractor = OnlyInterleavedNormals::new(&mut vertices, &sampler);
86+
let mut marcher = PointCloud::<Signed>::new(subdivisions);
8487

85-
marcher.extract_midpoints_with_normals(&central_difference, &mut vertices);
88+
marcher.extract(&sampler, &mut extractor);
8689

8790
let vertex_buffer: glium::VertexBuffer<VertexWithNormal> = {
8891
glium::VertexBuffer::new(&display, reinterpret_cast_slice(&vertices))
@@ -226,7 +229,8 @@ fn main() {
226229
);
227230
let model = Matrix4::identity();
228231

229-
// We need two textures to ping-pong between, and one of them needs an attached depth buffer for the initial pass
232+
// We need two textures to ping-pong between, and one of them needs an attached
233+
// depth buffer for the initial pass
230234
let position1 = Texture2d::empty_with_format(
231235
&display,
232236
UncompressedFloatFormat::F32F32F32F32,
@@ -358,8 +362,8 @@ fn main() {
358362
.expect("failed to draw to surface");
359363
}
360364

361-
// pass 1 through N-1, ping-pong render both buffers in turn, spreading the points across
362-
// the faces of their respective cubes
365+
// pass 1 through N-1, ping-pong render both buffers in turn, spreading the
366+
// points across the faces of their respective cubes
363367
for i in 0..3 {
364368
let framebuffer = if i % 2 == 0 {
365369
&mut framebuffer2
@@ -390,7 +394,8 @@ fn main() {
390394
.expect("failed to draw to surface");
391395
}
392396

393-
// final pass, composite the last buffer to the screen, performing lighting in the process
397+
// final pass, composite the last buffer to the screen, performing lighting in
398+
// the process
394399
{
395400
let mut surface = display.draw();
396401
surface.clear_color_and_depth((0.306, 0.267, 0.698, 0.0), 1.0);

0 commit comments

Comments
 (0)