- Three dimensional objects are composed of triangles, so we need to draw each triangle of the object one by one as before, and finally draw the whole three-dimensional object, but there is a significant difference between three-dimensional and two-dimensional. When drawing two-dimensional objects, we only need to consider X and Y information, but when drawing three-dimensional objects, we also need to consider their depth information
Viewpoint and line of sight
The position of the observation point is called the viewpoint, and the ray starting from the viewpoint along the observation direction is called the line of sight
Point of view, point of view
In order to determine the state of the observer, two pieces of information need to be obtained: viewpoint: that is, the position of the observer, and observation target point, that is, the point where the observation target is located, which can be used to determine the line of sight In addition, if we want to draw the observed scenery on the screen, we also need up direction With these three items, we can determine the state of the observer

- Viewpoint: the position of the observer in three-dimensional space and the starting point of the line of sight
- Observation target point: the point where the observed target is located. The line of sight starts from the viewpoint, passes through the target point and continues to extend,
- Up direction: the upward direction in the image finally drawn on the screen
In webgl, we can create a view matrix with the above three vectors, and then pass the matrix to the shader The view matrix can represent the observer’s state, including the observer’s viewpoint, observation target point and up direction information In three View matrix encapsulated in JS Setlookat method
Matrix4.setLookAt(eyeX,eyeY,eyeZ,atX,atY,atZ,upX,upY,upZ)
//Eyex, eyez, eyez stands for viewpoint
//ATX, ATY and ATZ represent observation points
//Upx, upy and upz represent the up direction
In webgl, the default state of the observer should be
- The viewpoint is at the coordinate origin by default
- The line of sight is the negative direction of Z, the observation point is (0,0, – 1), and the upward direction is the negative half axis of Y, i.e. (0,1,0)
The example code is as follows
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body onload="main()">
<canvas width="400" height="400"></canvas>
</body>
<script type="x-shader/x-vertex">
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix;
varying vec4 v_Color;
void main () {
gl_Position = u_ViewMatrix*a_Position;
v_Color = a_Color;
}
</script>
<script type="x-shader/x-fragment">
//Set the precision of floating-point numbers globally. Other types have default precision types. Floating-point numbers need to be set separately
precision mediump float;
varying vec4 v_Color;
void main () {
gl_FragColor = v_Color;
}
</script>
<script src="./jsm/util.js"></script>
<script src="tool/cuon-matrix.js"></script>
<script>
function main() {
const canvas = document.getElementById('webgl')
const gl = canvas.getContext('webgl')
const vertextShader = document.getElementById('vertextShader').innerText
const fragmentShader = document.getElementById('fragmentShader').innerText
if (!initShaders(gl, vertextShader, fragmentShader)) return
if (!gl) return
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
const a_Color = gl.getAttribLocation(gl.program, 'a_Color')
const u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix')
if (a_Position < 0) return
//Create buffer
let n = initVertexBuffers(gl, a_Position, a_Color)
//Write data to buffer
let viewMatrix = new Matrix4()
viewMatrix.setLookAt(
0.20, 0.25, 0.25, // viewpoint
0.0, 0.0, 0.0, // observation point
0, 1, 0, // up
)
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements)
gl.drawArrays(gl.TRIANGLES, 0, n)
}
function initVertexBuffers(gl, a_Position, a_Color) {
let verticesColors = new Float32Array([
0.0, 0.5, -0.4, 1.0, 0.0, 0.0,
-0.5, - 0.5, - 0.4, 1.0, 0.0, 0.0, // red triangle
0.5, -0.5, -0.4, 0.1, 0.0, 0.0,
0.5, 0.4, -0.2, 0.0, 1.0, 0.0,
-0.5, 0.4, - 0.2, 0.0, 1.0, 0.0, // Green Triangle
0.0, -0.6, -0.2, 0.0, 1.0, 0.0,
0.0, 0.5, 0.0, 0.0, 0.0, 1.0,
-0.5, - 0.5, 0.0, 0.0, 0.0, 1.0, // blue
0.5, -0.5, 0.0, 0.0, 0.0, 1.0,
])
var n = 9
var vertexColorbuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer)
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)
let fsize = verticesColors.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, fsize * 6, 0)
gl.enableVertexAttribArray(a_Position)
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, fsize * 6, fsize * 3)
gl.enableVertexAttribArray(a_Color)
return n
}
</script>
</html>
In fact, according to the custom observer state, drawing the scene seen by the observer is equivalent to translating, rotating and zooming with 3D objects using the default observer state
Visual range
Although we can place a 3D object anywhere in 3D space, web GL can only draw it when it is within its visual range In fact, not drawing objects outside the visual range is a means to reduce program overhead In addition to the limitations of horizontal and vertical range, webgl also limits the visual depth of the observer, that is, how far he can see. All these limitations, including horizontal perspective, vertical perspective and visual depth, define the visual space
visual space
There are two visual spaces
- Cuboid visual space, also known as boxed space, is generated by orthographic projection
- Pyramid visual space, generated by perspective projection
Under perspective projection, the generated 3D scene looks more deep and natural. In contrast, orthophoto projection allows users to easily compare the size of objects in the scene, because the size of objects has nothing to do with their location
Working principle of box visual space of Orthophoto projection
The shape of the box visual space is shown in the figure below The visual space is determined by the front and back rectangular surfaces, which are called near clipping surface and far clipping surface respectively The first four vertices are (right, top, – near), (- left, top, – near) (left, – bottom, – near) (right, – bottom, – near) and the last four vertices are (right, top, far) (- left, top, far) (left, – bottom, far) (right, – bottom, far) What is displayed on canvas is the projection of the visible space object on the near cut plane If the width ratio of the clipping surface is different from that of the canvas, the picture will be compressed according to the width ratio of the canvas, and the object will be deformed

