11

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.

No smoothing, circular smoothing, and bezier smoothing

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)
                    line->push_back(blockFromIndex(i));
            } 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) {
                lines.emplace_back();
                line = &lines.back();
                line->push_back(blockFromIndex(i));
            }
        }
    }

    // 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)
            lines.front().pop_front();
        if (lines.size() == 1) {
            // Close circle
            auto first_point = lines.front().front();
            lines.front().push_back(first_point);
        } else {
            // Insert last line into first one
            lines.front().insert(lines.front().begin(), line->begin(), line->end());
            lines.pop_back();
        }
    }

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

    // Convert to concrete points for output
    list<list<dvec2>> points;
    for (auto &line : lines) {
        points.emplace_back();
        for (auto &neighbour : line)
            points.back().push_back(pointTowards(neighbour));
    }
    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?

danijar
  • 32,406
  • 45
  • 166
  • 297
  • "Known algorithms that meight provide a similar result.". Please, please, look up marching cubes. You wouldn't want to make a new sorting algorithm without understanding quicksort first either. – starmole Sep 14 '12 at 05:59
  • Your technique can't handle overhanging terrain. So it's impossible to use your algorithm to handle overhanging cliffs. You *need* a different algorithm. Also all algorithms listed as answers are "ready (?) algorithms that are known". – Simon Sep 14 '12 at 09:01
  • Certainly my algorithm can't handle overhangs. That's why I would love your help to improve and generalize it. By now it is orientated on the top of my squares. I believe that there is a way to bring it up to handle all sides of cubes the same way. (I understand marching cubes but it is not what I need, it is another approach.) – danijar Sep 14 '12 at 09:38
  • Are you saying that marching cubes does not solve your problem? Why not? The answer might allow me to give a better answer. – Simon Sep 16 '12 at 20:44
  • i have the same problem but marching cubes simply made blocks in to 45 degree slopes not very smooth at all ... i couldn't figure out how to convert voxel "on off" data in to density data in any way that marching cubes would render smooth edges. – War Apr 10 '13 at 17:32

3 Answers3

4

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.

Simon
  • 1,814
  • 20
  • 37
  • I realized that I will do some research in my algorithm on my own. However this answer helped me the most, so I accept that. Thanks for your other answer, too. – danijar Oct 09 '12 at 16:38
  • hey danijar did you figure this out in the end? I still have a problem with my marching cubes ... only seems to do a very simple cube to 45 degree slope type conversion ... others have suggested implementing "surface nets" on the mc results and I have not yet figured out the uv mappings on the new mc mesh either since there are now definable "block faces" any more. – War Apr 10 '13 at 17:36
  • @Wardy You will probably want to take a look at this WebGL demo: * http://0fps.net/2012/07/12/smooth-voxel-terrain-part-2/ Here is the canonical paper for Marching Cube -- http://paulbourke.net/geometry/polygonise/ aka "Polygonising a scalar field" – Michaelangel007 Oct 01 '14 at 19:48
2

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.

Simon
  • 1,814
  • 20
  • 37
  • 1
    Also, have a look at dual contouring. Here's the paper http://www.frankpetterson.com/publications/dualcontour/dualcontour.pdf and some related SO question http://stackoverflow.com/questions/6485908/basic-dual-contouring-theory – Simon Sep 12 '12 at 12:03
1

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

here

or

here

nicholas
  • 2,969
  • 20
  • 39
  • Ok I mentioned bezier surfaces in my question. But how could I find the control points then? And how could I get it to handle overhangs. The question is about an algorithm or an approach to that. – danijar Sep 10 '12 at 16:36
  • @danijar Since a Bezier curve is always constrained inside the convex hull of the control points, you can simply "expand" your terrain data out by 1 blocks from the surface and use _those_ as the control points. – Michaelangel007 Oct 01 '14 at 18:55