Introduction Ă  la programmation des shaders pour les mises en page


WebGL existe depuis longtemps, de nombreux articles ont été écrits sur les shaders, il y a une série de leçons. Mais pour la plupart, ils sont trop compliqués pour le concepteur de mise en page. C'est encore mieux de dire qu'ils couvrent de grandes quantités d'informations dont le développeur du moteur de jeu a besoin plutôt que le concepteur de mise en page. Ils commencent tout de suite par construire une scène complexe, un appareil photo, de la lumière ... Sur un site régulier pour créer une paire d'effets avec des photos, tout ce savoir est redondant. En conséquence, les gens font des structures architecturales très complexes et écrivent de longs, longs shaders pour des actions par essence très simples.


Tout cela a conduit à une introduction aux aspects du travail avec les shaders qui sont les plus susceptibles d'être utiles au concepteur de mise en page pour créer divers effets 2D avec des images sur le site. Bien sûr, ajusté pour le fait qu'ils sont eux-mêmes relativement rarement utilisés dans la conception d'interfaces. Nous allons créer un modèle de départ en JS pur sans bibliothèques tierces et considérer les idées de création d'effets populaires basés sur le décalage de pixels, qui sont difficiles à faire sur SVG, mais en même temps, ils sont facilement mis en œuvre à l'aide de shaders.


On suppose que le lecteur est déjà familier avec la canvas , décrit ce qu'est WebGL et a une connaissance minimale des mathématiques. Certains points seront décrits de manière simpliste, et non académique, afin de donner une compréhension pratique des technologies pour travailler avec eux, et non une théorie complète de leur cuisine intérieure ou des termes d'apprentissage. Il existe des livres intelligents pour cela.

Il convient de noter tout de suite que les éditeurs intégrés dans l'article de CodePen ont la capacité d'influencer les performances de ce qui y est fait. Donc, avant d'écrire un commentaire indiquant que quelque chose ralentit sur votre macbook, assurez-vous que le problème ne vient pas d'eux.


Idées principales


Qu'est-ce qu'un shader?


Qu'est-ce qu'un shader de fragment? Il s'agit essentiellement d'un petit programme. Il est exécuté pour chaque pixel de la anvas . Si nous avons une canvas taille 1000x500px, ce programme s'exécutera 500 000 fois, recevant à chaque fois comme paramètres d'entrée les coordonnées du pixel pour lequel il s'exécute actuellement. Tout cela se produit sur le GPU dans une variété de threads parallèles. Sur le processeur central, ces calculs prendraient beaucoup plus de temps.


Un vertex shader est également un programme, mais il n'est pas exécuté pour chaque pixel de la canvas , mais pour chaque sommet dans les formes à partir desquelles tout est construit dans un espace tridimensionnel. Également parallèle à tous les sommets. Par conséquent, l'entrée reçoit les coordonnées du sommet, pas le pixel.


Plus loin dans le cadre de notre tâche, les événements suivants se produisent:


  • Nous prenons un ensemble de coordonnĂ©es des sommets du rectangle, sur lequel la photographie sera ensuite "dessinĂ©e".
  • Un vertex shader pour chaque sommet considère son emplacement dans l'espace. Pour nous, cela se rĂ©sumera Ă  un cas spĂ©cial - un plan parallèle Ă  l'Ă©cran. Photos en 3D dont nous n'avons pas besoin. Une projection ultĂ©rieure sur le plan de l'Ă©cran ne peut rien dire.
  • De plus, pour chaque fragment visible, et dans notre contexte pour tous les fragments de pixel, un shader de fragment est exĂ©cutĂ©, il prend une photo et les coordonnĂ©es actuelles, compte quelque chose et donne la couleur pour ce pixel particulier.
  • S'il n'y avait pas de logique dans le fragment shader, le comportement de tout cela ressemblera Ă  la mĂ©thode drawImage() du canvas . Mais ensuite, nous ajoutons cette logique mĂŞme et obtenons beaucoup de choses intĂ©ressantes.

