2023年12月13日水曜日

DirectX12 DrawText その2

前にDrawTextをする為の仕組みをGetGlyphOutlineを使って作った。
Unicodeの特殊事例に対して対処できたのはサロゲートペアだけで、結合文字や絵文字に対してはGetGlyphOutlineでは扱えずビットマップが取得できない。
この時はDirect2DとGDIのどちらにするか迷ってGDIを選択したけど、絵文字も表示するとなるとDirect2D(DirectWrite)しか選択肢がなかった。


IDWriteTextRenderer


GDIのGetGlyphOutlineに当たる機能はDirect2Dでは何になるかを調べたところ下記になるらしい。
グリフ メトリック -- IDWriteFontFace::GetDesignGlyphMetrics
実際のアウトライン情報 --IDwriteFontFace::GetGlyphRunOutline
グリフ ビットマップ -- IDWriteRenderBitmapRenderTarget::DrawGlyphRun

更に調べていくと、マイクロソフトのサンプルのDWriteHelloWorld/CustomTextReadererにたどり着いた。

CustomTextReadererはIDWriteTextRendererを継承して作られたクラス。
前回、WicBitmapRenderTarget経由でIWICBitmapに書き込むことは出来ていたけど、今回はCustomTextReadererで書き込むことに成功。
今回はビットマップのフォーマットをGUID_WICPixelFormat8bppAlphaにすることにより、GetGlyphOutlineで得られるデータに近いものが得られるようにした。

で、Bitmap自体は取得できたけど、グリフ情報は得られていない。
IDWriteFontFace::GetDesignGlyphMetricsで得られるらしいけど、IDWriteFontFaceを得る方法がかなり難しそうで、ファイルを指定しないといけなかったり、IDWriteFontFamilyやIDWriteFontCollectionからもたどり着くのが難しい。
一体どうすればいいのか悩んでいたけど、CustomTextReadererの中に答えがあった。

CustomTextReaderer::DrawGlyphRun関数の引数内にglyphRunがあり、glyphRun.fontFaceがIDWriteFontFaceだった。
これを利用すれば、GetDesignGlyphMetricsが呼び出せてDWRITE_GLYPH_METRICSが取得できる。
IDWriteFontFace::GetMetrics関数でDWRITE_FONT_METRICSが取得でき、この2つを組み合わせると下記の情報が得られる。

fm:DWRITE_FONT_METRICS
gm:DWRITE_GLYPH_METRICS



gm.advanceWidthが次の文字までの距離で、GLYPHMETRICSで言うところのgmCellIncXに当たる値。

書き始めの基準が薄い赤の枠の左端で、次の文字の書き始めが右端になる。
左端はDraw関数に指定した位置で、右端はadvanceWidthを足した位置になる。
上辺は、Draw関数に指定した位置にfm.ascentを足して、gm.verticalOriginYを引いた位置で、下辺は、Draw関数に指定した位置にfm.ascentとfm.descentを足した位置になる。

実際の文字のデータが含まれる範囲(濃い赤枠)を得るには、薄い枠の範囲にleftSideBearing、rightSideBearing、topSideBearing、bottomSideBearingを引いた値となる。
Aの場合はすべての値がプラスで薄い赤枠の内側になる。
jの場合は、leftSideBearing、topSideBearingはマイナスで薄い青枠の外側になり、描画開始位置が前の文字と被ることになる。
(GLYPHMETRICSのgmptGlyphOrigin.xがマイナスの場合と同じ)

また、これらの値の単位が違うので変換する(fm.designUnitsPerEmで割って、glyphRun->fontEmSizeを掛ける)必要がある。
得られるのは小数点を含むデータでピクセル単位に変換する際、top、leftは切り捨て、bottom、rightは切り上げした。メイリオの通常ではこれで問題ないけど、別フォントでイタリックにすると、縦が1ドット足りない場合はあった。必要に応じてマージンを設定するといいかも。


絵文字対応



通常の文字であれば、1つのグリフ情報だけが得られてそのSideBearingを計算すればよかったけど、例えば「👨‍👩‍👧‍👦」の絵文字の場合得られるグリフ情報が4つ返却され、それぞれの絵文字のSideBearingを見るだけだと計算が狂ってしまう。

glyphRunにはglyphAdvancesとglyphOffsets(※)があり、glyphOffsetsにはさらにadvanceOffsetとascenderOffsetが含まれている。
これらの値は、前述の単位変換は不要。
※glyphOffsetsはnullの場合があり、その時は0扱いとする。

メイリオ24で「👨‍👩‍👧‍👦」のDrawを行うと下記のグリフ情報が得られる。
X Y W H A OX OY
2.0 4.6 14.4 17.0 16.4 4.9 0.0
13.1 4.7 15.2 16.9 13.6 0.0 0.0
1.9 15.7 13.9 15.1 0.0 -25.4 0.0
息子 13.9 16.0 13.5 14.9 0.0 -13.6 0.0

X:advanceOffset + leftSideBearing
Y:ascent + topSideBearing - verticalOriginY - ascenderOffset
W:advanceWidth - leftSideBearing - rightSideBearing
H: advanceHeight - topSideBearing - bottomSideBearing
A:glyphAdvances
OX:advanceOffset
OY:ascenderOffset

父の描画範囲はadvanceOffset(4.9) + leftSideBearing(-2.9) = X(2.0)、ascent(25.8) + topSideBearing(-3.7) - verticalOriginY(17.4) - ascenderOffset(0.0) = Y(4.6)、advanceWidth(9.8) - leftSideBearing(-2.9) - rightSideBearing(-1.6) = W(14.4)、advanceHeight(22.5) - topSideBearing(-3.7) - bottomSideBearing(9.2) = H(17.0)という感じで算出される。それぞれの範囲を計算して、left、topの最小、right、bottomの最大を求めたのが絵文字全体の描画範囲となる。

上の方で「gm.advanceWidthが次の文字までの距離」と書いたが、実際にはglyphAdvancesの合計が次の文字までの距離となる。通常の1文字であればadvanceWidth=glyphAdvancesとなっているが、複数要素の絵文字の場合advanceWidthは、その要素単位の幅であって全体の幅には使えない。


結合文字対応



基本的には絵文字対応の内容で問題ないけど、よく5chなどでこんな書き込みがある。




m9(ด็็็็็็็็็็็็็็็็็Дด็็็็็็็็็็็็็็็็็)プギャーw

ブラウザ、VisualStudioのエディタはこの文字列の表示に対応してた。

VisualStudioエディタ

メモ帳は1行の範囲でクリッピングされていた。

メモ帳


いくらでも文字を合成できてしまうため、メモ帳と同じように制限を加える。
一時領域に描画しているため、水平方向に関してはマイナス、一時領域の幅以上の場合はクリッピング、垂直方向はマイナス、ascent + descentを超えた分はクリッピングする。

こうして見ると、ブラウザとメモ帳でadvanceOffsetの解釈が逆になっていて、VisualStudioはadvanceOffsetを無視してるっぽい。

スペース対応



