WebGL第四十四课:渲染二元函数图像

545 阅读5分钟

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

本文标题:WebGL第四十四课:渲染二元函数图像

友情提示

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

前情回顾

WebGL第四十三课:渲染正方体 - 掘金 (juejin.cn)篇文章中,我们渲染了一个非常基础的正方体。

虽然基础,但是也包含了渲染3D图形的一般方法:

  • 构造顶点数据
  • 设置MVP矩阵
  • 绘制三角形

就这三个步骤。

让我们来用这三个步骤,来试着绘制二元函数的图像,比如说 y=x2+z2y = x^2 + z^2

函数图像的数据描述

一个函数图像包含了无穷多个点,所以我们只能用采样的办法,来粗略画出图像。

我们在限定的自变量的范围内,尽量让采样点,离得近一点,最后得出的图像就越完美。

我们这里选取自变量的范围为:

  • x[1,1]x ∈ [-1, 1]
  • z[1,1]z ∈ [-1, 1]

y=x2+z2y = x^2 + z^2 得出,y 的范围是 [0,2][0, 2]

如果在x轴和z轴,分别用三个点,来采样的话,那么一共是九个点:

  • x ∈ [-1, 0, 1]
  • z ∈ [-1, 0, 1]

将上面两个变量组合一下就行。

不用想,这样的结果肯定不完美,我们直接让每个轴都采用101个点,拿x轴来说,

x ∈ [-1, .., .., .., 1] // 这里一共有101个点

函数图像数据的构造

先看看一个平坦的平面如何来构造:

    let step = 2 / 101;
    let Result = [];
    for (let xidx = 0; xidx <= 100; xidx++) {
        for (let zidx = 0; zidx <= 100; zidx++) {
            Result.push(min + xidx * step, 0, min + zidx * step);
        }
    }

上述代码就是遍历了每个采样点,然后填充到Result中,可以看见,y分量全部置了0。

之所以y分量全部置0,是因为y的实际值,可以在vertex_shader中进行计算得出。

这只是最基础的骨架的点数,如果我们想用TRIANGLES的方式来绘制,必须得将上面的点数扩充。

扩充的原理如下图:

image.png

对于骨架上的每四个点,我们必须扩充成两个三角形,也就是六个点,算法如下:

let v_data = [];
for (let xidx = 0; xidx < 100; xidx++) {
  for (let zidx = 0; zidx < 100; zidx++) {
   let b_index = 101 * 3 * xidx + zidx * 3;
   let up_index = b_index + 3;
   let right_index = b_index + 3 * 101;
   let dia_index = right_index + 3;
   v_data.push(raw_data[b_index], raw_data[b_index + 1], raw_data[b_index + 2]);
   v_data.push(raw_data[right_index], raw_data[right_index + 1], raw_data[right_index + 2]);
   v_data.push(raw_data[dia_index], raw_data[dia_index + 1], raw_data[dia_index + 2]);

   v_data.push(raw_data[b_index], raw_data[b_index + 1], raw_data[b_index + 2]);
   v_data.push(raw_data[dia_index], raw_data[dia_index + 1], raw_data[dia_index + 2]);
   v_data.push(raw_data[up_index], raw_data[up_index + 1], raw_data[up_index + 2]);
  }
}

要注意,此时,我们依然没有真正填充y的实际值,y依然是0。

vertex_shader的写法

这就是真正要计算y的地方,之所以放这里,才计算,是为了优化,毕竟放在gpu里算,肯定是要快的:

    vec3 pos = a_PointVertex;
    pos.y = pos.x*pos.x + pos.z*pos.z;

fragment_shade的写法

这里很简单,直接输出一个白色即可:

gl_FragColor = vec4(1.0, 1.0, 1.0 1.0);

绘制

正确设置好MVP矩阵之后,绘制函数和绘制立方体没什么区别,还是一句

gl.drawArrays(gl[object.draw_mode], 0, object.count);

注意,我们上述有了object的概念,其实就是把这个函数对象封装了一下,里面的count字段,就是真正的点数。

结果如下:

image.png

看起来有一点函数图像的意思,但是效果不明显。

3D渲染的时候,如果颜色设置的不好的话,那么看起来就跟平面图形没什么两样,所以我们要让颜色起点作用。

设置合适的颜色

上面说过, y=x2+z2y = x^2 + z^2 得出,y 的范围是 [0,2][0, 2]。 因为我们的自变量的范围都是 [1,1][-1, 1]之间。

我们不妨用y的值来影响颜色,比如说,y的值越大,颜色越亮,反正就越暗。

我们在vertex_shader中,先把y的值,传递到fragment_shader里,使用varying变量, 在两个shader中的最外层同时声明:

        varying float va_y;

vertex_shader里,传递上面的变量:

        vec3 pos = a_PointVertex;
        pos.y = pos.x*pos.x + pos.z*pos.z;
        va_y = pos.y;

此时此刻,我们在fragment_shader中,就可以进行颜色的计算了:

gl_FragColor = vec4(va_y/2.0,va_y/2.0,va_y/2.0, 1.0);

为什么不直接使用,而先除以2?

除以2可以是va_y的范围贴合我们颜色的范围,也就是[0,1]。

好了,观察一下效果:

image.png

比第一张图的效果好多了,因为多了明暗来展示y的大小。

让观察角度动起来

观察角度的变化,需要利用View矩阵,我们初始化设置View矩阵的时候,选取的观察点是:(10,10,10)(-10, 10, 10)

我们不妨让观察点,绕着y轴转起来,这样的话,可以无死角来观察这个函数图像了:

    // view
    view_mat = mat4_get_lookat([10 * Math.sin(frame_count * 2 * 3.14 / 180), 10, 10 * Math.cos(frame_count * 2 * 3.14 / 180)], [0, 0, 0], [0, 1, 0]);
    gl.uniformMatrix4fv(u_View_loc, false, new Float32Array(view_mat));

上面的代码,第一样,是计算新的View矩阵,注意里面有一个变量,frame_count。这个变量用来计数,每渲染一次,我们就加1,这样可以控制一些状态。比如说,这里的frame_count和三角函数sin和cos结合,就能让xz分量在一个圆周上来回的变化,这样可以让观察点绕着y轴转动。

一个新的问题来了,如何不停地渲染,产生动画效果呢?

两个方案:

  • setInterval
  • requestAnimationFrame

这两个方案的比较,这里就不涉及了,本文直接选取第二种方案:

function gl_draw() {
    frame_count++;
    // gl.enable(gl.CULL_FACE);
    gl.enable(gl.DEPTH_TEST);

    gl.clearColor(0.2, 0.2, 0.2, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    uniforms_update(gl);
    draw_object(quad_object);
    window.requestAnimationFrame(gl_draw);
}

我们可以看到,每次渲染的时候,frame_count都加1。 然后就是一般的渲染的过程,我们这里之所以没有打开背面裁剪,是因为函数图像可能有大量的背面,如果裁剪掉,那么看起来就会缺少很多。

好了,最后的动图如下:

NXnpdHyp - 码上掘金 (juejin.cn)