Log: KI

雑記ブログです。大体プログラミングのあれこれを書きます。

DirectX11のレンダリングパイプライン

はじめに

この記事はNCC Advent Calendar 2018の参加記事です。

今日は予定を変更して、2日間苦闘を強いられたDirectX11で用意されているレンダリングパイプラインの設定手順を備忘録的にざっくりとまとめます。 特に、昨日一昨日で組んだプログラム(https://github.com/Kuyuri-Iroha/Draw-PMX)に使用した部分の解説になります。説明しないところがたくさんありますので、ご注意ください。

レンダリングパイプラインの処理順序

MicrosoftWindows Dev Centerの「Graphics Pipeline - Windows applications」に全体の動きは載っているのですが、今回使ったところをまとめると以下のような順序で処理が行われます。

  1. インプットアセンブラ
  2. 頂点シェーダー
  3. ラスタライザー
  4. ピクセルシェーダー
  5. アウトプットマージャー

リストアップしてみるとかなり簡素ですね。 この内、プログラマブルなのは頂点シェーダーとピクセルシェーダーの2ステージだけですが、DirectX11では全てのステージをプログラマが初期化する必要があるため、かなり処理が長いです。 それでは、順番に私の理解を説明してゆきます。

1. インプットアセンブラ

Input-Assember、略してIAです。Direct3D 11(DirectX11 SDKの3D処理を担うライブラリ)でもpContext->IASetInputLayout()のように略称が関数名に使われています。 このステージでは主に頂点シェーダーに頂点バッファを渡す処理をします。またこの時、頂点インデックスをもとに頂点バッファーを整理する処理も担います。 また、プリミティブタイプの設定もこのステージに対して行います。これは、Point List や Line List、Triangle List のように、渡した頂点をどう解釈するかの設定となります。

2. 頂点シェーダー

Vertex Shader、略してVSです。このステージでは主に、頂点座標をスクリーン座標系に変換します。 これは3Dの座標系は3軸であるのに対して、描画するスクリーンの座標系は2Dなので2軸です。この異なる座標系間を破綻なく処理するための変換です。

Direct3D 11のスクリーン座標系はだいたいこんな感じになっています。

f:id:kuyuri-iroha:20181217205225p:plain
スクリーン座標系

これはWindowの縦横比が変わっても同じなので、つまり正規化されているということです。

さて、頂点座標変換は

  1. ワールド座標変換
  2. ビュー座標変換
  3. プロジェクション座標変換

の順で行われます。 これは、3Dのキャラクターを描画する時に例えるならば、

  1. 3Dキャラクターの現在位置、回転、スケーリングを頂点に適用する。
  2. カメラから見た相対座標系に変換する。
  3. 2Dのスクリーン座標系に落とし込む。

というプロセスであるとも考えられます。 上記の操作をすることで、3Dの空間データが2Dのスクリーンに映し出されるわけですね。

ちなみに、後のラスタライザで深度カリングをしたり、アウトプットマージャーで深度テストをするためにZ座標を捨てるわけではありません。 また、法線やUV座標は法線マップなどのテクスチャ形式でない限りは頂点と対応した形で用意されることがほとんどなので、頂点バッファに一緒にパッキングします。加えて、今回行った処理内容では頂点シェーダーでの加工はせず、そのまま次のステージに渡してしまいます。

3. ラスタライザー

Rasterizer、略してRSです。このステージでは主に、ポリゴンの表裏判定、ポリゴンが覆っているピクセルの選定、頂点シェーダーから渡された値の線形補間を行います。

また、深度テストで重なるポリゴンのピクセルを破棄する(ピクセルシェーダーに渡さない)ようにしたり、スクリーン座標系からはみ出てしまった部分を破棄する。というような、レンダリングパイプラインの中でも相当重要な処理を担います。

Direct3D 11やOpenGLでは1回の描画内で他の面に重なって見えなくなってしまうピクセルを破棄する深度カリングの無効化や、ポリゴン裏のピクセル破棄の無効化などの設定はできますが、基本的にはブラックボックスとなっています。

このラスタライザーを通して、頂点から生成されたジオメトリをピクセルシェーダーで1ピクセルずつ処理できるように加工して渡します。

4. ピクセルシェーダー

Pixel-Shader、略してPSです。このステージでは言わずもがなピクセルの色を決定します。

例えば、前日の「HLSLでトゥーンレンダリングをしたかったお話」で遊んでいたときの

f:id:kuyuri-iroha:20181217011429p:plain
法線描画
これは、頂点バッファとして1頂点と対応する形でGPUに送った法線をラスタライザーで線形補間した(x, y, z)というベクトルをそのまま(r, g, b)という風に色情報としてアウトプットマージャーに渡して描画したものです。 Direct3Dでは左手座標系を使っているので、画面に向かって右方向(X軸方向)に向いている面が赤く、上方向(Y軸方向)に向いている面が緑色になっている事がわかります。(Z軸方向はこの時画面に向かって奥側となっているので、青い面はこの視点からは確認できません。)

また、あまり使い所はありませんが、ただただ単色に塗りつぶすこともできますし、Direct3Dのシェーダー言語であるHLSLの文法的に間違っていなければ、結構自由に塗り絵が楽しめます。ちなみに、送出するデータ形式(r, g, b, a)成分の4次元ベクトルとなります。

さらに、ここで複数の描画ターゲットに対して別の色(データ)を送出することができます。これが、前回の記事で苦しんだMRT (Multi Rendering Target) です。

5. アウトプットマージャー

Output-Merger、略してOMです。このステージでは、ピクセルシェーダーから送出されたピクセル値(色)を描画ターゲットと呼ばれるテクスチャの一種に書き出します。 この時、ブレンドステートや、デプスステンシルステートという設定オブジェクトによって、描画ターゲットにもともと描画されているピクセル値とどうブレンドするか、または完全に上書きするかなどの色に関する処理方法や、深度テストと呼ばれる描画ターゲットにもともと書き込まれているピクセルより奥になるピクセルを描画しないようにする処理の有効化・無効化などを設定できます。

また、ここではステンシルに関しては私の中でイメージができていないため説明を省略させていただきます。ただ、今回のプログラムではステンシルテストを無効化しているため、あまり効果はないものとなっています。

描画完了

ざっくりと以上のステージを経て描画が完了します。 バックバッファに描画してスクリーンに表示するには、予めバックバッファ用の描画ターゲットビューオブジェクトと呼ばれるものを用意して、そこへ描画する必要がありますが、今回はレンダリングパイプラインというトピックからは若干逸れるので説明を省きます。

また、プログラマブルシェーダーへ定数としてのデータを渡すことに焦点を当てると、定数バッファであったり、シェーダリソースビュー、サンプラーステートなどがあります。特に定数バッファに関してはDirect3D 11では4 byteで割り切れるデータサイズである必要があるため注意することも多いです。

おわりに

今回は予定を変更してレンダリングパイプラインの私の理解を大雑把に説明しました。 勢いで書いている節があるのと、あくまで「私の理解」であると言うことで、詳しく知りたい方は序盤にも紹介した

Graphics Pipeline - Windows applications

を読めば正確で詳細な情報が手に入るので、そちらを参照ください。

HLSLでトゥーンレンダリングをしたかったお話

はじめに

この記事はNCC Advent Calendar 2018の参加記事です。

前日はこんなことをやっていました。 MMDのモデルデータをざっくり読み込む この流れで、今日はトゥーンレンダリングに挑戦してみようと思って作業を始めたのですが、MRT (Multiple render targets) がうまく実装できなくて頓挫してしまったので、その所感のようなものをつらつらと書いていきたいと思います。 少し、何も気にせず文章を書いてみたいという欲求もあったので、好き勝手に書くという意味で今日の分ははてなブログの方に書きました。(せっかくちょっと整えましたしね)

前回のあらすじ

DirectX11のレンダリングパイプラインを組んで、PMXファイルを読み込んで、頂点とテクスチャだけでレンダリングしました。

f:id:kuyuri-iroha:20181217005905p:plain
Koron-Miku-2.1-TextureOnly

そこそこ絶望的な所要時間でしたが、なんとか表示できるまでに至ったので、まあここまでは及第点でした。

最初にしていた遊び

法線を定数バッファで渡して表示したり、

f:id:kuyuri-iroha:20181217011429p:plain
KoronMiku-Normal

法線とディレクショナルライトベクトルの内積を正規化して表示してみたり、(私はこの絵の雰囲気が一番好きです。心が高揚します。)

f:id:kuyuri-iroha:20181217011842p:plain
KoronMiku-NormalDotLight

法線とディレクショナルライトベクトルの内積でシェーディングしてみたり、

f:id:kuyuri-iroha:20181217012024p:plain
Koron-Miku-NormalDotLightShading

上の絵にスペキュラーの値を適当に使って明るくしてみたり、

f:id:kuyuri-iroha:20181217012346p:plain
Koron-Miku-UseSpecularParam

こういう遊びはかなり楽しくて好きです。パラメータを弄って1分くらい表示結果を見てニマニマしてました。

トゥーンレンダリングをしていく

それから間もなくしてトゥーンレンダリングの実装に入りました。 トゥーンシェーダーをつくる このサイトを参考にして、ハイライトと1号影を実装したまでは良かったのですが、境界線を描くのが思ったより難しく、頓挫の原因となってしまいました。

MRT

エッジを検出するには、ラプラシアンカーネルを使ったフィルタリングをすることで、周辺ピクセルと現在のピクセルを比較する必要があるのですが、エッジを元の絵に重ねて描画するためにポストエフェクトとして実装する必要がありました。

ラプラシアンカーネルに関してざっくり説明すると、中心ピクセルのウェイト(乗算値)を8.0、周辺の8ピクセルのウェイトを-1.0とすることで、周辺ピクセルが同一なら演算結果は0.0に、異なれば異なるほど、演算結果の絶対値が大きくなるというものです。

これ自体はかなり単純な処理なので良かったのですが、前述の通りポストエフェクトとして実装する関係で、「ライティング済みのシェーダリソース」と「テクスチャを貼り付けただけのシェーダーリソース」の2つの、レンダーターゲット兼任のシェーダーリソースが必要でした。それを実現するために、MRTを実装しようとしたのですが、両方のシェーダーリソースが同一の値になってしまい、うまく動作させることができませんでした。

そんなこんなで、苦肉の策として、視線ベクトルと法線ベクトルの内積から「視線に対してのエッジ」を検出する方法を実装してみました。

f:id:kuyuri-iroha:20181217022219p:plain
Koron-Miku-Cstyle-Toon?

はい、かなりひどいです。やはり、法線だけではどうにもなりません。その上、この方法では、四角形などの角ばった形状のものは悲惨な描画結果となってしまいます。 本当はこんな感じでエッジを実装するみたいです。とても綺麗に描画されていて驚嘆します。

MRT を利用した多重エッジ検出 MRT を利用した多重エッジ検出-wgld.org

おわりに

今回は割と大失敗で、とっても辛いお気持ちです。 しかし、明るめに考えれば、2Dの板を画面上に表示するのが結構簡単にできることだというのが今更ながらにわかったので、それはそこそこの収穫だったとお茶を濁すこともできるかなと思います。

明日のアドベントカレンダーはこの2日間で苦闘を強いられたDirectX11のレンダリングパイプラインについてつらつらしたいと思います。

モーションブラーをかけよう

今回はCCC Advent Calender 2018の参加記事ということで、こんな感じのモーションブラーを簡単に実装するやり方を解説します。 また、ここではGLSLの解説は省かせていただきます。ご了承ください。

このモーションブラーは@FMS_Catさんの240x240上で公開されている作品を参考にして作成しました。

目次

概要

具体的なやり方を紹介する前に今回紹介するモーションブラーの手順をざっくりと説明します。

  1. テクスチャにモーションブラーさせたいシーンを描画。
  2. スクリーンにテクスチャを加算合成。
  3. 1, 2の手順を繰り返し、時間変数を調整してフレーム間を補間

シーンの描画後に行うことからわかる通り、この手法はポストエフェクトと呼ばれるものです。そのため、スクリーンに描画するシーンをすべて描画してからブラーをかけます。 また、ブラーをかけたいシーンでは時間変数を用意して、その時間変数の値によってアニメーションするようにします。(私は大抵 0.0-1.0 に正規化しておきます)

仕組み

1. シーンをテクスチャへ描画

まず、通常の描画時と同じような手順で、テクスチャにシーンを描画します。このとき、このテクスチャはGPUからの読み取りと書き込みが可能であれば問題ありません。 Processingでは、シーンの描画ができる(レンダーターゲットになる)テクスチャはPGraphicsを使うことで扱えるので、それを使って描画します。 詳しくは「PGraphics」で調べるなどしてみてください。描画するターゲットが変わるだけなので、そこまで理解に苦労はないと思います。

簡単に例をあげると、通常1つ目のコードになるところを2つ目にのようになるように書き直すということです。

void draw()
{
  clear();
  
  pushMatrix();

  fill(#ffffff);
  translate(width/2, height/2, 0);
  sphere(20);

  pushMatrix();
}
PGraphics target; // 初期化処理は省略

void draw()
{
  target.clear();
  target.pushMatrix();

  target.fill(#ffffff);
  target.translate(width/2, height/2, 0);
  target.sphere(20);

  target.pushMatrix();
}

上記のコードではスクリーンに何も描画されませんが、image()を使ってtargetの内容をそのままスクリーンに描画することもできます。

2. スクリーンにテクスチャを加算合成

次に、スクリーンへ先ほど描画したテクスチャを描き写しますが、その時に適当な透明度を設定して加算合成します。 加算合成自体はもともとAPI等に用意されている場合があります。(PrcessingではblendMode()を使えば加算合成モードに切り替えられます。)しかし、基本的にメッシュ単位で加算されてしまったりで少し面倒だったので今回はGLSLのフラグメントシェーダーで実装しました。 Processingのコードはこちらになります。

// 加算合成時の加算したいテクスチャのα値
uniform float m;
// Processingで決まっている、フィルターをかけられるテクスチャ
uniform sampler2D texture;
// 手順1で描画したシーンのテクスチャ
uniform sampler2D targeted;

void main()
{
  // 描画するピクセルの座標をセット
  ivec2 p = ivec2(gl_FragCoord.xy);
  // (スクリーン) + (手順1で描画したシーン) * m で加算合成
  gl_FragColor = texelFetch(texture, p, 0) + texelFetch(targeted, p, 0) * m;
}

少し補足すると、このuniformはCPUからGPUへ送った定数データで、このシェーダの処理が始まる前にセットするものです。

3. 繰り返してフレーム間を補間

最後に手順1, 2で行なった処理を繰り返します。 繰り返す回数は見栄えによりけりで調整するのがいいかと思いますが、一番重要なのはループ内で、アニメーションに使っている時間変数を1フレーム進む分より小さい値で進ませることです。 私のイメージとしては、「時間解像度を引き上げる」ような感じですが、「ループ内で∆tだけ時間を進める」というような表現もできそうです。 これにより、ループさせた回数だけフレーム間の補間ができるようになり、物体が動いた方向にぼかす(モーションブラーをかける)ことができます。

しかし、手順2においてm = 1.0として加算合成をループ回数分行なってしまうと、フレーム間の動きが少ないものが全て白とびしてしまいます。 背景が黒の場合は演出としてありかもしれませんが、これではモーションブラーにはなりません。 そこで、ループ毎にmの値を調整して、あるループ回数の時はm = 1.0、またあるループ回数の時はm = 0.01のようにすることでいい感じにブラーがかかってくれます。 今回紹介する私のコードでは、ループの最後の加算合成時はm = 0.2、ループの最初の加算合成時はm = 0.0になるようにm = cos(PI/2*i/10) * 0.2という感じで調整しています。

実装

最後に、実際に冒頭のTweetのプログラムで使っているモーションブラーのpdeファイルのコードを載せておきます。(フラグメントシェーダは手順2で載せたものが全てです。) ここで載せているコードではモーションブラーとして最低限動作に必要なもの以外省略しているので、全文読みたい方はgithub.com/Kuyuri-Iroha/pendulumにおいてあるのでご自由にどうぞ。

// Kuyuri Iroha
// 2018

PGraphics target;
PShader blurShader;
final int END_FRAME = 200;

void setup()
{
  size(500, 500, P3D);
  target = createGraphics(width, height, P3D);
  blurShader = loadShader("blur.glsl");
}

void draw()
{
  clear();
  for(int i=0; i<10; i++)
  {
    /*
    [0.0, 1.0] の間で時間変数t が動き、
    forループ内では(i / 1500)づつ増えてフレーム間の補間を行います。
    */
    float t = float(frameCount % END_FRAME) / END_FRAME + float(i) / 1500;

    target.beginDraw();

    //ここでシーンを描画

    target.endDraw();

    // uniform としてGPUへ値を渡す
    blurShader.set("rendered", target);
    blurShader.set("m", cos(PI/2*i/10)*0.2);

    // filterで簡易的にフラグメントシェーダを実行
    /*
    先ほどのフラグメントシェーダのtextureは、
    このfilter()を使用するとProcessing側がGPUに渡してくれるもの
    */
    filter(blurShader);
  }
}

後書き

今回はProcessingで簡単にできるモーションブラーの解説を行いました。 他の言語、環境でも大抵似たような手法で実装できるとは思いますが、PGraphicsのおかげでかなり簡単にできるようになっていると思います。 モーションブラーは適用するだけで描画される物質にグッと質量感が出るので、おすすめです。

また今回のような描画系の小ネタ的なものを書こうと思います。 それではまた。

このブログに書くことについて

はじめまして。このブログでは私 Kuyuri Iroha が行っていることの雑記をして行きたいと思います。

目次

だいたいこんなこと話すよ

  • プログラミング
  • イラスト
  • 動画編集

プログラミング

最近はProcessingでのグラフィック系のプログラミング、Python機械学習の勉強、Node.jsサーバーの試運用。 グラフィック系なら高速化話題より、表現手法に興味があります。(実現に使っている数式とか) Web系ならフロントエンドより、バックエンドのほうが興味があります。 機械学習は気分で勉強中です。

イラスト

トレスとか模写とかをして練習中です。自分の絵がかけるのように早くなりたい。

動画編集

ゲームの実況動画をのんびりほそぼそと投稿しているので、編集中に気がついた、調べたこととかを書いていきます。

注意

このブログはあくまで雑記用なので、いろいろ書きます。近況のこととかも書くかもしれませんし、しれっとここに書いてある内容を変更しているかもしれません。