最後にスペースの文字列について。
スペースはビットマップの範囲が0になっていたため、ビットマップ参照の為のLock関数でエラーになっていた。幅0の場合はビットマップ参照はスキップして、文字情報だけ書き込むようにする。
また描画の際、幅0の場合は何もせず次の文字までの幅だけを足すようにする。



2023年11月5日日曜日

Text Services Framework (TSF)

フルスクリーンでゲームを作る場合、IMEを制御して自動で表示されるものをすべて止めて、自前で描画する必要がある。

かなり昔に作ったことがあり、今回そのソースを移植して対応してみた。

一応動くようになったんだけど、その頃と違い最近は予測変換が表示されるようになっている。
このリストがうまく取得できない。
色々調べてみると、IME32は一度廃止された?されそうになった?がまた復活したという経緯があるらしい。
(個人のブログでIME32がなくなるから、今後は使えないというような記事を見つけた)

では新しいAPIは何になるのかというと、TSFというものらしい。
もしかするとこっちのAPIを使えば予測変換でも正しくイベントを受けられるかもと思い対応してみることにした。


TSF


Vistaの頃にできたものらしく、そこまで新しくはないAPIでCOMベースで作られているためかなり使うのにハードルが高い。
まったく知らなかった。

サンプルプログラムをダウンロードして実行してみるもいまいちよくわからない。
どうやら、TIP(Text Input Processor)を使う側と作る側両方のソースが含まれているようだ。
まずは個人で最低限のサンプルを載せてくれている人のソースをいくつか参考にしながら調査すると、ほんとの最小限は「ITextStoreACP」と「ITfContextOwnerCompositionSink」だけを実装すれば動くっぽい。

サンプルほぼ丸写しで実装してみた。
TextStoreはその名前の通り、EditBoxの持ってるテキストを管理するクラスという認識。
IME32の場合は、変換中の文字列だけが管理され変換後は別途管理が必要だったので一元管理できる分こっちのほうがいいかも。前後の文字列から変換結果を変えたり、変換済みの文字列を再変換できる機能があるからだと思われる。

内部で持つものは、「ITfDocumentMgr」「ITfContext」「ITfProperty」「ITfCategoryMgr」「ITfDisplayAttributeMgr」「ITextStoreACPSink

これらは内部で保持せず、使うたびに取り出す実装になっているのも見たがどちらがいいのかは判断つかず。MS-IMEとGoogle日本語入力を切り替えてもそのまま使えているので、切り替えタイミングをイベントで受けて、そのたびに作り直しということもないので保持しておくことにした。

最後のTextStoreACPSinkというのがイベントを通知させるためのもので、これをAdviseSinkした人にイベントが通知される。

ここでややこしいのが、TextStoreACPSinkを受け取るのがTIPの本体の方で自分ではない。
MS-IMEやGoogle日本語入力がAdviseSinkをしに来るから、それらに対してこちらからイベントを通知してあげる側になる。


変換文字列


先ほどのTextStoreACPSinkとは逆で、今度はこちらがイベントを受けることにより変換文字列の変更通知を受け取れるようにする。
ITfContextOwnerCompositionSink」を継承して、必要なイベント関数を実装する。
***SinkはAdviseSinkすると通知されるようになるはずなんだけど、これの為のAdviseSinkは見当たらない。CreateContextにITextStoreACP*として、自分自身を指定しているのでこれが代わりになっているのかも?

ここまでできるとImmGetCompositionStringの代わりができたことになる。

Property->EnumRangesで「IEnumTfRanges」を取得
EnumRanges->Nextで「ITfRange」を取得
Property->GetValueにRangeを指定して、「TfGuidAtom」を取得
CategoryMgr->GetGUIDにGuidAtomを指定して、「GUID」を取得
DisplayAttributeMgr->GetDisplayAttributeInfoにGUIDを指定して、「ITfDisplayAttributeInfo」を取得
DisplayAttributeInfo->GetAttributeInfoで「TF_DISPLAYATTRIBUTE」を取得
Range->QueryInterfaceで「ITfRangeACP」を取得
RangeACP->GetExtentで、変換中の区切り位置を取得
Enum分ループすると、それぞれのTF_DISPLAYATTRIBUTE.bAttrと、区切り位置で、変換中の文字列の区切り位置と属性が得られる。


変換候補


次に変換候補のリストを取得してみる。
メンバに「ITfUIElementMgr」を追加する。
イベントを受け取るためのインターフェース「ITfUIElementSink」を継承して、必要なイベント関数を実装する。
ITfThreadMgrEx」からQueryInterfaceで「ITfSource」を取り出し、Source->AdviseSinkで自分自身を登録する。
BeginUIElementを受けた際、一緒に渡されるbShowにFALSEを返却すると、変換候補のWindowが表示されなくなる。
また、ルール通りにTIP側が実装されていれば、FALSEを返した場合即座にUpdateUIElementが呼ばれるらしい。なので、リスト取得の実装はUpdateの方にのみしておけばいいみたい。

このイベントが受けられるようになるとImmGetCandidateListの代わりができたことになる。

UpdateUIElementに渡されたElementIDを使って、UIElementMgr->GetUIElementで、「ITfUIElement」を取得
UIElement->QueryInterfaceで「ITfCandidateListUIElementBehavior」を取得

※未解決
Google日本語入力だと、予測変換のタイミングでCandidateListの取得は失敗する。
Tabや↓矢印キー押下後は普通に取得できる。
MS-IMEでは予測変換のタイミングで取得できる。
ただ、MS-IMEでは予測変換時でも先頭の項目が選択された状態になっているため、↓矢印を押下すると2番目の項目が選択されてしまい、どちらも微妙。
変換候補を画面表示した状態であれば未選択状態になっており、↓矢印選択時に1番目が選択状態になる。これと同じ挙動にしたいが、やり方がわからず。

CandidateListUIElement->GetSelectionでリストの選択位置を取得
CandidateListUIElement->GetUpdatedFlagsでリストの前回からの変更点を取得
CandidateListUIElement->GetCurrentPageでリストの現在のページ番号を取得
CandidateListUIElement->GetCountでリストの項目数を取得
CandidateListUIElement->GetStringでリストの指定インデックスの文字列を取得

今のところ、UpdatedFlagsで取得した値が「TF_CLUIE_COUNT | TF_CLUIE_STRING」の時に、リストをすべて取り直すようにしている。
GetPageIndexを使えば、現在のページのみに絞れるためこっちを使った方がいいかも。


変換モード


通常タスクバーに埋まってる「A」とか「あ」とかの状態を取得する方法

変換モード

これがなかなか見つからなくて1週間くらい探してやっと見つけた。

メンバに「ITfCompartmentMgr」を追加する。
イベントを受け取るためのインターフェース「ITfCompartmentEventSink」を継承して、必要なイベント関数を実装する。

登録は、CompartmentMgr->GetCompartmentで、「ITfCompartment」を取得
Compartment->QueryInterfaceで、「ITfSource」を取得
Source->AdviseSinkで自分自身を登録するという流れ。

