川瀬式グレアエフェクトを勉強する(2)


 1.はじめに…
この記事はGraphics Advent Calendar 2017の23日目の記事です。
前回に引き続き川瀬式グレアエフェクトを勉強してみようと思います。
今回は,レンズゴーストを実装してみます。





 2.勉強資料
今回は以下の資料を参考にします。


 3.レンズゴースト
レンズフレアといえば,みなさん下記のような画像を思い浮かべるのではないでしょうか?


※図は Wise Camera(http://camera-beginner.sakura.ne.jp/wp/?p=669) より引用。

上記の画像はレンズフレアと呼ばれる部分とレンズゴーストと呼ばれる部分から構成されます。


レンズゴーストまで含めてレンズフレアと言ってしまっている人は少し注意しましょう。


 4. 実装
さて,今回は川瀬式のレンズゴーストの出し方を勉強してみようと思います。レンズゴーストについては[Kawase 2003]に詳細が載っています。

※図は[Kawase 2003]より引用。


※図は[Kawase 2003]より引用。


※図は[Kawase 2003]より引用。

スクリーン中心に対してスケーリングを適用していき,スケーリングしたテクスチャを合成していけば良いようですね。
このテクニックの最大のポイントは矩形の四隅に色が載ってしまうと,アーティファクトが出てしまうためマスクを掛けている点です。 こういったアーティファクトが出ないように考えられて実装されているところが「さすが!川瀬さん」と見習うべきところですね。

川瀬式レンズゴーストを適用した画像は下記のようになります。


※図は[Kawase 2003]より引用。

では,この最終結果を目指して実装してみましょう。
00386:  //---------------------------------------------------------------------------------------
00387:  //      描画時の処理です.
00388:  //---------------------------------------------------------------------------------------
00389:  void SampleApplication::OnFrameRender( double time, double elapsedTime )
00390:  {
00391:      auto w = m_Width;
00392:      auto h = m_Height;
00393:  
00394:      ID3D11ShaderResourceView*   pSrc = nullptr;
00395:      ID3D11RenderTargetView*     pDst = nullptr;
00396:  
00397:      auto pMask = m_MaskTexture.GetSRV();
00398:  
00399:      ID3D11ShaderResourceView* nullSRVs[2] = { nullptr, nullptr };
00400:  
00401:      UINT  sampleMask     = D3D11_DEFAULT_SAMPLE_MASK;
00402:      float blendFactor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
00403:      float clearColor[4]  = { 0.0f, 0.0f, 0.0f, 1.0f };
00404:      auto deviation = 5.0f;
00405:  
00406:      // 入力画像にガウスブラーを掛ける,
00407:      {
00408:          pDst = m_WorkBuffer[2].GetRTV();
00409:          pSrc = m_InputTexture.GetSRV();
00410:  
00411:          // クリア処理.
00412:          m_pDeviceContext->ClearRenderTargetView( pDst, m_ClearColor );
00413:  
00414:          // 出力マネージャに設定.
00415:          m_pDeviceContext->OMSetRenderTargets( 1, &pDst, nullptr );
00416:  
00417:          // ステートを設定.
00418:          m_pDeviceContext->RSSetState( m_pRasterizerState );
00419:          m_pDeviceContext->OMSetBlendState( m_pOpequeBS, blendFactor, sampleMask );
00420:          m_pDeviceContext->OMSetDepthStencilState( m_pDepthStencilState, m_StencilRef );
00421:  
00422:          // シェーダの設定.
00423:          m_pDeviceContext->VSSetShader( m_pFullScreenVS, nullptr, 0 );
00424:          m_pDeviceContext->GSSetShader( nullptr, nullptr, 0 );
00425:          m_pDeviceContext->HSSetShader( nullptr, nullptr, 0 );
00426:          m_pDeviceContext->DSSetShader( nullptr, nullptr, 0 );
00427:          m_pDeviceContext->PSSetShader( m_pGaussBlurPS, nullptr, 0 );
00428:  
00429:          // シェーダリソースビューを設定.
00430:          m_pDeviceContext->PSSetShaderResources( 0, 1, &pSrc );
00431:          m_pDeviceContext->PSSetSamplers( 0, 1, &m_pLinearClamp );
00432:  
00433:  
00434:          D3D11_VIEWPORT viewport;
00435:          viewport.TopLeftX   = 0;
00436:          viewport.TopLeftY   = 0;
00437:          viewport.Width      = float(w) / 4.0f;
00438:          viewport.Height     = float(h) / 4.0f;
00439:          viewport.MinDepth   = 0.0f;
00440:          viewport.MaxDepth   = 1.0f;
00441:  
00442:          m_pDeviceContext->RSSetViewports( 1, &viewport );
00443:  
00444:          auto pCB = m_GaussBlurBuffer.GetBuffer();
00445:          GaussBlurParam src = CalcBlurParam(w, h, asdx::Vector2(1.0f, 0.0f), deviation);
00446:          m_pDeviceContext->UpdateSubresource( pCB, 0, nullptr, &src, 0, 0 );
00447:          m_pDeviceContext->PSSetConstantBuffers( 0, 1, &pCB );
00448:  
00449:          // 描画.
00450:          m_Quad.Draw(m_pDeviceContext);
00451:   
00452:          pSrc = m_WorkBuffer[2].GetSRV();
00453:          pDst = m_WorkBuffer[3].GetRTV();
00454:  
00455:         // クリア処理.
00456:          m_pDeviceContext->ClearRenderTargetView( pDst, m_ClearColor );
00457:  
00458:          // 出力マネージャに設定.
00459:          m_pDeviceContext->OMSetRenderTargets( 1, &pDst, nullptr );
00460:  
00461:          // シェーダリソースビューを設定.
00462:          m_pDeviceContext->PSSetShaderResources( 0, 1, &pSrc );
00463:          m_pDeviceContext->PSSetSamplers( 0, 1, &m_pLinearClamp );
00464:  
00465:          src = CalcBlurParam(w, h, asdx::Vector2(0.0f, 1.0f), deviation);
00466:          m_pDeviceContext->UpdateSubresource( pCB, 0, nullptr, &src, 0, 0 );
00467:          m_pDeviceContext->PSSetConstantBuffers( 0, 1, &pCB );
00468:  
00469:          // ステートを設定.
00470:          m_pDeviceContext->RSSetState( m_pRasterizerState );
00471:          m_pDeviceContext->OMSetBlendState( m_pOpequeBS, blendFactor, sampleMask );
00472:          m_pDeviceContext->OMSetDepthStencilState( m_pDepthStencilState, m_StencilRef );
00473:  
00474:          // 描画.
00475:          m_Quad.Draw(m_pDeviceContext);
00476:  
00477:          // シェーダリソースをクリア.
00478:          ID3D11ShaderResourceView* nullTarget[1] = { nullptr };
00479:          m_pDeviceContext->PSSetShaderResources( 0, 1, nullTarget );
00480:      }
00481:  
00482:      // ゴーストを生成
00483:      {
00484:          // 乗算カラー / テクスチャスケール.
00485:          asdx::Vector4 colors[] = {
00486:              asdx::Vector4(1.0f,  0.5f,  0.6f, -1.2f),
00487:              asdx::Vector4(0.3f,  1.0f,  0.6f, -1.2f),
00488:              asdx::Vector4(0.6f,  0.35f, 1.0f, -1.5f),
00489:              asdx::Vector4(1.0f,  0.6f,  0.3f, -1.75f),
00490:              asdx::Vector4(0.25f, 1.0f,  0.7f, 1.2f),
00491:              asdx::Vector4(0.5f,  0.9f,  1.0f, 1.35f),
00492:              asdx::Vector4(0.3f,  1.0f,  0.5f, 1.5f),
00493:              asdx::Vector4(0.7f,  0.5f,  1.0f, 2.0f),
00494:          };
00495:  
00496:          pSrc = m_WorkBuffer[3].GetSRV();
00497:          pDst = m_WorkBuffer[0].GetRTV();
00498:  
00499:          auto pCB = m_LensGhostBuffer.GetBuffer();
00500:  
00501:          // レンダーターゲット設定.
00502:          m_pDeviceContext->ClearRenderTargetView( pDst, clearColor );
00503:          m_pDeviceContext->OMSetRenderTargets( 1, &pDst, nullptr );
00504:  
00505:          // ブレンドステート設定.
00506:          m_pDeviceContext->OMSetBlendState( m_pAdditiveBS, blendFactor, sampleMask );
00507:  
00508:          D3D11_VIEWPORT viewport;
00509:          viewport.TopLeftX   = 0;
00510:          viewport.TopLeftY   = 0;
00511:          viewport.Width      = float(w);
00512:          viewport.Height     = float(h);
00513:          viewport.MinDepth   = 0.0f;
00514:          viewport.MaxDepth   = 1.0f;
00515:  
00516:          m_pDeviceContext->RSSetViewports( 1, &viewport );
00517:  
00518:          // シェーダの設定.
00519:          m_pDeviceContext->VSSetShader( m_pFullScreenVS, nullptr, 0 );
00520:          m_pDeviceContext->GSSetShader( nullptr, nullptr, 0 );
00521:          m_pDeviceContext->HSSetShader( nullptr, nullptr, 0 );
00522:          m_pDeviceContext->DSSetShader( nullptr, nullptr, 0 );
00523:          m_pDeviceContext->PSSetShader( m_pLensGhostPS, nullptr, 0 );
00524:   
00525:          m_pDeviceContext->PSSetShaderResources( 0, 1, &pSrc );
00526:          m_pDeviceContext->PSSetShaderResources( 1, 1, &pMask );
00527:          m_pDeviceContext->PSSetSamplers( 0, 1, &m_pLinearClamp );
00528:  
00529:          // ゴーストを描画.
00530:          for(auto i=0; i<8;++i)
00531:          {
00532:              LensGhostParam param;
00533:              param.MultiplyColor = colors[i];
00534:  
00535:              // 定数バッファ更新.
00536:              m_pDeviceContext->UpdateSubresource( pCB, 0, nullptr, &param, 0, 0 );
00537:  
00538:              // 定数バッファ設定.
00539:              m_pDeviceContext->PSSetConstantBuffers( 0, 1, &pCB );
00540:  
00541:              // 矩形描画.
00542:              m_Quad.Draw(m_pDeviceContext);
00543:          }
00544:  
00545:          // リソースを解除.
00546:          m_pDeviceContext->PSSetShaderResources( 0, 2, nullSRVs );
00547:      }
00548:  
00549:      // WorkBuffer[0]を元にゴーストを生成.
00550:      {
00551:          // 乗算カラー / テクスチャスケール.
00552:          asdx::Vector4 colors[] = {
00553:              asdx::Vector4(0.1f, 0.7f, 1.0f, 1.0f),
00554:              asdx::Vector4(1.0f, 0.3f, 0.6f, 2.5f),
00555:              asdx::Vector4(0.3f, 0.8f, 0.8f, 2.3f),
00556:              asdx::Vector4(0.8f, 0.2f, 0.7f, 3.95f),
00557:              asdx::Vector4(0.2f, 0.1f, 0.9f, -1.5f),
00558:              asdx::Vector4(0.9f, 0.1f, 0.2f, -1.7f),
00559:              asdx::Vector4(0.1f, 0.8f, 0.3f, -2.25f),
00560:              asdx::Vector4(0.9f, 0.2f, 0.1f, -3.35f),
00561:          };
00562:  
00563:  
00564:          pSrc = m_WorkBuffer[0].GetSRV();
00565:          pDst = m_WorkBuffer[1].GetRTV();
00566:  
00567:          auto pCB = m_LensGhostBuffer.GetBuffer();
00568:   
00569:          // レンダーターゲット生成.
00570:          m_pDeviceContext->ClearRenderTargetView( pDst, clearColor );
00571:          m_pDeviceContext->OMSetRenderTargets( 1, &pDst, nullptr );
00572:  
00573:          // ブレンドステート設定.
00574:          m_pDeviceContext->OMSetBlendState( m_pAdditiveBS, blendFactor, sampleMask );
00575:  
00576:          // シェーダの設定.
00577:          m_pDeviceContext->VSSetShader( m_pFullScreenVS, nullptr, 0 );
00578:          m_pDeviceContext->GSSetShader( nullptr, nullptr, 0 );
00579:          m_pDeviceContext->HSSetShader( nullptr, nullptr, 0 );
00580:          m_pDeviceContext->DSSetShader( nullptr, nullptr, 0 );
00581:          m_pDeviceContext->PSSetShader( m_pLensGhostPS, nullptr, 0 );
00582:  
00583:          m_pDeviceContext->PSSetShaderResources( 0, 1, &pSrc );
00584:          m_pDeviceContext->PSSetShaderResources( 1, 1, &pMask );
00585:          m_pDeviceContext->PSSetSamplers( 0, 1, &m_pLinearClamp );
00586:  
00587:          // 8個描画.
00588:          for(auto i=0; i<8; ++i)
00589:          {
00590:              LensGhostParam param;
00591:              param.MultiplyColor = colors[i];
00592:  
00593:              // 定数バッファ更新.
00594:              m_pDeviceContext->UpdateSubresource( pCB, 0, nullptr, &param, 0, 0 );
00595:  
00596:              // 定数バッファ設定.
00597:              m_pDeviceContext->PSGetConstantBuffers( 0, 1, &pCB );
00598:  
00599:             // 矩形描画.
00600:              m_Quad.Draw(m_pDeviceContext);
00601:          }
00602:  
00603:          // リソースを解除.
00604:          m_pDeviceContext->PSSetShaderResources( 0, 2, nullSRVs );
00605:      }
00606:  
00607:      // コンポジット.
00608:      {
00609:          // レンダーターゲットビュー・深度ステンシルビューを取得.
00610:          auto pDstRTV = m_RenderTarget2D.GetRTV();
00611:          ID3D11DepthStencilView* pDSV = m_DepthStencilTarget.GetDSV();
00612:  
00613:          m_pDeviceContext->ClearDepthStencilView( pDSV, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0 );
00614:  
00615:          // 出力マネージャに設定.
00616:          m_pDeviceContext->OMSetRenderTargets( 1, &pDstRTV, pDSV );
00617:  
00618:          D3D11_VIEWPORT viewport;
00619:          viewport.TopLeftX   = 0;
00620:          viewport.TopLeftY   = 0;
00621:          viewport.Width      = float(m_Width);
00622:          viewport.Height     = float(m_Height);
00623:          viewport.MinDepth   = 0.0f;
00624:          viewport.MaxDepth   = 1.0f;
00625:  
00626:          m_pDeviceContext->RSSetViewports(1, &viewport);
00627:  
00628:          // ステートを設定.
00629:          m_pDeviceContext->RSSetState( m_pRasterizerState );
00630:          m_pDeviceContext->OMSetBlendState( m_pOpequeBS, blendFactor, sampleMask );
00631:          m_pDeviceContext->OMSetDepthStencilState( m_pDepthStencilState, m_StencilRef );
00632:  
00633:          // シェーダの設定.
00634:          m_pDeviceContext->VSSetShader( m_pFullScreenVS, nullptr, 0 );
00635:          m_pDeviceContext->GSSetShader( nullptr, nullptr, 0 );
00636:          m_pDeviceContext->HSSetShader( nullptr, nullptr, 0 );
00637:          m_pDeviceContext->DSSetShader( nullptr, nullptr, 0 );
00638:          m_pDeviceContext->PSSetShader( m_pCompositePS, nullptr, 0 );
00639:  
00640:          // シェーダリソースビューを設定.
00641:          ID3D11ShaderResourceView* pSRV[] = {
00642:              m_InputTexture.GetSRV(),
00643:              m_WorkBuffer[1].GetSRV(),
00644:          };
00645:          m_pDeviceContext->PSSetShaderResources( 0, 2, pSRV );
00646:          m_pDeviceContext->PSSetSamplers( 0, 1, &m_pLinearClamp );
00647:  
00648:          // 描画.
00649:          m_Quad.Draw(m_pDeviceContext);
00650:  
00651:          // シェーダリソースをクリア.
00652:          ID3D11ShaderResourceView* nullTarget[] = { nullptr, nullptr, nullptr, nullptr, nullptr };
00653:          m_pDeviceContext->PSSetShaderResources( 0, 2, nullTarget );
00654:  
00655:          // テキストを描画.
00656:          OnDrawText();
00657:      }
00658:  
00659:      // コマンドを実行して,画面に表示.
00660:      m_pSwapChain->Present( 0, 0 );
00661:  }
CPU側は入力画像をガウスブラーでぼかして,ゴーストの元になるテクスチャを生成します。あとはこれをゴーストを出したい分だけ書きます。GPUに送るパラメータはRGBに乗算カラー,A成分にテクスチャスケールを格納しています。パラメータは見た目が良い感じになるように調整したもので,とくに根拠はありません。色やゴーストの位置等を調整したい場合は485行目と552行目あたりに書いてあるパラメータをいじってください。
川瀬さんみたいに多くのゴーストを出すためには,下町のナポレオンさんのサイトに書いてあるようにレンズゴーストを適用した描画結果にしたてピンポンレンダリングすればよいようです。このピンポンレンダリングの処理を549行目あたりから行っています。

GPU側はCPU側から送られたテクスチャスケール値を使って,テクスチャフェッチを行い,このフェッチした色に乗算カラーを掛け合わせます。あとはエッジのアーティファクトがでないようにフェードアウトの重みを掛けてあげます。
下町のナポレオンさんのサイトでは,計算でフェードアウト項を求めているようですが,うちのページではオフラインでフェードアウト用のテクスチャを作成して,それをフェッチしてフェードアウト項として使用するようにします。フェードアウトに使用するテクスチャは下記のようなもので,適当に作りました。



上記のテクスチャを作成するコードは次のように作りました。
00007:  //-------------------------------------------------------------------------------------------------
00008:  // Inlcudes
00009:  //-------------------------------------------------------------------------------------------------
00010:  #include <vector>
00011:  #include <stb/stb_image_write.h>
00012:  
00013:  
00014:  //-------------------------------------------------------------------------------------------------
00015:  //      BMPファイルに保存します.
00016:  //-------------------------------------------------------------------------------------------------
00017:  void save_to_bmp(const char* filename)
00018:  {
00019:      auto w = 64;
00020:      auto h = 64;
00021:      std::vector<uint8_t> images;
00022:      images.resize(w * h * 3);
00023:  
00024:      auto threshold = 0.5f * 0.75f; // [0, 0.5]
00025:      auto max_r = 0.5f;
00026:  
00027:      for(auto y=0; y<h; ++y)
00028:      for(auto x=0; x<w; ++x)
00029:      {
00030:          auto px = (1.0f / w) * x;
00031:          auto py = (1.0f / h) * y;
00032:  
00033:          auto dx = px - 0.5f;
00034:          auto dy = py - 0.5f;
00035:          auto r = sqrt(dx * dx + dy * dy);
00036:  
00037:          float color = 1.0f;
00038:          if (r >= threshold && r <= max_r)
00039:          {
00040:              auto t = (r - threshold) / (max_r - threshold);
00041:              color = (1.0f - t);
00042:          }
00043:          else if (r > max_r)
00044:          { color = 0.0f;}
00045:  
00046:          auto idx = x * 3 + y * w * 3;
00047:          images[idx + 0] = static_cast<uint8_t>( color * 255.0 + 0.5 );
00048:          images[idx + 1] = static_cast<uint8_t>( color * 255.0 + 0.5 );
00049:          images[idx + 2] = static_cast<uint8_t>( color * 255.0 + 0.5 );
00050:      }
00051:  
00052:      stbi_write_bmp(filename, w, h, 3, images.data());
00053:  }
00054:  
00055:  //-------------------------------------------------------------------------------------------------
00056:  //      メインエントリーポイントです.
00057:  //-------------------------------------------------------------------------------------------------
00058:  int main(int argc, char** argv)
00059:  {
00060:      save_to_bmp("mask.bmp");
00061:  
00062:      return 0;
00063:  }

今回入力画像には以下のテクスチャを使用しました。超適当ですいません…。


シェーダは下記のように実装しました。
00013:  ///////////////////////////////////////////////////////////////////////////////////////////////////
00014:  // VSOutput structure
00015:  ///////////////////////////////////////////////////////////////////////////////////////////////////
00016:  struct VSOutput
00017:  {
00018:      float4 Position : SV_POSITION;
00019:      float2 TexCoord : TEXCOORD;
00020:  };
00021:  
00022:  ///////////////////////////////////////////////////////////////////////////////////////////////////
00023:  // CbLensGhost constant buffer.
00024:  ///////////////////////////////////////////////////////////////////////////////////////////////////
00025:  cbuffer CbLensGhost
00026:  {
00027:      float3 MultiplyColor : packoffset(c0);      // 乗算カラー.
00028:      float  Scale         : packoffset(c0.w);    // テクスチャスケール.
00029:  };
00030:  
00031:  //-------------------------------------------------------------------------------------------------
00032:  // Textures and Samplers.
00033:  //-------------------------------------------------------------------------------------------------
00034:  Texture2D       ColorBuffer  : register(t0);    // 入力画像.
00035:  Texture2D       MaskBuffer   : register(t1);    // マスク画像.
00036:  SamplerState    ColorSampler : register(s0);    // リニアサンプラー
00037:  
00038:  //-------------------------------------------------------------------------------------------------
00039:  //      メインエントリーポイントです.
00040:  //-------------------------------------------------------------------------------------------------
00041:  float4 main(const VSOutput input) : SV_TARGET0
00042:  {
00043:      float4 result = 0;
00044:  
00045:      float2 uv    = (input.TexCoord - 0.5) * Scale + float2(0.5f, 0.5f);
00046:      float4 color = ColorBuffer.SampleLevel(ColorSampler, uv, 0);
00047:      float  mask  = MaskBuffer .SampleLevel(ColorSampler, uv, 0).r;
00048:      
00049:      result.rgb = color.rgb * MultiplyColor * mask;
00050:      result.a   = 1.0f;
00051:  
00052:      return result;
00053:  }

実装出来たら実行して確認してみましょう。


それっぽく,出ていますね。
本家と比べるとクオリティがちょっといまいちな感じですが,一応実装できたということにしておきましょう。


 5. 最後に
 今回は,レンズゴーストについて学習しました。
次回は,平均輝度値の算出をやって,これまで学んだ内容を組み合わせて実際にグレアエフェクトを実装してみます。


 Download
本ソースコードおよびプログラムはMIT Licenseに準じます。
プログラムの作成にはMicrosoft Visual Studio 2017 Community を用いています。