DirectXでエッジ検出


法線マップや深度値を使用して、輪郭を強調する方法を紹介します。

概要

MRTで1パス目で普通にレンダリングして、2パス目で法線マップ、3パス目でカメラからの深度値マップを作製します。

1パス(通常レンダリング)

2パス(法線マップ)

3パス(深度値マップ)

法線マップと深度値マップを使用して、エッジ抽出を行います。

エッジ抽出したものを1パス目に乗算合成します。

乗算合成にはDirectX11のブレンドステートを使用しました。

    ID3D11BlendState* m_finalBlendState;        //乗算合成用のブレンディングステート。
    ID3D11Device* device;   //d3d11デバイス
    ID3D11DeviceContext* deviceContext   //d3d11デバイスコンテキスト


//設定
{
    CD3D11_DEFAULT defaultSettings;
    //デフォルトセッティングで初期化する。
    CD3D11_BLEND_DESC blendDesc(defaultSettings);
    //合成用のブレンドステートを作成する。
    //乗算合成。
    blendDesc.RenderTarget[0].BlendEnable = true;
    blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_ZERO;
    blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_SRC_COLOR;
    device->CreateBlendState(&blendDesc, &m_finalBlendState);
}

//ドロー時
{
    //乗算合成用のブレンディングステートを設定する。
    float blendFactor[] = { 0.0f, 0.0f, 0.0f, 0.0f };
    deviceContext->OMSetBlendState(m_finalBlendState, blendFactor, 0xffffffff);
}

エッジ検出

エッジ検出ですが、法線マップ、深度値マップ共に、ラプラシアンフィルタ(8方向)というものを使用しました。
例えばピクセルが

\left(
\begin{matrix}
1 & 0 & 0 \\
1 & 1 & 0 \\
1 & 1 & 1
\end{matrix}
\right)

とあったとします。中央がエッジかどうか判定したいピクセルです。各ピクセルに

\left(
\begin{matrix}
1 & 1 & 1 \\
1 & -8 & 1 \\
1 & 1 & 1
\end{matrix}
\right)

を掛けて足します。その結果の絶対値が一定数以上だとエッジとみなします。
この場合だと、

(1 * 1) * 5 + (0 * 1) * 3 + (1 * (-8))=  -3

となります。2より大きければエッジとしていた場合、このピクセルはエッジということになります。

シェーダー

/*!
  *@brief   頂点シェーダーの入力。
  */
struct VSInput {
    float4 pos : SV_Position;
    float2 uv  : TEXCOORD0;
};
/*!
 *@brief    ピクセルシェーダーへの入力。
 */
struct PS_EdgeInput {
    float4 pos : SV_Position;
    float2 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
    float4 tex2 : TEXCOORD2;
    float4 tex3 : TEXCOORD3;
    float4 tex4 : TEXCOORD4;
    float4 tex5 : TEXCOORD5;
    float4 tex6 : TEXCOORD6;
    float4 tex7 : TEXCOORD7;
    float4 tex8 : TEXCOORD8;
};

Texture2D<float4> normalTexture : register(t0); //シーンテクスチャ。
Texture2D<float4> depthValueTexture : register(t1); //深度値テクスチャ

sampler Sampler : register(s0);     //サンプラー

PS_EdgeInput VSXEdge(VSInput In)
{
    float2 texSize;
    float level;
    //テクスチャーのサイズを取得する
    normalTexture.GetDimensions(0, texSize.x, texSize.y, level);

    PS_EdgeInput Out;
    Out.pos = In.pos;
    float2 tex = In.uv;

    float offset = 0.2f;
    //法線
    {
        //真ん中のピクセル
        Out.tex0 = tex;

        //右上のピクセル
        Out.tex1.xy = tex + float2(offset / texSize.x, -offset / texSize.y);

        //上のピクセル
        Out.tex2.xy = tex + float2(0.0f, -offset / texSize.y);

        //左上のピクセル
        Out.tex3.xy = tex + float2(-offset / texSize.x, -offset / texSize.y);

        //右のピクセル
        Out.tex4.xy = tex + float2(offset / texSize.x, 0.0f);

        //左のピクセル
        Out.tex5.xy = tex + float2(-offset / texSize.x, 0.0f);

        //右下のピクセル
        Out.tex6.xy = tex + float2(offset / texSize.x, offset / texSize.y);

        //下のピクセル
        Out.tex7.xy = tex + float2(0.0f, offset / texSize.y);

        //左下のピクセル
        Out.tex8.xy = tex + float2(-offset / texSize.x, offset / texSize.y);
    }

    //深度値
    {
        //深度値を取り出すときに使うUV座標
        offset = 1.0f;
        //右上のピクセル
        Out.tex1.zw = tex + float2(offset / texSize.x, -offset / texSize.y);

        //上のピクセル
        Out.tex2.zw = tex + float2(0.0f, -offset / texSize.y);

        //左上のピクセル
        Out.tex3.zw = tex + float2(-offset / texSize.x, -offset / texSize.y);

        //右のピクセル
        Out.tex4.zw = tex + float2(offset / texSize.x, 0.0f);

        //左のピクセル
        Out.tex5.zw = tex + float2(-offset / texSize.x, 0.0f);

        //右下のピクセル
        Out.tex6.zw = tex + float2(offset / texSize.x, offset / texSize.y);

        //下のピクセル
        Out.tex7.zw = tex + float2(0.0f, offset / texSize.y);

        //左下のピクセル
        Out.tex8.zw = tex + float2(-offset / texSize.x, offset / texSize.y);

    }
    return Out;
}

float4 PSEdge(PS_EdgeInput In) : SV_Target0
{
    //周囲のピクセルの法線の値の平均を計算する。
    float3 Normal;
    Normal = normalTexture.Sample(Sampler, In.tex0).xyz * -8.0f;
    Normal += normalTexture.Sample(Sampler, In.tex1.xy).xyz;
    Normal += normalTexture.Sample(Sampler, In.tex2.xy).xyz;
    Normal += normalTexture.Sample(Sampler, In.tex3.xy).xyz;
    Normal += normalTexture.Sample(Sampler, In.tex4.xy).xyz;
    Normal += normalTexture.Sample(Sampler, In.tex5.xy).xyz;
    Normal += normalTexture.Sample(Sampler, In.tex6.xy).xyz;
    Normal += normalTexture.Sample(Sampler, In.tex7.xy).xyz;
    Normal += normalTexture.Sample(Sampler, In.tex8.xy).xyz;

    //周囲のピクセルの深度値の平均を計算する。
    float depth2 = depthValueTexture.Sample(Sampler, In.tex1).x;
    depth2 += depthValueTexture.Sample(Sampler, In.tex2.zw).x;
    depth2 += depthValueTexture.Sample(Sampler, In.tex3.zw).x;
    depth2 += depthValueTexture.Sample(Sampler, In.tex4.zw).x;
    depth2 += depthValueTexture.Sample(Sampler, In.tex5.zw).x;
    depth2 += depthValueTexture.Sample(Sampler, In.tex6.zw).x;
    depth2 += depthValueTexture.Sample(Sampler, In.tex7.zw).x;
    depth2 += depthValueTexture.Sample(Sampler, In.tex8.zw).x;
    depth2 /= 8.0f;

    float4 Color;
    //法線の計算結果、あるいは深度値の計算結果が一定以上ならエッジとみなす。
    if (length(Normal) >= 0.2f || abs(depth2-depth) > 0.001f ) {
        Color = float4(0.0f, 0.0f, 0.0f, 1.0f);
    }
    else {
        Color = float4(1.0f, 1.0f, 1.0f, 1.0f);
    }

    return Color;
}