Faites-le vous-même en 3D. Partie 1: pixels et lignes



Je veux consacrer cette série d'articles aux lecteurs qui souhaitent explorer le monde de la programmation 3D à partir de zéro, aux personnes qui veulent apprendre les bases de la création du composant 3D des jeux et des applications. Nous allons implémenter chaque opération à partir de zéro afin de comprendre tous les aspects, même s'il existe déjà une fonction prête à l'emploi qui la rend plus rapide. Après avoir appris, nous passerons aux outils intégrés pour travailler avec la 3D. Après avoir lu la série d'articles, vous comprendrez comment créer des scènes tridimensionnelles complexes avec de la lumière, des ombres, des textures et des effets, comment faire tout cela sans connaissances approfondies en mathématiques, et bien plus encore. Vous pouvez faire tout cela à la fois indépendamment et à l'aide d'outils prêts à l'emploi.

Dans la première partie, nous considérerons:

  • Concepts de rendu (logiciel, matériel)
  • Qu'est-ce qu'un pixel / surface?
  • Analyse détaillée de la sortie ligne

Afin de ne pas perdre votre temps précieux à lire des articles, qui peuvent être incompréhensibles pour une personne non préparée, je me tournerai immédiatement vers les exigences. Vous pouvez commencer à lire des articles sur la 3D en toute sécurité, si vous connaissez les bases de la programmation dans n'importe quel langage, car Je me concentrerai uniquement sur l'étude de la programmation 3D, et non sur l'étude des caractéristiques du langage et des fondamentaux de la programmation. En ce qui concerne la préparation mathématique, ne vous inquiétez pas ici, bien que beaucoup n'aient pas envie d'étudier la 3D, car ils sont effrayés par des calculs complexes et des formules furieuses à cause desquelles les cauchemars rêvent plus tard, mais en fait il n'y a rien à craindre. Je vais essayer d'expliquer le plus clairement possible tout ce qui est nécessaire pour la 3D, il suffit de pouvoir multiplier, diviser, additionner et soustraire. Donc, si vous avez réussi les critères de sélection, vous pouvez commencer à lire.

Avant de commencer à explorer le monde intéressant de la 3D, choisissons un langage de programmation pour les exemples, ainsi qu'un environnement de développement. Quelle langue dois-je choisir pour programmer des graphiques 3D? N'importe qui, vous pouvez travailler là où vous êtes le plus à l'aise, les mathématiques seront les mêmes partout. Dans cet article, tous les exemples seront présentés dans le contexte de JS (ici les tomates volent en moi). Pourquoi js? C'est simple - ces derniers temps, j'ai travaillé principalement avec lui, et donc je peux vous transmettre plus efficacement l'essence. Je contournerai toutes les fonctionnalités de JS dans les exemples, car nous n'avons besoin que des fonctionnalités les plus élémentaires de toutes les langues, nous ferons donc particulièrement attention à la 3D. Mais vous choisissez ce que vous aimez, car dans les articles, toutes les formules ne seront liées aux fonctionnalités d'aucun langage de programmation. Quel environnement choisir? Ce n'est pas important,dans le cas de JS, n'importe quel éditeur de texte convient, vous pouvez utiliser celui qui est le plus proche de vous.

Tous les exemples utiliseront une toile pour la peinture, comme avec lui, vous pouvez commencer à dessiner très rapidement, sans analyse détaillée. Canvas est un outil puissant, avec de nombreuses méthodes de dessin prêtes à l'emploi, mais de toutes ses fonctionnalités, pour la première fois, nous n'utiliserons que la sortie pixel! 

Tous les affichages en trois dimensions sur l'écran utilisant des pixels, plus loin dans les articles, vous verrez comment cela se produit. Va-t-il ralentir? Sans accélération matérielle (par exemple, accélération par une carte vidéo) - sera. Dans le premier article, nous n'utiliserons pas d'accélérations, nous écrirons tout à partir de zéro afin de comprendre les aspects fondamentaux de la 3D. Regardons quelques termes qui seront mentionnés dans les prochains articles:

  • (Rendering) — 3D- . , 3D- , , .
  • (Software Rendering) — . , , , - . , . 3D- , — .
  • Rendu matériel - Un processus de rendu assisté par matériel. Je l'utilise jeux et applications. Tout fonctionne très rapidement, car beaucoup de calcul de routine prend le relais de la carte vidéo, qui est conçue pour cela.

