└── 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 |
--------------------------------------------------------------------------------