Il s'agit d'une description très simplifiée, mais il devrait être clair qui fait quoi.


Un peu de syntaxe


Les shaders sont écrits en GLSL - OpenGL Shading Language. Cette langue est très similaire à C. Il n'a pas de sens de décrire l'ensemble de la syntase et des méthodes standard ici, mais vous pouvez toujours utiliser la feuille de triche:


Spoiler avec photos





Chaque shader a une fonction principale, avec laquelle son exécution commence. Les paramètres d'entrée standard pour les shaders et la sortie des résultats de leur travail sont implémentés via des variables spéciales avec le préfixe gl_ . Ils sont réservés à l'avance et disponibles à l'intérieur de ces mêmes shaders. Ainsi, les coordonnées du sommet du vertex shader se trouvent dans la variable gl_Position , les coordonnées du fragment (pixel) du fragment shader se trouvent dans gl_FragCoord , etc. Vous pouvez toujours trouver la liste complète des variables spéciales disponibles dans la même feuille de triche.


Les principaux types de variables dans GLSL sont plutôt sans prétention - void , bool , int , float ... Si vous avez travaillé avec n'importe quel langage de type C, vous les avez déjà vus. Il existe d'autres types, en particulier des vecteurs de différentes dimensions - vec2 , vec3 , vec4 . Nous les utiliserons constamment pour les coordonnées et les couleurs. Les variables que nous pouvons créer sont de trois modifications importantes:


  • Uniforme - Des donnĂ©es globales dans tous les sens. PassĂ© de l'extĂ©rieur, le mĂŞme pour tous les appels de vertex et de fragment shaders.
  • Attribut - Ces donnĂ©es sont transfĂ©rĂ©es plus prĂ©cisĂ©ment et pour chaque appel de shader, elles peuvent ĂŞtre diffĂ©rentes.
  • Variable - NĂ©cessaire pour transfĂ©rer les donnĂ©es des vertex shaders aux fragments shaders.

Il est utile de préfixer u / a / v à toutes les variables dans les shaders pour faciliter la compréhension de la provenance des données.

Je pense qu'il vaut la peine de passer à un exemple pratique afin de regarder immédiatement tout cela en action et de ne pas charger votre mémoire.


Modèle de début de cuisson


Commençons par JS. Comme cela se produit généralement lorsque vous travaillez avec un canvas , nous en avons besoin et du contexte. Afin de ne pas charger l'exemple de code, nous allons créer des variables globales:


 const CANVAS = document.getElementById(IDs.canvas); const GL = canvas.getContext('webgl'); 

Ignorez le moment associé à la taille du canvas et son recalcul lors du redimensionnement de la fenêtre du navigateur. Ce code est inclus dans les exemples et dépend généralement du reste de la mise en page. Cela n'a aucun sens de se concentrer sur lui. Passons aux actions avec WebGL.


 function createProgram() { const shaders = getShaders(); PROGRAM = GL.createProgram(); GL.attachShader(PROGRAM, shaders.vertex); GL.attachShader(PROGRAM, shaders.fragment); GL.linkProgram(PROGRAM); GL.useProgram(PROGRAM); } 

Tout d'abord, nous compilons les shaders (il sera légèrement inférieur), créons un programme, y ajoutons nos deux shaders et faisons un lien. À ce stade, la compatibilité des shaders est vérifiée. Vous vous souvenez des variables variables qui sont passées du sommet au fragment? - En particulier, leurs ensembles sont vérifiés ici afin que plus tard dans le processus, il ne s'avère pas que quelque chose n'a pas été transmis ou transmis, mais pas du tout. Bien sûr, cette vérification ne révèlera pas d'erreurs logiques, je pense que cela est compréhensible.