Je n'aspire pas au titre "définition de l'année" et j'essaie de formuler le plus clairement possible toutes les descriptions des termes. L'essentiel est de comprendre l'idée, qui peut ensuite être développée indépendamment. Je tiens également à attirer l'attention sur le fait que tous les exemples de code qui seront présentés dans les articles ne sont souvent pas optimisés pour la vitesse, afin de maintenir la facilité de compréhension. Lorsque vous comprenez l'essentiel - le fonctionnement des graphiques 3D, vous pouvez tout optimiser vous-même.

Tout d'abord, créez un projet, pour moi, c'est juste un fichier texte index.html , avec le contenu suivant:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>3D it’s easy. Part 1</title>
</head>

<body>
    <!--         -->
    <canvas id="surface" width="800" height="600"></canvas>

    <script>
        //    
    </script>
</body>

</html>

Je ne vais pas trop me concentrer sur JS et canvas maintenant - ce ne sont pas les personnages principaux de cet article. Mais pour une compréhension générale, je préciserai que <canvas ...> est un rectangle (dans mon cas, de 800 x 600 pixels) sur lequel je vais afficher tous les graphiques. J'ai enregistré la toile une fois et je ne la changerai plus.

<script></script> 

Script - un élément dans lequel nous écrirons toute la logique de rendu graphique 3D de nos propres mains (en JavaScript). 

Lorsque nous venons de revoir la structure du fichier index.html du projet nouvellement créé, nous allons commencer à traiter des graphiques 3D.

