创建视图矩阵
决定视图的参数
三维场景的名词定义:视点、目标点、上方向
为确定观察者状态,需要获取视点【观察者的位置】、目标点【被观察目标所在的点】两项信息。最后要把观察到的景象绘制到屏幕上,还需要知道上方向; 有这三个信息可以确定观察者看到的内容。
- 视点:观察者所在的三维空间中位置,视点坐标用(eyeX, eyeY, eyeZ)表示
- 目标点:被观察目标所在的位置,目标点坐标用(atX, atY, atZ)表示。目标点和视点之间的连线,是视线方向。
- 上方向:要想最终确定在屏幕上显示的内容,还需要确定一个上方向;如果仅仅确定视点和观察点,观察者还可以以视线为轴进行旋转,这样导致看到的目标点形状就会发生变化;上方向用3个分量的矢量来表示(upX, upY, upZ).
可视范围
三维空间中的三维物体只有在可视范围内,WEBGL才会绘制它;
人类只能看到眼前的东西,水平视角大约200度左右。绘制可视范围外的对象没有意义。
辅助函数
- 归一化函数 normalized:将数据调整到
-1到1或者0到1之间 - 叉集 cross : 获取两个平面的法向量
- 点集 dot : 获取某点在x、y、z轴的投影长度
- 向量差:获取目标点和视点的向量
// 归一化函数
function normalized(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i] * arr[i]
}
const middle = Math.sqrt(sum);
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] / middle;
}
}
// 叉积函数 获取法向量
function cross(a,b) {
return new Float32Array([
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
])
}
// 点积函数 获取投影长度
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
// 向量差
function minus(a, b) {
return new Float32Array([
a[0] - b[0],
a[1] - b[1],
a[2] - b[2],
])
}
通过使用以上函数,推导出视图矩阵
// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
// 视点
const eye = new Float32Array([eyex, eyey, eyez])
// 目标点
const lookAt = new Float32Array([lookAtx, lookAty, lookAtz])
// 上方向
const up = new Float32Array([upx, upy, upz])
// 确定z轴
const z = minus(eye, lookAt);
normalized(z);
normalized(up);
// 确定x轴
const x = cross(z, up);
normalized(x);
// 确定y轴
const y = cross(x, z);
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),-dot(y,eye),-dot(z,eye),1
])
}
视图矩阵实例代码
代码通过改变视点位置坐标,形成视图动画效果。
正射投影(平行投影)
概念定义
左侧为透视投影,右侧为正射投影;
在透视投影下,产生的三维场景有近大远小的透视感,更符合真是场景。
正射投影的好处是用户可以方便比较场景中物体的大小,这是因为物体看上去大小与其所在的位置没有关系,在建筑平面图等测绘图中使用。
下图是正射投影,投射到xy平面的示例。
A点做正射投影映射,转化到A';
首先推导左右区间
用L表示left的值,用R表示right的值
以此类推上下区间和远近区间公式
根据矢量点和一个矩阵相乘,推导出x'、y'、z'、w'的值
x' = ax + by+ cz + d;
y' = ex + fy + gz + h;
z' = ix + jy + kz + l;
w' = mx + ny + oz + p;
将左右区间、上下区间、远近区间公式和上面图中的x'、y'、z'、w'进行等式替换;
// 只有 a= 2/(r-l) 且 d=-(r+l)/(r-l)、b=c=0 时等式成立
// 只有 f= 2/(t-b) 且 h=-(t+b)/(t-b)、e=g=0 时等式成立
// 只有 k= 2/(f-n) 且 l=-(f+n)/(f-n)、i=j=0 时等式成立
// m=n=o=0且p=1
将以上等式矩阵,转为列矩阵,就是下面的正射投影矩阵
正射投影矩阵
// 获取正射投影矩阵
function getOrtho(l, r, t, b, n, f) {
return new Float32Array([
2 / (r - l), 0, 0, 0,
0, 2/(t-b), 0, 0,
0, 0, -2/(f-n), 0,
-(r+l)/(r-l),-(t+b)/(t-b),-(f+n)/(f-n),1
])
}
完整示例代码
案例通过移动视点坐标,实现视图改变的动画效果。
透视投影
概念定义
下图左侧为透视投影;
下图右侧在WEBGL场景中放置3个大小同样的三角形,区别是在Z轴的位置不同;视点在(0, 0, 5)位置,可以产生出如下图左侧的效果。
透视投影符合近大远小的规律;
先将透视投影缩放到正射投影,然后通过缩放矩阵和正射投影矩阵相乘,得到透视投影矩阵
推导透视矩阵公式
获取透视比例关系
根据相似三角形定律,可以得出左边的比例关系,从而推导出 y' = yn/f 和 x'=xn/f
在做映射时,z坐标的长度f是固定不变,f为定长1,所以 y' = yn和x'=xn
第一步:推算出a和b的值
从而可以推导出缩放矩阵的第一步
第二步:获取参数c和d值
然后进行透视矩阵的第二步推导,c和d的值;
根据相似三角形关系,得到如下等式
利用齐次坐标的一个性质,所谓齐次坐标就是使用n+1维来表示n维坐标。这里需要用到齐次坐标的一个性质,如果(x,y,z,1)表示投影到w=1平面的点坐标,那么(nx,ny,nz,n)坐标投影到w=1平面的是同一个点(nx/n,nz/n,nz/n,n/n)=(x,y,z,1),这就是齐次坐标的尺度不变性。
利用齐次坐标特性,将上面的公式得到的结果同时乘以Ze
Mpersp为4x4矩阵,根据上面的公式我们能得出矩阵的第一、二、四行的值
第三步根据收缩矩阵和正射投影矩阵的乘积,得到透视投影矩阵
[
n*2/(r-l) + 0*0 + 0*0 + 0*-(r+l)/(r-l), n*0 + 0*2/(t-b) + 0*0 + 0* -(t+b)/(t-b), n*0 + 0*0 + 0* -2/(f-n) + 0*-(f+n)/(f-n), n*0 + 0*0 + 0*0 +0*1,
0*2/(r-l) + n*0 + 0*0 + 0*-(r+l)/(r-l), 0*0 + n*2/(t-b) + 0*0 + 0* -(t+b)/(t-b), 0*0 + n*0 + 0* -2/(f-n) + 0*-(f+n)/(f-n), 0*0 + n*0 + 0*0 +0*1,
0*2/(r-l) + 0*0 + (f+n)*0+ -1*-(r+l)/(r-l), 0*0+0*2/(t-b)+(f+n)*0+ -1*-(t+b)/(t-b), 0*0+0*0+(f+n)*-2/(f-n)+ -1 *-(f+n)/(f-n),0*0+0*0+(f+n)*0+-1*1,
0*2/(r-l)+0*0+fn*0+0*-(r+l)/(r-l), 0*0+0*2/(t-b)+fn*0+0*-(t+b)/(t-b), 0*0 + 0*0 + fn* -2/(f-n) + 0*-(f+n)/(f-n), 0*0+0*0+fn*0 + 0*1,
]
// 由于r+l=0 以及 t+b = 0
// 可以将矩阵简化为
[
2n/(r-l), 0, 0, 0,
0, 2n/(t-b), 0, 0,
0, 0, -(f+n)/(f-n), -1,
0, 0, (-2nf)/(f-n), 0
]
// js中矩阵是列主序的矩阵,所以等到如下图公式
第一列*第一行 = 第一行的第一列
第1列*第2行 = 第1行的第2列
第1列*第3行 = 第1行的第3列
第1列*第4行 = 第1行的第4列
第2列*第1行 = 第2行的第1列
.......
收缩矩阵和正交投影矩阵相乘结果如下:
第四步通过角度将透视矩阵进行转换
根据视角α 和 宽高比 aspect 求出 top、bottom、left、right四个边界方向
t = n * tan(α /2)
b = -t
r = n * aspect * tan(α/2)
l = -r
计算之后结果
r-l = 2* n * aspect * tan(α/2)
t-b = 2n* tan(α/2)
r+l = 0;
t+b=0
将结果带入到透视投影矩阵中
透视投影实例代码
通过点击上下左右键盘,可以对视图的物体进行转动
三维场景中2个重要概念
隐藏面消除
正确处理物体的前后关系,是三维场景中重要的概念。前面的物体会阻挡后边物体的显示。在webgl中叫做隐藏面消除。
正常显示前后关系
var verticesColors = new Float32Array([
// 顶点坐标和颜色
0.0, 1.0, -4.0 , 0.4, 1.0, 0.4, // 绿色三角形在后边
-0.5, -1.0, -4.0 , 0.4, 1.0, 0.4,
0.5, -1.0, -4.0 , 1.0, 0.4, 0.4,
0.0, 1.0, -2.0 , 1.0, 1.0, 0.4, // 黄色的在中间
-0.5, -1.0, -2.0 , 1.0, 1.0, 0.4,
0.5, -1.0, -2.0 , 1.0, 0.4, 0.4,
0.0, 1.0, -1.5 , 0.4, 0.4, 1.0, // 蓝色的前面
-0.5, -1.0, -1.5 , 0.4, 0.4, 1.0,
0.5, -1.0, -1.5 , 1.0, 0.4, 0.4,
]);
WEBGL中按照定义顶点坐标的顺序来绘制,蓝色的最后定义,所以可以绘制到最外层,同时Z轴坐标也是最靠前,能够正确显示物体的前后关系。
完整示例代码
非正常显示前后关系
接下来调整下顶点坐标的绘制顺序,将蓝色的放到第一个绘制。
蓝色显示在最后,绿色显示在最前面。这个和定义的Z轴坐标是不符合的。按照定义Z轴坐标的关系,绿色的三角形依然在最后。
使用隐藏消除面,解决前后关系
为了解决不符合Z轴前后关系的问题,webgl提供隐藏消除面的功能。
开启隐藏面消除功能,需要以下2步。
- 开启隐藏面消除,
gl.enable(gl.DEPTH_TEST) - 在绘制之前,清除深度缓冲区
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.enable(cap)函数的使用,cap表示开启的功能
- cap: gl.DEPTH_TEST 或 gl.BLEND 或 gl.POLYGON_OFFSET_FILL
gl.clear()方法清除深度缓冲区
深度冲突
隐藏面消除可以解决坐标位置前后关系,但是如果Z轴坐标相同的图形,还是会出现问题,使得图像出现斑斑驳驳。这种现象称为深度冲突。
webgl提供一个多边形偏移的功能,可以解决这个问题。机制是在Z轴上添加一个偏移量,偏移量的值由物体表面相对于观察者视线的角度来确定。
- 启用多边形偏移,
gl.enable(gl.POLYGON_OFFSET_FILL); - 在绘制之前指定偏移量的参数,
gl.polygonOffset(1.0, 1.0);
const vm = getViewMatrix(eyex, eyey, eyez, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
const perspective = getPerspective(35, ctx.width / ctx.height, 100, 1);
// 创建隐藏面消除
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
// 清除视图缓存区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.uniformMatrix4fv(mat, false, mixMatrix(vm, perspective));
gl.enable(gl.POLYGON_OFFSET_FILL);
gl.drawArrays(gl.TRIANGLES, 0, 3 * 1);
gl.polygonOffset(1.0, 1.0);
gl.drawArrays(gl.TRIANGLES, 0, 3 * 1);
绘制立方体图形
顶点法
绘制立方体需要定义8个顶点;
创建基础模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
* {
margin: 0;
padding: 0;
}
canvas{
margin: 50px auto 0;
display: block;
background: yellow;
}
</style>
</head>
<body>
<canvas id="canvas" width="400" height="400">
此浏览器不支持canvas
</canvas>
</body>
</html>
<script>
function initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE) {
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE) // 指定顶点着色器的源码
gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE) // 指定片元着色器的源码
// 编译着色器
gl.compileShader(vertexShader)
gl.compileShader(fragmentShader)
// 创建一个程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
gl.useProgram(program)
return program;
}
// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {
// 视点
const eye = new Float32Array([eyex, eyey, eyez])
// 目标点
const lookAt = new Float32Array([lookAtx, lookAty, lookAtz])
// 上方向
const up = new Float32Array([upx, upy, upz])
// 确定z轴
const z = minus(eye, lookAt);
normalized(z);
normalized(up);
// 确定x轴
const x = cross(z, up);
normalized(x);
// 确定y轴
const y = cross(x, z);
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),-dot(y,eye),-dot(z,eye),1
])
}
// 矩阵复合函数
function mixMatrix(A, B) {
const result = new Float32Array(16);
for (let i = 0; i < 4; i++) {
result[i] = A[i] * B[0] + A[i + 4] * B[1] + A[i + 8] * B[2] + A[i + 12] * B[3]
result[i + 4] = A[i] * B[4] + A[i + 4] * B[5] + A[i + 8] * B[6] + A[i + 12] * B[7]
result[i + 8] = A[i] * B[8] + A[i + 4] * B[9] + A[i + 8] * B[10] + A[i + 12] * B[11]
result[i + 12] = A[i] * B[12] + A[i + 4] * B[13] + A[i + 8] * B[14] + A[i + 12] * B[15]
}
return result;
}
// 归一化函数
function normalized(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i] * arr[i]
}
const middle = Math.sqrt(sum);
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] / middle;
}
}
// 叉积函数 获取法向量
function cross(a,b) {
return new Float32Array([
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
])
}
// 点积函数 获取投影长度
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
// 向量差
function minus(a, b) {
return new Float32Array([
a[0] - b[0],
a[1] - b[1],
a[2] - b[2],
])
}
// 获取透视投影矩阵
function getPerspective(fov, aspect, far, near) {
fov = fov * Math.PI / 180;
return new Float32Array([
1/(aspect*Math.tan(fov / 2)), 0, 0, 0,
0, 1/(Math.tan(fov/2)),0,0,
0,0,-(far+near)/(far-near),-(2*far*near)/(far-near),
0,0,-1,0,
])
}
const ctx = document.getElementById('canvas')
const gl = ctx.getContext('webgl')
// 创建着色器源码
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute vec4 aColor;
varying vec4 vColor;
uniform mat4 mat;
void main() {
gl_Position = mat * aPosition;
vColor = (1.0, 0.0, 0.0, 1.0);
}
`; // 顶点着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`; // 片元着色器
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aColor = gl.getAttribLocation(program, 'aColor');
const mat = gl.getUniformLocation(program, 'mat');
let eyex = 0.0;
let eyey = -0.1;
let eyez = 0.2;
function draw() {
const vm = getViewMatrix(eyex,eyey,eyez,0.0,0.0,0.0,0.0,0.6,0.0);
const perspective = getPerspective(150, ctx.width / ctx.height, 100, 1);
gl.uniformMatrix4fv(mat, false, mixMatrix(perspective, vm));
gl.drawArrays(gl.TRIANGLES, 0, 3 * 6);
requestAnimationFrame(draw);
}
draw()
</script>
创建出需要的8个顶点数据
// 顶点
const v0 = [1,1,1];
const v1 = [-1,1,1];
const v2 = [-1,-1,1];
const v3 = [1,-1,1];
const v4 = [1,-1,-1];
const v5 = [1,1,-1];
const v6 = [-1,1,-1];
const v7 = [-1,-1,-1];
// 通过结构顶点数据,组成的面
const points = new Float32Array([
...v0,...v1,...v2, ...v0,...v2, ...v3, // 前
...v0,...v3,...v4, ...v0,...v4, ...v5, // 右
...v0,...v5,...v6, ...v0,...v6, ...v1, // 上面
...v1,...v6,...v7, ...v1,...v7, ...v2, // 左
...v7,...v4,...v3, ...v7,...v3, ...v2, // 底
...v4,...v7,...v6, ...v4,...v6, ...v5, // 后
])
完整绘制代码 绘制出来一个红色正方形,现在还看不出来立方体的形状,可以通过改变视角,进行查看
通过调整 eyeX、eyeY、eyeZ的值,可以看到透视的正方形
添加旋转动画
给每个面添加自定义颜色
主要是重新定义个颜色的缓冲对象buffer;
将bufferData数据进行绑定,然后进行colorData赋值;
之后通过gl.vertexAttribPointer给aColor设置值;
索引法
创建顶点数据信息
// 创建顶点数据信息vertices
const vertices = new Float32Array([
1, 1, 1,
-1, 1, 1,
-1,-1, 1,
1,-1, 1,
1,-1,-1,
1, 1,-1,
-1, 1,-1,
-1,-1,-1,
])
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition)
通过索引进行设置面
const indeces = new Uint8Array([
// 3点组成一个3角形,2个三角形组成一个面
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,6,7,4,6,5,
])
const indexBuffer = gl.createBuffer();
// 绑定索引的buffer数据,需要使用 ELEMENT_ARRAY_BUFFER 方法
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indeces, gl.STATIC_DRAW);
通过索引绘制面,需要使用drawElements函数;
drawElements方法函数的使用
用了绘制面信息,包含4个参数
- mode:同drawArrays时的参数值
- count: 要绘制的顶点数量
- type: 顶点数据类型,值为gl.UNSIGNED_BYTE 或者 gl.UNSIGNED_SHORT
- offset: 索引数组开始绘制的位置
完整的索引创建立方体代码
给每个面添加不同颜色
创建顶点缓存数据
// 顶点数据,组成面
const vertices = new Float32Array([
// 0123,组成前面
1, 1, 1,
-1, 1, 1,
-1,-1, 1,
1,-1, 1,
// 0345,组成右面
1, 1, 1,
1,-1, 1,
1,-1,-1,
1, 1,-1,
// 0156,组成上面
1, 1, 1,
1, 1, -1,
-1, 1,-1,
-1, 1,1,
// 1267,组成左面
-1, 1, 1,
-1,1, -1,
-1, -1,-1,
-1,-1,1,
// 2347组成下面
-1,-1, 1,
1,-1, 1,
1,-1,-1,
-1,-1,-1,
// 4567组成后面
1,-1,-1,
1, 1,-1,
-1, 1,-1,
-1,-1,-1,
])
// 顶点位置缓存区
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition)
创建颜色缓存数据
// 创建颜色数据
const colors = new Float32Array([
0.4,0.4,1.0, 0.4,0.4,1.0, 0.4,0.4,1.0, 0.4,0.4,1.0,
0.4,1.0,0.6, 0.4,1.0,0.6, 0.4,1.0,0.6, 0.4,1.0,0.6,
1.0,0.4,0.4, 1.0,0.4,0.4, 1.0,0.4,0.4, 1.0,0.4,0.4,
1.0,0.8,0.4, 1.0,0.8,0.4, 1.0,0.8,0.4, 1.0,0.8,0.4,
1.0,0.0,1.0, 1.0,0.0,1.0, 1.0,0.0,1.0, 1.0,0.0,1.0,
0.0,1.0,1.0, 0.0,1.0,1.0, 0.0,1.0,1.0, 0.0,1.0,1.0,
])
// 创建颜色缓存区
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aColor)
内容参考 《webgl编程指南》
更多资源文档和代码下载地址