这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战
本文标题:WebGL第三十二课:简单2D光照
复制代码
友情提示
这篇文章是WebGL课程专栏的第32篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
本课代码直接跳转获取:三十二课代码
引子
尽量将一些基础的知识在2D部分讲完,因为这些概念是2D和3D通用的,比如说,光照的概念。
给渲染的图形加一点光照可以让效果增加质感。这也是现代图形学的主要问题之一,就是尽量让光线更真实。
当然,我们这里就用一般的简化的算法就够了。
比如下面这个图,就好像一个探照灯一样,照到哪里,哪里就亮,并且有柔和的发散效果:
|
|
本篇就来讲一下,上面的效果如何实现。
回顾一下,WebGL 如何显示图片
-
- 将图片传到gl中去
-
- 设置顶点的uv
-
- 在fragment_shader中进行采样
这里重点回顾一下第二点,就是顶点uv的设定。
还是要先讲一下,我们的顶点数据是什么样子的:
this.data = [
// 第一个三角形
-1, -1, 0, 0, // 左下角点
1, -1, 1, 0, // 右下角点
1, 1, 1, 1, // 右上角点
// 第二个三角形
1, 1, 1, 1, // 右上角点
-1, 1, 0, 1, // 左上角点
-1, -1, 0, 0, // 左下角点
];
复制代码
我们可以看见是由两个三角形组成的,一共六个点。
每个顶点包含四个数据,分别是
- x
- y
- u
- v
例如,第一个左下角点:
-1, -1, 0, 0, // 左下角点
复制代码
(-1,-1)
代表这个点是屏幕左下角的意思。
(0,0)
就是说这个点应该和图片的左下角锚定的意思。
然后依次将这些点的坐标和uv写出来,就是上述代码。
有人问了,为什么是六个点,不是四个点。其中有些点的数据不是重复了吗?
问得好,因为我们绘制用的模式就是这样的,一定要这样列出来,然后按照一个三角形一个三角形这样的画,才行。
当然,画三角形有其他模式,我们这里为了讲解,就采用这种绘制模式,无伤大雅。
拉取图片和传递到gl
这个前面关于贴图的文章已经说过了,详情请看二十六课。
绘制三角形的过程
这个过程十分重要,因为对于我们理解贴图已经光照都有帮助。
首先,第一个三角形,就是上面data代码的前三行,也就是前三个点。
绘制的时候,先确定位置,这个时候可以定出这个三角形就是右下角的半个三角形
三个点的位置定了,那么这三个点的颜色,可以根据这三个点的uv信息,去图片里采样,如果锚定没问题的话,三个点最后显示的样子如下:
问题就来了,三个点的uv当然可以在图片中采样到正确的颜色,那么这三个点中间的颜色,又是从哪里来的?
答案就是插值:
fragment_shader
会对中间区域进行插值,然后求出uv,然后采样。
这一点很重要,因为一会算光照的时候有用。
光照怎么算
先用一个很简单的算法搞定:
设定一个uniform变量
uniform vec2 u_lightPos
这个变量用来代表光源的位置,我们的算法很简单,
也就是说,离这个光源越近,就越亮,越远就越暗,距离超过1,直接就是黑暗一片。
实现在shader中
第一个问题就是,上面的代码应该写到 vertex_shader
还是 fragment_shader
?
不妨做做实验,我们先写在vertex_shader
中:
vertex_shader
:
<script id="vertex_shader" type="myshader">
// Vertex Shader
precision mediump int;
precision mediump float;
uniform mat3 u_all;
uniform vec2 u_lightPos; // 光源的位置
attribute vec2 a_PointVertex; // 顶点坐标
attribute vec2 a_PointUV; // 顶点UV
varying vec2 uv;
varying float light;
void main() {
vec3 coord = u_all * vec3(a_PointVertex, 1.0);
gl_Position = vec4(coord.x, coord.y, 0.0, 1.0);
uv = a_PointUV;
uv.y = 1.0 - uv.y;
light = 1.0 - distance(coord.xy, u_lightPos);
}
</script>
复制代码
fragment_shader
:
<script id="fragment_shader" type="myshader">
// Fragment shader
precision mediump int;
precision mediump float;
uniform sampler2D u_funny_cat; // 有趣的猫的图片
varying vec2 uv;
varying float light;
void main() {
vec4 sample_color = texture2D(u_funny_cat, uv);
gl_FragColor = vec4(sample_color.xyz * light, 1.0);
}
</script>
复制代码
结果一运行,如图:
全黑,这是为什么呢?
我们如果没有设置uniform变量 的话, 这个变量默认就是0,也就是说,每一个维度都是0。
就是说,默认光源的位置就是 。
那么我data中的六个点的坐标离 点的距离都是
那么 这六个点 算到的 。
问题就来了,四周的六个点,算到的是 ,为什么中间也黑了?
对于每一个三角形来说,light
计算好之后,通过 varying
的方式,传递给fragment_shader
。fragment_shader
收到了同样的三个light
值,那么他不管怎么插值,中间的部分,也只能是
了。
也就是说,这个过程,应该放到fragment_shader
中,来计算。
我们把顶点的位置通过varying
的方式,传递给fragment_shader
,然后根据公式来计算,:
<script id="vertex_shader" type="myshader">
// Vertex Shader
precision mediump int;
precision mediump float;
uniform mat3 u_all;
attribute vec2 a_PointVertex; // 顶点坐标
attribute vec2 a_PointUV; // 顶点坐标
varying vec2 uv;
varying vec2 pos;
void main() {
vec3 coord = u_all * vec3(a_PointVertex, 1.0);
gl_Position = vec4(coord.x, coord.y, 0.0, 1.0);
uv = a_PointUV;
uv.y = 1.0 - uv.y;
pos = coord.xy;
}
</script>
<script id="fragment_shader" type="myshader">
// Fragment shader
precision mediump int;
precision mediump float;
uniform sampler2D u_funny_cat; // 有趣的猫的图片
uniform vec2 u_lightPos; // 光源的位置
varying vec2 uv;
varying vec2 pos;
void main() {
vec4 sample_color = texture2D(u_funny_cat, uv);
float light = 1.0 - distance(pos, u_lightPos);
gl_FragColor = vec4(sample_color.xyz * light, 1.0);
}
</script>
复制代码
这样的出的结果就如下了:
搞点气氛
一束白光打过去,没啥意思,我们整点气氛:
那这个怎么搞的,简单。
我们将图片上采样下来的结果, G 分量和 B分量,全部搞成0。然后只保留R分量就行了。
代码如下:
void main() {
vec4 sample_color = texture2D(u_funny_cat, uv);
float light = 1.0 - distance(pos, u_lightPos);
sample_color.x = sample_color.x * light;
sample_color.y = 0.0;
sample_color.z = 0.0;
gl_FragColor = vec4(sample_color.xyz, 1.0);
}
复制代码
动起来
依然在console里写个定时器,让光源的位置动一动,让结果更有意思:
// 在浏览器直接敲:
setInterval(() => {
let x = (Math.random() - 0.5) * 2;
let y = (Math.random() - 0.5) * 2;
gl.uniform2f(u_lightPos, x, y);
gl_draw();
}, 50);
复制代码
效果如下:
--- |
--- |
正文结束,下面是答疑
复制代码
小能能说:上面讲的插值那里,还真需要想一想呢。。
- 这个过程需要掌握,自然很多问题就有了答案。