WebGL Month. Day 19. Rendering multiple objects

lesnitsky

Andrei Lesnitsky

Posted on July 19, 2019

WebGL Month. Day 19. Rendering multiple objects

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.

In previous tutorials we've been rendering only a signle object, but typical 3D scene consists of a multiple objects.
Today we're going to learn how to render many objects on scene.

Since we're rendering objects with a solid color, let's get rid of colorIndex attribute and pass a signle color via uniform

šŸ“„ src/3d.js


  const { vertices, normals } = parseObj(monkeyObj);

- const faceColors = [
-     [0.5, 0.5, 0.5, 1.0]
- ];
- 
- const colors = [];
- 
- for (var j = 0; j < vertices.length / 3; ++j) {
-     colors.push(0, 0, 0, 0);
- }
- 
- faceColors.forEach((color, index) => {
-     gl.uniform4fv(programInfo.uniformLocations[`colors[${index}]`], color);
- });
+ gl.uniform3fv(programInfo.uniformLocations.color, [0.5, 0.5, 0.5]);

  const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
- const colorsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
  const normalsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);

  vertexBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0);

- colorsBuffer.bind(gl);
- gl.vertexAttribPointer(programInfo.attributeLocations.colorIndex, 1, gl.FLOAT, false, 0, 0);
- 
  normalsBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.normal, 3, gl.FLOAT, false, 0, 0);


Enter fullscreen mode Exit fullscreen mode

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

  attribute vec3 position;
  attribute vec3 normal;
- attribute float colorIndex;

  uniform mat4 modelMatrix;
  uniform mat4 viewMatrix;
  uniform mat4 projectionMatrix;
  uniform mat4 normalMatrix;
- uniform vec4 colors[6];
+ uniform vec3 color;
  uniform vec3 directionalLightVector;

  varying vec4 vColor;
      vec3 transformedNormal = (normalMatrix * vec4(normal, 1.0)).xyz;
      float intensity = dot(transformedNormal, normalize(directionalLightVector));

-     vColor.rgb = vec3(0.3, 0.3, 0.3) + colors[int(colorIndex)].rgb * intensity;
+     vColor.rgb = vec3(0.3, 0.3, 0.3) + color * intensity;
      vColor.a = 1.0;
  }

Enter fullscreen mode Exit fullscreen mode

We'll need a helper class to store object related info

šŸ“„ src/Object3D.js

export class Object3D {
    constructor() {

    } 
}

Enter fullscreen mode Exit fullscreen mode

Each object should contain it's own vertices and normals

šŸ“„ src/Object3D.js

+ import { parseObj } from "./gl-helpers";
+ 
  export class Object3D {
-     constructor() {
-         
-     } 
+     constructor(source) {
+         const { vertices, normals } = parseObj(source);
+ 
+         this.vertices = vertices;
+         this.normals = normals;
+     }
  }

Enter fullscreen mode Exit fullscreen mode

As well as a model matrix to store object transform

šŸ“„ src/Object3D.js

  import { parseObj } from "./gl-helpers";
+ import { mat4 } from "gl-matrix";

  export class Object3D {
      constructor(source) {

          this.vertices = vertices;
          this.normals = normals;
+ 
+         this.modelMatrix = mat4.create();
      }
  }

Enter fullscreen mode Exit fullscreen mode

Since normal matrix is computable from model matrix it makes sense to define a getter

šŸ“„ src/Object3D.js

          this.normals = normals;

          this.modelMatrix = mat4.create();
+         this._normalMatrix = mat4.create();
+     }
+ 
+     get normalMatrix () {
+         mat4.invert(this._normalMatrix, this.modelMatrix);
+         mat4.transpose(this._normalMatrix, this._normalMatrix);
+ 
+         return this._normalMatrix;
      }
  }

Enter fullscreen mode Exit fullscreen mode

Now we can refactor our code and use new helper class

šŸ“„ src/3d.js

  import { compileShader, setupShaderInput, parseObj } from './gl-helpers';
  import { GLBuffer } from './GLBuffer';
  import monkeyObj from '../assets/objects/monkey.obj';
+ import { Object3D } from './Object3D';

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

  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

- const { vertices, normals } = parseObj(monkeyObj);
+ const monkey = new Object3D(monkeyObj);

  gl.uniform3fv(programInfo.uniformLocations.color, [0.5, 0.5, 0.5]);

- const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
- const normalsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
+ const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, monkey.vertices, gl.STATIC_DRAW);
+ const normalsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, monkey.normals, gl.STATIC_DRAW);

  vertexBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.position, 3, gl.FLOAT, false, 0, 0);
  normalsBuffer.bind(gl);
  gl.vertexAttribPointer(programInfo.attributeLocations.normal, 3, gl.FLOAT, false, 0, 0);

- const modelMatrix = mat4.create();
  const viewMatrix = mat4.create();
  const projectionMatrix = mat4.create();
- const normalMatrix = mat4.create();

  mat4.lookAt(
      viewMatrix,
      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.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);
- 
  function frame() {
-     mat4.rotateY(modelMatrix, modelMatrix, Math.PI / 180);
- 
-     mat4.invert(normalMatrix, modelMatrix);
-     mat4.transpose(normalMatrix, normalMatrix);
+     mat4.rotateY(monkey.modelMatrix, monkey.modelMatrix, Math.PI / 180);

-     gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, modelMatrix);
-     gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, normalMatrix);
+     gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, monkey.modelMatrix);
+     gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, monkey.normalMatrix);

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


