Mise en route dans WebGL, partie 4 WebGL Viewport and Clipping

Dans les parties précédentes de cette série, nous avons beaucoup appris sur les shaders, l’élément canvas, les contextes WebGL et la manière dont le navigateur alpha-compose notre tampon de couleur par rapport au reste des éléments de page.. 

Dans cet article, nous continuons à écrire notre code standard WebGL. Nous préparons toujours notre toile pour le dessin WebGL, en tenant compte cette fois des fenêtres de visualisation et des primitives.. 

Cet article fait partie de la série "Mise en route dans WebGL". Si vous n'avez pas lu les parties précédentes, je vous recommande de les lire en premier:

  1. Introduction aux Shaders
  2. L'élément de toile pour notre premier shader
  3. WebGL Contexte et Clear

résumer

  • Dans le premier article de cette série, nous avons écrit un shader simple qui dessine un dégradé coloré et le fade légèrement.
  • Dans le deuxième article de cette série, nous avons commencé à travailler à l’utilisation de ce shader dans une page Web. En prenant de petits pas, nous avons expliqué le fond nécessaire de l’élément canvas.
  • Dans le troisième article, nous avons acquis notre contexte WebGL et l'avons utilisé pour effacer le tampon de couleur. Nous avons également expliqué comment la toile s’intègre aux autres éléments de la page..

Dans cet article, nous continuons là où nous sommes partis, en découvrant cette fois les fenêtres WebGL et leur incidence sur l'écrêtage des primitives..

Suivant dans cette série, si Allah le veut, nous allons compiler notre programme shader, en savoir plus sur les tampons WebGL, dessiner des primitives et exécuter le programme shader que nous avons écrit dans le premier article. Presque là!

Taille de la toile

C'est notre code jusqu'à présent:

Notez que j'ai restauré la couleur de fond CSS en noir et la couleur transparente en rouge opaque.

Grâce à notre CSS, nous avons un canevas qui s’étend pour remplir notre page Web, mais le tampon de dessin 1x1 sous-jacent n’est guère utile. Nous devons définir une taille appropriée pour notre tampon de dessin. Si le tampon est plus petit que le canevas, nous n'utilisons pas pleinement la résolution du périphérique et sommes soumis à des artefacts de dimensionnement (comme indiqué dans un article précédent). Si le tampon est plus grand que la toile, la qualité en profite beaucoup! C'est à cause de l'anti-aliasing de super échantillonnage que le navigateur applique pour réduire la taille de la mémoire tampon avant qu'elle ne soit transmise au compositeur.. 

Cependant, la performance prend un bon coup. Si l'anti-aliasing est souhaité, il est préférable de le réaliser grâce à MSAA (anti-aliasing multi-échantillonnage) et au filtrage de texture. Pour le moment, nous devrions viser un tampon de dessin de la même taille que notre toile pour tirer pleinement parti de la résolution de l'appareil et éviter toute mise à l'échelle..

Pour ce faire, nous emprunterons le ajusterCanvasBitmapSize de la partie 2 (avec quelques modifications):

function adjustDrawingBufferSize () var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; // Vérification individuelle de la largeur et de la hauteur pour éviter deux opérations de redimensionnement si // une seule était nécessaire. Depuis que cette fonction a été appelée, // au moins une d'entre elles a été modifiée, si (canvas.width! = Math.floor (canvas.clientWidth * pixelRatio)) canvas.width = pixelRatio * canvas.clientWidth; if (canvas.height! = Math.floor (canvas.clientHeight * pixelRatio)) canvas.height = pixelRatio * canvas.clientHeight; // Définit les nouvelles dimensions de la fenêtre d'affichage, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight); 

Changements:

  • Nous avons utilisé clientWidth et clientHeight au lieu de offsetWidth et offsetHeight. Les dernières incluent les bordures de la toile, elles ne sont donc peut-être pas exactement ce que nous recherchons. clientWidth et clientHeight sont plus adaptés à cet effet. Ma faute!
  • ajusterDrawingBufferSize est maintenant programmé pour s'exécuter que si des modifications ont eu lieu. Par conséquent, nous n'avons pas besoin de vérifier explicitement et d'abandonner si rien n'a changé.
  • Nous n'avons plus besoin d'appeler drawScene chaque fois que la taille change. Nous veillerons à ce qu'il soit appelé régulièrement ailleurs.
  • UNE glContext.viewport apparu! Il a sa propre section, alors laissez-le passer pour l'instant!