Lorsque nous dessinons quelque chose dans la fenêtre, cela dans le décompte final se transforme en pixels, car ce sont eux que le moniteur affiche. Plus il y a de pixels, plus l'image est nette, mais l'ordinateur charge également plus. Comment ce que nous dessinons dans la fenêtre est-il stocké? Les graphiques dans n'importe quelle fenêtre peuvent être représentés comme un tableau de pixels, et le pixel lui-même n'est qu'une couleur. Autrement dit, une résolution d'écran de 800x600 signifie que notre fenêtre contient 600 lignes de 800 pixels chacune, à savoir 800 * 600 = 480000 pixels, beaucoup, n'est-ce pas? Les pixels sont stockés dans un tableau. Pensons dans quel tableau nous stockons les pixels. Si nous devons avoir 800 par 600 pixels, alors l'option la plus évidente est dans un tableau bidimensionnel de 800 par 600. Et c'est presque la bonne option, ou plutôt, l'option complètement correcte. Mais les pixels de la fenêtre, il est préférable de stocker dans un tableau unidimensionnel de 480 000 éléments (si la résolution est de 800 par 600),juste parce qu'il est plus rapide de travailler avec un tableau unidimensionnel, car il est stocké en mémoire dans une séquence continue d'octets (tout se trouve à proximité et donc il est facile de l'obtenir). Dans un tableau à deux dimensions (par exemple, dans le cas de JS), chaque ligne peut être dispersée à différents endroits de la mémoire, donc l'accès aux éléments d'un tel tableau prendra plus de temps. De plus, pour parcourir un tableau unidimensionnel, un seul cycle est nécessaire, et pour les entiers bidimensionnels 2, étant donné la nécessité de faire des dizaines de milliers d'itérations du cycle, la vitesse est ici importante. Qu'est-ce qu'un pixel dans un tel tableau? Comme mentionné ci-dessus - ce n'est qu'une couleur, ou plutôt 3 de ses composants (rouge, vert, bleu). N'importe quelle image, même la plus colorée, n'est qu'un tableau de pixels de différentes couleurs. Un pixel en mémoire peut être stocké comme vous le souhaitez, soit un tableau de 3 éléments, soit dans une structure où le rouge, le vert,bleu; ou autre chose. Une image composée d'un tableau de pixels que nous venons d'analyser, je continuerai d'appeler la surface. Il s'avère que puisque tout ce qui est affiché à l'écran est stocké dans un tableau de pixels, puis en changeant les éléments (pixels) dans ce tableau - nous allons changer pixel par pixel l'image à l'écran. C'est exactement ce que nous allons faire dans cet article.

Il n'y a pas de fonction de dessin de pixels dans le canevas, mais il est possible d'accéder à un tableau unidimensionnel de pixels, dont nous avons discuté ci-dessus. La procédure à suivre est indiquée dans l'exemple ci-dessous (cet exemple et tous les autres à l'avenir ne seront que dans l'élément de script):

//     ()    
const ctx = document
.getElementById('surface')
.getContext('2d')

//     ,    
// +       
const imageData = ctx.createImageData(800, 600)

Dans l'exemple, imageData est un objet dans lequel il y a 3 propriétés:

  • hauteur et largeur - entiers stockant la hauteur et la largeur de la fenêtre pour le dessin
  • données - tableau d'entiers non signés 8 bits (vous pouvez y stocker des nombres compris entre 0 et 255)

Le tableau de données a une structure simple mais explicative. Ce tableau unidimensionnel stocke les données de chaque pixel, que nous afficherons à l'écran au format suivant:
Les 4 premiers éléments du tableau (indices 0,1,2,3) sont les données du premier pixel de la première ligne. Les 4 seconds éléments (indices 4, 5, 6, 7) sont les données du deuxième pixel de la première ligne. Lorsque nous arrivons au 800e pixel de la première ligne, à condition que la fenêtre ait une largeur de 800 pixels - le 801e pixel appartiendra déjà à la deuxième ligne. Si nous le changeons, à l'écran, nous verrons que le 1er pixel de la 2e ligne a changé (bien que par le nombre dans le tableau ce sera le 801e pixel). Pourquoi y a-t-il 4 éléments pour chaque pixel du tableau? En effet, en toile, en plus d'allouer 1 élément pour chaque couleur - rouge, vert, bleu (ce sont 3 éléments), 1 élément de plus pour la transparence (ils disent aussi le canal alpha ou l'opacité). Le canal alpha, comme la couleur, est défini dans la plage de 0 (transparent) à 255 (opaque). Avec cette structure, nous obtenons une image 32 bits,car chaque pixel est composé de 4 éléments de 8 bits. Pour résumer: chaque pixel contient: couleurs rouge, vert, bleu et canal alpha (transparence). Ce schéma de couleurs est appelé ARGB (Alpha Red Green Blue). Et le fait que chaque pixel occupe 32 bits indique que nous avons une image 32 bits (ils disent aussi une image avec une profondeur de couleur de 32 bits).

Par défaut, l'ensemble du tableau de pixels imageData.data (les données sont une propriété dans laquelle le tableau de pixels et imageData n'est qu'un objet) est rempli avec les valeurs 0, et si nous essayions de sortir un tel tableau, nous ne verrions rien d'intéressant à l'écran, car 0 , 0, 0 est noir, mais comme la transparence ici sera également 0, et c'est une couleur complètement transparente, nous ne verrons même pas le noir à l'écran!

Il n'est pas pratique de travailler directement avec un tel tableau unidimensionnel, nous allons donc lui écrire une classe dans laquelle nous allons créer des méthodes de dessin. Je nommerai la classe - Tiroir. Cette classe ne stockera que les données nécessaires et effectuera les calculs nécessaires, en faisant abstraction autant que possible de l'outil utilisé pour le rendu. C'est pourquoi nous placerons tous les calculs et travaillerons avec le tableau dedans. Et l'appel même à la méthode d'affichage sur toile, nous allons placer en dehors de la classe, car il pourrait y avoir autre chose au lieu de la toile. Dans ce cas, notre classe ne devra pas être changée. Pour travailler avec un tableau de pixels (surface), il est plus pratique pour nous de l'enregistrer dans la classe Drawer, ainsi que la largeur et la hauteur de l'image, afin que nous puissions accéder correctement au pixel souhaité. Ainsi, la classe Drawer, tout en préservant les données minimales nécessaires au dessin, ressemble à ceci pour moi:

class Drawer {
    surface = null
    width = 0
    height = 0

    constructor(surface, width, height) {
        this.surface = surface
        this.width = width
        this.height = height
    }
}

Comme vous pouvez le voir dans le constructeur, la classe Drawer prend toutes les données nécessaires et les enregistre. Vous pouvez maintenant créer une instance de cette classe et y passer un tableau de pixels, largeur et hauteur (nous avons déjà toutes ces données, car nous l'avons créé ci-dessus et stocké dans imageData):

const drawer = new Drawer(
    imageData.data,
    imageData.width,
    imageData.height
)

Dans la classe Drawer, nous écrirons plusieurs fonctions de dessin, pour faciliter le travail à l'avenir. Nous aurons une fonction pour dessiner un pixel, une fonction pour dessiner une ligne, et dans d'autres articles des fonctions pour dessiner un triangle et d'autres formes apparaîtront. Mais commençons par la méthode de dessin au pixel. Je l'appellerai drawPixel. Si nous dessinons un pixel, il devrait avoir des coordonnées, ainsi que de la couleur:

drawPixel(x, y, r, g, b)  { }

Veuillez noter que la fonction drawPixel n'accepte pas le paramètre alpha (transparence), et ci-dessus, nous avons compris que le tableau de pixels se compose de 3 paramètres de couleur et 1 paramètre de transparence. Je n'ai pas spécifiquement indiqué la transparence, car nous n'en avons absolument pas besoin pour les exemples. Par défaut, nous définirons 255 (c'est-à-dire que tout sera opaque). Voyons maintenant comment écrire la couleur souhaitée dans un tableau de pixels en coordonnées x, y. Puisque nous avons toutes les informations sur l'image sont stockées dans un tableau unidimensionnel, dans lequel 1 pixel (8 bits) est alloué pour chaque pixel. Pour accéder au pixel souhaité dans le tableau, nous devons d'abord déterminer l'indice de localisation rouge, car tout pixel commence par lui (par exemple [r, g, b, a]). Une petite explication de la structure du tableau:



Le tableau en vert indique comment les composants de couleur sont stockés dans un réseau de surfaces unidimensionnel. Leurs indices dans le même tableau sont indiqués en bleu, et les coordonnées du pixel qui accepte les fonctions drawPixel, que nous devons convertir en indices dans le tableau unidimensionnel, indiquent r, g, b, a pour le pixel en bleu. Ainsi, à partir du tableau, on peut voir que pour chaque pixel, la composante rouge de la couleur vient en premier, commençons par elle. Supposons que nous voulons changer la composante rouge de la couleur des pixels dans les coordonnées X1Y1 avec une taille d'image de 2 par 2 pixels. Dans le tableau, nous voyons que c'est l'indice 12, mais comment le calculer? Nous trouvons d'abord l'indice de la ligne dont nous avons besoin, pour cela nous multiplions la largeur de l'image par Y et par 4 (le nombre de valeurs par pixel) - ce sera:

width * y * 4 
//  :
2 * 1 * 4 = 8

On voit que la 2ème ligne commence par l'index 8. Si l'on compare avec la plaque, le résultat converge.

Vous devez maintenant ajouter un décalage de colonne à l'index de ligne trouvé pour obtenir l'index rouge souhaité. Pour ce faire, ajoutez X fois 4 à l'index de ligne. La formule complète sera:

width * y * 4 + x * 4 
//     :
(width * y + x) * 4
//  :
(2 * 1 + 1) * 4 = 12

Maintenant, nous comparons 12 au tableau et voyons que le pixel X1Y1 commence vraiment par l'index 12.

Pour trouver les indices des autres composants de couleur, vous devez ajouter le décalage de couleur à l'index rouge: +1 (vert), +2 (bleu), +3 (alpha) . Nous pouvons maintenant implémenter la méthode drawPixel dans la classe Drawer en utilisant la formule ci-dessus:

drawPixel(x, y, r, g, b) {
    const offset = (this.width * y + x) * 4

    this.surface[offset] = r
    this.surface[offset + 1] = g
    this.surface[offset + 2] = b
    this.surface[offset + 3] = 255
}

Dans cette méthode drawPixel, j'ai rendu la partie répétitive de la formule à la constante de décalage. On voit aussi qu'en alpha j'écris juste 255, parce que c'est dans la structure, mais maintenant nous n'avons pas besoin de sortir des pixels.

Il est temps de tester le code et de voir enfin le premier pixel à l'écran. Voici un exemple utilisant la méthode de rendu pixel:

//     Drawer
drawer.drawPixel(10, 10, 255, 0, 0)
drawer.drawPixel(10, 20, 0, 0, 255)

//         canvas
ctx.putImageData(imageData, 0, 0)

Dans l'exemple ci-dessus, je dessine 2 pixels, l'un rouge 255, 0, 0, l'autre bleu 0, 0, 255. Mais les changements dans le tableau imageData.data (c'est aussi la surface à l'intérieur de la classe Drawer) n'apparaîtront pas à l'écran. Pour dessiner, vous devez appeler ctx.putImageData (imageData, 0, 0), où imageData est l'objet dans lequel le tableau de pixels et la largeur / hauteur de la zone de dessin, et 0, 0 est le point par rapport auquel le tableau de pixels sera affiché (laissez toujours 0, 0 ) Si vous avez tout fait correctement, vous aurez l'image suivante en haut à gauche de l'élément canvas dans la fenêtre du navigateur: Avez-vous vu les



pixels? Ils sont si petits et combien de travail a été fait.

Essayons maintenant d'ajouter un peu de dynamique à l'exemple, par exemple, de sorte que toutes les 10 millisecondes, notre pixel se déplace vers la droite (nous changeons X pixels de +1 toutes les 10 millisecondes), nous corrigeons le code de dessin des pixels de un par intervalle:

let x = 10
setInterval(() => {

    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)

}, 10)

Dans cet exemple, je n'ai laissé que la sortie du pixel bleu et j'ai encapsulé la fonction setInterval avec le paramètre 10 en JavaScript. Cela signifie que le code sera appelé toutes les 10 millisecondes environ. Si vous exécutez un tel exemple, vous verrez qu'au lieu d'un pixel se déplaçant vers la droite, vous aurez quelque chose comme ceci:



Une telle longue bande (ou trace) reste parce que nous n'effaçons pas la couleur du pixel précédent dans le tableau de surface, donc à chaque appel à l'intervalle que nous ajoutons un pixel. Écrivons une méthode qui nettoiera la surface à son état d'origine. En d'autres termes, remplissez le tableau de zéros. Ajoutez la méthode clearSurface à la classe Drawer:

clearSurface() {
    const surfaceSize = this.width * this.height * 4
    for (let i = 0; i < surfaceSize; i++) {
        this.surface[i] = 0
    }
}

Il n'y a aucune logique dans ce tableau, juste un remplissage avec des zéros. Il est recommandé d'appeler cette méthode à chaque fois avant de dessiner une nouvelle image. Dans le cas d'une animation pixel, avant de dessiner ce pixel:

let x = 10
setInterval(() => {
    drawer.clearSurface()
    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)
}, 10)

