カイワレスタイル

ゲーム、アニメ、プログラム、興味のあることをツラツラと。

Boze入門

この記事はBoze Advent Calendar 2020の2日目の記事です(遅くなって申し訳ありません)

https://qiita.com/advent-calendar/2020/boze2020

前日は、オリジナルの三日坊主さんの「Bozeって何?」でした。
とても哲学的で良い記事でしたね。
前日の記事で"Boze"が何かわかったと思いますので、今回はより実践的な"Boze"について書いてみたいと思います。

Boze入門

私の"Boze"は、レイマーチングを用いた"Boze"です。
早速ですが、最低限のレイマーチングによる"Boze"のサンプルです。 neort.io

最低限ですが、結構な長さがあるので、要点部分だけ抜粋して解説します。

レイマーチングとは

レイマーチングとは、その名の通り、レイ(光)をマーチング(行進)させてレンダリングする手法です。

f:id:kaiware007:20201203231434p:plain
↑親の顔より見たレイマーチングの図

レイは、各図形の距離関数からレイとの距離を計算し、その距離の長さだけレイを進めて、再度距離を求めて…というのを繰り返していく感じです。
距離がものすごく小さくなったら図形に接触していると判断します。
ソース上では、traceRayがそれにあたります。

// 距離関数をマッピングする処理(空間の中心にBozeがあるだけ)
surface map(vec3 p)
{
    surface result = SURF_NOHIT(1e5);
    
    float ms = sin(iTime) * 0.5 + 0.5;

    // boze
    surface boze = sdBoze(p, vec3(1), ms);
    
    result = opU(result, boze);
    
    return result;
}

// レイを進ませる処理
surface traceRay(in vec3 origin, in vec3 direction, float dist, out vec3 pos) {
    float t = 0.0;
    
    pos = origin;

    int count = 0;
    surface hit;
    float d;
    
    for (int i = 0; i < MAX_MARCH; i++) {
        hit = map(pos);
        d = hit.dist;
        
        if (d <= EPS || d >= MAX_DIST) {
            break;
        }

        t += d;
        pos = origin + direction * t;
        count++;        
    }

    hit.dist = t;
    hit.count = count;

    pos = origin + direction * t;
        
    if(hit.isHit)
    {
        // Lighting
        vec3 normal = norm(pos);

        vec3 lightDir = normalize(vec3(cos(iTime), 1, sin(iTime)));
        vec3 lightColor = vec3(1.5);
        
        float NoL = saturate(dot(normal, lightDir));
        
        vec3 ambientColor = vec3(0.1);
        
        hit.albedo.rgb *= NoL * lightColor +  ambientColor;
    }
    
    if(d <= EPS){
        hit.isHit = true;
        return hit;
    }else{
        
        hit.isHit = false;
        return hit;
    }
}

// レイを進ませて、図形と衝突してなかったら背景色にする処理
vec3 render(vec3 p, vec3 ray, vec2 uv)
{
    vec3 pos;
    surface mat = traceRay(p, ray, 0., pos);
    
    vec3 col = vec3(0,0,0);
    vec3 sky = vec3(0.3);
    
    col = mat.isHit ? mat.albedo.rgb : sky;
    
    return col;
    
}

レイが図形に接触したら、図形の表面の色を取得する必要がありますが、"Boze"の場合、肌、目、口とそれぞれ色が違います。
そのため、各図形から距離を取得するときに、一緒に色も返すように戻り値を構造体にしています。

// 距離関数からの情報を格納する構造体
struct surface {
    float dist; // 距離
    vec4 albedo;    // 色 
    int count;  // 進んだ回数
    bool isHit; // 衝突フラグ
};

Bozeの構成要素

"Boze"は、基本的な距離関数の組み合わせでできています。
下記に"Boze"で使っている基本的な距離関数一覧を紹介します。

sdCappedTorus:欠けたトーラス(リング)

f:id:kaiware007:20201201212032p:plain

float sdCappedTorus(in vec3 p, in vec2 sc, in float ra, in float rb)
{
  p.x = abs(p.x);
  float k = (sc.y*p.x>sc.x*p.y) ? dot(p.xy,sc) : length(p.xy);
  return sqrt( dot(p,p) + ra*ra - 2.0*ra*k ) - rb;
}

sdEllipsoid:楕円球

f:id:kaiware007:20201201212036p:plain

float sdEllipsoid( vec3 p, vec3 r )
{
    float k0 = length(p/r);
    float k1 = length(p/(r*r));
    return k0*(k0-1.0)/k1;
}

sdCapsule:カプセル

f:id:kaiware007:20201201212419p:plain

float sdCapsule(vec3 p, vec3 a, vec3 b, float r)
{
    vec3 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba*h) - r;
}

sdRoundBox:角丸立方体

f:id:kaiware007:20201201212025p:plain

