Construisons un moteur graphique 3D Rasterizing Line Segments and Circles

Bonjour, voici la quatrième partie de notre série sur les moteurs graphiques 3D. Cette fois, nous couvrirons rastérisation: le processus de prise d'une forme décrite par des formules mathématiques et convertie en une image sur votre écran.

Pointe: Tous les concepts de cet article sont construits à partir des classes que nous avons établies dans les trois premiers articles. Veillez donc à les vérifier en premier..


résumer

Voici un aperçu des cours que nous avons effectués jusqu'à présent:

 Classe de points Variables: num tuple [3]; // (x, y, z) Opérateurs: Point AddVectorToPoint (Vector); Point SubtractVectorFromPoint (Vector); Vecteur SubtractPointFromPoint (Point); NULL SetPointToPoint (Point); Fonctions: drawPoint; // dessine un point sur son n-ucle de position Classe Vector Variables: num tuple [3]; // (x, y, z) Opérateurs: vecteur AddVectorToVector (vecteur); Vecteur SubtractVectorFromVector (vecteur); Vecteur RotateXY (degrés); Vecteur RotateYZ (degrés); Vecteur RotateXZ (degrés); Échelle vectorielle (s0, s1, s2); // reçoit un tuple de mise à l'échelle, renvoie le vecteur mis à l'échelle Camera Class Vars: int minX, maxX; int minY, maxY; int minZ, maxZ; tableau objetsInWorld; // un tableau de tous les objets existants Fonctions: null drawScene (); // dessine tous les objets nécessaires à l'écran

Vous pouvez consulter le programme exemple de la troisième partie de la série pour voir comment ils fonctionnent ensemble..

Maintenant, jetons un coup d'oeil à quelques nouvelles choses!


Rastérisation

Rastérisation (ou rastérisation, si vous aimez) est le processus de prise d'une forme décrite dans un format graphique vectoriel (ou dans notre cas, mathématiquement) et de la convertir en une image raster (où la forme est adaptée à une structure en pixels).

Comme les mathématiques ne sont pas toujours aussi précises que nous le voudrions en infographie, nous devons utiliser des algorithmes pour adapter les formes décrites sur notre écran à base d'entiers. Par exemple, un point peut tomber sur la coordonnée \ ((3.2, 4.6) \) en mathématiques, mais lorsque nous le rendons, nous devons le déplacer vers \ ((3, 5) \) afin qu'il puisse s'intégrer dans la structure de pixel de notre écran.

Chaque type de forme que nous rastérisons aura son propre algorithme. Commençons par l’une des formes les plus simples à pixelliser: le segment de ligne.


Segments de ligne


Source: http://en.wikipedia.org/wiki/File:Bresenham.svg

Les segments de ligne sont l'une des formes les plus simples pouvant être dessinées, et sont donc souvent l'une des premières choses couvertes par une classe de géométrie. Ils sont représentés par deux points distincts (un point de départ et un point de fin) et la ligne qui relie les deux. L’algorithme le plus couramment utilisé pour pixelliser un segment de ligne est appelé Algorithme de Bresenham.

Pas à pas, l'algorithme de Bresenham fonctionne comme suit:

  1. Recevoir les points de début et de fin d'un segment de ligne en tant qu'entrée.
  2. Identifiez la direction d'un segment de ligne en déterminant ses propriétés \ (dx \) et \ (dy \) (\ (dx = x_ 1 - x_ 0 \), \ (dy = y_ 1 - y_ 0 \)).
  3. Déterminer sx, sy, et les propriétés de capture d'erreur (je vais montrer la définition mathématique pour ceux-ci ci-dessous).
  4. Arrondissez chaque point du segment au pixel situé au-dessus ou au-dessous.

Avant de mettre en œuvre l'algorithme de Bresenham, mettons en place une classe de segment de ligne de base à utiliser dans notre moteur:

 LineSegment Class Variables: int startX, startY; // le point de départ de notre segment de droite int endX, endY; // le point d'arrivée de notre segment de droite Fonction: array returnPointsInSegment; // tous les points situés sur ce segment de droite

