📜 ⬆️ ⬇️

An introduction to shader programming: part 2

Having learned how to write shaders, you will be able to make the most efficient use of all the computing power of modern graphics chips, thousands of cores of which work in parallel in one thread, because all shader calculations are performed on the GPU and not on the CPU. Programming shaders requires a different thinking and approach to writing code than writing ordinary programs, but their almost unlimited potential more than pays for all the problems at the initial stages.




The first part of the “Introduction to Shader Programming” material can be found here.
')
The ShaderToy service that we used in the previous lesson is great for conducting quick tests and experiments, but its functionality is very limited. For example, you can not specify which data you need to send to the shader. To get more opportunities to create a variety of effects, you need your own working environment to run shaders.

To work with shaders in the browser, we will use Three.js. WebGL will provide us with a JavaScript API for rendering shaders, and Three.js will make this process even easier.

If you are not interested in JavaScript or development for web platforms, do not worry: we will not be closely involved in the specifics of web rendering (if you want to learn more about Three.js, read this lesson ). The fact is that setting up shaders in the browser is the fastest way to get started, but as you familiarize yourself with the process, you can easily set up and use shaders on any other platform.

Customization

In this section we will talk about how to configure shaders locally. Source code is available for viewing on CodePen :



Meet Three.js!

Three.js is a JavaScript framework that is responsible for the stereotypical code for WebGL needed to render shaders. To get started with Three.js, you can use its latest version on CDN.

Here you can download the HTML code of the Three.js base scene.

Save the file to disk, and then open it in a browser. You will see just a black screen - nothing interesting. Let's add a cube here to make sure everything works.

Before you add a cube to a scene, you need to define its geometry and material. Paste the following code snippet under Add your code here:

var geometry = new THREE.BoxGeometry( 1, 1, 1 ); var material = new THREE.MeshBasicMaterial( { color: 0x00ff00} );//   var cube = new THREE.Mesh( geometry, material ); //    scene.add( cube ); cube.position.z = -3;// ,    


We will not analyze this code in detail, since we are more interested in working with shaders. But if everything is correct, a green cube should appear in the middle of the screen:



Now let's add a spin. The render function is called for each frame. The rotation of the cube is set using cube.rotation.x (either .y or .z). Here you can play with the values, but the final render function should look like this:

 function render() { cube.rotation.y += 0.02; requestAnimationFrame( render ); renderer.render( scene, camera ); } 


Task: How to make the cube rotate on a different axis? And on two axes at the same time?
So, everything is ready, it's time to add shaders!

Adding Shaders

Now you can proceed to add shaders. Regardless of the platform used, you will probably have a question: everything seems to be set up, a cube rotates on the screen, but how to get access to the GPU?

Step 1: Upload to GLSL Code

To build our scene, we use JavaScript. In other cases, it can be C ++, Lua, or any other language. Anyway, a special language is used to write shaders - Shading Language. For OpenGL, that language is GLSL (OpenGL Shading Language). Given that WebGL is based on OpenGL, we will have to deal with GLSL.

How and where to write GLSL code? As a rule, the GLSL code is loaded as a string of characters (string), which is then parsed and executed in the GPU.
In JavaScript, you just need to add all the code to a variable:

 var shaderCode = "All your shader code here;" 


This method works, but since JavaScript is not so easy to create multi-line strings, it does not suit us. Most developers write the shader code in a text file, change its extension to .glsl or .frag (abbreviated from “fragment shader”) and then load it.

We will go another way: we will write the code of our shader inside the script tag and from there we will load it into JavaScript. Thus, we can, for convenience, store everything in one file.

Add a script tag inside our HTML file:

 <script id="fragShader" type="shader-code"> </script> 


To make it easy to find the tag later, we assign it the identifier fragShader. In fact, the type shader-code does not exist (instead, you can specify any other name). We need this so that the code is not executed and not displayed in HTML.
Now we add the simplest shader that returns only white color.

 <script id="fragShader" type="shader-code"> void main() { gl_FragColor = vec4(1.0,1.0,1.0,1.0); } </script> 


In this case, the vec4 components correspond to the rgba values, as described in the previous lesson.
Finally, download our code. In JavaScript, this is done using a simple string that finds the HTML file and parses the code inside it:

 var shaderCode = document.getElementById("fragShader").innerHTML; 


This line must be below the code for the cube.
Remember: only if the code is loaded as a string of characters will it be recognized as a valid GLSL code (that is, void main () {...}. The rest is just stereotypical HTML code).

Step 2: Shader Overlay

Shading methods may vary depending on the platform and how they interact with the GPU. However, nothing complicated here. In the same Google, you can easily find how to create an object and apply shaders to it using Three.js.
We need to create a special material and give him the code of our shader. Create a plane (although a cube would suit for this) and impose a shader on it:

 //  ,     var material = new THREE.ShaderMaterial({fragmentShader:shaderCode}) var geometry = new THREE.PlaneGeometry( 10, 10 ); var sprite = new THREE.Mesh( geometry,material ); scene.add( sprite ); sprite.position.z = -1; //  ,    


A white screen should appear:



If you change the current color in the shader code to another, the new color will appear after the update.

Task: How to make one part of the screen red and the other blue? If you are having difficulty, study the next step.

Step 3: Submitting Data