Maintenant, si vous exécutez cet exemple, le pixel se déplacera vers la droite, un par un - sans trace inutile des coordonnées précédentes.

La dernière chose que nous implémentons dans le premier article est la méthode de dessin au trait. Ajoutez-le, bien sûr, à la classe Drawer. La méthode que j'appellerai drawLine. Que prendra-t-il? Contrairement à un point, la ligne a toujours les coordonnées auxquelles elle se termine. En d'autres termes, la ligne a un début, une fin et une couleur, que nous passerons à la méthode:

drawLine(x1, y1, x2, y2, r, g, b) { }

Toute ligne est constituée de pixels, il ne reste plus qu'à la remplir correctement avec des pixels de x1, y1 à x2, y2. Pour commencer, puisque la ligne est constituée de pixels, nous la sortirons pixel par pixel dans la boucle, mais comment calculer le nombre de pixels à sortir? Par exemple, pour tracer une ligne de [0, 0] à [3, 0], il est intuitivement clair que vous avez besoin de 4 pixels ([0, 0], [1, 0], [2, 0], [3, 0],) . Mais de [12, 6] à [43, 14], on ne sait déjà pas combien de temps la ligne sera (combien de pixels afficher) et quelles coordonnées elles auront. Pour ce faire, rappelons un peu de géométrie. Donc, nous avons une ligne qui commence à x1, y1 et se termine à x2, y2.


