Physique et mathématiques visuelles/Comment filmer des flots 2D
Ce chapitre donne et explique un programme, écrit en C sharp, avec lequel il est possible de filmer des flots 2D.
Le calcul des trajectoires
[modifier | modifier le wikicode]Un flot dans le plan est une fonction de dans . En chaque point du plan le vecteur vitesse du flot est défini.
Quand le flot est connu, le calcul d'une trajectoire à partir d'un point initial peut être très simple.
On choisit un intervalle de temps petit et on calcule et à partir de et avec les formules :
où et sont les composantes de c'est à dire que
( Mais pour améliorer la précision du calcul, et donc sa puissance, on a intérêt à se servir d'un meilleur algorithme. Celui de Runge-Kutta (présenté dans Blanchard, Devaney et Hall 2011) est particulièrement performant et a été adopté par ce programme dans la fonction R2 Go( int nstep, double t) dans le fichier R2.cs. )
Le nuage de particules entraînées par le flot
[modifier | modifier le wikicode]Un flot transparent et vu de l'intérieur est aussi invisible que l'air. Il faut des nuages ou des poussières pour voir les écoulements.
Si on veut voir une seule ligne de courant, il suffit de créer des particules aléatoirement dans le temps mais toujours à la même place - on a supposé que le flot est stationnaire, c'est à dire qu'il ne varie pas dans le temps. Tout se passe comme si on déposait en permanence un peu d'encre en un point du flot qui l'entraîne.
Dans ce programme, les lignes de courant bleues sont issues d'une même ligne source (ou de plusieurs) qui peut être, mais pas nécessairement, le grand cercle qui entoure l'écran.
Pour visualiser le flot entre les lignes de courant, on crée également des particules noires réparties aléatoirement sur la ligne source.
Pour avoir un film périodique les particules sont recréées à la même place à intervalles de temps réguliers.
Le dessin en haute définition
[modifier | modifier le wikicode]Pour dessiner des mouvements bien réguliers, il ne faut pas représenter les particules par des pixels ou des groupes de pixels, parce qu'on les verrait sauter de pixel en pixel.
Une particule est représentée par sa position - un vecteur dans - et on calcule la façon dont elle colore un pixel à partir de sa distance au centre du pixel. Il faut choisir une fonction qui est maximale pour une distance nulle et nulle pour une distance supérieure au rayon de la particule. En choisissant une forme en par exemple on obtient une transition douce entre la coloration maximale et l'absence de coloration.
Dans un pixel est représenté non par un point mais par une surface carrée. Le pixel 0,0 par exemple, c'est à dire le pixel en haut à gauche, est le carré entre les points (0,0) et (1,1), il est donc centré en (0.5,0.5).
Configuration requise
[modifier | modifier le wikicode]Je compile ce programme avec MonoDevelop dans Ubuntu (Linux) et et je me sers d'Image Magick pour l'assemblage d'une série d'images en un seul film.
Avec quelques adaptations ce programme devrait tourner dans n'importe quel environnement. Il suffit de créer des images Bitmap, de pouvoir les enregistrer en .gif, d'avoir les fonctions SetPixel, qui colore les pixels, et GetPixel, qui donne leur couleur présente, et quelques fonctions mathématiques habituelles : Sin, Cos, Sqrt...
Comment assembler toutes les images en un seul film .gif
[modifier | modifier le wikicode]Le programme crée une série de 50 fichiers image, numérotés de Im100.gif à Im149.gif et les place dans un dossier Film qu'il faut avoir créé au préalable.
Image Magick permet d'assembler ces 50 images en un seul fichier de film .gif.
Si vous avez Ubuntu, Image Magick est déjà installé sur votre ordinateur.
Pour l'utiliser, il faut ouvrir un terminal (si vous ne savez pas faire, essayez Terminal avec l'onglet Chercher sur votre ordinateur en haut à gauche) puis se déplacer dans l'arborescence des dossiers pour trouver le dossier Film (essayez cette adresse si vous ne savez pas faire). Il faut alors écrire les trois lignes de code suivantes, chacune suivie d'une frappe de la touche Enter :
convert -delay 4 \
Im*.gif \
-loop 0 ./Nom_du_film.gif
4 veut dire qu'il y a 4/100 de seconde entre chaque image, donc 25 images par seconde.
-loop 0 veut dire que le film tourne en boucle infinie. Pour n passages, il suffit de choisir -loop n.
Tous les fichiers du programme
[modifier | modifier le wikicode]Pour compiler ce programme, vous devez rassembler tous les fichiers Program.cs, Film.cs, Calculate.cs, R2.cs, Draw.cs, Cloud.cs, Line.cs et LineSource.cs dans un seul projet (un console project avec Monodevelop).
.cs veut dire C sharp ou C#.
Le fichier de définition d'un film
[modifier | modifier le wikicode]L'en-tête donne les noms des paramètres qui servent à calculer un film.
La fonction R2 Flow(R2 v) est un flot 2D, c'est à dire une fonction de dans
Ici elle est définie par Flow(x,y)=(x+y,y)
R2 est la classe des couples de réels.
Un nombre réel est un double pour la machine. Cela veut dire double précision, par opposition à la simple précision (le type float, nommé ainsi pour sa virgule flottante) qui n'est plus très utilisée.
int veut dire nombre entier (integer).
Le fichier Film.cs :
using System; // Technical
using System.Drawing; // Technical : external library for the class Color
namespace See // Technical
{
public class Film // There will be only one element in this class, the film to be made.
{
// List of names of parameters (Technical)
// Parameters of the film
public Color screenColor;
public int wsc, hsc;// w : width / h : height / sc : screen
public int nim; // n : number of / im : images
public double x1, y1, x2, y2;// rectangle of observation of the flow
public double T; // duration of observation
public int nper;// number of periods of creation of particles
public double t;// duration of a period
public int nl;// number of line sources
public LineSource[] ls;// list of line sources
// Technical parameters
public int nsteptot; // total number of steps of calculus
public int nstep; // number of steps in the calculus of one period of creation of particles
public double inw; // internal width of a particle (real number of pixels)
public double exw; // external width of a particle
public double antial;// real number of pixels of anti-aliasing
//
// The flow to be seen : Flow(x,y)=(x+y,y)
//
public static R2 Flow(R2 v) // Flow is a function from R2 to R2
{
R2 w= new R2 (); // w is in R2
w.x = v.x + v.y ;
w.y = v.y ;
return w; // w is the value of the function at the point v : w = Flow(v)
}
// Initialization of the parameters of the film
public Film ()
{
Color black = Color.FromArgb (0, 0, 0);
Color blue = Color.FromArgb (0, 80, 255);
// Color.FromArgb(r,g,b) means an intensity r of red, g of green and b of blue,
// against a black background.
screenColor=Color.FromArgb (255, 255, 255);// White screen
wsc = 1920;// width of screen (in pixels)
hsc = 1080;// height of screen
nim = 50;// number of images
x1 = -16.0;// rectangle of observation of the flow
y1 = +9.0;
x2 = +16.0;
y2 = -9.0;
T = -10.0; // duration of observation; Negative means that time flows backward.
nper = 25; // number of periods of creation of particles
nl = 1; // number of line sources
ls = new LineSource [nl]; // (technical)
ls [0] = new LineSource (black,blue); // black is the color of randomly created paticles
// blue is the color of streamlines
ls [0].l = new Line (2, 0.0, 0.0, Calculate.ObservationRadius(x1,y1,x2,y2));
// 2 means circular line. The other three parameters are
// the coordinates x0,y0 of its center and its radius r
ls [0].a = 0.0; // angle from the horizontal of the first point of this circular arc
ls [0].b = 2*Math.PI;// angle of the last point
ls [0].npartrnd = 200000;// number of particles randomly created on the line
ls [0].nstr = 40; // number of streamlines originated on the line
ls [0].npartstr = 3000; // number of particles on each streamline
// For a straight line, try this code :
/*
ls [0] = new LineSource (black,blue);
ls [0].l = new Line (1, -8.0, -8.0, 8.0, 8.0);
// 1 means straight line. The other four parameters are
// the coordinates x1,y1,x2,y2 of its extremities.
ls [0].npartrnd = 200000;// number of particles randomly created on the line
ls [0].nstr = 20; // number of streamlines originated on the line
ls [0].npartstr = 3000; // number of particles on each streamline
*/
// Technical parameters
nsteptot=100;// total number of steps of calculus
inw = 0.0; // internal width of a particle (real number of pixels)
exw = 3.0; // external width of a particle
antial = 3.0;// real number of pixels of antialiasing
// Calculated parameters
t = T/nper; // duration of a period of creation of particles
nstep = nsteptot / nper; // number of steps in the calculus of one period of creation of particles
}
}
}
Le programme Main
[modifier | modifier le wikicode]Le fichier Program.cs :
using System;
namespace See
{
class MainClass
{
public static void Main (string[] args)
{
Film f= new Film(); // f is the film
Cloud cloud = Calculate.InitialConditions (f);
// cloud is the cloud of particles driven by the current
Draw.FilmCloud(f, cloud);
Console.WriteLine ("The end");
}
}
}
Les autres fichiers
[modifier | modifier le wikicode]Le fichier Calculate.cs :
using System;
using System.Drawing;
namespace See
{
public class Calculate
{
public static double ObservationRadius(double x1, double y1, double x2, double y2)
{
double dx = 0.5 * (x2 - x1);
double dy = 0.5 * (y2 - y1);
return Math.Sqrt (dx * dx + dy * dy);
}
public static Cloud InitialConditions(Film f)
{
// Calculation of the initial clouds
int nn=2*f.nl;// number of sources
Cloud[] cloud = new Cloud[nn];
int npt = 0;//nombre de points au total, à incrémenter
int i;
// Line sources
// Random
for (i = 0; i < f.nl; i++) {
Console.Write (" Line source. Random ");
Console.Write (i);
Console.Write (" ");
cloud [i] = new Cloud (f.ls[i].npartrnd);
cloud [i].InitLineSource (f, f.ls[i].l, f.ls[i].a, f.ls[i].b);
cloud [i].InitBichromatic(f.ls[i].col1, f.ls[i].col2, f.ls[i].mix);
npt += cloud [i].n;
}
// Streamlines (dotted line source)
for (i = f.nl; i < 2*f.nl; i++) {
Console.Write (" Line source. Streamlines ");
Console.Write (i);
Console.Write (" ");
cloud [i] = new Cloud (f.ls[i-f.nl].nstr*f.ls[i-f.nl].npartstr);
cloud [i].InitDottedLineSource (f, f.ls[i-f.nl].nstr, f.ls[i-f.nl].l, f.ls [i - f.nl].a,
f.ls [i - f.nl].b, f.ls[i-f.nl].wst);
cloud [i].InitBichromatic(f.ls[i-f.nl].colstr1, f.ls[i-f.nl].colstr2, f.ls[i-f.nl].mix);
npt += cloud [i].n;
}
return new Cloud (cloud, nn);
}
}
}
Le fichier R2.cs :
using System;
namespace See
{
public class R2
{
public double x,y;
public double Norm()
{
return Math.Sqrt(x*x+y*y);
}
public R2 Plus(R2 v)
{
R2 sum = new R2 ();
sum.x = x + v.x;
sum.y = y + v.y;
return sum;
}
public R2 Times(double lambda)
{
R2 mult= new R2();
mult.x= lambda*x;
mult.y= lambda*y;
return mult;
}
public R2 Go(int nstep, double t)
{
int i;
double dt;
dt = t / nstep;
R2 via1 = new R2 ();
R2 via2 = new R2 ();
R2 via3 = new R2 ();
R2 via4 = new R2 ();
R2 dvia1 = new R2 ();
R2 dvia2 = new R2 ();
R2 dvia3 = new R2 ();
R2 dvia4 = new R2 ();
via1.x = x;
via1.y = y;
for (i = 0; i < nstep; i++) {
// Runge-Kutta method
dvia1 = Film.Flow (via1);
via2 = via1.Plus (dvia1.Times (0.5 * dt));
dvia2 = Film.Flow (via2);
via3 = via1.Plus (dvia2.Times (0.5 * dt));
dvia3 = Film.Flow (via3);
via4 = via1.Plus (dvia3.Times (dt));
dvia4 = Film.Flow (via3);
via1 = via1.Plus (dvia1.Plus (dvia2.Times (2.0).Plus (dvia3.Times (2.0).Plus (dvia4))).Times (1.0 / 6).Times (dt));
}
return via1;
}
}
}
Le fichier Draw.cs :
using System;
using System.Drawing;
using System.Drawing.Imaging;
namespace See
{
class Draw
{
public static void Screen(Bitmap image, Color color)
{
int i, j;
for (i = 0; i < image.Width; i++) {
for (j = 0; j < image.Height; j++) {
image.SetPixel (i, j, color);
}
}
}
public static void Point(Film f, Bitmap im, double x1, double y1, Color colorp)
{
double xx1, yy1;
xx1 = f.wsc * (x1 - f.x1) / (f.x2 - f.x1);
yy1 = f.hsc * (y1 - f.y1) / (f.y2 - f.y1);
int n1 = (int) Math.Floor(xx1-f.exw-0.5);
int n2 = (int) Math.Floor(xx1+f.exw+1.5);
int m1 = (int) Math.Floor(yy1-f.exw-0.5);
int m2 = (int) Math.Floor(yy1+f.exw+1.5);
if (n1<0){n1 = 0;};
if (m1<0){m1 = 0;};
if (n2>im.Width){n2 = im.Width;};
if (m2>im.Height){m2 = im.Height;};
double posx, posy, varcolor;
double dist; //distance du centre du pixel à la ligne;
Color coulp, colorf;
int i, j;
for (i = n1; i < n2; i++) {
posx = i + 0.5;
for (j = m1; j < m2; j++) {
posy = j + 0.5;
dist = Math.Sqrt ((xx1 - posx) * (xx1 - posx) + (yy1 - posy) * (yy1 - posy));
if (dist < f.exw) {
varcolor = RoundedStep (dist, f.inw, f.exw);
colorf = im.GetPixel (i, j);
coulp= Color.FromArgb (colorf.R + (int)(varcolor * (colorp.R - colorf.R)),
colorf.G + (int)(varcolor * (colorp.G - colorf.G)),
colorf.B + (int)(varcolor * (colorp.B - colorf.B)));
if (colorf == f.screenColor) {
im.SetPixel (i, j, coulp);
} else {
im.SetPixel (i,j, Color.FromArgb ( (int)(0.5*(coulp.R + colorf.R)),
(int)(0.5*(coulp.G + colorf.G)),
(int)(0.5*(coulp.B + colorf.B))));
}
}
}
}
}
public static void Cloud(Film f, Bitmap im, Cloud cloud)
{
int i;
for (i = 0; i < cloud.n; i++)
{
Draw.Point(f, im, cloud.pt [i].x, cloud.pt [i].y, cloud.col[i]);
}
}
public static void FilmCloud(Film f, Cloud cloud)
{
Bitmap im = new Bitmap (f.wsc, f.hsc);
int i;
for (i = 0; i < f.nim; i++) {
Draw.Screen (im, f.screenColor);
Draw.Cloud (f, im, cloud);
im.Save ("Film/Im"+ Convert.ToString(i+100)+".gif", ImageFormat.Gif);
cloud.Anim (f.nstep, f.t/f.nim);
}
}
// RoundedStep is for anti-aliasing
public static double RoundedStep(double x, double win, double wout)
{
if (x > wout) {
return 0.0;
}
if (x < win) {
return 1.0;
}
return Math.Pow(Math.Cos(0.5*Math.PI* (x - win) / (wout - win)),4);
}
}
}
Le fichier Cloud.cs :
using System;
using System.Drawing;
namespace See
{
public class Cloud // A cloud of particles
{
public int n; // number of particles
public R2[] pt; // positions of particles
public Color[] col;// colors of particles
public Cloud (int npt)
{
n = npt;
pt = new R2[n];
col = new Color[n];
}
public Cloud ( Cloud[] cloud, int nn) // Union of nn clouds in a single one
{
int i,j,k;
n = 0;
for (i = 0; i < nn; i++) {
n += cloud [i].n;
}
pt = new R2[n];
col = new Color[n];
k = 0;
for (i = 0; i < nn; i++) {
for (j = 0; j < cloud [i].n; j++) {
pt[k] = new R2 ();
pt [k].x = cloud [i].pt [j].x;
pt [k].y = cloud [i].pt [j].y;
col [k] = cloud [i].col [j];
k++;
}
}
}
public void InitBichromatic(Color color1, Color color2, double mix)
// Bichromatic clouds are often nice but difficult for the GIF format which is limited to 256 colors
// (there are many shades between the two colors and with the color of the screen)
{
int i;
Random hasard;
hasard = new Random ();
for (i = 0; i < n; i++) {
if (hasard.NextDouble () < mix) {
col [i] = color1;
} else {
col [i] = color2;
}
}
}
public void InitLineSource(Film f, Line l , double a, double b)
// t is the duration of a period of creation of particles.
// Particles are created at each period at the same places. This is the trick to eternal return.
{
int i,j,n2,m;
n2 = (int)(n / f.nper);
m = 0;
Random hasard = new Random ();
for (i = 0; i < n2; i++) {
pt[i*f.nper] = Line.Def (l, a + (b-a)*hasard.NextDouble ());
pt[i*f.nper] = pt[i*f.nper].Go (f.nstep, f.t * hasard.NextDouble ());
m++;
if (f.nper>1){
pt[i*f.nper+1] = pt[i*f.nper].Go (f.nstep, -f.t );
m++;
if (f.nper > 2) {
pt [i * f.nper + 2] = pt[i*f.nper].Go (f.nstep, f.t);
m++;
for (j = 3; j < f.nper; j++) {
pt [i * f.nper + j] = pt [i * f.nper + j - 1].Go (f.nstep, f.t);
m++;
}
}
}
if (m > 999) {
m = 0;
Console.Write (i*f.nper);
Console.Write (" ");
}
}
}
public void InitDottedLineSource(Film f, int n1, Line l, double a, double b, double dx)
{
int i,j,k,m,n2;
m = 0;
n2 = (int)(n / (n1*f.nper));
double stepx;
stepx = (b - a) / n1;
Random hasard = new Random ();
for (i = 0; i < n1; i++) {
for (j = 0; j < n2; j++) {
pt[(i*n2+j)*f.nper] = Line.Def(l, a + stepx* i + hasard.NextDouble () * dx);
pt[(i*n2+j)*f.nper] = pt[(i*n2+j)*f.nper].Go (f.nstep, f.t * hasard.NextDouble ());
m++;
if (f.nper > 1) {
pt [(i*n2+j)*f.nper + 1] = pt[(i*n2+j)*f.nper].Go (f.nstep, -f.t);
m++;
if (f.nper > 2) {
pt [(i*n2+j)*f.nper + 2] = pt[(i*n2+j)*f.nper].Go (f.nstep, f.t);
m++;
for (k = 3; k < f.nper; k++) {
pt [(i*n2+j)*f.nper + k] = pt [(i*n2+j)*f.nper + k - 1].Go (f.nstep, f.t);
m++;
}
}
}
if (m > 999) {
m = 0;
Console.Write ((i*n2+j)*f.nper);
Console.Write (" ");
}
}
}
}
public void Anim(int nstep, double t)
{
int i;
R2 end =new R2();
for (i = 0; i < n; i++) {
end = pt [i].Go (nstep, t);
pt[i].x=end.x;
pt[i].y=end.y;
}
}
}
}
Le fichier Line.cs :
using System;
namespace See
{
public class Line // Parameterized line
// Def(l,x) gives the position of the point with parameter x on the line l
{
public int num; // number for the type of line
// 1 : straight line
// 2 : circle
public double x1,x2,x3,x4; // parameters for the definition of the line
// straight line from x1,x2 to x3,x4
// circle centered on x1,x2 and radius x3
public Line(int pnum, double px1, double px2, double px3)
{
num = pnum;
x1 = px1;
x2 = px2;
x3 = px3;
x4 = 0.0; // unused parameter for a circle
}
public Line(int pnum, double px1, double px2, double px3, double px4)
{
num = pnum;
x1 = px1;
x2 = px2;
x3 = px3;
x4 = px4;
}
public static R2 Def(Line l, double x)
{
R2 f= new R2 ();
f.x = 0.0;
f.y = 0.0;
if (l.num==1){// straight line from x1,x2 to x3,x4
f.x = l.x1+x*(l.x3-l.x1);
f.y = l.x2+x*(l.x4-l.x2);
}
if (l.num==2){// circle / center x1,x2 / radius x3
f.x = l.x1+l.x3*Math.Cos(x);
f.y = l.x2+l.x3*Math.Sin(x);
}
// other types of lines can be added here
return f;
}
}
}
Le fichier LineSource.cs :
using System;
using System.Drawing;
namespace See
{
public class LineSource // A line which acts as a source of particles
{
public Line l;// parameterized line identifier
public double a,b; // parameters of the beginning and of the end of the line source on the parameterized line
public int npartrnd;//number of particles randomly distributed on the line
public Color col1, col2;// colors of particles
public double mix; // coefficient of random color mixing
// 0.0 : col1 alone / 1.0 : col2 alone
public int nstr;// number of streamlines (particles periodically distributed on the line)
public int npartstr; // number of particles on a streamline
public double wst; // width of streamline
public Color colstr1, colstr2;// colors of particles
public double mixstr; // coefficient of random color mixing
// 0.0 : colstr1 alone / 1.0 : colstr2 alone
public LineSource (Color col, Color colstr)
{
// Initialization
col1 = col;
col2 = col;
colstr1 = colstr;
colstr2 = colstr;
mix = 0.0; // monocromatic cloud
mixstr = 0.0;// monocromatic cloud
a = 0.0;// parameter of the beginning for a straight line
b = 1.0;// parameter of the end for a straight line
wst = 0.0;// infinitely thin streamline
}
public LineSource ()
{
}
}
}