Define box visual space
THREE. JS provides matrix4 The setortho method can be used to set the projection matrix
parameter | Parameter description |
---|---|
left,right,bottom,top | Specify the left and right boundaries of the near cut surface |
bottom,top | Upper and lower boundary of near cutting surface |
near,far | Specify the location of the near and far cut faces, that is, the near and far boundaries of the visual space |
- Note: the object always looks at far from near. If the value of near is greater than far, it means looking at the object from the opposite side
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body onload="main()">
<canvas width="400" height="400"></canvas>
</body>
<script type="x-shader/x-vertex">
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ProjMatrix;
varying vec4 v_Color;
void main () {
gl_Position = u_ProjMatrix*a_Position;
v_Color = a_Color;
}
</script>
<script type="x-shader/x-fragment">
//Set the precision of floating-point numbers globally. Other types have default precision types. Floating-point numbers need to be set separately
precision mediump float;
varying vec4 v_Color;
void main () {
gl_FragColor = v_Color;
}
</script>
<script src="./jsm/util.js"></script>
<script src="tool/cuon-matrix.js"></script>
<script>
let g_near =0.0,g_far = 0.5;
function main() {
const canvas = document.getElementById('webgl')
const gl = canvas.getContext('webgl')
const vertextShader = document.getElementById('vertextShader').innerText
const fragmentShader = document.getElementById('fragmentShader').innerText
if (!initShaders(gl, vertextShader, fragmentShader)) return
if (!gl) return
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
const a_Color = gl.getAttribLocation(gl.program, 'a_Color')
const u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix')
if (a_Position < 0) return
let projMatrix = new Matrix4()
//Create buffer
let n = initVertexBuffers(gl, a_Position, a_Color);
draw(gl,n,u_ProjMatrix,projMatrix);
document.onkeydown = function(ev) {
keyDown(ev, gl, n, u_ProjMatrix, projMatrix)
}
}
function keyDown(ev, gl, n, u_ViewMatrix, viewMatrix) {
//Push button
console.log(ev.keyCode)
switch(ev.keyCode) {
case 39: {
g_near +=0.1;break
};
case 37: g_near -= 0.01;break;
case 38: g_far +=0.01;break;
case 40: g_far -=0.01;break;
}
draw(gl,n, u_ViewMatrix, viewMatrix)
}
function draw(gl,n, u_ViewMatrix, viewMatrix) {
// debugger
viewMatrix.setOrtho(-1,1,-1,1,g_near,g_far)
console.log( g_near,g_far)
// viewMatrix.setLookAt(g_eyeX, g_eyeY, g_eyeZ, 0, 0, 0, 0, 1, 0)
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, n)
}
function initVertexBuffers(gl, a_Position, a_Color) {
let verticesColors = new Float32Array([
0.0, 0.5, -0.4, 1.0, 0.0, 0.0,
-0.5, - 0.5, - 0.4, 1.0, 0.0, 0.0, // red triangle
0.5, -0.5, -0.4, 0.1, 0.0, 0.0,
0.5, 0.4, -0.2, 0.0, 1.0, 0.0,
-0.5, 0.4, - 0.2, 0.0, 1.0, 0.0, // Green Triangle
0.0, -0.6, -0.2, 0.0, 1.0, 0.0,
0.0, 0.5, 0.0, 0.0, 0.0, 1.0,
-0.5, - 0.5, 0.0, 0.0, 0.0, 1.0, // blue
0.5, -0.5, 0.0, 0.0, 0.0, 1.0,
])
var n = 9
var vertexColorbuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer)
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)
let fsize = verticesColors.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, fsize * 6, 0)
gl.enableVertexAttribArray(a_Position)
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, fsize * 6, fsize * 3)
gl.enableVertexAttribArray(a_Color)
return n
}
</script>
</html>
As can be seen from the above example, when the face near is at the origin, the specific of far can see the completed three triangles behind the third triangle. Far doesn’t understand. When near moves between the first triangle and the second triangle, only the second and third triangles can be seen This is because the first triangle is not in the visible range. Similarly, if near moves between the second and third triangles, the first and second triangles will not be visible. If near moves to the third triangle, they will not be visible
perspective projection
The visual space of perspective projection is shown in the figure below Perspective projection also has viewpoint, line of sight, near cut plane and far cut plane In this way, objects in visual space will be displayed, and objects outside visual space will not be displayed

