持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
本文标题:WebGL第四十四课:渲染二元函数图像
友情提示
这篇文章是WebGL课程专栏的第44篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
前情回顾
在WebGL第四十三课:渲染正方体 - 掘金 (juejin.cn)篇文章中,我们渲染了一个非常基础的正方体。
虽然基础,但是也包含了渲染3D图形的一般方法:
- 构造顶点数据
- 设置MVP矩阵
- 绘制三角形
就这三个步骤。
让我们来用这三个步骤,来试着绘制二元函数的图像,比如说 。
函数图像的数据描述
一个函数图像包含了无穷多个点,所以我们只能用采样的办法,来粗略画出图像。
我们在限定的自变量的范围内,尽量让采样点,离得近一点,最后得出的图像就越完美。
我们这里选取自变量的范围为:
由 得出,y 的范围是
如果在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的方式来绘制,必须得将上面的点数扩充。
扩充的原理如下图:
对于骨架上的每四个点,我们必须扩充成两个三角形,也就是六个点,算法如下:
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字段,就是真正的点数。
结果如下:
看起来有一点函数图像的意思,但是效果不明显。
3D渲染的时候,如果颜色设置的不好的话,那么看起来就跟平面图形没什么两样,所以我们要让颜色起点作用。
设置合适的颜色
上面说过, 得出,y 的范围是 。 因为我们的自变量的范围都是 之间。
我们不妨用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]。
好了,观察一下效果:
比第一张图的效果好多了,因为多了明暗来展示y的大小。
让观察角度动起来
观察角度的变化,需要利用View矩阵,我们初始化设置View矩阵的时候,选取的观察点是:。
我们不妨让观察点,绕着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。 然后就是一般的渲染的过程,我们这里之所以没有打开背面裁剪,是因为函数图像可能有大量的背面,如果裁剪掉,那么看起来就会缺少很多。
好了,最后的动图如下: