アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

スクリーンスペースリフレクション

こんにちは、みにくい社長です。

最近はゲームエンジンの普及でゲームが作りやすくなった反面、シェーダー周りの実装を説明している記事が減ってきたのが残念です。 せっかくなので、OROCHIエンジンの技術紹介も兼ねてやっていこうと思います。 最近実装したもので、スクリーンスペースリフレクションについてです。

スクリーンスペースリフレクション(SSR)とは、画面全体にかけるポストエフェクトの一種で、 3D空間ではなく2Dの描画結果から反射を計算して描画することができます。 スクリーンスペースアンビエントオクルージョン(SSAO)に似ていますね。 Zバッファを利用しつつ反射を計算するのですが、2Dの画像から計算する為、どうしてもできないことがあります。 それは、描画された物よりも後ろにあるものや画面外にあるものは反射させることができない、ということです。 それでも十分な効果が得られる為、現在でもゲーム開発ではよく使われています。

アルゴリズムを説明すると、 まずはカメラから描画される点のビュー座標と法線ベクトルより、反射ベクトルを計算します。 ビュー座標から反射ベクトルに向かって少しずつ座標を移動させます。 f:id:minikui_ahiru:20220406211140p:plain その移動先とZバッファの値を比較し、Zバッファよりも奥になったタイミングを交差とみなし、その座標の色を取得します。 その色を反射した色とし、スペキュラ値を係数としてブレンドします。

描画すると、こうなります。 f:id:minikui_ahiru:20220406211333p:plain なんか伸びてますね。。 原因は、反射ベクトルがオブジェクトの奥に進んだ場合でも交差したと判断されてしまう為です。 f:id:minikui_ahiru:20220406211222p:plain

これを回避するには、交差したときのzとの差分に閾値を設けます。 閾値を設定して描画すると、こうなります。 f:id:minikui_ahiru:20220406211624p:plain

良い感じに反射しています。 まだギザギザしているのをなんとかしたい気もします。 このギザギザは、反射ベクトルが1回に進む距離を細かくすることで改善できます。

f:id:minikui_ahiru:20220406211641p:plain

綺麗になりましたね。 ただし、距離を細かくするほど計算回数が増えますので、ここはトレードオフになります。

最後に、シェーダーはこんな感じです。

struct Input
{
    float4  _position       : POSITION;
    float4  _uv         : TEXCOORD0;
};

struct Interpolator
{
    float4  _position       : SV_Position;
    half4   _uv         : TEXCOORD4;
};

Interpolator VS_MAIN(Input input)
{
    Interpolator output;

    output._position = mul(gProjectionMatrix2D, input._position);
    output._uv.xy = input._uv.xy;
    // 逆射影変換用のスケール値
    output._uv.zw = (input._uv.xy - 0.5f) * float2(2.0f,-2.0f);

    return output;
}

float4 applySSR(half3 normal, float3 viewPosition, half2 screenUV)
{
    float4 color = gOpacitySampler.Sample(gOpacitySampler_SamplerState, screenUV);
    half4 totalSpecular = gSpecularSampler.Sample(gNoFilterSamplerState, screenUV);

    float3 position = viewPosition;
    float2 uvNow;
    float2 uvScale = (screenUV - 0.5f) * float2(2.0f, -2.0f);

    float3 reflectVec = normalize(reflect(normalize(viewPosition), normal)); // 反射ベクトル
    const int iteration = 200;  // 繰り返し数
    const int maxLength = 10; // 反射最大距離
    float3 delta = reflectVec * maxLength / iteration; // 1回で進む距離
    [loop]
    for (int i = 0; i < iteration; i++)
    {
        position += delta;
        // 現在の座標を射影変換してUV値を求める
        float4 projectPosition = mul(gProjectionMatrix, float4(position, 1.0));
        uvNow = projectPosition.xy / projectPosition.w * 0.5f + 0.5f;
        uvNow.y = 1.0f - uvNow.y;
        // zバッファの値を取得したらまた逆変換
        float4 pos;
        pos.z = texDepth2D(gDepthMapSampler, uvNow);
        pos.xy = uvScale;
        pos.w = 1.0f;
        pos = mul(gInversProjectionMatrix, pos);
        float z = pos.z / pos.w;
        // Z値を比較
        [branch]
        if(position.z < z && position.z + gSSRParameter.z > z)
        {
            // 交差したので色をブレンドする
            return lerp(color, gOpacitySampler.Sample(gOpacitySampler_SamplerState, uvNow), totalSpecular * gSSRParameter.w);
        }
    }
    return color;
}

float4 PS_MAIN( Interpolator input ) : SV_Target
{
    float4 pos;
    pos.z = texDepth2D(gDepthMapSampler, input._uv.xy);
    pos.xy = input._uv.zw;
    pos.w = 1.0f;
    // Z値からビュー座標を取得
    pos = mul(gInversProjectionMatrix, pos);
    float3 position = pos.xyz / pos.w;

    half3 normal = texViewNormalAndSpecularPower(gNormalMapSampler, input._uv).rgb;
    return applySSR(normal, position, input._uv);
}

では、またねー!