Search code examples
c#marching-cubes

Understanding Marching Square algorithm


The following picture is retrieved from Wikipedia:

I haven't understood this part:

I have two questions here:

  1. Where did they obtain [8,4,1,2] from and what did they want to tell us by that?
  2. Take a look at cell [0, 0] whose value is 13. If I go clockwise along with its contouring values, I obtain the binary string 0010 which is 2. How does the 1st cell-value become 13?

.

enum What
{
    lines, surface, both
}
class Program
{
    public static void Print(int[,] data, int xn, int yn)
    {
        for (int j = 0; j < yn; j++)
        {
            for (int i = 0; i < xn; i++)
            {
                Console.Write(data[i,j] + ", ");
            }
            Console.WriteLine();
        }
    }
    public static int[,] normalize(int[,] data, int xn, int yn)
    {

        for (int j = 0; j < yn; j++)
        {
            for (int i = 0; i < xn; i++)
            {
                if (data[i, j] > 1)
                {
                    data[i, j] = 0;
                }
                else
                {
                    data[i, j] = 1;
                }
            }
        }

        return data;
    }

    public static int[,] marching_square(int x, int y, int[,] data, int isovalue, What what)
    {
        int xn = x;
        int yn = y;

        data = normalize(data, xn, yn);

        int[,] bitMask = new int[xn - 1, yn - 1];


        for (int j = 0; j < yn - 1; j++)
        {
            for (int i = 0; i < xn - 1; i++)
            {
                StringBuilder sb = new StringBuilder();
                sb.Append(data[i, j]);
                sb.Append(data[i, j + 1]);
                sb.Append(data[i + 1, j]);
                sb.Append(data[i + 1, j + 1]);

                bitMask[i, j] = Convert.ToInt32(sb.ToString(), 2);
            }
        }

        return bitMask;
    }

    static void Main(string[] args)
    {
        int[,] data = new int[,] { 
                                  { 1,1,1,1,1 },
                                  { 1,2,3,2,1 },
                                  { 1,3,3,3,1 },
                                  { 1,2,3,2,1 },
                                  { 1,1,1,1,1 }
                                  };

        Print(data, 5,5);

        int[,] bitMask = marching_square(5,5,data, 0, What.lines);

        Print(bitMask, 4, 4);
    }
}

Output:

1, 1, 1, 1, 1,
1, 2, 3, 2, 1,
1, 3, 3, 3, 1,
1, 2, 3, 2, 1,
1, 1, 1, 1, 1,

14, 10, 10, 11,
12, 0, 0, 3,
12, 0, 0, 3,
13, 5, 5, 7,

I inverted the bits. But, the output looks different.


Solution

  • If you read carefully you will find the answers to your questions are in the three points specified as text just above the image you have included:

    For each cell in the contouring grid:

    1. Compose the 4 bits at the corners of the cell to build a binary index: walk around the cell in a clockwise direction appending the bit to the index, using bitwise OR and left-shift, from most significant bit at the top left, to least significant bit at the bottom left. The resulting 4-bit index can have 16 possible values in the range 0–15.

    2. Use the cell index to access a pre-built lookup table with 16 entries listing the edges needed to represent the cell (shown in the lower right part of the picture below).

    3. Apply linear interpolation between the original field data values to find the exact position of the contour line along the edges of the cell.

    So for your questions, the four numbers are bits that allow you to assign a unique value to each 2x2 pixel block/kernel for each 1/0 combination of the four corners (you meant the white pixels as set, instead they are the black ones).

    Now, proceeding clockwise from the bottom left corner of the block/kernel you need to logically OR the bits and you will get a value in the range 0-15:

    (1 << 0) | (0 << 1) | (1 << 2) | (1 << 3) == 13
    

    Here the set/unset pixel is the left operand of each left-shift operator.

    This value will be used as an index to choose which type of segment to use to draw the contour.

    Choosing to go clockwise or counterclockwise around each block is just a convention and depends on which direction you want the contours generated by concatenating the segments to go, clockwise or counterclockwise.

    Calculating the value of 2x2 blocks and then drawing the contour is an optimization that allows you to generate the types of segments you need beforehand and then parallelize the contour calculation if necessary.