1-直线
在高中,我们学过斜截式。从中可以知道,直线是一条只有斜率和截距的线,它没有长短和粗细的概念。
当然,我们在绘图时,为了方便观察,会给直线一个宽度。
直线可以通过其点斜式里的斜率和截距来定义,也可以通过2个点位来定义。
在此我们就通过2个点位定义一条直线。
1.封装一条直线。
vec4 Line(in vec2 C, in vec2 A, in vec2 B, in float lineWidth, in vec4 lineColor) {
// 向量AB的单位向量
vec2 ABn = normalize(B - A);
// 向量AC
vec2 AC = C - A;
// 点C到直线AB的距离 = 向量AC与单位向量ABn的叉乘
float distance = abs(AC.x * ABn.y - AC.y * ABn.x);
// 基于偏移导数的线宽
float width = dFdx(C.x) * lineWidth;
// 直线
return distance < width ? lineColor : vec4(0);
}
- C:当前片元的投影坐标位
- A、B:用于定义一条直线的两个点
- lineWidth:线宽
- lineColor:颜色
上面代码的本质就是求C点到直线AB的距离,当这个距离小于线宽时,为当前片元上色。
C点到直线AB的距离可以用向量AC和向量AB的单位向量的叉乘来求,如下图
// 向量AB的单位向量
vec2 ABn=normalize(B - A);
// 向量AC
vec2 AC = coord - A;
// 点C到直线AB的距离 = 向量AC与单位向量ABn的叉乘
float distance=abs(AC.x*ABn.y-AC.y*ABn.x);
2.绘制直线
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// 投影坐标
vec2 coord = ProjectionCoord(fragCoord, 3.);
// 背景色
vec4 backgroundColor = vec4(0, 0, 0, 1);
// 投影坐标系辅助对象
vec4 projectionHelper = ProjectionHelper(coord, 2., vec4(0, .4, 0, 1), vec4(.4, 0, 0, 1), 2., vec4(vec3(.3), 1));
// 直线
vec4 line = Line(coord, vec2(-1, -1), vec2(1, 1), 1., vec4(1));
// 最终的颜色
fragColor = backgroundColor + projectionHelper + line;
}
效果如下:
我们当前画出来的是一条无限长的直线,接下来咱们说一下如何去线段AB。
2-线段
线段AB可以基于有向距离AD和有向距离AB的比值来确定。
当点D在直线AB上式,点D和线段AB存在3种关系:
- 点D在AB中,有向距离AD和有向距离AB的比值在(0,1)]之间。
- 点D在A点之外的一侧,有向距离AD和有向距离AB的比值小于0。
- 点D在B点之外的一侧,有向距离AD和有向距离AB的比值大于1。
即知此理,我们便可以封装一条线段了。
1.封装线段。
vec4 Segment(in vec2 C, in vec2 A, in vec2 B, in float lineWidth, in vec4 lineColor) {
// 向量AB
vec2 AB = B - A;
// 向量AB的单位向量
vec2 ABn = normalize(AB);
// 向量AC
vec2 AC = C - A;
// 点C到直线AB的距离 = 向量AC与单位向量ABn的叉乘
float distance = abs(AC.x * ABn.y - AC.y * ABn.x);
// 基于偏移导数的线宽
float width = dFdx(C.x) * lineWidth;
// 有向距离AC比有向距离AB的值是否在[0,1] 之间
bool distanceBool = distance < width;
// 投影条件
float ratio = dot(AC, AB) / dot(AB, AB);
bool ratioBool = ratio > 0. && ratio < 1.;
// 直线
return distanceBool && ratioBool ? lineColor : vec4(0);
}
当前对于是否绘制线段,分成了两部分考虑:
- 当前片元点到直线的距离是否小于线宽。
bool distanceBool = distance < width;
- 有向距离AC比有向距离AB的值是否在[0,1] 之间
float ratio = dot(AC, AB) / dot(AB, AB);
bool ratioBool = ratio > 0. && ratio < 1.;
dot(AC, AB) 是有向距离AC乘以|AB|
dot(AB, AB) 是有向距离AB乘以|AB|
dot(AC, AB) / dot(AB, AB) 是有向距离AC比有向距离AB
2.绘制线段。
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// 投影坐标
vec2 coord = ProjectionCoord(fragCoord, 3.);
// 背景色
vec4 backgroundColor = vec4(0, 0, 0, 1);
// 投影坐标系辅助对象
vec4 projectionHelper = ProjectionHelper(coord, 2., vec4(0, .4, 0, 1), vec4(.4, 0, 0, 1), 2., vec4(vec3(.3), 1));
// 线段
vec4 segment = Segment(coord, vec2(-1, -1), vec2(1, 1), 1., vec4(1));
// 最终的颜色
fragColor = backgroundColor + projectionHelper + segment;
}
效果如下:
3.线段优化。
其实我当前封装的Segment() 方法为了然大家更好的来理解,写得有点冗余。
我们可以先将当前片元在AB上的投影D收束在AB之间,即:
- 点D在A点之外的一侧时,默认有向距离AD和有向距离AB的比值等于0;
- 点D在B点之外的一侧时,默认有向距离AD和有向距离AB的比值等于1;
- 点D在AB中时,有向距离AD和有向距离AB的比值依旧在(0,1)之间。
接下来,我们再判断点C到点D的距离是否小于线宽即可。
代码如下:
vec4 Segment(in vec2 C, in vec2 A, in vec2 B, in float lineWidth, in vec4 lineColor) {
// 向量AB
vec2 AB = B - A;
// 向量AC
vec2 AC = C - A;
// 基于偏移导数的线宽
float width = dFdx(C.x) * lineWidth;
// 将有向距离AC和有向距离AB的比值收束在[0,1] 之间
float ratio = clamp(dot(AC, AB) / dot(AB, AB), 0., 1.);
// 线段
return length(ratio * AB - AC) < width ? lineColor : vec4(0);
}
-
clamp(x,min,max):
- x在min,max之间时,返回x;
- x小于min时,返回min;
- x大于max时,返回max。
-
ratio * AB :
- 点D在AB之间时,返回点D;
- 点D在A点之外的一侧时,返回点A;
- 点D在B点之外的一侧时,返回点B。
其效果和之前一样:
以此原理,我们还可以绘制曲线。
3-曲线
我们可以将曲线理解为由多条线段拼成的线,线段越短越精确,如下图所示:
- 6段的正弦曲线
- 12段的正弦曲线
我们在每一次着色的时候,可以遍历构成曲线的每一条线段,然后判断当前片元位与相应线段的关系。
以正弦波为例,说一下其代码实现。
1.封装一个正弦型函数。
float SinFn(float x, float a, float omega, float alpha) {
return a * sin(omega * x + alpha);
}
- x:自变量
- a:振幅
- omega:频率
- alpha:偏移
2.封装一条正弦型路径。
vec4 SinPath(vec2 coord, float start, float end, int segs, float lineWidth, vec4 sinColor, float a, float omega, float alpha) {
vec4 color = vec4(0);
float step = (end - start) / float(segs);
for(int n = 0; n < segs; n++) {
float x = start + float(n) * step;
float nextX = x + step;
vec2 A = vec2(x, SinFn(x, a, omega, alpha));
vec2 B = vec2(nextX, SinFn(nextX, a, omega, alpha));
vec4 segment = Segment(coord, A, B, lineWidth, sinColor);
if(segment.a != 0.) {
color = segment;
}
}
return color;
}
- coord:当前点
- star:起点
- end:结束点
- segs:段数
- lineWidth:线宽
- sinColor:颜色
- a:振幅a
- omega:频率
- alpha:偏移
上面就是在遍历构成曲线的每一条线段,然后根据当前片元位与相应线段的关系判断如何着色。
3.绘制正弦曲线。
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// 投影坐标
vec2 coord = ProjectionCoord(fragCoord, 3.);
// 背景色
vec4 backgroundColor = vec4(0, 0, 0, 1);
// 投影坐标系辅助对象
vec4 projectionHelper = ProjectionHelper(coord, 2., vec4(0, .4, 0, 1), vec4(.4, 0, 0, 1), 2., vec4(vec3(.3), 1));
//正弦曲线
vec4 path = SinPath(coord, -2., 2., 12, 1., vec4(1), 1., 1., iTime);
// 最终的颜色
fragColor = backgroundColor + projectionHelper + path;
}
效果如下:
扩展
如果我们给曲线的偏移值一个iTime,还可以制作一个波浪动画。
vec4 path = SinPath(coord, -2., 2., 12, 1., vec4(1), 1., 1., iTime);