“我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!”
开篇前的小牢骚
已经很久没有发文了,距离上次写文章已经过去了4个月之久。感觉自己每天都在焦虑中度过,明明想要的很多,但是每天却又什么都没有做似的。这种状态时自己十分讨厌的,从现在开始我决心要改变这种状态,造成这样的状态正是因为每天想的太多做的太少。既然如此,不如每天做一件微不足道的小事,也许是写一篇文章,也许是学习一个编程小技巧,也或许是跑步突破了自己的极限…… 不能总是沉浸在工作中,这样会让人的精神崩溃。。。
前面说了这么多的废话,我们还是进入正文吧,今天给大家带来的技术是利用GPU的能力进行2D作画。我们可以先看一下最终我们可以得到什么(效果如下图)
其中使用到的技术就是
WebGL
/GLSL
SDF
(Signed Distance Field)(有向距离场)
其中 WebGL
是浏览器底层提供的一种底层绘图API。但是WebGL
在这里只是作为了运行GLSL的宿主环境,可以理解为是JavaScript运行在浏览器中的关系。
今天的重点不在于 WebGL
的各项API介绍,我们的重点会在于 WebGL
中使用的着色器语言 GLSL
—— OpenGL Shading Language
。我们在片元着色器中编写代码来完成这幅画。 如果想要了解关于 WebGL
更多的知识,可以查看这篇文章:WebGL概述——原理篇 - 掘金 (juejin.cn)
编程环境介绍
为了屏蔽除开GLSL
以外的因素,我为大家推荐一个VSCode插件: glsl-canvas
,该插件可以直接运行我们编写着色器代码。
第一个GLSL程序
现在,让我们来编写第一个GLSL程序。
- 新建一个文件
hello_world.glsl
- 写入以下内容
void main () {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0);
}
- 按
Ctrl + P
, 输入>show glslCanvas
就可以运行我们的着色器程序了。
此时,可以看到我们右侧的屏幕显示了一个纯色。现在我们就来简单解释一下上述的代码是什么意思。
其实GLSL的语法与C语言比较类似,void main
表示该着色器是入口程序,而 gl_FragColor
是一个内置变量,它代表了屏幕上像素的颜色。每个像素都拥有一个叫gl_FragColor
的值。
接下来,我们将引入更多的内置变量与函数,逐步实现我们最终的效果。
更进一步
接下来,我们让画面变的稍微绚丽多彩一些。在 glsl-canvas
插件中,它为我们提供了一系列的内置变量,比如 u_resolution
表示的是当前预览窗口的分辨率。而在 GLSL
中也提供了一些内置变量,比如: gl_FragCoord
表示了当前像素在预览窗口中的坐标。通常我们将其归一化表示: 通过 gl_FragCoord.xy / u_resolution
可以将坐标归一化到0~1之间。
注意:有的内置变量是插件所提供的,有的内置变量是GLSL本身自带的,这一点我们需要弄清楚。(以 u_xxx
开头的变量名是由插件提供的,由gl_XXX
开头的变量则是GLSL提供的)
现在,我们将程序修改如下:
precision mediump float;
uniform vec2 u_resolution;
void main () {
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = vec4(uv, 0.0, 1.0);
}
注意,在最上方需要加入 precision mediump float
,否则插件会报错。这是声明浮点数默认精度的声明。结果如下:
可以看到,我们的画面变的稍微有一点点的“炫”。上面我们也说过了,我们将预览窗口中的每个像素的坐标归一化了。所以反应到具体的颜色上:
- 左下角就是 (0, 0, 0, 1)
- 右下角是 (1, 0, 0, 1)
- 右上角是 (1, 1, 0, 1)
- 左上角是 (0, 1, 0, 1)
而矩形中间的的颜色则是通过插值得到。具体不过多阐述。
有了这些基础后,我们就可以进入SDF的部分了。
SDF (Signed Distance Field)
SDF——有向距离场,是一种能够描述物体内外关系的一种表示。以圆形为例:
在圆形内的一点P,距离圆形边缘的距离为d,但是由于点P在圆形的内部,我们定义它到圆形的距离为 -d;
而在圆形外的一点Q,距离圆形边缘的距离为 d',但是由于点Q在圆形的外部,我们定义他到圆形的距离为 d'。
简单的讲就是在图形内部的距离为负值,在图形外部的距离为正值。所以,圆形的SDF函数为:
float circle(vec2 uv, vec2 center, float d) {
return length(uv - center) - d;
}
现在,我们就将其可视化出来。
precision mediump float;
uniform vec2 u_resolution;
float circle(vec2 uv, vec2 center, float d) {
return length(uv - center) - d;
}
void main () {
vec2 uv = gl_FragCoord.xy / u_resolution;
float d = circle(uv, vec2(0.5, 0.5), 0.1);
vec3 color = vec3(d);
gl_FragColor = vec4(color, 1.0);
}
结果如下:
我们可以看出来越靠近中心的地方颜色就深,也就表示距离越小。其实中间的值已经是负值了,但是由于颜色是UINT8类型的值,所以看不出来。
我们将内部的负值全部设为0,外部的值设为1,让这个圆看起来更清晰、直观一些。修改程序如下:
precision mediump float;
uniform vec2 u_resolution;
float circle(vec2 uv, vec2 center, float d) {
return length(uv - center) - d;
}
void main () {
vec2 uv = gl_FragCoord.xy / u_resolution;
float d = circle(uv, vec2(0.5, 0.5), 0.1);
// ++++++++++++++++++++
if (d <= 0.0) {
d = 0.0;
} else {
d = 1.0;
}
// ++++++++++++++++++++
vec3 color = vec3(d);
gl_FragColor = vec4(color, 1.0);
}
此处新增了一个if
条件判断,但是在GLSL的最佳实践中,就是尽可能不要使用if
语句,能用内置函数性能更佳!(因为这些内置函数往往都是有硬件算力加成的),GLSL为我们提供了这样的一个函数,我们使用 step
函数来替代if
语句。程序如下:
step
generates a step function by comparingx
toedge
.
For element i of the return value, 0.0 is returned if x
[i] < edge
[i], and 1.0 is returned otherwise.
precision mediump float;
uniform vec2 u_resolution;
float circle(vec2 uv, vec2 center, float d) {
return length(uv - center) - d;
}
void main () {
vec2 uv = gl_FragCoord.xy / u_resolution;
float d = circle(uv, vec2(0.5, 0.5), 0.1);
d = 1.0 - step(0.0, d);
vec3 color = vec3(d);
gl_FragColor = vec4(color, 1.0);
}
OK,现在我们学会了使用SDF来表示图形,这里只使用了圆形来简单的展示了SDF的用处,还有更多的SDF函数等待大家发掘,大家可以自行查找资料。
参考资料:
2D基本图形的Sign Distance Function (SDF)详解(上)_T-Jhon的博客-CSDN博客_sdf函数
2D基本图形的Sign Distance Function (SDF)详解(下)_T-Jhon的博客-CSDN博客_距离函数sdf
SDF组合
我们学会了一些基本的SDF之后,我们就可以将这些基本的SDF图形进行组合。在SDF中,对他们进行组合是一件非常容易的事情。主要的操作就是两个图形求交集、求并集。 假设我现在已经有了菱形、圆形 这两个SDF函数。
我们可以通过交、并运算来产生不同的图形。在SDF中,对其求交就是求两个值中的max
,而求并则是求min
- 求交(max):
- 求并(min):
现在我们学会了使用求交、并集的方式来生成更加复杂的图形了。比如,我们可以使用这种方式来绘制云朵(代码与绘制结果如下)。
precision mediump float;
uniform vec2 u_resolution;
float circle(vec2 uv, vec2 center, float d) {
return length(uv - center) - d;
}
float cloud(vec2 uv, vec2 center, float d) {
float step = 1.2;
float c1 = circle(uv, vec2(center.x - d * 0.9 * 1.0 * step, center.y), d * 0.9);
float c2 = circle(uv, vec2(center.x - d * 0.8 * 2.0 * step, center.y), d * 0.8);
float c3 = circle(uv, vec2(center.x, center.y), d);
float c4 = circle(uv, vec2(center.x + d * 0.9 * 1.0 * step, center.y), d * 0.9);
float c5 = circle(uv, vec2(center.x + d * 0.8 * 2.0 * step, center.y), d * 0.8);
return min(c5, min(c4, min(c3, min(c1, c2))));
}
void main () {
vec2 uv = gl_FragCoord.xy / u_resolution;
float d = cloud(uv, vec2(0.5, 0.5), 0.05);
d = 1.0 - step(0.0, d);
vec3 color = vec3(d);
gl_FragColor = vec4(color, 1.0);
}
图层叠加
我们可以通过这种方式来绘制画面中的其他元素。现在,我们还需要将他们以正确对的图层顺序叠加起来。我们以背景与云朵叠加为例来说明。首先我们先修改背景的颜色。我们重新组织代码,如下:
void main () {
vec2 uv = gl_FragCoord.xy / u_resolution;
//+++++++++++++++
vec3 backCol = vec3(0.2078, 0.7765, 1.0);
vec3 col = backCol;
float d = cloud(uv, vec2(0.5, 0.5), 0.05);
d = 1.0 - step(0.0, d);
//+++++++++++++++
vec3 cloudCol = vec3(d);
col = mix(col, cloudCol, d);
gl_FragColor = vec4(col, 1.0);
}
在上面的代码中,我们使用 backCol
来表示背景颜色,使用col
来表示最终的颜色,由于背景颜色是最下面的图层,我们可以先将背景颜色赋给最终颜色,然后我们需要将云朵的颜色叠加在背景色上。这里,我们需要使用一个内置函数:mix
,该函数的意思就是线性插值。
mix
performs a linear interpolation betweenx
andy
usinga
to weight between them. The return value is computed as follows: x⋅(1−a)+y⋅a.
这样,我们就完成了图层叠加的操作。结果如下:
我们发现背景稍微有点单调,我们想实现一个渐变色的背景应该怎么做呢?刚刚我们提高了mix
函数可以实现线性插值,那么我们是不是可以利用这一点呢?答案是肯定的。例如我们想让顶部为天蓝色,底部为黄色,则可以利用mix
函数,通过y坐标来进行插值。修改代码如下
vec3 backCol = vec3(0.2078, 0.7765, 1.0);
vec3 backCol2 = vec3(0.7412, 0.4353, 0.0667);
backCol = mix(backCol2, backCol, uv.y);
不过,如果我们想要天蓝色和土黄色的部分更多一些又应该怎么办呢?如下图所示:
最容易让我们想到的办法就是根据y值来进行判定,我们可以写一个 if
条件来判断。不过还记得上面说过的吗?我们在GLSL中要尽可能的少使用 if
,用内置函数来代替它。GLSL为我们提供了这样的一个函数:smoothstep
smoothstep
performs smooth Hermite interpolation between 0 and 1 whenedge0
<x
<edge1
. This is useful in cases where a threshold function with a smooth transition is desired.smoothstep
returns 0.0 ifx
≤edge0
and 1.0 ifx
≥edge1
.
举个例子:smoothstep(0.2, 0.8, d)
,如果 d < 0.2
则返回0,如果d > 0.8
则返回1,如果在 0.2~0.8之间,则进行插值,但是这里不再是线性插值,而是某个三次函数。这样得到的结果更加的平滑。
我们修改代码及结果如下:
vec3 backCol = vec3(0.2078, 0.7765, 1.0);
vec3 backCol2 = vec3(0.7412, 0.4353, 0.0667);
float gradient = smoothstep(0.1, 0.8, uv.y);
backCol = mix(backCol2, backCol, gradient);
我们学会了这些就可以利用这些技术来对各个形状进行绘制了,篇幅有限,后面绘制太阳、河流等就不再一一举例了。大家可以参考完整的代码(完整代码在文末)。
动画
最后,我们要让其动起来。此时我们需要用到 glsl-canvas
这个插件为我们提供的一个内置变量u_time
。在程序的开头添加 uniform float u_time
即可。然后我们为uv
坐标加上u_time
的值,让uv
时刻发生改变,这样我们的画面就可以动起来了。修改代码如下:
vec2 uv = gl_FragCoord.xy / u_resolution;
uv.x += u_time * 0.3;
uv.x = fract(uv.x);
结语
嘛,今天的教程基本就告一段落了。本文为大家介绍了:
- glsl-canvas 插件,该插件允许直接打开 GLSL 程序,并显示结果。
- 介绍了一些常用的内置函数
mix
,smoothstep
,min
,max
- 介绍了SDF的定义及用法
- 介绍了在叠加图层的方法。
虽然文中举的例子比较简单,但是希望读者能够举一反三,最终创作出属于自己的场景。最后给出完整代码