持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
本文标题:WebGL第四十三课:渲染正方体
友情提示
这篇文章是WebGL课程专栏的第43篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
前情回顾
在前面的文章中,我们已经提及了对于3D渲染最重要的三个矩阵:
- Model 矩阵, 该矩阵用来控制渲染对象在世界坐标里的拉伸,渲染,以及位移。
- View 矩阵, 该矩阵用来控制眼睛或者说相机的位置以及相机的朝向。
- Projection 矩阵, 该矩阵用来将视野内的向量映射到NDC空间。
在这三个矩阵的总体作用,给定一个顶点位置之后,就能得到NDC空间中的坐标。
我们将此坐标作为vertex_shader中gl_Position变量的输出值,就能真正绘制出来了。
vertex_shader 的写法
根据上面的描述,vertex_shader需要接收四个变量
- 顶点坐标
- Model矩阵
- View矩阵
- Projection矩阵
顶点坐标是在vertex_shader中,需要以attribute的形式传入:
attribute vec3 a_PointVertex; // 顶点坐标
这个变量名是随意指定的,我这里用前缀a_是为了说明这是一个attribute变量。
其余三个矩阵均是uniform变量的形式传入,如下:
uniform mat4 u_Model; // Model 矩阵
uniform mat4 u_View; // View 矩阵
uniform mat4 u_Projection; // Projection 矩阵
同理,变量名随意指定,这里用前缀u_, 是为了说明这三个变量是uniform变量。
这里注意,矩阵是 4X4的矩阵,而非3X3矩阵,这是因为3X3矩阵无法处理三维向量的位移,不再赘述,感兴趣者可阅读另一篇文章WebGL第四十一课:3D前置知识点之齐次空间 - 掘金 (juejin.cn)。
整体vertex_shader放在index.html中,写法如下:
<script id="vertex_shader" type="myshader">
// Vertex Shader
precision mediump int;
precision mediump float;
attribute vec3 a_PointVertex; // 顶点坐标
uniform mat4 u_Model; // Model 矩阵
uniform mat4 u_View; // View 矩阵
uniform mat4 u_Projection; // Projection 矩阵
varying vec3 va_pos;
void main() {
va_pos = a_PointVertex;
mat4 MVP = u_Projection * u_View * u_Model;
gl_Position = MVP * vec4(a_PointVertex.xyz, 1.0);
}
</script>
这里面有一个 varying 变量, 我们就是把顶点坐标直接传递给 fragment_shader。
这个是为了什么呢,请看fragment_shader:
fragment_shader 的写法
我们先忽略上面的 varying vec3 va_pos,直接将颜色置成全白:
<script id="fragment_shader" type="myshader">
// Fragment shader
precision mediump int;
precision mediump float;
varying vec3 va_pos;
void main() {
float color = 1.0;
gl_FragColor = vec4(color, color, color, 1.0);
}
</script>
构造立方体数据
先来看一下立方体:
我们看一下,这个立方体,是八个顶点。
但是我们构造数据的时候,不能只有八个顶点,为什么呢?
是因为我们渲染的基本单位是三角形,而一个三角形有三个顶点。
一个立方体有六个面,每个面可以由两个三角形组成,所以总的顶点个数:
。
假设这个立方体的边长是2,那么我们来写一个面的顶点数据:
-1, 1, 1,/**/-1, -1, 1,/**/1, 1, 1,/**/-1, -1, 1,/**/1, -1, 1/**/, 1, 1, 1
可以在纸上画一画, 这是哪一个面的顶点数据呢?
其他面的顶点数据与此同理可以写出,有一点要注意,写的时候,一定要采用逆时针的顺序。
这里不赘述,完整代码放在结尾的链接里。
将顶点数据传递到gl
这个过程分三步:
-
- 构造立方体数据,这个上面已经完成
-
- 生成 WebGL buffer
-
- 将立方体数据传递到上面的buffer中
代码如下:
let dataArr = new Float32Array(cube_model);
//
let glbuffer = gl.createBuffer(); // 创建 buffer
gl.bindBuffer(gl.ARRAY_BUFFER, glbuffer); // 绑定 buffer
gl.bufferData(gl.ARRAY_BUFFER, dataArr, gl.STATIC_DRAW); // 上传顶点数据到 gl
上面的cube_model就是立方体顶点数据, 这里需要转换成底层的float32格式。
描述顶点数据格式
这一步就是说,buffer 中的数据,如何在vertex_shader中获取。
我们想要vertex_shader在buffer中取数据的时候,以上面的格式去取,代码如下:
let a_PointVertex_loc = gl.getAttribLocation(program, 'a_PointVertex');
gl.vertexAttribPointer(a_PointVertex_loc, 3, gl.FLOAT, false, 0, 0); // 坐标
gl.enableVertexAttribArray(a_PointVertex_loc);
MVP矩阵的设置
Model 矩阵
我们先简化操作,假设,立方体没有进行拉伸,旋转,位移。
那么 Model 矩阵就应该是一个单位阵:
这个矩阵对于输入的向量不会产生任何影响。
View矩阵
再来考虑View矩阵,View矩阵描述的是眼睛的位置,以及眼睛的朝向,我们用一个函数来生成这个矩阵:
function mat4_get_lookat(eye, target, up);
其中,eye 是眼睛位置,target是看向哪个点,up是眼睛的正上方。
函数的具体实现在文末代码给出。
这里我们假设眼睛在世界坐标系的点,看向点。也就是立方体所在的位置。
let view_mat = mat4_get_lookat([-10, 10, 10], [0, 0, 0], [0, 1, 0]);
Projection 矩阵
投射一般有两种,正交和透视,本篇用透视方式,也就是Perspective矩阵, 这个矩阵在WebGL第四十二课:3D前置知识点之Perspective矩阵 - 掘金 (juejin.cn)一文中已经讲过,这里直接用一个函数给出:
function mat4_get_perspective(near, far, whr, fov);
具体实现在文末代码。
这里给出一个固定的参数:
let projection_mat = mat4_get_perspective(0.1, 100, 1, 45);
上传uniform变量
本篇涉及到的uniform变量有三个矩阵,我们放在一个函数里统一上传。
// 用三个全局变量存储gl中三个uniform的引用
var u_Model_loc = null;
var u_View_loc = null;
var u_Projection_loc = null;
...
...
...
function uniforms_update(gl) {
// model
gl.uniformMatrix4fv(u_Model_loc, false, new Float32Array(model_mat));
// view
view_mat = mat4_get_lookat([-10, 10, 10], [0, 0, 0], [0, 1, 0]);
gl.uniformMatrix4fv(u_View_loc, false, new Float32Array(view_mat));
// perspective
projection_mat = mat4_get_perspective(0.1, 100, 1, 45);
gl.uniformMatrix4fv(u_Projection_loc, false, new Float32Array(projection_mat));
}
画出来吧,立方体!
就一句关键代码, 画出 12 个三角形:
gl.drawArrays(gl.TRIANGLES, 0, 12 * 3);
注意,最后一个参数,是顶点的个数,所以是 12 * 3。
结果如下:
看起来是那么回事,全白色的一个立方体,但是这很像一个六边形,而不像一个立方体。
这为啥?
因为从我们眼睛的位置和角度看向这个立方体,立方体就是呈现的这么个形状。
将颜色搞的丰富一点
记得我们fragment_shader中的varying vec3 va_pos;变量吗,这个变量是在vertex_shader中传递过来的,代表就是立方体本身的顶点的坐标。
注意,这个坐标没有经过 MVP 矩阵,也就是说,是原始坐标。
在fragment_shader中,这个坐标会被插值。
我们可以利用这个坐标,让立方体的颜色丰富一点。
比如说:
float color = va_pos.x;
gl_FragColor = vec4(color,color,color, 1.0);
我们直接将立方体的原始顶点的坐标的x分量,当做颜色直接输出,会是什么样:
这很容易理解,因为原始立方体的坐标越靠右边,那么就越亮。而左边就很黑。
再试试:
float color = va_pos.y;
gl_FragColor = vec4(color,color,color, 1.0);
我们用原始坐标的y分量,当做颜色,这很容易猜到了:
只有上面一部分是白的,下面就是黑的。
再试试:
float color = len(va_pos);
gl_FragColor = vec4(color,color,color, 1.0);
我们将坐标的长度,作为颜色输出,那么理论上讲,离原点越远,那么就越亮,所以八个顶点是最亮的:
还有什么别的有趣的着色办法吗,当然有。
比如说:
float color = 1.0 - len(va_pos);
gl_FragColor = vec4(color,color,color, 1.0);
顶点离原点的距离越远,那么就越黑,所以效果如下:
这里可以测试你脑海里的任何数学函数,来玩一玩这种效果。
那么黑白的颜色,没意思,我们下面来试试彩色。
gl_FragColor = vec4(va_pos, 1.0);
直接将va_pos,当做RGB,输出:
分析一下,最右边上边的那个点,原始坐标是, 所以这里的颜色是全白,最亮。
再来看看下面的那个顶点,为什么是蓝色,因为原始坐标是(-1, -1, 1)。
所以直接当做颜色的话,RG分量都是-1, 而B分量是1,所以是蓝色的。
有一个小问题,如果颜色的某个分量<0, WebGL会自动将其置成0。
我们将所有的坐标都变到[0, 1]之间, 然后当做颜色输出:
vec3 color = (va_pos + 1.0) * 0.5;
gl_FragColor = vec4(color, 1.0);
效果如下:
这张图与上图的区别就是,更多的地方有了亮度,因为我们手动将颜色为负值的地方进行了转变。
清晰的将三角形表示出来
这里很简单,我们不画三角形,只需要画线就行了:
gl.drawArrays(gl.LINES, 0, 12 * 3);
效果如图:
小习题
- 变化一下眼睛的位置试验一下效果
- 定时变化眼睛的位置,变成动画