DEV Community

Andrei Lesnitsky
Andrei Lesnitsky

Posted on

WebGL month. Day 20. Rendering a minecraft dirt 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

Today we're going to explore how to add textures to 3d objects.

First we'll need a new entry point

📄 index.html



      </head>
      <body>
          <canvas></canvas>
-         <script src="./dist/3d.js"></script>
+         <script src="./dist/3d-textured.js"></script>
      </body>
  </html>



Enter fullscreen mode Exit fullscreen mode

📄 src/3d-textured.js



console.log('Hello textures');



Enter fullscreen mode Exit fullscreen mode

📄 webpack.config.js



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

      output: {



Enter fullscreen mode Exit fullscreen mode

Now let's create simple shaders to render a 3d object with solid color. Learn more in this tutorial

📄 src/shaders/3d-textured.f.glsl



precision mediump float;

void main() {
    gl_FragColor = vec4(1, 0, 0, 1);
}



Enter fullscreen mode Exit fullscreen mode

📄 src/shaders/3d-textured.v.glsl



attribute vec3 position;

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;

void main() {
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}



Enter fullscreen mode Exit fullscreen mode

We'll need a canvas, webgl context and make canvas fullscreen

📄 src/3d-textured.js



- console.log('Hello textures');
+ 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`;



Enter fullscreen mode Exit fullscreen mode

Create and compile shaders. Learn more here

📄 src/3d-textured.js



+ import vShaderSource from './shaders/3d-textured.v.glsl';
+ import fShaderSource from './shaders/3d-textured.f.glsl';
+ import { compileShader } from './gl-helpers';
+ 
  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');


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



Enter fullscreen mode Exit fullscreen mode

Create, link and use webgl program

📄 src/3d-textured.js




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



Enter fullscreen mode Exit fullscreen mode

Enable depth test since we're rendering 3d. Learn more here

📄 src/3d-textured.js




  gl.linkProgram(program);
  gl.useProgram(program);
+ 
+ gl.enable(gl.DEPTH_TEST);



Enter fullscreen mode Exit fullscreen mode

Setup shader input. Learn more here

📄 src/3d-textured.js



  import vShaderSource from './shaders/3d-textured.v.glsl';
  import fShaderSource from './shaders/3d-textured.f.glsl';
- import { compileShader } from './gl-helpers';
+ import { compileShader, setupShaderInput } from './gl-helpers';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
  gl.useProgram(program);

  gl.enable(gl.DEPTH_TEST);
+ 
+ const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);



Enter fullscreen mode Exit fullscreen mode

Now let's go to Blender and create a cube, but make sure to check "Generate UVs" so that blender can map cube vertices to a plain image.

Blender 1

Next open "UV Editing" view

Blender 2

Enter edit mode

Blender 3

Unwrapped cube looks good already, so we can export UV layout

Blender 4

Now if we open exported image in some editor we'll see something like this

Photoshop 1

Cool, now we can actually fill our texture with some content

Let's render a minecraft dirt block

Photoshop 2

Next we need to export our object from blender, but don't forget to triangulate it first

Blender 5

And finally export our object

Blender 6

Now let's import our cube and create an object. Learn here about this helper class

📄 src/3d-textured.js



  import vShaderSource from './shaders/3d-textured.v.glsl';
  import fShaderSource from './shaders/3d-textured.f.glsl';
  import { compileShader, setupShaderInput } from './gl-helpers';
+ import cubeObj from '../assets/objects/textured-cube.obj';
+ import { Object3D } from './Object3D';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
  gl.enable(gl.DEPTH_TEST);

  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);
+ 
+ const cube = new Object3D(cubeObj, [0, 0, 0], [1, 0, 0]);



Enter fullscreen mode Exit fullscreen mode

If we'll look into object source, we'll see lines like below



vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.375000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.500000


Enter fullscreen mode Exit fullscreen mode

These are texture coordinates which are referenced by faces in the 2nd "property"



f 2/1/1 3/2/1 1/3/1

# vertexIndex / textureCoordinateIndex / normalIndex


Enter fullscreen mode Exit fullscreen mode

so we need to update our parser to support texture coordinates

📄 src/gl-helpers.js



  export function parseObj(objSource) {
      const _vertices = [];
      const _normals = [];
+     const _texCoords = [];
+ 
      const vertexIndices = [];
      const normalIndices = [];
+     const texCoordIndices = [];

      objSource.split('\n').forEach(line => {
          if (line.startsWith('v ')) {
              _normals.push(parseVec(line, 'vn '));
          }

+         if (line.startsWith('vt ')) {
+             _texCoords.push(parseVec(line, 'vt '));
+         }
+ 
          if (line.startsWith('f ')) {
              const parsedFace = parseFace(line);

              vertexIndices.push(...parsedFace.map(face => face[0] - 1));
+             texCoordIndices.push(...parsedFace.map(face => face[1] - 1));
              normalIndices.push(...parsedFace.map(face => face[2] - 1));
          }
      });

      const vertices = [];
      const normals = [];
+     const texCoords = [];

      for (let i = 0; i < vertexIndices.length; i++) {
          const vertexIndex = vertexIndices[i];
          const normalIndex = normalIndices[i];
+         const texCoordIndex = texCoordIndices[i];

          const vertex = _vertices[vertexIndex];
          const normal = _normals[normalIndex];
+         const texCoord = _texCoords[texCoordIndex];

          vertices.push(...vertex);
          normals.push(...normal);
+ 
+         if (texCoord) {
+             texCoords.push(...texCoord);
+         }
      }

      return { 
          vertices: new Float32Array(vertices), 
-         normals: new Float32Array(normals), 
+         normals: new Float32Array(normals),
+         texCoords: new Float32Array(texCoords), 
      };
  }



Enter fullscreen mode Exit fullscreen mode

and add this property to Object3D

📄 src/Object3D.js




  export class Object3D {
      constructor(source, position, color) {
-         const { vertices, normals } = parseObj(source);
+         const { vertices, normals, texCoords } = parseObj(source);

          this.vertices = vertices;
          this.normals = normals;
          this.position = position;
+         this.texCoords = texCoords;

          this.modelMatrix = mat4.create();
          mat4.fromTranslation(this.modelMatrix, position);



Enter fullscreen mode Exit fullscreen mode

Now we need to define gl buffers. Learn more about this helper class here

📄 src/3d-textured.js



  import { compileShader, setupShaderInput } from './gl-helpers';
  import cubeObj from '../assets/objects/textured-cube.obj';
  import { Object3D } from './Object3D';
+ import { GLBuffer } from './GLBuffer';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

  const cube = new Object3D(cubeObj, [0, 0, 0], [1, 0, 0]);
+ 
+ const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cube.vertices, gl.STATIC_DRAW);
+ const texCoordsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cube.texCoords, gl.STATIC_DRAW);



Enter fullscreen mode Exit fullscreen mode

We also need to define an attribute to pass tex coords to the vertex shader

📄 src/shaders/3d-textured.v.glsl



  attribute vec3 position;
+ attribute vec2 texCoord;

  uniform mat4 modelMatrix;
  uniform mat4 viewMatrix;



Enter fullscreen mode Exit fullscreen mode

and varying to pass texture coordinate to the fragment shader. Learn more here

📄 src/shaders/3d-textured.f.glsl



  precision mediump float;

+ varying vec2 vTexCoord;
+ 
  void main() {
      gl_FragColor = vec4(1, 0, 0, 1);
  }



Enter fullscreen mode Exit fullscreen mode

📄 src/shaders/3d-textured.v.glsl



  uniform mat4 viewMatrix;
  uniform mat4 projectionMatrix;

+ varying vec2 vTexCoord;
+ 
  void main() {
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
+ 
+     vTexCoord = texCoord;
  }



Enter fullscreen mode Exit fullscreen mode

Let's setup attributes

📄 src/3d-textured.js




  const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cube.vertices, gl.STATIC_DRAW);
  const texCoordsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cube.texCoords, gl.STATIC_DRAW);
+ 
+ vertexBuffer.bind(gl);
+ gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0);
+ 
+ texCoordsBuffer.bind(gl);
+ gl.vertexAttribPointer(programInfo.attributeLocations.texCoord, 2, gl.FLOAT, false, 0, 0);



Enter fullscreen mode Exit fullscreen mode

Create and setup view and projection matrix. Learn more here

📄 src/3d-textured.js



+ import { mat4 } from 'gl-matrix';
+ 
  import vShaderSource from './shaders/3d-textured.v.glsl';
  import fShaderSource from './shaders/3d-textured.f.glsl';
  import { compileShader, setupShaderInput } from './gl-helpers';

  texCoordsBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
+ 
+ const viewMatrix = mat4.create();
+ const projectionMatrix = mat4.create();
+ 
+ mat4.lookAt(
+     viewMatrix,
+     [0, 0, -7],
+     [0, 0, 0],
+     [0, 1, 0],
+ );
+ 
+ mat4.perspective(
+     projectionMatrix,
+     Math.PI / 360 * 90,
+     canvas.width / canvas.height,
+     0.01,
+     100,
+ );



Enter fullscreen mode Exit fullscreen mode

Pass view and projection matrices to shader via uniforms

📄 src/3d-textured.js



      0.01,
      100,
  );
+ 
+ gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix);
+ gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);



Enter fullscreen mode Exit fullscreen mode

Setup viewport

📄 src/3d-textured.js




  gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix);
  gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);
+ 
+ gl.viewport(0, 0, canvas.width, canvas.height);



Enter fullscreen mode Exit fullscreen mode

and finally render our cube

📄 src/3d-textured.js



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

  gl.viewport(0, 0, canvas.width, canvas.height);
+ 
+ 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);
+ 
+     gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);
+ 
+     requestAnimationFrame(frame);
+ }
+ 
+ frame();



Enter fullscreen mode Exit fullscreen mode

but before rendering the cube we need to load our texture image. Learn more about loadImage helper here

📄 src/3d-textured.js




  import vShaderSource from './shaders/3d-textured.v.glsl';
  import fShaderSource from './shaders/3d-textured.f.glsl';
- import { compileShader, setupShaderInput } from './gl-helpers';
+ import { compileShader, setupShaderInput, loadImage } from './gl-helpers';
  import cubeObj from '../assets/objects/textured-cube.obj';
  import { Object3D } from './Object3D';
  import { GLBuffer } from './GLBuffer';
+ import textureSource from '../assets/images/cube-texture.png';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
      requestAnimationFrame(frame);
  }

- frame();
+ loadImage(textureSource).then((image) => {
+     frame();
+ });



Enter fullscreen mode Exit fullscreen mode

📄 webpack.config.js



              },

              {
-                 test: /\.jpg$/,
+                 test: /\.(jpg|png)$/,
                  use: 'url-loader',
              },
          ],



Enter fullscreen mode Exit fullscreen mode

and create webgl texture. Learn more here

📄 src/3d-textured.js




  import vShaderSource from './shaders/3d-textured.v.glsl';
  import fShaderSource from './shaders/3d-textured.f.glsl';
- import { compileShader, setupShaderInput, loadImage } from './gl-helpers';
+ import { compileShader, setupShaderInput, loadImage, createTexture, setImage } from './gl-helpers';
  import cubeObj from '../assets/objects/textured-cube.obj';
  import { Object3D } from './Object3D';
  import { GLBuffer } from './GLBuffer';
  }

  loadImage(textureSource).then((image) => {
+     const texture = createTexture(gl);
+     setImage(gl, texture, image);
+ 
      frame();
  });



Enter fullscreen mode Exit fullscreen mode

and read fragment colors from texture

📄 src/shaders/3d-textured.f.glsl



  precision mediump float;
+ uniform sampler2D texture;

  varying vec2 vTexCoord;

  void main() {
-     gl_FragColor = vec4(1, 0, 0, 1);
+     gl_FragColor = texture2D(texture, vTexCoord);
  }



Enter fullscreen mode Exit fullscreen mode

Let's move camera a bit to top to see the "grass" side

📄 src/3d-textured.js




  mat4.lookAt(
      viewMatrix,
-     [0, 0, -7],
+     [0, 4, -7],
      [0, 0, 0],
      [0, 1, 0],
  );



Enter fullscreen mode Exit fullscreen mode

Something is wrong, top part is partially white, but why?

Turns out that image is flipped when read by GPU, so we need to flip it back

Turns out that image is flipped when read by GPU, so we need to flip it back

📄 src/shaders/3d-textured.f.glsl



  varying vec2 vTexCoord;

  void main() {
-     gl_FragColor = texture2D(texture, vTexCoord);
+     gl_FragColor = texture2D(texture, vTexCoord * vec2(1, -1) + vec2(0, 1));
  }



Enter fullscreen mode Exit fullscreen mode

Minecraft cube

Cool, we rendered a minecraft cube with WebGL 🎉

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)