surface_nets/
lib.rs

1use std::collections::HashMap;
2
3// Positive is "air"
4// Negative is "solid"
5
6type SDF = Fn(usize, usize, usize) -> f32;
7
8// Implements memoization (if memoize is true, copy the function into a vec and
9// use that instead of the function)
10pub fn surface_net(
11    resolution: usize,
12    signed_distance_field: &SDF,
13    memoize: bool,
14) -> (Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<usize>) {
15    if memoize {
16        let axis_length = resolution + 1;
17        let arr = coords(axis_length)
18            .map(|(x, y, z)| signed_distance_field(x, y, z))
19            .collect::<Vec<_>>();
20        surface_net_impl(resolution, &move |x, y, z| {
21            arr[z * axis_length * axis_length + y * axis_length + x]
22        })
23    } else {
24        surface_net_impl(resolution, signed_distance_field)
25    }
26}
27
28// Main algorithm driver.
29fn surface_net_impl(
30    resolution: usize,
31    grid_values: &SDF,
32) -> (Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<usize>) {
33    let mut vertex_positions = Vec::new();
34    let mut normals = Vec::new();
35    let mut grid_to_index = HashMap::new();
36    // Find all vertex positions. Addtionally, create a hashmap from grid
37    // position to index (i.e. OpenGL vertex index).
38    for coords in coords(resolution) {
39        if let Some((center, normal)) = find_center(grid_values, coords) {
40            grid_to_index.insert(coords, vertex_positions.len());
41            vertex_positions.push(center);
42            normals.push(normal);
43        }
44    }
45    // Find all triangles, in the form of [index, index, index] triples.
46    let mut indicies = Vec::new();
47    make_all_triangles(
48        grid_values,
49        resolution,
50        &grid_to_index,
51        &vertex_positions,
52        &mut indicies,
53    );
54    (vertex_positions, normals, indicies)
55}
56
57// Iterator over all integer points in a 3d cube from 0 to size
58fn coords(size: usize) -> impl Iterator<Item = (usize, usize, usize)> {
59    (0..size)
60        .flat_map(move |x| (0..size).map(move |y| (x, y)))
61        .flat_map(move |(x, y)| (0..size).map(move |z| (x, y, z)))
62}
63
64// List of all edges in a cube.
65const OFFSETS: [(usize, usize); 12] = [
66    (0b000, 0b001), // ((0, 0, 0), (0, 0, 1)),
67    (0b000, 0b010), // ((0, 0, 0), (0, 1, 0)),
68    (0b000, 0b100), // ((0, 0, 0), (1, 0, 0)),
69    (0b001, 0b011), // ((0, 0, 1), (0, 1, 1)),
70    (0b001, 0b101), // ((0, 0, 1), (1, 0, 1)),
71    (0b010, 0b011), // ((0, 1, 0), (0, 1, 1)),
72    (0b010, 0b110), // ((0, 1, 0), (1, 1, 0)),
73    (0b011, 0b111), // ((0, 1, 1), (1, 1, 1)),
74    (0b100, 0b101), // ((1, 0, 0), (1, 0, 1)),
75    (0b100, 0b110), // ((1, 0, 0), (1, 1, 0)),
76    (0b101, 0b111), // ((1, 0, 1), (1, 1, 1)),
77    (0b110, 0b111), // ((1, 1, 0), (1, 1, 1)),
78];
79
80// Find the vertex position for this grid: it will be somewhere within the cube
81// with coordinates [0,1].
82// How? First, for each edge in the cube, find if that edge crosses the SDF
83// boundary - i.e. one point is positive, one point is negative.
84// Second, calculate the "weighted midpoint" between these points (see
85// find_edge).
86// Third, take the average of all these points for all edges (for edges that
87// have crossings).
88// There are more complicated and better algorithms than this, but this is
89// simple and easy to implement.
90// Returns: (pos, normal)
91fn find_center(
92    grid_values: &SDF,
93    coord: (usize, usize, usize),
94) -> Option<([f32; 3], [f32; 3])> {
95    let mut values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
96    for (x, value) in values.iter_mut().enumerate() {
97        *value = grid_values(
98            coord.0 + (x & 1),
99            coord.1 + ((x >> 1) & 1),
100            coord.2 + ((x >> 2) & 1),
101        );
102    }
103    let edges = OFFSETS.iter().filter_map(|&(offset1, offset2)| {
104        find_edge(offset1, offset2, values[offset1], values[offset2])
105    });
106    let mut count = 0;
107    let mut sum = [0.0, 0.0, 0.0];
108    for edge in edges {
109        count += 1;
110        sum[0] += edge[0];
111        sum[1] += edge[1];
112        sum[2] += edge[2];
113    }
114    if count == 0 {
115        None
116    } else {
117        let normal_x = (values[0b001] + values[0b011] + values[0b101] + values[0b111])
118            - (values[0b000] + values[0b010] + values[0b100] + values[0b110]);
119        let normal_y = (values[0b010] + values[0b011] + values[0b110] + values[0b111])
120            - (values[0b000] + values[0b001] + values[0b100] + values[0b101]);
121        let normal_z = (values[0b100] + values[0b101] + values[0b110] + values[0b111])
122            - (values[0b000] + values[0b001] + values[0b010] + values[0b011]);
123        let normal_len = (normal_x * normal_x + normal_y * normal_y + normal_z * normal_z).sqrt();
124        Some((
125            [
126                sum[0] / count as f32 + coord.0 as f32,
127                sum[1] / count as f32 + coord.1 as f32,
128                sum[2] / count as f32 + coord.2 as f32,
129            ],
130            [
131                normal_x / normal_len,
132                normal_y / normal_len,
133                normal_z / normal_len,
134            ],
135        ))
136    }
137}
138
139// Given two points, A and B, find the point between them where the SDF is zero.
140// (This might not exist).
141// A and B are specified via A=coord+offset1 and B=coord+offset2, because code
142// is weird.
143fn find_edge(offset1: usize, offset2: usize, value1: f32, value2: f32) -> Option<[f32; 3]> {
144    if (value1 < 0.0) == (value2 < 0.0) {
145        return None;
146    }
147    let interp = value1 / (value1 - value2);
148    let point = [
149        (offset1 & 1) as f32 * (1.0 - interp) + (offset2 & 1) as f32 * interp,
150        ((offset1 >> 1) & 1) as f32 * (1.0 - interp) + ((offset2 >> 1) & 1) as f32 * interp,
151        ((offset1 >> 2) & 1) as f32 * (1.0 - interp) + ((offset2 >> 2) & 1) as f32 * interp,
152    ];
153    Some(point)
154}
155
156// For every edge that crosses the boundary, make a quad between the
157// "centers" of the four cubes touching that boundary. (Well, really, two
158// triangles) The "centers" are actually the vertex positions, found earlier.
159// (Also, make sure the triangles are facing the right way)
160// There's some hellish off-by-one conditions and whatnot that make this code
161// really gross.
162fn make_all_triangles(
163    grid_values: &SDF,
164    resolution: usize,
165    grid_to_index: &HashMap<(usize, usize, usize), usize>,
166    vertex_positions: &[[f32; 3]],
167    indicies: &mut Vec<usize>,
168) {
169    for coord in coords(resolution) {
170        // TODO: Cache grid_values(coord), it's called three times here.
171        // Do edges parallel with the X axis
172        if coord.1 != 0 && coord.2 != 0 {
173            make_triangle(
174                grid_values,
175                grid_to_index,
176                vertex_positions,
177                indicies,
178                coord,
179                (1, 0, 0),
180                (0, 1, 0),
181                (0, 0, 1),
182            );
183        }
184        // Do edges parallel with the Y axis
185        if coord.0 != 0 && coord.2 != 0 {
186            make_triangle(
187                grid_values,
188                grid_to_index,
189                vertex_positions,
190                indicies,
191                coord,
192                (0, 1, 0),
193                (0, 0, 1),
194                (1, 0, 0),
195            );
196        }
197        // Do edges parallel with the Z axis
198        if coord.0 != 0 && coord.1 != 0 {
199            make_triangle(
200                grid_values,
201                grid_to_index,
202                vertex_positions,
203                indicies,
204                coord,
205                (0, 0, 1),
206                (1, 0, 0),
207                (0, 1, 0),
208            );
209        }
210    }
211}
212
213fn make_triangle(
214    grid_values: &SDF,
215    grid_to_index: &HashMap<(usize, usize, usize), usize>,
216    vertex_positions: &[[f32; 3]],
217    indicies: &mut Vec<usize>,
218    coord: (usize, usize, usize),
219    offset: (usize, usize, usize),
220    axis1: (usize, usize, usize),
221    axis2: (usize, usize, usize),
222) {
223    let face_result = is_face(grid_values, coord, offset);
224    if let FaceResult::NoFace = face_result {
225        return;
226    }
227    // The triangle points, viewed face-front, look like this:
228    // v1 v3
229    // v2 v4
230    let v1 = *grid_to_index.get(&(coord.0, coord.1, coord.2)).unwrap();
231    let v2 = *grid_to_index
232        .get(&(coord.0 - axis1.0, coord.1 - axis1.1, coord.2 - axis1.2))
233        .unwrap();
234    let v3 = *grid_to_index
235        .get(&(coord.0 - axis2.0, coord.1 - axis2.1, coord.2 - axis2.2))
236        .unwrap();
237    let v4 = *grid_to_index
238        .get(&(
239            coord.0 - axis1.0 - axis2.0,
240            coord.1 - axis1.1 - axis2.1,
241            coord.2 - axis1.2 - axis2.2,
242        )).unwrap();
243    // optional addition to algorithm: split quad to triangles in a certain way
244    let p1 = vertex_positions[v1];
245    let p2 = vertex_positions[v2];
246    let p3 = vertex_positions[v3];
247    let p4 = vertex_positions[v4];
248    fn dist(a: [f32; 3], b: [f32; 3]) -> f32 {
249        let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
250        d[0] * d[0] + d[1] * d[1] + d[2] * d[2]
251    }
252    let d14 = dist(p1, p4);
253    let d23 = dist(p2, p3);
254    // Split the quad along the shorter axis, rather than the longer one.
255    if d14 < d23 {
256        match face_result {
257            FaceResult::NoFace => (),
258            FaceResult::FacePositive => {
259                indicies.push(v1);
260                indicies.push(v2);
261                indicies.push(v4);
262
263                indicies.push(v1);
264                indicies.push(v4);
265                indicies.push(v3);
266            }
267            FaceResult::FaceNegative => {
268                indicies.push(v1);
269                indicies.push(v4);
270                indicies.push(v2);
271
272                indicies.push(v1);
273                indicies.push(v3);
274                indicies.push(v4);
275            }
276        }
277    } else {
278        match face_result {
279            FaceResult::NoFace => (),
280            FaceResult::FacePositive => {
281                indicies.push(v2);
282                indicies.push(v4);
283                indicies.push(v3);
284
285                indicies.push(v2);
286                indicies.push(v3);
287                indicies.push(v1);
288            }
289            FaceResult::FaceNegative => {
290                indicies.push(v2);
291                indicies.push(v3);
292                indicies.push(v4);
293
294                indicies.push(v2);
295                indicies.push(v1);
296                indicies.push(v3);
297            }
298        }
299    }
300}
301
302enum FaceResult {
303    NoFace,
304    FacePositive,
305    FaceNegative,
306}
307
308// Determine if the sign of the SDF flips between coord and (coord+offset)
309fn is_face(
310    grid_values: &SDF,
311    coord: (usize, usize, usize),
312    offset: (usize, usize, usize),
313) -> FaceResult {
314    let other = (coord.0 + offset.0, coord.1 + offset.1, coord.2 + offset.2);
315    match (
316        grid_values(coord.0, coord.1, coord.2) < 0.0,
317        grid_values(other.0, other.1, other.2) < 0.0,
318    ) {
319        (true, false) => FaceResult::FacePositive,
320        (false, true) => FaceResult::FaceNegative,
321        _ => FaceResult::NoFace,
322    }
323}