Les problèmes posé par opengl.

Les technologies dans le jeux vidéo évoluent, et donc, les drivers et les cartes graphique aussi!

Le problème principal d’opengl est que celui-ci souffre beaucoup de son ancienneté, et les appele à draw (pour dessiner) sont bloquant!

Le CPU est donc obligé d’attendre après le GPU. (A moins que vous effectuer le rendu dans un autre thread, chose impossible sous linux avec la librairie X11 qui déteste cela.)

De plus, opengl n’est pas order independant transparency, c’est à dire, que, vous ne pouvez pas dessiner des images semi-transparente dans n’importe quel ordre!

Sinon, le blending ne se fera pas bien.

L’avenir du jeux vidéo.

On voit de plus en plus des technologies modernes voir le jour, comme par exemple openCL et Vulkan.

Ceux-ci permettront de faire du rendu graphique de manière beaucoup plus performante :

-Fini l’attente du CPU après le GPU, les deux pourront effectuer des traitements en parallèle.

-On est plus dépendant du driver pour les calculs au niveau du pipeline, dorénavant, tout est programmable. Possibilité donc de faire de l’order independant transparency et je vais vous le prouver!

Plus obligé donc de devoir fouiller dans pleins de fichiers en c pour optimiser un driver comme c’est le cas avec mesa et x11 qui de plus ne permettent pas de faire du traitement en parallèle à l’aide de plusieurs threads. (fils d’exécutions)

Ce qui facilitera grandement la tâche du programmeur en ayant une api suffisemment de haut niveau pour masquer tout ce qui est allocation de mémoire, exécution de shaders, …, au niveau du GPU, mais suffisemment bas niveau pour pouvoir gérer tout les calculs soit même en les partageant entre les différents threads du GPU.

Effectuer les calculs soit même au niveau du GPU.

C’est très simple, pour cela il va falloir coder plusieurs fonctions appelé “kernels” avec openCL.

Il y a 2 grandes étapes qui permettent de dessiner des objets en 3D du monde réel en pixels sur l’écran.

ETAPE1 : Passer des coordonnées 3D en coordonnées 2D. (Les matrices de projections de ODFAEG)

La première consiste à transformer les coordonnées 3D des sommets en coordonnées 2D à l’écran, pour cela, j’utilise une matrice de projection, il y a deux type de projection, la projection perspective qui donne l’impression que plus un objet est loin, plus il est petit, et la projection othographique plutôt utilisé dans le domaine mathématique ou là, tout les objets on la même grandeur peut importe leur éloignement.

Ce site (en Anglais) explique vraiment très bien comment projeter des sommets en 3D sur un écran en 2D à l’aide d’un tableau de nombres. (une matrice)

http://www.songho.ca/opengl/gl_projectionmatrix.html

Voyager dans le monde. (Les matrices de vue de ODFAEG)

Mais cette matrice à elle seules ne suffit pas, vous aimeriez bien bouger dans votre monde, comme si vous filmiez la scène à l’aide d’une caméra, contrairement à la réalité, ce n’est pas la caméra qui se déplace dans un jeux vidéo mais tout les objets de la scène!

Ceci à l’aide d’une matrice appelée la “lookat matrix”, celle-ci est construit à l’aide de trois axes (forward, up et left) qui pointent vers l’avant, vers la gauche et vers le haut de la caméra.

Voici un lien (en Anglais) qui explique comment construire cette matrice.

http://stackoverflow.com/questions/349050/calculating-a-lookat-matrix

Pour ma part, j’y ai ajouté une matrice de transformation afin de pouvoir faire une rotation de la caméra autout d’elle même, la matrice générale de rotation d’un point autour d’un axe est affichée dans ce tutoriel-ci :

http://openclassrooms.com/courses/creez-des-programmes-en-3d-avec-opengl/les-matrices-1

