Andrei Lesnitsky

Posted on

WebGL month. Day 21. Rendering a minecraft terrain

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

Source code available here

Built with

Hey ðŸ‘‹

Welcome to WebGL month.

Yesterday we rendered a single minecraft dirt cube, let's render a terrain today!

We'll need to store each block position in separate transform matrix

ðŸ“„ src/3d-textured.js

``````
gl.viewport(0, 0, canvas.width, canvas.height);

+ const matrices = [];
+
function frame() {
mat4.rotateY(cube.modelMatrix, cube.modelMatrix, Math.PI / 180);

``````

Now let's create 10k blocks iteration over x and z axis from -50 to 50

ðŸ“„ src/3d-textured.js

``````
const matrices = [];

+ for (let i = -50; i < 50; i++) {
+     for (let j = -50; j < 50; j++) {
+         const matrix = mat4.create();
+     }
+ }
+
function frame() {
mat4.rotateY(cube.modelMatrix, cube.modelMatrix, Math.PI / 180);

``````

Each block is a size of 2 (vertex coordinates are in [-1..1] range) so positions should be divisible by two

ðŸ“„ src/3d-textured.js

``````  for (let i = -50; i < 50; i++) {
for (let j = -50; j < 50; j++) {
const matrix = mat4.create();
+
+         const position = [i * 2, (Math.floor(Math.random() * 2) - 1) * 2, j * 2];
}
}

``````

Now we need to create a transform matrix. Let's use `ma4.fromTranslation`

ðŸ“„ src/3d-textured.js

``````          const matrix = mat4.create();

const position = [i * 2, (Math.floor(Math.random() * 2) - 1) * 2, j * 2];
+         mat4.fromTranslation(matrix, position);
}
}

``````

Let's also rotate each block around Y axis to make terrain look more random

ðŸ“„ src/3d-textured.js

``````  gl.viewport(0, 0, canvas.width, canvas.height);

const matrices = [];
+ const rotationMatrix = mat4.create();

for (let i = -50; i < 50; i++) {
for (let j = -50; j < 50; j++) {

const position = [i * 2, (Math.floor(Math.random() * 2) - 1) * 2, j * 2];
mat4.fromTranslation(matrix, position);
+
+         mat4.fromRotation(rotationMatrix, Math.PI * Math.round(Math.random() * 4), [0, 1, 0]);
+         mat4.multiply(matrix, matrix, rotationMatrix);
}
}

``````

and finally push matrix of each block to matrices collection

ðŸ“„ src/3d-textured.js

``````
mat4.fromRotation(rotationMatrix, Math.PI * Math.round(Math.random() * 4), [0, 1, 0]);
mat4.multiply(matrix, matrix, rotationMatrix);
+
+         matrices.push(matrix);
}
}

``````

Since our blocks are static, we don't need a rotation transform in each frame

ðŸ“„ src/3d-textured.js

``````  }

function frame() {
-     mat4.rotateY(cube.modelMatrix, cube.modelMatrix, Math.PI / 180);
-
gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, cube.modelMatrix);
gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, cube.normalMatrix);

``````

Now we'll need to iterate over matrices collection and issue a draw call for each cube with its transform matrix passed to uniform

ðŸ“„ src/3d-textured.js

``````  }

function frame() {
-     gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, cube.modelMatrix);
-     gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, cube.normalMatrix);
+     matrices.forEach((matrix) => {
+         gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, matrix);
+         gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, cube.normalMatrix);

-     gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);
+         gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);
+     });

requestAnimationFrame(frame);
}

``````

Now let's create an animation of rotating camera. Camera has a position and a point where it is pointed. So to implement this, we need to rotate focus point around camera position. Let's first get rid of static view matrix

ðŸ“„ src/3d-textured.js

``````  const viewMatrix = mat4.create();
const projectionMatrix = mat4.create();

- mat4.lookAt(viewMatrix, [0, 4, -7], [0, 0, 0], [0, 1, 0]);
-
mat4.perspective(projectionMatrix, (Math.PI / 360) * 90, canvas.width / canvas.height, 0.01, 100);

gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix);

``````

Define camera position, camera focus point vector and focus point transform matrix

ðŸ“„ src/3d-textured.js

``````- import { mat4 } from 'gl-matrix';
+ import { mat4, vec3 } from 'gl-matrix';

}
}

+ const cameraPosition = [0, 10, 0];
+ const cameraFocusPoint = vec3.fromValues(30, 0, 0);
+ const cameraFocusPointMatrix = mat4.create();
+
+ mat4.fromTranslation(cameraFocusPointMatrix, cameraFocusPoint);
+
function frame() {
matrices.forEach((matrix) => {
gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, matrix);

``````

Our camera is located in 0.0.0, so we need to translate camera focus point to 0.0.0, rotate it, and translate back to original position

ðŸ“„ src/3d-textured.js

``````  mat4.fromTranslation(cameraFocusPointMatrix, cameraFocusPoint);

function frame() {
+     mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [-30, 0, 0]);
+     mat4.rotateY(cameraFocusPointMatrix, cameraFocusPointMatrix, Math.PI / 360);
+     mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [30, 0, 0]);
+
matrices.forEach((matrix) => {
gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, matrix);
gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, cube.normalMatrix);

``````

Final step â€“ update view matrix uniform

ðŸ“„ src/3d-textured.js

``````      mat4.rotateY(cameraFocusPointMatrix, cameraFocusPointMatrix, Math.PI / 360);
mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [30, 0, 0]);

+     mat4.getTranslation(cameraFocusPoint, cameraFocusPointMatrix);
+
+     mat4.lookAt(viewMatrix, cameraPosition, cameraFocusPoint, [0, 1, 0]);
+     gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix);
+
matrices.forEach((matrix) => {
gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, matrix);
-         gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, cube.normalMatrix);

gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);
});

``````

That's it!

This approach is not very performant though, as we're issuing 2 gl calls for each object, so it is a 20k of gl calls each frame. GL calls are expensive, so we'll need to reduce this number. We'll learn a great technique tomorrow!

Join mailing list to get new posts right to your inbox

Source code available here

Built with