Edited at

2次元ディスタンスフィールドまとめ


はじめに

iq先生の2Dディスタンスフィールド(SDF)解説 を見ましょう。3Dの方は有名なのですが、こっち見逃してました。。。もっと早く知りたかった


ディスタンスフィールド(距離関数)とは

相手との距離を返す関数です。

glslのフラグメントシェーダーでシェイプを描くのに利用されます。


ディスタンスフィールド(距離関数)の種類

0以上か、マイマスがあるのかの、符号あり、なしの2種類あるのですが、全く用途が異なります。図形を描く場合はもっぱらSDFが使われるので、だいたいSDFを指しますが、USDFじゃないとダメな場合があるので注意が必要です。


  • 符号なしディスタンスフィールド(unsigned distance function 略してUSDF?)

原点からの距離を測るものなので絶対にマイナスになりません。ボロノイの距離関数として使えます。有名なのはマンハッタン距離を使った回路みたいな模様を作るボロノイですね。USDFはなんでも使えるので、多種多様なボロノイを作ることが出来ます。また、ディスプレイスメント、極座標変換にも利用できます。length関数になっているところは、すべて置き換え可能なので、実はかなり使えます。この関数単体では図形になりませんが、相手との距離を設定する(引く)ことで図形になります。これが後述するSDFです。


  • 符号ありディスタンスフィールド(signed distance function 略してSDF)

基本的には、USDFからある距離を引いたものになりますが、図形の辺を原点としている為、必ずしもそうではありません。(原点が1つの点にならない場合がある)なので、SDFをUSDFとして戻して使うことができない場合が多々あります。

左上 USDF 右上 SDF 相互変換できる場合(原点が1つの点)

image.png

左下 USDF 右下 SDF 相互変換できない場合(原点が縦軸の直線)


まずは基本のlength3兄弟


円(length)

length.png

https://goo.gl/KxQ9Rq

float l=length(p);

これは、いわゆるユークリッド(EUCLID)距離です。L2距離とも呼ばれています。

l=sqrt(p.x*p.x+p.y*p.y);

シンプルに、相手との距離を測る関数lengthを使うだけです。もちろん、それは円状になりますね。これがすべての基本になります。

後述のlengthN関数の中ではlength2に相当します。

length(p)と同じように円を描く距離関数にdot(p,p)がありますが、

dot.png

https://goo.gl/ZBLHrm

float l=dot(p,p);

こちらは、半径1の円を描くときには同じですが、等間隔ではないので注意が必要です。

球状の塗りをする場合にはこちらを使います。length(p)の場合は円錐状の塗りになります。

length(p)よりも軽いという特徴があります。ベタ塗りだったらdotの方が良いです。

ちなみに、これはユークリッド距離のsqrtを取ったものと同じです

l=p.x*p.x+p.y*p.y;


正方形

45度回転バージョンと通常バージョンの2パターンあります。

lengthを使っていませんが、ようは、X軸、Y軸に沿って距離を図るということですね。


  • まずは通常バージョン

square.png

https://goo.gl/VMcZwS

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l=max(abs(p.x),abs(p.y));
gl_FragColor = vec4(vec3(l), 1.0);
}

これは後述のlengthN関数の中ではlengthInfに相当します。

Nを無限にした場合ですね。 L∞距離とも呼ばれています。

チェビシェフ距離とも呼ばれています。

関数としてはmaxabs関数です


  • 正方形 45度バージョン L1距離

これは、マンハッタン距離です。

square2.png

https://goo.gl/sKx4FS

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l=abs(p.x)+abs(p.y);
gl_FragColor = vec4(vec3(l), 1.0);
}

これは後述のlengthN関数の中ではlength1に相当します。

関数としてはsumabs関数ですね


円と正方形の間 lengthN

乗数Nが増えると円の形状から段々正方形に近くなります。

尚、角丸正方形とは異なり、全体的に丸くなります。


  • N<1 星型

  • N=1 45度の正方形

  • N=2 円

  • N>2 円から正方形に近づく

イラストレーター的には、パンクと膨張です。


N=1で45度の正方形

square2.png

https://goo.gl/2rgudQ

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float lengthN(vec2 v, float n){
vec2 tmp = pow(abs(v), vec2(n));
return pow(tmp.x+tmp.y, 1.0/n);
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l=lengthN(p,1.);
gl_FragColor = vec4(vec3(l), 1.0);
}


N=2で円

length2.png

https://goo.gl/xKEh3B

つまりはlength関数と一緒です。length2 = length


Nが2より大きい(6)とNが大きいほど正方形に近づく

length6.png

https://goo.gl/tzkopj

    float l=lengthN(p,6.);


Nが1より小さい(.5)とアステロイド

length05.png

https://goo.gl/NpGq49

    float l=lengthN(p,.5);


Nが無限(Infinite)だとと正方形

square.png

といっても

    float l=lengthN(p,1./0.);

とかして、無限を入れるとシェーダーがエラーも吐かず動作しない可能性が高いので辞めましょうね。※ 環境によっては動作するかもしれません。

    float l=lengthN(p,1e20);

