一 三维“F”的绘制
1.着色器
按照上一篇提到的矩阵变换,我们可以直接在顶点着色器中加入相应的矩阵变换,这样就可以简化着色器代码,通过变量传入矩阵的值也便于之后矩阵变换的修改。
三维图形的绘制相比于二维图形只在参数类型上有一些变化(注意vec4以及mat4):
<script id="vertex-shader-3D" type="x-shader/x-vertex">
attribute vec4 a_position;
uniform mat4 u_matrix;
void main(){
gl_Position = u_matrix * a_position;
}
</script>
<script id="fragment-shader-3D" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main(){
gl_FragColor = u_color;
}
</script>
2.绘制信息(主要是位置坐标点)
![](https://img-blog.csdnimg.cn/05b7d0053e8649c4a49240ffdd55df36.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAU2lmYXRhc2luYW50,size_8,color_FFFFFF,t_70,g_se,x_16)
如上图,我们可以看出要绘制的三维图形”F"所需要的矩形面总共有16个,而每个矩形面需要2个三角形。
function setGeometry(gl) {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
// left column front
0, 0, 0,
30, 0, 0,
0, 150, 0,
0, 150, 0,
30, 0, 0,
30, 150, 0,
// top rung front
30, 0, 0,
100, 0, 0,
30, 30, 0,
30, 30, 0,
100, 0, 0,
100, 30, 0,
// middle rung front
30, 60, 0,
67, 60, 0,
30, 90, 0,
30, 90, 0,
67, 60, 0,
67, 90, 0,
// left column back
0, 0, 30,
30, 0, 30,
0, 150, 30,
0, 150, 30,
30, 0, 30,
30, 150, 30,
// top rung back
30, 0, 30,
100, 0, 30,
30, 30, 30,
30, 30, 30,
100, 0, 30,
100, 30, 30,
// middle rung back
30, 60, 30,
67, 60, 30,
30, 90, 30,
30, 90, 30,
67, 60, 30,
67, 90, 30,
// top
0, 0, 0,
100, 0, 0,
100, 0, 30,
0, 0, 0,
100, 0, 30,
0, 0, 30,
// top rung right
100, 0, 0,
100, 30, 0,
100, 30, 30,
100, 0, 0,
100, 30, 30,
100, 0, 30,
// under top rung
30, 30, 0,
30, 30, 30,
100, 30, 30,
30, 30, 0,
100, 30, 30,
100, 30, 0,
// between top rung and middle
30, 30, 0,
30, 30, 30,
30, 60, 30,
30, 30, 0,
30, 60, 30,
30, 60, 0,
// top of middle rung
30, 60, 0,
30, 60, 30,
67, 60, 30,
30, 60, 0,
67, 60, 30,
67, 60, 0,
// right of middle rung
67, 60, 0,
67, 60, 30,
67, 90, 30,
67, 60, 0,
67, 90, 30,
67, 90, 0,
// bottom of middle rung.
30, 90, 0,
30, 90, 30,
67, 90, 30,
30, 90, 0,
67, 90, 30,
67, 90, 0,
// right of bottom
30, 90, 0,
30, 90, 30,
30, 150, 30,
30, 90, 0,
30, 150, 30,
30, 150, 0,
// bottom
0, 150, 0,
0, 150, 30,
30, 150, 30,
0, 150, 0,
30, 150, 30,
30, 150, 0,
// left side
0, 0, 0,
0, 0, 30,
0, 150, 30,
0, 0, 0,
0, 150, 30,
0, 150, 0
]), gl.STATIC_DRAW)
}
3.绘制并渲染
webgl.useProgram(program);
webgl.enableVertexAttribArray(positionAttributeLocation);
webgl.bindBuffer(webgl.ARRAY_BUFFER, positionBuffer);
webgl.vertexAttribPointer(positionAttributeLocation, 3, webgl.FLOAT, false, 0, 0);
var matrix = m4.projection(webgl.canvas.clientWidth, webgl.canvas.clientHeight, 400);
matrix = m4.translate(matrix, translations[0], translations[1], translations[2]);
matrix = m4.xRotate(matrix, rotations[0]);
matrix = m4.yRotate(matrix, rotations[1]);
matrix = m4.zRotate(matrix, rotations[2]);
matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);
webgl.uniformMatrix4fv(matrixUniformLocation, false, matrix);
webgl.uniform4fv(colorUniformLocation, color);
webgl.drawArrays(webgl.TRIANGLES, 0, 16 * 6);
注意:vertexAttribPointer的第二个参数为3是因为咱们的顶点坐标变成了3维,也就是在bufferData中设定的各面的顶点坐标(x,y,z),而w可以使用默认值1从而能够不传(因为顶点着色器中坐标的变量类型为vec4)。而对于drawArrays中的第三个参数是指总共要绘制多少个顶点,由于我们需要绘制16个矩形,每个矩形包含两个三角形,而每个三角形又包含3个顶点,因此总共需要绘制16*6个顶点。
结果如下:
![](https://img-blog.csdnimg.cn/ea1e29dca19d4e06b14bca5c1ad51e4a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAU2lmYXRhc2luYW50,size_20,color_FFFFFF,t_70,g_se,x_16)
二 三维下的矩阵变换
1.屏幕像素空间与裁剪空间之间的关系 - 投影矩阵
在像素空间中,宽取值范围在0到width,高在0到height,深度在-depth/2 到 +depth/2。让我们对裁剪空间中的宽高深度根据投影矩阵进行拆解:
x' = 2/width * x - w,x' ∈ [-1,1]
y' = -2/height * y + w,y' ∈ [-1,1]
z' = 2/depth * w,z' ∈ [-1,1]
在二维当中,z的默认值为1;而在三维当中,w的默认值为1.
因此,反推回去,像素空间中的宽高以及深度也就一目了然了。在之后矩阵变换的传值当中会传一个与width相似的值给depth。
projection: function (width, height, depth) {
return [
2 / width, 0, 0, 0,
0, -2 / height, 0, 0,
0, 0, 2 / depth, 0,
-1, 1, 0, 1
]
},
translation: function (tx, ty, tz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1,
];
},
xRotation: function (angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1,
];
},
yRotation: function (angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1,
];
},
zRotation: function (angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
},
scaling: function (sx, sy, sz) {
return [
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1,
];
}
2. 旋转矩阵
二维图形绕着z轴做旋转就好了,而三维图形可以绕着三个轴分别进行旋转,其内里就是将其中一个轴作为旋转轴然后根据其他两个轴来计算旋转后的坐标,原理其实差不多,这里也就不赘述了。
绕z轴旋转:
x' = x * cos + y * sin;
y' = x*(-sin) + y * cos;
绕x轴旋转:
y' = y * cos + z * sin;
z' = y*(-sin) + z * cos;
绕y轴旋转:
x' = x * cos + z * sin;
z' = x * (-sin) + z * cos;
三 可变量的颜色属性
1.着色器代码(利用varying变量将颜色值从顶点着色器到片段着色器):
<script id="vertex-shader-3D" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;
uniform mat4 u_matrix;
varying vec4 v_color;
void main(){
gl_Position = u_matrix * a_position;
v_color = a_color;
}
</script>
<script id="fragment-shader-3D" type="x-shader/x-fragment">
precision mediump float;
varying vec4 v_color;
void main(){
gl_FragColor = v_color;
}
</script>
2.设置渲染颜色(每个面设置不同的颜色以便区分,注意颜色使用的类型与坐标使用的类型不同):
function setColors(gl) {
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array([
// left column front
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// top rung front
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// middle rung front
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// left column back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// top rung back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// middle rung back
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
80, 70, 200,
// top
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
70, 200, 210,
// top rung right
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
200, 200, 70,
// under top rung
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
210, 100, 70,
// between top rung and middle
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
210, 160, 70,
// top of middle rung
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
70, 180, 210,
// right of middle rung
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
100, 70, 210,
// bottom of middle rung.
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
76, 210, 100,
// right of bottom
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
140, 210, 80,
// bottom
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
90, 130, 110,
// left side
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220,
160, 160, 220
]), gl.STATIC_DRAW);
}
3.启用属性并传值
webgl.enableVertexAttribArray(colorAttributeLocation);
webgl.bindBuffer(webgl.ARRAY_BUFFER, colorBuffer);
webgl.vertexAttribPointer(colorAttributeLocation, 3, webgl.UNSIGNED_BYTE, true, 0, 0);
注意:vertexAttribPointer的第三个参数webgl.UNSIGNED_BYTE和第四个参数true(规范化:即将取值范围设成0-1)
结果如下:
![](https://img-blog.csdnimg.cn/46fd8744bf9747f0825c3e0b9f6613ac.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAU2lmYXRhc2luYW50,size_20,color_FFFFFF,t_70,g_se,x_16)
四 正反面的绘制
1.WebGL中的正反面
WebGL中的三角形有正反面的概念,正面三角形的顶点顺序是逆时针方向,反面三角形的顶点顺序是顺时针方向。
![](https://img-blog.csdnimg.cn/2ed0af7d54964374815cfdc616946301.png)
由上图可以看出,红色是正面,蓝色是背面。红色先绘制而蓝色后绘制的原因在于设置数据的顺序。
2.开启只绘制正面或反面
开启只绘制正面或者反面三角形(这行代码加载refreshDraw的函数里面):
webgl.enable(webgl.CULL_FACE);
结果如下:
![](https://img-blog.csdnimg.cn/6903cdf173ba483ea6099f9294641d8a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAU2lmYXRhc2luYW50,size_20,color_FFFFFF,t_70,g_se,x_16)
三角形的正反面是根据着色器的最终计算结果来判定的,如果将X轴缩放-1,那么一个顺时针的三角形就会变成逆时针;如果将顺时针的三角形旋转180°,那么它就会变成逆时针的三角形(相当于按旋转轴进行翻转,原来面向你的面现在背向了你,可以拿张纸实操一下更容易理解)。
结果如下:
![](https://img-blog.csdnimg.cn/ed1fa1d2cf864854a15e1ce8c52296f7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAU2lmYXRhc2luYW50,size_20,color_FFFFFF,t_70,g_se,x_16)
3.修正坐标顺序(即修正朝向)
function setGeometry(gl) {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
// left column front
0, 0, 0,
0, 150, 0,
30, 0, 0,
0, 150, 0,
30, 150, 0,
30, 0, 0,
// top rung front
30, 0, 0,
30, 30, 0,
100, 0, 0,
30, 30, 0,
100, 30, 0,
100, 0, 0,
// middle rung front
30, 60, 0,
30, 90, 0,
67, 60, 0,
30, 90, 0,
67, 90, 0,
67, 60, 0,
// left column back
0, 0, 30,
30, 0, 30,
0, 150, 30,
0, 150, 30,
30, 0, 30,
30, 150, 30,
// top rung back
30, 0, 30,
100, 0, 30,
30, 30, 30,
30, 30, 30,
100, 0, 30,
100, 30, 30,
// middle rung back
30, 60, 30,
67, 60, 30,
30, 90, 30,
30, 90, 30,
67, 60, 30,
67, 90, 30,
// top
0, 0, 0,
100, 0, 0,
100, 0, 30,
0, 0, 0,
100, 0, 30,
0, 0, 30,
// top rung right
100, 0, 0,
100, 30, 0,
100, 30, 30,
100, 0, 0,
100, 30, 30,
100, 0, 30,
// under top rung
30, 30, 0,
30, 30, 30,
100, 30, 30,
30, 30, 0,
100, 30, 30,
100, 30, 0,
// between top rung and middle
30, 30, 0,
30, 60, 30,
30, 30, 30,
30, 30, 0,
30, 60, 0,
30, 60, 30,
// top of middle rung
30, 60, 0,
67, 60, 30,
30, 60, 30,
30, 60, 0,
67, 60, 0,
67, 60, 30,
// right of middle rung
67, 60, 0,
67, 90, 30,
67, 60, 30,
67, 60, 0,
67, 90, 0,
67, 90, 30,
// bottom of middle rung.
30, 90, 0,
30, 90, 30,
67, 90, 30,
30, 90, 0,
67, 90, 30,
67, 90, 0,
// right of bottom
30, 90, 0,
30, 150, 30,
30, 90, 30,
30, 90, 0,
30, 150, 0,
30, 150, 30,
// bottom
0, 150, 0,
0, 150, 30,
30, 150, 30,
0, 150, 0,
30, 150, 30,
30, 150, 0,
// left side
0, 0, 0,
0, 0, 30,
0, 150, 30,
0, 0, 0,
0, 150, 30,
0, 150, 0
]), gl.STATIC_DRAW)
}
结果如下:
![](https://img-blog.csdnimg.cn/57d9b232f9e646dc8fd202c55f774268.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAU2lmYXRhc2luYW50,size_20,color_FFFFFF,t_70,g_se,x_16)
4.开启深度缓冲(Z-Buffer)
由上图可以看出,呈现出来的“F”还是有些奇怪的,按常理来说有的矩形面应该会被前面的面遮挡住才对呀。这是因为我们还没有开启深度缓存,简单理解深度缓存就是为了遮挡后面元素(即深度值不大的元素)。
webgl.clear(webgl.COLOR_BUFFER_BIT | webgl.DEPTH_BUFFER_BIT);
webgl.enable(webgl.DEPTH_TEST);
注意:在开启深度缓冲之前要先清空深度缓冲噢~这个应该是相当于将Z值从裁剪空间([-1,1])转换到深度空间([0,1])。
结果如下(是不是正常多啦~):
![](https://img-blog.csdnimg.cn/100186267215495fb96f639e32ac736c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAU2lmYXRhc2luYW50,size_20,color_FFFFFF,t_70,g_se,x_16)
五 正射投影
上面的涉及到的投影矩阵就是我们常说的正射投影啦,只不过真正的正射投影的函数会更完善一些,涉及到各个方向(上下左右前后),通常用ortho或者相关的函数来进行调用。
比如,ortho(left,right,bottom,top,near,far){}
具体的可以参考你所使用的外部库里面对于正射投影函数的定义。
调用时每个参数可以这样设置(作为参考):
ortho(0,webgl.canvas.clientWidth,webgl.canvas.clientHeight,0,400,-400)
发现了吗,就是我们上面提到过的像素空间的取值范围。