Metaballs
A propos du code
Introduction
Vec2f blobPos(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
Puis on va boucler sur chaque pixel de l'écran
for (int y = 0; y < SCREEN_HEIGHT; y++)
for (int x = 0; x < SCREEN_HEIGHT; x++)
{
Vec2f p(x, y);
Et on va calculer la distance du pixel à notre point.
Vec2f delta = p - blobPos;
float dist = delta.mag();
En limitant cette valeur à 255, on peut l'utiliser comme couleur pour dessiner à l'écran.
if (dist > 255)
dist = 255;
int c = (int)dist;
gfx.setPixel(x, y, Color(c, c, c));
}
Télécharger le code source
Télécharger l'exécutable pour Windows
Et voici ce qu'on obtient:
Le rayon du blob
Vec2f blobPos(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
float blobRadius = 100.0f;
Je veux aussi inverser les couleurs de ce qu'on voit à l'écran. Alors je vais soustraire la distance à ce rayon.
float dist = blobRadius - delta.mag();
if (dist < 0)
dist = 0;
if (dist > 255)
dist = 255;
Télécharger le code source
Télécharger l'exécutable pour Windows
Et maintenant on peut voir ça:
Plusieurs blobs
class CBlob
{
public:
CBlob();
Vec2f pos;
float radius;
};
Le constructeur de cette classe va mettre des valeurs aléatoire dans les membres:
CBlob::CBlob()
{
pos.x = rand() % SCREEN_WIDTH;
pos.y = rand() % SCREEN_HEIGHT;
radius = 50 + (rand() % 100);
}
Au début du programme on va allouer un tableau de ces blobs.
srand(time(NULL));
const int nbBlobs = 6;
CBlob* blobs = new CBlob[nbBlobs];
Et dans la boucle principale on va maintenant devoir ajouter les distances de chaque 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));
Télécharger le code source
Télécharger l'exécutable pour Windows
Et voilà le résultat:
La formule des metaballs
for (int i = 0; i < nbBlobs; i++)
{
Vec2f delta = p - blobs[i].pos;
power += blobs[i].radius / delta.mag();
}
Avec cette nouvelle formule, les blobs ont besoin de plus grands rayons:
radius = 2000 + (rand() % 6000);
Télécharger le code source
Télécharger l'exécutable pour Windows
Et voilà ce qu'on peut obtenir:
Animation
class CBlob
{
public:
CBlob();
void update();
Vec2f pos;
Vec2f vel;
float radius;
};
La vélocité sera initialisée au hasard avec des valeurs entre -3 et 3:
CBlob::CBlob()
{
[...]
vel.x = ((rand() % 61) - 30) / 10.0f;
vel.y = ((rand() % 61) - 30) / 10.0f;
Dans la fonction de mise à jour, on va ajouter la vélocité à la position du blob tester chaque bord de l'écran pour
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;
}
}
Puis dans la boucle principale, on va appeler cette fonction de mise à jour pour chaque blob:
for (int i = 0; i < nbBlobs; i++)
blobs[i].update();
Télécharger le code source
Télécharger l'exécutable pour Windows
Coloration
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);
Télécharger le code source
Télécharger l'exécutable pour Windows
Les couleurs sont belles:
Chronométrage
while (sys.isQuitRequested() == false)
{
sys.StartPerfCounter();
Et juste après qu'on ait mis à jour les blobs et avant qu'on fasse le rendu, on le stoppe et on affiche sa valeur.
for (int i = 0; i < nbBlobs; i++)
blobs[i].update();
float tim = sys.StopPerfCounter();
printf("%f\n", tim);
gfx.render();
Télécharger le code source
Télécharger l'exécutable pour Windows
Sur mon ordinateur avec un i7-4770K ça prend environ 93 ms pour calculer une frame. C'est vraiment lent.
Accélérer avec une 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));
}
Ensuite quand on dessine le pixel, on doit seulement prendre la couleur dans cette palette.
int c = (int)power;
gfx.setPixel(x, y, palette[c]);
Télécharger le code source
Télécharger l'exécutable pour Windows
Maintenant avec ce code une frame prend entre 24 et 25 ms.
Supprimer la racine carrée
float Vec2f::mag()
{
return sqrt(squaredMag());
}
C'est une fonction très coûteuse. Mais on peut utiliser la fonction squaredMag() qui renvoie le carré de la
float Vec2f::squaredMag()
{
return x * x + y * y;
}
Maintenant le calcul de la distance dans la boucle principale aura l'air de ça:
for (int i = 0; i < nbBlobs; i++)
{
Vec2f delta = p - blobs[i].pos;
power += blobs[i].radius / delta.squaredMag();
}
Bien sur, utiliser le carré de la distance modifie radicalement le résultat des calculs.
static Color palette[256];
for (int i = 0; i < 256; i++)
{
float angle = (pow(i, 0.64) + 78) * M_PI / 20;
Et on va devoir faire un réglage sur les rayons des blobs aussi:
radius = 4000 + (rand() % 12000);
radius *= 30;
Télécharger le code source
Télécharger l'exécutable pour Windows
Ca m'a pris du temps pour trouver la bonne formule pour la palette, et voilà le résultat:
Supprimer les floats
struct SVec
{
int x, y;
};
class CBlob
{
public:
CBlob();
void update();
SVec pos;
SVec vel;
int radius;
};
Le principal problème quand vous transformez tout en entiers est de garder assez de précision.
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;
}
On utilise le même multiplicateur dans la fonction de mise à jour quand on teste la position avec les limites de
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;
}
}
Dans la boucle principale la variable power sera un entier aussi.
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;
Les calculs en virgule flottante géraient le cas de la division par 0. Maintenant qu'on utilise des entiers, on
if (dist == 0)
power = 255;
else
power += blobs[i].radius / dist;
}
if (power > 255)
power = 255;
Et maintenant on n'a plus besoin de caster power en entier pour pouvoir l'utiliser comme un index de palette.
gfx.setPixel(x, y, palette[power]);
Télécharger le code source
Télécharger l'exécutable pour Windows
Maintenant on voit plus de bandes et d'interférences dans les parties jaunes à cause de la perte de précision.
Supprimer le calcul de la position des blobs
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++)
[...]
Puis dans la boucle intérieure on peut simplement utiliser les valeurs de ces tableaux:
int dX = x - bX[i];
int dY = y - bY[i];
int dist = dX * dX + dY * dY;
Télécharger le code source
Télécharger l'exécutable pour Windows
Ce code prend seulement 7.5 ms par frame. C'est un gain assez intéressant pour une opération si simple.
Supprimer la division
int dist = (dX * dX + dY * dY) / 128;
Ensuite, dans les lignes suivantes, on remplace la division par la valeur d'une table.
if (dist == 0)
power = 255;
else if (dist < 2048)
power += div[i][dist];
Maintenant on doit juste remplir les valeurs de ces 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)
[...]
Télécharger le code source
Télécharger l'exécutable pour Windows
Visuellement il n'y a pas trop de différence par rapport à la dernière image qu'on a prise.
Supprimer un if
if (dist == 0)
power = 255;
Mais on peut se débarrasser de ce test. Si vous avez remarqué on commence à remplir le tableau de divisions
for (int i = 0; i < nbBlobs; i++)
{
div[i][0] = 255;
for (int j = 1; j < 2048; j++)
[...]
Et dans la boucle intérieure on n'a maintenant besoin que d'un test:
int dist = (dX * dX + dY * dY) / 128;
if (dist < 2048)
power += div[i][dist];
Télécharger le code source
Télécharger l'exécutable pour Windows
Ce code prend environ 3.5 ms par frame.
Supprimer l'autre 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;
}
Et la boucle intérieure ressemblera à ça:
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];
}
Télécharger le code source
Télécharger l'exécutable pour Windows
Cette version a besoin d'environ 3.3 ms pour calculer une frame.
Réintégrer la racine carrée
for (int j = 1; j < 4096; j++)
div[i][j] = (float)blobs[i].radius / sqrt(j * 256);
Notre boucle de pixel sera maintenant:
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]);
On doit aussi utiliser la palette d'origine:
float angle = (i + 64 + 150) * M_PI / 128;
Et les rayons d'origine:
radius = 2000 + (rand() % 6000);
Télécharger le code source
Télécharger l'exécutable pour Windows
Maintenant cette version prend environ 4.2 ms par frame, Mais le résultat est beaucoup plus impressionnant: