理解了向量夹角,极坐标,对称性,线段的SDF函数之后,常见多边形基本思路都是一致的,本文开始对一些带曲线的形状进行推导, 这些形状都是有两个不同大小的圆组成。 本节的Shader都在
- shadertoy: www.shadertoy.com/view/4cGGWR
Vesica
- geogebra: www.geogebra.org/classic/xe2...
关于vesica形状,网上有做一个一些判断,但是我觉得只需要判断 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P 点到 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 点的距离,与 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P 点到 <math xmlns="http://www.w3.org/1998/Math/MathML"> G G </math>G 点的距离就行了。 代码如下
glsl
float sd_vesica(vec2 P, float r, float d)
{
P = abs(P);
vec2 PB = vec2(-d, 0.0);
float h = sqrt(r*r - d*d);
vec2 PG = vec2(0.0, h);
return min(
distance(P, PB) - r,
distance(P, PG)
);
}
Uneven Capsule
- geobebra: www.geogebra.org/classic/vcx...
- shadertoy: www.shadertoy.com/view/4cGGWR
假定有圆c1, 圆心位于中心,半径为r1, 圆c2, 圆心位于(0, h), 半径为r2. 最关键是找到上图中蓝色的v1向量,v2与v1垂直, v1的角度 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ 有 <math xmlns="http://www.w3.org/1998/Math/MathML"> s i n ( θ ) = ( r 1 − r 2 ) h sin(\theta)=\cfrac{(r1-r2)} h </math>sin(θ)=h(r1−r2)
于是有v1向量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( s i n ( θ ) , c o s ( θ ) ) (sin(\theta), cos(\theta)) </math>(sin(θ),cos(θ))
有v2向量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( − c o s ( θ ) , s i n ( θ ) ) (-cos(\theta),sin(\theta)) </math>(−cos(θ),sin(θ))
根据向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> O P → \overrightarrow{OP} </math>OP 在 v2上的投影k的大小, 可以划分出3个区域。 最后得到代码
glsl
float sdf_uneven_capsule(vec2 p, float r1, float r2, float h) {
p.x = abs(p.x);
float sin_theta = (r1-r2) / h;
float normalize_y = sin_theta * 1.0;
float normalize_x = sqrt(1.0 - (normalize_y * normalize_y));
float cos_theta = normalize_x;
vec2 v2 = vec2(-normalize_y, normalize_x);
vec2 v = vec2(normalize_x, normalize_y);
float k = dot(p, vec2(v2));
// area1
if( k < 0.0 ) return length(p) - r1;
// area3
if( k > cos_theta*h ) return length(p-vec2(0.0,h)) - r2;
// area2
return dot(p, v ) - r1;
}
Egg
- geogebra: www.geogebra.org/classic/ft8...
如何画一个蛋参考上图,点A的坐标为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( d , 0 ) (d, 0) </math>(d,0) 半径为 <math xmlns="http://www.w3.org/1998/Math/MathML"> r r </math>r . 求解一个蛋的SDF,主要需要确定上图的三个点 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B <math xmlns="http://www.w3.org/1998/Math/MathML"> D D </math>D <math xmlns="http://www.w3.org/1998/Math/MathML"> O O </math>O 为圆心做的图。 通过 <math xmlns="http://www.w3.org/1998/Math/MathML"> D F DF </math>DF 左右边判判断 选择 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 还是 <math xmlns="http://www.w3.org/1998/Math/MathML"> D D </math>D ,通过 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P 的 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y 正负号判断选择 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 还是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O O </math>O 点,最后代码有
glsl
float sdf_mossegg(vec2 P, float r, float d )
{
P.x = abs(P.x);
vec2 B = vec2(-d, 0.);
float hh = r-d;
vec2 D = vec2(0, hh);
if (P.y < 0.) {
return length(P) - hh;
} else {
float t = cross2(P-B, D-B);
if (t < 0.0) {
float tt = sqrt(hh *hh + d * d);
float rr =r - tt;
return distance(P, D) - rr ;
} else {
return distance(P, B) - d - hh;
}
}
}
Moon
- geogebra: www.geogebra.org/classic/mq8...
如何画一个月亮参考上图,点B的坐标为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( d , 0 ) (d, 0) </math>(d,0) , 半径为 <math xmlns="http://www.w3.org/1998/Math/MathML"> r 2 r2 </math>r2 . 中心圆的半径为 <math xmlns="http://www.w3.org/1998/Math/MathML"> r 1 r1 </math>r1 . 上图黑色加粗部分为d,r1,r2. 对于月亮的距离划分为三个区域, 分别是求P到B,C, D 三个点的距离。区域判断主要是通过向量叉积判断方向可得。 最关键是求出 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C 点的坐标 。 这个问题可以转化为两个圆的方程求解 两个圆的方程分别是:
- 圆A,圆心 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 0 , 0 ) (0, 0) </math>(0,0) ,半径 <math xmlns="http://www.w3.org/1998/Math/MathML"> r 1 r_1 </math>r1 : <math xmlns="http://www.w3.org/1998/Math/MathML"> x 2 + y 2 = r 1 2 x^2 + y^2 = r_1^2 </math>x2+y2=r12
- 圆B,圆心 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( d , 0 ) (d, 0) </math>(d,0) ,半径 <math xmlns="http://www.w3.org/1998/Math/MathML"> r 2 r_2 </math>r2 : <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x − d ) 2 + y 2 = r 2 2 (x - d)^2 + y^2 = r_2^2 </math>(x−d)2+y2=r22
为解这两个方程,首先可以通过从一个方程中减去另一个来消去 <math xmlns="http://www.w3.org/1998/Math/MathML"> y 2 y^2 </math>y2 ,得到 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 的关系式。 从上面两个方程中减得: <math xmlns="http://www.w3.org/1998/Math/MathML"> x 2 − ( x − d ) 2 = r 1 2 − r 2 2 x^2 - (x-d)^2 = r_1^2 - r_2^2 </math>x2−(x−d)2=r12−r22
展开后得到: <math xmlns="http://www.w3.org/1998/Math/MathML"> x 2 − ( x 2 − 2 x d + d 2 ) = r 1 2 − r 2 2 x^2 - (x^2 - 2xd + d^2) = r_1^2 - r_2^2 </math>x2−(x2−2xd+d2)=r12−r22
这可以简化为 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 的一个线性方程: <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 x d − d 2 = r 1 2 − r 2 2 2xd - d^2 = r_1^2 - r_2^2 </math>2xd−d2=r12−r22
从而有效的 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 为: <math xmlns="http://www.w3.org/1998/Math/MathML"> x = r 1 2 − r 2 2 + d 2 2 d x = \frac{r_1^2 - r_2^2 + d^2}{2d} </math>x=2dr12−r22+d2
之后我们可以将 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 的值代回任一圆的方程中来求解 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y 。将 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 代入圆A的方程: <math xmlns="http://www.w3.org/1998/Math/MathML"> y 2 = r 1 2 − x 2 y^2 = r_1^2 - x^2 </math>y2=r12−x2
最后有代码
glsl
float sdf_moon(vec2 P, float r1, float r2, float d )
{
P.y = abs(P.y);
float a = (r1*r1 - r2*r2 + d*d)/(2.0*d);
float b = sqrt(max(r1*r1-a*a,0.0));
vec2 PointC = vec2(a, b);
vec2 PointB = vec2(d, 0.);
vec2 PointD = vec2(0., 0.);
vec2 VectorCB = PointB - PointC;
vec2 VectorDC = PointC - PointD;
vec2 VectorCP = P - PointC;
vec2 VectorDP = P - PointD;
float t1 = cross2(VectorCB, VectorCP);
float t2 = cross2(VectorDC, VectorDP);
if (t1 > 0.0 && t2 < 0.0) {
return distance(P, PointC);
}
return max(
r2 - distance(P, PointB),
length(P) - r1
);
}
Heart
- geogebra: www.geogebra.org/classic/hep...
如何画一个心参考上图, 心外部的距离可以变成线段 <math xmlns="http://www.w3.org/1998/Math/MathML"> E C EC </math>EC 和圆 <math xmlns="http://www.w3.org/1998/Math/MathML"> D D </math>D 的SDF。心内部的距离在向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> B C BC </math>BC 左侧为圆的距离, 右侧为距离 <math xmlns="http://www.w3.org/1998/Math/MathML"> B B </math>B 点或者线段 <math xmlns="http://www.w3.org/1998/Math/MathML"> E C EC </math>EC 的最短距离。最后有一下代码
glsl
float sd_heart( in vec2 P, float r )
{
P.x = abs(P.x);
vec2 PointB = vec2(0., r);
vec2 PointA = vec2(r, 0.0);
vec2 PointC = vec2(0.5*r, 0.5*r);
vec2 PointD = vec2(0.25 * r, 0.75 *r );
vec2 VectorBA = PointA - PointB;
vec2 VectorBP = P - PointB;
float t = cross2(VectorBA, VectorBP);
if (t > 0.0) {
return distance(P, PointD) - 0.25 * r * sqrt(2.);
}
float hh = clamp(dot(P, PointC) / dot(PointC, PointC), 0., 1.);
return min(
distance(P, PointB),
length(P-hh*PointC)
) * sign(P.x-P.y);
}