Skip to content

Commit 9db3722

Browse files
committed
factor out a lot of junk in the 3D voronoi noise, it is slightly slower now (~10%) but the code is much more manageable
1 parent e96e11e commit 9db3722

File tree

2 files changed

+161
-123
lines changed

2 files changed

+161
-123
lines changed

sources/noise/cell.swift

Lines changed: 160 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,8 @@ struct CellNoise3D:Noise
228228
public
229229
func evaluate(_ x:Double, _ y:Double, _ z:Double) -> Double
230230
{
231-
let sample:DoubleV3 = (x * self.frequency, y * self.frequency, z * self.frequency)
232-
233-
let bin:IntV3 = (floor(sample.x), floor(sample.y), floor(sample.z)),
234-
offset:DoubleV3 = (sample.x - Double(bin.a), sample.y - Double(bin.b), sample.z - Double(bin.c))
231+
let sample:DoubleV3 = (x * self.frequency, y * self.frequency, z * self.frequency),
232+
bin:IntV3 = (floor(sample.x), floor(sample.y), floor(sample.z))
235233

236234
// determine kernel
237235

@@ -244,165 +242,205 @@ struct CellNoise3D:Noise
244242
// near - quadrant.x ————— near quadrant →
245243
// ↓
246244

247-
let quadrant:IntV3 = (offset.x > 0.5 ? 1 : -1, offset.y > 0.5 ? 1 : -1, offset.z > 0.5 ? 1 : -1),
248-
near:IntV3 = (bin.a + (quadrant.a + 1) >> 1, bin.b + (quadrant.b + 1) >> 1, bin.c + (quadrant.c + 1) >> 1)
245+
let quadrant:IntV3 = (sample.x - Double(bin.a) > 0.5 ? 1 : -1,
246+
sample.y - Double(bin.b) > 0.5 ? 1 : -1,
247+
sample.z - Double(bin.c) > 0.5 ? 1 : -1)
248+
let near:IntV3 = (bin.a + (quadrant.a + 1) >> 1, bin.b + (quadrant.b + 1) >> 1, bin.c + (quadrant.c + 1) >> 1)
249249

250250
let nearpoint_disp:DoubleV3 = (abs(offset.x - Double((quadrant.a + 1) >> 1)),
251251
abs(offset.y - Double((quadrant.b + 1) >> 1)),
252252
abs(offset.z - Double((quadrant.c + 1) >> 1)))
253253

254254
var r2:Double = self.distance(from: sample, generating_point: near)
255255

256-
// the following unrolled code is not actually necessary — the loop at the
257-
// bottom of the function is capable of handling all cases, but unrolling
258-
// it partially results in an enormous performance gain, about a factor
259-
// of 7 over the pure loop version.
260256
@inline(__always)
261-
func test(generating_point:IntV3, dx:Double = 0, dy:Double = 0, dz:Double = 0)
257+
func _inspect_cell(offset:IntV3)
262258
{
263-
if dx*dx + dy*dy + dz*dz < r2
259+
// calculate distance from quadrant volume to kernel cell
260+
var cell_distance2:Double
261+
if cell_offset.a != 0
262+
{ // move by 0.5 towards zero
263+
let dx:Double = nearpoint_disp.x + Double(cell_offset.a) + (cell_offset.a > 0 ? -0.5 : 0.5)
264+
cell_distance2 = dx*dx
265+
}
266+
else
264267
{
265-
r2 = min(r2, self.distance(from: sample, generating_point: generating_point))
268+
cell_distance2 = 0
266269
}
270+
271+
if cell_offset.b != 0
272+
{ // move by 0.5 towards zero
273+
let dy:Double = nearpoint_disp.y + Double(cell_offset.b) + (cell_offset.b > 0 ? -0.5 : 0.5)
274+
cell_distance2 += dy*dy
275+
}
276+
277+
if cell_offset.c != 0
278+
{ // move by 0.5 towards zero
279+
let dz:Double = nearpoint_disp.z + Double(cell_offset.c) + (cell_offset.c > 0 ? -0.5 : 0.5)
280+
cell_distance2 += dz*dz
281+
}
282+
283+
guard cell_distance2 < r2
284+
else
285+
{
286+
return
287+
}
288+
289+
let generating_point:IntV3 = (near.a + quadrant.a*cell_offset.a,
290+
near.b + quadrant.b*cell_offset.b,
291+
near.c + quadrant.c*cell_offset.c)
292+
r2 = min(r2, self.distance(from: sample, generating_point: generating_point))
267293
}
268294

269-
// (0.0 , [(-1, 0, 0), (0, -1, 0), (0, 0, -1), (0, -1, -1), (-1, 0, -1), (-1, -1, 0), (-1, -1, -1)])
270-
let far:IntV3 = (near.a - quadrant.a, near.b - quadrant.b, near.c - quadrant.c)
271-
test(generating_point: (far.a, near.b, near.c), dx: nearpoint_disp.x - 0.5)
272-
test(generating_point: (near.a, far.b, near.c), dy: nearpoint_disp.y - 0.5)
273-
test(generating_point: (near.a, near.b, far.c), dz: nearpoint_disp.z - 0.5)
295+
// check each cell group, exiting early if we are guaranteed to have found
296+
// the closest point
274297

275-
test(generating_point: (near.a, far.b, far.c), dy: nearpoint_disp.y - 0.5, dz: nearpoint_disp.z - 0.5)
276-
test(generating_point: (far.a, near.b, far.c), dx: nearpoint_disp.x - 0.5, dz: nearpoint_disp.z - 0.5)
277-
test(generating_point: (far.a, far.b, near.c), dx: nearpoint_disp.x - 0.5, dy: nearpoint_disp.y - 0.5)
298+
// Cell group:
299+
// within r^2 = 0.25
300+
// cumulative sample coverage = 47.85%
301+
_inspect_cell(offset: (-1, 0, 0))
302+
_inspect_cell(offset: ( 0, -1, 0))
303+
_inspect_cell(offset: ( 0, 0, -1))
278304

279-
test(generating_point: far, dx: nearpoint_disp.x - 0.5, dy: nearpoint_disp.y - 0.5, dz: nearpoint_disp.z - 0.5)
305+
_inspect_cell(offset: ( 0, -1, -1))
306+
_inspect_cell(offset: (-1, 0, -1))
307+
_inspect_cell(offset: (-1, -1, 0))
280308

281-
// EARLY EXIT: Testing shows about 47.85% of samples are eliminated by here
282-
// (0.25, [(1, 0, 0), ( 0, 1, 0), ( 0, 0, 1),
283-
// (0, -1, 1), ( 0, 1, -1), ( 1, 0, -1), (-1, 0, 1), (-1, 1, 0), (1, -1, 0),
284-
// (1, -1, -1), (-1, 1, -1), (-1, -1, 1)])
309+
_inspect_cell(offset: (-1, -1, -1))
285310
guard r2 > 0.25
286311
else
287312
{
288313
return self.amplitude * r2
289314
}
290315

291-
let inner:IntV3 = (near.a + quadrant.a, near.b + quadrant.b, near.c + quadrant.c)
292-
test(generating_point: (inner.a, near.b, near.c), dx: nearpoint_disp.x + 0.5)
293-
test(generating_point: (near.a, inner.b, near.c), dy: nearpoint_disp.y + 0.5)
294-
test(generating_point: (near.a, near.b, inner.c), dz: nearpoint_disp.z + 0.5)
316+
// Cell group:
317+
// within r^2 = 0.5
318+
// cumulative sample coverage = 88.60%
319+
for cell_offset in [(1, 0, 0), ( 0, 1, 0), ( 0, 0, 1),
320+
(0, -1, 1), ( 0, 1, -1), ( 1, 0, -1), (-1, 0, 1), (-1, 1, 0), (1, -1, 0),
321+
(1, -1, -1), (-1, 1, -1), (-1, -1, 1)]
322+
{
323+
_inspect_cell(offset: cell_offset)
324+
}
325+
guard r2 > 0.5
326+
else
327+
{
328+
return self.amplitude * r2
329+
}
295330

296-
test(generating_point: (near.a, far.b, inner.c), dy: nearpoint_disp.y - 0.5, dz: nearpoint_disp.z + 0.5)
297-
test(generating_point: (near.a, inner.b, far.c), dy: nearpoint_disp.y + 0.5, dz: nearpoint_disp.z - 0.5)
298-
test(generating_point: (inner.a, near.b, far.c), dx: nearpoint_disp.x + 0.5, dz: nearpoint_disp.z - 0.5)
299-
test(generating_point: (far.a, near.b, inner.c), dx: nearpoint_disp.x - 0.5, dz: nearpoint_disp.z + 0.5)
300-
test(generating_point: (far.a, inner.b, near.c), dx: nearpoint_disp.x - 0.5, dy: nearpoint_disp.y + 0.5)
301-
test(generating_point: (inner.a, far.b, near.c), dx: nearpoint_disp.x + 0.5, dy: nearpoint_disp.y - 0.5)
331+
// Cell group:
332+
// within r^2 = 0.75
333+
// cumulative sample coverage = 98.26%
334+
for cell_offset in [(0, 1, 1), (1, 0, 1), (1, 1, 0), (-1, 1, 1), (1, -1, 1), (1, 1, -1)]
335+
{
336+
_inspect_cell(offset: cell_offset)
337+
}
338+
guard r2 > 0.75
339+
else
340+
{
341+
return self.amplitude * r2
342+
}
302343

303-
test(generating_point: (inner.a, far.b, far.c), dx: nearpoint_disp.x + 0.5, dy: nearpoint_disp.y - 0.5, dz: nearpoint_disp.z - 0.5)
304-
test(generating_point: (far.a, inner.b, far.c), dx: nearpoint_disp.x - 0.5, dy: nearpoint_disp.y + 0.5, dz: nearpoint_disp.z - 0.5)
305-
test(generating_point: (far.a, far.b, inner.c), dx: nearpoint_disp.x - 0.5, dy: nearpoint_disp.y - 0.5, dz: nearpoint_disp.z + 0.5)
344+
// Cell group:
345+
// within r^2 = 1.0
346+
// cumulative sample coverage = 99.94%
347+
_inspect_cell(offset: (1, 1, 1))
348+
guard r2 > 1.0
349+
else
350+
{
351+
return self.amplitude * r2
352+
}
306353

307-
// EARLY EXIT: Testing shows about 88.60% of samples are eliminated by here
308-
// (0.5 , [(0, 1, 1), (1, 0, 1), (1, 1, 0), (-1, 1, 1), (1, -1, 1), (1, 1, -1)])
309-
guard r2 > 0.5
354+
// Cell group:
355+
// within r^2 = 1.25
356+
// cumulative sample coverage > 99.99%
357+
for cell_offset in [(-2, 0, 0), ( 0, -2, 0), ( 0, 0, -2),
358+
( 0, -2, -1), ( 0, -1, -2), (-2, 0, -1), (-1, 0, -2), (-2, -1, 0), (-1, -2, 0),
359+
(-2, -1, -1), (-1, -2, -1), (-1, -1, -2)]
360+
{
361+
_inspect_cell(offset: cell_offset)
362+
}
363+
guard r2 > 1.25
310364
else
311365
{
312366
return self.amplitude * r2
313367
}
314368

315-
test(generating_point: (near.a, inner.b, inner.c), dy: nearpoint_disp.y + 0.5, dz: nearpoint_disp.z + 0.5)
316-
test(generating_point: (inner.a, near.b, inner.c), dx: nearpoint_disp.x + 0.5, dz: nearpoint_disp.z + 0.5)
317-
test(generating_point: (inner.a, inner.b, near.c), dx: nearpoint_disp.x + 0.5, dy: nearpoint_disp.y + 0.5)
369+
// Cell group:
370+
// within r^2 = 1.5
371+
// cumulative sample coverage > 99.99%
372+
for cell_offset in [( 0, 1, -2), ( 0, -2, 1), (1, 0, -2), (-2, 0, 1), (1, -2, 0), (-2, 1, 0),
373+
(-2, 1, -1), (-2, -1, 1), (1, -2, -1), (-1, -2, 1), (1, -1, -2), (-1, 1, -2)]
374+
{
375+
_inspect_cell(offset: cell_offset)
376+
}
377+
guard r2 > 1.5
378+
else
379+
{
380+
return self.amplitude * r2
381+
}
318382

319-
test(generating_point: (far.a, inner.b, inner.c), dx: nearpoint_disp.x - 0.5, dy: nearpoint_disp.y + 0.5, dz: nearpoint_disp.z + 0.5)
320-
test(generating_point: (inner.a, far.b, inner.c), dx: nearpoint_disp.x + 0.5, dy: nearpoint_disp.y - 0.5, dz: nearpoint_disp.z + 0.5)
321-
test(generating_point: (inner.a, inner.b, far.c), dx: nearpoint_disp.x + 0.5, dy: nearpoint_disp.y + 0.5, dz: nearpoint_disp.z - 0.5)
383+
// Cell group:
384+
// within r^2 = 2.0
385+
// cumulative sample coverage > 99.99%
386+
_inspect_cell(offset: (-2, 1, 1))
387+
_inspect_cell(offset: ( 1, -2, 1))
388+
_inspect_cell(offset: ( 1, 1, -2))
389+
guard r2 > 2.0
390+
else
391+
{
392+
return self.amplitude * r2
393+
}
322394

323-
// EARLY EXIT: Testing shows about 98.26% of samples are eliminated by here
324-
// (0.75, [(1, 1, 1)])
325-
guard r2 > 0.75
395+
// Cell group:
396+
// within r^2 = 2.25
397+
// cumulative sample coverage > 99.99%
398+
for cell_offset in [(0, -2, -2), (-2, 0, -2), (-2, -2, 0), (-1, -2, -2), (-2, -1, -2), (-2, -2, -1)]
399+
{
400+
_inspect_cell(offset: cell_offset)
401+
}
402+
guard r2 > 2.25
326403
else
327404
{
328405
return self.amplitude * r2
329406
}
330407

331-
test(generating_point: inner, dx: nearpoint_disp.x + 0.5, dy: nearpoint_disp.y + 0.5, dz: nearpoint_disp.z + 0.5)
332-
333-
// Testing shows about 99.94% of samples are eliminated by here
334-
335-
// The following loop is responsible for about 25% of the noise generator’s
336-
// runtime. While it is possible to unroll the rest of it, we run up against
337-
// diminishing returns.
338-
let kernel:[(r2:Double, cell_offsets:[(Int, Int, Int)])] =
339-
[
340-
// (0.0 , [(-1, 0, 0), (0, -1, 0), (0, 0, -1), (-1, -1, 0), (-1, 0, -1), (0, -1, -1), (-1, -1, -1)]),
341-
// (0.25, [(1, 0, 0), (0, 1, 0), (0, 0, 1), (-1, 0, 1), (0, -1, 1), (-1, -1, 1), (-1, 1, 0), (1, -1, 0),
342-
// (0, 1, -1), (-1, 1, -1), (1, 0, -1), (1, -1, -1)]),
343-
// (0.5 , [(0, 1, 1), (1, 0, 1), (1, 1, 0), (-1, 1, 1), (1, -1, 1), (1, 1, -1)]),
344-
// (0.75, [(1, 1, 1)]),
345-
(1.0 , [(-2, 0, 0), (-2, -1, 0), (0, -2, 0), (-1, -2, 0), (-2, 0, -1), (-2, -1, -1), (0, -2, -1), (-1, -2, -1),
346-
(0, 0, -2), (-1, 0, -2), (0, -1, -2), (-1, -1, -2)]),
347-
(1.25, [(-2, 0, 1), (-2, -1, 1), (0, -2, 1), (-1, -2, 1), (-2, 1, 0), (1, -2, 0), (-2, 1, -1), (1, -2, -1),
348-
(0, 1, -2), (-1, 1, -2), (1, 0, -2), (1, -1, -2)]),
349-
(1.5 , [(-2, 1, 1), (1, -2, 1), (1, 1, -2)]),
350-
(2.0 , [(-2, -2, 0), (-2, -2, -1), (-2, 0, -2), (-2, -1, -2), (0, -2, -2), (-1, -2, -2)]),
351-
(2.25, [(0, 0, 2), (-1, 0, 2), (0, -1, 2), (-1, -1, 2), (-2, -2, 1), (0, 2, 0), (-1, 2, 0), (2, 0, 0),
352-
(2, -1, 0), (0, 2, -1), (-1, 2, -1), (2, 0, -1), (2, -1, -1), (-2, 1, -2), (1, -2, -2)]),
353-
(2.5 , [(0, 1, 2), (-1, 1, 2), (1, 0, 2), (1, -1, 2), (0, 2, 1), (-1, 2, 1), (2, 0, 1), (2, -1, 1),
354-
(1, 2, 0), (2, 1, 0), (1, 2, -1), (2, 1, -1)]),
355-
(2.75, [(1, 1, 2), (1, 2, 1), (2, 1, 1)])
356-
]
357-
358-
for (kernel_radius2, cell_offsets):(r2:Double, cell_offsets:[(Int, Int, Int)]) in kernel
408+
// Cell group:
409+
// within r^2 = 2.5
410+
// cumulative sample coverage > 99.99%
411+
for cell_offset in [(2, 0, 0), (0, 2, 0), (0, 0, 2),
412+
(0, -1, 2), (0, 2, -1), (-1, 0, 2), ( 2, 0, -1), (-1, 2, 0), ( 2, -1, 0),
413+
(1, -2, -2), (2, -1, -1), (-2, -2, 1), (-1, -1, 2), (-2, 1, -2), (-1, 2, -1)]
359414
{
360-
// EARLY EXIT
361-
guard kernel_radius2 < r2
362-
else
363-
{
364-
break
365-
}
415+
_inspect_cell(offset: cell_offset)
416+
}
417+
guard r2 > 2.5
418+
else
419+
{
420+
return self.amplitude * r2
421+
}
366422

367-
for cell_offset:IntV3 in cell_offsets
368-
{
369-
// calculate distance from quadrant volume to kernel cell
370-
var cell_distance2:Double
371-
if cell_offset.a == 0
372-
{
373-
cell_distance2 = 0
374-
}
375-
else
376-
{ // move by 0.5 towards zero
377-
let dx:Double = nearpoint_disp.x + Double(cell_offset.a) + (cell_offset.a > 0 ? -0.5 : 0.5)
378-
cell_distance2 = dx*dx
379-
}
380-
381-
if cell_offset.b != 0
382-
{ // move by 0.5 towards zero
383-
let dy:Double = nearpoint_disp.y + Double(cell_offset.b) + (cell_offset.b > 0 ? -0.5 : 0.5)
384-
cell_distance2 += dy*dy
385-
}
386-
387-
if cell_offset.c != 0
388-
{ // move by 0.5 towards zero
389-
let dz:Double = nearpoint_disp.z + Double(cell_offset.c) + (cell_offset.c > 0 ? -0.5 : 0.5)
390-
cell_distance2 += dz*dz
391-
}
392-
393-
guard cell_distance2 < r2
394-
else
395-
{
396-
continue
397-
}
398-
399-
let generating_point:IntV3 = (near.a + quadrant.a*cell_offset.a,
400-
near.b + quadrant.b*cell_offset.b,
401-
near.c + quadrant.c*cell_offset.c)
402-
r2 = min(r2, self.distance(from: sample, generating_point: generating_point))
403-
}
423+
// Cell group:
424+
// within r^2 = 2.75
425+
// cumulative sample coverage > 99.99%
426+
for cell_offset in [(0, 1, 2), (0, 2, 1), (1, 0, 2), ( 2, 0, 1), (1, 2, 0), ( 2, 1, 0),
427+
(2, 1, -1), (2, -1, 1), (1, 2, -1), (-1, 2, 1), (1, -1, 2), (-1, 1, 2)]
428+
{
429+
_inspect_cell(offset: cell_offset)
430+
}
431+
guard r2 > 2.75
432+
else
433+
{
434+
return self.amplitude * r2
404435
}
405436

437+
// Cell group:
438+
// within r^2 = 3.0
439+
// cumulative sample coverage = 100%
440+
for cell_offset in [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
441+
{
442+
_inspect_cell(offset: cell_offset)
443+
}
406444
return self.amplitude * r2
407445
}
408446

tests/LinuxMain.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ t0 = clock()
4949
pixbuf = V3.sample_area_saturated_to_u8(width: viewer_size, height: viewer_size, offset: 0)
5050
print(clock() - t0)
5151
try png_encode(path: "voronoi3D.png", raw_data: pixbuf, properties: png_properties)
52-
52+
print(1 - Double(CellNoise3D._ccount) / Double(CellNoise3D._ctotal))
5353

5454
let S:fBm<SimplexNoise2D> = fBm<SimplexNoise2D>(amplitude: 0.5*127.5, frequency: 0.001, octaves: 10)
5555
t0 = clock()

0 commit comments

Comments
 (0)