Si vous voulez effectuer une transformation sur notre nouveau Segment de ligne tout ce que vous avez à faire est d’appliquer la transformation que vous avez choisie aux points de début et de fin de la Segment de ligne et ensuite les replacer dans la classe. Tous les points qui se situent entre seront traités lorsque le Segment de ligne lui-même est dessiné, car l'algorithme de Bresenham ne demande que les points de début et de fin pour trouver chaque point suivant.

Pour la Segment de ligne classe pour aller avec notre moteur actuel, nous ne pouvons pas réellement avoir un dessiner() fonction intégrée dans la classe, c’est pourquoi j’ai opté pour l’utilisation d’un returnPointsInSegment fonction à la place. Cette fonction renverra un tableau de chaque point existant dans le segment de ligne, ce qui nous permettra de dessiner et de sélectionner facilement le segment de ligne, le cas échéant..

Notre fonction returnPointsInSegment () ressemble un peu à ça (en JavaScript):

 function returnPointsInSegment () // crée une liste pour stocker tous les points du segment de ligne dans var pointArray = new Array (); // définit les variables de cette fonction en fonction des points de départ et d'arrivée de la classe var x0 = this.startX; var y0 = this.startY; var x1 = this.endX; var y1 = this.endY; // définit les différences de vecteurs et les autres variables requises pour l'algorithme de Bresenham var dx = Math.abs (x1-x0); var dy = Math.abs (y1-y0); var sx = (x0 & x1)? 1: -1; // étape x var sy = (y0 & y1)? 1: -1; // étape y var err = dx-dy; // récupère la valeur d'erreur initiale // définit le premier point du tableau pointArray.push (new Point (x0, y0)); // Boucle de traitement principale while (! ((X0 == x1) && (y0 == y1))) var e2 = err * 2; // conserve la valeur d'erreur // utilise la valeur d'erreur pour déterminer si le point doit être arrondi à la valeur supérieure ou inférieure si (e2 => -dy) err - = dy; x0 + = sx;  if (e2 < dx)  err += dx; y0 += sy;  //add the new point to the array pointArray.push(new Point(x0, y0));  return pointArray; 

Le moyen le plus simple d’ajouter le rendu de nos segments dans notre classe de caméras est d’ajouter un simple si structure, semblable à la suivante:

 // parcourt un tableau d'objets if (type de classe == Point) // faisons notre code de rendu actuel else if (type de classe == LineSegment) var segmentArray = LineSegment.returnPointsInSegment (); // parcourt les points du tableau, les dessine et les sélectionne comme nous l'avons fait précédemment

