└── README.md /README.md: -------------------------------------------------------------------------------- 1 | ##
About WebGL
2 | 3 |
WebGL is a rasterization API used for rendering graphics directly within a webpage, based off of OpenGL. WebGL uses JavaScript and offers many features, being widely used for 3D programs within HTML websites.

WebGL has two versions, the basic WebGL1, and WebGL2. WebGL2 is a more modern version that offers many more features compared to WebGL1. WebGL2 is backwards compatible with WebGL1 and is better, making WebGL1 pointless.

Both WebGL versions have extensions that provide additional features, but support for these extensions are quite limited.

**In this tutorial, we'll be strictly using WebGL2 and no extenstions.**

WebGL utilizes your computer's Graphics Processing Unit to rasterize pixels at lightning speeds. It takes information you provide and does math to color pixels.

Most modern browsers and machines support WebGL1. WebGL2's support may be less, but still quite high. It is supported by major browsers, but some require the experimental version.

WebGL programs run using a special shader language similar to C++, called Graphics Library Shader Language(GLSL). GLSL is a type-strict language used to write the vertex and fragment shaders of a WebGL program.

Most of the WebGL API is boilerplate code, and the rest is for rendering. 4 | 5 | ##
Rendering a Triangle
6 | 7 |
To get started with WebGL, you'll first need to prepare an HTML program. Create a canvas element that'll be used for rendering the graphics. 8 | 9 | ``` 10 | 11 | ``` 12 | Then, create a script that gets a WebGL context. You can use _"webgl"_, _"webgl2"_, or _"experimental-webgl"_. Inside the script... 13 | ``` 14 | //fetches the canvas element 15 | let canvas=document.getElementById('myCanvas') 16 | 17 | //the webgl api 18 | let gl=canvas.getContext('webgl2') 19 | 20 | //if webgl2 is not supported 21 | if(!gl){ 22 | 23 | alert('Your browser does not support WebGL2!') 24 | return 25 | } 26 | ``` 27 | Now that you've got the WebGL context, it's time to start utilizing it, starting off with a basic background color and setting the viewport. 28 | 29 | ``` 30 | //color values are in RGBA format. each value is in the range of 0-1 31 | gl.clearColor(0.1,0,0,1) 32 | 33 | //clears the color buffer bits, showing the background color 34 | gl.clear(gl.COLOR_BUFFER_BIT) 35 | 36 | 37 | //sets the viewport. the first 2 params are the x and y offset values of the viewport from the lower left corner. the last 2 are the width and height 38 | //in webgl, the up direction are positive y values 39 | gl.viewport(0,0,canvas.width,canvas.height) 40 | ``` 41 | The canvas should now be filled with a dark maroon color. Now, it's time for the painful process of rendering a triangle.

We need to make a WebGLProgram object. Here's a helpful function: 42 | 43 | ``` 44 | //a function that returns a webglprogram object given the shader text 45 | function createProgram(vshText,fshText){ 46 | 47 | //these lines create and compile the shaders 48 | vsh=gl.createShader(gl.VERTEX_SHADER) 49 | fsh=gl.createShader(gl.FRAGMENT_SHADER) 50 | gl.shaderSource(vsh,vshText) 51 | gl.shaderSource(fsh,fshText) 52 | gl.compileShader(vsh) 53 | gl.compileShader(fsh) 54 | 55 | //these lines create the program and attaches the shaders 56 | let p=gl.createProgram() 57 | gl.attachShader(p,vsh) 58 | gl.attachShader(p,fsh) 59 | gl.linkProgram(p) 60 | 61 | return p 62 | } 63 | ``` 64 | Now, we use the previous function to create a WebGLProgram. We need to create the GLSL shaders first. The shaders are defined as strings and passed into the function. We'll use template literals to make the code neat while allowing for line breaks.

In WebGL2, GLSL shaders must have _"#version 300 es"_ at the very start of the string. The vertex shader does math using information passed in to determine the position of vertices. The fragment shader determines the color of fragments(aka. pixels). 65 | 66 | ``` 67 | let vertexShaderCode=`#version 300 es 68 | 69 | //this line tells the shader what the precision is. This is needed in both the shaders. 'lowp' means low, 'mediump' means medium, and 'highp' is high. 70 | precision mediump float; 71 | 72 | //this is the position of the vertices, and will be passed in from the buffers later 73 | in vec2 vertPos; 74 | 75 | void main(){ 76 | 77 | //gl_Position is a vec4. Your vertex shader needs to do math to find out where the vertex is. Because this is a 2D program, just put the vertex position at where it is passed in, without any changes. 78 | 79 | //GLSL is very cool when dealing with vectors. The vertPos is a vec2, but it can be inserted into a vec4 with 2 other nums, adding up to 4 components. 80 | gl_Position=vec4(vertPos,0,1); 81 | } 82 | ` 83 | 84 | let fragmentShaderCode=`#version 300 es 85 | 86 | precision mediump float; 87 | 88 | //In WebGL1, you set gl_FragColor, just like gl_Position, but in WebGL2, you use an 'out' vec4 statement, with the name of the output. The name can't be 'gl_FragColor' 89 | out vec4 fragColor; 90 | 91 | void main(){ 92 | 93 | //We set the color here. colors are in RGBA in the range of 0-1 94 | //this makes all the computed pixels green 95 | fragColor=vec4(0,1,0,1); 96 | } 97 | ` 98 | 99 | 100 | ``` 101 | Now, we create the WebGLProgram using the helpful function: 102 | 103 | ``` 104 | let program=createProgram(vertexShaderCode,fragmentShaderCode) 105 | 106 | //now we tell webgl to use the program we made 107 | gl.useProgram(program) 108 | ``` 109 | Our WebGLProgram is now initialized. When you call _gl.useProgram()_, it sets the current program being used as whatever program you put in. Graphics will be rendered with that program.

Now, it's time to create the mesh and buffers. Buffers are like arrays of numbers that can only be accessed by the GPU. A "mesh" is just data that describes the geometry being drawn.

Here, we define a mesh and create a buffer: 110 | 111 | ``` 112 | //the vertices of the triangle. the coords are in the range of -1 to 1, for both the axes. canvas width and height don't matter 113 | //the vertex positions are stored together. in this case, the format is x1,y1,x2,y2,x3,y3 114 | let verts=[ 115 | 116 | //in the middle, upwards 117 | 0,0.5, 118 | 119 | //on the left, downwards 120 | -0.5,-0.5, 121 | 122 | //on the right, downwards 123 | 0.5,-0.5, 124 | ] 125 | 126 | 127 | //creates the buffer 128 | let buffer=gl.createBuffer() 129 | 130 | //tells webgl that we are performing stuff on this specific buffer 131 | //gl.ARRAY_BUFFER indicates that this buffer is for vertex data 132 | gl.bindBuffer(gl.ARRAY_BUFFER,buffer) 133 | 134 | //sets data in the buffer using the mesh. the inputted data must be a Float32Aray 135 | gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(verts),gl.STATIC_DRAW) 136 | ``` 137 |
The buffer is ready. Now, we pass them into the GLSL shaders as attributes. The GLSL vertex shader defines _"vertPos"_ as a vec2(2 numbers, forming a 2D vector). The _"in"_ keyword before it indicates that it's an attribute, which is data(a number or groups of numbers(as in vec2)) that is defined and differs for each vertex. Attributes only exist in the vertex shader.

Here, we get the location of the attribute(from the WebGLProgram) and use it to tell WebGL how the vertex array's data is formatted(in this case, each vertex has an x and y value combined as a vec2 attribute). 138 | 139 | ``` 140 | //gets the location of the attribute from the WebGLProgram 141 | //the name(here it's "vertPos") must match what's defined in the vertex shader(the shader states "in vec2 vertPos;") 142 | let vertPosLocation=gl.getAttribLocation(program,'vertPos') 143 | 144 | //enable the vertex attribute 145 | gl.enableVertexAttribArray(vertPosLocation) 146 | 147 | //values in attribute. 2 values(x and y coords) are used in the "vertPos" attribute 148 | let via=2 149 | 150 | //bytes per vertex. the total amount of values per a vertex(here it's 2) multiplied by 4(which is the amount of bytes in a float32) 151 | let bpv=8 152 | 153 | //current attribute offset bytes. here, the "vertPos" attribute is the first attribute, so 0 values before it. the amount of bytes is the value multiplied by 4(which is the amount of bytes in a float32) 154 | let caob=0 155 | 156 | //tells webgl how to get values out of the supplied buffer 157 | gl.vertexAttribPointer(vertPosLocation,via,gl.FLOAT,gl.FALSE,bpv,caob) 158 | ``` 159 | Finally, we render the elements with a simple draw call! A draw call is just a call to _gl.drawArrays()_ or _gl.drawElements()_. Draw calls are quite expensive compared to the rest of the functions. You likely want less than 50 to 350 draw calls per frame for a good frame rate. 160 | 161 | ``` 162 | //1st param: type of primitive being drawn 163 | //2nd param: starting vertex(often kept as 0) 164 | //3rd param: amount of vertices 165 | gl.drawArrays(gl.TRIANGLES,0,3) 166 | ``` 167 | You should now see a green triangle! It may seem like a lot, but the code won't expand much and can be reused for more complicated programs.

A draw call uses the currently bound buffer and attributes. It's good practice to bind the buffer, make the _gl.vertexAttribPointer()_ calls, and then draw. 168 | 169 | ##
Varyings & More Attributes
170 | 171 |
In vertices, you can store much more information other than the vertex's position! Some common examples are vertex colors and normals. In this section, you'll learn how to create a multi-colored triangle.

We can use varyings to help accomplish this. A varying is a value that is set in the vertex shader and passed into the fragment shader. The value of the varying is set at each vertex, so what happens when a fragment(aka pixel) is between the vertices? The value is interpolated(or mixed) across the vertices, based on where the fragment is.

Starting off with the code for a basic triangle, we need to add colors to the vertices. Begin by adding a color attribute and a color varying to the vertex shader. The vertex shader now becomes... 172 | 173 | ``` 174 | let vertexShaderCode=`#version 300 es 175 | 176 | precision mediump float; 177 | 178 | in vec2 vertPos; 179 | 180 | //to pass in an RGB color as an attribute, we'll use a vec3 to store the values 181 | in vec3 vertColor; 182 | 183 | //in webgl1, use the varying keyword. in webgl2, we use the out keyword while inside the vertex shader 184 | out vec3 pixelColor; 185 | 186 | void main(){ 187 | 188 | //here, we set the varying so it can be passed into the fragment shader 189 | pixelColor=vertColor; 190 | 191 | gl_Position=vec4(vertPos,0,1); 192 | } 193 | ` 194 | ``` 195 | In the fragment shader, we set the color based on the varying value passed in from the vertex shader. 196 | 197 | ``` 198 | let fragmentShaderCode=`#version 300 es 199 | 200 | precision mediump float; 201 | 202 | //in webgl1, also use the varying keyword. in webgl2, we use the in keyword while inside the fragment shader 203 | in vec3 pixelColor; 204 | 205 | out vec4 fragColor; 206 | 207 | void main(){ 208 | 209 | //We set the color as the value of the varying 210 | //interpolation is done automatically 211 | //notice that the varying("pixelColor") is a vec3, but "fragColor" needs to be a vec4. we combine the RGB value with an alpha value of 1(fully visible) 212 | fragColor=vec4(pixelColor,1); 213 | } 214 | ` 215 | ``` 216 | Now, we just need to add the color values to our mesh and create another attribute.

