Skip to content

Commit

Permalink
support hexagonal grid
Browse files Browse the repository at this point in the history
  • Loading branch information
shinjiogaki committed Mar 9, 2021
1 parent 9e1a743 commit d789482
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 104 deletions.
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
## What is this?

<div align="center">
<img src="img/stone.jpg" alt="stone" title="stone" width="480"><img src="img/flake.png" alt="flake" title="flake" width="480">
<img src="img/fur.png" alt="fur" title="fur" width="960">
<img src="img/stone.jpg" alt="stone" title="stone" width="480">
<img src="img/flake.png" alt="flake" title="flake" width="480">
<img src="img/fur.png" alt="fur" title="fur" width="480">
<img src="img/hex.gif" alt="hex" title="hex" width="480">
</div>

**voroce** is a fast and simple voronoi class useful to create images like the above ones. **voroce** is a coind word of **voronoi** & **veloce**.
Expand All @@ -15,7 +17,7 @@
<img src="img/figure3.png" alt="order" title="order" width="320">
</div>

[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

Expand All @@ -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

Expand All @@ -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)
Expand Down
Binary file added img/hex.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed img/honeycomb.gif
Binary file not shown.
18 changes: 18 additions & 0 deletions include/voroce.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<int32_t>()(std::hash<int32_t>()(p.x) + p.y);
}

static int32_t Hash3D(const glm::ivec3& p)
{
return std::hash<int32_t>()(std::hash<int32_t>()(std::hash<int32_t>()(p.x) + p.y) + p.z);
}

static int32_t Hash4D(const glm::ivec4& p)
{
return std::hash<int32_t>()(std::hash<int32_t>()(std::hash<int32_t>()(std::hash<int32_t>()(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)
Expand Down Expand Up @@ -125,5 +142,6 @@ namespace voroce

// non rectangular grids
static std::tuple<int32_t, float, glm::vec2> Evaluate2DTri(const glm::vec2& source, int32_t(*my_hash)(const glm::ivec2& p), const float jitter = 1.0f);
static std::tuple<int32_t, float, glm::vec2> Evaluate2DHex(const glm::vec2& source, int32_t(*my_hash)(const glm::ivec2& p), const float jitter = 1.0f);
};
}
294 changes: 197 additions & 97 deletions src/voroce.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,103 +310,6 @@ std::tuple<int32_t, float, glm::vec2> Voronoi::Evaluate2DCache(const glm::vec2&
return std::make_tuple(cell_id, sq_dist, point);
}

// naive triangle implementation
std::tuple<int32_t, float, glm::vec2> 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<float>::max();
auto cell_id = 0;
glm::vec2 point;

// 1 (self) + 14 (neighbours)
const auto size = 15;
const std::array<int32_t, size> 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<int32_t, size> 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<int32_t, size> 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<int32_t, float, glm::vec3> Voronoi::Evaluate3DRef(const glm::vec3& source, int32_t (*my_hash)(const glm::ivec3& p), const float jitter)
{
Expand Down Expand Up @@ -1115,3 +1018,200 @@ std::tuple<int32_t, float, glm::vec4> Voronoi::Evaluate4DCache(const glm::vec4&

return std::make_tuple(cell_id, sq_dist, point);
}

// naive triangle implementation
std::tuple<int32_t, float, glm::vec2> 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<float>::max();
auto cell_id = 0;
glm::vec2 point;

// 1 (self) + 14 (neighbours)
const auto size = 15;
const std::array<int32_t, size> 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<int32_t, size> 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<int32_t, size> 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<int32_t, float, glm::vec2> 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<glm::vec2, 3> 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<glm::vec2, 3> ortho =
{
glm::vec2(-1 / sqrt_3, -1),
glm::vec2( 1 / sqrt_3, -1),
glm::vec2( 2 / sqrt_3, 0)
};

const std::array<float, 3> dots =
{
glm::dot(ortho[0], source),
glm::dot(ortho[1], source),
glm::dot(ortho[2], source)
};

const std::array<float, 6> 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<int32_t, 4> slices = { 0, 7, 13, 19 };
const std::array< float, 3> ranges = { 0, 1, 3 };
const std::array<int32_t, 19> us = { 0, 1, -1, -2, -1, 1, 2, 3, 0, -3, -3, 0, 3, 2, -2, -4, -2, 2, 4 };
const std::array<int32_t, 19> 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<float>::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);
}
1 change: 1 addition & 0 deletions voroce/voroce.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>..\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
Expand Down

0 comments on commit d789482

Please sign in to comment.