📜 ⬆️ ⬇️

We apply Voronoi mosaic, pixelation and geometric masks in shaders to decorate the site

image

This article is a logical continuation of the introduction to programming shaders for web designers . In it, we made a template for creating various two-dimensional effects with photos using shaders and looked at a couple of examples. In this article, we will add a couple more textures, apply Voronoi splitting to create a mosaic of them, talk about creating different masks in shaders, pixelation, and also address some of the problems of the ancient GLSL syntax that still exists in our browsers.


Just as last time, there will be a minimum of theory and a maximum of practice and reasoning in a mundane everyday language. Beginners will find here a sequence of actions with tips and helpful comments, and experienced front-tenders will probably find a couple of ideas for inspiration.


A survey in a previous article showed that the topic of WebGL effects for sites may be of interest not only to the layout designers, but also to our colleagues of other specializations. In order not to be puzzled by the latest ES chips, we intentionally confine ourselves to more traditional syntactic constructions that everyone can understand. And again I draw readers' attention to the fact that CodePen’s built-in editors affect the performance of what is being done in them.


But let's start ...


Template for working with shaders


For those who have not read the previous article - we have made the following template for working with shaders:



It creates a plane (in our case, a square) on which the picture-texture is drawn. No extra dependencies and a very simple vertex shader. Then we developed this pattern, but now we will start from the moment when there is no logic in the fragment shader yet.


Mosaic


Mosaic is a plane broken into small areas, where each of the areas is filled with a certain color or, as in our case, a texture. How can we even break our plane into pieces? Obviously, you can break it into rectangles. But this is so easy to do with the help of SVG, to drag WebGL into this task and to lay everything out of the blue to absolutely nothing.


For a mosaic to be interesting, there must be different fragments in it, both in shape and in size. There is one very simple, but at the same time very entertaining approach to building such a partition. It is known as the Voronoi mosaic or the Dirichlet partition, and on Wikipedia it is written that Descartes used something similar back in the distant XVII century. The idea is something like this:



Probably better show this process with a practical example. There are different algorithms for generating this partition, but we will act on the forehead, because calculating something for each point on the plane is just the task for the shader. First we need to make a set of random points. In order not to load the code of the examples we will make a global variable for them.


function createPoints() { for (let i = 0; i < NUMBER_OF_POINTS; i++) { POINTS.push([Math.random(), Math.random()]); } } 

Now we need to transfer them to the shaders. The data is global, so we will use the uniform modifier. But there is one subtle point: we simply cannot pass on an array. It would seem that the 21st century is in the courtyard, but still nothing will come of it. As a result, you have to transfer an array of points one by one.


 for (let i = 0; i < NUMBER_OF_POINTS; i++) { GL.uniform2fv(GL.getUniformLocation(PROGRAM, 'u_points[' + i + ']'), POINTS[i]); } 

Today we will not once encounter such problems of inconsistency between what is expected and what is in real browsers. Typically, WebGL lessons use THREE.js and this library hides some of the dirt in itself, as jQuery once did in its tasks, but if it is removed, the reality hurts the brain.


In the fragment shader, we have an array variable for points. We can create only fixed-length arrays. Let it start with 10 points:


 #define NUMBER_OF_POINTS 10 uniform vec2 u_points[NUMBER_OF_POINTS]; 

Make sure in the performance of all this by drawing circles on the ground points. Such drawing of various geometric primitives is often used for debugging - they can be clearly seen and you can immediately understand what is and where it is going.


Use the "drawing" circles, direct and other landmarks for invisible objects on which animations are built. This will give obvious clues about how they work, especially if the algorithms are difficult to quickly understand without prior preparation. Then all this can be commented out and left for colleagues - they will say thank you.

 for (int i = 0; i < NUMBER_OF_POINTS; i++) { if (distance(texture_coord, u_points[i]) < 0.02) { gl_FragColor = WHITE; break; } } 


Good. Let's also add a little movement to the points. Let them move in a circle to begin with, we will return to this issue later. The coefficients are also set by the eye, just to slow down their movement a bit and reduce the amplitude of oscillations.


 function movePoints(timeStamp) { if (timeStamp) { for (let i = 0; i < NUMBER_OF_POINTS; i++) { POINTS[i][0] += Math.sin(i * timeStamp / 5000.0) / 500.0; POINTS[i][1] += Math.cos(i * timeStamp / 5000.0) / 500.0; } } } 

We return to the shader. For future experiments, we will need the numbers of areas into which everything is divided. So we find the point from the set closest to the current pixel and save the number of this point - the same as the area number.


 float min_distance = 1.0; int area_index = 0; for (int i = 0; i < NUMBER_OF_POINTS; i++) { float current_distance = distance(texture_coord, u_points[i]); if (current_distance < min_distance) { min_distance = current_distance; area_index = i; } } 

