Metaballs
About the code
Introduction
Vec2f blobPos(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
Then we will loop over each pixel of the screen
for (int y = 0; y < SCREEN_HEIGHT; y++)
for (int x = 0; x < SCREEN_HEIGHT; x++)
{
Vec2f p(x, y);
And we will compute the distance of the pixel from our point.
Vec2f delta = p - blobPos;
float dist = delta.mag();
By limiting this value to 255, we can use it as a color to draw on the screen.
if (dist > 255)
dist = 255;
int c = (int)dist;
gfx.setPixel(x, y, Color(c, c, c));
}
Download source code
Download executable for Windows
And this is what we get:
Blob's radius
Vec2f blobPos(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
float blobRadius = 100.0f;
I want to invert the colors of what we see on the screen too. So I will substract the distance from this radius.
float dist = blobRadius - delta.mag();
if (dist < 0)
dist = 0;
if (dist > 255)
dist = 255;
Download source code
Download executable for Windows
And now we can see that:
Several blobs
class CBlob
{
public:
CBlob();
Vec2f pos;
float radius;
};
The constructor of this class will put random values in the members:
CBlob::CBlob()
{
pos.x = rand() % SCREEN_WIDTH;
pos.y = rand() % SCREEN_HEIGHT;
radius = 50 + (rand() % 100);
}
At the beginning of the program we will allocate an array of these blobs.
srand(time(NULL));
const int nbBlobs = 6;
CBlob* blobs = new CBlob[nbBlobs];
And in the main loop we will now need to add the distances from each blob.
Vec2f p(x, y);
float power = 0;
for (int i = 0; i < nbBlobs; i++)
{
Vec2f delta = p - blobs[i].pos;
float dist = blobs[i].radius - delta.mag();
if (dist < 0)
dist = 0;
power += dist;
}
if (power > 255)
power = 255;
int c = (int)power;
gfx.setPixel(x, y, Color(c, c, c));
Download source code
Download executable for Windows
And this is the result:
The metaballs formula
for (int i = 0; i < nbBlobs; i++)
{
Vec2f delta = p - blobs[i].pos;
power += blobs[i].radius / delta.mag();
}
With this new formula, the blobs need bigger radiuses:
radius = 2000 + (rand() % 6000);
Download source code
Download executable for Windows
And this is what we can get:
Animating
class CBlob
{
public:
CBlob();
void update();
Vec2f pos;
Vec2f vel;
float radius;
};
The velocity will be initialized randomly with values between -3 and 3:
CBlob::CBlob()
{
[...]
vel.x = ((rand() % 61) - 30) / 10.0f;
vel.y = ((rand() % 61) - 30) / 10.0f;
In the update function we will add the velocity to the blob's position and check each side of the screen to make it
void CBlob::update()
{
pos += vel;
if (pos.x < 0)
{
pos.x = 0;
vel.x = -vel.x;
}
if (pos.x > SCREEN_WIDTH - 1)
{
pos.x = SCREEN_WIDTH - 1;
vel.x = -vel.x;
}
if (pos.y < 0)
{
pos.y = 0;
vel.y = -vel.y;
}
if (pos.y > SCREEN_HEIGHT - 1)
{
pos.y = SCREEN_HEIGHT - 1;
vel.y = -vel.y;
}
}
Then in the main loop we will call this update function for each blob:
for (int i = 0; i < nbBlobs; i++)
blobs[i].update();
Download source code
Download executable for Windows
Colouring
Color c;
float angle = (power + 214) * M_PI / 128;
c.r = 127 * (1 + sin(angle));
c.g = 127 * (1 + sin(angle + 2 * M_PI / 3));
c.b = 127 * (1 + sin(angle + 4 * M_PI / 3));
gfx.setPixel(x, y, c);
Download source code
Download executable for Windows
The colors are nice:
Timing
while (sys.isQuitRequested() == false)
{
sys.StartPerfCounter();
And just after we updated the blobs and before rendering we stop it and print its value.
for (int i = 0; i < nbBlobs; i++)
blobs[i].update();
float tim = sys.StopPerfCounter();
printf("%f\n", tim);
gfx.render();
Download source code
Download executable for Windows
On my computer with an i7-4770K it takes about 93 ms to compute a frame. That's very slow.
Speeding up with a palette
static Color palette[256];
for (int i = 0; i < 256; i++)
{
float angle = (i + 64 + 150) * M_PI / 128;
palette[i].r = 127 * (1 + sin(angle));
palette[i].g = 127 * (1 + sin(angle + 2 * M_PI / 3));
palette[i].b = 127 * (1 + sin(angle + 4 * M_PI / 3));
}
Then when we draw the pixel we only have to take the color in this palette.
int c = (int)power;
gfx.setPixel(x, y, palette[c]);
Download source code
Download executable for Windows
Now a frame with this code takes between 24 and 25 ms.
Getting rid of the square root
float Vec2f::mag()
{
return sqrt(squaredMag());
}
That's a very expensive function. But we can use the squaredMag() function instead which returns the squared
float Vec2f::squaredMag()
{
return x * x + y * y;
}
Now the distance calculation in the main loop will look like that:
for (int i = 0; i < nbBlobs; i++)
{
Vec2f delta = p - blobs[i].pos;
power += blobs[i].radius / delta.squaredMag();
}
Of course using the squared distance changes radically the result of the calculation.
static Color palette[256];
for (int i = 0; i < 256; i++)
{
float angle = (pow(i, 0.64) + 78) * M_PI / 20;
And we will have to tune the radii of the blobs too:
radius = 4000 + (rand() % 12000);
radius *= 30;
Download source code
Download executable for Windows
It took me some time to find the right formula for the palette, but here is the result:
Getting rid of the floats
struct SVec
{
int x, y;
};
class CBlob
{
public:
CBlob();
void update();
SVec pos;
SVec vel;
int radius;
};
The main problem when you turn everything to integers is to keep enough precision.
CBlob::CBlob()
{
pos.x = (rand() % SCREEN_WIDTH) * 256;
pos.y = (rand() % SCREEN_HEIGHT) * 256;
vel.x = (((rand() % 61) - 30) * 256) / 20;
vel.y = (((rand() % 61) - 30) * 256) / 20;
radius = 4000 + (rand() % 12000);
radius *= 30;
}
We use the same multiplier in the update function when we test the position against the screen limits.
void CBlob::update()
{
pos.x += vel.x;
pos.y += vel.y;
if (pos.x < 0)
{
pos.x = 0;
vel.x = -vel.x;
}
if (pos.x > (SCREEN_WIDTH - 1) * 256)
{
pos.x = (SCREEN_WIDTH - 1) * 256;
vel.x = -vel.x;
}
if (pos.y < 0)
{
pos.y = 0;
vel.y = -vel.y;
}
if (pos.y > (SCREEN_HEIGHT - 1) * 256)
{
pos.y = (SCREEN_HEIGHT - 1) * 256;
vel.y = -vel.y;
}
}
In the main loop the power will be an integer too.
int power = 0;
for (int i = 0; i < nbBlobs; i++)
{
int dX = x - (blobs[i].pos.x + 128) / 256;
int dY = y - (blobs[i].pos.y + 128) / 256;
int dist = dX * dX + dY * dY;
The floating point calculations handled the case of a division by zero. Now that we use integers we have to add a
if (dist == 0)
power = 255;
else
power += blobs[i].radius / dist;
}
if (power > 255)
power = 255;
And now we don't need to cast power to an integer to be able to use it as a palette index.
gfx.setPixel(x, y, palette[power]);
Download source code
Download executable for Windows
Now we see more banding and interferences in the yellow areas due to the loss of precision.
Getting rid of the blobs position calculation
while (sys.isQuitRequested() == false)
{
sys.StartPerfCounter();
static int bX[nbBlobs];
static int bY[nbBlobs];
for (int i = 0; i < nbBlobs; i++)
{
bX[i] = (blobs[i].pos.x + 128) / 256;
bY[i] = (blobs[i].pos.y + 128) / 256;
}
for (int y = 0; y < SCREEN_HEIGHT; y++)
[...]
Then in the inner loop we can simply use the values of these arrays:
int dX = x - bX[i];
int dY = y - bY[i];
int dist = dX * dX + dY * dY;
Download source code
Download executable for Windows
This code only needs 7.5 ms per frame. That's a quite interesting gain for such a simple operation.
Getting rid of the division
int dist = (dX * dX + dY * dY) / 128;
Then in the following lines we will replace the division by an array value.
if (dist == 0)
power = 255;
else if (dist < 2048)
power += div[i][dist];
Now we only have to fill the values of these tables.
static int div[nbBlobs][2048];
for (int i = 0; i < nbBlobs; i++)
for (int j = 1; j < 2048; j++)
div[i][j] = (blobs[i].radius / 128) / j;
while (sys.isQuitRequested() == false)
[...]
Download source code
Download executable for Windows
Visually, there is not much difference from the last picture we took.
Getting rid of an if
if (dist == 0)
power = 255;
But we can get rid of this test. If you noticed we began to fill the precomputed division table at 1, because we
for (int i = 0; i < nbBlobs; i++)
{
div[i][0] = 255;
for (int j = 1; j < 2048; j++)
[...]
And in the inner loop we now just need one test:
int dist = (dX * dX + dY * dY) / 128;
if (dist < 2048)
power += div[i][dist];
Download source code
Download executable for Windows
This code takes about 3.5 ms per frame.
Getting rid of the other if
static int div[nbBlobs][4096];
for (int i = 0; i < nbBlobs; i++)
{
div[i][0] = 255;
for (int j = 1; j < 4096; j++)
div[i][j] = (blobs[i].radius / 256) / j;
}
And the inner loop will look like this:
for (int i = 0; i < nbBlobs; i++)
{
int dX = x - bX[i];
int dY = y - bY[i];
int dist = (dX * dX + dY * dY) / 256;
power += div[i][dist];
}
Download source code
Download executable for Windows
This version needs about 3.3 ms to compute a frame.
Bringing back the square root
for (int j = 1; j < 4096; j++)
div[i][j] = (float)blobs[i].radius / sqrt(j * 256);
Our pixel loop will now be:
float power = 0;
for (int i = 0; i < nbBlobs; i++)
{
int dX = x - bX[i];
int dY = y - bY[i];
int dist = (dX * dX + dY * dY) / 256;
power += div[i][dist];
}
int p = int(power);
if (p > 255)
p = 255;
gfx.setPixel(x, y, palette[p]);
We also need to use the original palette:
float angle = (i + 64 + 150) * M_PI / 128;
And the original radiuses:
radius = 2000 + (rand() % 6000);
Download source code
Download executable for Windows
Now this version takes about 4.2 ms per frame, but the result is far more impressive: