Shader从入门到放弃(三) —— 绘制函数

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

前言

接上文Shader从入门到放弃(二) —— 常见GLSL内置函数 – 掘金 (juejin.cn),上一篇文章我们介绍了GLSL中常用的一些函数:step, smoothstep, mix。今天,我们就运用我们之前所学习的知识来绘制函数。

绘制函数

今天我们的编程目标是在我们之前绘制的网格中绘制任意的函数。我们绘制函数的基本思路是利用了微积分的思想,将任意的曲线分割成若干的曲线,通过绘制一小段一小段的直线来达到绘制曲线的目的。

可以看下面的两张图,第一张图的步长比较大,所以我们能够看出一些折线的感觉,第二张图中,我采用了更小的步长来进行绘制,所以整个函数看起来十分的圆滑。
iShot_2022-11-21_21.18.20.png

iShot_2022-11-21_21.18.33.png

绘制线段

那么有了这样的一个基本思想过后,现在有一个直接的问题摆在我们的眼前,那就是如何绘制一条线段?
有了之前的基础我们很容易就可以想到:

我们可以求出点到直线的距离,然后计算该距离是否处于某个值内(线的宽度)?如果存在我们就给这个点赋予一个颜色。伪代码如下:

d = LineDist(point, line);
if (d < lineWidth / 2) {
    fragColor = vec3(1.0);
}

那么现在问题又转化为:如何计算点到直线的距离 有的小伙伴可能会反应出来之前初中学习过点到直线的距离公式:

$$
d = \frac{Ax + By + C}{\sqrt{A^2 + B^2}}
$$

But!!!这个公式有一个问题,我还得费劲的去确定A、B、C的值。所以,在图形学中,我们通常不使用这个公式。我们有一个更加简单的公式。如下图所示:

我们可以先求出AD点的位置,然后用AC减去AD的位置即可得到CD向量,再求长度即可。

image.png

$$
\vec{AD} = \frac{\vec{AC}\cdot\vec{AB}}{\vec{AB}\cdot\vec {AB}} \vec {AB}
$$
此时你可能有点懵逼,所以我们需要将上面的式子展开:

$$
\vec{AD} = \frac{|\vec{AC}||\vec{AB}|\cos{<\vec {AC},\vec {AB}>}}{|\vec {AB}||\vec AB|\cos <\vec {AB}, \vec {AB}>} \vec {AB}
$$

我们可以约去相同的 $\vec {AB}$,又因为 $\cos <\vec {AB}, \vec {AB}> = 1$所以,上面的式子变成了

$$
\vec{AD} = \frac{|\vec{AC}||\cos{<\vec {AC},\vec {AB}>}}{|\vec {AB}|} \vec {AB}
$$

此时我们引入一个$\vec {AB}$ 方向的单位向量 $\vec v$.
由于 $\vec v$ 是单位向量,所以上式又等于

$$
\vec{AD} = \frac{|\vec{AC}||\vec v||\cos{<\vec {AC},\vec {AB}>}}{|\vec {AB}|} \vec {AB}
$$

$$
\vec{AD} = \frac{\vec {AC} \cdot \vec v}{|\vec {AB}|} \vec {AB} = (\vec {AC} \cdot \vec v)\vec v
$$

这样就容易理解多了,$(\vec {AC} \cdot \vec v)\vec v$ 即是向量AC在向量AB方向上投影长度,在乘上AB方向的单位向量。

所以:
$$
\vec{AD} = \frac{\vec{AC}\cdot\vec{AB}}{\vec{AB}\cdot\vec {AB}} \vec {AB}
$$

我们可以写出下面的代码:

float distLine(vec2 p, vec2 a, vec2 b) {
    vec2 ab = b - a;
    vec2 ap = p - a;
    vec2 ad = dot(ab, ap) / dot(ab, ab) * ab;
    return length(ap - ad);
}


void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fixUV(fragCoord.xy);
    vec3 color = grid(uv);
    float d = distLine(uv, vec2(0.0), vec2(1.0));
    color = mix(color, vec3(0.0), smoothstep(0.02, 0.01, d));
    fragColor = vec4(color, 1.0);
}

结果如下:
image.png

现在我们已经成功的画出了一条直线,那么我们又如何画出一条线段呢?其实很简单,我们只需要对上面的程序稍加改动就好了。

由于线段是具有长度的,所以当我们的向量AC在AB方向上的投影小于0或者大于向量AB的长度时,我们需要将其截断。我们可以使用clamp函数来截断其范围,修改代码如下

float distLine(vec2 p, vec2 a, vec2 b) {
    vec2 ab = b - a;
    vec2 ap = p - a;
    vec2 ad = clamp(dot(ab, ap) / dot(ab, ab), 0.0, 1.0) * ab;
    return length(ap - ad);
}

结果如下:
image.png

OK,现在我们有了绘制线段的基础,我们可以进行函数的绘制了。基本思想就是将函数分为多个区间,在每个区间内进行绘制。所以,我们必然需要使用到 for循环。详细的代码如下:


float func(in float x) {
    return sin(x + sin(x + sin(x) * 0.5));
}

float plotFunc(in vec2 uv) {
    float f = 0.0;
    for(float x = 0.0; x <= iResolution.x; x += STEP) {
        float fx = fixUV(vec2(x, 0.0)).x;
        float nfx = fixUV(vec2(x + STEP, 0.0)).x;
        f += segment(uv, vec2(fx, func(fx)), vec2(nfx, func(nfx)), 0.02);
    }
    return f;
}

这里使用了一个STEP作为,来表示步长。func 中则是我们需要绘制的函数。各位前端er别问我为什么不把func作为参数传进去了,这里是GLSL,别走错片场了(笑)

  1. 首先第一步还是要将uv坐标和我们的坐标系的坐标保持一致(fixUV)。
  2. nfx表示的是第一段线段的第二个点。
  3. 然后绘制这两个点表示的直线。

最后,我们就能将函数画出来啦~

image.png

总结

最后我们进行一下总结:

  1. 学习了直线和线段的画法
  2. 通过将函数分割为若干小段,再绘制若干小段的直线来模拟一段曲线实现了绘制任意函数的功能。

希望读者下来能够自行多加练习,完整代码如下:

代码片段

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

昵称

取消
昵称表情代码图片

    暂无评论内容