C'est tout ce dont vous avez besoin pour que votre première classe de forme soit opérationnelle! Si vous voulez en savoir plus sur les aspects plus techniques de l'algorithme de Bresenham (en particulier les sections d'erreur), vous pouvez consulter l'article Wikipedia à ce sujet..


Cercles


Source: http://en.wikipedia.org/wiki/File:Bresenham_circle.svg

Rasteriser un cercle est un peu plus difficile que de rasteriser un segment de ligne, mais pas beaucoup. Nous allons utiliser le algorithme de cercle de point milieu pour faire tout notre lourd travail, qui est une extension de l'algorithme de Bresenham mentionné précédemment. En tant que tel, il suit des étapes similaires à celles énumérées ci-dessus, avec quelques différences mineures.

Notre nouvel algorithme fonctionne comme ceci:

  1. Recevoir le centre et le rayon d'un cercle.
  2. Définir de force les points dans chaque direction cardinale
  3. Parcourez chacun de nos quadrants en dessinant leurs arcs

Notre classe de cercle sera très similaire à notre classe de segment de ligne, ressemblant à ceci:

 Classe de cercle Variables: int centerX, centerY; // le point central de notre cercle int radius; // le rayon de notre cercle Fonction: array returnPointsInCircle; // tous les points dans ce cercle

Notre returnPointsInCircle () fonction va se comporter de la même manière que notre Segment de ligne la fonction de classe fait, renvoyant un tableau de points afin que notre caméra puisse les restituer et les sélectionner au besoin. Cela permet à notre moteur de gérer une variété de formes, avec seulement des modifications mineures nécessaires pour chaque.

Voici ce que notre returnPointsInCircle () la fonction va ressembler à (en JavaScript):

 function returnPointsInCircle () // stocke tous les points du cercle dans un tableau var pointArray = new Array (); // définit les valeurs nécessaires à l'algorithme var f = 1 - radius; // utilisé pour suivre la progression du cercle dessiné (depuis son semi-récursif) var ddFx = 1; // étape x var ddFy = -2 * this.radius; // étape y var x = 0; var y = this.radius; // cet algorithme ne prend pas en compte les points les plus éloignés, // nous devons donc les définir manuellement pointArray.push (new Point (this.centerX, this.centerY + this.radius)); pointArray.push (nouveau Point (this.centerX, this.centerY - this.radius)); pointArray.push (nouveau Point (this.centerX + this.radius, this.centerY)); pointArray.push (nouveau Point (this.centerX - this.radius, this.centerY)); tandis que (x < y)  if(f >= 0) y--; ddFy + = 2; f + = ddFy;  x ++; ddFx + = 2; f + = ddFx; // construit notre arc actuel pointArray.push (new Point (x0 + x, y0 + y)); pointArray.push (nouveau Point (x0 - x, y0 + y)); pointArray.push (nouveau Point (x0 + x, y0 - y)); pointArray.push (nouveau Point (x0 - x, y0 - y)); pointArray.push (nouveau Point (x0 + y, y0 + x)); pointArray.push (nouveau Point (x0 - y, y0 + x)); pointArray.push (nouveau Point (x0 + y, y0 - x)); pointArray.push (nouveau Point (x0 - y, y0 - x));  return pointArray; 

Maintenant, nous ajoutons simplement un autre si déclaration à notre boucle de dessin principale, et ces cercles sont entièrement intégrés!

Voici à quoi peut ressembler la boucle de dessin mise à jour:

 // parcourt un tableau d'objets if (type de classe == point) // faisons notre code de rendu actuel else if (type de classe == LineSegment) var segmentArray = LineSegment.returnPointsInSegment (); // parcourt les points du tableau, les dessine et les sélectionne comme précédemment else if (type de classe == Cercle) var circleArray = Circle.returnPointsInCircle (); // parcourt les points du tableau, les dessine et les sélectionne comme nous l'avons fait précédemment

Maintenant que nous avons nos nouvelles classes à l'écart, faisons quelque chose!


Maître de trame

Notre programme sera simple cette fois. Lorsque l'utilisateur clique sur l'écran, nous allons dessiner un cercle dont le centre est le point sur lequel vous avez cliqué et dont le rayon est un nombre aléatoire..

Jetons un coup d'oeil au code:

 main // configuration pour votre API graphique préférée ici // configuration pour la saisie au clavier (peut ne pas être nécessaire) ici var camera = new Camera (); // crée une instance de la classe camera camera.objectsInWorld []; // crée 100 espaces-objets dans le tableau de la caméra // définit l'espace de visualisation de la caméra camera.minX = 0; camera.maxX = screenWidth; camera.minY = 0; camera.maxY = screenHeight; camera.minZ = 0; camera.maxZ = 100; while (key! = echap) if (mouseClick) // créer un nouveau cercle camera.objectsInWorld.push (nouveau Circle (mouse.x, mouse.y, random (3,10)); // restituer le tout dans le scène camera.drawScene ();

Avec un peu de chance, vous devriez maintenant pouvoir utiliser votre moteur mis à jour pour dessiner des cercles impressionnants..


Conclusion

Maintenant que notre moteur intègre des fonctions de base de rastérisation, nous pouvons enfin commencer à afficher des éléments utiles sur notre écran! Rien de trop compliqué pour le moment, mais si vous le vouliez, vous pourriez assembler des figurines, ou quelque chose du genre..

!