Tirons une ligne pointillée du début et de la fin pour obtenir un triangle (image ci-dessus). Nous verrons qu'à la jonction des lignes tracées un angle de 90 degrés s'est formé. Si le triangle a un tel angle, alors le triangle est appelé rectangulaire, et ses côtés, entre lesquels l'angle est de 90 degrés, sont appelés jambes. La troisième ligne continue (que nous essayons de tracer) est appelée hypoténuse dans un triangle. En utilisant ces deux jambes introduites (c1 et c2 sur la figure), nous pouvons calculer la longueur de l'hypoténuse en utilisant le théorème de Pythagore. Voyons comment faire. La formule de la longueur de l'hypoténuse (ou longueur de ligne) sera la suivante: 

=12+22


Comment obtenir les deux jambes est également vu du triangle. Maintenant, en utilisant la formule ci-dessus, nous trouvons l'hypoténuse, qui sera la longue ligne (le nombre de pixels):

 drawLine(x1, y1, x2, y2, r, g, b) {
         const c1 = y2 - y1
         const c2 = x2 - x1

         const length = Math.sqrt(c1 * c1 + c2 * c2)

Nous savons déjà combien de pixels dessiner pour tracer une ligne. Mais nous ne savons pas encore comment les pixels sont décalés. Autrement dit, nous devons tracer une ligne de x1, y1 à x2, y2, nous savons que la longueur de la ligne sera, par exemple, de 20 pixels. On peut dessiner le 1er pixel en x1, y1 et le dernier en x2, y2, mais comment trouver les coordonnées des pixels intermédiaires? Pour ce faire, nous devons savoir comment décaler chaque pixel suivant par rapport à x1, y1 pour obtenir la ligne souhaitée. Je donnerai un autre exemple afin de mieux comprendre de quel type de déplacement nous parlons. Nous avons des points [0, 0] et [0, 3], nous devons tracer une ligne sur eux. L'exemple montre clairement que le point suivant après [0, 0] sera [0, 1], puis [0, 2] et enfin [0, 3]. Autrement dit, X de chaque point n'a pas été décalé, eh bien, ou nous pouvons dire qu'il a été décalé de 0 pixels, et Y a été décalé de 1 pixel, c'est le décalage,il peut être écrit comme [0, 1]. Un autre exemple: nous avons un point [0, 0] et un point [3, 6], essayons de calculer dans notre esprit comment ils se déplacent, le premier sera [0, 0], puis [0.5, 1], puis [1, 2] puis [1.5, 3] et ainsi de suite jusqu'à [3, 6], dans cet exemple le décalage sera [0.5, 1]. Comment le calculer? 

Vous pouvez utiliser la formule suivante:

   = 2 /  
  Y = 1 /   

Dans le code du programme, nous aurons ceci:

const xStep = c2 / length
const yStep = c1 / length

Toutes les données sont déjà là: la longueur de la ligne, le décalage des pixels le long de X et Y. On commence dans le cycle à dessiner:

for (let i = 0; i < length; i++) {
    this.drawPixel(
        Math.trunc(x1 + xStep * i),
        Math.trunc(y1 + yStep * i),
        r, g, b,
    )
}

En tant que coordonnée X de la fonction Pixel, nous transférons le début de la ligne X + décalage X * i, obtenant ainsi la coordonnée du i-ème pixel, nous calculons également la coordonnée Y. Math.trunc est une méthode en JS qui vous permet d'éliminer la partie fractionnaire d'un nombre. Le code de la méthode entière ressemble à ceci:

drawLine(x1, y1, x2, y2, r, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * c2)

    const xStep = c2 / length
    const yStep = c1 / length

    for (let i = 0; i < length; i++) {
        this.drawPixel(
            Math.trunc(x1 + xStep * i),
            Math.trunc(y1 + yStep * i),
            r, g, b,
        )
    }
}