float sdRoundBox(vec3 p, vec3 size, float r)
{
    return length(max(abs(p) - size * 0.5, 0.0)) - r;
}

sdRoundedCylinder:角丸円柱

f:id:kaiware007:20201201212028p:plain

float sdRoundedCylinder( vec3 p, float ra, float rb, float h )
{
  vec2 d = vec2( length(p.xz)-2.0*ra+rb, abs(p.y) - h );
  return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rb;
}

画像出典: distance functions by iq www.iquilezles.org

"Boze"の各パーツ

頭部

なめらかな凸型の頭部は、カプセルと角丸円柱を融合させて作っています。 上部がカプセルで、下部が角丸円柱です。

 // head
    float d = sdCapsule(p, vec3(0,0.05,0), vec3(0, 0.11, 0), 0.125);
    float d1 = sdRoundedCylinder(p + vec3(0,0.025,0), 0.095, 0.05, 0.0);

    d = smin(d, d1, 0.1);

2つの図形をSmooth minというなめらかに補完する関数を使って融合させています。

float smin( float d1, float d2, float k ) {
    float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
    return mix( d2, d1, h ) - k*h*(1.0-h); 
}

www.iquilezles.org

耳は、欠けたトーラスを90度ほど傾けて作っています。

float sdEar(vec3 p)
{
    p = rotate(p, RAD90+0.25, vec3(0,0,1));    
    return sdCappedTorus(p + vec3(0.05, 0.175, 0), vec2(sin(0.7),cos(0.7)), 0.03, 0.01);
}

また、顔のパーツは口以外は左右対称なので、レイの座標をX座標を絶対値にすることで、空間が左右対称になって距離関数を削減しています。

    vec3 mxp = vec3(-abs(p.x), p.yz);
    
    // ear
    float d2 = sdEar(mxp);
    d = opU(d, d2);

口は楕円球を使っていますが、口角を若干上げるために楕円球の中心からX方向に離れるほど回転させて歪めています。

float sdMouse(vec3 p, float ms)
{
    vec3 q = opBendXY(p, 2.0);
    ms += 0.00001;
    return sdEllipsoid(q - vec3(0,0,0.2), vec3(0.035, 0.01 * ms,0.05 * ms));
}

vec3 opBendXY(vec3 p, float k)
{
    float c = cos(k*p.x);
    float s = sin(k*p.x);
    mat2  m = mat2(c,-s,s,c);
    return vec3(m*p.xy,p.z);
}

目と眉毛

目は見たまんまですがカプセル、眉毛は角丸をすごく細くして作っています。

float sdEyeBrow(vec3 p)
{
    const float x = 0.05;
    p = opBendXZ(p + vec3(0.02,0,-0.02), -6.5);
    return sdRoundBox(p + vec3(0.005, -0.14,-0.11), vec3(0.003,0.0025,0.05), 0.001);
}
    // eye
    float d4 = sdCapsule(mxp, vec3(-EYE_SPACE, 0.06, 0.13), vec3(-EYE_SPACE, 0.08, 0.125), 0.0175);
    surface eye = SURF_BLACK(d4);

    // eyebrows
    float d9 = sdEyeBrow(mxp);
    eye.dist = opU(eye.dist, d9);

パーツ単位で一番複雑なのが、頬のうねうねの線です。
元のデザインでは手書きでうねうねさせていた線ですが、レイマーチングでは3本のカプセルを歪めながら連結して再現しています。

float sdCheep(vec3 p)
{    
    const float x = 0.05;
    const float z = -0.175;
    const float r = 0.0045;
    const float rb1 = 100.;
    
    p = rotate(p, M_PI * -0.6 * (p.x - x), vec3(-0.2,0.8,0));
    
    float d = sdCapsule(opBendXY(p + vec3(x, -0.01, z), rb1), vec3(-0.005,0.0,0.0), vec3(0.005, 0., 0.001), r);
    float d1 = sdCapsule(opBendXY(p + vec3(x+0.01, -0.01, z), 200.0), vec3(-0.0026,0.0,0), vec3(0.0026, 0., 0), r);
    float d2 = sdCapsule(opBendXY(p + vec3(x+0.019, -0.015, z), -rb1), vec3(-0.01,0.0,-0.01), vec3(0.0045, 0., 0.0), r);
    
    return opU(opU(d, d1), d2);
}

パーツの合成

前述の顔のパーツを合成します。
距離関数での図形の合成は、以下のとおりです。

// 和
float opUnion( float d1, float d2 ) {  min(d1,d2); }

// 除
float opSubtraction( float d1, float d2 ) { return max(-d1,d2); }

// 交差(論理積)
float opIntersection( float d1, float d2 ) { return max(d1,d2); }

まず、顔と耳と頬をopUnionで合成します。 目、口、眉毛は、凹んでいるのでopSubtractionで削除します。
これで完成!

f:id:kaiware007:20201201233447p:plain

君だけの"Boze"を作ろう!