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
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.