NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

ShaderToyでカメラのテストをやってみた。

「レイマーチング」*1を実装させるために、まず下準備としてカメラを実装してみました。 こっちで使うカメラは、View・Projectionマトリックスを使いません。(Projectionはあとで複雑になると使うかもしれませんね) なぜならシェーダーではViewの代わりに、カメラとなる地点からの仮想のスクリーンを作り、原点から打つレイ(光線)をスクリーンのピクセルに通して環境をレンダリングするからです。

では、作成したコードをちょっと自己分析したいと思います。

分析

このコードでの主なる部分は3つに分けられます。

  1. スクリーンのUV座標を使用して中央に寄せること
  2. カメラを設定し、仮想の光線を準備すること
  3. テストでポイントをレンダリングさせること。

1. スクリーンUVの設定

vec2 uv = fragCoord.xy / min(iResolution.x, iResolution.y);
uv.x -= (iResolution.x - iResolution.y) / (2 * min(iResolution.x, iResolution.y)); 
uv -= 0.5;

まずスクリーンの中心を[0, 0]座標にするために、fragCoordiResolutionを使用して計算しました。

注意したところは、ただ一行目のようにuvを計算すると、glsl仕様上uvが左によるため、シーン自体も左によります。 なので二行目のようにuv.xを移動させ(中央のUV座標が[0, 0]になるように)、そして0.5を引くことで中央が0なものが出来上がります。

2. カメラ設定

vec3 camera = vec3(0, 3, 5);
camera = vec3(
    cos(iTime) * camera.x + sin(iTime) * camera.z, 
    camera.y,
    -sin(iTime) * camera.x + cos(iTime) * camera.z);
vec3 view   = vec3(0, 0, 0);
vec3 up     = vec3(0, 1, 0);

カメラの位置(camera)、カメラが見る位置(view)、そしてシーン環境の上の方向(up)を設定します。

cameraはシーンの原点を見ながら、iTime時間変数によってY軸でぐるぐると回るようにしました。 glslでQuerternionが実装出来るかはわかりませんが、出来たらジンバルロックから解放した複雑な回転ができそうですね。。 この3つの変数は、View行列を作るのに必要となります。しかしこっちでは、View行列を作らずに仮想のスクリーン・光線を設定するのに必要です。

// Setting up View matrix (don't make view matrix itsel...
vec3 forward = normalize(view - camera);
vec3 side    = cross(forward, up);
vec3 camUp   = cross(side, forward);
float zoom = 2.0f;

仮想のスクリーン・光線を設定する下準備として、s(Side) v(CamUp) f(Forward)を計算します。

計算する時には、外積(normalize)関数を使用して値を記録しました。 View行列を求める時に必要となる、-p \cdot s-p \cdot v-p \cdot fは記録しなくても良いです。なぜならView行列を求めるのじゃないからです。

// Setting up virtual screen, and lay direction of virt...
vec3 scrOrg = camera + zoom * forward;
vec3 pxlPos = scrOrg + (side * uv.x) + (camUp * uv.y);
vec3 rayDir = normalize(pxlPos - camera);

そしてカメラ時点でのnear面の仮想スクリーンを作ります。

f:id:neuliliilli:20190524090052p:plain
https://computergraphics.stackexchange.com/questions/5293/why-are-width-and-height-divided-by-2-in-the-perspective-projection-matrix

上の画像から見ながら説明すると、scrOrgは十字線が交差するとこを示します。 上から記録したsidecamUpforwardはお互いに直交するので、カメラ時点(仮想スクリーン)での座標軸を示すことが出来ます。なので、sidecamUpを使用し、仮想スクリーンの各ピクセルでの位置を求めます。この任意の位置をpxlPosと言います。 ただし注意するところは、今現在はuv座標が解像度の大きさによって違っていくので、uvが必ずしも[0, 1]に収まることはありませんね。今は一応uvを解像度に依存させます。