Enter fullscreen mode Exit fullscreen mode

Now let's import more objects

šŸ“„ src/3d.js

  import { compileShader, setupShaderInput, parseObj } from './gl-helpers';
  import { GLBuffer } from './GLBuffer';
  import monkeyObj from '../assets/objects/monkey.obj';
+ import torusObj from '../assets/objects/torus.obj';
+ import coneObj from '../assets/objects/cone.obj';
+ 
  import { Object3D } from './Object3D';

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

  const monkey = new Object3D(monkeyObj);
+ const torus = new Object3D(torusObj);
+ const cone = new Object3D(coneObj);

  gl.uniform3fv(programInfo.uniformLocations.color, [0.5, 0.5, 0.5]);


Enter fullscreen mode Exit fullscreen mode

and store them in a collection

šŸ“„ src/3d.js

  const torus = new Object3D(torusObj);
  const cone = new Object3D(coneObj);

+ const objects = [
+     monkey,
+     torus,
+     cone,
+ ];
+ 
  gl.uniform3fv(programInfo.uniformLocations.color, [0.5, 0.5, 0.5]);

  const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, monkey.vertices, gl.STATIC_DRAW);

Enter fullscreen mode Exit fullscreen mode

and instead of issuing a draw call for just a monkey, we'll iterate over collection

šŸ“„ src/3d.js

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

  function frame() {
-     mat4.rotateY(monkey.modelMatrix, monkey.modelMatrix, Math.PI / 180);
+     objects.forEach((object) => {
+         mat4.rotateY(object.modelMatrix, object.modelMatrix, Math.PI / 180);

-     gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, monkey.modelMatrix);
-     gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, monkey.normalMatrix);
+         gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, object.modelMatrix);
+         gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, object.normalMatrix);

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

      requestAnimationFrame(frame);
  }

Enter fullscreen mode Exit fullscreen mode

Ok, but why do we still have only monkey rendered?

No wonder, vertex and normals buffer stays the same, so we just render the same object N times. Let's update vertex and normals buffer each time we want to render an object

šŸ“„ src/3d.js

          gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, object.modelMatrix);
          gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, object.normalMatrix);

+         vertexBuffer.setData(gl, object.vertices, gl.STATIC_DRAW);
+         normalsBuffer.setData(gl, object.normals, gl.STATIC_DRAW);
+ 
          gl.drawArrays(gl.TRIANGLES, 0, vertexBuffer.data.length / 3);
      });


Enter fullscreen mode Exit fullscreen mode

Multiple objects 1

Cool, we've rendered multiple objects, but they are all in the same spot. Let's fix that

Each object will have a property storing a position in space

šŸ“„ src/3d.js


  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

- const monkey = new Object3D(monkeyObj);
- const torus = new Object3D(torusObj);
- const cone = new Object3D(coneObj);
+ const monkey = new Object3D(monkeyObj, [0, 0, 0]);
+ const torus = new Object3D(torusObj, [-3, 0, 0]);
+ const cone = new Object3D(coneObj, [3, 0, 0]);

  const objects = [
      monkey,

Enter fullscreen mode Exit fullscreen mode

šŸ“„ src/Object3D.js

  import { mat4 } from "gl-matrix";

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

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

          this.modelMatrix = mat4.create();
          this._normalMatrix = mat4.create();

Enter fullscreen mode Exit fullscreen mode

and this position should be respected by model matrix

šŸ“„ src/Object3D.js

          this.position = position;

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


Enter fullscreen mode Exit fullscreen mode

And one more thing. We can also define a color specific to each object

šŸ“„ src/3d.js


  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

- const monkey = new Object3D(monkeyObj, [0, 0, 0]);
- const torus = new Object3D(torusObj, [-3, 0, 0]);
- const cone = new Object3D(coneObj, [3, 0, 0]);
+ const monkey = new Object3D(monkeyObj, [0, 0, 0], [1, 0, 0]);
+ const torus = new Object3D(torusObj, [-3, 0, 0], [0, 1, 0]);
+ const cone = new Object3D(coneObj, [3, 0, 0], [0, 0, 1]);

  const objects = [
      monkey,
      cone,
  ];

- gl.uniform3fv(programInfo.uniformLocations.color, [0.5, 0.5, 0.5]);
- 
  const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, monkey.vertices, gl.STATIC_DRAW);
  const normalsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, monkey.normals, gl.STATIC_DRAW);

          gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, object.modelMatrix);
          gl.uniformMatrix4fv(programInfo.uniformLocations.normalMatrix, false, object.normalMatrix);

+         gl.uniform3fv(programInfo.uniformLocations.color, object.color);
+ 
          vertexBuffer.setData(gl, object.vertices, gl.STATIC_DRAW);
          normalsBuffer.setData(gl, object.normals, gl.STATIC_DRAW);


Enter fullscreen mode Exit fullscreen mode

šŸ“„ src/Object3D.js

  import { mat4 } from "gl-matrix";

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

          this.vertices = vertices;
          this.modelMatrix = mat4.create();
          mat4.fromTranslation(this.modelMatrix, position);
          this._normalMatrix = mat4.create();
+ 
+         this.color = color;
      }

      get normalMatrix () {

Enter fullscreen mode Exit fullscreen mode

Multiple objects 2

Yay! We now can render multiple objects with individual transforms and colors šŸŽ‰

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

šŸ’– šŸ’Ŗ šŸ™… šŸš©
lesnitsky
Andrei Lesnitsky

Posted on July 19, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related