Nous emprunterons également la fonction de limitation des événements de redimensionnement, onWindowResize (avec quelques modifications aussi):

function onCanvasResize () // Calcule les dimensions en pixels physiques, var canvas = glContext.canvas; var pixelRatio = window.devicePixelRatio? window.devicePixelRatio: 1.0; var physicalWidth = Math.floor (canvas.clientWidth * pixelRatio); var physicalHeight = Math.floor (canvas.clientHeight * pixelRatio); // Abandonne si rien n'a changé, if ((onCanvasResize.targetWidth == physicalWidth) && (onCanvasResize.targetHeight == physicalHeight)) return;  // Définissez les nouvelles dimensions requises, onCanvasResize.targetWidth = physicalWidth; onCanvasResize.targetHeight = physicalHeight; // Attend que l'inondation des événements de redimensionnement soit réglée, if (onCanvasResize.timeoutId) window.clearTimeout (onCanvasResize.timeoutId); onCanvasResize.timeoutId = window.setTimeout (adjustDrawingBufferSize, 600); 

Changements:

  • La neige onCanvasResize au lieu de onWindowResize. Dans notre exemple, il est correct de supposer que la taille de la toile ne change que lorsque la taille de la fenêtre est modifiée, mais dans la réalité, notre toile peut faire partie d'une page contenant d'autres éléments, des éléments redimensionnables affectant la taille de la toile..
  • Au lieu d'écouter les événements liés aux modifications de la taille de la zone de travail, nous allons simplement vérifier les modifications chaque fois que nous allons redessiner le contenu de la zone de travail. En d'autres termes, onCanvasResize est appelé si des changements se sont produits ou non, il est donc nécessaire d'abandonner lorsque rien n'a changé.

Maintenant, appelons onCanvasResize de drawScene:

function drawScene () // Traite les modifications de taille de la toile, onCanvasResize (); // Efface le tampon de couleur, glContext.clear (glContext.COLOR_BUFFER_BIT); 

J'ai mentionné que nous appellerons drawScene régulièrement. Cela signifie que nous sommes rendu en continu, non seulement lorsque des changements se produisent (alias quand sale). Dessiner consomme en permanence plus d'énergie que dessiner uniquement lorsqu'il est sale, mais cela nous évite d'avoir à suivre lorsque le contenu doit être mis à jour.. 

Mais cela vaut la peine d’envisager si vous envisagez de créer une application fonctionnant sur de longues périodes, telle que des fonds d’écran et des lanceurs (mais vous ne feriez pas cela dans WebGL pour commencer, et vous?). Par conséquent, pour ce tutoriel, le rendu sera continu. Le moyen le plus simple de le faire est de planifier la ré-exécution drawScene de l'intérieur de lui-même:

function drawScene () … stuff… // Demande à nouveau de dessiner l'image suivante, window.requestAnimationFrame (drawScene); 