The updated mesh, with added color values takes the format of x1,y1,r1,g1,b1,x2,y2,r2,g2,b2,x3,y3,r3,g3,b3... 217 | 218 | ``` 219 | let verts=[ 220 | 221 | //in the middle, upwards, red color 222 | 0,0.5, 1,0,0, 223 | 224 | //on the left, downwards, green color 225 | -0.5,-0.5, 0,1,0, 226 | 227 | //on the right, downwards, blue color 228 | 0.5,-0.5, 0,0,1 229 | ] 230 | ``` 231 | We don't need to change the way the buffer is created, but we need to update the way values are read from the buffer(the attributes). Create a new attribute for the color values, similar to the way the _"vertPos"_ attribute was created. 232 | 233 | ``` 234 | let vertColorLocation=gl.getAttribLocation(program,'vertColor') 235 | gl.enableVertexAttribArray(vertColorLocation) 236 | ``` 237 | Now, we have to specify the new way values are read out of the buffer: 238 | 239 | ``` 240 | //bytes per vertex. the total amount of values per a vertex(now it's 5(x,y,r,g,b)) multiplied by 4(which is the amount of bytes in a float32) 241 | let bpv=20 242 | 243 | //2 values for the position, 0 bytes before the position values 244 | gl.vertexAttribPointer(vertPosLocation,2,gl.FLOAT,gl.FALSE,bpv,0) 245 | 246 | //3 values for the color, 2 values(x & y coords) * 4 bytes per value = 8 bytes before the color values 247 | gl.vertexAttribPointer(vertColorLocation,3,gl.FLOAT,gl.FALSE,bpv,8) 248 | ``` 249 | The _"gl.drawArrays()"_ function call doesn't need changing, as we didn't add more vertices.

You should now see a rainbow triangle! The color of each pixel in the triangle is mixed with the nearby vertex's color value. Pixels closer to the higher corner are red, the lower left corner is green, and the lower right corner is blue. Pixels on the edge of the triangle are a blend of the colors of the 2 nearest corners, and the pixels fade to a dull gray-ish color in the middle. 250 | 251 | ##
Uniforms
252 | 253 |
In GLSL, uniforms are global values that stay the same for each draw call. Before a WebGL draw call, they can be set. WebGL has many functions for setting uniforms, as they can come in many types.

In this section, you'll learn how to create, set, and use uniforms to change your triangle.

Our goal is to make a system that allows us to scale and move our triangle around *without* changing the mesh!

Start with the code from the **Varyings & More Attributes** section. We are using uniforms to do math with the vertex position of the triangles. We are going to create 3 uniform values: a scale value, an x translation value, and a y translation value to transform the triangle. The scale value is a float, and the 2 translation values will be combined into a vec2. So inside the vertex shader... 254 | 255 | ``` 256 | let vertexShaderCode=`#version 300 es 257 | 258 | precision mediump float; 259 | 260 | in vec2 vertPos; 261 | 262 | in vec3 vertColor; 263 | 264 | out vec3 pixelColor; 265 | 266 | //declare a uniform with the "uniform" keyword 267 | 268 | //this is the scaling amount 269 | uniform float scaleAmount; 270 | 271 | //this is the translation vector 272 | uniform vec2 translationAmount; 273 | 274 | void main(){ 275 | 276 | pixelColor=vertColor; 277 | 278 | //we multiply the "vertPos" by the "scaleAmount", and translate it by adding "translationAmount" 279 | gl_Position=vec4(vertPos*scaleAmount+translationAmount,0,1); 280 | } 281 | ` 282 | ``` 283 | That's already half of the process done! Now we need to create and set the uniform values with the WebGL.

Get the uniform by calling _gl.getUniformLocation()_, similar to getting an attribute's location. 284 | 285 | ``` 286 | //notice that the names match up to what was defined in the vertex shader 287 | 288 | let scaleAmountLocation=gl.getUniformLocation(program,'scaleAmount') 289 | let translationAmountLocation=gl.getUniformLocation(program,'translationAmount') 290 | ``` 291 | Now, we set the uniforms. Make sure to do this before the draw call. 292 | ``` 293 | //the values that will transform the triangle 294 | let scaleAmount=0.3 295 | let xTranslation=0.5 296 | let yTranslation=0.5 297 | 298 | //set the uniforms to the corresponding values 299 | //use gl.uniform1f for a float, gl.uniform2f for 2 floats(a vec2) 300 | 301 | gl.uniform1f(scaleAmountLocation,scaleAmount) 302 | gl.uniform2f(translationAmountLocation,xTranslation,yTranslation) 303 | ``` 304 | The triangle should be smaller and moved to the up and right side! Play around with the values to change the triangles in different ways!

When setting uniforms, you can choose to pass an array into the function. GLSL supports arrays, but their usage is very limited.

Here is a list of all the types of uniforms in WebGL2, with their corresponding _"gl.uniform....."_ function: 305 | 306 | ``` 307 | gl.uniform1f (floatUniformLoc, v); // for float 308 | gl.uniform1fv(floatUniformLoc, [v]); // for float or float array 309 | gl.uniform2f (vec2UniformLoc, v0, v1); // for vec2 310 | gl.uniform2fv(vec2UniformLoc, [v0, v1]); // for vec2 or vec2 array 311 | gl.uniform3f (vec3UniformLoc, v0, v1, v2); // for vec3 312 | gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // for vec3 or vec3 array 313 | gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4); // for vec4 314 | gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // for vec4 or vec4 array 315 | 316 | gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // for mat2 or mat2 array 317 | gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // for mat3 or mat3 array 318 | gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ]) // for mat4 or mat4 array 319 | 320 | gl.uniform1i (intUniformLoc, v); // for int 321 | gl.uniform1iv(intUniformLoc, [v]); // for int or int array 322 | gl.uniform2i (ivec2UniformLoc, v0, v1); // for ivec2 323 | gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // for ivec2 or ivec2 array 324 | gl.uniform3i (ivec3UniformLoc, v0, v1, v2); // for ivec3 325 | gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // for ivec3 or ivec3 array 326 | gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4); // for ivec4 327 | gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // for ivec4 or ivec4 array 328 | 329 | gl.uniform1u (intUniformLoc, v); // for uint 330 | gl.uniform1uv(intUniformLoc, [v]); // for uint or uint array 331 | gl.uniform2u (ivec2UniformLoc, v0, v1); // for uvec2 332 | gl.uniform2uv(ivec2UniformLoc, [v0, v1]); // for uvec2 or uvec2 array 333 | gl.uniform3u (ivec3UniformLoc, v0, v1, v2); // for uvec3 334 | gl.uniform3uv(ivec3UniformLoc, [v0, v1, v2]); // for uvec3 or uvec3 array 335 | gl.uniform4u (ivec4UniformLoc, v0, v1, v2, v4); // for uvec4 336 | gl.uniform4uv(ivec4UniformLoc, [v0, v1, v2, v4]); // for uvec4 or uvec4 array 337 | 338 | // for sampler2D, sampler3D, samplerCube, samplerCubeShadow, sampler2DShadow, 339 | // sampler2DArray, sampler2DArrayShadow 340 | gl.uniform1i (samplerUniformLoc, v); 341 | gl.uniform1iv(samplerUniformLoc, [v]); 342 | 343 | 344 | @ Credits to WebGL2Fundamentals 345 | ``` 346 | 347 | ##
Multiple Triangles
348 | 349 |
To draw multiple meshes, there are 2 main ways: mesh merging and multiple draw calls. These 2 methods both have their pros and cons. In this section, we'll be exploring both.

350 | ####
Mesh Merging
351 | With mesh merging, you add more vertices for triangles to a big main mesh. Starting with the code from the **Varyings & More Attributes** section, we can make an _"addTriangle()"_ function that adds more vertices to the mesh to form additional triangles. 352 | 353 | ``` 354 | let verts=[] 355 | 356 | //params: x pos, y pos, size 357 | function addTriangle(x,y,s){ 358 | 359 | //adds 3 more vertices to the mesh via pushing to the array 360 | //also transforms the vertices according to the params 361 | verts.push( 362 | 363 | //in the middle, upwards, red color 364 | 0+x,0.5*s+y, 1,0,0, 365 | 366 | //on the left, downwards, green color 367 | -0.5*s+x,-0.5*s+y, 0,1,0, 368 | 369 | //on the right, downwards, blue color 370 | 0.5*s+x,-0.5*s+y, 0,0,1 371 | ) 372 | } 373 | 374 | //adds many triangles to be rendered 375 | addTriangle(-0.3,-0.4,0.3) 376 | addTriangle(-0.5,0.5,0.4) 377 | addTriangle(0.5,0.2,0.6) 378 | ``` 379 | Wait! We're not done! Only 1 triangle is showing up. Why? Remember the _"gl.drawArrays()"_ function? You have to provide the amount of vertices to be drawn!

Instead of making a counter that increments everything you call _"addTriangle"_, you can calculate the amount of vertices based on the mesh(_"verts"_ array) itself. 380 | 381 | ``` 382 | //1st param: type of primitive being drawn 383 | //2nd param: starting vertex(often kept as 0) 384 | //3rd param: amount of vertices 385 | 386 | //there are 5 values per vertex. divide the total amount of values by 5 and you get the amount of vertices! 387 | gl.drawArrays(gl.TRIANGLES,0,verts.length/5) 388 | ``` 389 | That's it! You should see 3 triangles created by the _"addTriangle"_ function.

Mesh merging is a great way to draw multiple objects, but it won't be as fast if the mesh for each object is complex and changes every frame. 390 |

391 | ####
Multiple Draw Calls
392 | Another way to draw many objects is drawing each of them separately using 1 draw call.

You can choose to create a new mesh for each object, but that's unnecessary. Instead, you can use uniforms to change the meshes.

Start with the code from the **Uniforms** section. We can reset the uniforms and draw again. 393 | 394 | ``` 395 | //params: x pos, y pos, size 396 | function renderTriangle(x,y,s){ 397 | 398 | //set the uniforms to the corresponding params 399 | gl.uniform1f(scaleAmountLocation,s) 400 | gl.uniform2f(translationAmountLocation,x,y) 401 | 402 | //1st param: type of primitive being drawn 403 | //2nd param: starting vertex(often kept as 0) 404 | //3rd param: amount of vertices 405 | gl.drawArrays(gl.TRIANGLES,0,3) 406 | } 407 | 408 | renderTriangle(0.3,-0.4,0.3) 409 | renderTriangle(0.5,0.5,0.4) 410 | renderTriangle(-0.5,0.2,0.6) 411 | ``` 412 | It's already done! The downside to this method is that it's unnecessary to do this for large amounts of non-changing objects every frame. It's best to use mesh merging once at the start in that scenario.

Notice that we don't need to re-bind and re-call the buffers and attribute pointers. You should always bind buffers and call _"gl.vertexAttribPointer()"_ together, and only call them if a different buffer is bound and you want to draw this mesh, otherwise it's unnecessary. 413 | 414 | ##
Animations
415 |
If you're familiar with animation frames, you can probably animate your WebGL graphics already!

You can use _"window.requestAnimationFrame()"_ to animate WebGL graphics. Start with code from the **Multiple Draw Calls** section.

You'll need to create a loop function that gets run every frame, and do delta time computations to ensure the animation is frame rate independent. 416 | 417 | ``` 418 | let dt,then=0,time=0 419 | 420 | function loop(now){ 421 | 422 | dt=Math.min((now-then)*0.001,0.1) 423 | time+=dt 424 | 425 | //blah blah goes here 426 | 427 | then=now 428 | window.requestAnimationFrame(loop) 429 | } 430 | 431 | loop(0) 432 | ``` 433 | Because the objects are constantly moving in an animation, drawing many triangles using multiple draw calls will be a better option!