最後にrayDirを記録します。これはカメラの位置から仮想スクリーンへのある地点に対し、仮想の光線(Ray)を放つための方向となります。このrayDirはベクトルですので平準化します。

3. ポイントのレンダリング

struct DPoint
{
    vec3 mPos;
    vec3 mCol;
};

DPoint point;
point.mPos = vec3(1, 0, 0); point.mCol = vec3(1, 1, 0);
{
    float v = DrawPoint(camera, rayDir, point.mPos);
    result += point.mCol * v;
}

まずは大まかなロジックから。(1, 0, 0)の位置にポイントを、そして色を黄色((1, 1, 0))に指定して、光線を放つことで描画Weight値を得ます。

ポイントの構造体はDPointとして、変数をばらまくんじゃなくて一応構造化して置きました。vDrawPointという、光線テストをしてから得られるレンダリングWeight値です。ほぼ0か1になって、1の場合にはポイントが描画されます。

float DistLineToPoint(vec3 ro, vec3 rd, vec3 pos)
{
    vec3 ap = pos - ro;
    return length(cross(ap, rd)) / length(rd);
}

float DrawPoint(vec3 ro, vec3 rd, vec3 pos)
{
    float dist = DistLineToPoint(ro, rd, pos);
    return smoothstep(0.1, 0.09, dist);
}

DrawPoint関数では、DisLineToPoint関数を使用してWeight値を返します。 dist変数は、カメラの位置、カメラから放出する光線の方向、そしてポイントの位置の最短距離を測った値となります。 smoothstepは、そのdistが一定値以下だったら0じゃない値を返すようにしました。今思うとsmoothstepではなくてstepにしても良さそうですね。また、smoothstepの二番目引数を0にしなかった理由は、0にするとグラデーションが起こるからです。

DistLIneToPoint関数は以下の数式に基づいて実装しました。


d(\mathbf{P}, (l)) = \frac{|| \bar{\mathbf{AP}} \times \vec{u} ||}{|| \vec{u} ||}

コード全体

struct DPoint
{
    vec3 mPos;
    vec3 mCol;
};

float DistLineToPoint(vec3 ro, vec3 rd, vec3 pos)
{
    vec3 ap = pos - ro;
    return length(cross(ap, rd)) / length(rd);
}

float DrawPoint(vec3 ro, vec3 rd, vec3 pos)
{
    float dist = DistLineToPoint(ro, rd, pos);
    return smoothstep(0.1, 0.09, dist);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 uv = fragCoord.xy / min(iResolution.x, iResolution.y);
    uv.x -= (iResolution.x - iResolution.y) / (2 * min(iResolution.x, iResolution.y)); 
    uv -= 0.5;
    
    vec3 camera = vec3(0, 3, 5);
    camera = vec3(
        cos(iTime) * camera.x + sin(iTime) * camera.z, 
        camera.y,
        -sin(iTime) * camera.x + cos(iTime) * camera.z);
    vec3 view   = vec3(0, 0, 0);
    vec3 up     = vec3(0, 1, 0);
    
    // Setting up View matrix (don't make view matrix itself, We just need v,s,u element).
    vec3 forward = normalize(view - camera);
    vec3 side    = cross(forward, up);
    vec3 camUp   = cross(side, forward);

    float zoom = 2.0f;

    // Setting up virtual screen, and lay direction of virtual screen pixel.
    vec3 scrOrg = camera + zoom * forward;
    vec3 pxlPos = scrOrg + (side * uv.x) + (camUp * uv.y);
    vec3 rayDir = normalize(pxlPos - camera);
    
    vec3 result = vec3(0);
    
    DPoint point;
    point.mPos = vec3(1, 0, 0); point.mCol = vec3(1, 1, 0);
    {
        float v = DrawPoint(camera, rayDir, point.mPos);
        result += point.mCol * v;
    }
    // いくつかのポイントをレンダリングさせる。
    
    fragColor = vec4(result, 1.0);
}

参照

computergraphics.stackexchange.com

en.wikipedia.org

qiita.com