ODFAEG combine ces deux matrices afin de faire des rotations dans un espace en 2D (faire tourner la caméra sur elle-même, sur l’axe forward) et dans un espace en 3D. (faire tourner la caméra à l’aide de la “lookat matrix” et les coordonnées polaire)

Ce tutoriel explique aussi comment faire des rotations de la caméra à l’aide de coordonnées polaires.

Transformer des objets. (Les matrices de transformation de ODFAEG)

La matrice de transformation, contrairement aux matrices de vue et de projection, s’appliquent non pas à tout les objets du monde, mais à un seul objet, plusieurs objets enfant peuvent hériter de la transofmration de leur objet parent, pour cela, il suffit de combiner les matrices de transformation.

ODFAEG permet de faire des transformations quelconque d’objets autour d’un axe et d’une origine!

Ces trois matrices (projection, vue et transformation) sont combinée entre elle afin de donner la position finale des sommets à l’écran.

Le viewport.

Le viewport est la zone à l’écran dans laquelle on souhaite afficher la scène.

Donc tout ce qui est en dehors du viewport ne sera pas afficher.

Mais la matrice de projection donne des coordonnées entre -1 et 1, hors, il faut convertir ses coordonnées entre la position du viewport à l’écran et sa taille, pour cela ODFAEG utilise une matrice de viewport, la formule est simple : pixel = taille * 0.5 * coordonne + taille * 0.5 + position.

Pour la coordonnées z, elle est toujours comprise entre 0 et 1 par défaut avec opengl, se sera aussi le cas avec ODFAEG.

Passer d’un système de coordonnées en un autre.

Alors, les différentes coordonnées portent un nom :

-Les coordonnées des sommets d”un l’objet porte le nom de coordonnées de l’objet.

-Les coordonnées des sommets d’un objet transformées par la matrice de transformation de l’objet portent le nom de coordonnées monde.

-Les coordonnées monde transformées par la matrice de vue portent le nom de coordonnées vue.

-Les coordonnées vues transformées par la matrice de projection portent le nom de coordonnées clipées.

-Les coordonnées “clippées” sont ensuite divisée par leurs valeurs w, car, en projection perspective, les objets sont plus petit lorsqu’il sont plus élloigné, on parle de la division perspective, après cette division les coordonnées portent le nom de coordonnées normalisées. (Normalized device coordinates)

-Les coordonnées du viewport ne sont rien d’autre que les coordonnées normalisée transformée par la matrice de viewport.

Pour passer des coordonnées objets en coordonnées écran, il suffit alors de faire ceci :

coordonneesMonde = coordonneesObjet * matriceTransformationObjet.

coordonneesVue = coordonneesMonde * matriceDeVue.

coordonneesClippees = coordonneesVue * matriceDeProjection.

coordonneesNormalisees = coordonneesClippees / coordonnees.w

coordonneesViewport = coordonneesNormalisees * matriceDeViewport.

Pour passer des coordonnées écran en coordonnées monde il suffit de faire l’inverse :

coordonneesNormalisees = coordonneesViewport * inverseMatrixViewport.

coordonneesClippees = coordonneesNormalisees * inverseMatriceProjection

coordonneesVue = coordonneesClippees / coordonneesClippees.w

coordonneesMonde = coordonneesVue * inverseMatriceVue.

coordonneesObjet = coordonneesMonde * inverseMatriceTransformationObjet.

Tout est plus simple donc avec des matrices! 😛

ETAPE II : Le rasteriseur!

Ici plus haut je vous ai expliqué comment ODFAEG (et par conséquent opengl) procèdent pour passer des coordonnées des objets en 3D en coordonnées viewport.

Mais cela ne suffit pas, supposons que l’on aie trois sommets d’un triangle en coordonnées viewport (car tout est toujours décomposé en triangles par le driver), maintenant il faut remplir le triangle!

Pour cela, on utilise ce que l’on appelle un rasteriseur, celui-ci va dessiner tout les pixels qui se trouves, à l’intérieur de notre triangles.