Here, we clear the background and then redraw the triangles every frame. I use some fancy math to make the triangles move around. 434 | ``` 435 | for(let i=time;i<6.28318531+time;i+=6.28318531/12){ 436 | 437 | let r=Math.sin(i*2.25)*0.75 438 | 439 | renderTriangle(Math.sin(i)*r,Math.cos(i)*r,0.4*Math.cos(i*3.5)) 440 | } 441 | ``` 442 | We also need to clear the canvas every frame. Remember to clear the canvas _before_ rendering. 443 | ``` 444 | function loop(now){ 445 | 446 | dt=(now-then)*0.001*0.5 447 | time+=dt 448 | 449 | gl.clearColor(0.1,0,0,1) 450 | gl.clear(gl.COLOR_BUFFER_BIT) 451 | 452 | for(let i=time;i<6.28318531+time;i+=6.28318531/12){ 453 | 454 | let r=Math.sin(i*2.25)*0.75 455 | 456 | renderTriangle(Math.sin(i)*r,Math.cos(i)*r,0.4*Math.cos(i*3.5)) 457 | } 458 | 459 | 460 | then=now 461 | window.requestAnimationFrame(loop) 462 | } 463 | ``` 464 | That's animation done! However, you might need to remember what parts of the code belong and don't belong in the loop to maximize performance.
465 | - It's always best to not have _"gl.getAttribLocation()"_ or _"gl.getUniformLocation()"_ calls inside the loop, but at initialization time instead. 466 | 467 | - Buffers _can_ be set every frame, but shouldn't be created often. 468 | 469 | - Programs should be created and shaders should be compiled once at initialization. Sometimes, you may update shaders, but this is expensive. 470 | 471 | ##
Indexed Rendering
472 |
Up until this point, we've only been drawing triangles. What if we want to draw rectangles or other shapes instead?

You can draw just about anything with triangles. A rectangle can be made from 2 triangles positioned in a specific way next to each other.

Let's look at the simplest code for a triangle, after the **Rendering a Triangle** section. We'll now create the mesh for a rectangle. 473 | ``` 474 | let verts=[ 475 | 476 | //top left 477 | -0.6,0.5, 478 | //lower left 479 | -0.6,-0.5, 480 | //lower right 481 | 0.6,-0.5, 482 | 483 | //that's 3 vertices, making 1 triangle 484 | //next set of vertices for the 2nd triangle: 485 | 486 | //top left 487 | -0.6,0.5, 488 | //top right 489 | 0.6,0.5, 490 | //lower right 491 | 0.6,-0.5, 492 | ] 493 | 494 | ``` 495 | Then, we update the parameter of _"gl.drawArrays()"_ to specify the new amount of vertices. There are now 6 vertices, as there are 3 per triangle, and 2 triangles. 496 | ``` 497 | //1st param: type of primitive being drawn 498 | //2nd param: starting vertex(often kept as 0) 499 | //3rd param: amount of vertices 500 | gl.drawArrays(gl.TRIANGLES,0,6) 501 | ``` 502 | That'll render 2 triangles next to each other, arranged into a rectangle. You can further visualize the way the 2 triangles are oriented by offsetting their vertices by a bit: 503 | ``` 504 | let aSmallNumber=0.01 505 | 506 | let verts=[ 507 | 508 | //top left 509 | -0.6-aSmallNumber,0.5-aSmallNumber, 510 | //lower left 511 | -0.6-aSmallNumber,-0.5-aSmallNumber, 512 | //lower right 513 | 0.6-aSmallNumber,-0.5-aSmallNumber, 514 | 515 | //that's 3 vertices, making 1 triangle 516 | //next set of vertices for the 2nd triangle: 517 | 518 | //top left 519 | -0.6+aSmallNumber,0.5+aSmallNumber, 520 | //top right 521 | 0.6+aSmallNumber,0.5+aSmallNumber, 522 | //lower right 523 | 0.6+aSmallNumber,-0.5+aSmallNumber, 524 | ] 525 | ``` 526 | Let's go back to the normal rectangle. Notice how the resulting rectangle has 4 vertices, but we have to use 6 to render it! There are 2 sets of 2 vertices with the same position in the mesh, but they are also mandatory in order to correctly render the triangles. The vertex shader's computations run once per vertex, so many unnecessary vertices will be bad for performance. How can we reduce the amount of excess vertices and computations while still being able to render a rectangle?

The answer is _indexed rendering_! It is a very simple and efficient method, and should always be used whenever possible!

In indexed rendering, we provide 2 buffers. In this example, the vertex buffer stays the same, but with the excess vertices discarded. The second buffer is called the _index buffer_. It is a list of integers that specifies how vertices are connected to form triangles.

We'll start by updating our vertex array, removing the duplicate vertices. We should have 4 vertices that will become the rectangle's corners: 527 | ``` 528 | let verts=[ 529 | 530 | //top left 531 | -0.6,0.5, 532 | 533 | //lower left 534 | -0.6,-0.5, 535 | 536 | //lower right 537 | 0.6,-0.5, 538 | 539 | //top right 540 | 0.6,0.5, 541 | ] 542 | ``` 543 | Now, we create the index buffer. Similar to the process of creating the vertex buffer, we first define an index array that uses integers to indicate how the vertices are connected. 544 | ``` 545 | //each integer references a specific vertex from the "verts" array 546 | let index=[ 547 | 548 | //1st triangle 549 | //top left, lower left, and lower right corners 550 | 0,1,2, 551 | 552 | //2nd triangle 553 | //top left, lower right, and top right corners 554 | 0,2,3 555 | 556 | ] 557 | ``` 558 | A very useful pattern to memorize is the "0, 1, 2, 0, 2, 3" pattern. We will often see this pattern when using indexed rendering, and it'll be useful once we start making more complicated meshes.

Next, create the index buffer. The process is similar to creating the vertex buffer, but with 2 small changes. 559 | ``` 560 | //creates the index buffer 561 | let indexBuffer=gl.createBuffer() 562 | 563 | //tells webgl that we are performing stuff on this specific buffer 564 | //gl.ELEMENT_ARRAY_BUFFER indicates that this buffer is for index data 565 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer) 566 | 567 | //sets data in the buffer using the mesh. the inputted data must be a Uint16Array 568 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(index),gl.STATIC_DRAW) 569 | ``` 570 | We're almost done! The attribute pointers and uniforms(if there are any) won't need to be changed.

Now, we update the draw call function. The _"gl.drawArrays()"_ will be replaced with the _"gl.drawElements()"_ function. Then we set the proper parameters. 571 | ``` 572 | //1st param: type of primitive being drawn 573 | //2nd param: amount of vertices to be rendered(vertex array's length won't matter with indexed rendering) 574 | //3rd param: the type of data(often kept as gl.UNSIGNED_SHORT) 575 | //4th param: offset(often kept as 0) 576 | gl.drawElements(gl.TRIANGLES,index.length,gl.UNSIGNED_SHORT,0) 577 | ``` 578 | You should see a rectangle. With indexed rendering, the amount of vertices computed is 4, with no excess duplicate vertices! 579 | 580 | ##
2D Matrices & Transformations
581 |
In this section, we're going to learn how matrices work and how to apply transformations with them.

Matrices are basically grids of numbers that can be used to transform vectors(translate, scale, rotate). In 2D programs, 3x3 matrices are used as they are able to perform translation transformations. 2x2 matrices can only scale and rotate in 2D.

Get started with the code from the **Indexed Rendering** section. We are first going to add vertex colors(see **Varyings & More Attributes**) and a matrix uniform.

Updated shaders: 582 | 583 | ``` 584 | let vertexShaderCode=`#version 300 es 585 | 586 | precision mediump float; 587 | 588 | in vec2 vertPos; 589 | in vec3 vertColor; 590 | 591 | //the matrix for transformations 592 | uniform mat3 modelMatrix; 593 | 594 | out vec3 pixColor; 595 | 596 | void main(){ 597 | 598 | pixColor=vertColor; 599 | 600 | //transform the vec2 by turning it into a vec3 then multiply with the mat3 601 | //gl_Position needs to be a vec4 so we add another useless component 602 | gl_Position=vec4(modelMatrix*vec3(vertPos,1),1); 603 | } 604 | ` 605 | 606 | let fragmentShaderCode=`#version 300 es 607 | 608 | precision mediump float; 609 | 610 | in vec3 pixColor; 611 | 612 | out vec4 fragColor; 613 | 614 | void main(){ 615 | 616 | fragColor=vec4(pixColor,1); 617 | } 618 | ` 619 | ``` 620 | Updated mesh: 621 | ``` 622 | let verts=[ 623 | 624 | //top left, red 625 | -0.5,0.5, 1,0,0, 626 | //bottom left, green 627 | -0.5,-0.5, 0,1,0, 628 | //bottom right, blue 629 | 0.5,-0.5, 0,0,1, 630 | //top right front, yellow 631 | 0.5,0.5, 1,1,0, 632 | ] 633 | 634 | let index=[ 635 | 636 | //front side 637 | 0,1,2, 638 | 0,2,3 639 | 640 | ] 641 | ``` 642 | Buffer creation stays the same. Updated attribute locations and pointers: 643 | ``` 644 | let vertPosLocation=gl.getAttribLocation(program,'vertPos') 645 | gl.enableVertexAttribArray(vertPosLocation) 646 | 647 | let vertColorLocation=gl.getAttribLocation(program,'vertColor') 648 | gl.enableVertexAttribArray(vertColorLocation) 649 | 650 | //bytes per vertex. the total amount of values per a vertex(now it's 5(x,y,r,g,b)) multiplied by 4(which is the amount of bytes in a float32) 651 | let bpv=20 652 | 653 | //2 values for the position, 0 bytes before the position values 654 | gl.vertexAttribPointer(vertPosLocation,2,gl.FLOAT,gl.FALSE,bpv,0) 655 | 656 | //3 values for the color, 2 values(x & y coords) * 4 bytes per value = 8 bytes before the color values 657 | gl.vertexAttribPointer(vertColorLocation,3,gl.FLOAT,gl.FALSE,bpv,8) 658 | ``` 659 | Getting and setting the matrix uniform: 660 | ``` 661 | let modelMatrixLocation=gl.getUniformLocation(program,'modelMatrix') 662 | 663 | //currently an identity matrix, which applies no transformations 664 | let modelMatrix=new Float32Array([ 665 | 666 | 1,0,0, 667 | 0,1,0, 668 | 0,0,1 669 | ]) 670 | 671 | //use gl.uniformMatrix3fv for 3x3 matrices 672 | gl.uniformMatrix3fv(modelMatrixLocation,false,modelMatrix) 673 | ``` 674 | After adding the changes, you should see a multi-colored square.

Now, we're going to get started with matrix math. You can perform simple operations like addition, subtraction, and multiplication on matrices.