代わりに無限に近い巨大な数1e20 (= 10^20 = 一垓)と入れるとほぼ正方形になります。

まぁ、計算量が無駄ですので、素直に、正方形の描き方の方を使いましょう。


十字

maxがN∞だったらminはN-∞

image.png

    float l=min(abs(p.x),abs(p.y));

尚powは重いのでNの数が決まっているのであれば専用のlength6やlength8を作って使っているパターンもあります。

float length6( vec2 p ){

p = p*p*p; p = p*p;
return pow( p.x + p.y, 1.0/6.0 );
}

float length8( vec2 p ){
p = p*p; p = p*p; p = p*p;
return pow( p.x + p.y, 1.0/8.0 );
}

これはノルム(lpnorm)です。

ミンコフスキー距離とも言われています。

ノルム(n)
図形
数学的距離名
関数名

-∞
十字
-
minabs

.5
アステロイド
-

1
正方形(45度)
マンハッタン距離(L1距離)
sumabs

2

ユークリッド距離(L2距離)
length


正方形
チェビシェフ距離
maxabs

n
-
ミンコフスキー距離
lpnorm(lengthN)


多角形


多角形

l3poly.png

https://goo.gl/AdfFVm

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

const float PI = 3.141592653589793;
const float PI2 = PI * 2.;

float lPolygon(vec2 p,int n){
float a = atan(p.x,p.y)+PI;
float r = PI2/float(n);
return cos(floor(.5+a/r)*r-a)*length(p);
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l=lPolygon(p,3);
gl_FragColor = vec4(vec3(l), 1.0);
}


多角形星

starpolygon.png

https://goo.gl/xHVUqP

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

const float PI = 3.14159265359;
const float PI2 = PI *2.;

float lPolygon(vec2 p,int n){
float a = atan(p.x,p.y)+PI;
float r = PI2/float(n);
return cos(floor(.5+a/r)*r-a)*length(p);
}

mat2 mRotate(float a){
float c=cos(a);
float s=sin(a);
return mat2(c,-s,s,c);
}

float lStarPolygon(vec2 p,int n,float o){
return (lPolygon(p,n)-lPolygon(p*mRotate(PI2/float(n)/2.),n)*o)/(1.-o);
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l = lStarPolygon(p,5,.5);
gl_FragColor = vec4(vec3(l), 1.0);
}


多角形カーブ

多角形をパンクさせたり、収縮させたような図形を描く-でパンク+で収縮

polycurve.png

https://goo.gl/NegXtW

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

#define PI 3.141592653589793
#define PI2 PI * 2.

float lPolygonCurve(vec2 p,int n,float f){
float a = atan(p.x,p.y)+PI;
float r = PI2/float(n);
return cos(floor(.5+a/r)*r-a)*(length(p)+f)/(1.+f);
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l=lPolygonCurve(p,5,.5);
gl_FragColor = vec4(vec3(l), 1.0);
}


直線

ディスタンスフィールドに(永久に続く)直線はあるが、基本的に線分(2点で区切られた直線)はない。

ので、結論としては、直線をif文で条件分けして実現するのが一番てっとり早い。

他には、テッセレーションで直線を線分に分割したり、四角形の対向する2辺を極限まで小さくする方法があるが、if文で分岐するのが一番楽でしょう。

線分の仕方もいくつあるが、直線の描画方法にもいくつかの種類がある。


横軸直線

xline.png

https://goo.gl/6qC4pN

    l=abs(p.y);

    p.x=0.;

    p*=vec2(0.,1.);

    p*=mat2(0.,0.,0.,1.);


縦軸直線

yline.png

https://goo.gl/C4y5To

    l=abs(p.x);

    p.y=0.;

    p*=vec2(1.,0.);

    p*=mat2(1.,0.,0.,0.);


任意角度直線

linerotate.png

https://goo.gl/2vbVh3

    p=vec2(p.x*cos(t)+p.y*sin(t));


任意ベクトル方向直線

vectline.png

https://goo.gl/zvTdVP

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float cross(vec2 v1, vec2 v2) {return v1.x*v2.y - v1.y*v2.x;}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=abs(cross(p,normalize(vec2(3.,4.))));
gl_FragColor = vec4(vec3(l), 1.0);
}


片側直線

absline.png


任意ベクトル

片方しか要らない場合はabsを外す

https://goo.gl/zuHgCG

    l=cross(p,normalize(vec2(3.,4.)));

dotにすると90度回転、このまま使う場合もある

    l=dot(p,normalize(vec2(3.,4.)));

実際に使うときはマイナスの時は NaN,Infinite or 1として使う


  • NaN

l=l<0.?sqrt(l):l;


  • 1

l=l<0.?1.:l;


  • 1(三項演算子ではなく、stepを使ったバージョン、どっちが早い?)

l=step(l,0.) + step(0.,l)*l;


  • Infinite

l=l<0.?1./0.:l;


    l=p.y;


    l=p.x;


任意方向

l=p.x*cos(t)+p.y*sin(t);


線分

ディスタンスフィールドは、極座標系なので、基本的に線分は存在しない。

ので、以下の3つの方法のどれかを選択します。

イラストレーターの線の先端に相当