ただ2回行う必要があって、1つ目のGetCompartmentはGUIDにGUID_COMPARTMENT_KEYBOARD_OPENCLOSEを指定する。2つ目は、GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSIONを指定する。
1つ目はIMEのOpenとCloseのタイミングを得るためで、ImmGetOpenStatusの代わりになるもの。
2つ目は、ImmGetConversionStatusの代わりになるもの。

ちょっと癖があって、Close状態でも変換モードは直前のものが取れてしまうため、自分で「A」にする必要がある。Open状態はInputModeの値をそのまま使えばいい。
取得できるEnum値はTF_CONVERSIONMODE_*で、IME_CMODE_*と同じ値らしい。


これでImm系の関数を一切使わず、TSFに移行できた。

当初の目的の予測変換のタイミングについては、MS-IMEについては完ぺきに取得できるようになったが、候補リストの先頭が選択されている状態になっている問題は残っている。
Google日本語入力に関しては、予測変換が表示されるタイミングでOnUpdateUIElementは呼び出されはするが、候補リストを取得できない問題が残っている。



1/22追記


変換候補については、自前で描画せずに任せることにした。変換中は別Windowsが表示されることになるが大した問題ではないと思うことにした。
変換候補に実際は描画しない文字列もたくさん含まれているため、描画用のテクスチャがどんどん消費されていくのも防げる。

ItextStoreAcp::GetTextExtの引数に渡されるRECT型の変数に、変換中の矩形をスクリーン座標で返すと変換候補の画面を出す位置を調整してくれるようになる。
似たような関数でItextStoreAcp::GetScreenExtがあるが、面倒なので同じ座標を返すようにしている。今のところ不都合は発生していない。



2023年10月22日日曜日

International Components for Unicode (ICU)

Windowsで文字列を扱う際UTF-16を使っているが、ユーザ入力データを扱う場合まともには扱えないということがわかった。


サロゲートペア


UTF-16でも1文字は2バイトで表現しきれないので、サロゲートペアという苦肉の策が取られた。
これは今までのShiftJISなんかと同じで、最初のコードがこの範囲ならその後も含めて1文字と表すような感じで、ありがたみがない。
ただShiftJISと違って普段使うほとんどの文字は2バイトで表せるため、仮にサロゲートペアに対応してなくても不具合が顕在化しない場合もある。

内部処理的には問題はほとんど起きないが、文字数を制限するような場合に問題になる。
サロゲートペアが文字列中に含まれていると、含まれている文字数分文字数が増えてしまう。
10文字制限の場合、サロゲートペアが1文字含まれていると9文字入力した時点で制限にかかってしまい、ユーザの見た目とギャップが生まれる。

また、EditBoxなどに任せずに自前で1文字ずつ追加、削除をする場合、サロゲートペアを考慮してない場合は上位のみ削除して下位が残ったままだと表示に影響が出る。

サロゲートペアだけであれば、適切に範囲チェックを行い、文字数のカウントもサロゲートペアを考慮してやれば比較的簡単に対応できる。

ただ問題なのが結合文字や絵文字だ。


結合文字 合字 絵文字


例えばMS-IMEで、「あ」を入力して変換確定後、続けて「3099」と入力してF5を押下すると、「あ」とこのコードが合体して「あ゙」となる。
合体させるのはいくらでもよくて、ここに書かれているようなコードを何回も同じように付け足すと、どんどん付け足されて1文字扱いとなる。
この仕様はやばい。
どうやら、単純なルールは存在せず、ここに書かれていることをすべて理解して実装すれば文字の区切り判断はできるらしい・・・が、そんなことやってたら時間が足りないし、今後も仕様が追加されていくので実装しきれない。


ICU


というわけで、ICUというライブラリの出番。
この中のICU4CというのがC/C++用のライブラリなのでGitHubからVisualStudioで開いてみた。
allinoneというソリューションを開いてビルドすると何やらいろいろビルドされた。
やりたいことは「書記素単位で文字を取得する」ということになるので、インクルードファイルは「ubrk.h」、ライブラリは「icuuc.lib」でコンパイルは通った。
早速動かしてみると、「icuucNN.dll」がないと怒られる。
(NNはバージョン。試したときは74)
binから持ってきて再実行すると、「icudtNN.dll」がないと怒られる。
binから持ってきて再実行すると、U_MISSING_RESOURCE_ERRORというエラーになる。
icudt.dllはunicodeのデータが入っていて、本来は20MBくらいあるみたい。
自分の環境で出来上がったファイルは3KBしかなく、中身がなさそう。
どうやったら中身がある状態でビルドできるのかわからなかった。
ファイル指定もできるらしく、chromiumとかに含まれているicudt.datとかがおそらくこれのことっぽい。icudt.datを配置してu_setDataDirectoryで指定してみたけどうまく動かなかった。

次に試したのがnugetにあるicu4cのライブラリ。
古いものしかなくて一番新しそうな58というのをインストールしてみた。
こっちは問題なく動いた。icudt58.dllのサイズは19MBぐらい。

さらに調べてみると、Windows10以降にはマイクロソフト版ICUが含まれているらしい。
何も入れる必要ないみたいだ。
早速、インクルードファイル「icu.h」、ライブラリファイル「icu.lib」で試したら問題なく動いた。

「あ゙」はもちろん、「👨‍👩‍👧‍👦」も1文字として判定できるようになった。
対処してないと、「あ゙」は2文字、サロゲートペアを考慮しても2文字、「👨‍👩‍👧‍👦」は11文字、サロゲートペアを考慮しても7文字として判定されてしまう。




2023年10月15日日曜日

DirectX12 DrawText

DirectX12で文字描画する場合、最初に見つけた方法がID3D11On12Device経由でDirect2D/DirectWriteを使って描画するというもの。

最初はこの仕組みで満足していたんだけど、マルチスレッド対応とちゃんとしたダブルバッファリングの仕組みに変えてから、11on12を捨てたいと思うようになった。


マルチスレッドとダブルバッファリング


マルチスレッド&ダブルバッファリングで完全にCPUとGPUを並列に処理できるようになった時、Direct2D用の描画が気になるようになってきた。

描画コマンドはすべて別スレッドでコマンドリストに書き込むようになっており、メインスレッドの利用を極力抑えるようにしているが、Direct2D部分はマルチスレッド対応以前のメインスレッドが描画コマンドを発行する仕組みのまま。

いまはFPSぐらいしか表示してないので特に影響はなさそうだけど、大量の線、矩形、文字列を描画するような時に問題になってくると思われる。


フォントテクスチャ



というわけでDirect12だけにして、11on12はやめることにした。

といっても、文字描画の仕組みがなくなってしまうので裏でDirect2D/DirectWriteの仕組みで文字列を描画して、結果のビットマップをテクスチャにして表示してみようと思う。


DirectWrite版フォントテクスチャ


Direct2DのID2D1RenderTargetクラスにCreateWicBitmapRenderTargetという関数があったのでこれを使って文字描画したビットマップを取得してみる。
途中ハマったことは、IWICBitmap::CreateBitmapのピクセルフォーマットの指定。
最終的には、GUID_WICPixelFormat32bppBGRを指定してうまく行ったけど、アルファ付きに出来ない。アンチエイリアス部分が背景色に影響して結果が変わってくる。