To test the performance again, let's paint everything in bright colors:


 gl_FragColor = texture2D(u_texture, texture_coord); gl_FragColor.g = abs(sin(float(area_index))); gl_FragColor.b = abs(sin(float(area_index))); 

The combination of the module (abs) and limited functions (in particular, sin and cos) is often used when working with similar effects. On the one hand, this adds a bit of randomness, but on the other hand, it immediately gives a normalized result from 0 to 1, which is very convenient - we have very many values ​​that will lie within these limits.

We also find points that are more or less equidistant from several points from the set, and paint them. This action does not carry much payload, but it's still interesting to look at the result.


 int number_of_near_points = 0; for (int i = 0; i < NUMBER_OF_POINTS; i++) { if (distance(texture_coord, u_points[i]) < min_distance + EPSILON) { number_of_near_points++; } } if (number_of_near_points > 1) { gl_FragColor.rgb = vec3(1.0); } 

It should turn out something like this:



This is still a draft version, we will continue to refine it. But now the general concept of such a separation of the plane is clear.


Mosaic of photos


It is clear that in the pure form of the benefits of such a partition is not very much. To expand the horizons and just for the sake of interest, you can play with it, but on a real site it would be worth adding a couple more photos and making a mosaic of them. Let's alter the texture creation function a little to make it more than one.


 function createTextures() { for (let i = 0; i < URLS.textures.length; i++) { createTexture(i); } } function createTexture(index) { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => { const texture = GL.createTexture(); GL.activeTexture(GL['TEXTURE' + index]); GL.bindTexture(GL.TEXTURE_2D, texture); GL.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, true); GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGB, GL.RGB, GL.UNSIGNED_BYTE, image); 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); GL.uniform1i(GL.getUniformLocation(PROGRAM, 'u_textures[' + index + ']'), index); }; image.src = URLS.textures[index]; } 

Nothing out of the ordinary happened, we just replaced the zeros with the index parameter and reused the existing code to load three textures. In the shader, we now have an array of textures:


 #define NUMBER_OF_TEXTURES 3 uniform sampler2D u_textures[NUMBER_OF_TEXTURES]; 

Now we can use the region number saved earlier to select one of the three textures. But...


But before that I would like to make a small digression. About sore. About syntax. Modern Javascript (conditionally ES6 +) is a nice language. It allows you to express your thoughts as they arise, does not limit the scope of a particular programming paradigm, completes some points for us and allows us to focus more on the idea than on its implementation. For the creator - the most it. Some people think that it gives too much freedom and switch to TypeScript for example. Pure C is a stricter language. He also allows a lot, on it you can find anything you want, but after JS, he is perceived as a little awkward, old-fashioned or something. Nevertheless, he is still good. GLSL as it exists in browsers is just something. Not only is it an order of magnitude stricter than C, it still lacks many familiar operators and syntactic constructs. This is probably the biggest problem when writing more or less complex shaders for WebGL. Behind the horror that the code turns into, it can be very difficult to look at the original algorithm. Some layout designers think that until they have learned C, the way to shaders is closed for them. So: knowledge of C here does not help. Here is a world of its own. The world of madness, dinosaurs and crutches.


How can I choose one of three textures with one number - the area number. The remainder of dividing the number by the number of textures comes to mind. Great idea. Only the operator % , which the hands themselves are already writing, is not here. The impression of understanding this fact is well described by the picture:


image


Of course, you will say "yes, not a problem, there is a mod function - let's take it!". But it turns out that it does not accept two integers, only fractional ones. Ok, well, make a float of them. We get float too, but we need an int . You have to convert everything back, otherwise there is a non-illusory chance to get an error when compiling.


 int texture_index = int(mod(float(area_index), float(NUMBER_OF_TEXTURES))); 

And here's a rhetorical question: maybe it would be easier to implement its remainder function from dividing integers, rather than trying to assemble it from standard methods? And this is still a simple function, and it happens that very deeply nested sequences of such transformations are obtained, in which it is not quite clear what is happening.


Okay, let's leave it as it is. Just take the color of the desired pixel from the selected texture and assign it to the variable gl_FragColor . So? We have already done this? And then this cat appears again. You cannot use a non-constant when referring to an array. And all that we have calculated is no longer a constant. Ba-dum-tsss !!!


You have to do something like this:


 if (texture_index == 0) { gl_FragColor = texture2D(u_textures[0], texture_coord); } else if (texture_index == 1) { gl_FragColor = texture2D(u_textures[1], texture_coord); } else if (texture_index == 2) { gl_FragColor = texture2D(u_textures[2], texture_coord); } 