In WebGL graphics, only matrix multiplication is required. Multiplying matrices "stack" transformations on top of each other. The code for multiplying 2 3x3 matrices follows: 675 | ``` 676 | //params "out": the output matrix(array) 677 | //params "a": a matrix to be multiplied with "b" 678 | //params "b": a matrix to be multiplied with "a" 679 | 680 | function mult3x3Mat(out, a, b) { 681 | 682 | let a00 = a[0], 683 | a01 = a[1], 684 | a02 = a[2]; 685 | 686 | let a10 = a[3], 687 | a11 = a[4], 688 | a12 = a[5]; 689 | 690 | let a20 = a[6], 691 | a21 = a[7], 692 | a22 = a[8]; 693 | 694 | let b00 = b[0], 695 | b01 = b[1], 696 | b02 = b[2]; 697 | 698 | let b10 = b[3], 699 | b11 = b[4], 700 | b12 = b[5]; 701 | 702 | let b20 = b[6], 703 | b21 = b[7], 704 | b22 = b[8]; 705 | 706 | out[0] = b00 * a00 + b01 * a10 + b02 * a20; 707 | out[1] = b00 * a01 + b01 * a11 + b02 * a21; 708 | out[2] = b00 * a02 + b01 * a12 + b02 * a22; 709 | out[3] = b10 * a00 + b11 * a10 + b12 * a20; 710 | out[4] = b10 * a01 + b11 * a11 + b12 * a21; 711 | out[5] = b10 * a02 + b11 * a12 + b12 * a22; 712 | out[6] = b20 * a00 + b21 * a10 + b22 * a20; 713 | out[7] = b20 * a01 + b21 * a11 + b22 * a21; 714 | out[8] = b20 * a02 + b21 * a12 + b22 * a22; 715 | 716 | return out; 717 | } 718 | 719 | 720 | @ Credits to glMatrix 721 | ``` 722 | To apply transformations, you can directly perform a transformation on a matrix, or multiply a matrix with another matrix with the the transformations already in place.

Often, it's best to reduce matrix operations for performance, by computing them once with JS, and then uniforming them to the GPU. The technique of directly applying transformations on a single matrix is favored. It is done by manually optimizing and simplifying the process of creating an alternate matrix with the transformations and multiplying. This means that matrix multiplication may be used less often or not at all.

Small note: matrix naming may vary a lot. There are many ways to name matrices depending on what information they contain. It's often best to just stick to your own naming preferences.

Here are ways to transform 3x3 matrices: 723 | ``` 724 | function translate3x3Mat(out, a, x, y) { 725 | 726 | let a00 = a[0], 727 | a01 = a[1], 728 | a02 = a[2], 729 | a10 = a[3], 730 | a11 = a[4], 731 | a12 = a[5], 732 | a20 = a[6], 733 | a21 = a[7], 734 | a22 = a[8]; 735 | 736 | out[0] = a00; 737 | out[1] = a01; 738 | out[2] = a02; 739 | out[3] = a10; 740 | out[4] = a11; 741 | out[5] = a12; 742 | out[6] = x * a00 + y * a10 + a20; 743 | out[7] = x * a01 + y * a11 + a21; 744 | out[8] = x * a02 + y * a12 + a22; 745 | 746 | return out; 747 | } 748 | 749 | function rotate3x3Mat(out, a, rad) { 750 | 751 | let a00 = a[0], 752 | a01 = a[1], 753 | a02 = a[2], 754 | a10 = a[3], 755 | a11 = a[4], 756 | a12 = a[5], 757 | a20 = a[6], 758 | a21 = a[7], 759 | a22 = a[8], 760 | s = Math.sin(rad), 761 | c = Math.cos(rad); 762 | 763 | out[0] = c * a00 + s * a10; 764 | out[1] = c * a01 + s * a11; 765 | out[2] = c * a02 + s * a12; 766 | out[3] = c * a10 - s * a00; 767 | out[4] = c * a11 - s * a01; 768 | out[5] = c * a12 - s * a02; 769 | out[6] = a20; 770 | out[7] = a21; 771 | out[8] = a22; 772 | 773 | return out; 774 | } 775 | 776 | function scale3x3Mat(out, a, x, y) { 777 | 778 | out[0] = x * a[0]; 779 | out[1] = x * a[1]; 780 | out[2] = x * a[2]; 781 | out[3] = y * a[3]; 782 | out[4] = y * a[4]; 783 | out[5] = y * a[5]; 784 | out[6] = a[6]; 785 | out[7] = a[7]; 786 | out[8] = a[8]; 787 | 788 | return out; 789 | } 790 | 791 | 792 | @ Credits to glMatrix 793 | ``` 794 | Because matrix transformations stack on top of each other, transformations applied are affected by previous transformations. For example, if you translate a mesh and rotate it, it will rotate about its center. If you rotate it then translate it, it will be rotated then translated, but the translation vector will be rotated too. This is the same for transformations done by matrix multiplication(they are the same process, but multiplication is simplified into the direct transformations).

With the functions in place, you can now transform your geometry. Here's an example of what you can do to the model matrix: 795 | ``` 796 | let x=0.3, 797 | y=-0.3, 798 | rot=3.5, 799 | sx=0.7, 800 | sy=0.5 801 | 802 | translate3x3Mat(modelMatrix,modelMatrix,x,y) 803 | rotate3x3Mat(modelMatrix,modelMatrix,rot) 804 | scale3x3Mat(modelMatrix,modelMatrix,sx,sy) 805 | ``` 806 | You can change the numbers to see the effects on the mesh. You can also change the mesh by reordering the transformation operations.

Important! 807 | - The "default" matrix is called an identity matrix, and goes like 808 | ``` 809 | [ 810 | 1,0,0, 811 | 0,1,0, 812 | 0,0,1 813 | ] 814 | ``` 815 | as you already saw above. It applies no transformations with transforming a vector and does nothing when multiplied with a matrix. Remember this! 816 | - When providing a matrix as a uniform, make sure the supplied matrix is a typed array! Float32Array is very commonly used. This will result in a large performance boost, possible up to x9 speed! 817 | 818 | ##
3D Transformations
819 | 820 |
This section is the first part of 3D graphics. The next part is the **3D Graphics** section.

3D graphics are quite simple if you can understand the matrix math. Essentially, matrix transformation moves, rotates, and projects vertices, resulting in a new position vector. Perspective is applied automatically by WebGL, and that's basically it.

The complicated parts of 3D graphics with WebGL(especially WebGL2) are the techniques you can utilize to improve quality and performance.

To get started with 3D transformations, we need to make a 3D mesh. Starting with the code from the **2D Matrices & Transformations** section, turn the attribute _"vertPos"_ into a vec3 and supply another number(the z position) for each vertex's position. We also need to turn the mat3 _"modelMatrix"_ into a mat4. 821 | ``` 822 | let vertexShaderCode=`#version 300 es 823 | 824 | precision mediump float; 825 | 826 | //vertPos is now a vec3! x, y, and z values! 827 | in vec3 vertPos; 828 | 829 | in vec3 vertColor; 830 | 831 | //the model matrix is now a mat4! 4x4 matrices are needed for 3D stuff 832 | uniform mat4 modelMatrix; 833 | 834 | out vec3 pixColor; 835 | 836 | void main(){ 837 | 838 | pixColor=vertColor; 839 | 840 | //transform the vec3 by turning it into a vec4 then multiply with the mat4 841 | gl_Position=modelMatrix*vec4(vertPos,1); 842 | } 843 | ` 844 | ``` 845 | The new vertex buffer, now with z positions for each vertex. The z positions right now are set as -0.5, which is a bit closer to the camera. 846 | ``` 847 | let verts=[ 848 | 849 | //top left front, red 850 | -0.5,0.5,-0.5, 1,0,0, 851 | //bottom left front, green 852 | -0.5,-0.5,-0.5, 0,1,0, 853 | //bottom right front, blue 854 | 0.5,-0.5,-0.5, 0,0,1, 855 | //top right front, yellow 856 | 0.5,0.5,-0.5, 1,1,0, 857 | ] 858 | ``` 859 | Buffer creation stays the same, of course. Now we update the attribute pointers to specify the new format of the mesh's vertices. 860 | ``` 861 | //bytes per vertex. the total amount of values per a vertex(now it's 6(x,y,z,r,g,b)) multiplied by 4(which is the amount of bytes in a float32) 862 | let bpv=24 863 | 864 | //3 values for the position, 0 bytes before the position values 865 | gl.vertexAttribPointer(vertPosLocation,3,gl.FLOAT,gl.FALSE,bpv,0) 866 | 867 | //3 values for the color, 3 values(x & y & z coords) * 4 bytes per value = 12 bytes before the color values 868 | gl.vertexAttribPointer(vertColorLocation,3,gl.FLOAT,gl.FALSE,bpv,12) 869 | ``` 870 | The _"modelMatrix"_ is now a 4x4 matrix. We set it to an identity matrix for now. 871 | ``` 872 | //currently an identity matrix, which applies no transformations 873 | let modelMatrix=new Float32Array([ 874 | 875 | 1,0,0,0, 876 | 0,1,0,0, 877 | 0,0,1,0, 878 | 0,0,0,1 879 | ]) 880 | 881 | //use gl.uniformMatrix4fv for 4x4 matrices 882 | gl.uniformMatrix4fv(modelMatrixLocation,false,modelMatrix) 883 | ``` 884 | The _"gl.drawElements()"_ function stays the same. Now, you should see the same square again.

Transformations with 4x4 matrices are similar to 3x3 ones. We use 3x3 matrices for 2D, and then 4x4 matrices for 3D. In 3D, transformations and matrix operations are nearly the same.

