LoginSignup
25
27

More than 1 year has passed since last update.

ポストエフェクトクエスト - ウルトラ簡単なんちゃってGodRay -

Last updated at Posted at 2020-11-23

初めに


の記事が少しウケたようで嬉しいもんです.
さて今回はタイトルにもあるようにGodRayフィルタをなんちゃってで実装しようと思います.
今回は光学の知識全く使いませんし,式もないのでご安心ください.(僕は式がないとアレルギーが出る体質です.我慢します.)
前回長かったので今回はサクッと読めるもの目指します.正直めちゃくちゃ簡単です.僕が書いた記事の中ではずば抜けてEASYです.こんな表現ができるようになります.

初めに断っておきますが,今回の記事ではコード部分を大幅に省略しています.そのためコードの大部分は前回の記事を参考にしていただければと思います.よろしくお願いいたします.途中ガウシアンブラーが出てきますがそれについてもここでは書きませんので,僕の別記事を読んでいただくようにご案内しています.

GodRayとは

薄明光線とも呼ばれます.レンブラント・ファン・レインという画家の方がよく用いたとかなんとかで「レンブラント光線(インプラントではない)」とも呼ばれているとか何とか.
雲の切れ目の下部分に,目視できないレベルの水滴がいい感じに浮遊していると,光が散乱されて光の筋が見えるようになります.水滴が多すぎたり,少なすぎたりすると途中で切れてしまったり,そもそも薄すぎて見えなかったりと意外とレアな現象です.チンダル現象に分類されます.

ゲームでもよく見られますね.個人的に印象的なのはフロムソフトウェアのダークソウルに登場する「アノールロンド」のGodRayですね.初めて訪れたとき,こんな表現できたらいいなあと,思ったものです.めちゃくちゃきれいなので調べてみてください.

どう表現する?

使うものは以下の2つです.

  • ラジアルブラー
  • ガウシアンブラー

「ブラーばっかりやないかい!!!」と思ったそこのあなた.「そうだよ(適当)」.
正直ガウシアンブラーはあってもなくてもいいかなと思いますが,まああったらいい見た目にはなります.(結局いるやん)

ラジアルブラー

基準となる点から放射状にかかるブラーのことです.(正直これだけでゴッドレイできる...おっと誰か来たようだ)
概念的にはこんな感じです.

簡単な例を見てみましょう.左上基準点としてラジアルブラーをかけると以下のようになります.

下側中央を基準点としてラジアルブラーをかけると以下のようになります.

おしまいです.ほんとこれだけです.今回の実装だとラジアルブラーだけではある形状の相似形が一定間隔で発生するような見た目になるのですが,ラジアルブラーを反復してかけるとこの相似形状の間隔が狭くなるので,あまり気にならなくなります.後述のガウシアンブラーもかければなお良しです.

ガウシアンブラー

これはラジアルブラーでできたゴッドレイをぼかすために使います.

エフェクトをかける

前回のグレア同様,高輝度部分にエフェクトをのせます.従って以下のような手順を踏みます.

使用例1

最初の対象画像はこちらになります.(若干GodRay出ちゃってますが…)
cloud.png
結果はこんな感じです.
godray2.gif

使用例2

次にこのような窓を模した画像とカーテン的な何かで遮蔽されたバージョンを使用します.

結果はこんな感じです.
図13.png
図14.png
光が遮蔽されている感が出ていますね.簡単なエフェクトですが意外と柔軟です.

ガウシアンブラ-の効果

ガウシアンブラーが弱い場合.
図5.png
ガウシアンブラーが強い場合.
図6.png
後者のほうが柔らかな表現になります.使用はお好みでといった感じでしょうか.
中間あたりであれば使ってもいいかなという感じです.
図7.png
gifにするとガウシアンブラーの強弱はこのように現れることがわかります.

コード

Cpp

