LoginSignup
2
2

More than 3 years have passed since last update.

GLSLSandboxでレイマーチング

Last updated at Posted at 2020-01-16

経緯

シェーダーを勉強しようと思い、色々調べていたらレイマーチングというものを知った。
下記のサイトでレイマーチングについて学んだ後、
取り敢えず何か作りたかったので、簡単そうな鉄火巻きを作ることにした。

使用したもの

GLSL Sandbox 

目標

1.複数オブジェクトの表示
2.回転や平行移動の使用
3.陰影の表示(ソフトシャドウ)
を実現することを目標とした。

コード

precision mediump float;
uniform float time;
uniform vec2  mouse;
uniform vec2  resolution;

//カメラ
const float PI = 3.14159265;
const float angle = 60.0;
const float fov = angle * 0.5 * PI / 180.0;
const vec3 cPos = vec3(0.0, 2.0, 5.2);
const vec3 cDir = vec3(0.0, -1.0, -2.0);

//光
const vec3 lightPos = vec3(0.0, 5.0, 5.0);
const vec3 lightDir = vec3(0.0, 1.0, 1.0);

//色
vec3 noriColor = vec3(0.2, 0.2, 0.2);
vec3 riceColor = vec3(1.0, 1.0, 1.0);
vec3 tunaColor = vec3(1.0, 0.0, 0.0);
vec3 wPlainColor = vec3(0.7, 0.7, 0.7);
vec3 bPlainColor = vec3(0.3, 0.3, 0.3);

//平行移動
vec3 translate(vec3 p, vec3 t) {
    mat4 m = mat4(vec4(1.0, 0.0, 0.0, 0.0),
                  vec4(0.0, 1.0, 0.0, 0.0),
                  vec4(0.0, 0.0, 1.0, 0.0),
                  vec4(-t.x, -t.y, -t.z, 1.0));

    return (m * vec4(p, 1.0)).xyz;
}

//任意軸回転
vec3 rotate(vec3 p, float angle, vec3 axis){
    vec3 a = normalize(axis);
    float s = sin(angle);
    float c = cos(angle);
    float r = 1.0 - c;
    mat3 m = mat3(
        a.x * a.x * r + c,
        a.y * a.x * r + a.z * s,
        a.z * a.x * r - a.y * s,
        a.x * a.y * r - a.z * s,
        a.y * a.y * r + c,
        a.z * a.y * r + a.x * s,
        a.x * a.z * r + a.y * s,
        a.y * a.z * r - a.x * s,
        a.z * a.z * r + c
    );
    return m * p;
}