下記は取得したビットマップの背景色を透過させてPNG保存した画像。

背景黒

背景色が黒だとアンチエイリアス部分に水色や茶色などが使われている模様。

グレースケール


SetTextAntialiasModeをD2D1_TEXT_ANTIALIAS_MODE_GRAYSCALEにすると想像していた結果になった。D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPEの方が見た目がいいんだろうけど、加工する場合はこっちのほうが使いやすい。

背景緑

背景を真緑にして、透過色として識別しようと思ったらこんな結果に。
透過を意識したデータを作れないので、これを使う場合はグレースケールになるかな。


GetGlyphOutline版フォントテクスチャ


昔、DirectX9とか?でID3DXFontが遅かった時代に使われていた手法で、GetGlyphOutlineを使ってフォントテクスチャを作って文字を描いていたらしく、この方法でも試してみた。

取得できる値は指定したクオリティのグレースケールのビットマップ。
これをアルファ値として別途真っ白のビットマップとして保存してみた。



GGO_GRAY2_BITMAP



GGO_GRAY4_BITMAP



GGO_GRAY8_BITMAP

それぞれの処理時間の違いは、ほとんどなさそう。0.2~0.9msとぶれ幅が大きいけど、大体が0.3ms前後だった。
これなら常にGGO_GRAY8_BITMAPで良さそう。

フォント作成時、CLEARTYPE_QUALITY、CLEARTYPE_NATURAL_QUALITYを指定するとClearTypeも使えるっぽい。ただ、CLEARTYPE_QUALITYはほとんど処理速度は変わらなかったけど、CLEARTYPE_NATURAL_QUALITYにすると約2倍になった。


比較


フォント品質


Direct2D版の方が見た目がきれい。アンチエイリアス部分に別の色が入るとキリッとする。ただ、取得したデータをキャッシュして色をシェーダ側でつけるとなると、グレースケールにすることになるので差はほとんど無くなる。GetGlyphOutlineは純粋に白のデータにアルファ値だけのテクスチャが作れるため、こちらの方が使い勝手がいい。


処理速度


2文字の場合、Direct2Dが1.5ms(1.2ms)に対して、GetGlyphOutlineは0.3ms。
20文字の場合、Direct2Dが1.5ms(1.2ms)に対して、GetGlyphOutlineは1.0ms。
※Direct2Dのカッコ内はグレースケールの処理時間
Direct2Dの方はこの程度の文字数の差によって速度は殆ど変わらなかった。
GetGlyphOutlineは文字が増えると処理時間も線形に延びていく。
どちらの仕組みでも、毎フレームのテクスチャ生成は現実的ではなさそう。

数字、アルファベット等よく使う字は予め生成し、ほかは逐次キャッシュしていく仕組みにしたり、ゲーム内で使う文字列をローカライズファイルに纏めておいて、そこから使ってる文字のテクスチャを事前生成するなど工夫すれば速度に関しては問題なさそう。



フォントテクスチャ作成



文字のビットマップを取得する


TEXTMETRIC oTM ;
::GetTextMetrics( hDC, &oTM ) ;

GLYPHMETRICS oGM ;
const MAT2 oMat = {{0,1},{0,0},{0,0},{0,1}} ;

tWC c = L'漢' ;
auto nSize = ::GetGlyphOutline( hDC, c, nGradFlag, &oGM, 0, nullptr, &oMat ) ;
tBin oData ;
oData->Resize( nSize ) ;
::GetGlyphOutline( hDC, c, GGO_GRAY8_BITMAP, &oGM, oData.Count(), oData.Val(), &oMat ) ;

TEXTMETRICGLYPHMETRICを使って、テクスチャに書き込みと描画する際の頂点データ用の値を準備する。

フォントのサイズについて



TEXTMETRICとGLYPHMETRICの関係図

一般的なフォントサイズといえばtmHeight。
それが、青い線(ベースライン)を挟んで、tmAscentとtmDescentに分かれる。
ビットマップの画像はgmBlackBoxX、gmBlackBoxYのサイズで返却される。
ただし、返却されたメモリの横幅は4バイトアラインになっているため、4の倍数に補正する必要がある。
(例えばgmBlackBoxX=7の時、メモリの横幅は8となる)

auto w = oGM.gmBlackBoxX ;
auto nMask = a-1 ;
w = ( w + nMask ) & ~nMask ;


テクスチャの書き込み座標


最初の文字のX座標は0。次の文字はgmBlackBoxXをインクリメントしていく。
テクスチャの横幅が残りtmMaxCharWidth分ない場合は次の行に改行して0にリセット。
Y座標は、行数にtmHeightを掛けたものが基準。
その基準にtmAscent - gmptGlyphOrigin.yを足した位置が書き込み位置となる。


頂点座標


配置位置(=基準)にgmptGlyphOrigin.xを足す。注意点は、例えば一番最初の文字がjの場合、上図のようにgmptGlyphOrigin.xがマイナスになっている(前の文字に重なる)
必要に応じて位置を補正する
テクスチャの横幅はgmBlackBoxXなので、その分の幅を取る。
次の文字の開始位置は基準からgmCellIncX行ったところとなる。
高さは常にtmHeightで処理する。


パフォーマンス


横400文字、縦100行、計4万文字を表示してみたところ、FPSが20ぐらいになった。
試しに同じ条件でDirect2Dで描画すると60FPSで表示される。
文字列は1行0~9を40回繰り返した文字列を100行分表示している。
おそらく、100行分同じ文字列なのでうまく使いまわしているのかもしれない。
そこで全行ランダムにして表示するようにしたらDirect2DでもFPSが30まで落ちた。


パフォーマンスチューニング1


頂点バッファをx,y,u,v,r,g,b,aで作って1文字4頂点1セットでバッファを毎フレーム書き込んでいる。インデックスバッファは適当な文字数分予め用意しておき、足りなくなったらまたある程度余裕を持たせて作成するようにしていた。

1文字のデータはfloat(4byte)✕8✕4=128byteとなっているため、これを減らそうと考えた。
頂点バッファを渡すのをやめて、構造化バッファを渡して頂点シェーダ内で位置、UVを作ることにした。色に関しては一旦保留。

構造化バッファで渡す値は、x,y,w,h,u,v,uw,vhで、1文字のデータはfloat(4byte)✕8=32byteとなり1/4に削減した。
結果、FPSは25に上昇したけどまだDirect2Dに負けている。これなら新しい仕組みにする意味がない。


パフォーマンスチューニング2


渡しているx,yはディスプレイの座標なので、unsigned shortで問題ない。
また、w,u,vはフォントのマスタ情報を別途構造化バッファで作って、そのインデックスを指定するようにすれば、1つのunsigned shortで表現できる。
パフォーマンスチューニング1では、色を一旦保留にしたが、色マスタ情報作って同様の考え方にすれば1つのunsigned shortで表現できる。