定数バッファになる構造体.

    struct ComputeParameters
    {
        float threshold = 0.8;//二値化閾値
        float raylength = 1;//光線の長さ
        float gausssigma = 1.0;//ブラーの強さ 大きいほど強い
        float posx = 0;//放射基準x座標(0から1)
        float posy = 0;//放射基準y座標(0から1)
    };

以下の関数で実行します.各関数の詳細は前の記事で確認していただければと思います.シェーダとCpp側の対応付けも同様です.
説明していない関数のみ書きます.

int ComputeFilterApp::ExecuteGodRayCommand()
{
    ComputeParameters cp;
    cp.threshold = m_threshold;
    cp.raylength = m_raylength;
    cp.gausssigma = m_gausssigma;
    cp.posx = m_posx;
    cp.posy = m_posy;

    WriteToUploadHeapMemory(m_mainComputeCB[0].Get(), sizeof(cp), &cp);

    ExecuteCopyCommand(m_RfullsizeTex.at(0), m_RWfullsizeTex.at(0));
    ExecuteCopyCommand(m_RfullsizeTex.at(0), m_RWfullsizeTex.at(1));

    ExecuteClearCommand(m_RWfullsizeTex.at(1));

    ExecuteBinaryThresholdCommand(m_RWfullsizeTex.at(0), m_RWfullsizeTex.at(2));

    //ラジアルブラー反復部分 やればやるほどよく伸びる!!!
    for (int i = 0; i < m_godrayquarity; ++i)
    {
        ExecuteRadialBlurCommand(m_RWfullsizeTex.at(2), m_RWfullsizeTex.at(3));
        ExecuteCopyCommand(m_RWfullsizeTex.at(3), m_RWfullsizeTex.at(2));
    }

    ExecuteDrawGaussianCommand(m_RWfullsizeTex.at(6), m_RWfullsizeTex.at(7));

    ExecuteClearCommand(m_RWfullsizeTex.at(2));

    ExecuteConvolutionCommand(
        m_RWfullsizeTex.at(6), m_RWfullsizeTex.at(7)
        , m_RWfullsizeTex.at(3), m_RWfullsizeTex.at(2)
        , m_RWfullsizeTex.at(0), m_RWfullsizeTex.at(1));
    ExecuteCalcurateAmplitudeCommand(
        m_RWfullsizeTex.at(0), m_RWfullsizeTex.at(1)
        , m_RWfullsizeTex.at(2), m_RWfullsizeTex.at(3));

    ExecuteCalcMaxMinCommand(m_RWfullsizeTex.at(2), m_RWmaxminTex.at(0), m_RWmaxminTex.at(1));
    ExecuteDivideMaxAmpCommand(
        m_RWmaxminTex.at(0), m_RWmaxminTex.at(1)
        , m_RWfullsizeTex.at(2), m_RWfullsizeTex.at(3)
        , m_RWfullsizeTex.at(4), m_RWfullsizeTex.at(5));

    ExecuteCopyCommand(m_RfullsizeTex.at(0), m_RWfullsizeTex.at(0));

    ExecuteAddCommand(m_RWfullsizeTex.at(0), m_RWfullsizeTex.at(4), m_RWfullsizeTex.at(1));

    return 1;
}

説明のないもの.

void ComputeFilterApp::ExecuteDrawGaussianCommand(AutoTransitionTexture& Real, AutoTransitionTexture& Image)
{
    m_commandList->SetComputeRootConstantBufferView(0, m_mainComputeCB[0]->GetGPUVirtualAddress());
    m_commandList->SetComputeRootDescriptorTable(3, Real.WriteState());
    m_commandList->SetComputeRootDescriptorTable(4, Image.WriteState());
    m_commandList->SetPipelineState(m_pipelines["gaussianCS"].Get());
    m_commandList->Dispatch(1, m_texheight, 1);
}

