How to smooth the blocks of a 3D voxel world?

2020-08-19 02:40发布


In my (Minecraft-like) 3D voxel world, I want to smooth the shapes for more natural visuals. Let's look at this example in 2D first.

Left is how the world looks without any smoothing. The terrain data is binary and each voxel is rendered as a unit size cube.

In the center you can see a naive circular smoothing. It only takes the four directly adjacent blocks into account. It is still not very natural looking. Moreover, I'd like to have flat 45-degree slopes emerge.

On the right you can see a smoothing algorithm I came up with. It takes the eight direct and diagonal neighbors into account in order to come up with the shape of a block. I have the C++ code online. Here is the code that comes up with the control points that the bezier curve is drawn along.

#include <iostream>

using namespace std;
using namespace glm;

list<list<dvec2>> Points::find(ivec2 block)
    // Control points
    list<list<ivec2>> lines;
    list<ivec2> *line = nullptr;

    // Fetch blocks, neighbours start top left and count
    // around the center block clock wise
    int center = m_blocks->get(block);
    int neighs[8];
    for (int i = 0; i < 8; i++) {
        auto coord = blockFromIndex(i);
        neighs[i] = m_blocks->get(block + coord);

    // Iterate over neighbour blocks
    for (int i = 0; i < 8; i++) {
        int current = neighs[i];
        int next = neighs[(i + 1) % 8];
        bool is_side   = (((i + 1) % 2) == 0);
        bool is_corner = (((i + 1) % 2) == 1);

        if (line) {
            // Border between air and ground needs a line
            if (current != center) {
                // Sides are cool, but corners get skipped when they don't
                // stop a line
                if (is_side || next == center)
            } else if (center || is_side || next == center) {
                // Stop line since we found an end of the border. Always
                // stop for ground blocks here, since they connect over
                // corners so there must be open docking sites
                line = nullptr;
        } else {
            // Start a new line for the border between air and ground that
            // just appeared. However, corners get skipped if they don't
            // end a line.
            if (current != center) {
                line = &lines.back();

    // Merge last line with first if touching. Only close around a differing corner for air
    // blocks.
    if (neighs[7] != center && (neighs[0] != center || (!center && neighs[1] != center))) {
        // Skip first corner if enclosed
        if (neighs[0] != center && neighs[1] != center)
        if (lines.size() == 1) {
            // Close circle
            auto first_point = lines.front().front();
        } else {
            // Insert last line into first one
            lines.front().insert(lines.front().begin(), line->begin(), line->end());

    // Discard lines with too few points
    auto i = lines.begin();
    while (i != lines.end()) {
        if (i->size() < 2)

    // Convert to concrete points for output
    list<list<dvec2>> points;
    for (auto &line : lines) {
        for (auto &neighbour : line)
    return points;

glm::ivec2 Points::blockFromIndex(int i)
    // Returns first positive representant, we need this so that the
    // conditions below "wrap around"
    auto modulo = [](int i, int n) { return (i % n + n) % n; };

    ivec2 block(0, 0);
    // For two indices, zero is right so skip
    if (modulo(i - 1, 4))
        // The others are either 1 or -1
        block.x = modulo(i - 1, 8) / 4 ? -1 : 1;
    // Other axis is same sequence but shifted
    if (modulo(i - 3, 4))
        block.y = modulo(i - 3, 8) / 4 ? -1 : 1;
    return block;

dvec2 Points::pointTowards(ivec2 neighbour)
    dvec2 point;
    point.x = static_cast<double>(neighbour.x);
    point.y = static_cast<double>(neighbour.y);

    // Convert from neighbour space into
    // drawing space of the block
    point *= 0.5;
    point += dvec2(.5);

    return point;

However, this is still in 2D. How to translate this algorithm into three dimensions?


You should probably have a look at the marching cubes algorithm and work from there. You can easily control the smoothness of the resulting blob:

  1. Imagine that each voxel defines a field, with a high density at it's center, slowly fading to nothing as you move away from the center. For example, you could use a function that is 1 inside a voxel and goes to 0 two voxels away. No matter what exact function you choose, make sure that it's only non-zero inside a limited (preferrably small) area.
  2. For each point, sum the densities of all fields.
  3. Use the marching cubes algorithm on the sum of those fields
  4. Use a high resolution mesh for the algorithm

In order to change the look/smoothness you change the density function and the threshold of the marching cubes algorithm. A possible extension to marching cubes to create smoother meshes is the following idea: Imagine that you encounter two points on an edge of a cube, where one point lies inside your volume (above a threshold) and the other outside (under the threshold). In this case many marching cubes algorithms place the boundary exactly at the middle of the edge. One can calculate the exact boundary point - this gets rid of aliasing.

Also I would recommend that you run a mesh simplification algorithm after that. Using marching cubes results in meshes with many unnecessary triangles.


As an alternative to my answer above: You could also use NURBS or any algorithm for subdivision surfaces. Especially the subdivision surfaces algorithms are spezialized to smooth meshes. Depending on the algorithm and it's configuration you will get smoother versions of your original mesh with

  • the same volume
  • the same surface
  • the same silhouette

and so on.


Use 3D implementations for Biezer curves known as Biezer surfaces or use the B-Spline Surface algorithms explained: