Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

A Beginner's Guide to Coding Graphics Shaders: Part 2

$
0
0

ShaderToy, which we used in the previous tutorial in this series, is great for quick tests and experiments, but it's rather limited. You can't control what data gets sent to the shader for example, among other things. Having your own environment where you can run shaders means you can do all sorts of fancy effects, and you can apply them to your own projects!

We're going to be using Three.js as our framework to run shaders in the browser. WebGL is the Javascript API that will allow us to render shaders; Three.js just makes this job easier. 

If you're not interested in JavaScript or the web platform, don't worry: we won't be focusing on the specifics of web rendering (though if you'd like to learn more about the framework, check out this tutorial). Setting up shaders in the browser is the quickest way to get started, but becoming comfortable with this process will allow you to easily set up and use shaders on whatever platform you like. 

The Setup

This section will guide you through setting up shaders locally. You can follow along without needing to download anything with this pre-built CodePen:

You can fork and edit this on CodePen.

Hello Three.js!

Three.js is a JavaScript framework that takes care of a lot of boilerplate code for WebGL that we'll need to render our shaders. The easiest way to get started is to use a version hosted on a CDN

Here's an HTML file you can download which has just a basic Threejs scene. 

Try saving that file to disk, then opening it in your web browser. You should see a black screen. That isn't very exciting, so let's try adding a cube, just to make sure everything is working. 

To create a cube, we need to define its geometry and its material, and then add it to the scene. Add this code snippet under where it says Add your code here:

We won't go into too much detail in all this code, since we're more interested in the shader part. But if all went right, you should see a green cube in the center of the screen:

While we're at it, let's make it rotate. The render function runs every frame. We can access the cube's rotation through cube.rotation.x (or .y or .z). Try incrementing that, so that your render function looks like this:

Challenge: Can you make it rotate along a different axis? What about along two axes at the same time?

Now you've got everything set up, let's add some shaders!

Adding Shaders

At this point, we can start thinking about the process of implementing shaders. You're likely to find yourself in a similar situation regardless of the platform you plan to use shaders on: you've got everything set up, and you have things being drawn on screen, now how do you access the GPU?

Step 1: Loading in GLSL Code

We're using JavaScript to build this scene. In other situations you might be using C++, or Lua or any other language. Shaders, regardless, are written in a special Shading LanguageOpenGL's shading language is GLSL(OpenGLShading Language). Since we're using WebGL, which is based on OpenGL, then GLSL is what we use.

So how and where do we write our GLSL code? The general rule is that you want to load your GLSL code in as a string. You can then send it off to be parsed and executed by the GPU. 

In JavaScript, you can do this by simply throwing all your code inline inside a variable like so:

This works, but since JavaScript doesn't have a way to easily make multiline strings, this isn't very convenient for us. Most people tend to write the shader code in a text file and give it an extension of .glsl or .frag (short for fragment shader), then just load that file in.

This is valid, but we're going to write our shader code inside a new <script> tag and load it into the JavaScript from there, so that we can keep everything in one file for the purpose of this tutorial. 

Create a new <script> taginside the HTML that looks like this:

We give it the ID of fragShader is so that we can access it later. The type shader-code is actually a bogus script type that does not exist. (You could put in any name there and it would work). The reason we do this is so that the code doesn't get executed, and doesn't get displayed in the HTML.

Now let's throw in a very basic shader that just returns white.

(The components of vec4 in this case correspond to the rgba value, as explained in the previous tutorial.)

Finally, we have to load in this code. We can do this with a simple JavaScript line that finds the HTML element and pulls the inner text:

This should go under your cube code.

Remember: only what's loaded as a string will be parsed as valid GLSL code (that is, void main() {...}. The rest is just HTML boilerplate.) 

You can fork and edit this on CodePen.

Step 2: Applying the Shader

The method for applying the shader might be different depending on what platform you're using and how it interfaces with the GPU. It's never a complicated step, though, and a cursory Google search shows us how to create an object and apply shaders to it with Three.js.

We need to create a special material, and give it our shader code. We'll create a plane as our shader object (but we could just as well use the cube). This is all we need to do:

By now, you should be seeing a white screen:

You can fork and edit this on CodePen.


If you change the code in the shader to any other color and refresh, you should see the new color!