void ComputeFilterApp::ExecuteRadialBlurCommand(AutoTransitionTexture& In, AutoTransitionTexture& Out)
{
    m_commandList->SetComputeRootConstantBufferView(0, m_mainComputeCB[0]->GetGPUVirtualAddress());
    m_commandList->SetComputeRootDescriptorTable(1, In.ReadState());
    m_commandList->SetComputeRootDescriptorTable(3, Out.WriteState());
    m_commandList->SetPipelineState(m_pipelines["radialblurCS"].Get());
    m_commandList->Dispatch(1, m_texheight, 1);
}

HLSL

struct ComputeParameters
{
    float gausssigma;
    float threshold;
    float raylength;
    float posx;
    float posy;
};

ConstantBuffer<ComputeParameters> computeConstants : register(b0);
Texture2D<float4> sourceImageR : register(t0);
Texture2D<float4> sourceImageI : register(t1);
RWTexture2D<float4> destinationImageR : register(u0);
RWTexture2D<float4> destinationImageI : register(u1);
RWTexture2D<float4> destinationImageR1 : register(u2);
RWTexture2D<float4> destinationImageI1 : register(u3);

//ガウシアン描画(gaussianCS)
[numthreads(WIDTH, 1, 1)]
void mainDrawGaussian(uint3 dispatchID : SV_DispatchThreadID)
{
    float2 index = dispatchID.xy;

    float x = index.x - WIDTH / 2.0f;
    float y = index.y - HEIGHT / 2.0f;

    float s = computeConstants.gausssigma;

    float val = exp(-(x * x + y * y) / 2 / s / s) / sqrt(2 * PI) / s;//必ずサイズで割ること

    float3 color_real = float3(val, val, val);

    destinationImageR[index] = float4(color_real, 1.0f);
    destinationImageI[index] = float4(0.0, 0.0, 0.0, 1.0f);
}

//画像インデックスの画像範囲内へのクランプ
float2 clampF2(float2 value)
{
    return float2(clamp(value.x, 0, WIDTH), clamp(value.y, 0, HEIGHT));
}

//ラジアルブラーをするシェーダ(radialblurCS)
[numthreads(WIDTH, 1, 1)]
void mainRadialBlur(uint3 dispatchID : SV_DispatchThreadID)
{
    float3 color[10];

    float2 index = dispatchID.xy;

    float2 center = float2(computeConstants.posx * WIDTH, computeConstants.posy * HEIGHT);

    float2 direction = index - center;

    //これで基準点から放射状に広がるようになる しなければ基準点に収束
    direction = -direction;

    float len = length(direction);

    direction = normalize(direction) * computeConstants.raylength;

    float w = 9.0/11.0;//(ふつうは10/11とすべきですが,ガウシアンとの兼ね合いで1を超えないようにしています)

    //減衰させながら伸ばす
    color[0] = sourceImageR[clampF2(index                  )].rgb * 0.20;
    color[1] = sourceImageR[clampF2(index + direction * 1.0)].rgb * 0.18;
    color[2] = sourceImageR[clampF2(index + direction * 2.0)].rgb * 0.16;
    color[3] = sourceImageR[clampF2(index + direction * 3.0)].rgb * 0.14;
    color[4] = sourceImageR[clampF2(index + direction * 4.0)].rgb * 0.12;
    color[5] = sourceImageR[clampF2(index + direction * 5.0)].rgb * 0.10;
    color[6] = sourceImageR[clampF2(index + direction * 6.0)].rgb * 0.08;
    color[7] = sourceImageR[clampF2(index + direction * 7.0)].rgb * 0.06;
    color[8] = sourceImageR[clampF2(index + direction * 8.0)].rgb * 0.04;
    color[9] = sourceImageR[clampF2(index + direction * 9.0)].rgb * 0.02;

    destinationImageR[index].rgb = w * (color[0] + color[1] + color[2] + color[3] + color[4]
        + color[5] + color[6] + color[7] + color[8] + color[9]);
}

これだけです.前回からの追加量はかなり少ないです.

おわりに

お疲れさまでした.今回も我慢できずフーリエ変換(FFT)と畳み込みを使用してしまいましたね.もう病気です.
もし質問等ありましたらコメントにお願いします.ではまたどこかで.

25
27
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
25
27