Les coordonnées des sommets seront stockées dans un tableau tampon spécial et seront transmises en morceaux, un sommet, à chaque appel de shader. Ensuite, nous décrivons quelques détails pour travailler avec ces pièces. Tout d'abord, nous utiliserons les coordonnées du sommet dans le shader via la a_position attribut a_position . On peut l'appeler différemment, peu importe. Nous obtenons son emplacement (c'est quelque chose comme un pointeur en C, mais pas un pointeur, mais plutôt un numéro d'entité qui n'existe que dans le programme).


 const vertexPositionAttribute = GL.getAttribLocation(PROGRAM, 'a_position'); 

Ensuite, nous indiquons qu'un tableau avec des coordonnées sera passé à travers cette variable (dans le shader lui-même, nous le percevrons déjà comme un vecteur). WebGL déterminera indépendamment quelles coordonnées de quels points de nos formes doivent être transmises à quel appel de shader. Nous ne fixons que les paramètres du tableau vectoriel qui sera transmis: dimension - 2 (nous transmettrons les coordonnées (x,y) ), il est composé de nombres et n'est pas normalisé. Les derniers paramètres ne nous intéressent pas, nous laissons des zéros par défaut.


 GL.enableVertexAttribArray(vertexPositionAttribute); GL.vertexAttribPointer(vertexPositionAttribute, 2, GL.FLOAT, false, 0, 0); 

Créez maintenant le tampon lui-même avec les coordonnées des sommets de notre avion, sur lequel la photo sera ensuite affichée. Les coordonnées "2d" sont plus claires, mais pour nos tâches c'est la chose la plus importante.


 function createPlane() { GL.bindBuffer(GL.ARRAY_BUFFER, GL.createBuffer()); GL.bufferData( GL.ARRAY_BUFFER, new Float32Array([ -1, -1, -1, 1, 1, -1, 1, 1 ]), GL.STATIC_DRAW ); } 

Ce carré sera suffisant pour tous nos exemples. STATIC_DRAW signifie que le tampon est chargé une fois puis sera réutilisé. Nous ne téléchargerons plus rien.


Avant de passer aux shaders eux-mĂŞmes, regardons leur compilation:


 function getShaders() { return { vertex: compileShader( GL.VERTEX_SHADER, document.getElementById(IDs.shaders.vertex).textContent ), fragment: compileShader( GL.FRAGMENT_SHADER, document.getElementById(IDs.shaders.fragment).textContent ) }; } function compileShader(type, source) { const shader = GL.createShader(type); GL.shaderSource(shader, source); GL.compileShader(shader); return shader; } 

Nous obtenons le code du shader à partir des éléments de la page, créons un shader et compilons-le. En théorie, vous pouvez stocker le code du shader dans des fichiers séparés et le charger lors de l'assemblage sous forme de chaîne au bon endroit, mais CodePen ne fournit pas une telle opportunité pour des exemples. De nombreuses leçons suggèrent d'écrire du code directement sur la ligne dans JS, mais le langage ne le transforme pas en langage pratique. Bien sûr, il a un goût et une couleur ...


Si une erreur se produit pendant la compilation, le script continuera à s'exécuter, affichant quelques avertissements dans la console qui n'ont pas beaucoup de sens. Il est utile de consulter les journaux après la compilation afin de ne pas déranger votre cerveau sur ce qui n'y a pas été compilé:


 console.log(GL.getShaderInfoLog(shader)); 

WebGL fournit plusieurs options différentes pour suivre les problèmes lors de la compilation des shaders et de la création d'un programme, mais en pratique, il s'avère qu'en temps réel, nous ne pouvons rien résoudre de toute façon. Si souvent, nous serons guidés par l'idée "tombé - puis tombé" et nous ne chargerons pas le code avec un tas de vérifications supplémentaires.

Passons aux shaders eux-mĂŞmes


Comme nous n'aurons qu'un seul plan avec lequel nous n'allons rien faire, un simple vertex shader nous suffit, ce que nous ferons au tout début. Les principaux efforts seront concentrés sur les shaders de fragments et tous les exemples ultérieurs seront pertinents pour eux.


Essayez d'écrire du code de shader avec des noms de variables plus ou moins significatifs. Sur le réseau, vous trouverez des exemples où des fonctions avec des mathématiques vigoureuses pour 200 lignes de texte continu seront assemblées à partir de variables à une lettre, mais ce n'est pas parce que quelqu'un le fait que cela vaut la peine d'être répété. Une telle approche n'est pas une «spécificité de travailler avec GL», c'est un banal copier-coller des codes sources du siècle dernier écrit par des personnes qui, dans leur jeunesse, avaient des restrictions sur la longueur des noms de variables.

Tout d'abord, le vertex shader. Un vecteur a_position avec des coordonnées (x,y) sera transféré dans la variable d'attribut a_position , comme nous l'avons dit. Le shader doit renvoyer un vecteur de quatre valeurs (x,y,z,w) . Il ne bougera rien dans l'espace, donc sur l'axe z, nous mettons simplement tout à zéro et définissons la valeur de w sur l'unité standard. Si vous vous demandez pourquoi il y a quatre plutôt que trois coordonnées, vous pouvez utiliser la recherche de réseau pour des "coordonnées uniformes".


 <script id='vertex-shader' type='x-shader/x-vertex'> precision mediump float; attribute vec2 a_position; void main() { gl_Position = vec4(position, 0, 1); } </script> 

Le résultat du travail est enregistré dans une variable spéciale gl_Position . Les shaders n'ont pas de return au sens plein du terme, ils notent tous les résultats de leur travail dans des variables spécialement réservées à ces fins.


Notez le travail de précision pour le type de données float. Pour éviter certains des problèmes sur les appareils mobiles, la précision doit être pire que highp et doit être la même dans les deux shaders. Ceci est montré à titre d'exemple ici, mais c'est une bonne pratique sur les téléphones pour désactiver complètement cette beauté avec des shaders.

Le fragment shader renverra toujours la même couleur pour commencer. Notre carré occupera toute la canvas , donc en fait ici, nous définissons la couleur pour chaque pixel:


 <script id='fragment-shader' type='x-shader/x-fragment'> precision mediump float; #define GOLD vec4(1.0, 0.86, 0.6, 1.0) void main() { gl_FragColor = GOLD; } </script> 

Vous pouvez faire attention aux chiffres décrivant la couleur. Ceci est familier à tous les compositeurs RGBA, seulement normalisé. Les valeurs ne sont pas des entiers de 0 à 255, mais des fractions de 0 à 1. L'ordre est le même.


N'oubliez pas d'utiliser le préprocesseur pour toutes les constantes magiques dans les projets réels - cela rend le code plus compréhensible sans affecter les performances (la substitution, comme en C, se produit pendant la compilation).

Il convient de noter un autre point sur le préprocesseur:


L'utilisation de vérifications constantes #ifdef GL_ES dans diverses leçons est dénuée de sens pratique. dans notre navigateur aujourd'hui, aucune autre option GL n'existe simplement.

Mais il est temps de regarder déjà le résultat:



Le carré doré indique que les shaders fonctionnent comme prévu. Il est logique de jouer avec eux un peu avant de passer aux photos.


Vecteurs de gradient et de transformation


En règle générale, les didacticiels WebGL commencent par dessiner des dégradés. Cela n'a pas beaucoup de sens pratique, mais il sera utile de noter quelques points.


 void main() { gl_FragColor = vec4(gl_FragCoord.zxy / 500.0, 1.0); } 

Dans cet exemple, nous utilisons les coordonnées du pixel actuel comme couleur. Vous le verrez souvent dans des exemples sur le net. Les deux sont des vecteurs. Donc, personne ne prend la peine de tout mélanger en un tas. Les évangélistes de TypeScript devraient avoir une attaque ici. Un point important est de savoir comment nous n'obtenons qu'une partie des coordonnées du vecteur. Propriétés .x , .y , .z , .xy , .zy , .xyz , .zyx , .xyzw , etc. dans différentes séquences vous permettent d'extraire les éléments d'un vecteur dans un certain ordre sous la forme d'un autre vecteur. Très commodément mis en œuvre. De plus, un vecteur de dimension supérieure peut être créé à partir d'un vecteur de dimension inférieure en ajoutant les valeurs manquantes, comme nous l'avons fait.


Indiquez toujours explicitement la partie fractionnaire des nombres. Il n'y a pas de conversion automatique int -> float ici.


Les uniformes et le temps qui passe


Le prochain exemple utile est l'utilisation d'uniformes. Il s'agit des données les plus courantes pour tous les appels de shader. Nous obtenons leur emplacement de la même manière que pour les variables d'attribut, par exemple:


 GL.getUniformLocation(PROGRAM, 'u_time') 

Ensuite, nous pouvons leur attribuer des valeurs avant chaque image. Comme pour les vecteurs, il existe de nombreuses méthodes similaires ici, à commencer par le mot uniform , puis vient la dimension de la variable (1 pour les nombres, 2, 3 ou 4 pour les vecteurs) et le type (f - float, i - int, v - vector) .


 function draw(timeStamp) { GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_time'), timeStamp / 1000.0); GL.drawArrays(GL.TRIANGLE_STRIP, 0, 4); window.requestAnimationFrame(draw); } 

En fait, nous n'avons pas toujours besoin de 60fps dans les interfaces. Il est tout à fait possible d'ajouter un ralentissement à requestAnimationFrame et de réduire la fréquence de redessin des trames.

Par exemple, nous changerons la couleur de remplissage. Dans les shaders, toutes les fonctions mathématiques de base sont disponibles - sin , cos , tan , asin , acos , atan , pow , exp , log , sqrt , abs et autres. Nous en utiliserons deux.


 uniform float u_time; void main() { gl_FragColor = vec4( abs(sin(u_time)), abs(sin(u_time * 3.0)), abs(sin(u_time * 5.0)), 1.0); } 

Le temps dans de telles animations est un concept relatif. Ici, nous utilisons les valeurs fournies par requestAnimationFrame , mais nous pouvons créer notre propre "temps". L'idée est que si certains paramètres sont décrits par une fonction du temps, alors nous pouvons tourner le temps dans la direction opposée, le ralentir, l'accélérer ou revenir à son état d'origine. Cela peut être très utile.



Mais assez d'exemples abstraits, passons Ă  l'utilisation d'images.


Chargement d'une image dans une texture


Pour utiliser l'image, nous devons créer une texture, qui sera ensuite rendue sur notre avion. Pour commencer, chargez l'image elle-même:


 function createTexture() { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => { // .... }; image.src = 'example.jpg'; } 

Après son chargement, créez une texture et indiquez qu'elle ira au numéro 0. Dans WebGL, il peut y avoir de nombreuses textures en même temps et nous devons indiquer explicitement à quelles commandes suivantes se rapportera. Dans nos exemples, il n'y aura qu'une seule texture, mais nous indiquons toujours explicitement qu'elle sera nulle.


 const texture = GL.createTexture(); GL.activeTexture(GL.TEXTURE0); GL.bindTexture(GL.TEXTURE_2D, texture); 

Reste à ajouter une photo. Nous disons également immédiatement qu'il doit être inversé le long de l'axe Y, car dans WebGL, l'axe est à l'envers:


 GL.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, true); GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGB, GL.RGB, GL.UNSIGNED_BYTE, image); 

En théorie, la texture doit être carrée. Plus précisément, ils devraient même avoir une taille égale à la puissance de deux - 32px, 64px, 128px, etc. Mais nous comprenons tous que personne ne traitera les photos et qu'elles seront à chaque fois dans des proportions différentes. Cela entraînera des erreurs même si la taille de la canvas correspond parfaitement à la texture. Par conséquent, nous remplissons tout l'espace jusqu'aux bords de l'avion avec les pixels extrêmes de l'image. C'est une pratique standard, même si cela semble un peu béquille.


 GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR); 

Reste à transférer la texture aux shaders. Ces données sont communes à tous, nous utilisons donc le modificateur uniform .


 GL.uniform1i(GL.getUniformLocation(PROGRAM, 'u_texture'), 0); 

Nous pouvons maintenant utiliser les couleurs de la texture dans le fragment shader. Mais nous voulons également que l'image occupe toute la canvas . Si l'image et la canvas ont les mêmes proportions, cette tâche devient triviale. Tout d'abord, nous transférons la taille de la canvas aux shaders (cela doit être fait à chaque fois que vous changez sa taille):


 GL.uniform1f(GL.getUniformLocation(PROGRAM, 'u_canvas_size'), Math.max(CANVAS.height, CANVAS.width)); 

Et divisez-y les coordonnées:


 uniform sampler2D u_texture; uniform float u_canvas_size; void main() { gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size); } 


