Rendring a 3d cube
This is a series of blog posts related to WebGL. New post will be available every day
Join mailing list to get new posts right to your inbox
Built with
Hey π Welcome to WebGL month.
[Yesterday] we've explored some concepts required for 3d rendering, so let's finally render something πͺ
We'll need a new entry point
π index.html
</head> <body> <canvas></canvas> - <script src="./dist/rotating-square.js"></script> + <script src="./dist/3d.js"></script> </body> </html>
π src/3d.js
console.log('Hello 3d!');
π webpack.config.js
'week-1': './src/week-1.js', texture: './src/texture.js', 'rotating-square': './src/rotating-square.js', + '3d': './src/3d.js', }, output: {
Simple vertex and fragment shaders. Notice that we use vec3
for position now as we'll work in 3-dimensional clipsace.
π src/shaders/3d.f.glsl
precision mediump float; void main() { gl_FragColor = vec4(1, 0, 0, 1); }
π src/shaders/3d.v.glsl
attribute vec3 position; void main() { gl_Position = vec4(position, 1.0); }
We'll also need a familiar from previous tutorials boilerplate for our WebGL program
π src/3d.js
- console.log('Hello 3d!'); + import vShaderSource from './shaders/3d.v.glsl'; + import fShaderSource from './shaders/3d.f.glsl'; + import { compileShader, setupShaderInput } from './gl-helpers'; + + const canvas = document.querySelector('canvas'); + const gl = canvas.getContext('webgl'); + + const width = document.body.offsetWidth; + const height = document.body.offsetHeight; + + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + + const vShader = gl.createShader(gl.VERTEX_SHADER); + const fShader = gl.createShader(gl.FRAGMENT_SHADER); + + compileShader(gl, vShader, vShaderSource); + compileShader(gl, fShader, fShaderSource); + + const program = gl.createProgram(); + + gl.attachShader(program, vShader); + gl.attachShader(program, fShader); + + gl.linkProgram(program); + gl.useProgram(program); + + const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);
Now let's define cube vertices for each face. We'll start with front face
π src/3d.js
gl.useProgram(program); const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource); + + const cubeVertices = new Float32Array([ + // Front face + -1.0, -1.0, 1.0, + 1.0, -1.0, 1.0, + 1.0, 1.0, 1.0, + -1.0, 1.0, 1.0, + ]);
back face
π src/3d.js
1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, + + // Back face + -1.0, -1.0, -1.0, + -1.0, 1.0, -1.0, + 1.0, 1.0, -1.0, + 1.0, -1.0, -1.0, ]);
top face
π src/3d.js
-1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, + + // Top face + -1.0, 1.0, -1.0, + -1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, + 1.0, 1.0, -1.0, ]);
bottom face
π src/3d.js
-1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, + + // Bottom face + -1.0, -1.0, -1.0, + 1.0, -1.0, -1.0, + 1.0, -1.0, 1.0, + -1.0, -1.0, 1.0, ]);
right face
π src/3d.js
1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, + + // Right face + 1.0, -1.0, -1.0, + 1.0, 1.0, -1.0, + 1.0, 1.0, 1.0, + 1.0, -1.0, 1.0, ]);
left face
π src/3d.js
1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, + + // Left face + -1.0, -1.0, -1.0, + -1.0, -1.0, 1.0, + -1.0, 1.0, 1.0, + -1.0, 1.0, -1.0, ]);
Now we need to define vertex indices
π src/3d.js
-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, ]); + + const indices = new Uint8Array([ + 0, 1, 2, 0, 2, 3, // front + 4, 5, 6, 4, 6, 7, // back + 8, 9, 10, 8, 10, 11, // top + 12, 13, 14, 12, 14, 15, // bottom + 16, 17, 18, 16, 18, 19, // right + 20, 21, 22, 20, 22, 23, // left + ]);
and create gl buffers
π src/3d.js
import vShaderSource from './shaders/3d.v.glsl'; import fShaderSource from './shaders/3d.f.glsl'; import { compileShader, setupShaderInput } from './gl-helpers'; + import { GLBuffer } from './GLBuffer'; const canvas = document.querySelector('canvas'); const gl = canvas.getContext('webgl'); 16, 17, 18, 16, 18, 19, // right 20, 21, 22, 20, 22, 23, // left ]); + + const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cubeVertices, gl.STATIC_DRAW); + const indexBuffer = new GLBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
Setup vertex attribute pointer
π src/3d.js
const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cubeVertices, gl.STATIC_DRAW); const indexBuffer = new GLBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); + + vertexBuffer.bind(gl); + gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0);
setup viewport
π src/3d.js
vertexBuffer.bind(gl); gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0); + + gl.viewport(0, 0, canvas.width, canvas.height);
and issue a draw call
π src/3d.js
gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0); gl.viewport(0, 0, canvas.width, canvas.height); + + gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
Ok, we did everything right, but we just see a red canvas? That's expected result, because every face of cube has a length of 2
with left-most vertices at -1
and right-most at 1
, so we need to add some matrix magic from yesterday.
Let's define uniforms for each matrix
π src/shaders/3d.v.glsl
attribute vec3 position; + uniform mat4 modelMatrix; + uniform mat4 viewMatrix; + uniform mat4 projectionMatrix; + void main() { gl_Position = vec4(position, 1.0); }
and multiply every matrix.
π src/shaders/3d.v.glsl
uniform mat4 projectionMatrix; void main() { - gl_Position = vec4(position, 1.0); + gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0); }
Now we need to define JS representations of the same matrices
π src/3d.js
+ import { mat4 } from 'gl-matrix'; + import vShaderSource from './shaders/3d.v.glsl'; import fShaderSource from './shaders/3d.f.glsl'; import { compileShader, setupShaderInput } from './gl-helpers'; vertexBuffer.bind(gl); gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0); + const modelMatrix = mat4.create(); + const viewMatrix = mat4.create(); + const projectionMatrix = mat4.create(); + gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
We'll leave model matrix as-is (mat4.create returns an identity matrix), meaning cube won't have any transforms (no translation, no rotation, no scale).
We'll use lookAt
method to setup viewMatrix
π src/3d.js
const viewMatrix = mat4.create(); const projectionMatrix = mat4.create(); + mat4.lookAt( + viewMatrix, + ); + gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
The 2nd argument is a position of a viewer. Let's place this point on top and in front of the cube
π src/3d.js
mat4.lookAt( viewMatrix, + [0, 7, -7], ); gl.viewport(0, 0, canvas.width, canvas.height);
The 3rd argument is a point where we want to look at. Coordinate of our cube is (0, 0, 0), that's exactly what we want to look at
π src/3d.js
mat4.lookAt( viewMatrix, [0, 7, -7], + [0, 0, 0], ); gl.viewport(0, 0, canvas.width, canvas.height);
The last argument is up vector. We can setup a view matrix in a way that any vector will be treated as pointing to the top of our world, so let's make y axis pointing to the top
π src/3d.js
viewMatrix, [0, 7, -7], [0, 0, 0], + [0, 1, 0], ); gl.viewport(0, 0, canvas.width, canvas.height);
To setup projection matrix we'll use perspective method
π src/3d.js
[0, 1, 0], ); + mat4.perspective( + projectionMatrix, + ); + gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
View and perspective matrices together are kind of a "camera" parameters.
We already have a position and direction of a camera, let's setup other parameters.
The 2nd argument of perspective
method is a field of view
(how wide is camera lens). Wider the angle β more objecs will fit the screen (you surely heard of a "wide angle" camera in recent years phones, that's about the same).
π src/3d.js
mat4.perspective( projectionMatrix, + Math.PI / 360 * 90, ); gl.viewport(0, 0, canvas.width, canvas.height);
Next argument is aspect ration of a canvas. It could be calculated by a simple division
π src/3d.js
mat4.perspective( projectionMatrix, Math.PI / 360 * 90, + canvas.width / canvas.height, ); gl.viewport(0, 0, canvas.width, canvas.height);
The 4th and 5th argumnts setup a distance to objects which are visible by camera. Some objects might be too close, others too far, so they shouldn't be rendered. The 4th argument β distance to the closest object to render, the 5th β to the farthest
π src/3d.js
projectionMatrix, Math.PI / 360 * 90, canvas.width / canvas.height, + 0.01, + 100, ); gl.viewport(0, 0, canvas.width, canvas.height);
and finally we need to pass matrices to shader
π src/3d.js
100, ); + gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, modelMatrix); + gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix); + gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix); + gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
Looks quite like a cube π
Now let's implement a rotation animation with help of model matrix and rotate method from gl-matrix
π src/3d.js
gl.viewport(0, 0, canvas.width, canvas.height); gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0); + + function frame() { + mat4.rotateY(modelMatrix, modelMatrix, Math.PI / 180); + + requestAnimationFrame(frame); + } + + frame();
We also need to update a uniform
π src/3d.js
function frame() { mat4.rotateY(modelMatrix, modelMatrix, Math.PI / 180); + gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, modelMatrix); + requestAnimationFrame(frame); }
and issue a draw call
π src/3d.js
mat4.rotateY(modelMatrix, modelMatrix, Math.PI / 180); gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, modelMatrix); + gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0); requestAnimationFrame(frame); }
Cool! We have a rotation π
That's it for today, see you tomorrow π
Join mailing list to get new posts right to your inbox
Built with
Top comments (0)