Agree, this code direct road to govnokod.ru , but nevertheless in a different way. Even the switch-case operator is not here to somehow improve this outrage. There is a truth yet another, less obvious crutch, solving the same task:


 for (int i = 0; i < 3; i++) { if (texture_index == i) { gl_FragColor = texture2D(u_textures[i], texture_coord); } } 

Cycle counters that increment by one can be taken by the compiler as a constant. But with an array of textures, this could not be done - in the last Chrome there was an error saying that it is impossible to do this with an array of textures. With an array of numbers worked. Guess why it works with one array, but not with the other? If you thought that the type conversion system in JS is full of magic - sort out the "constant - not constant" system in GLSL. The funniest thing here is that the results also depend on the video card used, so that the tricky crutches that worked on the NVIDIA video card may well break on AMD.


It is better to avoid such decisions based on assumptions about the operation of the compiler. They tend to break down and are difficult to test.

Sadness, sadness. But if we want to do interesting things, we need to abstract from all this and continue.


At the moment we got a mosaic of photos. But there is one detail: if the points are very close to each other, then there is a rapid transition of two areas. It is not very beautiful. We need to add some algorithm that does not allow the points to come closer. You can make a simple option, which checks the distance between points and, if it is less than a certain value, then we push them apart. This option is not without flaws, in particular, it sometimes leads to a slight twitching of points, but in many cases it can be enough, especially since the calculations here are not very much. More advanced options would be a system of moving charges and a "web", in which pairs of points are connected by invisible springs. If you are interested in implementing them, then you can easily find all the formulas in the reference book on physics for secondary school.


 for (let i = 0; i < NUMBER_OF_POINTS; i++) { for (let j = i; j < NUMBER_OF_POINTS; j++) { let deltaX = POINTS[i][0] - POINTS[j][0]; let deltaY = POINTS[i][1] - POINTS[j][1]; let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); if (distance < 0.1) { POINTS[i][0] += 0.001 * Math.sign(deltaX); POINTS[i][1] += 0.001 * Math.sign(deltaY); POINTS[j][0] -= 0.001 * Math.sign(deltaX); POINTS[j][1] -= 0.001 * Math.sign(deltaY); } } } 

The main problem of this approach, as well as the one we used in the shader, is a comparison of all points with all. You do not need to be a great mathematician to understand that the number of distance calculations will be incredible if we do not 10 points, but 1000. Yes, even 100 is enough for everything to slow down. Therefore, it makes sense to apply it only for a small number of points.


If we want to make such a mosaic for a large number of points, then we can use the already familiar to us division of the plane into identical squares. The idea is to place a single point in each square, and then carry out all comparisons only with points from neighboring squares. The idea is good, but experiments have shown that, with a large number of points, inexpensive laptops with integrated graphics cards still fail. Therefore, it is worth thinking ten times before deciding to make on your website such a mosaic of a large number of fragments.


Do not be radishes, check the speed of their crafts, not only on your mining farm, but on ordinary laptops. Most users will have them.


Splitting a plane into parts according to the function graph


Let's look at another option of dividing the plane into parts. He will not require large computing power. The basic idea is to take some mathematical function and plot it. The resulting line will divide the plane into two parts. If we use a function of the form y = f(x) , we get a division in the form of a cut. Replacing X with Y, we can change the horizontal section to the vertical one. If we take the function in polar coordinates, then it will be necessary to convert everything to Cartesian and vice versa, but the essence of the calculations will not change. In this case, you will not get a cut into two parts, but rather cutting a hole. But we will see the first option.


For each Y, we will calculate the value of X to make a vertical cut. We could take for this purpose a sine wave for example, but this is too boring. It is better to take them at once a few pieces and lay down.


We take several sinusoids, each of which is tied to the Y coordinate and time, and we add them. Physicists would call such an addition a superposition. Obviously, by multiplying the whole result by some number, we change the amplitude. We take it out to a separate macro. If you multiply the coordinate - the sine parameter, the frequency will change. We have already seen this in the last article. We also remove from the formula the frequency modifier common to all sinusoids. It will not be superfluous to play with time, a negative sign will give the effect of moving the line in the opposite direction.


 float time = u_time * SPEED; float x = (sin(texture_coord.y * FREQUENCY) + sin(texture_coord.y * FREQUENCY * 2.1 + time) + sin(texture_coord.y * FREQUENCY * 1.72 + time * 1.121) + sin(texture_coord.y * FREQUENCY * 2.221 + time * 0.437) + sin(texture_coord.y * FREQUENCY * 3.1122 + time * 4.269)) * AMPLITUDE; 

Having made such global settings for our function, we will encounter the problem of repeating the same movement at fairly short intervals. In order to solve this question, we need to multiply everything by the coefficients, which have the smallest common multiple and a very large one. Something similar is used in the random number generator, remember? In this case, we did not think and took ready numbers from some example from the Internet, but no one bothers to experiment with their values.