In three The setperspective method is defined in JS matrix to set the projection matrix
parameter | Parameter description | Parameter location |
---|---|---|
fov | Specify vertical viewing angle | 0 |
aspect | Specifies the aspect ratio of the near cut face | 1 |
near,far | Distance between near cutting surface and far cutting surface | 2,3 |
Correctly handle the context of objects
By default, webgl will draw graphics in the order of buffer, and the graphics drawn later will overwrite the graphics drawn earlier Because it’s efficient If the objects in the scene do not change, the observer’s state is unique There’s no problem with this, but if you want to keep moving your point of view and look at objects from different angles, you can’t decide the order of objects in advance In order to solve this problem, web GL provides the function of hidden surface elimination This function will help us eliminate those occluded surfaces. We can draw the scene safely without worrying about the order of objects in the buffer To enable the hidden surface elimination function, you need to follow the following steps
- Turn on hidden surface elimination
gl.enable(gl.DEPTH_TEST) - Clear the depth buffer before drawing
gl.clear(gl.DEPTH_BUFFER_BIT)
Use GL The clear () method clears the depth buffer Depth buffer is an intermediate object, which is used to help webgl eliminate hidden faces Webgl draws geometric figures in the color punch, and displays the color buffer on canvas after drawing If you want to eliminate the hidden surface, you must know the depth information of each geometry, and the depth buffer is used to store the depth information. Because the depth direction is usually the z-axis direction, we also call it Z buffer
Deep conflict
Hidden surface elimination is a complex and powerful feature of Web GL. In most cases, it can complete the task well However, problems arise when the two surfaces of a geometry or object are close This phenomenon is called deep conflict In order to solve this problem, web GL provides a mechanism called polygon offset to solve this problem This mechanism will automatically add an offset to the Z value The value of this offset is determined by the angle of the object surface relative to the observer Starting this mechanism requires the following two steps
- Start polygon offset
gl.enable(gl.POLY_OFFSET_FILL) - Specify the parameters for calculating the offset before drawing
gl.polygonOffset(1.0,1.0)
Cube
As mentioned earlier, we draw a cube by drawing two triangles on each face of the cube, that is, a cube with 36 vertices But in fact, the cube has only 8 vertices, but 36 vertices are used, which increases the complexity of the code Now provides a more comprehensive way to draw cubes Firstly, the cube is divided into six faces. Each face is composed of two triangles, which is related to the two triangles in China As shown in the figure below

