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'; import vShaderSource from './shaders/3d-textured.v.glsl'; import fShaderSource from './shaders/3d-textured.f.glsl'; } } + 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