WebGL第四十三课:渲染正方体

1,008 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

本文标题:WebGL第四十三课:渲染正方体

友情提示

这篇文章是WebGL课程专栏的第43篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。

前情回顾

在前面的文章中,我们已经提及了对于3D渲染最重要的三个矩阵:

  • Model 矩阵, 该矩阵用来控制渲染对象在世界坐标里的拉伸,渲染,以及位移。
  • View 矩阵, 该矩阵用来控制眼睛或者说相机的位置以及相机的朝向。
  • Projection 矩阵, 该矩阵用来将视野内的向量映射到NDC空间。

在这三个矩阵的总体作用,给定一个顶点位置之后,就能得到NDC空间中的坐标。

我们将此坐标作为vertex_shadergl_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>

构造立方体数据

先来看一下立方体:

cube.webp

我们看一下,这个立方体,是八个顶点。

但是我们构造数据的时候,不能只有八个顶点,为什么呢?

是因为我们渲染的基本单位是三角形,而一个三角形有三个顶点。

一个立方体有六个面,每个面可以由两个三角形组成,所以总的顶点个数:

623=366 * 2 * 3 = 36

假设这个立方体的边长是2,那么我们来写一个面的顶点数据:

-1, 1, 1,/**/-1, -1, 1,/**/1, 1, 1,/**/-1, -1, 1,/**/1, -1, 1/**/, 1, 1, 1

可以在纸上画一画, 这是哪一个面的顶点数据呢?

其他面的顶点数据与此同理可以写出,有一点要注意,写的时候,一定要采用逆时针的顺序。

这里不赘述,完整代码放在结尾的链接里。

将顶点数据传递到gl

这个过程分三步:

    1. 构造立方体数据,这个上面已经完成
    1. 生成 WebGL buffer
    1. 将立方体数据传递到上面的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中获取。

image.png

我们想要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 矩阵就应该是一个单位阵:

[1000010000100001]\begin{bmatrix} 1 & 0& 0& 0 \\0 & 1 &0&0 \\0 & 0&1&0 \\ 0 & 0& 0& 1 \end{bmatrix}

这个矩阵对于输入的向量不会产生任何影响。

View矩阵

再来考虑View矩阵,View矩阵描述的是眼睛的位置,以及眼睛的朝向,我们用一个函数来生成这个矩阵:

function mat4_get_lookat(eye, target, up);

其中,eye 是眼睛位置,target是看向哪个点,up是眼睛的正上方。

函数的具体实现在文末代码给出。

这里我们假设眼睛在世界坐标系的(10,10,10)(-10, 10, 10)点,看向(0,0,0)(0,0,0)点。也就是立方体所在的位置。

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。

结果如下:

image.png

看起来是那么回事,全白色的一个立方体,但是这很像一个六边形,而不像一个立方体。

这为啥?

因为从我们眼睛的位置和角度看向这个立方体,立方体就是呈现的这么个形状。

将颜色搞的丰富一点

记得我们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分量,当做颜色直接输出,会是什么样:

image.png

这很容易理解,因为原始立方体的坐标越靠右边,那么就越亮。而左边就很黑。

再试试:

    float color = va_pos.y;
    gl_FragColor = vec4(color,color,color, 1.0);

我们用原始坐标的y分量,当做颜色,这很容易猜到了:

image.png

只有上面一部分是白的,下面就是黑的。

再试试:

    float color = len(va_pos);
    gl_FragColor = vec4(color,color,color, 1.0);

我们将坐标的长度,作为颜色输出,那么理论上讲,离原点越远,那么就越亮,所以八个顶点是最亮的:

image.png

还有什么别的有趣的着色办法吗,当然有。

比如说:

    float color = 1.0 - len(va_pos);
    gl_FragColor = vec4(color,color,color, 1.0);

顶点离原点的距离越远,那么就越黑,所以效果如下:

image.png

这里可以测试你脑海里的任何数学函数,来玩一玩这种效果。

那么黑白的颜色,没意思,我们下面来试试彩色。

    gl_FragColor = vec4(va_pos, 1.0);

直接将va_pos,当做RGB,输出:

image.png

分析一下,最右边上边的那个点,原始坐标是(1.0,1.0,1.0)(1.0, 1.0, 1.0), 所以这里的颜色是全白,最亮。

再来看看下面的那个顶点,为什么是蓝色,因为原始坐标是(-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);

效果如下:

image.png

这张图与上图的区别就是,更多的地方有了亮度,因为我们手动将颜色为负值的地方进行了转变。

清晰的将三角形表示出来

这里很简单,我们不画三角形,只需要画线就行了:

    gl.drawArrays(gl.LINES, 0, 12 * 3);

效果如图:

image.png

小习题

  • 变化一下眼睛的位置试验一下效果
  • 定时变化眼睛的位置,变成动画

完整代码链接

WebGL第四十三课完整代码 - 掘金 (juejin.cn)