La première chose à faire est de rechercher les minimums et les maximums des sommets du triangles car c’est dans cette zone que seront affiché les pixels du triangle.

Ensuite il suffit de faire une boucle, et de tester si les pixels est dans le triangle, pour des raisons de performances et d’anti-crênelage, on va traiter plusieurs pixels d’un coup, ce groupe de pixel s’appelle un fragment.

Ce lien (en Anglais) explique très bien comment vérifier si un ensemble de fragments est dans le triangle ou pas : (cette étape porte le nom de triangle setup)

https://fgiesen.wordpress.com/2013/02/10/optimizing-the-basic-rasterizer/

l suffit alors de faire deux boucles supplémentaire pour dessiner chaque pixel de nos fragments.

Maintenant il reste un dernier problème, comment connaître la profondeur de chaque fragment du triangle ?

Pour cela on utilise un technique qui porte de le nom d’interpolation hardware, on va convertir les coordonnées x et y en coordonnées barycentriques, ce lien :

http://en.wikipedia.org/wiki/Barycentric_coordinate_system

Pour trouver la valeur en z du fragment, il suffit alors de faire. (u, v et w étant les coordonnées barycentrique du fragment de notre triangle)

z = p1.z * u + p2.z * v + p3.z * w.

Il suffit alors d’appliquer cette formule pour tout les attributs des sommets des triangles. (Coordonnées de textures, couleurs, etc…)

Le blending.

Le blending est la dernière étape et elle consiste à déterminer la couleur du pixel final à l’écran, en fonction de la couleur du pixel qui est affiché à l’écran.

Le depth test.

Ce test consiste à déterminé si le pixel à dessiner est devant ou derrière le pixel dessiné.

ODFAEG stocke la valeur de z et de l’alpha du pixel le plus proche de l’observateur dans un buffer (appelé le depthbuffer), ses informations permettront de savoir comment effectuer le blending.

J’aurai pu aussi stocker ça dans deux buffers différent. (Un depthbuffer et un alphabuffer)

L’alpha test.

Ce test évite d’écrire des pixel qui sont invisible dans les buffers, en effet, ça ne sert à rien d’effectuer des traitements sur des pixels invisibles. (C’est à dire qui ont une valeur de alpha égale à  0)

Donc si le pixel à dessiné à une valeur de alpha égale à 0, alors on ne remet pas à jour les buffers.

L’accumulateur.

Le dernier point est de pouvoir calculer la couleur final du pixel en fonction du pixel à dessiner et du pixel déja dessiner et de leurs position par rapport à l’observateur.

La formule est très simple, ici, par exemple, le pixel dessiné est rouge, et le pixel à dessiné est vert, voici quelle sera sa couleur suivant leur positions en z :

rouge (1, 0, 0, 0.5)
z = 0.
vert  (0, 1, 0, 0.5)
z = 1.
nouvellecouleur = (0, 1, 0, 1) * 0.5 + (1, 0, 0, 0.5) * (1 – 0.5) =
SRCCOLOR * SRCALPHA + DSTCOLOR * (1 – SRCALPHA)
(0, 0.5, 0, 0.25) + (0, 0.5, 0, 0.5) = (0.5, 0.5, 0, 0.75)
rouge (1, 0, 0, 0.5)
z = 1.
vert  (0, 1, 0, 0.5)
z = 0.
nouvellecouleur = (0, 1, 0, 1) * 0.5 + (1, 0, 0, 0.5) * (1 – 0.5)
DSTCOLOR * DSTALPHA + SRCCOLOR * (1 – DSTALPHA)
(0, 0.5, 0, 0.5) + (0.5, 0, 0, 0.25) = (0.5, 0.5, 0, 0.75)

SRCCOLOR est la couleur du pixel à dessiner, SRCALPHA est la valeur de alpha du pixel à dessiner, DSTCOLOR est la couleur du pixel dessiné, et DSTALPHA est la valeur alpha présente dans le tampon de profondeur. (Le depthbuffer)

