Suivez-nous sur X

|
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
ALL
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Dossier : Histoire du développement de PixelMachine
(Article écrit par SuperJer et extrait de www.superjer.com - février 2007)
|
|
Créer un moteur de rendu 3D de lancer de rayons à partir de zéro en un week-end
J'ai toujours aimé l'idée du lancer de rayons pour restituer des images 3D avec une précision incroyable.
Samedi 17 février 2017 au soir (étant un grand féru d'informatique), j'ai décidé d'essayer d'en écrire
un de toutes pièces, juste pour le plaisir. Par "de toutes pièces", j'entends que j'ai commencé avec ce
code C++ dans un fichier texte :
#include <stdlib.h>
#include <stdio.h>
int main()
{
return 0;
}
|
Je n'ai utilisé aucune bibliothèque graphique ni aucun code extérieur autre que les éléments C/C++ standard
de Visual Studio Express 2005. J'ai décidé de générer des fichiers BMP Windows car le format est d'une
simplicité déconcertante.
Petit rappel sur le lancer de rayons
Le lancer de rayons fonctionne un peu comme un appareil photo réel, mais à l'envers. Avec un appareil
photo (ou vos yeux, d'ailleurs), les rayons lumineux de l'environnement pénètrent dans l'objectif et
frappent le film, la puce numérique et les cellules oculaires. Un phénomène magique se produit là où la
lumière frappe et où l'on obtient une image !
Avec le lancer de rayons, on part de chaque point de notre "film" ou image, on projette un rayon depuis
l'objectif de l'appareil photo et on observe ce qu'il touche. Ce qu'il touche détermine la couleur et la
luminosité à cet endroit du film. Bien sûr, par "film", j'entends image numérique, et par "point", pixel.
J'ai décidé que tout mon programme serait centré sur une fonction, appelée raytrace(). L'idée est la suivante :
on donne à raytrace() un point de départ et une direction, et elle suit ce rayon jusqu'à ce qu'il entre en
collision avec un élément de mon environnement virtuel. Elle renvoie la couleur de l'objet touché.
Lors de la génération d'une image 3D, raytrace() ne trouvera la couleur que pour un seul pixel de l'image
résultante. En exécutant le lancer de rayons une fois pour chaque pixel, nous pouvons obtenir la scène entière !
Le lancer de rayons est un peu lent, car, par exemple, pour une image d'un mégapixel, il faudrait l'exécuter
un million de fois.
Je me suis donc mis à la programmation !
...et voici un journal visuel de mes progrès...
Samedi 17 février 2007
Première image rendue vers 22h. Pour simplifier, j'ai choisi une matrice 16x16x16 de blocs colorés ou
invisibles. J'ai initialisé le monde avec un damier de blocs bleus unis sur le mur du fond. L'objectif
était de rendre la scène et de rechercher un motif en damier pour vérifier son bon fonctionnement.
Les pixels roses sont ceux dont le rendu a échoué à cause d'un bogue. Au lieu de s'éteindre lorsque la
fonction raytrace() rencontre un problème, elle renvoie simplement du rose.
J'ai déplacé la caméra pour voir si cela changeait la perspective. Et oui ! Un bon signe que le rendu
était vraiment en 3D.
J'ai ensuite placé la caméra dans les blocs. Comme vous pouvez le voir, ils sont partiellement transparents.
Je pense qu'ils étaient solides à environ 15%. Le rose à droite correspond à l'endroit où les rayons ont
quitté le monde 16x16x16 et ont subi une sorte de défaillance.
J'ai supprimé le damier et placé des blocs de couleurs aléatoires à travers l'espace. J'ai oublié d'utiliser
le mode binaire lors de l'écriture du fichier BMP, ce qui a entraîné une distorsion.
Correction du problème du mode binaire.
J'ai rendu les blocs moins transparents. Bon, maintenant il y a trop de blocs.
...j'ai donc configuré la génération aléatoire pour créer moins de blocs.
Au lieu de placer des blocs de manière totalement aléatoire, j'ai décidé de placer une couche de sol
remplie de blocs, puis d'empiler des blocs aléatoirement par-dessus. L'objectif était de créer un
environnement de type paysage urbain.
J'ai également fait en sorte que lorsque le rayon frappe les différentes faces des cubes, sa luminosité
varie légèrement.
Ici, j'ai augmenté la taille du monde de 16x16x16 à 64x64x64.
Je m'assure que la transparence fonctionne toujours. Le plus intéressant, c'est que cela n'a pas vraiment
ralenti le rendu. Seulement d'environ 5% !
J'ai ajouté une source lumineuse. Plus elle est éloignée, moins la lumière atteint chaque pixel, d'un facteur
égal à l'inverse de la distance au carré. Comme dans la vraie vie.
On y arrive ! Lorsqu'un rayon de l'écran touche un bloc solide, le code calcule un nouveau rayon depuis le
point d'impact jusqu'à la source lumineuse. Si ce rayon ne touche pas d'objet, on obtient de la lumière !
Sinon, on obtient de l'ombre. Le résultat est un éclairage et des ombres parfaits au pixel près.
J'ai ajouté des tonnes de blocs aléatoires au "ciel" pour rendre l'effet plus spectaculaire.
J'ai ajouté une deuxième source de lumière et pris une autre photo.
J'en avais assez de ce fond noir, alors j'ai essayé d'ajouter un ciel bleu. La pente à laquelle un rayon
s'échappe du monde détermine la luminosité qu'il renvoie. Je l'ai inversé, donc le ciel est à l'envers. :/
Plus important encore, j'ai rendu la couche de blocs la plus basse réfléchissante.
Lorsqu'un rayon en touche un, il rebondit et continue à chercher un autre objet à percuter. C'était assez
simple à faire.
De plus, ces pixels d'erreur "roses" sont de retour, sauf qu'ils sont maintenant noirs. Ils étaient probablement
là depuis le début, cachés dans l'obscurité... :O
Un très bon exemple d'ombrage parfait au pixel près. Si un rayon atteint un bloc coloré et saute directement
vers la source lumineuse sans autre collision, on obtient un pixel éclairé. C'est aussi simple que ça !
Le "terrain" est généré grâce à un algorithme assez basique qui consiste à choisir des hauteurs complètement
aléatoires pour quelques points d'origine, puis à les lisser entre eux.
J'ai enfin corrigé les pixels d'erreur. Lorsqu'un rayon sortait du monde dans l'espace "-x" ou "-y",
mon code n'avait aucun sens et provoquait une boucle infinie. Enfin, infinie, sauf pour mon instruction
d'urgence if(k>1000) break;.
Un aperçu du bord du monde. On peut également voir le relief se refléter dans la couche de blocs la plus basse.
J'ai encore agrandi le monde. Et puis je me suis endormi. Il était environ 5h du matin. :D
Dimanche 18 février 2007
Ajout d'une imprécision intentionnelle lorsqu'un rayon rebondit sur une surface réfléchissante. Cette
diffusion donne à la surface un aspect texturé, comme des vagues d'eau très réfléchissantes.
J'ai ajouté des ombres aux surfaces réfléchissantes. C'était très simple : il m'a suffi de calculer
l'éclairage, même lorsque le rayon rebondit. Il s'agissait simplement de réorganiser quelques lignes de code.
J'ai créé une zone carrée au centre du monde, plane et haute de 30 blocs, et je les ai tous rendus réfléchissants.
Je voulais tester la réflexion sur d'autres surfaces que des surfaces planes.
Waouh, regardez ça !
Aussi génial que cela puisse paraître, le lancer de rayons n'était pas si compliqué ! La sphère est
simplement un point central et un rayon. Chaque fois que le code envoie un rayon dans la scène, il calcule
la distance jusqu'au premier point d'intersection avec la sphère. Il suffit de résoudre les racines de
l'équation du second degré. Il y a toujours 0, 1 ou 2 points où un rayon frappera une sphère, soit en le
manquant, soit en le heurtant tangentiellement, soit en créant des blessures d'entrée et de sortie. Une
fois le point d'impact sur la sphère défini, la normale à la surface n'est plus que le vecteur rayon,
ce qui permet de rebondir facilement et d'obtenir un nouveau rayon ! Le reste est déjà géré par l'ancien
code de réflexion.
L'éclairage du point d'impact sur la sphère est également géré par l'ancien code de test de lumière.
J'ai ajouté 100 sphères supplémentaires, mais la plupart sont très haut dans le ciel
(on les voit dans les reflets).
Toutes ces sphères ralentissent à peine le rendu, de seulement 10 à 15% ! Grâce aux cadres de
délimitation, nous pouvons éviter de tester la plupart des sphères la plupart du temps. Comme la
plupart des rayons ne se réfléchissent pas, et que ceux qui le font ont peu de chances de le faire à
nouveau, j'ai autorisé jusqu'à 100 rebonds par rayon envoyé dans la scène, sans trop de souci.
J'ai limité la hauteur maximale des sphères placées au hasard pour essayer de voir comment elles
intersectent le terrain et tout ça.
J'ai légèrement tourné la caméra vers la gauche (en fait, j'ai déplacé la cible juste en dessous de
la sphère d'origine).
J'ai décidé de rendre les couleurs du terrain moins aléatoires et plus jaunes. J'ai perdu la génération
aléatoire pour cette photo, sinon j'en aurais créé une en haute résolution. :(
Regardant vers le bas d'une sphère haute dans le ciel.
Il est possible qu'un rayon rebondisse indéfiniment entre les sphères, mais c'est peu probable. Il
faut abandonner après un certain nombre de rebonds.
C'est la première photo que j'ai utilisée pour mon fond d'écran. Rien de vraiment nouveau ici, mais
j'ai raté la génération du terrain. Après cette photo, j'ai pris des pilules mystérieuses pour mon
mal de dents et je ne me souviens plus de ce qui s'est passé, mais c'était la fin de la programmation
pour la journée !
Réflexions finales
Le titre provisoire de ma nouvelle application est "PixelMachine"... parce que ça sonne un peu bizarre.
Le tout a nécessité environ 800 nouvelles lignes de code. Je n'ai recyclé que 20 lignes de CollideRaySphere(),
une fonction de mon moteur de jeu insaisissable sur lequel je travaille depuis toujours. En fait, c'était
un excellent test pour voir si CollideRaySphere() fonctionne (et C'est le cas !).
Ensuite, je testerai CollideRayCylinder(), CollideRayTriangle(), CollideTriangleTriangle() et plus encore...
Concernant les futurs effets graphiques de PixelMachine, j'ajouterai un éclairage spéculaire, un effet
bloom surbrillant, une plage dynamique élevée, une profondeur de champ, un brouillard/lumière volumétrique,
ce genre de choses...
Si tout se passe bien, j'intégrerai toutes ces fonctionnalités dans mon moteur de jeu et j'ajouterai
un bouton "prendre une capture d'écran en lancer de rayons" ou quelque chose de génial comme ça.
|