距離関数のfold(折りたたみ)による形状設計

レイマーチング(別名 Sphere Tracing)とは、距離関数と呼ばれる数式で定義したシーンに対して、レイの衝突判定を行って絵を出す手法です。

この距離関数に対し、fold(折りたたみ)の操作を行うと、万華鏡のような美しい形状や、フラクタルのような複雑な形状の設計が可能です。

先日のTokyoDemoFest2017でも、このfoldを用いた作品を投稿しました。

この記事では、距離関数のfoldについて、解説していきます。

2Dのfold

分かりやすさのために、まずは2Dの例から説明します。

2Dのfoldの分かりやすい例は「鏡文字」です。

アルファベットのGをY軸中心に折りたたんだ鏡文字の2Dシェーダのサンプルを作りました。

Gの鏡文字

このようなY軸中心のfold(Y軸中心 == X方向へのfold)は、単純にxを絶対値することにより実現できます。

絶対値をとることで、Xの負の方向の領域が正の方向の領域と一致して鏡文字になります。

vec2 foldX(vec2 p) {
    p.x = abs(p.x);
    return p;
}

foldXの使用方法です。距離関数の引数pにfoldXを適用すればOKです。

p = foldX(p);// Y軸方向を中心に空間(p)を折りたたむ
p -= vec2(0.4, 0.0);// 右方向に平行移動
float color = sign(dCharG(p));// 距離関数の符号で色分け

3Dのfold

次は3Dのfoldです。

このように箱をZ軸に時計回りに回転させたものをYZ平面にfoldすると、距離関数としては1つの箱なのに、左右2つに枝分かれさせることができます。

YZ平面にfoldしたBox

前の例では2DだったのでY軸に対するfoldでしたが、今回は3DなのでYZ平面に対するfoldになっています。 foldの中心が軸から面になりましたが、処理的には2Dと変わりません。 foldXの中身も一緒で、引数と返り値の型がvec2からvec3になったことが変更点です。

vec3 foldX(vec3 p) {
    p.x = abs(p.x);
    return p;
}

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

float dTree(vec3 p) {
    vec3 size = vec3(0.1, 1.0, 0.1);
    float d = sdBox(p, size);
    p = foldX(p);
    p.y -= 0.1;
    p.xy *= rotate(-1.2);
    d = min(d, sdBox(p, size));
    return d;
}

再帰的なfold

このfoldを再帰的に適用すると、フラクタル図形ができます。

再帰的なfold

これがGLSLの距離関数です。

ループの末尾でpを更新することで、再帰的にfoldを適用しています。

float dTree(vec3 p) {
    float scale = 0.8;
    vec3 size = vec3(0.1, 1.0, 0.1);
    float d = sdBox(p, size);
    for (int i = 0; i < 7; i++) {
        vec3 q = foldX(p);
        q.y -= size.y;
        q.xy *= rotate(-0.5);
        d = min(d, sdBox(p, size));
        p = q;
        size *= scale;
    }
    return d;
}

通常、フラクタル図形を表現するためには再帰関数が必要ですが、距離関数を用いればループで十分表現できるというのが興味深いポイントですね。 たとえば、dTree の中には、sdBoxがたったの2回しか登場していません。再帰的に foldX を適用することで、Boxを無数に複製しています。

回転のfold Polar Mod

  • 2023-05-02 追記
    • foldは境界が連続し、modは境界が分断するという違いがあるため、呼び方を区別するべきでした
    • 詳しくgazさんのSDF for raymarching (距離関数のスキル)の記事を参照してください
    • 「回転のfold」から「Polar Mod」に呼び方と関数名を訂正しました

foldとは別系統のテクニックになりますが、関連するテクニックとして Polar Mod を紹介します。

通常のmod( opRep )ではXYZなどの移動に対するReputationとなりますが、Polar Mod( pmod )では回転に対してのReputationとなります。

pmodの実装は@gaziya5さんのDE used foldingからお借りしました。

dTreeの木のような形をうまく調整し、Z軸方向に360°を1/6ずつ回転させるpmodを適用すると「Fusioned Bismuth」に登場した雪の結晶のような形状を得られます。

Fusioned Bismuth - 雪の結晶

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

// https://www.shadertoy.com/view/Mlf3Wj
vec2 pmod(in vec2 p, in float s) {
    float a = PI / s - atan(p.x, p.y);
    float n = PI2 / s;
    a = floor(a / n) * n;
    p *= rotate(a);
    return p;
}

float dTree(vec3 p) {
    float scale = 0.6 * saturate(1.5 * sin(0.05 * time));
    float width = mix(0.3 * scale, 0.0, saturate(p.y));
    vec3 size = vec3(width, 1.0, width);
    float d = sdBox(p, size);
    for (int i = 0; i < 10; i++) {
        vec3 q = p;
        q.x = abs(q.x);
        q.y -= 0.5 * size.y;
        q.xy *= rotate(-1.2);
        d = min(d, sdBox(p, size));
        p = q;
        size *= scale;
    }
    return d;
}

float dSnowCrystal(inout vec3 p) {
    p.xy = pmod(p.xy, 6.0);
    return dTree(p);
}

Polar Modの別例

この pmod はUFO風の形状にも利用しています。 たった4つのBoxにmodpmod を適用しただけなのに、それなりに雰囲気を出すことができたと思っています。

Fusioned Bismuth - UFO風の形状

#define opRep(p, interval) (mod(p, interval) - 0.5 * interval)
#define opRepLimit(p, interval, limit) (mod(clamp(p, -limit, limit), interval) - 0.5 * interval)

float dWing(in vec3 p) {
    float t = time;
    float l = length(p.xz);
    float fusion = gauss(time * 2.0);

    float a = 0.1 + 0.06 * (1.0 + sin(PI * t + l));
    float b = min(0.2 * t, 10.0) * gauss(l) + 0.1 * fusion * hWave(p.xz, t);
    p.y += -b + 15.0;

    vec3 p1 = p;
    p1.xz = opRepLimit(p.xz, 1.0, 20.0);

    vec3 p2 = p;
    p2 = opRep(p, 0.5);

    float d =   sdBox(p1, vec3(0.2 + a * 3.0, 12.0 - a,       0.2 + a));
    d = min(d,  sdBox(p1, vec3(0.4 - a,       13.0 - 4.0 * a, 0.1 + a)));
    d = max(d, -sdBox(p1, vec3(0.3 - a,       14.0 - 4.0 * a, a)));
    d = max(d, -sdBox(p2, vec3(0.8 * a, 1.0 - a, 0.8 * a)));
    return d;
}

float dUfo(inout vec3 p) {
    float t = max(time * 0.5, 1.0);
    float t1 = floor(t);
    float t2 = t1 + easeInOutCubic(t - t1);

    p.xz = pmod(p.xz, min(t2, 10.0));
    p.z -= 0.5;

    float d = dWing(p);
    return d;
}

一般化されたfold

任意の法線 n を持った面に対する一般化されたfoldがSyntopiaに紹介されています。興味のある方は、見てみると良いでしょう。

absによるfoldでは、座標軸に垂直な特殊な平面に限定されていましたが、これで自由な向きにfoldができます!

p -= 2.0 * min(0.0, dot(p, n)) * n;

まとめ

距離関数のfoldについて、実例を踏まえて紹介しました。

foldは直感的に理解することが難しく、使いこなすのも大変ですが、かなり強力なテクニックだと思います!

もしこの記事がお役に立てたのなら幸いです。

comments powered by Disqus

gam0022.net's Tag Cloud