Challenge: Can you set a portion of the screen to red, and another portion to blue? (If you're stuck, the next step should give you a hint!)

Step 3: Sending Data

At this point, we can do whatever we want with our shader, but there's not much we can do. We only have the built-in pixel position gl_FragCoord to work with, and if you recall, that's not normalized. We need to have at least the screen dimensions. 

To send data to our shader, we need to send it as what's called a uniform variable. To do this, we create an object called uniforms and add our variables to it. Here's the syntax for sending the resolution:

Every uniform variable must have a type and a value. In this case, it's a 2 dimensional vector with the window's width and height as its coordinates. The table below (taken from the Three.js docs) shows you all the data types you can send and their identifiers:
Uniform type stringGLSL typeJavaScript type
'i', '1i'
int
Number
'f', '1f'float
Number
'v2'
vec2
THREE.Vector2
'v3'
vec3
THREE.Vector3
'c'vec3
THREE.Color
'v4'vec4
THREE.Vector4
'm3'mat3
THREE.Matrix3
'm4'mat4
THREE.Matrix4
't'sampler2D
THREE.Texture
't'samplerCube
THREE.CubeTexture
To actually send it to the shader, modify the ShaderMaterial instantiator to include it, like this:

We're not done yet! Now our shader is receiving this variable, we need to do something with it. Let's create a gradient in the same way we did in the previous tutorial: by normalizing our co-ordinate and using it to create our color value.

Modify your shader code so that it looks like this:

And you should see a nice looking gradient!

You can fork and edit this on CodePen.

If you're a little fuzzy on how we managed to create such a nice gradient with only two lines of shader code, check out the first part of this tutorial series for an in-depth run down of the logic behind this.

Challenge: Can you split the screen into 4 equal sections with different colors? Something like this:

Step 4: Updating Data

It's nice to be able to send data to our shader, but what if we need to update it? For example, if you open the previous example in a new tab, then resize the window, the gradient does not update, because it's still using the initial screen dimensions.

To update your variables, usually you'd just resend the uniform variable and it will update. With Three.js, however, we just need to update the uniforms object in our render function—no need to resend it to the shader. 

So here's what our render function looks like after making that change:

If you open the new CodePen and resize the window, you will see the colors changing (although the initial viewport size stays the same). It's easiest to see this by looking at the colors in each corner to verify that they don't change.

Note: Sending data to the GPU like this is generally costly. Sending a handful of variables per frame is okay, but your framerate can really slow down if you're sending hundreds per frame. It might not sound like a realistic scenario, but if you have a few hundred objects on screen, and all need to have lighting applied to them, for example, all with different properties, then things can quickly get out of control. We'll learn more about optimizing our shaders in future articles!

Challenge: Can you make the colors change over time? (If you're stuck, look at how we did it in the first part of this tutorial series.)

Step 5: Dealing With Textures

Regardless of how you load in your textures or in what format, you'll send them to your shader in the same way across platforms, as uniform variables.

A quick note about loading files in JavaScript: you can load images from an external URL without much trouble (which is what we'll be doing here) but if you want to load an image locally, you'll run into permission issues, because JavaScript can't, and shouldn't, normally access files on your system. The easiest way to get around this is to start a local Python server, which is simpler than it perhaps sounds.

Three.js provides us with a handy little function for loading an image as a texture:

The first line just needs to be set once. You can put in any URL to an image there. 

Next, we want to add our texture to the uniforms object.

Finally, we want to declare our uniform variable in our shader code, and draw it in the same way we did in the previous tutorial, with the texture2D function:

And you should see some tasty jelly beans, stretched across our screen:

You can fork and edit this on CodePen.

(This picture is a standard test image in the field of computer graphics, taken from the University of Southern California's Signal and Image Processing Institute (hence the IPI initials). It seems fitting to use it as our test image while learning about graphics shaders!)

Challenge: Can you make the texture go from full color to grayscale over time? (Again, if you're stuck, we did this in the first part of this series.)

Bonus Step: Applying Shaders to Other Objects

There's nothing special about the plane we've created. We could have applied all of this onto our cube. In fact, we can just change the plane geometry line:

to:

Voila, jelly beans on a cube:

You can fork and edit this on CodePen.

Now you might be thinking, "Hold on, that doesn't look like proper projection of a texture onto a cube!". And you'd be right; if we look back to our shader, we'll see that all we really did was say "map all the pixels of this image onto the screen". The fact that it's on a cube just means the pixels outside are being discarded. 

If you wanted to apply it so that it looks like it's drawn physically onto the cube, that would involve a lot of reinventing a 3D engine (which sounds a bit silly considering we're already using a 3D engine and we can just ask it to draw the texture onto each side individually).This tutorial series is more about using shaders to do things we couldn't achieve otherwise, so we won't be delving into details like that. (Udacity has a great course on the fundamentals of 3D graphics, if you're eager to learn more!)

Next Steps

At this point, you should be able to do everything we've done in ShaderToy, except now you have the freedom to use whatever textures you want on whatever shapes you like, and hopefully on whatever platform you choose. 

With this freedom, we can now do something like set up a lighting system, with realistic looking shadows and highlights. This is what the next part will focus on, as well as tips and techniques for optimizing shaders!


Viewing all articles
Browse latest Browse all 728

Trending Articles