Now we can do anything with a shader. But, frankly, there is nothing special to do with it. We can only set the built-in variable gl_FragCoord, which is responsible for the position of the pixels, but for this we need to at least know the parameters of the screen.

The data should be sent to the shader in the form of a so-called uniform variable. To do this, create an object called uniforms and add our variables to it. Here is an example syntax for sending screen resolution data:

 var uniforms = {}; uniforms.resolution = {type:'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)}; 


Each uniform variable must have two parameters: type and value. In this case, we have a two-dimensional vector, where the width and height of the screen act as coordinates. Below is a table (from the Three.js specifications ) with all types and data identifiers that can be sent:



To send data to the shader, add it to ShaderMaterial:

 var material = new THREE.ShaderMaterial({uniforms:uniforms,fragmentShader:shaderCode}) 


And that is not all! Now we can use this variable. Let's create a gradient just like in the previous lesson: adjust the coordinates and adjust the color value.

Change the code as follows:

 uniform vec2 resolution; //  uniform- void main() { //     vec2 pos = gl_FragCoord.xy / resolution.xy; //   ! gl_FragColor = vec4(1.0,pos.x,pos.y,1.0); } 


And you will see a beautiful gradient!



On CodePen, you can create a source code branch and edit it.
If it's not entirely clear to you how we managed to get such a nice gradient with just two lines of code, take a look at the first lesson. In it, we analyzed all the details in detail.
Task: How to divide the screen into four identical sectors with different colors? Like that:



Step 4: Update Data

Now we can send data to the shader. But what if you need to update them? For example, if you open the previous example in a new tab and change the size of the window, the gradient does not update, because it still uses the initial screen settings.

Usually to update the variables you need to re-send the uniform variable. But in Three.js, you simply update the uniforms object in the render function:

 function render() { cube.rotation.y += 0.02; uniforms.resolution.value.x = window.innerWidth; uniforms.resolution.value.y = window.innerHeight; requestAnimationFrame( render ); renderer.render( scene, camera ); } 


If you open the updated code on CodePen and change the window size, the colors will change, although the initial viewing area remains unchanged (you can easily see this if you look at the colors in each corner).

Note. This way of sending data to the GPU is very resource intensive. When sending multiple variables in one frame, you will not feel the difference. But if there are hundreds of variables, the frame rate will drop noticeably. This may sound unbelievable, but if there are several hundred objects on the screen, and different lighting needs to be applied to everyone, the situation can quickly get out of control. In the following articles we will definitely talk about optimizing the operation of shaders.

Task: How to make the colors change over time? If there are difficulties, look at how we dealt with this in the first lesson.

Step 5: Working with Textures

Regardless of which platform you use and in what format you load textures, they are sent to the shader as uniform variables.

For reference: uploading files to JavaScript is very simple from an external URL (which we will do). When downloading an image from a local computer, access rights problems may occur, since JavaScript cannot and should not have access to your system files. The easiest way to get around this is to set up a local server for Python. But do not worry: it is much easier than it seems.
Three.js is equipped with a very convenient function for loading an image as a texture:

 THREE.ImageUtils.crossOrigin = ''; //       var tex = THREE.ImageUtils.loadTexture( "https://tutsplus.imtqy.com/Beginners-Guide-to-Shaders/Part2/SIPI_Jelly_Beans.jpg" ); 


The first line is entered only once. Here you can paste the URL of any image.
Now add a texture to the uniforms object.

 uniforms.texture = {type:'t',value:tex}; 


Finally, let's declare our uniform variable in the shader code and draw it in the same way as in the previous lesson — using the texture2D function:
 uniform vec2 resolution; uniform sampler2D texture; void main() { vec2 pos = gl_FragCoord.xy / resolution.xy; gl_FragColor = texture2D(texture,pos); } 


You should see a multi-colored dragee image stretched across the entire screen:



This is a standard computer graphics test image provided by the Signal and Image Processing Institute of the University of Southern California ( hereinafter referred to as the Signal and Image Processing Institute , hence the abbreviation IPI). It is perfect for testing our graphic shaders.

Task: How to make a gradual transition from full-color texture to grayscale? Again, if there are difficulties, refer to the first lesson.

Bonus Step: Overlaying Shaders to Other Objects

There is nothing special about the plane we created. All this could be imposed on the cube.
In fact, for this you need to change just one line of geometry, with this:

 var geometry = new THREE.PlaneGeometry( 10, 10 ); 


on this:

 var geometry = new THREE.BoxGeometry( 1, 1, 1 ); 


Voila! Now dragee is displayed on the cube:



You can say: “Wait a second, but this is not a completely correct projection of the texture onto the cube!” - and you will be right. If you look closely at the shader, it becomes clear that we just put all the pixels of the test image on the screen. That is, the image is flatly projected onto the cube, and all the pixels outside the cube are cropped.

For a full projection on the edges of the cube, you would have to redo the 3D engine. It sounds a little silly, considering that we already have a 3D engine that we could use to draw a texture on each facet separately. But this series of lessons focuses more on the use of shaders to produce effects that could not have been achieved by other means. Therefore, we will not analyze this issue. If you're interested, Udacity has an excellent course on the basics of 3D graphics.

Further steps

At this stage, you already know how to use ShaderToy, and in addition you can apply any texture on any surface and on almost any platform. So, we are ready to move to a more complex topic - setting up a lighting system with realistic shadows. That is what we will do in the next lesson!

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


All Articles