Voici ce à quoi ressemble l’algorithme une fois terminé :

math::Vec3f p1 = math::Vec3f((*m_instances[i]->getVertexArrays()[j])[0].position.x,(*m_instances[i]->getVertexArrays()[j])[0].position.y,(*m_instances[i]->getVertexArrays()[j])[0].position.z);
math::Vec3f p2 = math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+1].position.x,(*m_instances[i]->getVertexArrays()[j])[k+1].position.y,(*m_instances[i]->getVertexArrays()[j])[k+1].position.z);
math::Vec3f p3 = math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+2].position.x,(*m_instances[i]->getVertexArrays()[j])[k+2].position.y,(*m_instances[i]->getVertexArrays()[j])[k+2].position.z);
p1 = tm.transform(p1);
p2 = tm.transform(p2);
p3 = tm.transform(p3);
p1 = window.mapCoordsToPixel(p1, view);
p2 = window.mapCoordsToPixel(p2, view);
p3 = window.mapCoordsToPixel(p3, view);
sf::Color c1 = (*m_instances[i]->getVertexArrays()[j])[0].color;
sf::Color c2 = (*m_instances[i]->getVertexArrays()[j])[k+1].color;
sf::Color c3 = (*m_instances[i]->getVertexArrays()[j])[k+2].color;
math::Vec3f ct1 = texM * math::Vec3f((*m_instances[i]->getVertexArrays()[j])[0].texCoords.x,(*m_instances[i]->getVertexArrays()[j])[0].texCoords.y, 1.f);
math::Vec3f ct2 = texM * math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+1].texCoords.x,(*m_instances[i]->getVertexArrays()[j])[k+1].texCoords.y, 1.f);
math::Vec3f ct3 = texM * math::Vec3f((*m_instances[i]->getVertexArrays()[j])[k+2].texCoords.x,(*m_instances[i]->getVertexArrays()[j])[k+2].texCoords.y, 1.f);
std::array<math::Vec3f, 3> vertices = {math::Vec3f(p1.x, p1.y, 0),math::Vec3f(p2.x, p2.y, 0), math::Vec3f(p3.x, p3.y, 0)};
std::array<std::array<float, 2>, 3> extends = math::Computer::getExtends(vertices);
int minX = (extends[0][0] < 0) ? 0 : extends[0][0];
int minY = (extends[1][0] < 0) ? 0 : extends[1][0]; int maxX = (extends[0][1] >= size.x) ? size.x-1 : extends[0][1];
int maxY = (extends[1][0] >= size.y) ? size.y-1 : extends[1][1];
math::Vec2f p(minX, minY);
Edge e01, e12, e20;
math::Vec3f w0_row = e12.init(p2, p3, p);
math::Vec3f w1_row = e20.init(p3, p1, p);
math::Vec3f w2_row = e01.init(p1, p2, p);
for (p.y = minY; p.y < maxY; p.y += Edge::stepYSize) {
math::Vec3f w0 = w0_row;
math::Vec3f w1 = w1_row;
math::Vec3f w2 = w2_row;
for (p.x = minX; p.x <= maxX; p.x += Edge::stepXSize) {
math::Vec3f mask ((int) w0.x | (int) w1.x | (int) w2.x,
(int) w0.y | (int) w1.y | (int) w2.y,
(int) w0.z | (int) w1.z | (int) w2.z,
(int) w0.w | (int) w1.w | (int) w2.w);
if (mask.x < 0 || mask.y < 0 || mask.z < 0 || mask.w < 0) {
for (unsigned int y = p.y; y < p.y + Edge::stepYSize; y++) {
for (unsigned int x = p.x; x < p.x + Edge::stepXSize; x++) { math::Matrix2f m1(p1.x – p3.x, p2.x – p3.x, p1.y – p3.y, p2.y – p3.y); float u = ((p2.y – p3.y) * (x – p3.x) + (p3.x – p2.x) * (y – p3.y)) / m1.getDet(); float v = ((p3.y – p1.y) * (x – p3.x) + (p1.x – p3.x) * (y – p3.y)) / m1.getDet(); float w = 1 – u – v; math::Vec3f z (p1.z, p2.z, p3.z); float bz = z.x * u + z.y * v + z.z * w; float actualZ = depthBuffer[y * size.x + x].z; if (bz >= view.getViewport().getPosition().z && bz <= view.getViewport().getSize().z) { math::Vec3f tcx = math::Vec3f(ct1.x, ct2.x, ct3.x); math::Vec3f tcy = math::Vec3f(ct1.y, ct2.y, ct3.y); math::Vec3f tc = invTexM * math::Vec3f(tcx.x * u + tcx.y * v + tcx.z * w, tcy.x * u + tcy.y * v + tcy.z * w, 1.f); math::Vec3f texColor(1.f, 1.f, 1.f, 1.f); if (m_instances[i]->getMaterial().getTexture() != nullptr) {
const sf::Image& texImg = m_instances[i]->getMaterial().getTexture()->getImage();
tc.x = math::Math::clamp(tc.x, 0, texImg.getSize().x);
tc.y = math::Math::clamp(tc.y, 0, texImg.getSize().y);
sf::Color color = texImg.getPixel(tc.x, tc.y);
texColor = math::Vec3f (color.r / 255.f, color.g / 255.f, color.b / 255.f, color.a / 255.f);
}
math::Vec3f r = math::Vec3f (c1.r / 255.f, c2.r / 255.f, c3.r / 255.f);
math::Vec3f g = math::Vec3f (c1.g / 255.f, c2.g / 255.f, c3.g / 255.f);
math::Vec3f b = math::Vec3f (c1.b / 255.f, c2.b / 255.f, c3.b / 255.f);
math::Vec3f a = math::Vec3f (c1.a / 255.f, c2.a / 255.f, c3.a / 255.f);
std::array<math::Vec3f, 2> colors;
colors[0] = math::Vec3f(frameBuffer[(y * size.x + x)*4] / 255.f,
frameBuffer[(y * size.x + x)*4+1] / 255.f,
frameBuffer[(y * size.x + x)*4+2] / 255.f,
depthBuffer[y * size.x + x].w);
colors[1] = math::Vec3f(r.x * u + r.y * v + r.z * w,
g.x * u + g.y * v + g.z * w,
b.x * u + b.y * v + b.z * w,
a.x * u + a.y * v + a.z * w) * texColor;
bool src=(bz >= actualZ);
float z[2];
z[0] = actualZ;
z[1] = bz;
if (colors[1].w != 0) {
math::Vec3f finalColor = colors[src] * colors[src].w + colors[!src] * (1 – colors[src].w);
depthBuffer[y * size.x + x] = math::Vec3f(0, 0, z[src], colors[src].w);
frameBuffer[(y * size.x + x) * 4] = finalColor.x * 255;
frameBuffer[(y * size.x + x) * 4 + 1] = finalColor.y * 255;
frameBuffer[(y * size.x + x) * 4 + 2] = finalColor.z * 255;
frameBuffer[(y * size.x + x) * 4 + 3] = finalColor.w * 255;
}
}
}
}
}
w0 += e12.oneStepX;
w1 += e20.oneStepX;
w2 += e01.oneStepX;
}
w0_row += e12.oneStepY;
w1_row += e20.oneStepY;
w2_row += e01.oneStepY;
}
}
}

Cet algorithme ne gère pas les inversions des axes en x ou en y, mais il suffit pour cela d’inverser l’image du framebuffer et le problème est résolu!

Voilà sur se billet je vous ai présenté comment coder un rasterizer faire maison, libre à vous maintenant de l’utiliser au mieux à travers openCL et à l’avenir Vulkan!

Le but est de faire un rendu de manière la plus performante et la plus “order indepedant” qui soit!