À ce stade, vous pouvez faire une pause et préparer du thé. Nous avons fait tout le travail préparatoire et passons à la création d'effets divers.


Les effets


En créant divers effets, l'intuition et l'expérimentation jouent un rôle important. Souvent, vous pouvez remplacer un algorithme complexe par quelque chose de complètement simple et donner un résultat similaire. L'utilisateur final ne remarquera pas la différence, mais nous accélérons le travail et simplifions le support. WebGL ne fournit pas d'outils sensibles pour le débogage des shaders, il est donc avantageux pour nous d'avoir de petits morceaux de code qui peuvent tenir dans la tête dans son ensemble.


Moins de code signifie moins de problèmes. Et c'est plus facile à lire. Vérifiez toujours les shaders trouvés sur le réseau pour les actions inutiles. Il arrive que vous puissiez supprimer la moitié du code et rien ne changera.

Jouons un peu avec le shader. La plupart de nos effets seront basés sur le fait que nous retournons la couleur non pas du pixel sur la texture qui devrait être à cet endroit, mais certains des voisins. Il est utile d'essayer d'ajouter aux coordonnées le résultat d'une fonction standard des coordonnées. Le temps sera également utile à utiliser - donc le résultat de l'exécution sera plus facile à suivre, et à la fin, nous ferons toujours des effets animés. Essayons d'utiliser le sinus:


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y)); 

