Mise en route dans WebGL, partie 2 L'élément Canvas pour notre premier shader

Dans l'article précédent, nous avons écrit nos premiers shaders de vertex et de fragment. Après avoir écrit le code côté GPU, il est temps d’apprendre à écrire le code côté CPU. Dans ce tutoriel et le suivant, je vais vous montrer comment incorporer des shaders dans votre application WebGL. Nous allons commencer à partir de zéro, en utilisant JavaScript uniquement et aucune bibliothèque tierce. Dans cette partie, nous couvrirons le code spécifique à la toile. Dans le prochain, nous couvrirons celui spécifique à WebGL.

Notez que ces articles:

  • supposons que vous connaissez les shaders GLSL. Si non, s'il vous plaît lire le premier article.
  • ne sont pas destinés à vous apprendre HTML, CSS ou JavaScript. J'essaierai d'expliquer les concepts difficiles à mesure que nous les rencontrons, mais vous devrez chercher plus d'informations à leur sujet sur le Web. Le MDN (Mozilla Developer Network) est un excellent endroit pour le faire..

Commençons déjà!

Qu'est-ce que WebGL??

WebGL 1.0 est une API graphique 3D de bas niveau pour le Web, exposée via l'élément HTML5 Canvas. C'est une API basée sur le shader qui est très similaire à l'API OpenGL ES 2.0. WebGL 2.0 est identique, mais est basé sur OpenGL ES 3.0. WebGL 2.0 n'est pas entièrement compatible avec WebGL 1.0, mais la plupart des applications WebGL 1.0 sans erreur qui n'utilisent pas d'extensions devraient fonctionner sans problème sur WebGL 2.0..

Au moment de la rédaction de cet article, les implémentations de WebGL 2.0 sont encore expérimentales dans les quelques navigateurs qui l’implémentent. Ils ne sont également pas activés par défaut. Par conséquent, le code que nous allons écrire dans cette série est destiné à WebGL 1.0..

