Forem

Andrei Lesnitsky
Andrei Lesnitsky

Posted on

WebGL month. Day 15. Rendering a 3d cube

Rendring a 3d cube

This is a series of blog posts related to WebGL. New post will be available every day

GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo


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>

Enter fullscreen mode Exit fullscreen mode

๐Ÿ“„ src/3d.js

console.log('Hello 3d!');

Enter fullscreen mode Exit fullscreen mode

๐Ÿ“„ webpack.config.js

          'week-1': './src/week-1.js',
          texture: './src/texture.js',
          'rotating-square': './src/rotating-square.js',
+         '3d': './src/3d.js',
      },

      output: {

Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

๐Ÿ“„ src/shaders/3d.v.glsl

attribute vec3 position;

void main() {
    gl_Position = vec4(position, 1.0);
}

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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,
+ ]);

Enter fullscreen mode Exit fullscreen mode

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,
  ]);

Enter fullscreen mode Exit fullscreen mode

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,
  ]);

Enter fullscreen mode Exit fullscreen mode

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,
  ]);

Enter fullscreen mode Exit fullscreen mode

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,
  ]);

Enter fullscreen mode Exit fullscreen mode

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,
  ]);

Enter fullscreen mode Exit fullscreen mode

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
+ ]);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);
  }

Enter fullscreen mode Exit fullscreen mode

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);
  }

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Looks quite like a cube ๐ŸŽ‰

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();

Enter fullscreen mode Exit fullscreen mode

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);
  }


Enter fullscreen mode Exit fullscreen mode

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);
  }

Enter fullscreen mode Exit fullscreen mode

Cool! We have a rotation ๐ŸŽ‰

Rotating cube

That's it for today, see you tomorrow ๐Ÿ‘‹


GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo

Top comments (0)