Le résultat est étrange. Évidemment, tout bouge avec trop d'amplitude. Divisez tout par un certain nombre:


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y) / 250.0); 

Déjà mieux. Maintenant, il est clair que nous avons eu un peu d'excitation. En théorie, pour augmenter chaque vague, nous devons diviser l'argument sinus - la coordonnée. Faisons-le:


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + sin(u_time + gl_FragCoord.y / 30.0) / 250.0); 


Des effets similaires s'accompagnent souvent de la sélection de coefficients. Cela se fait à l'œil nu. Comme pour la cuisine, au début, il sera difficile à deviner, mais ensuite cela se fera tout seul. L'essentiel est de comprendre au moins approximativement ce que tel ou tel coefficient dans la formule résultante affecte. Une fois les coefficients sélectionnés, il est logique de les mettre dans des macros (comme dans le premier exemple) et de donner des noms significatifs.


Miroir tordu, vélos et expériences


Penser, c'est bien. Oui, il existe des algorithmes prêts à l'emploi pour résoudre certains problèmes que nous pouvons simplement prendre et utiliser. , .


, " ", . Que faire?


, , ? . , rand() - . , , , , . . . , . . . -, . . , , , . , "":


 float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } 

, , , NVIDIA ATI . , .


, , :


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy) / 100.0); 

