1. An overview of the

In my last tutorial, WebGL Easy Tutorial 6: The First 3D Example (Using Model-view Projection Transforms), I drew a set of triangles from far to near using model-view projection transforms. However, this example is still too simple. The coordinates of these triangles are still between -1 and 1. It is easy to set parameters anyway, and it may not be very in-depth to understand the model view projection transformation.

In this tutorial I’ll take it a step further and draw a slightly more complex entity called a rectangular body. The rectangular body can be used as a bounding box for 3D objects in many cases. The bounding box is particularly useful in many cases, especially in UI interaction. As long as the parameters can be set to let the bounding box see, its 3D objects must also be visible. In order to better understand the model view projection transformation, deliberately set the coordinates of the rectangle body to a relatively large floating point number.

Example 2.

Improve the JS code from the previous tutorial, resulting in the new code as follows:

// Vertex shader program
var VSHADER_SOURCE =
  'attribute vec4 a_Position; \n' + // attribute variable
  'attribute vec4 a_Color; \n' +
  'uniform mat4 u_MvpMatrix; \n' +
  'varying vec4 v_Color; \n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' + // Set the vertex coordinates of the point
  '  v_Color = a_Color;\n' +
  '}\n';

// Chip shader program
var FSHADER_SOURCE =
  'precision mediump float; \n' +
  'varying vec4 v_Color; \n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

// Define a rectangle: hybrid constructor prototype pattern
function Cuboid(minX, maxX, minY, maxY, minZ, maxZ) {
  this.minX = minX;
  this.maxX = maxX;
  this.minY = minY;
  this.maxY = maxY;
  this.minZ = minZ;
  this.maxZ = maxZ;
}

Cuboid.prototype = {
  constructor: Cuboid,
  CenterX: function () {
    return (this.minX + this.maxX) / 2.0;
  },
  CenterY: function () {
    return (this.minY + this.maxY) / 2.0;
  },
  CenterZ: function () {
    return (this.minZ + this.maxZ) / 2.0;
  },
  LengthX: function () {
    return (this.maxX - this.minX);
  },
  LengthY: function () {
    return (this.maxY - this.minY); }}var currentAngle = [35.0.30.0]; // The rotation Angle about the X-axis and Y-axis ([X-axis, Y-axis])
var curScale = 1.0;   // The current scale

function main() {
  // Get the 
      
        element
      
  var canvas = document.getElementById('webgl');

  // Get the WebGL rendering context
  var gl = getWebGLContext(canvas);
  if(! gl) {console.log('Failed to get the rendering context for WebGL');
    return;
  }

  // Initialize the shader
  if(! initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {console.log('Failed to intialize shaders.');
    return;
  }

  // Set the vertex position
  var cuboid = new Cuboid(399589.072.400469.072.3995118.062.3997558.062.732.1268);
  var n = initVertexBuffers(gl, cuboid);
  if (n < 0) {
    console.log('Failed to set the positions of the vertices');
    return;
  }

  // specify to clear the color of 
      
  gl.clearColor(0.0.0.0.0.0.1.0);

  // Enable depth testing
  gl.enable(gl.DEPTH_TEST);

  // Draw the function
  var tick = function () {
    // Set the MVP matrix
    setMVPMatrix(gl, canvas, cuboid);

    // Clear the color and depth buffers
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // Draw a rectangle
    gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);

    // Request the browser to call the tick
    requestAnimationFrame(tick);  
  };

  // Start drawing
  tick();

  // Draw a rectangle
  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

// Set the MVP matrix
function setMVPMatrix(gl, canvas, cuboid) {
  // Get the storage location of u_MvpMatrix
  var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
  if(! u_MvpMatrix) {console.log('Failed to get the storage location of u_MvpMatrix');
    return;
  }

  // Model matrix
  var modelMatrix = new Matrix4();
  modelMatrix.scale(curScale, curScale, curScale);
  modelMatrix.rotate(currentAngle[0].1.0.0.0.0.0); // Rotation around x-axis 
  modelMatrix.rotate(currentAngle[1].0.0.1.0.0.0); // Rotation around y-axis 
  modelMatrix.translate(-cuboid.CenterX(), -cuboid.CenterY(), -cuboid.CenterZ());

  // Projection matrix
  var fovy = 60;
  var near = 1;
  var projMatrix = new Matrix4();
  projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1.10000);

  // Calculate the height of the initial viewpoint of lookAt()
  var angle = fovy / 2 * Math.PI / 180.0;  
  var eyeHight = (cuboid.LengthY() * 1.2) / 2.0 / angle;

  // View matrix
  var viewMatrix = new Matrix4();  // View matrix   
  viewMatrix.lookAt(0.0, eyeHight, 0.0.0.0.1.0);

  / / the MVP matrix
  var mvpMatrix = new Matrix4();
  mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);

  // Transfer the MVP matrix to the uniform variable u_MvpMatrix in the shader
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
}

