文章目录
- 前言
- 三角形构成三维物体
- 视点、目标、正方向
- 视图矩阵
- 辅助函数:归一化、向量差、点积、叉积
- 视图矩阵的数学表示与使用
- 视图矩阵构建三维世界
- 注意
前言
在前面的学习中,已经得知了webgl是如何绘制二维图形,并进行仿射变换(矩阵变换)、纹理、交互等操作也适用于绘制三维图形。本节将介绍webgl是如何通过视图矩阵进入三维世界的。
三角形构成三维物体
三角形是三维物体的基本组成单元,如上图所示的一个立方体,绘制在屏幕上其实是由若干个形状大小不一的三角形依次组装而成的。但是,三维与二维相比多出了Z轴一个维度,这个维度代表的是深度信息,有了这个维度能正确不同三角形的遮挡信息,从而绘制出三维物体。
视点、目标、正方向
为了正确表示webgl坐标系中每个点在观察者(或者相机)中的坐标,首先要构建一个属于观察者的三维坐标系统。为了确定观察者的状态,你需要获取三项信息:视点,目标点,上方向。有了这三项信息,就可以确定物体的三维坐标了,它们的含义如下:
- 视点
视线的起点,也就是眼睛所在三维空间中的位置(eyeX,eyeY,eyeZ)。
- 目标点
被观察目标所在的点,当确立目标点和视点时,视线方向也随着确立。目标点的坐标用(atX,atY,atZ)表示。
- 上方向
最终绘制在屏幕上的影像中的向上的方向,即正方向。当视线方向确定时,观察者还能够以视线为轴旋转的。当指定上方向时,整个坐标就彻底固定住。上方向是具有3 个分量的矢量(upX,upY,upZ)。
在webgl的默认规则中,视点位于坐标系统原点(0,0,0),视线为Z轴负方向(屏幕内),上方向为Y轴负方向(向下)。
视图矩阵
使用 视点,目标点,上方向 三个矢量可以创建一个视图矩阵(view matrix),将该矩阵传给顶点着色器,重新构建观察者的状态,最终影响了显示在屏幕上的视图。
辅助函数:归一化、向量差、点积、叉积
为了创建视图矩阵,需要使用几个线性几何函数辅助计算,由于javascript中没有相关的数据类型,因此需要自己构建。
- 向量归一化
将向量转化为单位向量
function normlized (arr) {
let sum = 0;
let _mod;
arr.map((item)=>{
sum += item * item
})
_mod = Math.sqrt(sum);
const normlizedVecArr =
arr.map((item) =>
item / mod
)
return normlizedVecArr;
}
- 向量减法
两点确定指向
function diff (arr1, arr2) {
if (arr1.length !==3 || arr2.length !==3) return
return new Float32Array([
arr1[0] - arr2[0],
arr1[1] - arr2[1],
arr1[2] - arr2[2]
]);
}
- 向量点积
用来投影长度
function dot (arr1, arr2) {
if (arr1.length !==3 || arr2.length !==3) return
return arr1[0] * arr2[0] + arr1[1] * arr2[1] + arr1[2] * arr2[2];
}
- 向量叉积
用来获取法向量
function cross (arr1, arr2) {
if (arr1.length !==3 || arr2.length !==3) return
return new Float32Array([
arr1[1] *arr2[2] - arr1[2] *arr2[1],
arr1[2] *arr2[0] - arr1[0] *arr2[2],
arr1[0] *arr2[1] - arr1[1] *arr2[0],
]);
}
视图矩阵的数学表示与使用
- 视图矩阵创建
function createViewMatrix(eye_Matrix, look_Matrix, up_Matrix){
// Z轴
const Z = normlized(diff( look_Matrix, eye_Matrix));
// X轴
const X = normlized(cross(Z, normlized(up_Matrix)));
// Y轴
const Y = normlized(cross(Z, X));
console.log(Z, X, Y)
return new Float32Array([
X[0], Y[0], -Z[0], 0,
X[1], Y[1], -Z[1], 0,
X[2], Y[2], -Z[2], 0,
-dot(X,eye_Matrix), -dot(Y,eye_Matrix), -dot(Z,eye_Matrix), 1,
])
}
- 在webgl中使用
// 获取canvas元素对象
let canvas = document.getElementById('canvas');
// canvas绑定事件
canvas.onmousedown = function (ev) { click(ev, gl, canvas, aPosition) }
let pointList = []
// 获取webgl绘图上下文
const gl = canvas.getContext('webgl');
if (!gl) {
throw new Error('WebGL not supported');
}
canvas.width = 500;
canvas.height = 500;
gl.viewport(0, 0, canvas.width, canvas.height)
// 设置背景色
gl.clearColor(1.0, 1.0, 0.0, 1.0)
// 清空缓冲区
gl.clear(gl.COLOR_BUFFER_BIT)
const vertex = `
attribute vec4 aPosition;
uniform mat4 uViewMatrix;
void main() {
gl_Position = uViewMatrix * aPosition;
gl_PointSize = 10.0;
}
`
const fragment = `
precision highp float;
void main(){
gl_FragColor =vec4(1.0,0.0,1.0,1.0);
}
`
// 创建program
const program = initShader(gl, vertex, fragment)
// 获取attribute变量的数据存储位置
const aPosition = gl.getAttribLocation(program, 'aPosition');
// 获取uniform变量的数据存储位置
const uViewMatrix = gl.getUniformLocation(program, 'uViewMatrix');
// 创建缓冲区对象
const buffer = gl.createBuffer();
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 传入的数据
const vertices = new Float32Array([
0.0, 0.5, // 顶点着色器补全为(0.0,0.5,0.0,1.0)
-0.5, -0.5,
0.5, -0.5
])
// 开辟空间并写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
// 缓冲区对象分配给attribute变量
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
// 开启attribue变量
gl.enableVertexAttribArray(aPosition)
const viewMatrix = createViewMatrix(
new Float32Array(), // 视点位置
new Float32Array(), // 目标点位置
new Float32Array(), // 上方向
)
console.log(viewMatrix)
gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix)
// 开始绘制
gl.drawArrays(gl.TRIANGLES, 0, 3)
视图矩阵构建三维世界
如上图,揭示了视图矩阵与webgl世界坐标之间的关系,下面我们通过几个例子来深入理解视图矩阵。
- 使用默认规则
前面我们说到,在webgl的默认规则中,视点位于坐标系统原点(0,0,0),视线为Z轴负方向(屏幕内),上方向为Y轴负方向(向下),首先我们以这个规则进行尝试,与默认效果相同。
const viewMatrix = createViewMatrix(
new Float32Array([0.0, 0.0, 0.0]), // 视点位置
new Float32Array([0.0, 0.0, 1.0]), // 目标点位置
new Float32Array([0.0, -1.0, 0.0]), // 上方向
)
- 增加三角形
接下来,我们在缓冲区中存储两个三角形的顶点和颜色信息进行绘制:
let canvas = document.getElementById('canvas');
// canvas绑定事件
canvas.onmousedown = function (ev) { click(ev, gl, canvas, aPosition) }
let pointList = []
// 获取webgl绘图上下文
const gl = canvas.getContext('webgl');
if (!gl) {
throw new Error('WebGL not supported');
}
canvas.width = 500;
canvas.height = 500;
gl.viewport(0, 0, canvas.width, canvas.height)
// 设置背景色
gl.clearColor(1.0, 1.0, 0.0, 1.0)
// 清空缓冲区
gl.clear(gl.COLOR_BUFFER_BIT)
const vertex = `
attribute vec4 aPosition;
uniform mat4 uViewMatrix;
attribute vec4 aColor;
varying vec4 v_Color;
void main() {
gl_Position = uViewMatrix * aPosition;
gl_PointSize = 10.0;
v_Color = aColor;
}
`
const fragment = `
precision highp float;
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
`
// 创建program
const program = initShader(gl, vertex, fragment)
// 获取attribute变量的数据存储位置
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aColor = gl.getAttribLocation(program, 'aColor');
// 获取uniform变量的数据存储位置
const uViewMatrix = gl.getUniformLocation(program, 'uViewMatrix');
// 创建缓冲区对象
const buffer = gl.createBuffer();
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 传入的数据
const vertices = new Float32Array([
0.0, 0.5, -0.4, 0.4, 1.0, 0.4, // The back green one
-0.5, -0.5, -0.4, 0.4, 1.0, 0.4,
0.5, -0.5, -0.4, 1.0, 0.4, 0.4,
0.5, 0.4, -0.2, 1.0, 0.4, 0.4, // The middle yellow one
-0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
0.0, -0.6, -0.2, 1.0, 1.0, 0.4,
])
const BYTES = vertices.BYTES_PER_ELEMENT;
// 开辟空间并写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
// 缓冲区对象分配给attribute变量
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 6 * BYTES, 0)
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 6 * BYTES, 3 * BYTES)
// 开启attribue变量
gl.enableVertexAttribArray(aPosition)
gl.enableVertexAttribArray(aColor)
const viewMatrix = createViewMatrix(
new Float32Array([0.0, 0.0, 0.0]), // 视点位置
new Float32Array([0.0, 0.0, 1.0]), // 目标点位置
new Float32Array([0.0, 1.0, 0.0]), // 上方向
)
gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix)
// 开始绘制
gl.drawArrays(gl.TRIANGLES, 0, 6)
效果如下:
修改视点和目标点的位置,会产生不一样的效果。
const viewMatrix = createViewMatrix(
new Float32Array([0.20, 0.25, 0.25]), // 视点位置
new Float32Array([0.0, 0.0, 0.0]), // 目标点位置
new Float32Array([0.0, 1.0, 0.0]), // 上方向
)
注意
-
根据两个三角形的遮挡关系,容易让人以为webgl是右手坐标系,实际上它是左手坐标系,当开启深度检测时,前后关系就会反转。
-
GPU 在绘制过程会经过视口变换后,将所有在 [-1, 1] 之间的坐标映射到屏幕空间中,所以将视点位置设置在两个三角形之间时会看到它们仍然都被绘制。
-
经过视图矩阵变换后并不能直接转化为屏幕上的坐标,还要经过投影变换(正射/透视)。