Jetez un coup d'œil à l'exemple suivant (n'oubliez pas de changer d'onglet et de jeter un coup d'œil sur le code):

C'est le code que nous allons écrire. Oui, il faut en réalité un peu plus de cent lignes de JavaScript pour mettre en oeuvre quelque chose d'aussi simple. Mais ne vous inquiétez pas, nous prendrons notre temps pour les expliquer afin qu'ils aient tous un sens à la fin. Nous allons couvrir le code relatif à la toile dans ce didacticiel et passer au code spécifique à WebGL dans le suivant..

La toile

Premièrement, nous devons créer une toile sur laquelle nous montrerons nos trucs rendus. 

Ce joli petit carré est notre toile! Basculer vers le HTML voir et voyons comment nous l'avons fait.

C'est pour dire au navigateur que nous ne voulons pas que notre page soit zoomable sur les appareils mobiles.

Et ceci est notre élément de toile. Si nous n'avions pas affecté de dimensions à notre zone de dessin, la valeur par défaut serait 300 * 150px (pixels CSS). Passons maintenant au CSS voir pour vérifier comment nous l'avons appelé.

Toile … 

Ceci est un sélecteur CSS. Cela signifie notamment que les règles suivantes vont être appliquées à tous les éléments de canevas de notre document. 

arrière-plan: # 0f0;

Enfin, la règle à appliquer aux éléments de la toile. Le fond est vert clair (# 0f0).

Remarque: dans l'éditeur ci-dessus, le texte CSS est automatiquement attaché au document. Lorsque vous créez vos propres fichiers, vous devrez créer un lien vers le fichier CSS dans votre fichier HTML, comme suit:

De préférence, mettez-le dans le tête étiquette.

Maintenant que la toile est prête, il est temps de dessiner des choses! Malheureusement, bien que la toile soit belle et tout, nous avons encore un long chemin à parcourir avant de pouvoir dessiner quoi que ce soit avec WebGL. Alors débris WebGL! Pour ce tutoriel, nous allons faire un simple dessin 2D pour expliquer certains concepts avant de passer à WebGL. Que notre dessin soit une ligne diagonale.

Contexte de rendu

Le code HTML est identique au dernier exemple, à l'exception de cette ligne:   

dans lequel nous avons donné un identifiant à l'élément canvas afin que nous puissions facilement le récupérer en JavaScript. Le CSS est exactement le même et un nouvel onglet JavaScript a été ajouté pour effectuer le dessin..

Basculer vers le JS languette,

window.addEventListener ('load', function () …);

Dans l'exemple ci-dessus, le code JavaScript que nous avons écrit doit être attaché à l'en-tête du document, ce qui signifie qu'il est exécuté avant le chargement de la page. Mais si c'est le cas, nous ne pourrons pas dessiner sur la toile, qui n'a pas encore été créée. C'est pourquoi nous différons l'exécution de notre code après le chargement de la page. Pour ce faire, nous utilisons window.addEventListener, en précisant charge en tant qu'événement que nous voulons écouter et notre code en tant que fonction qui s'exécute lorsque l'événement est déclenché.

Passer à:

var canvas = document.getElementById ("canvas");

Vous vous souvenez de l'identifiant que nous avons attribué à la toile précédemment dans le code HTML Voici où cela devient utile. Dans la ligne ci-dessus, nous récupérons l'élément canvas du document en utilisant son identifiant comme référence. A partir de maintenant, les choses deviennent plus intéressantes,

context = canvas.getContext ('2d');

Pour pouvoir dessiner n’importe quel dessin sur la toile, nous devons d’abord acquérir un contexte de dessin. Un contexte dans ce sens est un objet d'assistance qui expose l'API de dessin requise et le lie à l'élément canvas. Cela signifie que toute utilisation ultérieure de l'API utilisant ce contexte sera effectuée sur l'objet de la toile en question..

Dans ce cas particulier, nous avons demandé une 2d contexte de dessin (CanvasRenderingContext2D) qui nous permet d’utiliser des fonctions de dessin 2D arbitraires. Nous aurions pu demander un webgl, une webgl2 ou un bitmaprenderer contextes à la place, chacun ayant exposé un ensemble différent de fonctions.

Une toile a toujours son mode de contexte défini sur aucun initialement. Puis, en appelant getContext, son mode change en permanence. Peu importe combien de fois tu appelles getContext sur un canevas, il ne changera pas de mode après avoir été défini initialement. Appel getContext à nouveau pour la même API retournera le même objet de contexte retourné lors de la première utilisation. Appel getContext pour une autre API reviendra nul.

Malheureusement, les choses peuvent mal tourner. Dans certains cas particuliers, getContext peut être incapable de créer un contexte et déclenche une exception à la place. Bien que cela soit plutôt rare de nos jours, c’est possible avec 2d contextes. Donc, au lieu de planter si cela se produit, nous avons encapsulé notre code dans un try-catch bloc:

try context = canvas.getContext ('2d');  catch (exception) alert ("Euh… désolé, pas de contexte 2D pour vous!" + exception.message); revenir ; 

De cette façon, si une exception est levée, nous pouvons l’attraper et afficher un message d’erreur, puis nous attaquer gracieusement à la tête contre le mur. Ou peut-être afficher une image statique d'une ligne diagonale. Bien que nous puissions le faire, cela défie le but de ce tutoriel!

En supposant que nous ayons réussi à acquérir un contexte, il ne reste plus qu'à tracer la ligne:

context.beginPath ();

le 2d contexte rappelle le dernier chemin que vous avez construit. Dessiner un chemin ne le supprime pas automatiquement de la mémoire du contexte. beginPath indique au contexte d'oublier tous les chemins précédents et de recommencer à zéro. Donc oui, dans ce cas, nous aurions pu omettre complètement cette ligne et cela aurait parfaitement fonctionné, car il n'y avait pas de chemin précédent pour commencer..

context.moveTo (0, 0);

Un chemin peut être composé de plusieurs sous-chemins. déménager à commence un nouveau sous-chemin aux coordonnées requises.

context.lineTo (30, 30);

Crée un segment de ligne à partir du dernier point du sous-chemin vers (30, 30). Cela signifie une ligne diagonale allant du coin supérieur gauche de la toile (0, 0) à son coin inférieur droit (30, 30)..

context.stroke ();

Créer un chemin est une chose. c'est un autre dessin. accident vasculaire cérébral indique au contexte de dessiner tous les sous-chemins dans sa mémoire.

beginPath, déménager à, lineto, et accident vasculaire cérébral sont disponibles uniquement parce que nous avons demandé un 2d le contexte. Si, par exemple, nous avons demandé une webgl contexte, ces fonctions n'auraient pas été disponibles.

Remarque: dans l'éditeur ci-dessus, le code JavaScript est automatiquement associé au document. Lorsque vous créez vos propres fichiers, vous devez créer un lien vers le fichier JavaScript de votre fichier HTML, comme suit:

Vous devriez le mettre dans le tête étiquette.

Ceci conclut notre tutoriel de dessin au trait! Mais de toute façon, je ne suis pas satisfait de cette petite toile. On peut faire plus gros que ça!

Dimensionnement de la toile

Nous allons ajouter quelques règles à notre CSS pour que la toile remplisse toute la page. Le nouveau code CSS va ressembler à ceci:

html, corps hauteur: 100%;  body margin: 0;  canvas display: block; largeur: 100%; hauteur: 100%; arrière-plan: # 888; 

Démontons-le:

html, corps hauteur: 100%; 

le html et corps les éléments sont traités comme des éléments de bloc; ils consomment toute la largeur disponible. Cependant, ils se développent verticalement juste assez pour envelopper leur contenu. En d'autres termes, leur hauteur dépend de celle de leurs enfants. Régler l'une des hauteurs de leurs enfants sur un pourcentage de leur hauteur provoquera une boucle de dépendance. Donc, à moins que nous assignions explicitement des valeurs à leur hauteur, nous ne pourrions pas définir la hauteur des enfants par rapport à elles..

Puisque nous voulons que le canevas remplisse la page entière (définissez sa hauteur à 100% de son parent), nous définissons leurs hauteurs à 100% (de la hauteur de la page).

corps marge: 0; 

Les navigateurs ont des feuilles de style de base qui donnent un style par défaut à tout document rendu. Ça s'appelle le feuilles de style d'agent utilisateur. Les styles de ces feuilles dépendent du navigateur en question. Parfois, ils peuvent même être ajustés par l'utilisateur.

le corps L'élément a généralement une marge par défaut dans les feuilles de style de l'agent utilisateur. Nous voulons que la toile remplisse la page entière, nous avons donc défini ses marges sur 0.

canvas display: block;

Contrairement aux éléments de bloc, les éléments en ligne sont des éléments qui peuvent être traités comme du texte sur une ligne régulière. Ils peuvent avoir des éléments avant ou après eux sur la même ligne, et ils ont un espace vide en dessous d'eux dont la taille dépend de la police utilisée et de la taille de la police utilisée. Nous ne voulons pas d’espace vide sous notre canevas, nous avons donc défini son mode d’affichage sur bloc.

largeur: 100%; hauteur: 100%;

Comme prévu, nous avons défini les dimensions de la toile sur 100% de la largeur et de la hauteur de la page.

arrière-plan: # 888;

Nous avons déjà expliqué cela auparavant, n'avons-nous pas?!

Voici le résultat de nos changements…

Non, nous n'avons rien fait de mal! C'est un comportement totalement normal. Rappelez-vous les dimensions que nous avons données à la toile dans le HTML étiquette?

Nous avons maintenant donné à la toile d'autres dimensions dans le CSS:

toile … largeur: 100%; hauteur: 100%;… 

Il s'avère que les dimensions que nous avons définies dans la balise HTML contrôlent la dimensions intrinsèques de la toile. Le canevas est plus ou moins un conteneur bitmap. Les dimensions de l'image bitmap sont indépendantes de la manière dont le canevas sera affiché dans sa position finale et des dimensions dans la page. Qu'est-ce qui définit ce sont les dimensions extrinsèques, ceux que nous avons définis dans le CSS.

Comme nous pouvons le constater, notre minuscule bitmap 30 * 30 a été étendu pour remplir la totalité de la toile. Ceci est contrôlé par le CSS ajustement d'objet propriété, qui par défaut est remplir. Il existe d’autres modes qui, par exemple, découpent au lieu d’échelle, mais depuis remplir ne entrera pas dans notre chemin (en fait cela peut être utile), nous le laisserons simplement. Si vous envisagez de prendre en charge Internet Explorer ou Edge, vous ne pouvez rien y faire. Au moment d'écrire cet article, ils ne supportent pas ajustement d'objet du tout.

Toutefois, sachez que la manière dont le navigateur adapte le contenu fait encore l’objet d’un débat. La propriété CSS rendu d'image a été proposé pour gérer cela, mais il est toujours expérimental (si pris en charge du tout), et il ne dicte pas certains algorithmes de mise à l'échelle. Non seulement cela, le navigateur peut choisir de le négliger entièrement, car il ne s'agit que d'un indice. Cela signifie que pour le moment, différents navigateurs utiliseront différents algorithmes de mise à l'échelle pour mettre à l'échelle votre image. Certains d'entre eux ont des artefacts vraiment terribles, alors évitez de trop les redimensionner.

Que nous dessinions en utilisant un 2d contexte ou d’autres types de contextes (comme webgl), la toile se comporte presque de la même manière. Si nous voulons que notre petit bitmap remplisse la totalité de la toile et que nous n'aimons pas l'étirement, nous devons surveiller les modifications de la taille de la toile et ajuster les dimensions de l'image en conséquence. Faisons-le maintenant,

En examinant les modifications apportées, nous avons ajouté ces deux lignes au code JavaScript:

canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight;

Oui, en utilisant 2d Dans les contextes, il est facile de définir les dimensions internes du bitmap sur les dimensions de la toile. La toile largeur et la taille sont surveillés et quand l’un d’eux est écrit (même si c’est la même valeur):

  • Le bitmap actuel est détruit.
  • Un nouveau avec les nouvelles dimensions est créé.
  • Le nouveau bitmap est initialisé avec la valeur par défaut (noir transparent).
  • Tout contexte associé est réinitialisé à son état initial et est réinitialisé avec les dimensions d'espace de coordonnées nouvellement spécifiées.

Notez que, pour régler à la fois largeur et la taille, les étapes ci-dessus sont effectuées deux fois! Une fois lors du changement largeur et l'autre lors du changement la taille. Non, il n'y a pas d'autre moyen de le faire, pas que je sache.

Nous avons également élargi notre ligne courte pour devenir la nouvelle diagonale,

context.lineTo (canvas.width, canvas.height);

au lieu de: 

context.lineTo (30, 30);

Puisque nous n'utilisons plus les dimensions d'origine 30 * 30, elles ne sont plus nécessaires dans le code HTML:

Nous aurions pu les laisser initialisées à de très petites valeurs (comme 1 * 1) pour éviter la surcharge liée à la création d’un bitmap utilisant les dimensions par défaut relativement grandes (300 * 150), à son initialisation, à sa suppression, puis à la création d’un nouveau taille correcte que nous avons définie en JavaScript.

à la réflexion, faisons juste ça!

Personne ne devrait jamais remarquer la différence, mais je ne supporte pas la culpabilité!

CSS pixel vs pixel physique

J'aurais aimé dire que c'est ça, mais ce n'est pas! offsetWidth et offsetHeight sont spécifiés en pixels CSS. 

Voici le piège. Les pixels CSS sont ne pas pixels physiques. Ce sont des pixels indépendants de la densité. En fonction de la densité de pixels physiques de votre appareil (et de votre navigateur), un pixel CSS peut correspondre à un ou plusieurs pixels physiques..

En clair, si vous avez un smartphone Full HD 5 pouces, alors offsetWidth*offsetHeight serait 640 * 360 au lieu de 1920 * 1080. Bien sûr, cela remplit l'écran, mais comme les dimensions internes sont définies sur 640 * 360, le résultat est un bitmap étiré qui n'utilise pas pleinement la haute résolution de l'appareil. Pour résoudre ce problème, nous prenons en compte les devicePixelRatio:

var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; canvas.width = pixelRatio * canvas.offsetWidth; canvas.height = pixelRatio * canvas.offsetHeight;

devicePixelRatio est le rapport entre le pixel CSS et le pixel physique. En d’autres termes, combien de pixels physiques un pixel CSS unique représente.

var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0;

window.devicePixelRatio est bien supporté par la plupart des navigateurs modernes, mais juste au cas où il serait indéfini, nous retomberons sur la valeur par défaut de 1,0.

canvas.width = pixelRatio * canvas.offsetWidth; canvas.height = pixelRatio * canvas.offsetHeight;

En multipliant les dimensions CSS par le ratio de pixels, nous revenons aux dimensions physiques. Maintenant, notre bitmap interne a exactement la même taille que la toile et aucun étirement ne se produira.

Si ton devicePixelRatio est 1 alors il n'y aura pas de différence. Cependant, pour toute autre valeur, la différence est significative.

Répondre aux changements de taille

Ce n'est pas tout ce qu'il y a à gérer la taille de la toile. Puisque nous avons spécifié nos dimensions CSS par rapport à la taille de la page, les modifications apportées à la taille de la page nous affectent. Si nous exécutons sur un navigateur de bureau, l'utilisateur peut redimensionner la fenêtre manuellement. Si nous courons sur un appareil mobile, nous sommes soumis à des changements d'orientation. Sans mentionner que nous courons peut-être à l'intérieur d'un iframe cela change sa taille arbitrairement. Pour conserver à tout moment la taille correcte de notre bitmap interne, nous devons surveiller les modifications de la taille de la page (fenêtre).,

Nous avons déplacé notre code de redimensionnement bitmap:

// Obtenir le rapport de pixels du périphérique, var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Ajuste la taille de la toile, canvas.width = pixelRatio * canvas.offsetWidth; canvas.height = pixelRatio * canvas.offsetHeight;

À une fonction séparée, ajusterCanvasBitmapSize:

function adjustCanvasBitmapSize () // Récupère le ratio de pixels du périphérique, var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; if ((canvas.width / pixelRatio)!! = canvas.offsetWidth) canvas.width = pixelRatio * canvas.offsetWidth; if ((canvas.height / pixelRatio)!! = canvas.offsetHeight) canvas.height = pixelRatio * canvas.offsetHeight; 

avec une petite modification. Puisque nous savons combien il est coûteux d’attribuer des valeurs à largeur ou la taille est, il serait irresponsable de le faire inutilement. Maintenant, nous ne mettons que largeur et la taille quand ils changent réellement.

Puisque notre fonction accède à notre toile, nous la déclarons où elle peut la voir. Initialement, il a été déclaré dans cette ligne:

var canvas = document.getElementById ("canvas");

Cela le rend local à notre fonction anonyme. Nous aurions pu simplement enlever le var partie et il serait devenu global (ou plus précisément, un propriété du objet global, auquel on peut accéder par la fenêtre):

canvas = document.getElementById ("canvas");

Cependant, je déconseille fortement déclaration implicite. Si vous déclarez toujours vos variables, vous éviterez beaucoup de confusion. Donc, au lieu de cela, je vais le déclarer en dehors de toutes les fonctions:

toile var; var contexte;

Cela en fait également une propriété de l'objet global (avec une petite différence qui ne nous dérange pas vraiment). Il y a d'autres façons de créer une variable globale - vérifiez-les dans ce thread StackOverflow. 

Oh, et j'ai faufilé le contexte là-haut aussi! Cela s'avérera utile plus tard.

Maintenant, accrochons notre fonction à la fenêtre redimensionner un événement:

window.addEventListener ('resize', adjustCanvasBitmapSize);

A partir de maintenant, chaque fois que la taille de la fenêtre est modifiée, ajusterCanvasBitmapSize est appelé. Mais puisque l'événement de taille de fenêtre n'est pas déclenché lors du chargement initial, notre bitmap sera toujours 1 * 1. Par conséquent, nous devons appeler ajusterCanvasBitmapSize une fois par nous-mêmes.

adjustCanvasBitmapSize ();

Cela s’en occupe presque… sauf que lorsque vous redimensionnez la fenêtre, la ligne disparaît! Essayez-le dans cette démo.

Heureusement, c'est à prévoir. Rappelez-vous les étapes effectuées lorsque le bitmap est redimensionné? L'un d'eux était de l'initialiser au noir transparent. C'est ce qui s'est passé ici. Le bitmap a été écrasé en noir transparent et le fond vert de la toile est maintenant visible. Cela se produit parce que nous ne tirons notre ligne qu'une fois au début. Lorsque l'événement de redimensionnement a lieu, le contenu est effacé et non redessiné. Cela devrait être facile. Déplaçons notre ligne vers une fonction distincte:

function drawScene () // Trace notre ligne, context.beginPath (); context.moveTo (0, 0); context.lineTo (canvas.width, canvas.height); context.stroke (); 

et appeler cette fonction de l'intérieur ajusterCanvasBitmapSize:

// Redessine tout à nouveau, drawScene ();

Cependant, de cette façon, notre scène sera redessinée à chaque fois ajusterCanvasBitmapSize est appelé, même si aucun changement de dimensions n’a eu lieu. Pour gérer cela, nous allons ajouter un simple contrôle:

// Abandonne si rien n'a changé, if (((canvas.width / pixelRatio) == canvas.offsetWidth) && ((canvas.height / pixelRatio) == canvas.offsetHeight)) return; 

Découvrez le résultat final:

Essayez de le redimensionner ici.

Limiter les événements de redimensionnement

Jusqu'ici nous allons très bien! Cependant, tout redimensionner et tout redessiner peut facilement devenir très coûteux lorsque votre toile est assez grande et / ou lorsque la scène est compliquée. De plus, le redimensionnement de la fenêtre avec la souris peut déclencher des événements de redimensionnement à un taux élevé. C'est pourquoi nous l'étoufferons. Au lieu de:

window.addEventListener ('resize', adjustCanvasBitmapSize);

nous allons utiliser:

window.addEventListener ('resize', fonction onWindowResize (event) // Attend que l'inondation des événements de redimensionnement soit réglée, if (onWindowResize.timeoutId) window.clearTimeout (onWindowResize.timeoutId); ););

Premier,

window.addEventListener ('resize', fonction onWindowResize (event) …);

au lieu d'appeler directement ajusterCanvasBitmapSize quand le redimensionner événement est déclenché, nous avons utilisé un expression de fonction définir le comportement souhaité. Contrairement à la fonction utilisée précédemment pour la charge événement, cette fonction est un fonction nommée. Donner un nom à la fonction permet de s'y référer facilement depuis la fonction elle-même.

if (onWindowResize.timeoutId) window.clearTimeout (onWindowResize.timeoutId);

Tout comme les autres objets, des propriétés peuvent être ajoutées à objets de fonction. Initialement, timeoutId est indéfini, ainsi, cette instruction n'est pas exécutée. Attention quand vous utilisez indéfini et nul dans expressions logiques, parce qu'ils peuvent être difficiles. En savoir plus sur eux dans la spécification de langage ECMAScript.

Plus tard, timeoutId tiendra le timeoutID d'un ajusterCanvasBitmapSize temps libre:

onWindowResize.timeoutId = window.setTimeout (adjustCanvasBitmapSize, 600);

Cela retarde l'appel ajusterCanvasBitmapSize pendant 600 millisecondes après le déclenchement de l'événement. Mais cela n'empêche pas l'événement de se déclencher. Si ce n'est pas à nouveau tiré dans les 600 millisecondes, alors ajusterCanvasBitmapSize est exécuté et le bitmap est redimensionné. Autrement, clearTimeout annule l'horaire ajusterCanvasBitmapSize et setTimeout programme un autre 600 millisecondes dans le futur. Le résultat est, tant que l'utilisateur redimensionne encore la fenêtre, ajusterCanvasBitmapSize n'est pas appelé. Lorsque l'utilisateur s'arrête ou fait une pause pendant un moment, il est appelé. Allez-y, essayez-le:

Euh… je veux dire, ici.

Pourquoi 600 millisecondes? Je pense que ce n'est pas trop rapide ni trop lent, mais plus que tout, cela fonctionne bien avec l'entrée / la sortie d'animations en plein écran, ce qui sort du cadre de ce tutoriel..

Ceci conclut notre tutoriel pour aujourd'hui! Nous avons couvert tout le code spécifique à la toile dont nous avons besoin pour configurer notre toile. La prochaine fois, si Allah le veut bien, nous allons couvrir le code spécifique à WebGL et exécuter le shader. Jusque-là, merci d'avoir lu!

Références

  • Elément Canvas dans le brouillon des éditeurs W3C
  • Version w3c où le comportement d'initialisation de la toile est réellement documenté
  • Elément de toile dans la spécification live de whatwg