本文正在参加「金石计划 . 瓜分6万现金大奖」
前言
接上文,在Shader从入门到放弃 —— Shader编程简介及坐标系绘制 - 掘金 (juejin.cn)文章中,我们简单的介绍了什么是shader编程,并且利用shadertoy来进行shader编程。在上文中,我们完成了坐标系的绘制工作,主要用到了这几点:
- uv坐标的矫正
- 如何绘制网格(对uv坐标乘上一个数并且取小数部分作为新的uv坐标)
今天,我们将要进行进一步的学习,我们要学习一些GLSL中常用的内置函数,为我们后面的学习打下坚实的基础。
常用的内置函数
接上文中的代码,我们将绘制网格的函数抽离出来单独的作为一个函数:
vec3 grid(vec2 uv) {
vec3 color = vec3(1.0);
vec2 cell = 1.0 - 2.0 * abs(fract(uv) - 0.5);
// color = vec3(cell, 0.0);
if(abs(uv.x) <= 2. * fwidth(uv.x)) {
color = vec3(0.0, 1.0, 0.0);
} else if(abs(uv.y) <= 2. * fwidth(uv.y)) {
color = vec3(1.0, 0.0, 0.0);
} else if(abs(cell.x) <= 2. * fwidth(uv.x) || abs(cell.y) < 2. * fwidth(uv.y)) {
color = vec3(0.6);
}
return color;
}
复制代码
在Shader编程中有一条黄金准则:
能不用if则不用if. 具体是为什么,可以参考这篇文章:GPU 是如何工作的? - 掘金 (juejin.cn)
所以,我们现在要做的就是消除掉上述函数中的 if
。
现在我们开始介绍第一个内置函数 : step(edge, x)
step
step
generates a step function by comparingx
toedge
. For element i of the return value, 0.0 is returned ifx
[i] <edge
[i], and 1.0 is returned otherwise.
它的意思就是:如果x 小于某个值,则返回 0,如果大于等于这个值,则返回1。
所以上面的if语句我们可以写成:
// if(abs(uv.x) <= 2. * fwidth(uv.x)) {
// color = vec3(0.0, 1.0, 0.0);
// }
// ===>
color = step(abs(uv.x), 2.0 * fwidth(uv.x)) * vec3(0.0, 1.0, 0.0);
复制代码
但是这样的话,好像我们之前画的坐标系的格子都看不见了。所以我们要想办法将他们融合起来。所以,现在我们介绍第二个函数:mix
mix
它的函数签名如下:
genType mix
(genType x
, genType y
, genType a
);
mix
performs a linear interpolation betweenx
andy
usinga
to weight between them. The return value is computed as follows: x⋅(1−a)+y⋅a.
该函数的实际上执行的就是一个简单的 线性差值。 简单的说就是:当a = 1时,返回的值为y, 但a = 0时,返回x的值,如果返回的值为0.5,则返回 0.5x + 0.5y
的值,同理,如果a = 0.2, 返回 0.8 * a + 0.2 * y
。
我们可以利用这个函数来做颜色叠加,颜色叠加的公式可以写为:
这刚好符合上面的 mix
函数的定义。所以可以写出下面的式子:
color = mix(color, vec3(0.0, 1.0, 0.0), step(abs(uv.x), 2.0 * fwidth(uv.x)));
复制代码
对于其他的坐标我们都可以同理写出:
color = mix(color, vec3(0.6), step(abs(_uv.x), 2.0 * fwidth(uv.x)));
color = mix(color, vec3(0.6), step(abs(_uv.y), 2.0 * fwidth(uv.x)));
color = mix(color, vec3(0.0, 1.0, 0.0), step(abs(uv.x), 2.0 * fwidth(uv.x)));
color = mix(color, vec3(1.0, 0.0, 0.0), step(abs(uv.y), 2.0 * fwidth(uv.y)));
复制代码
OK,现在我们就已经完成了if 分支的替换工作。但是使用 step
函数它有一个小小的缺陷,我们可以通过一张图看出来。
如上图所示,我们可以看出,当我们的坐标系发生了旋转的情况下,在线条的边缘会产生很多的锯齿。这是因为step
函数它不是返回0就是返回1,它发生了突变,没有一个平滑过渡的区间。
所以,现在我们介绍第三个函数:smoothstep
smoothstep
genType smoothstep
(genType edge0
, genType edge1
, genType x
)
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
is equivalent to:
genType t; /* Or genDType t; */
t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
复制代码
smoothstep
returns 0.0 ifx
≤edge0
and 1.0 ifx
≥edge1
.
其函数图像如下:
我们可以直观的看出:当x < 0时,返回0,当 x > 1时,返回1,如果 x 处于0~1之间时,则有一个类似于线性插值的过渡,但是请注意,这里并不是线性插值,因为在接近两端的值的时候,该函数有一个平滑的过渡。所以我们可以通过这一点来对 step
函数进行替换以解决锯齿的问题。
我们可以写出下面的代码:
color = mix(color, vec3(0.6), smoothstep(3.0 * fwidth(uv.x), 1.0 * fwidth(uv.x), abs(cell.x)));
color = mix(color, vec3(0.6), smoothstep(3.0 * fwidth(uv.x), 1.0 * fwidth(uv.x), abs(cell.y)));
复制代码
结果如下:
我们可以发现在格子的一侧的锯齿情况有明显的改善,但是另一侧还是锯齿比较严重。这是因为我们格子的坐标是 0~1之间,但是我们是对 abs(x)
进行差值,abs(x)
函数小于 3.0 * fwidth(uv.x)
的值始终都处于格子的一侧,所以我们需要对uv坐标进行一点修改。
修改如下:
vec2 cell = 1.0 - 2.0 * abs(fract(uv) - 0.5);
color = mix(color, vec3(0.6), smoothstep(3.0 * fwidth(uv.x), 1.0 * fwidth(uv.x), abs(cell.x)));
color = mix(color, vec3(0.6), smoothstep(3.0 * fwidth(uv.x), 1.0 * fwidth(uv.x), abs(cell.y)));
复制代码
结果如下:
通过对比红色和绿色XY轴与灰色的格子线,我们可以清晰的看出格子上的锯齿有了明显的改善,这就是smoothstep
的功劳!那么类似的,我们可以通过此方法来优化我们的XY轴。
最终优化效果如下:
总结
今天我们通过解决解决一系列的问题学习了以下三个内置函数:
- 为了解决掉
if
分支,我们使用了step
函数 - 为了解决颜色融合的问题,我们使用了
mix
函数, - 为了解决线条锯齿的问题,我们使用了
smoothstep
函数。
希望读者后续多多使用这几个内置函数,达到熟练的程度。我们下篇文章再见!