Scale and translation transformations now use x, y, and z values or all the axes, instead of just x and y. However, in 3D, you have 3 axes you can rotate objects about. Here is the code for each types of useful 4x4 operations and transformations: 885 | ``` 886 | function mult4x4Mat(out, a, b) { 887 | 888 | let a00 = a[0], 889 | a01 = a[1], 890 | a02 = a[2], 891 | a03 = a[3]; 892 | 893 | let a10 = a[4], 894 | a11 = a[5], 895 | a12 = a[6], 896 | a13 = a[7]; 897 | 898 | let a20 = a[8], 899 | a21 = a[9], 900 | a22 = a[10], 901 | a23 = a[11]; 902 | 903 | let a30 = a[12], 904 | a31 = a[13], 905 | a32 = a[14], 906 | a33 = a[15]; 907 | 908 | let b0 = b[0], 909 | b1 = b[1], 910 | b2 = b[2], 911 | b3 = b[3]; 912 | 913 | out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 914 | out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 915 | out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 916 | out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 917 | 918 | b0 = b[4]; 919 | b1 = b[5]; 920 | b2 = b[6]; 921 | b3 = b[7]; 922 | 923 | out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 924 | out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 925 | out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 926 | out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 927 | 928 | b0 = b[8]; 929 | b1 = b[9]; 930 | b2 = b[10]; 931 | b3 = b[11]; 932 | 933 | out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 934 | out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 935 | out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 936 | out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 937 | 938 | b0 = b[12]; 939 | b1 = b[13]; 940 | b2 = b[14]; 941 | b3 = b[15]; 942 | 943 | out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; 944 | out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; 945 | out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; 946 | out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; 947 | 948 | return out; 949 | } 950 | 951 | function translate4x4Mat(out, a, x, y, z) { 952 | 953 | let a00, a01, a02, a03; 954 | let a10, a11, a12, a13; 955 | let a20, a21, a22, a23; 956 | 957 | if (a === out) { 958 | 959 | out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; 960 | out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; 961 | out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; 962 | out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; 963 | 964 | } else { 965 | 966 | a00 = a[0]; 967 | a01 = a[1]; 968 | a02 = a[2]; 969 | a03 = a[3]; 970 | a10 = a[4]; 971 | a11 = a[5]; 972 | a12 = a[6]; 973 | a13 = a[7]; 974 | a20 = a[8]; 975 | a21 = a[9]; 976 | a22 = a[10]; 977 | a23 = a[11]; 978 | out[0] = a00; 979 | out[1] = a01; 980 | out[2] = a02; 981 | out[3] = a03; 982 | out[4] = a10; 983 | out[5] = a11; 984 | out[6] = a12; 985 | out[7] = a13; 986 | out[8] = a20; 987 | out[9] = a21; 988 | out[10] = a22; 989 | out[11] = a23; 990 | out[12] = a00 * x + a10 * y + a20 * z + a[12]; 991 | out[13] = a01 * x + a11 * y + a21 * z + a[13]; 992 | out[14] = a02 * x + a12 * y + a22 * z + a[14]; 993 | out[15] = a03 * x + a13 * y + a23 * z + a[15]; 994 | 995 | } 996 | 997 | return out; 998 | } 999 | 1000 | function rotateX4x4Mat(out, a, rad) { 1001 | 1002 | let s = Math.sin(rad); 1003 | let c = Math.cos(rad); 1004 | let a10 = a[4]; 1005 | let a11 = a[5]; 1006 | let a12 = a[6]; 1007 | let a13 = a[7]; 1008 | let a20 = a[8]; 1009 | let a21 = a[9]; 1010 | let a22 = a[10]; 1011 | let a23 = a[11]; 1012 | 1013 | if (a !== out) { 1014 | 1015 | out[0] = a[0]; 1016 | out[1] = a[1]; 1017 | out[2] = a[2]; 1018 | out[3] = a[3]; 1019 | out[12] = a[12]; 1020 | out[13] = a[13]; 1021 | out[14] = a[14]; 1022 | out[15] = a[15]; 1023 | } 1024 | 1025 | out[4] = a10 * c + a20 * s; 1026 | out[5] = a11 * c + a21 * s; 1027 | out[6] = a12 * c + a22 * s; 1028 | out[7] = a13 * c + a23 * s; 1029 | out[8] = a20 * c - a10 * s; 1030 | out[9] = a21 * c - a11 * s; 1031 | out[10] = a22 * c - a12 * s; 1032 | out[11] = a23 * c - a13 * s; 1033 | 1034 | return out; 1035 | } 1036 | 1037 | function rotateY4x4Mat(out, a, rad) { 1038 | 1039 | let s = Math.sin(rad); 1040 | let c = Math.cos(rad); 1041 | 1042 | let a00 = a[0]; 1043 | let a01 = a[1]; 1044 | let a02 = a[2]; 1045 | let a03 = a[3]; 1046 | let a20 = a[8]; 1047 | let a21 = a[9]; 1048 | let a22 = a[10]; 1049 | let a23 = a[11]; 1050 | 1051 | if (a !== out) { 1052 | 1053 | out[4] = a[4]; 1054 | out[5] = a[5]; 1055 | out[6] = a[6]; 1056 | out[7] = a[7]; 1057 | out[12] = a[12]; 1058 | out[13] = a[13]; 1059 | out[14] = a[14]; 1060 | out[15] = a[15]; 1061 | } 1062 | 1063 | out[0] = a00 * c - a20 * s; 1064 | out[1] = a01 * c - a21 * s; 1065 | out[2] = a02 * c - a22 * s; 1066 | out[3] = a03 * c - a23 * s; 1067 | out[8] = a00 * s + a20 * c; 1068 | out[9] = a01 * s + a21 * c; 1069 | out[10] = a02 * s + a22 * c; 1070 | out[11] = a03 * s + a23 * c; 1071 | 1072 | return out; 1073 | } 1074 | 1075 | function rotateZ4x4Mat(out, a, rad) { 1076 | 1077 | let s = Math.sin(rad); 1078 | let c = Math.cos(rad); 1079 | 1080 | let a00 = a[0]; 1081 | let a01 = a[1]; 1082 | let a02 = a[2]; 1083 | let a03 = a[3]; 1084 | let a10 = a[4]; 1085 | let a11 = a[5]; 1086 | let a12 = a[6]; 1087 | let a13 = a[7]; 1088 | 1089 | if (a !== out) { 1090 | 1091 | out[8] = a[8]; 1092 | out[9] = a[9]; 1093 | out[10] = a[10]; 1094 | out[11] = a[11]; 1095 | out[12] = a[12]; 1096 | out[13] = a[13]; 1097 | out[14] = a[14]; 1098 | out[15] = a[15]; 1099 | } 1100 | 1101 | out[0] = a00 * c + a10 * s; 1102 | out[1] = a01 * c + a11 * s; 1103 | out[2] = a02 * c + a12 * s; 1104 | out[3] = a03 * c + a13 * s; 1105 | out[4] = a10 * c - a00 * s; 1106 | out[5] = a11 * c - a01 * s; 1107 | out[6] = a12 * c - a02 * s; 1108 | out[7] = a13 * c - a03 * s; 1109 | 1110 | return out; 1111 | } 1112 | 1113 | function scale4x4Mat(out, a, x, y, z) { 1114 | 1115 | out[0] = a[0] * x; 1116 | out[1] = a[1] * x; 1117 | out[2] = a[2] * x; 1118 | out[3] = a[3] * x; 1119 | out[4] = a[4] * y; 1120 | out[5] = a[5] * y; 1121 | out[6] = a[6] * y; 1122 | out[7] = a[7] * y; 1123 | out[8] = a[8] * z; 1124 | out[9] = a[9] * z; 1125 | out[10] = a[10] * z; 1126 | out[11] = a[11] * z; 1127 | out[12] = a[12]; 1128 | out[13] = a[13]; 1129 | out[14] = a[14]; 1130 | out[15] = a[15]; 1131 | 1132 | return out; 1133 | } 1134 | 1135 | 1136 | @ Credits to glMatrix 1137 | ``` 1138 | With the transformation functions in place, you can now transform the mesh. 1139 | ``` 1140 | let rx=0, 1141 | ry=0.8, 1142 | rz=0 1143 | 1144 | rotateX4x4Mat(modelMatrix,modelMatrix,rx) 1145 | rotateY4x4Mat(modelMatrix,modelMatrix,ry) 1146 | rotateZ4x4Mat(modelMatrix,modelMatrix,rz) 1147 | ``` 1148 | It's hard to tell what the 3D transformations do, so let's animate it. 1149 | ``` 1150 | let dt,then=0,time=0 1151 | 1152 | function loop(now){ 1153 | 1154 | dt=(now-then)*0.001 1155 | time+=dt 1156 | 1157 | gl.clearColor(0.1,0,0,1) 1158 | gl.clear(gl.COLOR_BUFFER_BIT) 1159 | 1160 | let modelMatrix=new Float32Array([ 1161 | 1162 | 1,0,0,0, 1163 | 0,1,0,0, 1164 | 0,0,1,0, 1165 | 0,0,0,1 1166 | ]) 1167 | 1168 | let rx=time*0.1, 1169 | ry=time, 1170 | rz=0 1171 | 1172 | rotateX4x4Mat(modelMatrix,modelMatrix,rx) 1173 | rotateY4x4Mat(modelMatrix,modelMatrix,ry) 1174 | rotateZ4x4Mat(modelMatrix,modelMatrix,rz) 1175 | 1176 | gl.uniformMatrix4fv(modelMatrixLocation,false,modelMatrix) 1177 | 1178 | gl.drawElements(gl.TRIANGLES,index.length,gl.UNSIGNED_SHORT,0) 1179 | 1180 | 1181 | then=now 1182 | window.requestAnimationFrame(loop) 1183 | } 1184 | 1185 | loop(0) 1186 | ``` 1187 | It's still quite difficult to see, but with some imagination, you can make a sense of what is happening.

In the next section, we'll add cameras and perspective to create actual 3D graphics. 1188 | 1189 | ##
3D Graphics
1190 | 1191 |
In this section, we are going to create a basic cube.

First of all, a vital part of 3D programs are the backface culling and depth testing features. Backface culling gets rid of unnecessary triangles facing away from the camera, which will speed up rendering by 2x! Depth testing decides if a fragment should be drawn if there is an existing fragment in front of it. This will get rid of weird clipping effects.

You can enable these features anywhere outside the render loop: 1192 | ``` 1193 | gl.enable(gl.CULL_FACE) 1194 | gl.enable(gl.DEPTH_TEST) 1195 | 1196 | 1197 | //this part below is optional! these features are the default settings 1198 | //it's unlikely you'll need to use settings other than these 1199 | 1200 | gl.cullFace(gl.BACK) 1201 | gl.depthFunc(gl.LEQUAL) 1202 | ``` 1203 | You will also need to update the _"gl.clear()"_ function: 1204 | ``` 1205 | gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT) 1206 | ``` 1207 | It's time to add a camera to properly render the scene.

There are 2 main types of 3D projection: perspective and orthogonal. Orthogonal projection makes objects farther away appear at the same size, unlike perspective projection. Perspective projection resembles our eyes in real life, where distant objects shrink in size.

Projection is done in the vertex shader by multiplying the vertex transformation matrix with the projection matrix. The transformed vector is a vec4 with the w component containing the depth of the object. The WebGL pipeline automatically divides the set _"gl\_Position"_ value with _"gl\_Position.w"_. This is called perspective division.