Web GL provides drawing objects through vertex index gl. drawElement(mode,count,type,offset)
parameter | Parameter description |
---|---|
mode | Specifies how to draw |
count | Specifies the number of vertices to paint |
type | Index data type |
offset | Specifies where to start drawing in the index array |
The sample code for drawing a cube is as follows
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body onload="main()">
<canvas width="400" height="400"></canvas>
</body>
<script type="x-shader/x-vertex">
attribute vec4 a_Position;
uniform mat4 u_MvpMatrix;
attribute vec4 a_Color;
varying vec4 v_Color;
void main () {
gl_Position = u_MvpMatrix *a_Position;
v_Color = a_Color;
}
</script>
<script src="tool/cuon-matrix.js"></script>
<script type="x-shader/x-fragment">
precision mediump float;
varying vec4 v_Color;
void main () {
gl_FragColor = v_Color;
}
</script>
<script src="./jsm/util.js"></script>
<script src="tool/cuon-matrix.js"></script>
<script>
function main() {
const canvas = document.getElementById('webgl')
const gl = canvas.getContext('webgl')
const vertextShader = document.getElementById('vertextShader').innerText
const fragmentShader = document.getElementById('fragmentShader').innerText
if (!initShaders(gl, vertextShader, fragmentShader)) return
if (!gl) return
const u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
// if (a_Position < 0) return
// debugger
let n = initVertexBuffers(gl)
var viewMatrix = new Matrix4()
viewMatrix.setLookAt(3,3,7,0,0,0,0,1,0)
var modeMatrix = new Matrix4()
modeMatrix.setRotate(0,0,0,1)
let projMatrix = new Matrix4()
projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100)
var modeViewMatrix = projMatrix.multiply(viewMatrix.multiply(modeMatrix))
gl.enable(gl.DEPTH_TEST)
// mvpMatrix.setLookAt(3,3,7,0,0,0,0,1,0)
gl.uniformMatrix4fv(u_MvpMatrix, false, modeViewMatrix.elements)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0)
}
function initVertexBuffers(gl) {
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
const a_Color = gl.getAttribLocation(gl.program, 'a_Color')
var verticesColors = new Float32Array([
1.0, 1.0, 1.0, 1.0, 0.0, 0.0, // dot color 0
-1.0, 1.0, 1.0, 1.0, 1.0, 0.0, // dot color 1
-1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, // 2 dots
1.0, - 1.0, 1.0, 0.8, 0.9, 0.2, // 3 dots
1.0, - 1.0, - 1.0, 0.3, 0.5, 0.7, // 4 dots
1.0, 1.0, - 1.0, 0.4, 0.6, 0.8, // 5 dots
-1.0, 1.0, - 1.0, 0.5, 0.6, 0.7, // 6 dots
-1.0, - 1.0, - 1.0, 0.9, 0.6, 0.3 // color 7
])
var indices = new Uint8Array([
0,1,2,0,2,3,
0,3,4,0,4,5,
0,5,6,0,6,1,
1,6,7,1,7,2,
7,4,3,7,3,2,
4,7,6,4,6,5
])
var vertexColorBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorBuffer)
gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW)
var FSize = verticesColors.BYTES_PER_ELEMENT
gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSize*6,0)
gl.enableVertexAttribArray(a_Position)
gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSize*6,FSize*3)
gl.enableVertexAttribArray(a_Color)
var indicesBuffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indicesBuffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW)
return indices.length
}
</script>
</html>