というわけで、x, y, no, colとなり、1文字のデータはunsigned short(2byte)✕4=8byteで更に1/4となり、色情報も追加できた。
結果、FPSは30に上昇し、ほぼDirect2Dと同じになった。


追加の検証で、1行の文字列をまとめて描画ではなく1文字づつ描画するように変えてみた。

Direct2Dは3FPSとなり、1/10に落ちた。
新しい仕組みの方は12FPSでDirect2Dの4倍で動いてる。
Releaseビルドならこの状態でも60FPSでてる。
限界を探るため、今度は色をすべての文字でランダムにすると25FPSとなった。
色マスタ情報が大きくなり毎フレームの更新に負荷がかかるのだろう。
色数が多すぎる場合は、使う色を限定化してマスタの更新をなくせばこの状態でも問題なく動きそう。

結果的にドロ-コールを100回呼び出しで比較するとほぼ同じ速度で、4万回呼び出す場合は、マルチスレッド対応されている新方式の方が速くなった。


サロゲートペア対応


実装している最中気になっていたのが、サロゲートペアについて。
GetGlyphOutline関数に渡す値が1文字分のコードなので、サロゲートペアの場合だめだろうと思っていた。
サロゲートペアといえば「𩸽(ほっけ)」なので文字列に指定してみる。
Direct2Dでは問題なく表示されるが、GetGlyphOutlineでは中点が2つが表示される。
GetGlyphOutlineのコードを渡す型が32bitなので、まとめて渡してみてもだめ。

調べてみると、GetCharacterPlacement関数でGlyphIndexというものを取得して、GlyphIndex指定でGetGlyphOutlineを呼び出せば取得できるらしい。

tStr s = L"𩸽" ;
tWC nBuf[2] ;
GCP_RESULTS oGR = {} ;
oGR.lStructSize = sizeof(oGR) ;
oGR.lpGlyphs = nBuf ;
oGR.nGlyphs = 2 ;
::GetCharacterPlacement( hDC, s, oGR.nGlyphs, 0, &oGR, GCP_GLYPHSHAPE ) ;

関数がうまくいくと、oGR.nGlyphsは1になり、nBuf[0]にGlyphIndexが入っている。
GetGlyphOutline関数に渡していたGGO_GRAY8_BITMAPをGGO_GRAY8_BITMAP | GGO_GLYPH_INDEXに変えて、文字コードを渡していたところにGlyphIndexを指定するとサロゲートペアのビットマップが返ってくるようになった。



なんと読むのかもわからない文字だけど混在していても表示できている。


2023年10月5日木曜日

Motion Blend

今回はモーションブレンドをやってみる。


アニメーション


FBXをAssimpで読み込む際、アニメーションを含むメッシュの場合は一緒に読み込んでいる。
各アニメーションはそれぞれ切り替えれば作られた通り動きはするんだけど、アニメーションを切り替えた際、その前の姿勢を無視して切り替えたアニメーションIDの再生が始まるため、ものすごく不自然になる。


歩き⇔走りの切り替えアニメーション



Unreal Engineでゲーム制作をしている人の動画を見ているとき、2つあるアニメーションを混ぜて1つのアニメーションとして利用していた。
そんなことができるのかと驚いた記憶がある。

この仕組みを使えば、あるアニメーションから別のアニメーションへの切り替えもスムーズに行えるはず。


モーションブレンド


FKアニメーションの仕組みで、アニメーションのある時間の状態を作る際、前後の確定したキーフレームから、位置、スケール、クォータニオンのベクトルを取り出して線形補間をしている。
この処理がモーションブレンドなんじゃないか?
実は既に似たようなことはやっていたのか。
現在のアニメーションの補間されたベクトルと、切り替えるアニメーションの補間されたベクトル同士を補間すれば期待した動きになると予想。


ブレンド結果

あっさりうまく行った。
ただ、切り替えのタイミングによってはぎこちない場合もあったりする。
この辺はアニメーション同士のつながる位置のオフセット情報を設定できるようにしたりすれば常にきれいにつながるような気がする。





2023年10月1日日曜日

Particle5 Trail

前回に引き続きパーティクルの拡張をする。

今回は、トレールのメッシュを追加する。


トレールメッシュ


今までのメッシュはVertexBufferに書き込んで、そのあとメッシュデータを書き換えることはしなかった。
トレールメッシュの場合は毎フレーム書き込むため、今までの仕組みとは別にする必要がある。

通常のパーティクルは1つのオブジェクト=1パーティクルに対応しているが、トレイルの場合はどうしようか悩んだ末、複数オブジェクト=1パーティクルとした。
複数のオブジェクトはトレイルの軌跡を表す位置として利用する。

パーティクル内の複数データの管理は今まで名称で行ってきたけど、これは最初に考えたキャラクタ管理用のデータだったから。
ただ、パーティクルでもそのまま使っていたけど、ユニークな名称にする必要があるし、そもそも名前で管理してもあとから名前で参照することもないしメモリが無駄になる。
また、トレールの場合データのインデックスが重要になってくるが、今のデータ構造だとインデックス順に取り出すことが出来ないため、管理方法を変えることにした。

いろいろ考えた結果、Listで管理することにする。
トレールの場合、オブジェクトの追加を先頭にしていく必要がある。
Vectorだと追加の度にデータ全体をずらす必要が発生するためListのほうが都合がよい。
ただ、データの参照に不安が残るが、実際参照する場面では先頭から順番にアクセスする使い方しかないため問題にならなさそう。

頂点データを順番に、とりあえずy座標を-0.5、+0.5したものを並べていく。

表示してみると、なんとなくうまく行っているように見える。
しばらく動かしてみると、当然のことながら上下から見ると完全に見えなくなる場所が出てくる。


トレールのビルボード


視線のベクトルと、ノード間のベクトルの外積を使えば視線とトレールの線に直交する方向が得られるので、それに幅を掛けて+とー方向に足せばいい。


視錐台カリング


通常の仕組みに乗せて、各点単位に視錐台カリングを行う。
頂点バッファを作る際に、すべての点がカリング対象になっていたら描画をスキップする。


ホーミングレーザ


仕組みが出来たので、ホーミングレーザを作ってみる。
昔、Catmull–Rom Spline補間でレーザの軌跡を作ったけど、今回はこの記事を参考した。


パーティクルオブジェクトの位置を、この計算式で算出して追加するだけで、予定時間にその点を通るような加速度を算出してくれる。
予定時刻を過ぎたら、1秒間で徐々にアルファを0にして消す。
実際には命中して爆発などを起こすんだろうなぁ。



初速は視線のベクトルで、2秒後に赤い四角の的を射抜くホーミングレーザ。





2023年9月24日日曜日

Particle4 オーラ

前回に引き続きパーティクルの拡張をする。

今回は、円柱のメッシュを追加する。


円柱メッシュ


円盤と違って、円柱の場合は特に悩むことなくUV展開は出来そう。
矩形を円の分割数分つなげるだけ。

円柱

特に悩むことなくあっさり出来た。