Since most 3D programs use realistic perspective projection, we'll use that too. However, changing between the types of projection is easy, as you only need to change the projection matrix. Here's the matrix for perspective projection: 1208 | ``` 1209 | //fov: field of view in degrees 1210 | //aspect: canvas width divided by height 1211 | //zn: nearest distance camera can render 1212 | //zf: farthest distance camera can render 1213 | //make sure zn is not 0 and zf is not a giant number(1,000 is often fine)! 1214 | //limiting the distance you render and help with performance 1215 | 1216 | function perspectiveMat(fov,aspect,zn,zf){ 1217 | 1218 | let f=Math.tan(1.57079632679-fov*0.008726646), 1219 | rangeInv=1/(zn-zf) 1220 | 1221 | return new Float32Array([ 1222 | f/aspect,0,0,0, 1223 | 0,f,0,0, 1224 | 0,0,(zn+zf)*rangeInv,-1, 1225 | 0,0,zn*zf*rangeInv*2,0 1226 | ]) 1227 | } 1228 | ``` 1229 | Create the projection matrix: 1230 | ``` 1231 | let projectionMatrix=perspectiveMat(60,canvas.width/canvas.height,0.1,1000) 1232 | ``` 1233 | The updated _"modelMatrix"_ in the render loop: 1234 | ``` 1235 | //notice that here we apply translation before rotation! 1236 | //normally, we rotate first, and then translate to create an actual camera transformation matrix(given rotation and position) 1237 | //here, translation is applied first so that the camera "orbits" around the shape given the rotation 1238 | translate4x4Mat(modelMatrix,modelMatrix,0,0,-3) 1239 | 1240 | rotateX4x4Mat(modelMatrix,modelMatrix,rx) 1241 | rotateY4x4Mat(modelMatrix,modelMatrix,ry) 1242 | rotateZ4x4Mat(modelMatrix,modelMatrix,rz) 1243 | 1244 | //matrix multiplication isn't commutative! mat1*mat2 != mat2*mat1 1245 | //switch the "projectionMatrix" and "modelMatrix" multiplication around and stuff will break! 1246 | mult4x4Mat(modelMatrix,projectionMatrix,modelMatrix) 1247 | ``` 1248 | Almost done! Now, the only thing left to do is to create a cube mesh to render: 1249 | ``` 1250 | let verts=[ 1251 | 1252 | //front side 1253 | -0.5,0.5,-0.5, 0,1,0, 1254 | -0.5,-0.5,-0.5, 0,1,0, 1255 | 0.5,-0.5,-0.5, 0,1,0, 1256 | 0.5,0.5,-0.5, 0,1,0, 1257 | 1258 | //back side 1259 | -0.5,0.5,0.5, 1,1,0, 1260 | -0.5,-0.5,0.5, 1,1,0, 1261 | 0.5,-0.5,0.5, 1,1,0, 1262 | 0.5,0.5,0.5, 1,1,0, 1263 | 1264 | //top side 1265 | -0.5,0.5,0.5, 1,0,0, 1266 | -0.5,0.5,-0.5, 1,0,0, 1267 | 0.5,0.5,-0.5, 1,0,0, 1268 | 0.5,0.5,0.5, 1,0,0, 1269 | 1270 | //bottom side 1271 | -0.5,-0.5,0.5, 1,0,1, 1272 | -0.5,-0.5,-0.5, 1,0,1, 1273 | 0.5,-0.5,-0.5, 1,0,1, 1274 | 0.5,-0.5,0.5, 1,0,1, 1275 | 1276 | //left side 1277 | -0.5,0.5,-0.5, 0,0,1, 1278 | -0.5,-0.5,-0.5, 0,0,1, 1279 | -0.5,-0.5,0.5, 0,0,1, 1280 | -0.5,0.5,0.5, 0,0,1, 1281 | 1282 | //right side 1283 | 0.5,0.5,-0.5, 0,1,1, 1284 | 0.5,-0.5,-0.5, 0,1,1, 1285 | 0.5,-0.5,0.5, 0,1,1, 1286 | 0.5,0.5,0.5, 0,1,1, 1287 | ] 1288 | 1289 | let index=[ 1290 | 1291 | //front side 1292 | 2,1,0, 1293 | 3,2,0, 1294 | 1295 | //back side 1296 | 4,5,6, 1297 | 4,6,7, 1298 | 1299 | //top side 1300 | 10,9,8, 1301 | 11,10,8, 1302 | 1303 | //bottom side 1304 | 12,13,14, 1305 | 12,14,15, 1306 | 1307 | //left side 1308 | 16,17,18, 1309 | 16,18,19, 1310 | 1311 | //right side 1312 | 22,21,20, 1313 | 23,22,20, 1314 | ] 1315 | ``` 1316 | It's done! You should see a spinning cube with differently colored sides! Notice that in the mesh, we have to make separate vertices for each side of the cube, each with their own color. However, with indexed rendering, we still are saving computations as we only need 4 vertices for each side(as opposed to 6). 4 vertices are shared within the 2 triangles for each face and so 2 are reused.

In the next section, we will learn how to apply lighting. 1317 | 1318 | ##
Lighting and Normals
1319 | 1320 |
In 3D graphics, normals are essential in lighting computations. A normal is a normalized vector that points in the direction that a surface is facing in. At any given pixel of a rendered mesh, the face the pixel is in has a direction it's pointing towards.

There's no reliable and simple way to compute what a normal vector is at a point, so we need to provide normals from the mesh. We can transfer the provided normal vector into the fragment shader using varyings. This will also be beneficial for more complicated meshes where surfaces need to look smooth.

Let's start with the code from the **3D Graphics** section. First, we will make the mesh faces the same color to see the shading better. Then, add the normal of the face that each vertex is in. The format of the vertex array is now x1,y1,z1,r1,g1,b1,nx1,ny1,nz1,x2,y2,z2,r2... and so on 1321 | ``` 1322 | let verts=[ 1323 | 1324 | //front side, normal faces towards 1325 | -0.5,0.5,-0.5, 0,1,0, 0,0,-1, 1326 | -0.5,-0.5,-0.5, 0,1,0, 0,0,-1, 1327 | 0.5,-0.5,-0.5, 0,1,0, 0,0,-1, 1328 | 0.5,0.5,-0.5, 0,1,0, 0,0,-1, 1329 | 1330 | //back side, normal faces away 1331 | -0.5,0.5,0.5, 0,1,0, 0,0,1, 1332 | -0.5,-0.5,0.5, 0,1,0, 0,0,1, 1333 | 0.5,-0.5,0.5, 0,1,0, 0,0,1, 1334 | 0.5,0.5,0.5, 0,1,0, 0,0,1, 1335 | 1336 | //top side, normal faces up 1337 | -0.5,0.5,0.5, 0,1,0, 0,1,0, 1338 | -0.5,0.5,-0.5, 0,1,0, 0,1,0, 1339 | 0.5,0.5,-0.5, 0,1,0, 0,1,0, 1340 | 0.5,0.5,0.5, 0,1,0, 0,1,0, 1341 | 1342 | //bottom side, normal faces down 1343 | -0.5,-0.5,0.5, 0,1,0, 0,-1,0, 1344 | -0.5,-0.5,-0.5, 0,1,0, 0,-1,0, 1345 | 0.5,-0.5,-0.5, 0,1,0, 0,-1,0, 1346 | 0.5,-0.5,0.5, 0,1,0, 0,-1,0, 1347 | 1348 | //left side, normal faces left 1349 | -0.5,0.5,-0.5, 0,1,0, -1,0,0, 1350 | -0.5,-0.5,-0.5, 0,1,0, -1,0,0, 1351 | -0.5,-0.5,0.5, 0,1,0, -1,0,0, 1352 | -0.5,0.5,0.5, 0,1,0, -1,0,0, 1353 | 1354 | //right side, normal faces right 1355 | 0.5,0.5,-0.5, 0,1,0, 1,0,0, 1356 | 0.5,-0.5,-0.5, 0,1,0, 1,0,0, 1357 | 0.5,-0.5,0.5, 0,1,0, 1,0,0, 1358 | 0.5,0.5,0.5, 0,1,0, 1,0,0, 1359 | ] 1360 | ``` 1361 | Get the normal attribute's location(we will add it to the vertex shader later): 1362 | ``` 1363 | let vertNormalLocation=gl.getAttribLocation(program,'vertNormal') 1364 | gl.enableVertexAttribArray(vertNormalLocation) 1365 | ``` 1366 | Updated vertex attribute pointers, adjusted for the new normal attributes: 1367 | ``` 1368 | //bytes per vertex. the total amount of values per a vertex(now it's 9(x,y,z,r,g,b,nx,ny,nz)) multiplied by 4(which is the amount of bytes in a float32) 1369 | let bpv=36 1370 | 1371 | //3 values for the position, 0 bytes before the position values 1372 | gl.vertexAttribPointer(vertPosLocation,3,gl.FLOAT,gl.FALSE,bpv,0) 1373 | 1374 | //3 values for the color, 3 values(x & y & z coords) * 4 bytes per value = 12 bytes before the color values 1375 | gl.vertexAttribPointer(vertColorLocation,3,gl.FLOAT,gl.FALSE,bpv,12) 1376 | 1377 | //3 values for the normal, 6 values(x & y & z & nx & ny & nz coords) * 4 bytes per value = 24 bytes before the color values 1378 | gl.vertexAttribPointer(vertNormalLocation,3,gl.FLOAT,gl.FALSE,bpv,24) 1379 | ``` 1380 | Here's the new vertex shader, with the normal attribute and varying added: 1381 | ``` 1382 | let vertexShaderCode=`#version 300 es 1383 | 1384 | precision mediump float; 1385 | 1386 | in vec3 vertPos; 1387 | in vec3 vertColor; 1388 | 1389 | //the vertex normal's attribute 1390 | in vec3 vertNormal; 1391 | 1392 | uniform mat4 modelMatrix; 1393 | 1394 | out vec3 pixColor; 1395 | 1396 | //transfer the normal to the fragment shader using a varying 1397 | out vec3 pixNormal; 1398 | 1399 | void main(){ 1400 | 1401 | pixColor=vertColor; 1402 | pixNormal=vertNormal; 1403 | 1404 | gl_Position=modelMatrix*vec4(vertPos,1); 1405 | } 1406 | ` 1407 | ``` 1408 | To compute lighting, we need to understand how dot products work. A dot product of 2 vec3s is computed as _"a.x\*b.x+a.y\*b.y+a.z\*b.z"_. If the 2 vectors are normalized, the dot product will be the cosine of the angle between them. Basically, if we normalize 2 vectors and take their dot product, the result is a number ranging from [-1,1] that tells how close they are to facing each other.

If 2 normalized vectors are in the same direction, the dot product is 1. If they are completely opposite of each other, the dot product is -1. If they are perpendicular, the dot product is 0.

We can use this number to multiply it by the fragment's color to darken it based on the normal and light direction. This is called directional lighting, because the light source has no point, but illuminates objects from a single direction.

Here's the fragment shader that applies directional lighting: 1409 | ``` 1410 | let fragmentShaderCode=`#version 300 es 1411 | 1412 | precision mediump float; 1413 | 1414 | in vec3 pixColor; 1415 | 1416 | //the vertex normal varying, transferred from the vertex shader 1417 | in vec3 pixNormal; 1418 | 1419 | out vec4 fragColor; 1420 | 1421 | void main(){ 1422 | 1423 | //the direction of the light 1424 | vec3 lightDir=normalize(vec3(-0.7,-1.5,-1)); 1425 | 1426 | //a measure of how different normal and light dir are(in terms of direction) 1427 | //the dot product ranges from [-1,1] so the "*0.5+0.5" remaps it to the [0,1] range 1428 | //faces facing directly away from the light will be lit 0%, and if towards the light, up to 100% 1429 | float diffuse=dot(-lightDir,pixNormal)*0.5+0.5; 1430 | 1431 | //because "diffuse" is between [0,1], multiplying it by the color values will decrease them, therefore darkening the color 1432 | fragColor=vec4(pixColor*diffuse,1); 1433 | } 1434 | ` 1435 | ``` 1436 | That's it for basic directional lighting! Your green box's sides should now be shaded according to the light direction.

If you want to learn more lighting techniques, there are many more useful resources available. 1437 | 1438 | ##
Textures
1439 | 1440 |
In WebGL, textures aren't really considered as "images" per se. They can be interpreted as grids or arrays of vec4(or sometimes other types) values, called texels(pixels but in textures. "texel" isn't used commonly, as "pixel" can still be used almost always). Textures are mainly used for texturing, but can also be utilized to hold and transfer large amounts of data to the fragment shader.

We're going to look at the most simple and common usage for textures: applying images onto meshes.

To access a pixel in a texture, _"UV"_ coordinates(also called texture coordinates) are used. UV coordinates are vec2s that specify a pixel from a texture. They always range from 0 to 1, no matter the width or height of the texture. A _"u"_ coordinate is like the x value of a texture coordinate. A _"v"_ coordinate is like the y value of a texture coordinate, and goes up the texture as it increases.

For each pixel in a face, we need to figure out its appropriate UV value to find out what pixel in the texture it corresponds to. To do this, we specify a UV coordinate at each vertex of a face and use varyings in interpolate and transfer them into the fragment shader, where the texture's color at that point can be looked up and applied.