Non, nous n'avons pas utilisé setInterval ou setTimeout pour ça. requestAnimationFrame indique au navigateur que vous souhaitez effectuer une animation et demande à être appelé drawScene avant le prochain repeindre. C'est le plus approprié pour les animations parmi les trois, car:

  • Les horaires de setInterval et setTimeout ne sont souvent pas honorés avec précision - ils sont basés sur le meilleur effort. Avec requestAnimationFrame, le minutage correspond généralement au taux de rafraîchissement de l'affichage.
  • Si le code planifié contient des modifications dans la disposition du contenu de la page, setInterval et setTimeout pourrait causer la mise en page (mais ce n'est pas notre cas). requestAnimationFrame prend soin de cela et ne déclenche pas de cycles inutiles de refusion et de repeinte.
  • En utilisant requestAnimationFrame permet au navigateur de décider à quelle fréquence appeler notre fonction d’animation / dessin. Cela signifie qu'il peut l'étouffer si la page / iframe devient cachée ou inactive, ce qui signifie une plus grande autonomie de la batterie pour les appareils mobiles. Cela arrive aussi avec setInterval et setTimeout dans plusieurs navigateurs (Firefox, Chrome) - prétendez que vous ne savez pas!

Retour à notre page. Maintenant, notre mécanisme de redimensionnement est terminé:

  • drawScene est appelé régulièrement, et il appelle onCanvasResize à chaque fois.
  • onCanvasResize vérifie la taille de la toile et, si des modifications sont apportées, programme ajusterDrawingBufferSize appeler, ou le reporter s'il était déjà programmé.
  • ajusterDrawingBufferSize change réellement la taille de la mémoire tampon de dessin et définit les nouvelles dimensions de la fenêtre d'affichage.

Tout mettre ensemble:

J'ai ajouté une alerte qui apparaît chaque fois que le tampon de dessin est redimensionné. Vous souhaiterez peut-être ouvrir l'exemple ci-dessus dans un nouvel onglet et redimensionner la fenêtre ou modifier l'orientation du périphérique pour le tester. Notez qu'il ne redimensionne que lorsque vous avez arrêté de le redimensionner pendant 0,6 seconde (comme si vous mesuriez cela!).

Une dernière remarque avant de terminer ce redimensionnement du tampon. Il existe des limites à la taille maximale d'un tampon de dessin. Celles-ci dépendent du matériel et du navigateur utilisés. Si vous êtes:

  • en utilisant un smartphone, ou
  • un écran ridiculement haute résolution, ou
  • avoir plusieurs moniteurs / espaces de travail / bureaux virtuels définis, ou
  • utilisez un smartphone, ou
  • visualisez votre page depuis un très grand iframe (qui est le moyen le plus facile de le tester), ou
  • utilise un smartphone

il se peut que la toile soit redimensionnée au-delà des limites possibles. Dans ce cas, la largeur et la hauteur de la zone de travail n'indiqueront aucune objection, mais la taille réelle de la mémoire tampon sera limitée au maximum possible. Vous pouvez obtenir la taille réelle du tampon en utilisant les membres en lecture seule. glContext.drawingBufferWidth et glContext.drawingBufferHeight, que j'ai utilisé pour construire l'alerte. 

En dehors de cela, tout devrait bien fonctionner… sauf que sur certains navigateurs, une partie de ce que vous dessinez (ou la totalité) risque de ne jamais se retrouver à l'écran! Dans ce cas, l’ajout de ces deux lignes à ajusterDrawingBufferSize après le redimensionnement pourrait être utile:

if (canvas.width! = glContext.drawingBufferWidth) canvas.width = glContext.drawingBufferWidth; if (canvas.height! = glContext.drawingBufferHeight) canvas.height = glContext.drawingBufferHeight;

Nous sommes maintenant de retour là où les choses ont du sens. Mais notez que serrer à dessinBufferWidth et dessinBuffeurHauteur peut ne pas être la meilleure action. Vous voudrez peut-être envisager de conserver un certain rapport d'aspect.

Maintenant faisons un peu de dessin!

Viewport et Scissoring 

// Définit les nouvelles dimensions de la fenêtre d'affichage, glContext.viewport (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight);

Rappelez-vous dans le premier article de cette série lorsque j'ai mentionné que dans le shader, WebGL utilise les coordonnées (-1, -1) pour représenter le coin inférieur gauche de votre fenêtre d'affichage, et (1, 1) représenter le coin supérieur droit? C'est tout. fenêtre d'affichage indique à WebGL à quel rectangle de notre tampon de dessin doit être associé (-1, -1) et (1, 1). C'est juste une transformation, rien de plus. Cela n'affecte pas les tampons ou quoi que ce soit.

J'ai également dit que tout ce qui se trouvait en dehors des dimensions de la fenêtre d'affichage était ignoré et non dessiné. C'est presque entièrement vrai, mais il y a une différence. L'astuce réside dans les mots "dessiné" et "à l'extérieur". Qu'est-ce qui compte vraiment comme dessin ou comme dehors?

// Limite le dessin à la moitié gauche du canevas, glContext.viewport (0, 0, glContext.drawingBufferWidth / 2, glContext.drawingBufferHeight);

Cette ligne limite notre rectangle de fenêtre d'affichage à la moitié gauche de la toile. Je l'ai ajouté à la drawScene une fonction. Nous n'avons généralement pas besoin d'appeler fenêtre d'affichage sauf lorsque la taille de la toile change et que nous l’avons fait ici. Vous pouvez supprimer celui de la fonction de redimensionnement, mais je vais le laisser. En pratique, essayez de réduire autant que possible vos appels WebGL. Voyons ce que cette ligne fait:

Oh, clear (glContext.COLOR_BUFFER_BIT) totalement ignoré nos paramètres de viewport! C'est ce que ça fait, duh! fenêtre d'affichage n'a aucun effet sur les appels clairs du tout. Les dimensions de la fenêtre d'affichage affectent le découpage des primitives. N'oubliez pas dans le premier article, j'ai dit que nous ne pouvons dessiner que des points, des lignes et des triangles dans WebGL. Celles-ci seront coupées dans les dimensions de la fenêtre comme vous le pensez… sauf les points. 

Points

Un point est entièrement dessiné si son centre se trouve dans les dimensions de la fenêtre et sera entièrement omis si son centre se trouve en dehors de celles-ci. Si un point est assez gros, son centre peut rester dans la fenêtre d'affichage, tandis qu'une partie s'étend vers l'extérieur. Cette partie extensible doit être dessinée. C'est comme ça que ça devrait être, mais ce n'est pas nécessairement le cas dans la pratique:

Vous devriez voir quelque chose qui ressemble à ceci si votre navigateur, votre appareil et vos pilotes respectent la norme (à cet égard):

La taille des points dépend de la résolution réelle de votre appareil. Ne faites donc pas attention à la différence de taille. Faites juste attention à la quantité de points qui apparaissent. Dans l'exemple ci-dessus, j'ai défini la zone de la fenêtre d'affichage sur la partie centrale de la zone de dessin (la zone avec le dégradé), mais étant donné que les centres des points sont toujours dans la fenêtre, ils doivent être entièrement dessinés (les éléments verts). Si tel est le cas dans votre navigateur, alors super! Mais tous les utilisateurs n'ont pas cette chance. Certains utilisateurs verront les parties extérieures coupées, comme ceci:

La plupart du temps, cela ne fait vraiment aucune différence. Si la fenêtre doit couvrir la totalité de la toile, nous ne nous soucions pas de savoir si les surfaces extérieures seront découpées ou non. Mais il importerait que ces points bougent doucement en dehors de la toile, puis ils disparaissent soudainement parce que leurs centres sont sortis:

(Presse Résultat pour redémarrer l'animation.)

Encore une fois, ce comportement n’est pas nécessairement ce que vous voyez. Selon l’histoire, les appareils Nvidia ne couperont pas les points lorsque leurs centres iront à l’extérieur, mais couperont les parties qui s’y trouvent Sur ma machine (utilisant un périphérique AMD), Chrome, Firefox et Edge se comportent de la même manière lorsqu'ils sont exécutés sous Windows. Cependant, sur la même machine, Chrome et Firefox coupent les points et ne les coupent pas lorsqu'ils sont exécutés sous Linux. Sur mon téléphone Android, Chrome et Firefox vont à la fois couper et couper les points.!

Scissoring

Il semble que dessiner des points est gênant. Pourquoi même se soucier? Parce que les points ne doivent pas nécessairement être circulaires. Ce sont des régions rectangulaires alignées sur les axes. C'est le fragment shader qui décide comment les dessiner. Ils peuvent être texturés, auquel cas ils sont connus comme point-sprites. Celles-ci peuvent être utilisées pour créer plein de choses, comme des mosaïques de tuiles et des effets de particules, dans lesquelles elles sont vraiment pratiques, car il suffit de passer un sommet par sprite (le centre), au lieu de quatre dans le cas d'une bande triangulaire. . Réduire la quantité de données transférées de la CPU au GPU peut s'avérer très utile dans les scènes complexes. Dans WebGL 2, nous pouvons utiliser instanciation géométrique (qui a ses propres prises), mais nous n'y sommes pas encore.

Alors, comment traitons-nous les coupures de points? Pour couper les parties extérieures, nous utilisons scissoring:

function initializeState () … // Activer le scissoring, glContext.enable (glContext.SCISSOR_TEST); 

Le cisaillement est maintenant activé, alors voici comment définir la région à découper:

function adjustDrawingBufferSize () … // Définit la nouvelle boîte à ciseaux, glContext.scissor (xInPixels, yInPixels, widthInPixels, heightInPixels); 

Alors que les positions des primitives sont relatives aux dimensions de la fenêtre, les dimensions de la boîte à ciseaux ne le sont pas. Ils spécifient un rectangle brut dans le tampon de dessin, sans se soucier de savoir s'il recouvre ou non la fenêtre. Dans l'exemple suivant, j'ai défini les fenêtres Viewport et Scissor Box sur la partie centrale de la zone de dessin:

(Presse Résultat pour redémarrer l'animation.)

Notez que le test des ciseaux est une opération par échantillon qui supprime les fragments qui se trouvent en dehors de la zone de test. Cela n'a rien à voir avec ce qui est dessiné; il ne fait que jeter les fragments qui sortent. Même clair respecte le test aux ciseaux! C'est pourquoi la couleur bleue (la couleur claire) est liée à la boîte à ciseaux. Il ne reste plus qu'à empêcher les points de disparaître lorsque leurs centres sortiront. Pour ce faire, je vais m'assurer que la fenêtre d'affichage est plus grande que la boîte à ciseaux, avec une marge permettant aux points d'être encore dessinés jusqu'à ce qu'ils soient complètement à l'extérieur de la boîte à ciseaux:

(Presse Résultat pour redémarrer l'animation.)

Yay! Cela devrait bien fonctionner partout. Mais dans le code ci-dessus, nous n’utilisions qu’une partie de la toile pour faire le dessin. Et si nous voulions occuper toute la toile? Cela ne fait vraiment aucune différence. La fenêtre d'affichage peut être plus grande que la mémoire tampon de dessin sans problème (ignorez les discours de Firefox à ce sujet dans la sortie de la console):

function adjustDrawingBufferSize () … // Définit les nouvelles dimensions de la fenêtre d'affichage, var pointSize = 150; glContext.viewport (-0.5 * pointSize, -0.5 * pointSize, glContext.drawingBufferWidth + pointSize, glContext.drawingBufferHeight + pointSize); // Définit la nouvelle boîte à ciseaux, glContext.scissor (0, 0, glContext.drawingBufferWidth, glContext.drawingBufferHeight); 

Voir:

Soyez conscient de la taille de la fenêtre d'affichage. Même si la fenêtre d'affichage n'est qu'une transformation qui ne vous coûte aucune ressource, vous ne voulez pas vous fier uniquement à l'écrêtage par échantillon. Pensez à ne modifier la fenêtre d'affichage qu'en cas de nécessité et restaurez-la pour le reste du dessin. Et rappelez-vous que la fenêtre d'affichage affecte la position des primitives à l'écran, alors tenez compte de cela aussi.

C'est tout pour le moment! La prochaine fois, mettons toute la taille, la fenêtre d'affichage et les éléments coupés derrière nous. Dessiner des triangles! Merci d'avoir lu jusqu'à présent, et j'espère que cela a été utile.

Références

  • Opérations qui écrivent dans le tampon de dessin dans la spécification WebGL
  • Comment le navigateur sous-échantillonne le tampon de dessin sur MDN
  • Contre-modèles de la fenêtre WebGL sur les fondamentaux de WebGL
  • Timers en HTML
  • requestAnimationFrame sur MDN
  • Wiki de test des ciseaux WebGL