Shader 优化 | OpenGL 绘制网格效果

2020-05-26 15:39:35 浏览数 (2)

前几天发布了这样一篇文章:

KodeLife | Shader 实时编辑预览的强大工具使用实践

除了介绍 KodeLife 的使用之外,还附带了一个 Shader 绘制网格效果的代码。Shader 讲解

在我的 Shader 代码中是这样绘制网格的:

代码语言:javascript复制
    vec2 fragcoord = vec2(gl_FragCoord.xy / u_resolution);
    vec3 bgColor = vec3(1.0,1.0,1.0);
    vec3 pixelColor = bgColor;
    vec3 gridColor = vec3(0.5,0.5,0.5);

    const float width = 0.1;
    const float minWidth = 0.003;
    for(float i = 0.0; i < 1.0; i =width){
    if (mod(fragcoord.x,width) < minWidth || mod(fragcoord.y,width) < minWidth){
            pixelColor = gridColor;
        }
    }
    gl_FragColor = vec4(pixelColor,1.0);

首先,讲解几个概念:

gl_FragCoord 代表当前像素相对于屏幕的坐标,屏幕左下角为原点。

u_resolution 是当前图像的分辨率。

gl_FragCoord 除以 u_resolution 得到的结果 fragcoord 就是归一化的屏幕坐标。

由于已经归一化了那么 fragcoord 的值就在 [0,1] 的闭区间内。

同时用 gridColor 作为网格的颜色,bgColor 作为背景色,也是默认的颜色,pixelColor 作为最后输出的颜色。

那么,代码的重点就在于 for 循环里面了。

由于 fragcoord 归一化有了确定的值域范围,所以可以在 for 循环中将它十等分。

另外,因为片段着色器每个像素都会执行一遍,每次 fragcoord 值都是变化的,但不管怎么变化,它的范围都会落在 for 循环的十等份里面。

比如其中某一份的范围是 [0.2,0.3) 的左闭右开区间,当前像素就落在这个范围内。

那么 mod 取模函数就会判断当前值距离左区间阈值是否在 minWidth 范围内,其中 minWidth 相当于是指定网格线的宽度。

如果在范围内,那么显示的颜色就是网格色,否则就是默认的背景色。

以上的讲解对于坐标的 xy 值是一样的道理。原理通过判断该像素点的坐标是否位于临界范围内来选择性着色。

显示这种绘制方式是有它的弊端,因为每一个像素执行片段着色器的时候,都要进行一次 for 循环判断它处于哪个区域内。

这样就有了太多不必要的计算流程,尤其是 for 循环的每次遍历。


接下来就是微信群中大佬给出的 Shader 代码:

代码语言:javascript复制
vec2 st = vec2(gl_FragCoord.xy / u_resolution);
st.x *= u_resolution.x / u_resolution.y;

vec3 color = vec3(.0);

st *= 10.;

vec2 i_st = floor(st);
vec2 f_st = fract(st);

color  = step(.98,f_st.x)   step(.98,f_st.y);

gl_FragColor = vec4(color,1.0);

可以一眼看出这里面没有 for 循环的操作了。

还是先讲解几个级别操作:

floor 函数就是向下取整的操作

fract 函数是 x - floor(x) 的操作,也就是取小数部分的意思。

通过对 st 进行 floorfract 操作可以分出它的整数和小数部分。

step 函数类似于 if 判断,当第二个参数大于等于第一个参数,则返回 1 ,否则返回 0 。

整个 Shader 代码第一行还是相同的,都是归一化操作。

然后在第二行

代码语言:javascript复制
st.x *= u_resolution.x / u_resolution.y

实际上是做了一个比例切换的操作。将 stxy 值按照图像分辨率的宽高比做了调整,其中以 y 为基准 1 。

这样一来,sty 值还是在 [0,1] 范围内,而 x 值可能大于也可能小于这个范围了,这都取决于图像分辨率了。

接下来将 st 乘了 10 ,这下 st 的值域范围就在 [0,10] 了 ,这样的操作是为了接下来的 floor 函数,因为它是取整,如果都在 [0,1] 范围内,取的整数永远都是 0 了。

前面转换操作是为了接下来的重点函数 step

代码语言:javascript复制
color  = step(.98,f_st.x)   step(.98,f_st.y);

前面的 floor 其实也是将 xy 轴做了等分,比如 y 的值域是 [0,1] ,乘以 10 之后,就是十等分,x 的值域如果是 [0,1.7] ,乘以 10 之后,就是十七等分。

fract 操作的结果范围必然是 [0,1) 的左闭右开区间。

step 函数的意图就是如果该像素点的坐标接近于等分线,那么 color 的颜色值返回的就是 1 ,显示白色,否则返回 0 ,显示黑色。

比如,st 的 x 值是 7.99 了,接近于 8 ,那么就要显示白色网格线了,对于 y 值同理。

这样一来就可以对每个像素点进行判断,根据它的坐标决定要显示什么颜色。

总结对比

在第二种绘制中,由于做了比例转换操作,所以绘制出来的网格大小都是一致的,且都是正方形。

而第一种没有比例切换操作,当宽高不同的情况下,同样进行十等分的话,画出来的网格是个长方形了。

但是,两种绘制的思路都是相同的,姑且称它为 接近法 吧,当绘制的像素接近等分线时,就显示不一样的颜色。

于是,等分线的操作思路就各有不同了。前者是利用 for 循环来制造划分,后者则是利用当前像素的 xy 值的特点来绘制的。

当然更推崇后者的绘制方式了,也是学到了新技巧~~~

0 人点赞