実装が簡単なので、glslでは丸形先端が使われることが多いですが、線分と線分の接合部分がおかしくなりがちなので、止めた方がいいです。


  • 90度反転した直線のディスタンスフィールドでマスク(バット先端)

  • 細長いカプセルで、太さを最小としたもの(丸形先端)

  • 細長い長方形で、太さを最小としたもの(突出先端)


バット先端

lineseg.png

https://goo.gl/oFpkgj


  • 90度回転させた直線のディタンスフィールドをもう1個使ってマスクする方法

90度回転させた直線のディタンスフィールドは外積の代わりに内積を使えばいい

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float cross(vec2 v1, vec2 v2) {return dot(v1,vec2(v2.y,-v2.x));}

float lLine(vec2 p,vec2 v1,vec2 v2){
vec2 v = v2 - v1;
vec2 d = normalize(v); //direction
p-=v1;
return 1.-(1.-abs(cross(p,d)))*step(0.,length(v)*.5-abs(dot(p-v*.5,d)));
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=lLine(p,vec2(-.8,-0.8),vec2(.8,.8));
gl_FragColor = vec4(vec3(l), 1.0);
}


  • dotだけで作りif文で分岐するバージョン

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float cross(vec2 v1, vec2 v2) {return dot(v1,vec2(v2.y,-v2.x));}

float lLine( vec2 p,vec2 v1, vec2 v2){
vec2 v = v1 - v2;
vec2 p2p = p - v2;
vec2 p1p = p - v1;
vec2 pd = normalize(vec2(v.y, -v.x));
float proj = dot(pd, -p1p);
float pr1 = dot( v, p2p);
float pr2 = dot(-v, p1p);
return (pr1 > 0.0 && pr2 > 0.0) ? abs(proj): 0.;
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=lLine(p,vec2(-.8),vec2(.8));
gl_FragColor = vec4(vec3(l), 1.0);
}


丸形先端

liner.png

https://goo.gl/QdMY9m

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float cross(vec2 v1, vec2 v2) {return v1.x*v2.y - v1.y*v2.x;}

float lLineRound(vec2 p, vec2 v1, vec2 v2) {
p -= v1;
vec2 v = v2-v1;
float t = dot(p, normalize(v));
if (t<0.0) {
return length(p);
} else if (t>length(v)) {
return length(p-v);
} else {
return abs(cross(p, normalize(v)));
}
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=lLineRound(p,vec2(-.8),vec2(.8));
gl_FragColor = vec4(vec3(l), 1.0);
}


参考


楕円

ellipse.png

https://goo.gl/zy5mW6

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float lEllipse(vec2 p, vec2 r, float s) {
return (length(p / r) - s);
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=lEllipse(p,vec2(.5,.25),1.);
gl_FragColor = vec4(vec3(l), 1.0);
}


長方形

rect.png

https://goo.gl/fkTvUm

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float lRect(vec2 p, vec2 size) {
vec2 d = abs(p) - size;
return min(max(d.x, d.y), 0.0) + length(max(d,0.0));
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=lRect(p,vec2(.5,.25));
gl_FragColor = vec4(vec3(l), 1.0);
}


角丸長方形

rrect.png

https://goo.gl/Kgjpo5

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float lRoundRect(vec2 p, vec2 size, float radius) {
vec2 d = abs(p) - size + radius;
return min(max(d.x, d.y), 0.0) + length(max(d,0.0))- radius;
}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=lRoundRect(p,vec2(.5,.25),.1);
gl_FragColor = vec4(vec3(l), 1.0);
}


十字星

ltwincle.png

https://goo.gl/54RVfF

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float lTwincle(vec2 p){return abs(p.x) * abs(p.y);}

void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
float l;
l=lTwincle(p);
gl_FragColor = vec4(vec3(l), 1.0);
}


7セグメント(デジタル)数字

image.png

precision mediump float;

uniform vec2 m; // mouse
uniform float t; // time
uniform vec2 r; // resolution
uniform sampler2D smp; // prev scene

float seg(vec2 p,float s,float k){p=abs(p*k);return max(s+p.x,s*.5+p.y+p.x);}
float seg7(vec2 p,int n,float s,float k){
float l=5.,w=s*.5;
vec2 q=p.yx,h=vec2(s,0.),v=vec2(-w,w);
l = (n!=1 && n!=4 ?min(seg(q-h,s,k),l):l);
l = (n!=1 && n!=2 && n!=3 && n!=7?min(seg(p-v,s,k),l):l);
l = (n!=5 && n!=6 ?min(seg(p-w,s,k),l):l);
l = (n!=0 && n!=1 && n!=7 ?min(seg(q ,s,k),l):l);
l = (n==0 || n==2 || n==6 || n==8?min(seg(p+w,s,k),l):l);
l = (n!=2 ?min(seg(p+v,s,k),l):l);
l = (n!=1 && n!=4 && n!=7 ?min(seg(q+h,s,k),l):l);
return l;
}
void main(void){
vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);

float l=seg7(p,int(mod(t,10.)),.8,1.3);
gl_FragColor = vec4(vec3(l), 1.0);
}


関連