Shader从入门到放弃(四) —— 绘制闪耀星际


theme: smartblue

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

经过3个章节的学习,相信大家对shader编程也逐渐的有了一些感觉,所以这次我们玩个“大”的!

今天的学习内容是绘制“闪耀星际”,正如歌中唱的那样:

星际闪耀光影,落入你的眼睛,如迷人的水晶,把浪漫放映……

好,废话不多说,直接进入正题!

绘制“闪耀星际”

原理概述

image.png
如上图所示,其实我们的原理很简单,就是找到当前点的周围8个点,然后将它们连接起来。再根据每对点之间的距离来改变线的透明度即可。最后再多绘制几层,叠加起来,我们的星际就完成了。

说起来简单,做起来又是怎么一回事呢。Let’s code!

编码

格子绘制

首先第一步要做的还是归一化uv坐标,然后给uv坐标乘上一个数,这个数就是我们想要将坐标划分几个格子的数量。这里我们将先将画布划分成5个格子吧。

再通过fract 函数来获得每个“格子”中的uv坐标,我们将其称为st

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    uv *= 5.0;
    vec2 st = fract(uv) - 0.5;
    
    vec3 col = vec3(0.0);
    
    col.rg = st;
    fragColor = vec4(col, 1.0);
}

结果如下:

shadertoy.png

在格子中绘制点

现在,我们开始在每个格子中绘制一个圆吧。

为了方便我们观察格子的边界,所以我们加了一个if条件,如果st坐标处于某个范围内就把当前像素显示为红色。代码如下:


float d = length(st);
float m = smoothstep(0.1, 0.09, d);

col += m;
if(st.x > 0.45 || st.y > 0.45) {
    col = vec3(1.0, 0.0, 0.0);
}    

结果如下:

shadertoy.png

接下来,我们让每个点与周围8个点都进行连线。绘制直线的方法还记得吗?如果不记得了,可以回头再复习一下如何绘制一条直线。Shader从入门到放弃(三) —— 绘制函数 – 掘金 (juejin.cn)

连线

此处就不再赘述如何绘制直线了,直接给出绘制直线的函数:


float DistLine(vec2 p, vec2 a, vec2 b) {
    vec2 pa = p - a;
    vec2 ba = b - a;
    float t = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * t);
}


float Line(vec2 p, vec2 a, vec2 b) {
    float d = DistLine(p, a, b);
    float m = smoothstep(0.02, 0.01, d);
    return m;
}

当前格子的点要与周围的点产生连线就势必会用到 for 循环了。此处,我们还使用了一个数组来保存当前格子点的坐标与周围点的坐标,截止到目前 mainImage 中的完整代码如下:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0);

    uv *= 5.0;
    vec2 st = fract(uv) - 0.5;

    float d = length(st);
    float m = smoothstep(0.1, 0.09, d);

    int i = 0;
    vec2 p[9];
    for(float y = -1.0; y <= 1.0; y++) {
        for(float x = -1.0; x <= 1.0; x++) {
            vec2 offs = vec2(x, y);
            p[i++] = offs;
        }
    }

    for(i = 0; i < 9; i++) {
        m += Line(st, p[i], p[4]);
    }
    col += m;
    fragColor = vec4(col, 1.0);
    return;

    col += m;
    if(st.x > 0.48 || st.y > 0.48) {
        col += 1.0;
    }

    fragColor = vec4(col, 1.);
}

结果如下:

shadertoy.png

动起来吧

由于我们现在每个格子中的圆心坐标st都等于 vec2(0.0, 0.0),但是现在我们需要让每个点在自己的格子范围内动起来。所以,我们需要让每个格子中的圆心坐标st发生变化。而且,我们想要的是每个格子都发生不一样的变化。

此时,我们需要用到随机数。在shader程序中,并没有真正意义上的一个随机数,而是进行一些计算,让输入值改变比较小的时候,函数的输出结果发生比较大的改变。随机数是一个很大的话题,由于今天我们的重点不在随机数上,所以读者们可以跳过这一段,直接把函数拿来使用即可。

随机数函数如下:

vec2 N22(vec2 p) {
    p += vec2(434.67, 534.23);
    vec3 a = fract(p.xyx * vec3(123.34, 234.34, 345.65));
    a += dot(a, a + 34.45);
    return fract(vec2(a.x * a.y, a.y * a.z));
}

该函数是根据一个2维的向量来生成一个另一个随机的2维向量。我们可以通过下面这个程序来看看我们的随机数是否分布的比较均匀。

vec2 n = N22(uv);
fragColor = vec4(n, 0.0, 1.0);

shadertoy.png

通过上图我们可以看出,最终随机产生的结果还是不错的。我们就使用这个函数即可。

由于我们想要的是每个格子产生的随机数需要保持一致,所以我们不能够使用st 坐标来产生随机数,而是要获取每个格子的index或者说是 id来产生随机数。

我们可以使用floor 函数来获取每个格子的id。

vec2 id = floor(uv);
col.rg += id * 0.3;

shadertoy.png

通过上面的程序我们可以将id “可视化”出来以验证代码是否正确。

有了id过后,我们就可以产生随机数了。此处,我们新写一个 GetPos 函数来表示我们获取的点。

获取圆心坐标的函数如下,由于我们不仅仅是要获取当前像素所在格子中的圆心位置。而且我们还需要获取周围8个格子的圆心位置。所以我们还需要增加一个 offs 作为偏移量传入函数中。

vec2 GetPos(vec2 id, vec2 offs) {
    // 根据格子的 id和offs 产生一个随机数,
    // 乘上iTime使其根据时间发生变化
    vec2 n = N22(id + offs) * iTime;
    return sin(n) * 0.4 + offs;
}

再修改双重 for 循环中的代码如下:



// --- 
// float m = smoothstep(0.1, 0.09, d);
// +++
float m = 0.0;

for(float y = -1.0; y <= 1.0; y++) {
    for(float x = -1.0; x <= 1.0; x++) {
        vec2 offs = vec2(x, y);
        p[i++] = GetPos(id, offs);
    }
}

结果如下:

shadertoy.png

现在我们已经给格子中的圆心设置了随机的值,但是我们很容易得就发现了另一个问题:

image.png

在某些连线上,他们断开了。这是因为前面格子绘制的线被后面的格子绘制的线所覆盖了。这里有一个比较简单的解决方法:

把这些断开的线重新绘制一次。

image.png
如上图所示,我们可以发现,断开的线就是上图中画了红叉的线,所以我们需要将1-3, 1-5, 3-7, 5-7这四条线再重新画一遍即可。代码如下:

for(i = 0; i < 9; i++) {
    m += Line(st, p[i], p[4]);
}

m += Line(st, p[1], p[3]);
m += Line(st, p[1], p[5]);
m += Line(st, p[7], p[3]);
m += Line(st, p[7], p[5]);

结果如下:

20221123-140246.gif

渐隐渐显连线

我们可以看到每个点都与它附近的8个点完成了连线。现在我们需要做的就是根据连线的长度来设置线的透明度。我们需要修改一下 Line 函数。

float Line(vec2 p, vec2 a, vec2 b) {
    float d = DistLine(p, a, b);
    float m = smoothstep(0.02, 0.01, d);
    float d2 = length(a - b);
    m *= smoothstep(1.2, 0.8, d2);
    return m;
}

在上面的函数中,我们计算了线段两点的距离,如果他们之间的距离大于1.2,则隐藏,小于0.8则显示,在0.8~1.2的区间范围则在其间进行插值,得到的结果如下:

shadertoy.png

闪烁光点

连线基本完成,现在我们需要让我们的“星星”开始进行闪烁,发出blingbling的亮光!我们修改连线处的for 循环代码。

for(i = 0; i < 9; i++) {
    m += Line(st, p[i], p[4]);
    float d = length(st - p[i]);
    float spark = 1.0 / (d * d * 200.0);
    m += spark;
}

上面的1.0 / (d * d* 200.0) 是因为我们希望我们的闪光点产生一些“辉光”的效果,所以使用了形如 $\frac{1}{x^2}$ 的函数,在靠近中心的位置很亮,然后远离中心时亮度会迅速的减弱,分母上的200是控制衰减快慢的系数。值越大则衰减的越快。

iShot_2022-11-23_14.38.28-tuya.png

我们可以得到以下的效果:

shadertoy.png

嗯~~~ 看起来相当的不错。我们再利用一个sin函数让他们闪烁起来!


float spark = 1.0 / (d * d * 200.0) * (sin(t * 13.0 + p[i].x * 17.0) * 0.4 + 0.6);

此时我们的“星星”应该都闪烁起来了。我们可以把我们的格子和id 都关闭看一下效果。如下:

20221123145115.gif

效果看起来很不错。到目前为止,我们的工作已经进行了一大半了。胜利就在前方了!勇士们继续加油!

星域绘制

我们把刚刚的代码封装一下,命名为Layer,正如函数名一样,它表示的是“一层”的星星绘制,我们多绘制几层,就可以得到不可思议的效果!

float Layer(vec2 uv) {
    vec2 st = fract(uv) - 0.5;
    vec2 id = floor(uv);
    float m = 0.0;

    int i = 0;
    vec2 p[9];
    for(float y = -1.0; y <= 1.0; y++) {
        for(float x = -1.0; x <= 1.0; x++) {
            vec2 offs = vec2(x, y);
            p[i++] = GetPos(id, offs);
        }
    }
    float t = iTime;
    for(i = 0; i < 9; i++) {
        m += Line(st, p[i], p[4]);
        float d = length(st - p[i]);
        float spark = 1.0 / (d * d * 200.0) * (sin(t * 13.0 + p[i].x * 17.0) * 0.4 + 0.6);
        m += spark;
    }

    m += Line(st, p[1], p[3]);
    m += Line(st, p[1], p[5]);
    m += Line(st, p[7], p[3]);
    m += Line(st, p[7], p[5]);
    return m;
}


void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0);

    uv *= 5.0;
    col += Layer(uv);
    fragColor = vec4(col, 1.0);
}

我们再次利用for循环来多绘制几层

float t = iTime * 0.1;
for(float i = 0.0; i < 1.0; i += 1. / 4.) {
    float depth = fract(i + t);
    float size = mix(10.0, 0.5, depth);
    col += Layer(uv * size + i);
}

此处我们引入了另一个变量depth来表示每层的深度,深度值越大(离屏幕越近)则size越小,反之则越大。效果如下:

shadertoy.png

唔,现在看起来是相当的不错!不过我们还可以添加一些细节。现在每层星星的出现和消失看起来都有一些的突兀,所以我们可以根据当前层的深度值来增加一些淡入和淡出。

20221123150534.gif

现在的效果看起来是真的真的很不错!!!

最后我们稍加润色,可以给星星加上一些变化的颜色。再加上一个渐变的背景色。

此处给出最终的代码:
代码片段

总结

今天的代码总算是完结了,这算是我们经过之前的学习可以完成的“大作业”了吧。只需要短短的不到100行代码就绘制出了如此迷人的场景,这正是shader的迷人之处。我们回顾一下其中涉及到的技巧:

  1. uv坐标归一化
  2. 格子分割(id)
  3. 随机数
  4. 线的绘制
  5. 利用淡入淡出来润色

以上就是今天的全部内容了,希望各位能够多加练习,尽早达到熟练的程度。如果你觉得本文很不错,别忘了点赞哦~

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容