:


 gl_FragColor = texture2D(u_texture, gl_FragCoord.xy / u_canvas_size + rand(gl_FragCoord.xy + vec2(sin(u_time))) / 250.0); 

, , :



, . , , . — . Comment faire . .


0 1, - . 5 — . , .


 vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; gl_FragColor = texture2D(u_texture, texture_coord + rand(floor(texture_coord * 5.0) + vec2(sin(u_time))) / 100.0); 

, - . - . , , . ?


, , , - . , . , .. -. , . . , , . .


sin cos , . . .


 gl_FragColor = texture2D(u_texture, texture_coord + vec2( noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0)); 

. fract . 1 1 — :


 float noise(vec2 position) { vec2 block_position = floor(position); float top_left_value = rand(block_position); float top_right_value = rand(block_position + vec2(1.0, 0.0)); float bottom_left_value = rand(block_position + vec2(0.0, 1.0)); float bottom_right_value = rand(block_position + vec2(1.0, 1.0)); vec2 computed_value = fract(position); // ... } 

. WebGL smoothstep , :


 vec2 computed_value = smoothstep(0.0, 1.0, fract(position)); 

, . , X :


 return computed_value.x; 


… , , ...


- , , ... .

y — , . ?


 return length(computed_value); 


.


. 0.5 — .


 return mix(top_left_value, top_right_value, computed_value.x) + (bottom_left_value - top_left_value) * computed_value.y * (1.0 - computed_value.x) + (bottom_right_value - top_right_value) * computed_value.x * computed_value.y - 0.5; 

:



, , , .



, , . - .


uniform-, . 0 1, 0 — , 1 — .


 uniform float u_intensity; 

:


 gl_FragColor = texture2D(u_texture, texture_coord + vec2(noise(texture_coord * 10.0 + sin(u_time + texture_coord.x * 5.0)) / 10.0, noise(texture_coord * 10.0 + cos(u_time + texture_coord.y * 5.0)) / 10.0) * u_intensity); 

, .



( 0 1), .


, , , . — requestAnimationFrame. , FPS.


, . uniform-.


 document.addEventListener('mousemove', (e) => { let rect = CANVAS.getBoundingClientRect(); MOUSE_POSITION = [ e.clientX - rect.left, rect.height - (e.clientY - rect.top) ]; GL.uniform2fv(GL.getUniformLocation(PROGRAM, 'u_mouse_position'), MOUSE_POSITION); }); 

, . — , .


 void main() { vec2 texture_coord = gl_FragCoord.xy / u_canvas_size; vec2 direction = u_mouse_position / u_canvas_size - texture_coord; float dist = distance(gl_FragCoord.xy, u_mouse_position) / u_canvas_size; if (dist < 0.4) { gl_FragColor = texture2D(u_texture, texture_coord + u_intensity * direction * dist * 1.2 ); } else { gl_FragColor = texture2D(u_texture, texture_coord); } } 

- . .


. , .



. Glitch- , SVG. . — . ? — , , , .


 float random_value = rand(vec2(texture_coord.y, u_time)); if (random_value < 0.05) { gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 5.0, texture_coord.y)); } else { gl_FragColor = texture2D(u_texture, texture_coord); } 

" ?" — , . .

. — , .


 float random_value = rand(vec2(floor(texture_coord.y * 20.0), u_time)); 

. , :


 gl_FragColor = texture2D(u_texture, vec2(texture_coord.x + random_value / 4.0, texture_coord.y)) + vec4(vec3(random_value), 1.0); 

. — . , — .r , .g , .b , .rg , .rb , .rgb , .bgr , ... .


:


 float random_value = u_intensity * rand(vec2(floor(texture_coord.y * 20.0), u_time)); 


Quel est le résultat?


, , . , , — .

Source: https://habr.com/ru/post/fr420847/


All Articles