Réaliser un effet graphique avec DirectX sous DELPHI par :
Plouf
(plouf@win.be ou theplouf@yahoo.com)

PARTIE III (3/3)

Chapitre 3 : Les métasphères
a) Le programme de base
Bien, avant toute chose, nous savons qu'il va nous falloir afficher plusieurs de ces sprites, donc il est une bonne idée de prévoir une structure "tball" afin que l'on s'y retrouve. Une telle structure doit contenir la position, et la direction de la balle :
type
tball=record
px,py:single;
dx,dy:single;
end;
.
A partir de maintenant, pensons à rendre l'effet indépendant de la résolution, et mettons donc les variables tx et ty en constantes au début du programme. Nous mettrons également le rayon de la balle ainsi que le nombre de balles affichées :
const
nballs=40;
ray=50;
tx=640;
ty=480;
.
De ce fait, le nombre de nouvelles variables à créer est faible : il nous faut l'ensemble des balles, ainsi qu'un tableau contenant le graphisme de ces balles :
ball:array[0..nballs-1] of tball;
graph:array[0..4*ray*ray-1] of byte;
.
Remarquez que je n'ai pas mis comme on pouvait s'y attendre graph
:array[0..2*ray-1,0..2*ray-1]
Ce choix sera expliqué plus tard. Sinon, en mémoire, cela revient exactement au même...
.
Bien, faisons les choses dans l'ordre, tout d'abord il nous fait créer le graphisme des balles, et donc remplir notre tableau graph.
procedure createball;
var x,y,c:integer;
dist:single;
begin
for y:=0 to 2*ray-1 do begin
for x:=0 to 2*ray-1 do begin
dist:=sqrt(sqr(x-ray)+sqr(y-ray));
if dist255 then c:=255;
end else c:=0;
graph[x+y*2*ray]:=c;
end;
end;
end;
.
On comprend aisément la syntaxe de la ligne graph[x+y*2*ray]:=c, c'est toujours le même système du x+y*longueur_de_ligne avec évidement longueur=2*ray.
Il nous faut à présent une routine qui gère le déplacement des balles. J'ai personellement une préférence "gravitationelle", chaque balle attire les autres :
procedure move;
var i,j:integer;
dist:single;
begin
for i:=0 to nballs-1 do begin
for j:=0 to nballs-1 do begin
if i<>j then begin
dist:=sqrt(sqr(ball[i].px-ball[j].px)+sqr(ball[i].py-ball[j].py));
if dist>200 then begin
ball[i].dx:=ball[i].dx+250*(ball[j].px-ball[i].px)/(dist*dist*dist);
ball[i].dy:=ball[i].dy+250*(ball[j].py-ball[i].py)/(dist*dist*dist);
end;
end;
end;
ball[i].px:=ball[i].px+ball[i].dx;
ball[i].py:=ball[i].py+ball[i].dy;
if (ball[i].px<0) or (ball[i].px>=tx) then ball[i].dx:=ball[i].dx*0.999;
if (ball[i].py<0) or (ball[i].py>=ty) then ball[i].dy:=ball[i].dy*0.999;
end;
end;
.
Bon, on se fiche des détails ici, cet algo marche bien et puis on est contents, ça n'est pas un tutoriel sur le déplacement de particules...
Là où ça devient réellement interessant, c'est quand il nous faut afficher les balles, créons donc une routine pour ce faire :
procedure showball(px,py:integer;desc:tddsurfacedesc);
Qui doit donc afficher une métaball sur la surface précisée par le déscripteur de surface qu'on lui donne, à la coordonée également donnée. Le déscripteur est celui retourné par Lock.
Nous devons prendre en compte le fait que la balle pourrait sortir de l'écran, auquel cas nous ne devons pas l'afficher. Nous devons également prévoir le cas où la balle ne serait que partiellement visible.
Calculons d'abord les coordonées des coins du rectangle que nous devons modifier à l'écran :
a:=px-ray;
b:=px+ray-1;
c:=py-ray;
d:=py+ray-1;
.
Ensuite, nous pouvons directement voir si la balle est totalement invisible :
if (a>=tx) or (c>=ty) or (b<0) or (d<0) then exit;
.
Le cas intermédiaire est plus délicat à gérer, mais on comprendra facilement ces lignes :
if a<0 then a:=0;
if b>=tx then b:=tx-1;
if c<0 then c:=0;
if d>=ty then d:=ty-1;
.
A partir de cet instant, nous sommes sûrs que notre zone se trouve totalement dans l'écran.
Ensuite, comme nous avons affaire à un pointeur vers l'écran, et que la situation est approximativement la même pour la balle, nous devons connaître l'addresse du premier pixel modifié à l'écran, ainsi que l'adresse (l'indice) du premier pixel affiché de la balle (qui n'est pas forcément nul, vu que la balle peut n'être que partiellement visible, son coin supérieur gauche alors non dessiné).
En ce qui concerne l'écran, c'est chose facile, vu que nous savons que la première coordonées est en (a,c) :
osbuff:=c*desc.lpitch+a;
.
Pour la balle, le premier pixel affiché est en réalité (a-px+ray,c-py+ray). En effet, il s'agit tout simplement du décalage que nous avons créé entre la position du coin inférieur gauche donné : (px-ray,py-ray) et le coin inférieur gauche à partir du quel nous allons effectivement commencer à tracer : (a,b). Faites un dessin, vous comprendrez que c'est correct :)
Nous avons donc :
osball:=(a-px+ray)+(c-py+ray)*ray*2;
.
Ensuite nous devons parcourir la première ligne, et après chaque pixel de cette ligne, nous devons incrémenter la valeur de osbuff et osball pour passer au pixel suivant. Dans le cas présent il sera plus simple de faire une boucle allant de 0 à b-a, ce qui fait qu'au lieu d'incrémenter la valeur de osbuff et osball, nous utiliserons osbuff+x et osball+x. A la fin de chaque ligne, il faudra évidement penser à incrémenter osball et osbuff de la taille de leur longueurs de ligne respectives afin de les voir pointer vers le début de la ligne suivante.
.
Bref que au final, l'algo donne ceci :
for y:=0 to d-c do begin
for x:=0 to b-a do begin
lum:=tgraph(desc.lpSurface)[osbuff+x]+graph[osball+x];
if lum>255 then lum:=255;
tgraph(desc.lPsurface)[osbuff+x]:=lum;
end;
inc(osball,ray*2);
inc(osbuff,desc.lpitch);
end;
Avec tgraph déclaré comme array of byte.
.
A présent, l'utilité de déclarer graph comme un tableau monodimensionnel au lieu de bidimensionnel prend tout son sens. Il est en effet bien plus rapide de procéder comme nous faisons ici que de passer par indices de tableau bidimensionnels.
Au final, la routine donne :
procedure showball(px,py:integer;desc:tddsurfacedesc);
type tgraph=array of byte;
var osbuff,osball:integer;
a,b,c,d,lum:integer;
x,y:integer;
begin
a:=px-ray;
b:=px+ray-1;
c:=py-ray;
d:=py+ray-1;
begin a:=px-ray; b:=px+ray-1; c:=py-ray; d:=py+ray-1;
if a<0 then a:=0;
if b>=tx then b:=tx-1;
if c<0 then c:=0;
if d>=ty then d:=ty-1;
osbuff:=c*desc.lpitch+a;
osball:=(a-px+ray)+(c-py+ray)*ray*2;
for y:=0 to d-c do begin
for x:=0 to b-a do begin
lum:=tgraph(desc.lpSurface)[osbuff+x]+graph[osball+x];
if lum>255 then lum:=255;
tgraph(desc.lPsurface)[osbuff+x]:=lum;
end;
inc(osball,ray*2);
inc(osbuff,desc.lpitch);
end;
end;
.
Show, quand à lui, donne d'une manière triviale :
procedure show;
var surfacedesc:tddsurfacedesc;
i:integer;
begin
surfacedesc.dwSize:=sizeof(surfacedesc);
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok then begin
isterminated:=true;
exit;
end;
fillchar(surfacedesc.lpsurface^,ty*surfacedesc.lPitch,0);
for i:=0 to nballs-1 do showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc);
buffer.Unlock(surfacedesc.lpsurface);
lpdd.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
surface.Flip(nil,0);
end;
.
Pour le programme principal, il faudra penser à initialiser comme ceci :
randomize;
createball;
for i:=0 to nballs-1 do begin
ball[i].px:=random(tx);
ball[i].py:=random(ty);
ball[i].dx:=0;
ball[i].dy:=0;
end;
.
Tout cela mis ensemble nous donne le programme complet :
program balls;
uses
Forms, DDraw, Windows;
const
nballs=40; ray=50; tx=640; ty=480;
type
tball=record
px,py:single;
dx,dy:single;
end;
var
form:TForm;
isterminated:boolean;
lpdd:idirectdraw;
surface,buffer:IDirectDrawSurface;
ddsd:TDDSURFACEDESC;
palette:array[0..255] of paletteentry;
ddpalette:IDirectDrawPalette;
i:integer;
ball:array[0..nballs-1] of tball;
graph:array[0..4*ray*ray-1] of byte;
.
procedure createball;
var x,y,c:integer;
dist:single;
begin for y:=0 to 2*ray-1 do begin
for x:=0 to 2*ray-1 do begin
dist:=sqrt(sqr(x-ray)+sqr(y-ray));
if dist255 then c:=255;
end else c:=0;
graph[x+y*2*ray]:=c;
end;
end;
end;
.
procedure showball(px,py:integer;desc:tddsurfacedesc);
type tgraph=array of byte;
var osbuff,osball:integer;
a,b,c,d,lum:integer;
x,y:integer;
begin
a:=px-ray;
b:=px+ray-1;
c:=py-ray;
d:=py+ray-1;
if (a>=tx) or (c>=ty) or (b<0) or (d<0) then exit;
if a<0 then a:=0;
if b>=tx then b:=tx-1;
if c<0 then c:=0;
if d>=ty then d:=ty-1;
osbuff:=c*desc.lpitch+a;
osball:=(a-px+ray)+(c-py+ray)*ray*2;
for y:=0 to d-c do begin
for x:=0 to b-a do begin
lum:=tgraph(desc.lpSurface)[osbuff+x]+graph[osball+x];
if lum>255 then lum:=255;
tgraph(desc.lPsurface)[osbuff+x]:=lum;
end;
inc(osball,ray*2);
inc(osbuff,desc.lpitch);
end;
end;
.
procedure key(Sender: TObject; var Key: Char);
begin
isterminated:=true;
end;
.
procedure show;
var surfacedesc:tddsurfacedesc;
i:integer;
begin
surfacedesc.dwSize:=sizeof(surfacedesc);
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok then begin isterminated:=true;
exit;
end;
fillchar(surfacedesc.lpsurface^,ty*surfacedesc.lPitch,0);
for i:=0 to nballs-1 do showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc);
buffer.Unlock(surfacedesc.lpsurface);
lpdd.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
surface.Flip(nil,0);
end;
.
procedure move;
var i,j:integer;
dist:single;
begin
for i:=0 to nballs-1 do begin
for j:=0 to nballs-1 do begin
if i<>j then begin
dist:=sqrt(sqr(ball[i].px-ball[j].px)+sqr(ball[i].py-ball[j].py));
if dist>200 then begin
ball[i].dx:=ball[i].dx+250*(ball[j].px-ball[i].px)/(dist*dist*dist);
ball[i].dy:=ball[i].dy+250*(ball[j].py-ball[i].py)/(dist*dist*dist);
end;
end;
end;
ball[i].px:=ball[i].px+ball[i].dx;
ball[i].py:=ball[i].py+ball[i].dy;
if (ball[i].px<0) or (ball[i].px>=tx) then ball[i].dx:=ball[i].dx*0.999;
if (ball[i].py<0) or (ball[i].py>=ty) then ball[i].dy:=ball[i].dy*0.999;
end;
end;
.
begin
form:=TForm.create(application);
form.KeyPreview:=true;
form.borderstyle:=bsnone;
@form.onkeypress:=@key;
form.show;
if directdrawcreate(nil,lpdd,nil)<>dd_ok then exit;
if lpdd.SetCooperativeLevel(form.handle,DDSCL_EXCLUSIVE OR DDSCL_FULLSCREEN OR DDSCL_ALLOWMODEX OR DDSCL_ALLOWREBOOT)<>dd_ok then exit;
if lpdd.SetDisplayMode(tx,ty,8)<>dd_ok then exit;
fillchar(ddsd,sizeof(ddsd),0);
ddsd.dwSize:=sizeof(ddsd);
ddsd.dwflags:=ddsd_caps or ddsd_backbuffercount;
ddsd.ddscaps.dwcaps:=ddscaps_primarysurface or ddscaps_systemmemory or ddscaps_complex or ddscaps_flip;
ddsd.dwbackbuffercount:=1;
if lpdd.CreateSurface(ddsd,surface,nil)<>dd_ok then exit;
ddsd.ddscaps.dwCaps:=DDSCAPS_BACKBUFFER;
if surface.GetAttachedSurface(ddsd.ddscaps,buffer)<>dd_ok then exit;
for i:=0 to 255 do begin
palette[i].peRed:=i;
if i<192 then palette[i].peGreen:=0 else palette[i].peGreen:=(i-192)*4;
palette[i].peBlue:=i;
end;
if lpdd.CreatePalette(DDPCAPS_8BIT,@Palette,ddpalette,nil)<>dd_ok then exit;
surface.SetPalette(ddpalette);
form.WindowState:=wsmaximized;
showcursor(false);
randomize;
createball;
for i:=0 to nballs-1 do begin
ball[i].px:=random(tx);
ball[i].py:=random(ty);
ball[i].dx:=0;
ball[i].dy:=0;
end;
isterminated:=false;
repeat
move;
show;
application.ProcessMessages;
until isterminated;
form.hide;
lpdd.restoredisplaymode; showcursor(true);
ddpalette:=nil;
buffer:=nil;
surface:=nil;
lpdd:=nil;
end.
.
b) Quelques optimisations
Je ne vais pas parler ici des optimisations des routines en elles-mêmes car l'algo que j'ai donné pour l'affichage de la balle, bien que fortement optimisable, n'est quand même pas trop mal. Il existe quelques subtiles optimisations avec les instructions pentium pro pour éviter le saut conditionnel (un MOVC al, 255 par exemple), mais cela c'est le problème du compilateur, et même si je n'hésite souvent pas à passer par l'assembleur pour optimiser les algos, ça va être un peu compliqué dans ce tutoriel.
Je vais plutôt vous expliquer comment mieux utiliser les routines.
Première constatation : on passe son temps à effacer tout l'écran en mémoire, alors qu'une partie de celui-ci n'a pas à être effacé. Il serait diablement plus efficace (du moins si le nombre de balles n'atteint pas un gigantisme) de n'effacer l'écran QUE là où des balles se trouvent affichées. Voici une routine, directement issue de showball mais en plus simple, qui efface le rectangle donné :
procedure clear(x1,y1,x2,y2:integer;desc:tddsurfacedesc);
type tgraph=array of byte;
var osbuff:integer;
x,y:integer;
begin
if (x1>=tx) or (y1>=ty) or (x2<0) or (y2<0) then exit;
if x1<0 then x1:=0;
if x2>=tx then x2:=tx-1;
if y1<0 then y1:=0;
if y2>=ty then y2:=ty-1;
osbuff:=y1*desc.lpitch+x1;
for y:=0 to y2-y1 do begin
for x:=0 to x2-x1 do tgraph(desc.lPsurface)[osbuff+x]:=0;
inc(osbuff,desc.lpitch);
end;
end;
.
Pour l'utiliser, il faut juste se souvenir d'effacer l'écran, mais APRES que celui-ci ai été copié à l'écran. Il faudra donc redemander un pointeur vers cet écran, ce qui nous donne dans show :
procedure show;
var surfacedesc:tddsurfacedesc;
i:integer;
begin
surfacedesc.dwSize:=sizeof(surfacedesc);
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok then begin
isterminated:=true;
exit;
end;
for i:=0 to nballs-1 do showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc);
buffer.Unlock(surfacedesc.lpsurface);
lpdd.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
surface.Flip(nil,0);
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok then begin
isterminated:=true;
exit;
end;
for i:=0 to nballs-1 do clear(trunc(ball[i].px)-ray,trunc(ball[i].py)-ray,trunc(ball[i].px)+ray,trunc(ball[i].py)+ray,surfacedesc);
buffer.Unlock(surfacedesc.lpsurface);
end;
.
Ce qui est plus rapide.
Mais on peut encore faire beaucoup mieux : on se rend bien compte que quand toutes les balles se superposent, il est totalement inutile d'effacer chaque balle, car la première aussi effacé toutes les autres. Bien sur la superposition n'est pas forcément parfaite.
Mais on peu très bien imaginer de n'effacer que le rectangle qui englobe toutes les balles, ce qui règle le problème. Mais si ces balles sont assez éparpillées, on en revient à effacer tout l'écran, et il est alors préférable de n'effacer que les balles séparément. Il faut donc être potentiellement capable d'effectuer ces deux méthodes, avec un système de décision efficace.
D'abord : le calcul du rectangle est facile. On initialise ses coins à des valeurs bidons, genre x1=tx, y1=ty, x2=-1, y2=-1. Ensuite on passe en revue les balles, ce qui est déjà fait dans la boucle d'affichage et on en profitera. On regardera avec ce système :
x1:=tx;y1:=ty;x2:=-1;y2:=-1;
for i:=0 to nballs-1 do begin
showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc);
if trunc(ball[i].px)-ray < x1 then x1:=trunc(ball[i].px)-ray;
if trunc(ball[i].py)-ray < y1 then y1:=trunc(ball[i].py)-ray;
if trunc(ball[i].px)+ray>x2 then x2:=trunc(ball[i].px)+ray;
if trunc(ball[i].py)+ray>y2 then y2:=trunc(ball[i].py)+ray;
end;
A la fin, on connaîtra le rectangle qui englobe toutes les balles : (x1,y1)-(x2,y2).
.
Et pour décider quelle méthode d'affichage utiliser, il suffit de regarder la surface de ce rectangle par rapport à la somme des surfaces des balles à effacer :
if(x2-x1+1)*(y2-y1+1) < nballs*ray*ray*4 then begin
clear(x1,y1,x2,y2,surfacedesc);
end else begin
for i:=0 to nballs-1 do clear(trunc(ball[i].px)-ray,trunc(ball[i].py)-ray,trunc(ball[i].px)+ray,trunc(ball[i].py)+ray,surfacedesc);
end;
.
Ce qui est sacrément plus rapide.
Et je n'ai rien de mieux à vous proposer pour le moment d'un point de vue optimisation.
N'oubliez tout de même pas que même si on n'efface que ce qui a effectivement changé, le Flip, lui, copie l'intégralité de notre buffer vers l'écran. Une optimisation supplémentaire sera de se passer de buffer secondaire pour directement travailler à l'écran, mais comme cela est lent et qu'on ne peut pas lire et écrire dessus en même temps (ici on en a besoin pour notre métasphere), il faudra encore passer par un buffer temporaire, encore des complications, mais ici le jeu n'en vaut plus la chandelle. Si on passait en 1024x768, il est clair qu'il faudrait adopter cette approche.

Retour | PARTIE I | PARTIE II

Hit-Parade