上を広くした円柱

上を狭くした円柱

パラメータを追加して、上の半径を調整できるようにした。


オーラっぽい効果


魔法陣+オーラ

2つの円柱を、前回作った魔法陣に重ねてみた。


シェーダ

float2 uv = In.UV + float2( -0.7 * In.UV.y, 0.0 ) ;	// ひねり
Out.Col = Tex.Sample( Sampler, In.UV + float2( 0.0, Param.Time )) ;
float mask = ( sin( ( uv.x + Param.Time*0.5) * 8 * 3.1419 )+1 + cos( ( uv.x + -Param.Time) * 2 * 3.1419 )+1  ) * In.UV.y ;
Out.Col = pow( Out.Col, 1.25 ) * mask ;
Out.Col *= In.Col ;
Out.Emissive = Out.Col ;



2023年9月23日土曜日

Particle3 魔法陣

前回に引き続きパーティクルの拡張をする。

今回は魔法陣の演出をやってみたい。


円盤メッシュ


まずパーティクル用メッシュに用意したのは矩形メッシュ。
今回はこれに加えて、円盤のメッシュを追加しようと思う。
ただ、今まで円盤のメッシュを扱ったことがない。
やりたいことは、テクスチャの画像が円に沿ってぐるっと張り付いて、UVのU方向のスクロールが回転、V方向のスクロールが中心に吸い込まれていく感じに動いてほしい。

円盤のUVの展開について色々調べたけど全然見つからなかった。
仕方がないので自分で考えた方法がこれ。

UV展開図


円の分割数を指定すると各頂点の位置を計算する。
x = cos( i ÷ 分割数 ✕ 2π ) ※ i は0~分割数までのループカウンタ
y = sin( i ÷ 分割数 ✕ 2π )
u = i  ÷ 分割数

この図の場合、分割数3で中心に点0、UVは点0が一番上の中心。
そこから半径 (x,y) ✕ 0.5の位置に点2、(x,y) ✕ 約10%の位置に1の点を置く。
Vは点2が1、点1が上から約10%の位置。
後は分割数分ループする。


謎のエネルギー


とりあえずはメッシュにテクスチャを貼り付けて確認してみる。

セルラーノイズにフラクタルノイズを掛けたもの

まずノイズテクスチャを用意する。


円盤メッシュにテクスチャ展開

メッシュに貼り付けた結果。
なんかうまく行ってるっぽい。

マスクイメージ

上記にこのマスクを掛けて


ひねりを加えるとこうなって


色をつけたらこうなる。


最後にUVスクロールでvのマイナス方向に動かすとこんな感じになった。

シェーダ


float2 uv = In.UV + float2( -0.7 * In.UV.y, 0.0 ) ;	// ひねり
Out.Col = Tex.Sample( Sampler, uv + float2( 0.0, -Param.Time )) ;	// マイナス:外側 / プラス:内側
float mask = ( 1 - cos( In.UV.y*2*3.14159265 )) * 0.5 ;
Out.Col = pow( Out.Col, 1.25 ) * mask ;
Out.Col *= In.Col ;
Out.Emissive = Out.Col ;




魔法陣


魔法陣テクスチャ

絵かきツールで適当に画像を用意して、背景をアルファ0になるようにして保存。


シェーダ


Out.Col = Tex.Sample( Sampler, In.UV + float2( Param.Time*-(( step( 0.5,In.UV.y ) + step( 0.5,In.UV.y ) - step( 0.75,In.UV.y ))*2-1)*0.1, 0.0f )) ;
Out.Col *= float4( 0.25f, In.Col.gba ) ;
Out.Emissive = Out.Col * 2 + In.Col.r * float4(1,1,1,1) ;

3つの階層に分かれているので、中心を時計回り、真ん中を反時計回りで高速、外側も反時計回りで低速で回すようにしてみた。
あと、出現時にEmissiveを最大にして光らせるようにした。
更新関数から受け取るrの値で制御している。


魔法陣




2023年9月19日火曜日

Particle2 炎

前回パーティクルの仕組みを作ったので今回はその続き

前回からの課題は、パーティクルを追加とともにシェーダ作成も行っていたので、当たり前なんだけどシェーダ作成は切り離すことにした。


パーティクルシェーダ登録


これまでのシェーダは基本的には固定で、決められたデータを追加するとそれに応じて描画されていた。
パーティクル用のシェーダはそれぞれがカスタムシェーダで、パーティクルの種類毎にシェーダが増えていくことになる。
システムから渡される固定のリソースは、カメラ、パーティクルオブジェクトのWorldマトリックス、時間。
これに加えて前回は入れてなかったカラー、ID、インデックスも追加した。

オプションでサンプラ、テクスチャ、ピクセルシェーダソースを指定できるようにした。



炎のエフェクト


パーティクルの仕組みが出来たので、なにか作ってみようと思う。
定番の炎にチャレンジする。

よく見るのが、ノイズのテクスチャを用意してUVスクロールさせるというもの。

まず、セルラーノイズを動的に作成してテクスチャとして登録する。
パーティクルは適当に散らばせて、上昇させる。
表面にノイズテクスチャを貼り付けるとこんな感じ。

シェーダ

Out.Col = Tex.Sample( Sampler, ( In.UV * 0.5f )) ;


テクスチャを貼り付けただけ


全部同じ見た目になってるので、ランダムにテクスチャの表示位置をずらして、タイマーでUVスクロールをする。
distanceを使って、円形にくり抜く。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.ID * 0.0001 ) ;
Out.Col = Tex.Sample( Sampler, ( In.UV * 0.5f ) + r + float2( 0.0f, Param.Time*0.3 )) ;
Out.Col.a *= step( 0.5, d ) * d ;


丸くなった


登録時に色をランダムに設定する。(赤は192~255、緑は96~159、青は64~95)
また、更新タイミングで全体を減らしていく。
アルファは時間により255~0にする。
Emissiveにも同じように書き込む。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.ID * 0.0001 ) ;
float c = Tex.Sample( Sampler, In.UV * 0.5f + r + float2( 0.0f, Param.Time*0.3 )).r ;
Out.Col = In.Col * step( 0.5, c ) * c ;
Out.Col.a *= step( 0.5, d ) * d ;
Out.Emissive = Out.Col ;


それっぽくなった


ランダムのパラメータにIDを指定しているが、これをインデックスにすることにより、別のオブジェクトが消えたときにインデックスがずれて、テクスチャの表示する場所がワープすることにより、メラメラ感が増す。

シェーダ

float d = 1 - distance( float2( 0.5, 0.5 ), In.UV ) ;
float2 r = rand( In.Idx * 0.0001 ) ;
float c = Tex.Sample( Sampler, In.UV * 0.5f + r + float2( 0.0f, Param.Time*0.3 )).r ;
Out.Col = In.Col * step( 0.5, c ) * c ;
Out.Col.a *= step( 0.5, d ) * d ;
Out.Emissive = Out.Col ;

メラメラ増し


パフォーマンス


