I has looking into Marching cube algorithms for extracting meshes from volume data yesterday, and thought, as a little test, I’d try making a very simple 2D edge-detection type setup, using some of the same principles. There’s lots of stuff on the web about marching cubes, and the 2D version, marching squares, so I’m not going to go into a detailed explanation here, but to summarise my setup:

• divide the image into tiles

• for each tile, test to see if the colour/luminosity value pixel at each corner is above or below a given threshold value.

• use the results of these tests to lookup into a table.

In my case, this table is an image strip something like this (I’ve added the numbers for debugging purposes)

In the original Marching Cubes code, the logic for choosing the correct cell from the table is quite elegant. You basically create a binary number, where each digit is either a 0 or a 1, according to whether the corresponding grid-point value is above or below the threshold. Decoding the binary into an integer gives you the index into the table. Unfortunately, because GLSL doesn’t have support for binary numbers, I’ve had to kludge that a bit, but it works fine.

• draw the selected table cell into the working tile of the input image

Here’s some results.

It’s a bit glitchy around the edges of the tiles, but I’m quite pleased with it. I’ve added some controls to recolour the tiles in various ways, and to deliberately scan the corners in the wrong order, for a more abstract look.

I call this ‘non-marching cubes’ because I’m not really doing the marching bit. Variations on this technique are often used iteratively to detect, and then follow edges in an image. Tools like the Magic Wand in Photoshop use similar techniques to create selections based on colour values, for example.

Vertex Shader code for the simple black-and-white version:

uniform float CellCount;
varying vec2 cellCoords;
void main()
{
//Transform vertex by modelview and projection matrices
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
//Forward current color and texture coordinates after applying texture matrix
gl_FrontColor = gl_Color;
gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
cellCoords = gl_TexCoord[0].xy * CellCount;
}

..and the Fragment Shader:

uniform sampler2D Texture, EdgeTable;
uniform vec2 CellSize;
uniform float Threshold;
uniform int Scramble;
uniform vec2 ClampAmt; // Values for clamping edges
varying vec2 cellCoords;
const float eTableCellWidth = 1.0 / 16.0;
void main()
{
// Cell corners (putting in vec4 allows swizzling for different tile patterns)
vec4 corners;
corners[0] = max(floor(cellCoords.x) * CellSize.x, ClampAmt[0]); // Left
corners[1] = min(ceil(cellCoords.x) *CellSize.x, 1.0 - ClampAmt[0]); // Right
corners[2] = max(floor(cellCoords.y) * CellSize.y, ClampAmt[0]); // Top
corners[3] = min(ceil(cellCoords.y) * CellSize.y, 1.0 - ClampAmt[0]); // Bottom
// Swizzle corners for different effects
corners = (Scramble == 1) ? corners.xywz : (Scramble == 2) ? corners.yxzw : (Scramble == 3) ? corners.zwyx : corners;
// Sample corners
float c0 = texture2D(Texture, vec2(corners[0], corners[2])).r;
float c1 = texture2D(Texture, vec2(corners[1], corners[2])).r;
float c2 = texture2D(Texture, vec2(corners[0], corners[3])).r;
float c3 = texture2D(Texture, vec2(corners[1], corners[3])).r;
// Clamp coords for edge-table
vec2 cellCoordsClamp = fract(cellCoords);
cellCoordsClamp.x = clamp(cellCoordsClamp.x, ClampAmt[1], 1.0 - ClampAmt[1]);
cellCoordsClamp.y = clamp(cellCoordsClamp.y,ClampAmt[1], 1.0 - ClampAmt[1]);
// Calculate index for edge-table
// Returns 0.0 > 15.0 to index into edge-table strip
float index = 0.0;
index += step(Threshold, c0);
index += step(Threshold, c1) * 2.0;
index += step(Threshold, c2) * 4.0;
index += step(Threshold, c3) * 8.0;
// Sample edge-table and return
vec2 ttcoords = vec2(cellCoordsClamp.x * eTableCellWidth + (eTableCellWidth * index), cellCoordsClamp.y);
gl_FragColor = texture2D(EdgeTable, ttcoords);
}