La première partie est arrivée à son terme, un chemin long mais passionnant pour comprendre le monde 3D. Il n'y avait encore rien de tridimensionnel, mais nous avons effectué des opérations préparatoires pour le dessin: nous avons mis en œuvre les fonctions de dessin d'un pixel, d'une ligne, de nettoyage d'une fenêtre et appris quelques termes. Tout le code de la classe Drawer peut être consulté sous le spoiler:

Code de classe de tiroir
class Drawer {
  surface = null
  width = 0
  height = 0

  constructor(surface, width, height) {
    this.surface = surface
    this.width = width
    this.height = height
  }

  drawPixel(x, y, r, g, b)  {
    const offset = (this.width * y + x) * 4

    this.surface[offset] = r
    this.surface[offset + 1] = g
    this.surface[offset + 2] = b
    this.surface[offset + 3] = 255
  }

  drawLine(x1, y1, x2, y2, r, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * c2)

    const xStep = c2 / length
    const yStep = c1 / length

    for (let i = 0 ; i < length ; i++) {
        this.drawPixel(
          Math.trunc(x1 + xStep * i),
          Math.trunc(y1 + yStep * i),
          r, g, b,
        )
    }
  }

  clearSurface() {
    const surfaceSize = this.width * this.height * 4
    for (let i = 0; i < surfaceSize; i++) {
      this.surface[i] = 0
    }
  }
}

Et après?


Dans le prochain article, nous verrons comment une opération aussi simple que la sortie d'un pixel et d'une ligne peut se transformer en objets 3D intéressants. Nous allons nous familiariser avec les matrices et leurs opérations, afficher un objet tridimensionnel dans une fenêtre et même ajouter une animation.

All Articles