調子に乗って連打しまくっていると、FPSがどんどん落ちて30台まで落ちた。
5000パーティクルぐらいでこれだと、先が辛そう。

1パーティクルに1スレッドをその場で生成してそれぞれ更新処理を任せてみたけど、1スレッドだけで処理したほうが速かった。
パーティクル専用スレッドを1つ用意して、処理対象を2つに分けてメインスレッドと分担するようにしてみたら、多少マシになった。
試しにリリースビルドでやってみたら60FPSから落ちることもなく、シングルスレッドでも60FPS維持してた。速くなることは確認できてるので、パーティクル専用スレッドはそのまま採用することにする。


まとめ


初めてエフェクト作ってみたけど、意外とすごいのが出来た。
エフェクトは1つだけでなく、複数のエフェクトを重ねて1つのエフェクトを作ってるみたいで、例えば今回の炎にしても、別途煙を炎の消えたあたりからモクモク出すといいらしい。


2023年9月14日木曜日

Particle

パーティクルの仕組みを実装してみる。

いろいろ調べていくと既存のツールではやれることがいろいろありすぎて、それと同じものを作ろうと考えるとなかなか進まない。

すでに1つのメッシュに対してオブジェクトを追加すると描画する仕組み自体はあるので、その仕組みにパーティクル要素を追加して肉付けしていくことにした。


パーティクル用メッシュ


色々なメッシュ形状が必要になると思うけど、とりあえず矩形を用意。
パーティクル用のメッシュの頂点データは、x,y,z,u,vのみにした。

後々、円盤とか、円柱とか用意する予定。


パーティクル用シェーダ


とりあえずパーティクル単位でシェーダを作るようにしてみる。

VS

Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], float4( In.Pos, 1.0f ))) ;
Out.UV = In.UV ;

頂点にはWorld、View、Projectionマトリックスを掛けて、UVはそのままPSに渡す。

PS

Out.Col = float4( 1, 1, 1, 1 ) ;

固定で白を出力。


パーティクル用パラメータ


既存のツールではかなり多くのパラメータが設定できる。汎用ツールなので殆ど使われないものも含んで細かくなってしまっているのだろう。
自分の実装としては、パラメータ化はせず生成と更新の関数を渡せるようにする。
たくさん作っていくうちに傾向、パターン化ができるようになれば毎回関数を作るのではなく、ライブラリ化したものを渡せるようになるはず。


生成関数


毎フレームオブジェクトを生成して100個作ったらやめるようにする。


更新関数


毎フレーム1ずつYを増やしていって、配置された位置から100行ったところで消滅するようにする。


実行結果


パーティクル自体は、オブジェクトが0になったタイミングで消滅するようにした。


この時点での実行結果

結果を見ると、とりあえず動いているように見えるが、カメラを動かすと背面カリングが効いて消えてしまう。使い方によるが、今回のパターンではビルボードの動きをしてほしい。

以前、草をGSとHSで生やした際、ビルボードについて調べたときにViewの逆行列を利用すれば良いというのを見つけたが結局出来ず、直接計算して頂点を操作した。GSではそれで問題なかったけど、今回の場合はそうは行かないので再度チャレンジしてみる。


VS

Out.Pos = mul( Cam.TransPV, mul( World[ In.IID ], mul( Cam.InverseV, float4( In.Pos, 1.0f )))) ;
Out.UV = In.UV ;

カメラの定数バッファにViewの逆行列(Cam.InverseV)を含めた。その際、移動部分_41~_43を0にする必要がある。用途によっては回転に対しての打ち消しも必要になる。
試行錯誤した結果、上記でうまく行った。

草のときには、カメラの上下には追従してしまうと、草が全部寝てしまうのでY軸を打ち消す様に_22は1、_21、_23は0にしてたことになる。
ただ、パーティクルとして使う場合は不要なので移動のみ打ち消す。


PS

Out.Col = float4( 1, 1, 1, 0.5 ) ;

パーティクルといえばアルファブレンドで加算合成だろということで、アルファ値を0.5に修正。


実行結果


向きはカメラの方を、向くようになった。

ビルボード版

ただ、アルファブレンドがおかしい。白いオブジェクトが連なってるのでずっと白のハズなのに途切れてる部分が見える。
これは深度バッファを利用して物体の前後関係は維持しつつ、パーティクル自体は深度バッファに書き込まないようにしないといけないけど、いつも通り書き込んでしまっているために同じZのオブジェクトが描かれなくなっているせいで起こっている。

機能を拡張して、シェーダを作るときに色々パラメータ指定をできるようにする必要がある。


ラスタライザ


D3D12_GRAPHICS_PIPELINE_STATE_DESC.RasterizerStateの項目でいくつか外部から設定できるようにする。


FillMode


たまにワイヤーフレームで確認したいときがあって、ライブラリを直接変えて実行していたけど、指定できるようにする。デフォルトはD3D12_FILL_MODE_SOLID。


CullMode


これもメッシュが表示されないとき、両面描画に変えたりして確認していたけど、パーティクルやエフェクトの描画の場合種類によって両面描画する必要があったりするので、指定できるようにする。デフォルトはD3D12_CULL_MODE_BACK。


アルファブレンド


アルファブレンドについては、D3D12_GRAPHICS_PIPELINE_STATE_DESC.BlendStateを設定する必要がある。
元々アルファブレンドOn/Off自体はあったんだけど、今回いろんなブレンド方法を追加しておこう。


アルファブレンドなし


D3D12_BLEND_DESC & oBD = oGPSD.BlendState ;
oBD.AlphaToCoverageEnable = FALSE ;
oBD.IndependentBlendEnable = FALSE ;
for( tNum i = 0 ; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT ; i++ ) {
	auto & oRT = oBD.RenderTarget[i] ;
	oRT.BlendEnable = FALSE ;
	oRT.LogicOpEnable = FALSE ;
	oRT.SrcBlend = D3D12_BLEND_ONE ;
	oRT.DestBlend = D3D12_BLEND_ZERO ;
	oRT.BlendOp = D3D12_BLEND_OP_ADD ;
	oRT.SrcBlendAlpha = D3D12_BLEND_ONE ;
	oRT.DestBlendAlpha = D3D12_BLEND_ZERO ;
	oRT.BlendOpAlpha = D3D12_BLEND_OP_ADD ;
	oRT.LogicOp = D3D12_LOGIC_OP_NOOP ;
	oRT.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL ;
}

アルファブレンドあり


switch( eABT ) {
case tAlphaBlendType::Normal :		// 通常ブレンド
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_SRC_ALPHA ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_ALPHA ;
	break ;
case tAlphaBlendType::Add :			// 加算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ONE ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::AddAlpha :	// 加算半透明合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_SRC_ALPHA ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::Sub :			// 減算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ZERO ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_COLOR ;
	break ;
case tAlphaBlendType::Mul :			// 乗算合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_ZERO ;
	oRT.DestBlend = D3D12_BLEND_SRC_COLOR ;
	break ;
case tAlphaBlendType::Mul2 :		// 乗算合成2
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_SRC_COLOR ;
	break ;
case tAlphaBlendType::Screen :		// スクリーン合成
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_INV_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_ONE ;
	break ;
case tAlphaBlendType::Reverse :		// リバース
	oRT.BlendEnable = TRUE ;
	oRT.SrcBlend = D3D12_BLEND_INV_DEST_COLOR ;
	oRT.DestBlend = D3D12_BLEND_INV_SRC_COLOR ;
	break ;
}