It remains only to choose one of two textures for points above our graph of the function and the second for points below it. More precisely, to the left and to the right, we all turned:


 if (texture_coord.x - 0.5 > x) { gl_FragColor = texture2D(u_textures[0], texture_coord); } else { gl_FragColor = texture2D(u_textures[1], texture_coord); } 

What we received is reminiscent of sound waves. More precisely, their image on the oscilloscope. And indeed, instead of our sine waves, we could transfer data from some sound file. But working with sound is a topic for a separate article.



Masks


The previous examples should push on quite a logical remark: it all looks like the work of masks in SVG (if you haven't worked with them - see examples from the SVG mask article and wow effects ). Just here we do them a little differently. As a result, the same is obtained - some areas are painted with one texture, some with another. Only smooth transitions have not yet been. So let's do one.


We remove all unnecessary and return the coordinates of the mouse. Make a radial gradient with the center at the cursor location and use it as a mask. In this example, the behavior of the shader will more closely resemble the logic of the masks in the SVG than in the previous examples. We need a mix function and some function of distance. The first will mix the color values ​​of pixels from both textures, taking as a third parameter a coefficient (from 0 to 1), which determines which of the values ​​will prevail in the result. As a function of distance we take the sine modulus - it will just give a smooth change in the value between 0 and 1.


 gl_FragColor = mix( texture2D(u_textures[0], texture_coord), texture2D(u_textures[1], texture_coord), abs(sin(length(texture_coord - u_mouse_position / u_canvas_size)))); 

Actually that's all. Let's look at the result:



The main advantage over SVG is obvious:


In contrast to the SVG, here we can easily make smooth gradients for various mathematical functions, and not collect them from a variety of linear gradients.

If you have a simpler task that does not require such smooth transitions or complex shapes that are calculated in the process, then most likely it will be easier to implement without using shaders. And the performance on a weak gland is likely to be better. Choose a tool based on your tasks.


For educational purposes, let's look at another example. To begin with, we make a circle in which the texture will remain as it is:


 gl_FragColor = texture2D(u_textures[0], texture_coord); float dist = distance(texture_coord, u_mouse_position / u_canvas_size); if (dist < 0.3) { return; } 

And all the rest is filled with diagonal stripes:


 float value = sin((texture_coord.y - texture_coord.x) * 200.0); if (value > 0.0) { gl_FragColor.rgb *= dist; } else { gl_FragColor.rgb *= dist / 10.0; } 

All the same, we multiply the parameter for the sine to increase the frequency of the strips; we divide the obtained values ​​into two parts; for each of the halves we convert the color of the pixels in our own way. It is useful to remember that drawing diagonal lines is usually associated with adding coordinates along X and Y. Note that we all also use the distance to the mouse cursor when colors change, thereby creating a kind of shadow. In the same way you can use it in geometric transformations, we will soon look at the example of pixelation. In the meantime, take a look at the result of this shader:



Simple and cute.


And yes, if you get a little confused, you can make textures not from images, but from frames from video (there are many examples in the network, you can easily figure them out), and apply all our effects to them. Many sites from directories like Awwwards use similar effects in combination with video.

One more thought to remember:


No one bothers to use one of the textures as a mask. We can take a picture and use the color values ​​of its pixels in our transformations, be it changes of other colors, side shifts or something else that comes to your mind.

But back to breaking the plane into parts.


Pixelate


This effect is somewhat obvious, but at the same time it is so common that it would be wrong to pass by. We divide our plane into squares, in the same way as in the example with the noise generator, and then for all pixels inside each square we set the same color. It is obtained by mixing the values ​​from the corners of the square, we have already done something similar. For this effect, we do not need complex formulas, so we simply add all the values ​​and divide by 4 - the number of corners of the square.


 float block_size = abs(sin(u_time)) / 20.0; vec2 block_position = floor(texture_coord / block_size) * block_size; gl_FragColor = ( texture2D(u_textures[0], block_position) + texture2D(u_textures[0], block_position + vec2(1.0, 0.0) * block_size) + texture2D(u_textures[0], block_position + vec2(0.0, 1.0) * block_size) + texture2D(u_textures[0], block_position + vec2(1.0, 1.0) * block_size) ) / 4.0; 

Again, we tied one of the parameters to time through the sine module in order to visually see what happens when it changes.



Pixel waves


, .


 float block_size = abs(sin( length(texture_coord - u_mouse_position / u_canvas_size) * 2.0 - u_time)) / 100.0 + 0.001; 

, 0 1; , , , . , .



"" , , -. . " ", , . . — . .


Results


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




PS: , WebGL ( ) ? , , . ?


')

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


All Articles