//立方体の距離関数
float sdBox( vec3 p, vec3 b )
{
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

//円柱の距離関数
float sdCappedCylinder( vec3 p, float h, float r )
{
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(h,r);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

//平面の距離関数
float sdPlane( vec3 p, vec4 n )
{
  return dot(p,n.xyz) + n.w;
}

//描画関数
float distanceFunc(vec3 p) {
    //平行移動と回転
    vec3 q = translate(p, vec3(0.0, 0.5, 0.0));
    vec3 r = rotate(q, radians(20.0 * time), vec3(0.0, 1.0, 0.0));
    vec3 s = rotate(r, radians(70.0), vec3(1.5, 0.5, 0.5));

    //オブジェクトの描画
    float tuna = sdBox(s, vec3(0.05, 0.53, 0.05));
    float rice = sdCappedCylinder(s, 0.15, 0.50);
    float nori = sdCappedCylinder(s, 0.17, 0.50);

    //床の描画
    float plain = sdPlane(p, vec4(0.0, 1.0, 0.0, 1.0));

    //結合
    return min(min(min(nori, rice), tuna), plain);
}

//色の計算
vec3 calcColor(vec3 p) {
    //上と同様
    vec3 q = translate(p, vec3(0.0, 0.5, 0.0));
    vec3 r = rotate(q, radians(20.0 * time), vec3(0.0, 1.0, 0.0));
    vec3 s = rotate(r, radians(70.0), vec3(1.5, 0.5, 0.5));

    float tuna = sdBox(s, vec3(0.05, 0.55, 0.05));
    float rice = sdCappedCylinder(s, 0.15, 0.50);
    float nori = sdCappedCylinder(s, 0.17, 0.50);

    float plain = sdPlane(p, vec4(0.0, 1.0, 0.0, 1.0));

    //距離関数が最小のものの色を描画
    float minDist = tuna;
    vec3 color = tunaColor;

    if(rice < minDist) {
        minDist = rice;
        color = riceColor;
    }
    if(nori < minDist) {
        minDist = nori;
        color = noriColor;
    }

    if(plain < minDist) {
        minDist = plain;
        color = bPlainColor;

        //チェック柄にする
        float u = 1.0 - floor(mod(p.x, 2.0));
        float v = 1.0 - floor(mod(p.z, 2.0));
        if((u == 1.0 && v < 1.0) || (u < 1.0 && v == 1.0)){
            color = wPlainColor;
        }
    }

    return color;
}

//法線
vec3 getNormal(vec3 p){
    float d = 0.0001;
    return normalize(vec3(
        distanceFunc(p + vec3(  d, 0.0, 0.0)) - distanceFunc(p + vec3( -d, 0.0, 0.0)),
        distanceFunc(p + vec3(0.0,   d, 0.0)) - distanceFunc(p + vec3(0.0,  -d, 0.0)),
        distanceFunc(p + vec3(0.0, 0.0,   d)) - distanceFunc(p + vec3(0.0, 0.0,  -d))
    ));
}

//影
float genShadow(vec3 ro, vec3 rd){
    float h = 0.0;
    float c = 0.001;
    float r = 1.0;
    float shadowCoef = 0.5;
    for(float t = 0.0; t < 50.0; t++){
        h = distanceFunc(ro + rd * c);
        if(h < 0.001){
            return shadowCoef;
        }
        r = min(r, h * 16.0 / c);
        c += h;
    }
    return 1.0 - shadowCoef + r * shadowCoef;
}

void main(void){
    //位置
    vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

    //レイの方向
    float targetDepth = 1.0;
    vec3 ray = normalize(vec3(sin(fov) * p.x, sin(fov) * p.y, -cos(fov)) + cDir * targetDepth);   

    //レイマーチング
    float distance = 0.0; // レイとオブジェクト間の最短距離
    float rLen = 0.0;     // レイに継ぎ足す長さ
    vec3  rPos = cPos;    // レイの先端位置
    for(int i = 0; i < 64; i++){
        distance = distanceFunc(rPos);
        if(distance < 0.001) break;
        rLen += distance;
        rPos = cPos + ray * rLen;
    }

    //光
    vec3 light = normalize(lightDir);
    //影
    float shadow = 1.0;
    //出力色
    vec3 outColor;

    //レイの当たり判定
    if(abs(distance) < 0.001){
        //法線
        vec3 normal = getNormal(rPos);

        //光
        vec3 halfLE = normalize(light - ray);
        float diff = clamp(dot(light, normal), 0.1, 1.0);
        float spec = pow(clamp(dot(halfLE, normal), 0.0, 1.0), 30.0);

        //影
        shadow = genShadow(rPos + normal * 0.001, light);

        //色
        vec3 objColor = calcColor(rPos);

        outColor = objColor * diff + vec3(spec);
    }else{
        outColor = vec3(0.0);
    }

    //最終的な出力
    gl_FragColor = vec4(outColor * max(0.5, shadow), 1.0);
}

出力結果

鉄火巻き.png
オブジェクトも正常に表示され、影も映っている。

感想

作ったものはシンプルだがこれだけでも結構悩まされた。
距離関数を理解するのが難しい。
もっと勉強して色々できるようになったら楽しそう。

参考にしたサイト

2019年度GLSLお役立ちシート
wgld.org
WebGL と GLSL で始める、はじめてのシェーダコーディング(1)
The Book of Shaders

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2