//
function initVertexBuffers(gl, cuboid) {
  // Create a cube
  // v6----- v5
  // /| /|
  // v1------v0|
  // | | | |
  // | |v7---|-|v4
  // |/ |/
  // v2------v3
  // Vertex coordinates and colors
  var verticesColors = new Float32Array([
    cuboid.maxX, cuboid.maxY, cuboid.maxZ, 1.0.1.0.1.0.// v0 White
    cuboid.minX, cuboid.maxY, cuboid.maxZ, 1.0.0.0.1.0.// v1 Magenta
    cuboid.minX, cuboid.minY, cuboid.maxZ, 1.0.0.0.0.0.// v2 Red
    cuboid.maxX, cuboid.minY, cuboid.maxZ, 1.0.1.0.0.0.// v3 Yellow
    cuboid.maxX, cuboid.minY, cuboid.minZ, 0.0.1.0.0.0.// v4 Green
    cuboid.maxX, cuboid.maxY, cuboid.minZ, 0.0.1.0.1.0.// v5 Cyan
    cuboid.minX, cuboid.maxY, cuboid.minZ, 0.0.0.0.1.0.// v6 Blue
    cuboid.minX, cuboid.minY, cuboid.minZ, 1.0.0.0.1.0   // v7 Black
  ]);

  // Vertex index
  var indices = new Uint8Array([
    0.1.2.0.2.3./ / before
    0.3.4.0.4.5./ / right
    0.5.6.0.6.1./ /
    1.6.7.1.7.2./ / left
    7.4.3.7.3.2./ /
    4.7.6.4.6.5     / / after
  ]);

  //
  var FSIZE = verticesColors.BYTES_PER_ELEMENT;   // The number of bytes for each element in the array

  // Create a buffer object
  var vertexColorBuffer = gl.createBuffer();
  var indexBuffer = gl.createBuffer();
  if(! vertexColorBuffer || ! indexBuffer) {console.log('Failed to create the buffer object');
    return -1;
  }

  // Bind the buffer object to the target
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
  // Writes data to the buffer object
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  // Get the address of the attribute variable a_Position in the shader
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  // Assign the buffer object to the a_Position variable
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6.0);

  // Connect the a_Position variable to the buffer object assigned to it
  gl.enableVertexAttribArray(a_Position);

  // Get the address of the attribute variable a_Color in the shader
  var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
  if (a_Color < 0) {
    console.log('Failed to get the storage location of a_Color');
    return -1;
  }
  // Assign the buffer object to the a_Color variable
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  // Connect the a_Color variable with the buffer object assigned to it
  gl.enableVertexAttribArray(a_Color);

  // Write the vertex index to the buffer object
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

  return indices.length;
}
Copy the code

The flow of this code is basically the same as the JS code in the previous article, and the shader part is basically unchanged. There are two main concerns: drawing objects by vertex index and setting up the MVP matrix.

2.1. Vertex index rendering

If you draw a rectangular body based on the previous knowledge, a rectangular body has 6 faces, 2 triangles per face, and 3 points per triangle, that means 36 vertices need to be defined. But we know that a rectangle only needs to have eight vertices, and defining 36 vertices means a waste of memory and video memory. To solve this problem, WebGL provides a way to draw through vertex indexes: gl.drawelements (). Its function is defined as follows:

In this example, we first define an object that describes a rectangular body and, based on its parameters, define an array of its vertices, containing XYZ information and color information.

// Define a rectangle: hybrid constructor prototype pattern
function Cuboid(minX, maxX, minY, maxY, minZ, maxZ) {
  this.minX = minX;
  this.maxX = maxX;
  this.minY = minY;
  this.maxY = maxY;
  this.minZ = minZ;
  this.maxZ = maxZ;
}

Cuboid.prototype = {
  constructor: Cuboid,
  CenterX: function () {
    return (this.minX + this.maxX) / 2.0;
  },
  CenterY: function () {
    return (this.minY + this.maxY) / 2.0;
  },
  CenterZ: function () {
    return (this.minZ + this.maxZ) / 2.0;
  },
  LengthX: function () {
    return (this.maxX - this.minX);
  },
  LengthY: function () {
    return (this.maxY - this.minY); }}/ /...