To start of, we add another attribute, a vec2, to each vertex. This will contain the UV coordinates at the corresponding corner of the texture. 1441 | ``` 1442 | let verts=[ 1443 | 1444 | //front side 1445 | -0.5,0.5,-0.5, 0,1,0, 0,0,-1, 1,0, 1446 | -0.5,-0.5,-0.5, 0,1,0, 0,0,-1, 1,1, 1447 | 0.5,-0.5,-0.5, 0,1,0, 0,0,-1, 0,1, 1448 | 0.5,0.5,-0.5, 0,1,0, 0,0,-1, 0,0, 1449 | 1450 | //back side 1451 | -0.5,0.5,0.5, 0,1,0, 0,0,1, 0,0, 1452 | -0.5,-0.5,0.5, 0,1,0, 0,0,1, 0,1, 1453 | 0.5,-0.5,0.5, 0,1,0, 0,0,1, 1,1, 1454 | 0.5,0.5,0.5, 0,1,0, 0,0,1, 1,0, 1455 | 1456 | //top side 1457 | -0.5,0.5,0.5, 0,1,0, 0,1,0, 1,0, 1458 | -0.5,0.5,-0.5, 0,1,0, 0,1,0, 1,1, 1459 | 0.5,0.5,-0.5, 0,1,0, 0,1,0, 0,1, 1460 | 0.5,0.5,0.5, 0,1,0, 0,1,0, 0,0, 1461 | 1462 | //bottom side 1463 | -0.5,-0.5,0.5, 0,1,0, 0,-1,0, 1,0, 1464 | -0.5,-0.5,-0.5, 0,1,0, 0,-1,0, 1,1, 1465 | 0.5,-0.5,-0.5, 0,1,0, 0,-1,0, 0,1, 1466 | 0.5,-0.5,0.5, 0,1,0, 0,-1,0, 0,0, 1467 | 1468 | //left side 1469 | -0.5,0.5,-0.5, 0,1,0, -1,0,0, 0,0, 1470 | -0.5,-0.5,-0.5, 0,1,0, -1,0,0, 0,1, 1471 | -0.5,-0.5,0.5, 0,1,0, -1,0,0, 1,1, 1472 | -0.5,0.5,0.5, 0,1,0, -1,0,0, 1,0, 1473 | 1474 | //right side 1475 | 0.5,0.5,-0.5, 0,1,0, 1,0,0, 1,0, 1476 | 0.5,-0.5,-0.5, 0,1,0, 1,0,0, 1,1, 1477 | 0.5,-0.5,0.5, 0,1,0, 1,0,0, 0,1, 1478 | 0.5,0.5,0.5, 0,1,0, 1,0,0, 0,0, 1479 | ] 1480 | ``` 1481 | The vertex UV attribute's location: 1482 | ``` 1483 | let vertUVLocation=gl.getAttribLocation(program,'vertUV') 1484 | gl.enableVertexAttribArray(vertUVLocation) 1485 | ``` 1486 | Update the attribute pointers: 1487 | ``` 1488 | //bytes per vertex. the total amount of values per a vertex(now it's 11(x,y,z,r,g,b,nx,ny,nz,u,v)) multiplied by 4(which is the amount of bytes in a float32) 1489 | let bpv=44 1490 | 1491 | //3 values for the position, 0 bytes before the position values 1492 | gl.vertexAttribPointer(vertPosLocation,3,gl.FLOAT,gl.FALSE,bpv,0) 1493 | 1494 | //3 values for the color, 3 values(x & y & z coords) * 4 bytes per value = 12 bytes before the color values 1495 | gl.vertexAttribPointer(vertColorLocation,3,gl.FLOAT,gl.FALSE,bpv,12) 1496 | 1497 | //3 values for the normal, 6 values(x & y & z & nx & ny & nz coords) * 4 bytes per value = 24 bytes before the color values 1498 | gl.vertexAttribPointer(vertNormalLocation,3,gl.FLOAT,gl.FALSE,bpv,24) 1499 | 1500 | //2 values for the uv, 9 values(x & y & z & nx & ny & nz coords, r & g & b values) * 4 bytes per value = 36 bytes before the color values 1501 | gl.vertexAttribPointer(vertUVLocation,2,gl.FLOAT,gl.FALSE,bpv,36) 1502 | ``` 1503 | We now create the texture to use. We need to know the width, height, and _"imageData"_ of the texture. An _"imageData"_ is just a giant _"Uint8Array"_ that has 4 color values(RGBA values) for each pixel. _"imageData"_ pixels go from left to right, and up to down. We'll keep the texture simple for now. 1504 | ``` 1505 | //creates a texture 1506 | let texture=gl.createTexture() 1507 | 1508 | //binds the texture 1509 | gl.bindTexture(gl.TEXTURE_2D,texture) 1510 | 1511 | //defines what's in the texture 1512 | //params: texture type, level(almost always at 0), format, width, height, border(almost always 0), internal format, type, imageData 1513 | gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,2,2,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([0,0,0,0,0,0,0,100,0,0,0,100,0,0,0,0])) 1514 | 1515 | //texture filtering: specifies how texels are picked when uvs are not directly on a texel. 1516 | //most common settings: nearest and linear 1517 | //nearest picks the closest texel to the specified uv point 1518 | //linear blends the closest texels get a smoother, non-pixely texture 1519 | gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST) 1520 | gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST) 1521 | 1522 | //automatically creates several smaller versions of the texture to use when the rendered texture is smaller 1523 | //this will reduce pixely images and increase performance, but also will increase memory usage by 33.33% 1524 | gl.generateMipmap(gl.TEXTURE_2D) 1525 | ``` 1526 | And finally, add textures to the shaders. We input the texture as a _"uniform sampler2D"_. 1527 | ``` 1528 | let vertexShaderCode=`#version 300 es 1529 | 1530 | precision mediump float; 1531 | 1532 | in vec3 vertPos; 1533 | in vec3 vertColor; 1534 | in vec3 vertNormal; 1535 | 1536 | //the vertex uv attribute 1537 | in vec2 vertUV; 1538 | 1539 | uniform mat4 modelMatrix; 1540 | 1541 | out vec3 pixColor; 1542 | out vec3 pixNormal; 1543 | 1544 | //transfer and interpolate uvs using a varying 1545 | out vec2 pixUV; 1546 | 1547 | void main(){ 1548 | 1549 | pixColor=vertColor; 1550 | pixNormal=vertNormal; 1551 | pixUV=vertUV; 1552 | 1553 | gl_Position=modelMatrix*vec4(vertPos,1); 1554 | } 1555 | ` 1556 | 1557 | let fragmentShaderCode=`#version 300 es 1558 | 1559 | precision mediump float; 1560 | 1561 | in vec3 pixColor; 1562 | in vec3 pixNormal; 1563 | 1564 | //the vertex uv varying, interpolated and transferred from the vertex shader 1565 | in vec2 pixUV; 1566 | 1567 | uniform sampler2D tex; 1568 | 1569 | out vec4 fragColor; 1570 | 1571 | void main(){ 1572 | 1573 | vec3 lightDir=normalize(vec3(-0.7,-1.5,-1)); 1574 | float diffuse=dot(-lightDir,pixNormal)*0.5+0.5; 1575 | 1576 | //the surface color 1577 | vec3 surfaceColor=pixColor; 1578 | 1579 | //the texture's texel color for this fragment 1580 | //"texture" always outputs a vec4 1581 | vec4 textureColor=texture(tex,pixUV); 1582 | 1583 | //applies texture's color based on the texel's alpha value 1584 | //"mix" is linear interpolation and works with vectors 1585 | surfaceColor=mix(surfaceColor,textureColor.rgb,textureColor.a); 1586 | 1587 | fragColor=vec4(surfaceColor*diffuse,1); 1588 | } 1589 | ` 1590 | ``` 1591 | You should see the same spinning cube, but this time, a 2x2 checkerboard pattern on the faces. Lighting is also applied after the texture.

In some programs, vertex colors won't be needed(replaced with UVs) as the textures themselves provide the colors. However, in this program, we use both to get more flexibility, which is fine for performance.

Also, notice that we use _"uniform sampler2D tex;"_ in the fragment shader, but we never uniformed the texture! WebGL automatically uniforms the currently bound texture at the time of the draw call. You can still uniform the texture yourself, it's optional: 1592 | ``` 1593 | let textureLocation=gl.getUniformLocation(program,'tex') 1594 | 1595 | gl.uniform1i(textureLocation,texture) 1596 | ``` 1597 | When using WebGL1, usage of textures is much more limited. Texture sizes must be a power of 2 in order to generate mipmaps. WebGL2 gets rid of these terrible restrictions. 1598 | 1599 | Now, let's spice up our texture! Loading images and turning them into textures is easy, as you only need the width, height, and data of the image.

Instead of loading images, we can also draw and generate a texture directly in our program. Because we only need the _"imageData"_, we can draw onto an invisible canvas and get its _"imageData"_ to use as our texture.

We'll do this with the simple and common _"Canvas2DRenderingContext"_. Start by adding an invisible canvas to draw an image on. 1600 | ``` 1601 | 1602 | ``` 1603 | Now we get the canvas and context: 1604 | ``` 1605 | //fetches the texture canvas 1606 | let texCanvas=document.getElementById('textureCanvas') 1607 | 1608 | //the canvas2d api 1609 | let tex_ctx=texCanvas.getContext('2d') 1610 | ``` 1611 | Draw our texture! 1612 | ``` 1613 | tex_ctx.clearRect(0,0,texCanvas.width,texCanvas.height) 1614 | 1615 | tex_ctx.fillStyle='black' 1616 | tex_ctx.font='bold 30px arial' 1617 | 1618 | tex_ctx.fillText('this is a texture!',15,100) 1619 | tex_ctx.fillText('wowie :O',65,190) 1620 | ``` 1621 | We get the _"imageData"_ of the canvas using _"tex\_ctx.getImageData()"_. Updated _"gl.texImage2D()"_ function, with added texture width, height, and data: 1622 | ``` 1623 | gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,texCanvas.width,texCanvas.height,0,gl.RGBA,gl.UNSIGNED_BYTE,tex_ctx.getImageData(0,0,texCanvas.width,texCanvas.height)) 1624 | ``` 1625 | Our cube should now have a very cool texture! In the next section, we'll be learning about more features with textures. 1626 | 1627 | ##
More Texturing
1628 | 1629 |
In this section, we'll be learning about more ways to utilize textures to color meshes.

1630 | ####
Multiple Textures
1631 | You shouldn't use more than 1 texture in the fragment shader, but if you have to, here's how.

