1. 画一个立方体
画一个立方体不仅仅是画出几个面就完了,还要考虑在什么地方
朝哪里看
视野有多宽
能看多远
。所以在实现立方体之前需要了解这些概念。
2. 视点、视线、观察目标点、上方向
视点
:简单来说就是观察者,或者就是我们的位置。我们用(eyeX, eyeY, eyeZ)
来表示
视线
:就是从视点发出的射线。
对于之前我们画的二维图像,视点就是坐标原点,视线就是z
轴的负半轴。
观察目标点
:被观察目标所在的点,视线就是连接观点和目标点而形成的。用(atX, atY, atZ)
来表示
上方向
:如果观察者绕视线旋转,那么图像也得确定朝上的方向。用(upX, upY, upZ)
来表示
所以,要描述观察者的视角,我们只需要知道视点(在哪看)
、观察目标点(目标是谁,朝哪里看)
、上方向(怎么旋转的)
2.1 视图矩阵
描述视点
目标点
上方向
的矩阵,当我们给图像坐标乘以视图矩阵之后,就能得到观察者在不同状态下的图像。
它该如何使用呢?我们通过下面的例子来看,这里有三个层叠的三角形,越靠近我们颜色越深。
// 顶点着色器
attribute vec4 pos;
// 声明一个矩阵,作为视图矩阵
uniform mat4 viewMat;
// 这个变量是为了渲染不同颜色
varying vec4 vPos;
void main(){
// 将坐标乘以视图矩阵,得到新的位置
gl_Position = viewMat * pos;
vPos = pos;
gl_PointSize = 20.0;
}
// 片元着色器
precision mediump float;
varying vec4 vPos;
void main(){
// 根据z的值不同渲染不同颜色
if(vPos.z == -0.4) {
gl_FragColor = vec4(0.83, 1.0, 0.89, 1.0);
}else if(vPos.z == -0.2) {
gl_FragColor = vec4(0.49, 0.98, 0.66, 1.0);
}else {
gl_FragColor = vec4(0.0, 0.97, 0.34, 1.0);
}
}
import { mat4, vec3 } from 'gl-matrix'
// 省略初始化代码
const pos = gl.getAttribLocation(program, 'pos')
const viewMat = gl.getUniformLocation(program, 'viewMat')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
// 三个三角形的坐标(x, y, z)
const data = new Float32Array([
0.0, 0.5, -0.4,
-0.5, -0.5, -0.4,
0.5, -0.5, -0.4,
0.0, 0.5, -0.2,
-0.5, -0.5, -0.2,
0.5, -0.5, -0.2,
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
])
const eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
// viewMat视图矩阵的值使用gl-matrix生成一个mat4
/**
* mat4.lookAt
*
* 参数1:out,保存结果的变量
* 参数2:eye,观察者坐标
* 参数3:center:目标点坐标
* 参数4:up:上方向(0, 1, 0)表示y轴正方向为上
*/
gl.uniformMatrix4fv(viewMat, false, mat4.lookAt(mat4.create(), vec3.fromValues(0.25, 0.25, 0.25), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 1, 0)))
gl.vertexAttribPointer(pos, 3, gl.FLOAT, false, eleSize * 3, 0)
gl.enableVertexAttribArray(pos)
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
可以看到核心就是给位置矢量乘了一个视图矩阵gl_Position = viewMat * pos;
,这本质上就是前面的图形变换,只是参考不一样。
如上图,左侧表示图形不动将视点远离图形,右侧表示视点不动将图形远离。左边可以通过改变视图矩阵实现,右边可以改变变换矩阵来实现。
两种观察变换方法,可以使物体变化和观察者变换独立。
我们试试其他视角的这三个三角形是什么样的:
正视
:gl.uniformMatrix4fv(viewMat, false, mat4.lookAt(mat4.create(), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 1, 0)))
,视点设置为中心,目标点设置为中心,y轴正方形为上。
可以看到后面几个三角形都被遮挡了,这很符合经验。
从上偏一点往下看
:gl.uniformMatrix4fv(viewMat, false, mat4.lookAt(mat4.create(), vec3.fromValues(0, 0.5, 0.5), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 1, 0)))
3.从左偏一点往右
:gl.uniformMatrix4fv(viewMat, false, mat4.lookAt(mat4.create(), vec3.fromValues(-0.25, 0, 0.2), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 1, 0)))
2.2 模型视图矩阵
如果你想要在某一个视角看变换后的三角形,可以继续乘以一个变换矩阵。这种组合矩阵就叫模型视图矩阵。
比如:<变换后的坐标> = <视图矩阵> * <模型矩阵> * <原始坐标>
<模型视图矩阵> = <视图矩阵> * <模型矩阵>
// 修改一下上面的代码
const viewMat = mat4.lookAt(mat4.create(), vec3.fromValues(0.25, 0.25, 0.25), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 1, 0))
// 创建一个变换矩阵
const modelMat = mat4.fromRotation(mat4.create(), 30, vec3.fromValues(0, 0, 1))
// 将他们相乘得到模型视图矩阵
gl.uniformMatrix4fv(modelViewMat, false, mat4.multiply(mat4.create(), viewMat, modelMat))
确实旋转了:
2.3 来点好玩的
我们加入键盘来操作左右观察视角。
// 顶点着色器
attribute vec4 pos;
uniform mat4 viewMat;
varying vec4 vPos;
void main(){
gl_Position = viewMat * pos;
vPos = pos;
gl_PointSize = 10.0;
}
// 片元着色器
precision mediump float;
varying vec4 vPos;
void main(){
if(vPos.z == -0.4) {
gl_FragColor = vec4(0.83, 1.0, 0.89, 1.0);
}else if(vPos.z == -0.2) {
gl_FragColor = vec4(0.49, 0.98, 0.66, 1.0);
}else {
gl_FragColor = vec4(0.0, 0.97, 0.34, 1.0);
}
}
// 略过初始化代码
const pos = gl.getAttribLocation(program, 'pos')
const viewMat = gl.getUniformLocation(program, 'viewMat')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
const data = new Float32Array([
0.0, 0.5, -0.4,
-0.5, -0.5, -0.4,
0.5, -0.5, -0.4,
0.0, 0.5, -0.2,
-0.5, -0.5, -0.2,
0.5, -0.5, -0.2,
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
])
const eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
gl.vertexAttribPointer(pos, 3, gl.FLOAT, false, eleSize * 3, 0)
gl.enableVertexAttribArray(pos)
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
// 上面的代码仍然是画三个三角形
// 视图矩阵抽成了单独的代码,方便键盘事件调用
let eyeX = 0
// 根据输入的视点坐标改变图像
const draw = (eyeX: number) => {
gl.uniformMatrix4fv(viewMat, false, mat4.lookAt(mat4.create(), vec3.fromValues(eyeX, 0, 0.5), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 1, 0)))
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
}
draw(eyeX)
document.addEventListener('keydown', (e) => {
if (e.code === 'ArrowRight') {
eyeX += 0.01;
}
if (e.code === 'ArrowLeft') {
eyeX -= 0.01;
}
draw(eyeX);
})
可以看到随着键盘的按下,图像在转变视角。不过有个问题,在转动到一定角度的时候,三角形出现了缺角。
这是因为我们没有指定可视范围,超出可视范围的部分就被切除了。
水平视角
垂直视角
可视深度
这些属性组成了可视空间
,只有当我们指定了合适的可视空间,图像才会正确的显示。
3. 可视空间
常用的可视空间有两种:
- 长方体可视空间,盒状空间,由正投影
orthographic projection
产生 - 四棱锥/金字塔可视空间,由透视投影
perspective projection
产生
我们眼中的世界就是透视投影,近大远小,强调的是深度。模拟了相机的成像原理,不同的参考点反射出了不同的大小的图像。
而正投影就是平行投影,不会受到深度的影响,无论在哪里的参考点大小是固定的,适合对比两个物体:
3.1 盒状空间(正投影)
盒状空间由下面几个参数组成,下图中的长方体就是一个盒状空间:
近裁剪面
:前面的矩形远裁剪面
:后面的矩形近裁剪面
的上下左右
边界:left、right、top、bottom
近裁剪面
的位置near
远裁剪面
的位置far
根据上一个代码实现一个例子,来观察这些参数的区别:
实现之前,介绍一下投影矩阵
,就是将原始坐标转化到投影空间中。这里我们实现一个正投影
矩阵。
// 顶点着色器
attribute vec4 pos;
uniform mat4 projectMat;
varying vec4 vPos;
void main(){
gl_Position = projectMat * pos;
vPos = pos;
gl_PointSize = 10.0;
}
// 片元着色器
precision mediump float;
varying vec4 vPos;
void main(){
if(vPos.z == -0.4) {
// 离观察者最远的为红色
gl_FragColor = vec4(0.97, 0.6, 0.6, 1.0);
}else if(vPos.z == -0.2) {
// 稍远的是蓝色
gl_FragColor = vec4(0.49, 0.73, 0.98, 1.0);
}else {
// 最近的是绿色
gl_FragColor = vec4(0.0, 0.97, 0.34, 1.0);
}
}
// 省略初始化函数
// 省略位置顶点赋值
// 修改一下draw函数,接受near和far两个参数
let near = 0
let far = 0.5
// 创建一个元素显示参数
const info = document.createElement('div')
info.setAttribute('id', 'info')
document.body.append(info)
const draw = (near: number, far: number) => {
// 显示参数
info.innerText = `near: ${near.toFixed(2)} far: ${far.toFixed(2)}`
const orthoMat = mat4.ortho(mat4.create(), -1, 1, -1, 1, near, far)
// 进行投影变换
gl.uniformMatrix4fv(projectMat, false, orthoMat)
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
}
draw(near, far)
document.addEventListener('keydown', (e) => {
// 改变near
if (e.code === 'ArrowRight') {
near += 0.01
}
if (e.code === 'ArrowLeft') {
near -= 0.01
}
// 改变far
if (e.code === 'ArrowUp') {
far += 0.01
}
if (e.code === 'ArrowDown') {
far -= 0.01
}
draw(near, far)
})
默认状态下,near
值为0,也就是说近裁剪面
是由x、y
轴组成的平面,所以可以看到z=0
的绿色三角形。
当near
值增大,开始远离观察者,近裁剪面
往z
轴负方向移动,我们便看不到绿色三角形,而是蓝色三角形。
蓝色三角形的z=-0.2
,当near
大于0.2
时,蓝色三角形也会消失,看到红色三角形。
红色三角形的z=-0.4
,当near
继续增大超过0.5
时,什么也看不见了。
相似的far
也一遵循这个逻辑。far
值减小,代表远裁剪面
在向z轴正方形移动。
3.2 补上缺角
前面提到过,直接移动视点会发现某些角度,三角形缺了一些,这是由于没有设置合适的可视空间,现在我们设置合适的可视空间,保证三角形不被裁剪。
在当前设置下,图像被裁剪了
可以发现,原点那个角被裁剪了。
但是我将远裁剪面放远一点,那个角又出现了。
// 在前面的代码基础上只改变一点
// <投影矩阵>*<视图矩阵>*<坐标点>
const draw = (eyeX: number, near: number, far: number) => {
// 显示参数
info.innerText = `near: ${near.toFixed(2)} far: ${far.toFixed(2)}`
const orthoMat = mat4.ortho(mat4.create(), -1, 1, -1, 1, near, far)
const _viewMat = mat4.lookAt(mat4.create(), vec3.fromValues(eyeX, 0, 0.5), vec3.fromValues(0, 0, 0), vec3.fromValues(0, 1, 0))
// <投影矩阵>*<视图矩阵>
gl.uniformMatrix4fv(viewMat, false, mat4.multiply(mat4.create(), orthoMat, _viewMat))
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
}
draw(eyeX, near, far)
document.addEventListener('keydown', (e) => {
// 改变eyeX
if (e.code === 'KeyA') {
eyeX -= 0.01
}
if (e.code === 'KeyD') {
eyeX += 0.01
}
// 改变near
if (e.code === 'ArrowRight') {
near += 0.1
}
if (e.code === 'ArrowLeft') {
near -= 0.1
}
// 改变far
if (e.code === 'ArrowUp') {
far += 0.1
}
if (e.code === 'ArrowDown') {
far -= 0.1
}
draw(eyeX, near, far)
})
3.3 近裁面宽高比
投影矩阵还有几个参数我们没有改变过,就是近裁剪面的尺寸。
如果我们等比例裁切:
// 我们将尺寸按照比例,缩小了一半
mat4.ortho(mat4.create(), -0.5, 0.5, -0.5, 0.5, -1, 1)
由于canvas
并没有缩小,所以图像将会被放大成两倍。
如果我们没有等比例裁切:
// 高度不变,宽度缩小一半
mat4.ortho(mat4.create(), -0.5, 0.5, -1, 1, -1, 1)
同样的,图像将会被压缩变形,宽度被放大了两倍,高度没变,看起来就是这样。
这里还需要注意一点,在压缩变形的过程中,超出的部分被裁切了。
3.4 透视投影
简单的说就是近大远小。越远的图像将会被缩小。
透视投影空间包含下面几个关键参数:
fov
:指垂直视角,可视空间顶部和底部的夹角,必须大于0
aspect
:斤裁剪面的宽高比(宽/高)
near、far
:必须都大于0,表示远/近裁剪面的位置
我们实现一个简单的例子:
// 顶点着色器
attribute vec4 pos;
uniform mat4 viewMat;
varying vec4 vPos;
void main(){
gl_Position = viewMat * pos;
vPos = pos;
gl_PointSize = 10.0;
}
// 片元着色器
precision mediump float;
varying vec4 vPos;
void main(){
if(vPos.z == -4.0) {
// 离观察者最远的为红色
gl_FragColor = vec4(0.97, 0.6, 0.6, 1.0);
}else if(vPos.z == -2.0) {
// 稍远的是蓝色
gl_FragColor = vec4(0.49, 0.73, 0.98, 1.0);
}else if(vPos.z == 0.0){
// 最近的是绿色
gl_FragColor = vec4(0.0, 0.97, 0.34, 1.0);
}
}
const pos = gl.getAttribLocation(program, 'pos')
const viewMat = gl.getUniformLocation(program, 'viewMat')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
const data = new Float32Array([
-0.75, 1.0, -4.0,
-1.25, -1.0, -4.0,
-0.25, -1.0, -4.0,
-0.75, 1.0, -2.0,
-1.25, -1.0, -2.0,
-0.25, -1.0, -2.0,
-0.75, 1.0, 0.0,
-1.25, -1.0, 0.0,
-0.25, -1.0, 0.0,
])
const eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
gl.vertexAttribPointer(pos, 3, gl.FLOAT, false, eleSize * 3, 0)
gl.enableVertexAttribArray(pos)
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
let fov = 65
let aspect = canvas.width / canvas.height
let near = 1
let far = 100
let eyeX = 0
// 创建一个元素显示参数
const info = document.createElement('div')
info.setAttribute('id', 'info')
info.setAttribute('style', 'display: flex; justify-content: center;')
document.body.append(info)
const draw = (eyeX: number, fov: number, aspect: number, near: number, far: number) => {
// 显示参数
info.innerText = `fov: ${fov.toFixed(2)}`
const perspectiveMat = mat4.perspective(mat4.create(), fov, aspect, near, far)
// 这里的目标视点坐标很有意思,z被设置到了很远处,就是far的位置,这是因为透视点在这个位置
// 我们实际看的位置也是这里
const _viewMat = mat4.lookAt(mat4.create(), vec3.fromValues(eyeX, 0, 1), vec3.fromValues(0, 0, -100), vec3.fromValues(0, 1, 0))
const res = mat4.multiply(mat4.create(), perspectiveMat, _viewMat)
gl.uniformMatrix4fv(viewMat, false, res)
gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
}
draw(eyeX, fov, aspect, near, far)
document.addEventListener('keydown', (e) => {
if (e.code === 'ArrowUp') {
fov += 0.1
}
if (e.code === 'ArrowDown') {
fov -= 0.1
}
draw(eyeX, fov, aspect, near, far)
})
随着fov
的增大,也就是垂直视角的增大,物体越来越小。想象自己原理一栋房子,视野越来越开阔,但是房子却越来越小。TODO:为什么fov
大到一定程度图像会倒置。
值得注意的是,这三个三角形是一样大的。
4. 正确处理前后关系
虽然我们给坐标点设置了z
轴的值,画出来的图像看起来也似乎按照z
值出现了层叠关系。但是webgl
只是简单的按照输入顺序先后画出了图形。
const data = 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, 0.0, 0.4, 0.4, 1.0, // 紫色
-0.5, -1.0, 0.0, 0.4, 0.4, 1.0,
0.5, -1.0, 0.0, 1.0, 0.4, 0.4,
])
z=-4.0 -> z=-2.0 -> z=0.0
,画出来是这样
我们换个顺序:
0.0, 1.0, 0.0, 0.4, 0.4, 1.0, // 紫色
-0.5, -1.0, 0.0, 0.4, 0.4, 1.0,
0.5, -1.0, 0.0, 1.0, 0.4, 0.4,
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,
黄色跑到了最前面。
解决这个问题很简单,使用webgl
自带的隐藏面消除
功能,自动隐藏被遮挡的表面。
- 开启功能:
gl.enable(gl.DEPTH_TEST)
- 绘制之前清除深度缓冲区:
gl.clear(gl.DEPTH_BUFFER_BIT)
webgl
通过检测像素的深度,通常来说就是z
轴的值,来判断是否遮挡。这就是为什么不默认开启,因为直接按照输入顺序渲染可以减少开销。
还要注意的是,必须设置一个可视空间,不然不会有效果
下面是完整示例:
// 顶点着色器
uniform mat4 viewMat;
attribute vec4 pos;
attribute vec4 color;
varying vec4 _color;
void main(){
gl_Position = viewMat * pos;
_color = color;
gl_PointSize = 10.0;
}
// 片元着色器
precision mediump float;
varying vec4 _color;
void main(){
gl_FragColor = _color;
}
// 省略初始化
const pos = gl.getAttribLocation(program, 'pos')
const viewMat = gl.getUniformLocation(program, 'viewMat')
const color = gl.getAttribLocation(program, 'color')
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
// 顺序不再按照z值排列
const data = new Float32Array([
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, 0.0, 0.4, 0.4, 1.0,
-0.5, -1.0, 0.0, 0.4, 0.4, 1.0,
0.5, -1.0, 0.0, 1.0, 0.4, 0.4,
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,
])
const eleSize = data.BYTES_PER_ELEMENT
gl.bufferData(
gl.ARRAY_BUFFER,
data,
gl.STATIC_DRAW
)
gl.vertexAttribPointer(pos, 3, gl.FLOAT, false, eleSize * 6, 0)
gl.vertexAttribPointer(color, 3, gl.FLOAT, false, eleSize * 6, eleSize * 3)
gl.enableVertexAttribArray(pos)
gl.enableVertexAttribArray(color)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.clearColor(0.4, 0.4, 0.4, 1)
let fov = 32
let aspect = canvas.width / canvas.height
let near = 1
let far = 100
let eyeX = 0
// 创建一个元素显示参数
const info = document.createElement('div')
info.setAttribute('id', 'info')
info.setAttribute('style', 'display: flex; justify-content: center;')
document.body.append(info)
const draw = (eyeX: number, fov: number, aspect: number, near: number, far: number) => {
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
gl.clearColor(0.4, 0.4, 0.4, 1)
// 显示参数
info.innerText = `fov: ${fov.toFixed(2)}`
let transformMat = mat4.create();
const perspectiveMat = mat4.perspective(mat4.create(), fov, aspect, near, far)
const _viewMat = mat4.lookAt(mat4.create(), vec3.fromValues(eyeX, 0, 5), vec3.fromValues(0, 0, -100), vec3.fromValues(0, 1, 0))
// 左移一点
transformMat = mat4.fromTranslation(mat4.create(), vec3.fromValues(-0.75, 0, 0))
let res = mat4.multiply(mat4.create(), perspectiveMat, mat4.multiply(mat4.create(), _viewMat, transformMat))
gl.uniformMatrix4fv(viewMat, false, res)
gl.drawArrays(gl.TRIANGLES, 0, 9)
// 平移一点再画一组
transformMat = mat4.fromTranslation(mat4.create(), vec3.fromValues(0.75, 0, 0))
res = mat4.multiply(mat4.create(), perspectiveMat, mat4.multiply(mat4.create(), _viewMat, transformMat))
gl.uniformMatrix4fv(viewMat, false, res)
gl.drawArrays(gl.TRIANGLES, 0, 9)
}
gl.enable(gl.DEPTH_TEST)
draw(eyeX, fov, aspect, near, far)
document.addEventListener('keydown', (e) => {
if (e.code === 'ArrowUp') {
fov += 0.3
}
if (e.code === 'ArrowDown') {
fov -= 0.3
}
draw(eyeX, fov, aspect, near, far)
})
即使输入不一样,也能正确显示出来。
4.1 深度冲突
两图形如果z
值很接近,由于浮点数精度的问题,会导致每个像素点实际的深度会有误差,导致图像相互交错。
好在
webgl
提供了方法来解决,多边形偏移:设置顶点的z
值偏移量
指定加到每个顶点绘制后z值上的偏移量,偏移量按照公式
m*factor+r*units
计算,其中m
表 示顶点所在表面相对于观察者的视线的角度,而r
表示硬件能够区分两个Z值之差的最小值。
// 启用
gl.enable(gl.POLYGON_OFFSET_FILL);
// 绘制图形之前设置偏移
gl.polygonOffset(1.0, 1.0);
gl.drawArrays(gl.TRIANGLES, n / 2, n / 2);
5. 五彩立方体
这样一个立方体应该怎么绘制呢,我们可以使用之前的三角形,用两个三角形拼成一个表面,然后再组合成一个立方体,但是这样做太复杂。一个三角形3个顶点,一个立方体面两个三角形6个顶点,6个面总共使用36个顶点,但是一个立方体只有8个顶点。
所以我们需要新的绘制方式:
- 将立方体分为
前后左右上下
6个面 - 每个面有两个三角形,每个三角形使用了三个顶点。比如:
front
面使用了两个三角形:△012
和△023
。 - 每个顶点对应一个坐标。
可以看到主要就是两个表,三角形索引表
和顶点列表
,怎么才能让他们联系起来呢,这里要用到gl.drawElements
:
下面看看代码:
// 顶点着色器
uniform mat4 viewMat;
attribute vec4 pos;
attribute vec4 color;
varying vec4 _color;
void main(){
gl_Position = viewMat * pos;
_color = color;
gl_PointSize = 10.0;
}
// 片元着色器
precision mediump float;
varying vec4 _color;
void main(){
gl_FragColor = _color;
}
// 省略初始化代码
const pos = gl.getAttribLocation(program, 'pos')
const viewMat = gl.getUniformLocation(program, 'viewMat')
const color = gl.getAttribLocation(program, 'color')
var vertexBuffer = gl.createBuffer();
var indexBuffer = gl.createBuffer();
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
// 顶点、颜色列表
const vertex = new Float32Array([
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // v0
-1.0, 1.0, 1.0, 1.0, 0.0, 1.0, // v1
-1.0, -1.0, 1.0, 1.0, 0.0, 0.0, // v2
1.0, -1.0, 1.0, 1.0, 1.0, 0.0, // v3
1.0, -1.0, -1.0, 0.0, 1.0, 0.0, // v4
1.0, 1.0, -1.0, 0.0, 1.0, 1.0, // v5
-1.0, 1.0, -1.0, 0.0, 0.0, 1.0, // v6
-1.0, -1.0, -1.0, 0.0, 0.0, 0.0 // v7
])
// 三角形顶点索引
// 这里只有8个顶点,所以8位就够了
// 如果超过2^8个顶点,就应该用Uint16Array
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 // 后
]);
const eleSize = vertex.BYTES_PER_ELEMENT
// 顶点数据绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(
gl.ARRAY_BUFFER,
vertex,
gl.STATIC_DRAW
)
// 索引数据绑定缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
gl.vertexAttribPointer(pos, 3, gl.FLOAT, false, eleSize * 6, 0)
gl.vertexAttribPointer(color, 3, gl.FLOAT, false, eleSize * 6, eleSize * 3)
gl.enableVertexAttribArray(pos)
gl.enableVertexAttribArray(color)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.clearColor(0.4, 0.4, 0.4, 1)
let fov = 0.5
let aspect = canvas.width / canvas.height
let near = 1
let far = 100
let eyeX = 3.06
const info = document.createElement('div')
info.setAttribute('id', 'info')
info.setAttribute('style', 'display: flex; justify-content: center;')
document.body.append(info)
const draw = (eyeX: number, fov: number, aspect: number, near: number, far: number) => {
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
gl.clearColor(0.4, 0.4, 0.4, 1)
info.innerText = `fov: ${fov.toFixed(2)}`
const perspectiveMat = mat4.perspective(mat4.create(), fov, aspect, near, far)
const _viewMat = mat4.lookAt(mat4.create(), vec3.fromValues(5, 5, 10.0), vec3.fromValues(0, 0, -2), vec3.fromValues(0, 1, 0))
let res = mat4.multiply(mat4.create(), perspectiveMat, _viewMat)
gl.uniformMatrix4fv(viewMat, false, res)
// 使用drawElements绘制索引数据
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
}
gl.enable(gl.DEPTH_TEST)
draw(eyeX, fov, aspect, near, far)
document.addEventListener('keydown', (e) => {
if (e.code === 'ArrowUp') {
fov += 0.3
}
if (e.code === 'ArrowDown') {
fov -= 0.3
}
draw(eyeX, fov, aspect, near, far)
})
一般三维建模工具可以导出顶点索引和顶点信息。
虽然我们只调用了一次drawElements
,但是webgl
内部却调用了indices.length
次,因为每一次都要从顶点列表中去除顶点数据。
6.单色立方体
五彩立方体虽然好看,但是按照上面的写法,我们无法指定某个面的单一颜色,因为颜色被定义在了顶点中,这会导致光栅化的时候发生内插
,下面就来研究一下怎么画出单一颜色的立方体。
思路:给一个面的所有顶点设置一个颜色,但是有公用顶点,比如前面和右面的连接处有两个公用顶点,如果设置了一个就会影响两个面。
所以,我们把每个面的顶点都单独设置,不再共享顶点,这样就不会出现这种情况了。
如上图所示,front
和right
的顶点不再有交叉的情况,都单独设置了一组。
// 顶点着色器
uniform mat4 viewMat;
attribute vec4 pos;
attribute vec4 color;
varying vec4 _color;
void main(){
gl_Position = viewMat * pos;
_color = color;
gl_PointSize = 10.0;
}
// 片元着色器
precision mediump float;
varying vec4 _color;
void main(){
gl_FragColor = _color;
}
const pos = gl.getAttribLocation(program, 'pos')
const viewMat = gl.getUniformLocation(program, 'viewMat')
const color = gl.getAttribLocation(program, 'color')
var vertexBuffer = gl.createBuffer();
var colorBuffer = gl.createBuffer();
var indexBuffer = gl.createBuffer();
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
// 每个面都单独定义了一组顶点
const vertex = new Float32Array([
1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // v0-v1-v2-v3 前
1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, // v0-v3-v4-v5 右
1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 上
-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, // v1-v6-v7-v2 左
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, // v7-v4-v3-v2 下
1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0 // v4-v7-v6-v5 后
])
// 分离颜色参数
var 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, // v0-v1-v2-v3 前(blue)
0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, // v0-v3-v4-v5 右(green)
1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, // v0-v5-v6-v1 上(red)
1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, // v1-v6-v7-v2 左
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, // v7-v4-v3-v2 下
0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0 // v4-v7-v6-v5 后
]);
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // 前
4, 5, 6, 4, 6, 7, // 右
8, 9, 10, 8, 10, 11, // 上
12, 13, 14, 12, 14, 15, // 左
16, 17, 18, 16, 18, 19, // 下
20, 21, 22, 20, 22, 23 // 后
]);
// 绑定顶点缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, vertex, gl.STATIC_DRAW)
gl.vertexAttribPointer(pos, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(pos)
// 绑定颜色缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW)
gl.vertexAttribPointer(color, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(color)
// 绑定索引
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
let fov = 0.5
let aspect = canvas.width / canvas.height
let near = 1
let far = 100
gl.enable(gl.DEPTH_TEST)
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
gl.clearColor(0.4, 0.4, 0.4, 1)
const perspectiveMat = mat4.perspective(mat4.create(), fov, aspect, near, far)
const _viewMat = mat4.lookAt(mat4.create(), vec3.fromValues(5, 5, 10.0), vec3.fromValues(0, 0, -2), vec3.fromValues(0, 1, 0))
let res = mat4.multiply(mat4.create(), perspectiveMat, _viewMat)
gl.uniformMatrix4fv(viewMat, false, res)
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);