【零基础】充分理解WebGL(四)
接上篇 http://juejin.cn/post/7103437998640857119
几何图形
通过前面两节我们已经知道距离场构图法的关键就是要构造距离场,并学习了如何构建圆、直线、线段的距离场,以及如果通过采样来构建连续函数曲线的距离场。
接下来,我们来讨论如何构建三角形、正多边形和椭圆的距离场。
首先是三角形:
我们定义点到三角形的距离为点到三角形三条边距离中最短的一条边的距离。
根据上面的定义,在三角形内部,点到三角形的距离不会超过三角形内接圆半径l,所以我们可以通过将距离除以半径l,得到归一化的距离场,如下图所示:
还有一个关键是需要确定点在三角形内部还是外部,这一点,我们可以通过距离的符号来判断,这是因为:
如果点P在三角形内部,对于P点的观察者来说,三角形的三条边的向量矩方向(或者说三条边的旋转方向)一致,反之,如果P点在三角形外部,三条边向量矩方向不一致,这一点表现在三个距离的正负号上,如果P在三角形内部,三个距离的正负号情况一致,否则不一致。这个判断方法不仅对三角形有效,对所有凸多边形都有效。
因此我们得到三角形距离场函数:
```glsl float sdf_line(vec2 st, vec2 a, vec2 b) { vec2 ap = st - a; vec2 ab = b - a; return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab); }
float sdf_seg(vec2 st, vec2 a, vec2 b) { vec2 ap = st - a; vec2 ab = b - a; vec2 bp = st - b; float l = length(ab); float proj = dot(ap, ab) / l; if(proj >= 0.0 && proj <= l) { return sdf_line(st, a, b); } return min(length(ap), length(bp)); }
/* 三角形的 SDF / float sdf_triangle(vec2 st, vec2 a, vec2 b, vec2 c) { vec2 va = a - b; vec2 vb = b - c; vec2 vc = c - a;
float d1 = sdf_line(st, a, b); float d2 = sdf_line(st, b, c); float d3 = sdf_line(st, c, a);
// 三角形内切圆半径 float l = abs(va.x * vb.y - va.y * vb.x) / (length(va) + length(vb) + length(vc));
// 点在三角形内部,定义距离为正 if(d1 >= 0.0 && d2 >= 0.0 && d3 >= 0.0 || d1 <= 0.0 && d2 <= 0.0 && d3 <= 0.0) { return min(abs(d1), min(abs(d2), abs(d3))) / l; }
// 点在三角形外部,定义距离为负 d1 = sdf_seg(st, a, b); d2 = sdf_seg(st, b, c); d3 = sdf_seg(st, c, a); return -min(abs(d1), min(abs(d2), abs(d3))) / l; } ```
注意:我们这里对上一节的函数做了一点小调整,主要是更改了参数次序。因为有许多不同图形的距离场函数,它们的参数不同,但至少都接受st,所以我们将st统一作为距离场函数的第一个参数。
最终我们可以通过这个距离场函数,给定三个顶点坐标,绘制出三角形了。
http://code.juejin.cn/pen/7105944424164622350
接着是正多边形
我们可以定义点P到正多边形距离为P到正多边形最近一条边的距离,并将它除以圆心到边的距离l进行归一化。
具体的代码实现如下:
```glsl
ifndef PI
define PI 3.141592653589793
endif
ifndef FLT_EPSILON
define FLT_EPSILON 0.000001
endif
vec2 transform(vec2 v0, mat3 matrix) { return vec2(matrix * vec3(v0, 1.0)); }
vec2 rotate(vec2 v0, vec2 origin, float ang) { float sinA = sin(ang); float cosA = cos(ang); mat3 m = mat3(cosA, -sinA, 0, sinA, cosA, 0, 0, 0, 1); return transform(v0 - origin, m) + origin; }
vec2 rotate(vec2 v0, float ang) { return rotate(v0, vec2(0.0), ang); }
float atan2(float dy, float dx) { float ax = abs(dx); float ay = abs(dy); float a = min(ax, ay) / (max(ax, ay) + FLT_EPSILON); float s = a * a; float r = ((-0.0464964749 * s + 0.15931422) * s - 0.327622764) * s * a + a; if(ay > ax) r = 1.57079637 - r; if(dx < 0.0) r = PI - r; if(dy < 0.0) r = -r; return r; }
float atan2(vec2 v) { return atan2(v.y, v.x); }
/* 从 v1 相对 v2 的逆时针夹角, 0 ~ 2 * PI / float angle(vec2 v1, vec2 v2) { float ang = atan2(v1) - atan2(v2); if(ang < 0.0) ang += 2.0 * PI; return ang; }
/* 正多边形 / float regular_polygon(vec2 st, vec2 center, float r, float rotation, const int edges) { vec2 p = st - center; vec2 v0 = vec2(0, r); // 第一个顶点 v0 = rotate(v0, -rotation);
float a = 2.0 * PI / float(edges); // 每条边与中心点的夹角
float ang = angle(v0, p); // 取夹角 ang = floor(ang / a); // 在哪个区间
vec2 v1 = rotate(v0, a * ang); // 左顶点 vec2 v2 = rotate(v0, a * (ang + 1.0)); // 右顶点
float c_a = cos(0.5 * a);
float l = r * c_a; float d = sdf_line(p, v1, v2);
return d / l; } ```
注意这里面有些细节,我们通过反正切来求向量夹角,通过线性变换rotate来旋转向量,其中线性变换是非常重要的数学方法,在后续章节中会进一步详细介绍。
这样我们就可以绘制正多边形了,比如下面的代码绘制了一个正7边形。
http://code.juejin.cn/pen/7105959050231152647
椭圆和椭扇形
和圆类似,我们通过椭圆方程来定义椭圆的距离场。
$d = 1.0 - (x^2/a^2 + y^2/b^2)$
然后我们通过夹角来近似计算椭扇形距离场,这里我们把距离场分成三个部分,两个向量夹角中间的部分,以及左侧、右侧两边的部分。
具体实现代码如下,有很多细节,这里不再赘述,有兴趣的同学可以仔细研究,在码上掘金上修改一些参数运行看看。
```glsl float sdf_ellipse(vec2 st, vec2 c, float a, float b) { vec2 p = st - c; return 1.0 - sqrt(pow(p.x / a, 2.0) + pow(p.y / b, 2.0)); }
float sdf_ellipse(vec2 st, vec2 c, float a, float b, float sAng, float eAng) { vec2 ua = vec2(cos(sAng), sin(sAng)); vec2 ub = vec2(cos(eAng), sin(eAng));
float d1 = sdf_line(st, c, ua + c); float d2 = sdf_line(st, c, ub + c);
float d3 = sdf_ellipse(st, c, a, b); float r = min(a, b);
vec2 v = st - c; float ang = angle(v, vec2(1.0, 0));
if(eAng - sAng > 2.0 * PI) { return d3; }
if(ang >= sAng && ang <= eAng) { // 两个向量夹角中间的部分 float m = max(a, b); float d11 = sdf_seg(st, c, ua * m + c); float d12 = sdf_seg(st, c, ub * m + c); if(d3 >= 0.0) { return min(abs(d11 / r), min(abs(d12 / r), d3)); } return d3; }
float pa = dot(ua, v); // 求投影 float pb = dot(ub, v);
if(pa < 0.0 && pb < 0.0) { return -length(st - c) / r; }
if(d1 > 0.0 && pa >= 0.0) { vec2 va = pa * ua; float da = pow(va.x / a, 2.0) + pow(va.y / b, 2.0); if(d3 > 0.0 || da <= pow(1.0 + abs(d1 / r), 2.0)) { return -abs(d1 / r); } else { return d3; } }
if(d2 < 0.0 && pb >= 0.0) { vec2 vb = pb * ub; float db = pow(vb.x / a, 2.0) + pow(vb.y / b, 2.0); if(d3 >= 0.0 || db <= pow(1.0 + abs(d2 / r), 2.0)) { return -abs(d2 / r); } else { return d3; } } } ```
http://code.juejin.cn/pen/7105966227348488200
重复
如果要在画布上绘制多个相同图形,不必一一绘制每一个图形,要我们有一些数学手段可以运用。
简单来说,我们可以扩大st或d的值,然后对它取小数部分,比如下面的代码绘制多条直线:
glsl
void main() {
vec2 st = gl_FragCoord.xy / resolution;
float d = sdf_line(st, vec2(0), vec2(0.5));
d = abs(d);
d = fract(10.0 * d);
FragColor.rgb = stroke(d, 0.5, 0.2, 0.3) * vec3(1.0);
FragColor.a = 1.0;
}
http://code.juejin.cn/pen/7105970707141492750
类似地,我们绘制多重菱形:
glsl
void main() {
vec2 st = gl_FragCoord.xy / resolution;
float d = regular_polygon(st, vec2(0.5), 0.5, 0.0, 4);
d = abs(d);
d = fract(10.0 * d);
FragColor.rgb = stroke(d, 0.5, 0.5, 0.5) * vec3(1.0);
FragColor.a = 1.0;
}
http://code.juejin.cn/pen/7105972595907887140
如果我们扩大的是st而非d,得到的是另一种重复:
glsl
void main() {
vec2 st = gl_FragCoord.xy / resolution;
st = mix(vec2(-5.0), vec2(5.0), st);
float d = regular_polygon(fract(st), vec2(0.5), 0.55, 0.0, 4);
d = abs(d);
FragColor.rgb = stroke(d, 0.5, 0.5, 0.5) * vec3(1.0);
FragColor.a = 1.0;
}
http://code.juejin.cn/pen/7105973697403420709
根据这个原理,我们可以绘制出类似中国传统纹饰的图案,例如:
http://code.juejin.cn/pen/7105975492284514334
我们看到,利用距离场加上重复,我们可以用非常简单的代码绘制出看起来比较复杂的规律图案,这个过程需要一些数学知识和想象力,但创造图案是非常有趣的,到这一步,我们的WebGL渲染渐入佳境,在后续还有更多令人惊叹的效果可以通过寥寥几行代码实现,而这正是GPU绘图的魅力!
- Day1:用原生JS把你的设备变成一台架子鼓!
- 【零基础】充分理解WebGL(七)
- 【零基础】充分理解WebGL(六)
- 【零基础】充分理解WebGL(五)
- 冷知识:不起眼但有用的String.raw方法
- 【零基础】充分理解WebGL(四)
- 【零基础】充分理解WebGL(三)
- 【零基础】充分理解WebGL(二)
- 【零基础】充分理解WebGL(一)
- css-doodle:如何让CSS成为艺术?
- 创建合辑,将【码上掘金】作为开源项目的demo库使用
- 使用 babel 插件来打造真正的“私有”属性
- 使用 Node.js 对文本内容分词和关键词抽取
- 用信号来控制异步流程
- 设计 Timeline 时间轴来更精确地控制动画
- 简单构建 ThinkJS Vue2.0 前后端分离的多页应用
- 冷门函数之Math.hypot
- 你还在用charCodeAt那你就out了
- 巧用 currentColor 属性来实现自定义 checkbox 样式
- 在什么情况下 a === a - 1 ?