Start with the code from the **Textures** section. We need to create another texture: 1632 | ``` 1633 | let texture2=gl.createTexture() 1634 | 1635 | gl.bindTexture(gl.TEXTURE_2D,texture2) 1636 | 1637 | gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,texCanvas.width,texCanvas.height,0,gl.RGBA,gl.UNSIGNED_BYTE,tex_ctx.getImageData(0,0,texCanvas.width,texCanvas.height)) 1638 | 1639 | gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST) 1640 | gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST) 1641 | 1642 | gl.generateMipmap(gl.TEXTURE_2D) 1643 | ``` 1644 | To draw the second texture, we need to clear the texture canvas and draw before creating the second texture and after creating the first. 1645 | ``` 1646 | tex_ctx.clearRect(0,0,texCanvas.width,texCanvas.height) 1647 | tex_ctx.fillStyle='black' 1648 | tex_ctx.font='bold 30px arial' 1649 | tex_ctx.fillText('this is texture 1',15,100) 1650 | 1651 | //create first texture here... 1652 | 1653 | tex_ctx.clearRect(0,0,texCanvas.width,texCanvas.height) 1654 | tex_ctx.fillStyle='black' 1655 | tex_ctx.font='bold 30px arial' 1656 | tex_ctx.fillText('this is texture 2',15,190) 1657 | 1658 | //create second texture here... 1659 | ``` 1660 | Now that we have our textures created, add another uniform into the fragment shader. 1661 | ``` 1662 | let fragmentShaderCode=`#version 300 es 1663 | 1664 | precision mediump float; 1665 | 1666 | in vec3 pixColor; 1667 | in vec3 pixNormal; 1668 | in vec2 pixUV; 1669 | 1670 | uniform sampler2D tex; 1671 | 1672 | //the second texture 1673 | uniform sampler2D tex2; 1674 | 1675 | out vec4 fragColor; 1676 | 1677 | void main(){ 1678 | 1679 | vec3 lightDir=normalize(vec3(-0.7,-1.5,-1)); 1680 | float diffuse=dot(-lightDir,pixNormal)*0.5+0.5; 1681 | 1682 | vec3 surfaceColor=pixColor; 1683 | 1684 | vec4 textureColor=texture(tex,pixUV); 1685 | 1686 | //the 2nd texture's texel color 1687 | vec4 texture2Color=texture(tex2,pixUV); 1688 | 1689 | surfaceColor=mix(surfaceColor,textureColor.rgb,textureColor.a); 1690 | 1691 | //mix in the 2nd texture's color too 1692 | surfaceColor=mix(surfaceColor,textureColor.rgb,texture2Color.a); 1693 | 1694 | fragColor=vec4(surfaceColor*diffuse,1); 1695 | } 1696 | ` 1697 | ``` 1698 | To uniform the 2 textures, you need to use the _"gl.activeTextures()"_ function. 1699 | ``` 1700 | //gets locations 1701 | let textureLocation=gl.getUniformLocation(program,'tex') 1702 | let texture2Location=gl.getUniformLocation(program,'tex2') 1703 | 1704 | //"texture" is now known as active texture "0" 1705 | gl.activeTexture(gl.TEXTURE0) 1706 | gl.bindTexture(gl.TEXTURE_2D,texture) 1707 | 1708 | //"texture2" is now known as active texture "1" 1709 | gl.activeTexture(gl.TEXTURE1) 1710 | gl.bindTexture(gl.TEXTURE_2D,texture2) 1711 | 1712 | //uniform based on the texture's active number value 1713 | gl.uniform1i(textureLocation,0) 1714 | gl.uniform1i(texture2Location,1) 1715 | ``` 1716 | You should see that the faces of the cube contain 2 lines of text, each from their own textures.

1717 | ####
Texture Atlas
1718 | A texture atlas is a large texture with multiple other textures placed side by side. In programs that require many different textures(such as Minecraft), this is the only solution.

Start with the code from the **Textures** section. We are going to change the texture and draw 6 different textures next to each other. 1719 | ``` 1720 | tex_ctx.clearRect(0,0,texCanvas.width,texCanvas.height) 1721 | 1722 | tex_ctx.fillStyle='black' 1723 | tex_ctx.lineWidth=5 1724 | tex_ctx.strokeStyle='black' 1725 | tex_ctx.font='bold 40px arial' 1726 | 1727 | tex_ctx.strokeRect(0,0,256/3,256/3) 1728 | tex_ctx.strokeRect(256/3,0,256/3,256/3) 1729 | tex_ctx.strokeRect(256*2/3,0,256/3,256/3) 1730 | tex_ctx.strokeRect(2,256/3,256/3,256/3) 1731 | tex_ctx.strokeRect(256/3,256/3,256/3,256/3) 1732 | tex_ctx.strokeRect(256*2/3,256/3,256/3,256/3) 1733 | 1734 | tex_ctx.fillText('A',30,55) 1735 | tex_ctx.fillText('B',30+85,55) 1736 | tex_ctx.fillText('C',30+85*2,55) 1737 | tex_ctx.fillText('D',30,55+85) 1738 | tex_ctx.fillText('E',30+85,55+85) 1739 | tex_ctx.fillText('F',30+85*2,55+85) 1740 | ``` 1741 | Adjust the texture coordinates to cover a different portion of the texture for each face: 1742 | ``` 1743 | let s=0.333333 1744 | 1745 | let verts=[ 1746 | 1747 | //front side 1748 | -0.5,0.5,-0.5, 0,1,0, 0,0,-1, s,s, 1749 | -0.5,-0.5,-0.5, 0,1,0, 0,0,-1, s,s+s, 1750 | 0.5,-0.5,-0.5, 0,1,0, 0,0,-1, 0,s+s, 1751 | 0.5,0.5,-0.5, 0,1,0, 0,0,-1, 0,s, 1752 | 1753 | //back side 1754 | -0.5,0.5,0.5, 0,1,0, 0,0,1, s,0, 1755 | -0.5,-0.5,0.5, 0,1,0, 0,0,1, s,s, 1756 | 0.5,-0.5,0.5, 0,1,0, 0,0,1, s+s,s, 1757 | 0.5,0.5,0.5, 0,1,0, 0,0,1, s+s,0, 1758 | 1759 | //top side 1760 | -0.5,0.5,0.5, 0,1,0, 0,1,0, s,s, 1761 | -0.5,0.5,-0.5, 0,1,0, 0,1,0, s,s+s, 1762 | 0.5,0.5,-0.5, 0,1,0, 0,1,0, s+s,s+s, 1763 | 0.5,0.5,0.5, 0,1,0, 0,1,0, s+s,s, 1764 | 1765 | //bottom side 1766 | -0.5,-0.5,0.5, 0,1,0, 0,-1,0, s+s+s,s, 1767 | -0.5,-0.5,-0.5, 0,1,0, 0,-1,0, s+s+s,s+s, 1768 | 0.5,-0.5,-0.5, 0,1,0, 0,-1,0, s+s,s+s, 1769 | 0.5,-0.5,0.5, 0,1,0, 0,-1,0, s+s,s, 1770 | 1771 | //left side 1772 | -0.5,0.5,-0.5, 0,1,0, -1,0,0, s,0, 1773 | -0.5,-0.5,-0.5, 0,1,0, -1,0,0, s,s, 1774 | -0.5,-0.5,0.5, 0,1,0, -1,0,0, 0,s, 1775 | -0.5,0.5,0.5, 0,1,0, -1,0,0, 0,0, 1776 | 1777 | //right side 1778 | 0.5,0.5,-0.5, 0,1,0, 1,0,0, s+s+s,0, 1779 | 0.5,-0.5,-0.5, 0,1,0, 1,0,0, s+s+s,s, 1780 | 0.5,-0.5,0.5, 0,1,0, 1,0,0, s+s,s, 1781 | 0.5,0.5,0.5, 0,1,0, 1,0,0, s+s,0, 1782 | ] 1783 | ``` 1784 | You should now see a different texture for each face! This method is extremely efficient and is the only viable method for using multiple textures at a fast speed. 1785 | 1786 | ##
Framebuffers & Post Processing
1787 | 1788 |
In WebGL, framebuffers are like collections of attachments of different types of textures. The attachments can be color, depth, or stencil attachments. Framebuffers are especially useful as they allow you to render to a texture. You can attach a texture to a framebuffer, bind the framebuffer, draw geometry, and it'll end up in the framebuffer's attached texture instead of the canvas!

Rendering to a texture will allow you to perform post processing on the rendered image. You can apply effects like blurring, bloom, and color grading to the final image.

First, create a new _"WebGLProgramObject"_ that performs the post processing. In this example, the post processing shader will invert the colors of the rendered image. 1789 | ``` 1790 | let pp_program=createProgram(`#version 300 es 1791 | 1792 | precision mediump float; 1793 | 1794 | in vec2 vertPos; 1795 | 1796 | out vec2 pixUV; 1797 | 1798 | void main(){ 1799 | 1800 | pixUV=vertPos*0.5+0.5; 1801 | gl_Position=vec4(vertPos,0,1); 1802 | } 1803 | 1804 | `,`#version 300 es 1805 | 1806 | precision mediump float; 1807 | 1808 | in vec2 pixUV; 1809 | 1810 | uniform sampler2D tex; 1811 | 1812 | out vec4 fragColor; 1813 | 1814 | void main(){ 1815 | 1816 | fragColor=vec4(1.0-texture(tex,pixUV).rgb,1); 1817 | } 1818 | `) 1819 | ``` 1820 | After creating the original mesh, we create a 2D mesh that covers the screen. The screen mesh can be created using 2 triangles forming a rectangle that covers the screen, or from 1 large triangle. Here, we also get the attribute location while we're at it: 1821 | ``` 1822 | let pp_vertPosLocation=gl.getAttribLocation(program,'vertPos') 1823 | gl.enableVertexAttribArray(pp_vertPosLocation) 1824 | 1825 | let pp_buffer=gl.createBuffer() 1826 | 1827 | gl.bindBuffer(gl.ARRAY_BUFFER,pp_buffer) 1828 | 1829 | //vertices are a large triangle covering the screen 1830 | //the format of the buffer is x1,y1,x2,y2,x3,y3 1831 | gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW) 1832 | ``` 1833 | We now create the texture and framebuffer. We also have to attach a depth renderbuffer to the framebuffer in order for the program to perform depth testing when rendering to a texture. 1834 | ``` 1835 | //create the texture to be rendered to 1836 | let texture=gl.createTexture() 1837 | gl.bindTexture(gl.TEXTURE_2D,texture) 1838 | 1839 | //data is "null" as it's not needed to be set here 1840 | gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,canvas.width,canvas.height,0,gl.RGBA,gl.UNSIGNED_BYTE,null) 1841 | 1842 | gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR) 1843 | 1844 | 1845 | //create the framebuffer 1846 | let framebuffer=gl.createFramebuffer() 1847 | 1848 | //bind the framebuffer 1849 | gl.bindFramebuffer(gl.FRAMEBUFFER,framebuffer) 1850 | 1851 | //attach the texture to the framebuffer 1852 | gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,texture,0) 1853 | 1854 | //create a depth renderbuffer and attach it to the framebuffer 1855 | let depthBuffer=gl.createRenderbuffer() 1856 | gl.bindRenderbuffer(gl.RENDERBUFFER,depthBuffer) 1857 | gl.renderbufferStorage(gl.RENDERBUFFER,gl.DEPTH_COMPONENT16,canvas.width,canvas.height) 1858 | gl.framebufferRenderbuffer(gl.FRAMEBUFFER,gl.DEPTH_ATTACHMENT,gl.RENDERBUFFER,depthBuffer) 1859 | ``` 1860 | Then, we update the rendering process... 1861 | ``` 1862 | //stuff rendered after a bound framebuffer with render to the framebuffer's attached texture 1863 | gl.bindFramebuffer(gl.FRAMEBUFFER,framebuffer) 1864 | 1865 | //use the normal program 1866 | gl.useProgram(program) 1867 | 1868 | 1869 | //original cube mesh rendering goes here... 1870 | 1871 | 1872 | //unbinds the framebuffer, meaning stuff after draws to the real canvas 1873 | gl.bindFramebuffer(gl.FRAMEBUFFER,null) 1874 | 1875 | //"texture" now contains the rendered cube! 1876 | gl.bindTexture(gl.TEXTURE_2D,texture) 1877 | 1878 | //use the post processing program 1879 | gl.useProgram(pp_program) 1880 | 1881 | //draw the big triangle that covers the screen 1882 | gl.bindBuffer(gl.ARRAY_BUFFER,pp_buffer) 1883 | gl.vertexAttribPointer(pp_vertPosLocation,2,gl.FLOAT,gl.FALSE,8,0) 1884 | gl.drawArrays(gl.TRIANGLES,0,3) 1885 | ``` 1886 | You should see the same cube, but the final rendered image's color is inverted! 1887 | 1888 | 1889 | 1890 | 1891 | 1892 | 1893 | 1894 |


















1895 | 1896 | 1897 | 1898 | 1899 | 1900 | 1901 | 1902 | --------------------------------------------------------------------------------