diff --git a/README.md b/README.md index 6a6188b..cc5a88a 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ ## What is this?
-stoneflake -fur +stone +flake +fur +hex
**voroce** is a fast and simple voronoi class useful to create images like the above ones. **voroce** is a coind word of **voronoi** & **veloce**. @@ -15,7 +17,7 @@ order -[Voronoi](http://www.rhythmiccanvas.com/research/papers/worley.pdf) is very useful for generating interesting patterns. Surprisingly, however, I have not found much literature on how to compute them efficiently. The most relevant work to this project was done by [Jontier et al.](http://jcgt.org/published/0008/01/02/paper.pdf) who proposed an optimal visiting order for efficient cellular noise generation. In the 2D rectangular grid case, they gave the order of 20 neighboring cells, which I think is not optimal because some cells do not need to be visited (let me know if I'm wrong :-)). If the shading point is in the green region and each cell has at least one sample point, it is unnecessary to visit the cells 10, 13, 14, 15, 16, 17, 18, and 19. For example, the cell 10 cannot have a sample point closer than the one in the cell 0. Also, note that the underlying grid does not have to be rectangular. We can use triangular or honeycomb grids as well. In [voronoise](https://iquilezles.org/www/articles/voronoise/voronoise.htm) by Inigo Quilez, sample points are jittered so that cell noise and voronoi can be generated in a single framework. The number of neighboring cells to be visited depends on the amount of jitter. +[Voronoi](http://www.rhythmiccanvas.com/research/papers/worley.pdf) is very useful for generating interesting patterns. Surprisingly, however, I have not found much literature on how to compute them efficiently. The most relevant work to this project was done by [Jontier et al.](http://jcgt.org/published/0008/01/02/paper.pdf) who proposed an optimal visiting order for efficient cellular noise generation. In the 2D rectangular grid case, they gave the order of 20 neighboring cells, which I think is not optimal because some cells do not need to be visited (let me know if I'm wrong :-)). If the shading point is in the green region and each cell has at least one sample point, it is unnecessary to visit the cells labeled 10, 13, 14, 15, 16, 17, 18, and 19. For example, the cell labeled 10 cannot have a sample point closer than the one in the cell labeled 0. Also, note that the underlying grid does not have to be rectangular. We can use triangular and hexagonal grids as well. In [voronoise](https://iquilezles.org/www/articles/voronoise/voronoise.htm) by Inigo Quilez, sample points are jittered so that cell noise and voronoi can be generated in a single framework. The number of neighboring cells to be visited depends on the amount of jitter. Table 1: The number of cells to traverse @@ -25,6 +27,7 @@ Table 1: The number of cells to traverse | 3D | 5^3 - 2^3 = 117 | 39 | 20 | | 4D | 5^4 - 2^4 = 609 | 195 | 85? | | 2D / triangle | 25 triangular cells | coming soon | coming soon | +| 2D / hexagon | 19 hexagonal cells | coming soon | coming soon | ## Features @@ -34,10 +37,10 @@ Table 1: The number of cells to traverse * [x] sample point jittering * [x] optimized code path for small jitter values -* [x] triangle (2D) -* [ ] optimized triangle (2D) -* [ ] honeycomb (2D) -* [ ] optimized honeycomb (2D) +* [x] triangular grid (2D) +* [ ] optimized triangular grid (2D) +* [x] hexagonal grid (2D) +* [ ] optimized hexagonal grid (2D) * [x] cache (2D) (brings nearly 2x speedup for primary rays) * [x] cache (3D) (brings nearly 2x speedup for primary rays) * [x] cache (4D) (brings **over** 3x speedup for primary rays) diff --git a/img/hex.gif b/img/hex.gif new file mode 100644 index 0000000..76bcbd0 Binary files /dev/null and b/img/hex.gif differ diff --git a/img/honeycomb.gif b/img/honeycomb.gif deleted file mode 100644 index 6135d07..0000000 Binary files a/img/honeycomb.gif and /dev/null differ diff --git a/include/voroce.h b/include/voroce.h index 8424755..f519702 100644 --- a/include/voroce.h +++ b/include/voroce.h @@ -17,6 +17,7 @@ namespace voroce static const auto LCG = 48271; + // These are not recommended because of artifacts especially for hexgrid static int32_t Hash2DLowQuality(const glm::ivec2& p) { return ((p.x * PrimeU) ^ (p.y * PrimeV)) * LCG; @@ -32,6 +33,22 @@ namespace voroce return ((p.x * PrimeU) ^ (p.y * PrimeV) ^ (p.z * PrimeW) ^ (p.w * PrimeT)) * LCG; } + // These are expensive but behave way better + static int32_t Hash2D(const glm::ivec2& p) + { + return std::hash()(std::hash()(p.x) + p.y); + } + + static int32_t Hash3D(const glm::ivec3& p) + { + return std::hash()(std::hash()(std::hash()(p.x) + p.y) + p.z); + } + + static int32_t Hash4D(const glm::ivec4& p) + { + return std::hash()(std::hash()(std::hash()(std::hash()(p.x) + p.y) + p.z) + p.w); + } + // minstd_rand (TODO: use better hash, do something beter here, fast but a bit ugly...) static auto OffsetX(const int32_t seed) @@ -125,5 +142,6 @@ namespace voroce // non rectangular grids static std::tuple Evaluate2DTri(const glm::vec2& source, int32_t(*my_hash)(const glm::ivec2& p), const float jitter = 1.0f); + static std::tuple Evaluate2DHex(const glm::vec2& source, int32_t(*my_hash)(const glm::ivec2& p), const float jitter = 1.0f); }; } \ No newline at end of file diff --git a/src/voroce.cpp b/src/voroce.cpp index 1caf9ef..999f8b9 100644 --- a/src/voroce.cpp +++ b/src/voroce.cpp @@ -310,103 +310,6 @@ std::tuple Voronoi::Evaluate2DCache(const glm::vec2& return std::make_tuple(cell_id, sq_dist, point); } -// naive triangle implementation -std::tuple Voronoi::Evaluate2DTri(const glm::vec2& source, int32_t(*my_hash)(const glm::ivec2& p), const float jitter) -{ - assert(0.0f <= jitter && jitter <= 1.0f); - - const auto one_3 = 1.0f / std::sqrt(3.0f); - const auto local = glm::vec2(glm::dot(glm::vec2(1.0f, -one_3), source), glm::dot(glm::vec2(0.0f, 2.0f * one_3), source)); - - const auto origin = glm::vec2(std::floor(local.x), std::floor(local.y)); - const auto quantized = glm::ivec2(int32_t(origin.x), int32_t(origin.y)); - - auto sq_dist = std::numeric_limits::max(); - auto cell_id = 0; - glm::vec2 point; - - // 1 (self) + 14 (neighbours) - const auto size = 15; - const std::array us[2] = - { - { 0, 0,-1, 1, 0,-1, 1,-1, 1, -1,-2,-2, 2, 1, 0 }, - { 0, 0,-1, 1, 0,-1, 1,-1, 1, 0,-1,-2, 2, 2, 1 }, - }; - const std::array vs[2] = - { - { 0,-1, 0, 0, 1,-1,-1, 1, 1, 2, 1, 0,-1,-2,-2 }, - { 0,-1, 0, 0, 1,-1,-1, 1, 1, 2, 2, 1, 0,-1,-2 }, - }; - const std::array flags[2] = - { - { 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 3, 2, 1, 3, 2 }, - { 3, 3, 3, 3, 3, 2, 3, 3, 3, 1, 3, 2, 1, 3, 2 }, - }; - - // "A Low-Distortion Map Between Triangle and Square" by Eric Heitz - // maps a unit - square point (x, y) to a unit - triangle point - auto triangle = [&](float& x, float& y) - { - if (y > x) - { - x *= 0.5f; - y -= x; - } - else - { - y *= 0.5f; - x -= y; - } - }; - - const auto tmp = local - origin; - const auto which = (1.0f > tmp.x + tmp.y) ? 0 : 1; - for (auto loop = 0; loop < size; ++loop) - { - const auto shift = glm::ivec2(us[which][loop], vs[which][loop]); - - // lower triangle - if (1 & flags[which][loop]) - { - const auto hash = my_hash(quantized + shift + 0); - auto randomX = OffsetX(hash); - auto randomY = OffsetY(hash); - triangle(randomX, randomY); - const auto offset = glm::vec2(randomX, randomY) * jitter + 1.0f / 3.0f; - const auto sample = origin + offset + glm::vec2(shift); - const auto global = glm::vec2(glm::dot(glm::vec2(1.0f, 0.5f), sample), glm::dot(glm::vec2(0.0f, std::sqrt(3) * 0.5f), sample)); - const auto tmp = glm::dot(source - global, source - global); - if (sq_dist > tmp) - { - sq_dist = tmp; - cell_id = hash; - point = sample; - } - } - - // upper triangle - if (2 & flags[which][loop]) - { - const auto hash = my_hash(quantized + shift + PrimeW); - auto randomX = OffsetX(hash); - auto randomY = OffsetY(hash); - triangle(randomX, randomY); - const auto offset = (glm::vec2(0.5f, 0.5f) - glm::vec2(randomX, randomY)) * jitter + 2.0f / 3.0f; - const auto sample = origin + offset + glm::vec2(shift); - const auto global = glm::vec2(glm::dot(glm::vec2(1.0f, 0.5f), sample), glm::dot(glm::vec2(0.0f, std::sqrt(3) * 0.5f), sample)); - const auto tmp = glm::dot(source - global, source - global); - if (sq_dist > tmp) - { - sq_dist = tmp; - cell_id = hash; - point = sample; - } - } - } - - return std::make_tuple(cell_id, sq_dist, point); -} - // naive implementation std::tuple Voronoi::Evaluate3DRef(const glm::vec3& source, int32_t (*my_hash)(const glm::ivec3& p), const float jitter) { @@ -1115,3 +1018,200 @@ std::tuple Voronoi::Evaluate4DCache(const glm::vec4& return std::make_tuple(cell_id, sq_dist, point); } + +// naive triangle implementation +std::tuple Voronoi::Evaluate2DTri(const glm::vec2& source, int32_t(*my_hash)(const glm::ivec2& p), const float jitter) +{ + assert(0.0f <= jitter && jitter <= 1.0f); + + const auto one_3 = 1.0f / std::sqrt(3.0f); + const auto local = glm::vec2(glm::dot(glm::vec2(1.0f, -one_3), source), glm::dot(glm::vec2(0.0f, 2.0f * one_3), source)); + + const auto origin = glm::vec2(std::floor(local.x), std::floor(local.y)); + const auto quantized = glm::ivec2(int32_t(origin.x), int32_t(origin.y)); + + auto sq_dist = std::numeric_limits::max(); + auto cell_id = 0; + glm::vec2 point; + + // 1 (self) + 14 (neighbours) + const auto size = 15; + const std::array us[2] = + { + { 0, 0,-1, 1, 0,-1, 1,-1, 1, -1,-2,-2, 2, 1, 0 }, + { 0, 0,-1, 1, 0,-1, 1,-1, 1, 0,-1,-2, 2, 2, 1 }, + }; + const std::array vs[2] = + { + { 0,-1, 0, 0, 1,-1,-1, 1, 1, 2, 1, 0,-1,-2,-2 }, + { 0,-1, 0, 0, 1,-1,-1, 1, 1, 2, 2, 1, 0,-1,-2 }, + }; + const std::array flags[2] = + { + { 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 3, 2, 1, 3, 2 }, + { 3, 3, 3, 3, 3, 2, 3, 3, 3, 1, 3, 2, 1, 3, 2 }, + }; + + // "A Low-Distortion Map Between Triangle and Square" by Eric Heitz + // maps a unit - square point (x, y) to a unit - triangle point + auto triangle = [&](float& x, float& y) + { + if (y > x) + { + x *= 0.5f; + y -= x; + } + else + { + y *= 0.5f; + x -= y; + } + }; + + const auto tmp = local - origin; + const auto which = (1.0f > tmp.x + tmp.y) ? 0 : 1; + for (auto loop = 0; loop < size; ++loop) + { + const auto shift = glm::ivec2(us[which][loop], vs[which][loop]); + + // lower triangle + if (1 & flags[which][loop]) + { + const auto hash = my_hash(quantized + shift + 0); + auto randomX = OffsetX(hash); + auto randomY = OffsetY(hash); + triangle(randomX, randomY); + const auto offset = glm::vec2(randomX, randomY) * jitter + 1.0f / 3.0f; + const auto sample = origin + offset + glm::vec2(shift); + const auto global = glm::vec2(glm::dot(glm::vec2(1.0f, 0.5f), sample), glm::dot(glm::vec2(0.0f, std::sqrt(3) * 0.5f), sample)); + const auto tmp = glm::dot(source - global, source - global); + if (sq_dist > tmp) + { + sq_dist = tmp; + cell_id = hash; + point = sample; + } + } + + // upper triangle + if (2 & flags[which][loop]) + { + const auto hash = my_hash(quantized + shift + PrimeW); + auto randomX = OffsetX(hash); + auto randomY = OffsetY(hash); + triangle(randomX, randomY); + const auto offset = (glm::vec2(0.5f, 0.5f) - glm::vec2(randomX, randomY)) * jitter + 2.0f / 3.0f; + const auto sample = origin + offset + glm::vec2(shift); + const auto global = glm::vec2(glm::dot(glm::vec2(1.0f, 0.5f), sample), glm::dot(glm::vec2(0.0f, std::sqrt(3) * 0.5f), sample)); + const auto tmp = glm::dot(source - global, source - global); + if (sq_dist > tmp) + { + sq_dist = tmp; + cell_id = hash; + point = sample; + } + } + } + + return std::make_tuple(cell_id, sq_dist, point); +} + +// naive honeycomb implementation +std::tuple Voronoi::Evaluate2DHex(const glm::vec2& source, int32_t(*my_hash)(const glm::ivec2& p), const float jitter) +{ + assert(0.0f <= jitter && jitter <= 1.0f); + + static const auto sqrt_3 = std::sqrt(3.0f); + + static const std::array basis = + { + glm::vec2(-sqrt_3 * 0.5f, -0.5f), + glm::vec2( sqrt_3 * 0.5f, -0.5f), + glm::vec2(0, 1) + }; + + static const std::array ortho = + { + glm::vec2(-1 / sqrt_3, -1), + glm::vec2( 1 / sqrt_3, -1), + glm::vec2( 2 / sqrt_3, 0) + }; + + const std::array dots = + { + glm::dot(ortho[0], source), + glm::dot(ortho[1], source), + glm::dot(ortho[2], source) + }; + + const std::array grids = + { + std::floor( dots[0]), std::floor( dots[1]), std::floor( dots[2]), + std::floor(-dots[0]), std::floor(-dots[1]), std::floor(-dots[2]) + }; + + const int32_t int_grids[6] = + { + int32_t(grids[0]), int32_t(grids[1]), int32_t(grids[2]), + int32_t(grids[3]), int32_t(grids[4]), int32_t(grids[5]) + }; + + glm::ivec2 quantized; + glm::vec2 origin(0, 0); + if ((int_grids[0] + int_grids[1]) % 3 == 0) + { + origin = basis[0] * grids[0] + basis[1] * grids[1]; + quantized = { int_grids[0], int_grids[1] }; + } + else if ((int_grids[2] + int_grids[3]) % 3 == 0) + { + origin = basis[1] * grids[2] + basis[2] * grids[3]; + quantized = { -int_grids[3], int_grids[2] - int_grids[3] }; + } + else if ((int_grids[4] + int_grids[5]) % 3 == 0) + { + origin = basis[2] * grids[4] + basis[0] * grids[5]; + quantized = { int_grids[5] - int_grids[4], -int_grids[4] }; + } + + const std::array slices = { 0, 7, 13, 19 }; + const std::array< float, 3> ranges = { 0, 1, 3 }; + const std::array us = { 0, 1, -1, -2, -1, 1, 2, 3, 0, -3, -3, 0, 3, 2, -2, -4, -2, 2, 4 }; + const std::array vs = { 0, 2, 1, -1, -2, -1, 1, 3, 3, 0, -3, -3, 0, 4, 2, -2, -4, -2, 2 }; + + auto hexagon = [&](const int32_t hash) + { + const auto axis = hash % 3; + return basis[axis] * OffsetX(hash) + basis[(axis+1) % 3] * OffsetY(hash); + }; + + auto sq_dist = std::numeric_limits::max(); + auto cell_id = 0; + glm::vec2 point; + + for (auto dist = 0; dist < 3; ++dist) + { + if (ranges[dist] < sq_dist) + { + for (auto loop = slices[dist]; loop < slices[dist + 1]; ++loop) + { + const auto hash = my_hash(quantized + glm::ivec2(us[loop], vs[loop])); + const auto offset = hexagon(hash) * jitter; + const auto sample = origin + offset + basis[0] * float(us[loop]) + basis[1] * float(vs[loop]); + const auto tmp = glm::dot(source - sample, source - sample); + if (sq_dist > tmp) + { + sq_dist = tmp; + cell_id = hash; + point = sample; + } + } + } + else + { + break; + } + } + + return std::make_tuple(cell_id, sq_dist, point); +} \ No newline at end of file diff --git a/voroce/voroce.vcxproj b/voroce/voroce.vcxproj index bdc7f57..b5206af 100644 --- a/voroce/voroce.vcxproj +++ b/voroce/voroce.vcxproj @@ -89,6 +89,7 @@ false + ..\