// Vertex coordinates and colors
var verticesColors = new Float32Array([
  cuboid.maxX, cuboid.maxY, cuboid.maxZ, 1.0.1.0.1.0.// v0 White
  cuboid.minX, cuboid.maxY, cuboid.maxZ, 1.0.0.0.1.0.// v1 Magenta
  cuboid.minX, cuboid.minY, cuboid.maxZ, 1.0.0.0.0.0.// v2 Red
  cuboid.maxX, cuboid.minY, cuboid.maxZ, 1.0.1.0.0.0.// v3 Yellow
  cuboid.maxX, cuboid.minY, cuboid.minZ, 0.0.1.0.0.0.// v4 Green
  cuboid.maxX, cuboid.maxY, cuboid.minZ, 0.0.1.0.1.0.// v5 Cyan
  cuboid.minX, cuboid.maxY, cuboid.minZ, 0.0.0.0.1.0.// v6 Blue
  cuboid.minX, cuboid.minY, cuboid.minZ, 1.0.0.0.1.0   // v7 Black
]);

/ /...
Copy the code

As in the previous code, the vertex and color arrays are passed to the vertex buffer object. The difference is that we also define a vertex-indexed array:

// Vertex index
var indices = new Uint8Array([
  0.1.2.0.2.3./ / before
  0.3.4.0.4.5./ / right
  0.5.6.0.6.1./ /
  1.6.7.1.7.2./ / left
  7.4.3.7.3.2./ /
  4.7.6.4.6.5     / / after
]);
Copy the code

This array really defines the drawing order of triangles in the rectangle body. The vertices of each triangle are replaced by the index value in the vertex array, which is handed to WebGL to identify, as shown in the figure:

Similarly, the vertex-indexed array should also be passed to the buffer object. Except instead of binding to gl.array_buffer, it is bound to gl.element_array_buffer. This parameter indicates that the contents of the buffer are vertex index data. The relevant codes are as follows:

// Create a buffer object
var indexBuffer = gl.createBuffer();

/ /...

// Write the vertex index to the buffer object
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
Copy the code

Finally, draw the gl.drawelements () function above:

// Draw a rectangle
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
Copy the code

Drawing 3d objects by vertex index can obviously save memory and video memory overhead. The more common points of 3D objects, the more should adopt this method.

2.2. MVP matrix setting

The MVP matrix is also set in the setMVPMatrix() function.

2.2.1. Model matrix

var currentAngle = [35.0.30.0]; // The rotation Angle about the X-axis and Y-axis ([X-axis, Y-axis])
var curScale = 1.0;   // The current scale

/ /...

// Model matrix
var modelMatrix = new Matrix4();
modelMatrix.scale(curScale, curScale, curScale);
modelMatrix.rotate(currentAngle[0].1.0.0.0.0.0); // Rotation around x-axis 
modelMatrix.rotate(currentAngle[1].0.0.1.0.0.0); // Rotation around y-axis 
modelMatrix.translate(-cuboid.CenterX(), -cuboid.CenterY(), -cuboid.CenterZ());
Copy the code

In the model matrix, first shift the center of the rectangular body to the origin of the coordinate system, then rotate 35 degrees about the X axis and 30 degrees about the Y axis, and finally keep the scaling ratio unchanged.

2.2.2. Projection matrix

Generally speaking, the parameters of perspective projection matrix are not easy to set, and can generally be set to a fixed experience value (not absolute).

// Projection matrix
var fovy = 60;
var near = 1;
var projMatrix = new Matrix4();
projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1.10000);
Copy the code

2.2.3. View matrix

Then set the view matrix to display the rectangle in the view using the previous parameters:

// Calculate the height of the initial viewpoint of lookAt()
var angle = fovy / 2 * Math.PI / 180.0;  
var eyeHight = (cuboid.LengthY() * 1.2) / 2.0 / angle;

// View matrix
var viewMatrix = new Matrix4();  // View matrix   
viewMatrix.lookAt(0.0, eyeHight, 0.0.0.0.1.0);
Copy the code

For lookat(), the observation point is the origin of the existing coordinate system, which is the center of the rectangle (which has been shifted); The upper direction is generally the default experience value (0,1,0); So the key is to find the position of the viewpoint, and further, the position of the apparent height.

Then the apparent height can be obtained according to the vertical Angle set by perspective projection, as shown in the figure:

It is obvious that when the light hits the center of the bounding box, half the length of the bounding box in the Y direction, divided by the height of the viewpoint, is half the tangent of fovy. That’s where eyeHight comes from in the code above.

2.2.4. MVP matrix

The MVP matrix is obtained by cascading model matrix, view matrix and projection matrix:

/ / the MVP matrix
var mvpMatrix = new Matrix4();
mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
Copy the code

Results 3.

Open the corresponding HTML in your browser and you’ll see a colored rectangle. The running results are as follows:

4. Reference

Part of the original code and illustrations from the WebGL Programming Guide, source link: address. Subsequent content is continuously updated in this shared directory.