本文正在参加「金石计划 . 瓜分6万现金大奖」
前言
接上文Shader从入门到放弃(二) —— 常见GLSL内置函数 – 掘金 (juejin.cn),上一篇文章我们介绍了GLSL中常用的一些函数:step
, smoothstep
, mix
。今天,我们就运用我们之前所学习的知识来绘制函数。
绘制函数
今天我们的编程目标是在我们之前绘制的网格中绘制任意的函数。我们绘制函数的基本思路是利用了微积分的思想,将任意的曲线分割成若干的曲线,通过绘制一小段一小段的直线来达到绘制曲线的目的。
可以看下面的两张图,第一张图的步长比较大,所以我们能够看出一些折线的感觉,第二张图中,我采用了更小的步长来进行绘制,所以整个函数看起来十分的圆滑。
绘制线段
那么有了这样的一个基本思想过后,现在有一个直接的问题摆在我们的眼前,那就是如何绘制一条线段?
有了之前的基础我们很容易就可以想到:
我们可以求出点到直线的距离,然后计算该距离是否处于某个值内(线的宽度)?如果存在我们就给这个点赋予一个颜色。伪代码如下:
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向量,再求长度即可。
$$
\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);
}
结果如下:
现在我们已经成功的画出了一条直线,那么我们又如何画出一条线段呢?其实很简单,我们只需要对上面的程序稍加改动就好了。
由于线段是具有长度的,所以当我们的向量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);
}
结果如下:
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,别走错片场了(笑)
- 首先第一步还是要将uv坐标和我们的坐标系的坐标保持一致(
fixUV
)。 nfx
表示的是第一段线段的第二个点。- 然后绘制这两个点表示的直线。
最后,我们就能将函数画出来啦~
总结
最后我们进行一下总结:
- 学习了直线和线段的画法
- 通过将函数分割为若干小段,再绘制若干小段的直线来模拟一段曲线实现了绘制任意函数的功能。
希望读者下来能够自行多加练习,完整代码如下:
暂无评论内容