深度バッファ


 
深度バッファについては、D3D12_GRAPHICS_PIPELINE_STATE_DESC.DepthStencilStateを設定する必要がある。


DepthWriteMask


深度バッファの書き込みをしない場合は、この値をD3D12_DEPTH_WRITE_MASK_ZEROにする。デフォルトはD3D12_DEPTH_WRITE_MASK_ALL。



シェーダコンパイル


パラメータを指定することで正しい描画になった。
調子に乗ってボタンを押しまくると、若干カクつく。
試しにパーティクル追加の時間を測ってみると、なんと追加に16~17msもかかっている。
シェーダの生成、コンパイル、PSO作成はかなり重い処理ということか。

パーティクルのシェーダはパーティクル追加とは切り離して、事前に行う必要がありそう。



とりあえず完成


PSをちょっと修正して、生成関数で各方向にランダム値を設定。更新関数で指定方向に進むように変更してみた。

それっぽい

一応それっぽいものが出来た。
後はシェーダとパーティクルの紐づけを変更しつつ、描画時に各種リソースを設定する部分ができればいろんなことができるようになる。




2023年9月6日水曜日

Radial Blur

モーションブラーについて調べていたら、このサイトを見つけて放射状ブラーのポストエフェクトの機能追加をしてみることにした。


シェーダ


float2 uv = In.UV - Param.CenterPos ;
float blur = Param.Blur * rcp( Param.Sample - 1 ) ;
float2 ofs = Param.Blur * rand( uv ) * 0.01 ;
float4 color = 0.0 ;
[unroll(16)]
for( uint i = 0 ; i < uint( Param.Sample ) ; i++ ) {
	float scale = Param.Scale + ( i * blur ) + ofs * ( Param.Sample - i ) ;
	color += Src.Sample( Sampler, uv * scale + Param.CenterPos ) ;
}
Out.Col = color / Param.Sample ;

パラメータ


Param.CenterPos


焦点。uv座標で、( 0.5, 0.5 )が中心。


Param.Blur


ぼかし強度。0.1~0.3ぐらいの範囲で使うのがよさそう。


Param.Scale


スケール。1を指定すると元画像そのままで、値を小さくすると大きくなる。
Param.Blurを大きくしていくと、画像の外側の参照が多くなるので、連動してスケールを調整すると、画像が近づくのと、外周の間延び感が消せていい感じになる。


Param.Sample


サンプリング数。8~16ぐらいの範囲で使うのがよさそう。
シェーダも「[unroll(16)]」を指定してるので、それ以上は指定不可。
Param.Blurを大きくすると間が目立つので、連動してサンプリング数も増やすといい。


ofsについて


このサイトでUnityのモーションブラーの実装について解説されており、その中でノイズを入れると少ないサンプル数でもきれいに見えるというふうに書かれていた。
そのアイデアを取り入れて、ランダム値を足すようにしてみた。


出力結果



元の画像

Blur=0.1 Sample=8 ノイズなし

Blur=0.3 Sample=16 ノイズなし

Blur=0.1 Sample=8 ノイズあり

Blur=0.3 Sample=16 ノイズあり

Blur=0.3 Sample=16 ノイズなし

Blur=0.3 Sample=16 ノイズあり

最後の2つを比べると、ノイズなしの方はアウトラインシェーダで書いた枠線が縦に何本もみえるが、ノイズありの方では縦線が目立たなくなっている。



2023年8月26日土曜日

Depth Of Field 2 (被写界深度)

以前被写界深度についてやったとき、結果がいまいちだったので再度チャレンジ。

このサイトを見つけて単純な仕組みっぽいので簡単にできそう。


準備


・出来上がりの絵にブラーを掛けて、全体をぼかした画像を用意する。
・描画位置の座標を利用するため、深度バッファを用意する。


描画


・UVから、描画位置の位置を算出する。
・元絵の色とぼかした絵の色を用意する。
・フォーカス位置からの距離によって、元絵とぼかした絵の使う割合を変える。


シェーダ

	float Depth = TexDepth.Sample( BorderSampler, In.UV ).r ;
	float3 Pos = DepthToPos( Depth, In.UV, Cam.InversePV ).xyz ;

	float4 Col  = Src.Sample( Sampler, In.UV ) ;
	float4 BCol = Blur.Sample( Sampler, In.UV ) ;

	float blur = smoothstep( Param.MinDistance, Param.MaxDistance, length( Pos - Param.FocusPos )) ;
	Out.Col = lerp( Col, BCol, blur ) ;

FocusPos

焦点を合わせる位置

MinDistance

焦点を合わせる範囲

MaxDistance

焦点が完全に合わなくなる範囲

MinDistanceとMaxDistanceを同じにすると、ある部分から焦点があっている部分と合わない部分がくっきり分かれる。


結果



前回の結果とは雲泥の差になった。

以前実装したときはミップマップの仕組みすらない状態だった。
現状はミップマップぼかす仕組みがあり、それを呼び出すだけで今のシーンのぼかした画像が手に入るため、2つのテクスチャのブレンドをシェーダで書けば完成する。

昔の自分にこの手順を伝えてもすぐには作れないだろうから、手を出すタイミングも重要だな。


Outline Shader 2

前にアウトラインシェーダについて書いたけど、今回はその続き。

ディファードレンダリングの中で枠線を描くと全体に枠線が描かれてしまう為、ある特定のキャラにだけ枠線を書くとか、色を変えるとかが出来ない。

HLSLの魔導書には、別にしたい部分だけフォワードレンダリングでやれば実現できると書いてある。

やっては見たものの、色々と問題があった。


問題1:枠線がすべて描かれない

枠線が中途半端

法線のテクスチャが無いため深度バッファしか使えない。
一応枠線が描かれるが、中途半端にしか描かれない。


問題2:手前のオブジェクトに線が描かれてしまう

奥のキャラに青枠線を書いているが手前のキャラの枠線みたい

Z Prepassで作った深度バッファを元に描画しているため、他のオブジェクトと重なるような構図だと、手前のオブジェクトにも枠線が描かれてしまう。

これはフォワードレンダリング対象のみの深度バッファにすれば解決しそうだけど、複数色対応させる場合、フォワードレンダリング対象の各オブジェクトが重なったら同じことが起こってしまう。


問題3:カメラが近いと汚くなる


カメラが対象のオブジェクトに近づくと面が塗られてしまう。

カメラとオブジェクトの距離によって、パラメータを多少調整する必要がありそう。
ディファードレンダリングで枠線描いたときには、いくら近づいても問題なかったのに。



これらの問題を解決する方法を思いつかないので、個別のアウトラインは別の方法で対応したほうが良さそう。