syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

ATL::CPathユーティリティ

(これは2010-10-31に書いた故OCNブログの記事を移植したものです)

Visual C++ 付属の ATL にはわりと便利なユーティリティ クラスがあるんですが、あまり知られてないのでひとつ紹介します。

ATL::CPath は Windows Shell API の Path*関数を薄くラップしたクラスなんですが、ATL::CString とともに Windows アプリでは重宝するかと。
atlpath.h をインクルードすれば、非 ATL/MFC プロジェクトであっても使えるようになります。

#include <atlpath.h>

void AtlPathTest()
{
    const LPCWSTR pAnswerStr[] = { L"No", L"Yes" };
    {
        const CPathW paths[] = {
            CPathW(L"C:\\test_file.txt"),
            CPathW(L"test_file.txt"),
            CPathW(L".\\test_file.txt"),
            CPathW(L"..\\test_file.txt"),
            CPathW(L"\\\\127.0.0.1\\shared\\test_file.txt"),
        };

        for (int i = 0; i < sizeof(paths) / sizeof(*paths); ++i)
        {
            wprintf(L"'%s' is relative path ? %s\n", static_cast<LPCWSTR>(paths[i]), pAnswerStr[paths[i].IsRelative()]);
        }
        // --> N, Y, Y, Y, N
        puts("");
    }

    {
        // '\\' の代わりに '/' を使うと、意図した結果にならない。
        const LPCWSTR srcPath = L"C:\\directory\\test_file.txt";
        CPathW path = srcPath;
        wprintf(L"RemoveFileSpec('%s') = \n", srcPath);
        path.RemoveFileSpec();
        wprintf(L"'%s'\n\n", static_cast<LPCWSTR>(path)); // 'C:\directory'
    }

    {
        const LPCTSTR srcPath = _T("C:\\directory\\test_file.txt");
        CPath path = srcPath;
        _tprintf(_T("StripPath('%s') = \n"), srcPath);
        path.StripPath();
        _tprintf(_T("'%s'\n\n"), static_cast<LPCTSTR>(path)); // 'test_file.txt'

        path = srcPath;
        _tprintf(_T("RemoveExtension('%s') = \n"), srcPath);
        path.RemoveExtension();
        _tprintf(_T("'%s'\n\n"), static_cast<LPCTSTR>(path)); // 'C:\directory\test_file'
    }

    {
        const LPCSTR path1 = "C:\\directory";
        const LPCSTR path2 = "test_file.txt";
        CPathA path = path1;
        path += path2;
        printf("'%s' + '%s' = \n", path1, path2);
        printf("'%s'\n\n", static_cast<LPCSTR>(path)); // 'C:\directory\test_file.txt'
    }
}

CString は C++ 標準の std::string や std::wstring に比べて、速度面やメモリ効率面においてかなり最適化されているし、ANSIUnicode 変換も簡単にできて、COM 用の _bstr_t との相互変換もサポートされているなど、VBDelphi の文字列に引けを取らない機能を持っているので、ATL/MFC 開発ではまず CString をいかに使いこなせるか、が重要になってきます。ちなみに MFC と ATL は有償ライブラリなので、無償版(Express エディション)の Visual C++ では使えません*1。また、Windows 以外の OS や Visual C++ 以外のコンパイラーへの移植性を考えると、標準 C/C++ ライブラリの使い方も習得しておいたほうがよいでしょう。なお、Unicode 版の CStringW に関しては Windows ストア アプリ開発でも使えますが、MBCS 版の CStringA はデスクトップ アプリ専用です。

あと実際のデバッグで役に立つ機能といえば、ATLTRACE() と ATLASSERT() マクロです。MFC には同等機能として TRACE() と ASSERT() というマクロが存在しますが、ATL 版は非 MFC プロジェクトでも使えるので便利です。ちなみに標準 C ライブラリの assert() マクロは、MSVC 実装ではアサーション失敗時のプログラム停止位置が DebugBreak() 呼び出しまでさかのぼってしまうため使いづらいので、可能であれば ATL 版や MFC 版のアサート、もしくは CRT 版の _ASSERTE() を使うと楽になります*2

*1:Visual Studio 2013 以降は小規模チームや個人開発に限り無償で利用可能な Community エディションが用意されていて、Community エディションでは Professional エディション以上と同様に ATL/MFC を使うことができます。

*2:Visual Studio 2013 では改善されていて、assert() マクロでもアサーションが失敗した位置で停止するようになっています。

WinSDKおよびATLのCOMユーティリティ

(これは2010-11-23に書いた故OCNブログの記事を移植したものです)

MSXMLなどのCOMタイプ ライブラリを#importしたときなど、COMを扱うときによく見かけるのが、Visual C++ CRTヘルパーの _bstr_t クラスと _variant_t クラス (comutil.h) です。それぞれ、COMのBSTR文字列のラッパーと、VARIANT型のラッパーとなっています。
ただしC++プログラマーとしてはこれらがグローバル名前空間で定義されてやがることに殺意を覚えます。シンボル名がアンダースコアで始まっているのは、処理系依存のデータ型であることを誇示しているのか……
あとアンダースコアなしのエイリアス bstr_tvariant_t がtypedefじゃなく#defineなのも情けないです。これらのエイリアスは使わないほうがよいでしょう。MSの#define好きは異常*1

似たようなラッパーはATL (atlcomcli.h) にも用意されていて、ATL::CComBSTRATL::CComVariantが該当するのですが、ついでにATL::CComSafeArray (atlsafe.h) なんかもあります。MFC専用だとCOleVariantというのもあります。

なお、_variant_tは、MFCライブラリでもたまに使われているのを見かけます(CMFCPropertyGridProperty::SetValue()/GetValue()とか)。

正直VC++ネイティブで文字列を扱う場合、他の環境への移植性を考慮しないで良いのであれば、迷わずCStringA/CStringW/CStringを使うのがベストです*2。そもそもテンプレート機能まである静的型付け言語C++で何が悲しくてVariantなんぞ使わないといけないのかとも思いますが、VBスクリプト言語との相互運用を考慮して設計されたCOMデュアル インターフェイスの境界では、たとえ非効率でもBSTRやVARIANT、SAFEARRAYを使わざるを得ません。で、COMのBSTRは::SysAllocString()/::SysFreeString()、SAFEARRAYは::SafeArrayCreate()/::SafeArrayDestroy()でヒープ管理されるので、こういうRAIIラッパーがないと発狂するでしょう。ただ、ラッパークラスはCRTアロケータを使っていない時点で速度が出ない(とくに文字列の結合が毎回再割り当てを伴うために遅いらしい)し、下手な使い方をするとメモリーリークの原因になるので、COMのRAIIラッパーを使用する場合はCOM境界のみに限定するべきです。ちなみにATL::CComBSTRはアタッチ・デタッチがやや特殊で、使い方を誤ると簡単にメモリーリークするらしいので、極力_bstr_tを使ったほうが無難らしいです。

実装を確認してみたところ、一応 CComBSTR::operator& にはアサーションが入っているので、デバッグビルドであれば誤った使い方をしたときに気付くはずです。

*1:Win32 APIでも、enumを使うべき所なのにやたら#define使っているのがかなりうっとうしいです。あと悪名高いmin/maxマクロも後世に語り継がれる大ポカでしょう。

*2:C++標準のstd::string/wstringより最適化されていて高機能です。Windowsの文字列リソースを直接読み込めるLoadString()メソッドも用意されています。

MFC-CLI 相互運用時の注意点‏

(これは2011-05-09に書いた故OCNブログの記事を移植したものです)

それなりに規模の大きい MFC プロジェクトなどで、共通言語ランタイムのサポート /clr を追加して .NET ハイブリッド アプリケーションを作るときに、起動時に EETypeLoadException 例外が発生して実行できない場合、コンパイル オプションに /GF(同一文字列の削除)を追加します。

VS 2008 IDE から設定する場合は、「構成プロパティ」→「C/C++」→「コード生成」→「文字列プール」を「はい」にします。
特にデバッグ ビルドで最適化を行なわない場合はこのオプションが OFF になっていることが多いので注意。ただし特定の最適化オプションを有効にすると自動で ON になることもあるらしいです。

今、仕事で関わっている案件では、これまでまともなビルドマスターやモジュール構成管理者がいなかったらしく、ひとつのEXEのプロジェクト ファイルですさまじい数のソースファイルがコンパイル&リンクされます。どうやらMFC拡張DLLの存在を知らなかったらしい(MFC拡張DLLを使えば簡単かつ効率的にMFCアプリを分割ビルドできるのに……)。

この状態で、すべてのソースを/clrコンパイルすると、ただでさえネイティブのみの場合でも長かったリンク時間がさらに長くなります。

多分、「x86 + MSIL」や「x64 + MSIL」の混在コードをリンクする処理に時間がかかっているみたいです。なので、すべて /clr を使ってコンパイルするのではなく、ネイティブのみでコンパイルできる部分は明示的に /clr なしで通常どおりコンパイルするようにしないと厳しいかもしれません。

なお、/clr ありと /clr なしとでは、プリコンパイル済みヘッダー(pch)を共有できないので、pchを作成するヘッダーとソース(通例stdafx.hとstdafx.cpp)を明示的に分ける必要があります。

例えば、

  • ネイティブ用に stdafx.h, stdafx.cpp, $(TargetName).pch
  • /clr 用に stdclr.h, stdclr.cpp, $(TargetName)_clr.pch

とか。

はっきり言ってめんどいことこの上なくて死にそうなので、もういっそネイティブのみを使うコードと/clrを使うコードを別々のプロジェクト(ネイティブEXEと混合DLL、もしくはネイティブDLLと混合EXE)に分離してしまったほうが良いです。

方法 : /clr に移行する

あと、C#はともかくC++/CLIをまともに使える開発者というのはかなり少ないので、MFCしか使えない人達には、

・/clrを使うホストEXE側のコードはいじらないで、ネイティブのMFC拡張DLLのコードだけいじるようにしてね(要するに筐体はいじらないで部品だけいじるようにしてね)

もしくは

・/clrを使うMFC拡張DLLにWPFラッパーのネイティブMFCインターフェイスのみを公開してあげるから、ホストとなるネイティブEXE側のコードだけいじるようにしてね(要するに部品はブラックボックスとして使ってね)

というルール作りも必要かと。そうでないと/clrオプションのもとにとんでもないコードを書き始める危険性があります。C#の素人にはunsafeを許可させないで制約を課したほうがいいのと似たようなものです(状況は逆だけど)。

ネイティブ・マネージ混在デバッグの注意点

VS 2008でx64のネイティブ・マネージ混在コードをデバッグするとき、EXE側の「構成プロパティ→デバッグ→デバッガのタイプ」を「混合」にしておくと、デバッグ実行時にブレークポイントが機能しなかったりステップインできなかったり、さらにASSERT(ATLASSERT)などによるアサートに引っかかったときプロセスが落ちる(アサート ダイアログで「再試行」とか「無視」を選択すると、本来続行可能な場面でも落ちる)現象が発生することがあるので、もしx64ネイティブ コードのほうをデバッグしたかったら「ネイティブ」に設定しておく必要があります。ただし「ネイティブ」に設定すると、今度はマネージ コードに対してブレークポイントが効かなくなったり、.NET側でIDE出力ウィンドウへのUnicode文字の出力ができなくなったりするので注意。これは結構面倒。x86のネイティブ・マネージ混在コードをデバッグ実行するときは「混合」でもアサートで落ちることはないのに……なので、64bit対応のMFC + .NETハイブリッド アプリケーションを作る場合は、まず32bit版で十分デバッグしてバグを取り除いておいたほうがいいです。64bit版でしかテスト・デバッグできないコードに関しては、面倒だがあきらめてデバッグ設定で回避するしかなさそう。なお、「デバッガのタイプ」のデフォルト設定「自動」は、EXEのプロジェクトがネイティブかそれとも/clrありの混合か、に左右されます。

ちなみにC#のコードからWin32のネイティブDLL関数をP/Invoke呼び出ししている場合に、デバッガでネイティブ関数内にステップインする場合、C#プロジェクト設定の「デバッグ」タブにある「アンマネージ コード デバッグを有効にする」にチェックを入れておきます。VC++もVC#も、こういったデバッグ時の設定に関しては、.vcprojとか.csprojといったプロジェクト ファイルではなく、.vcproj.<Windows Log-in User Name>.userとか.csproj.userとかのユーザー設定ファイルに書き込まれるので、開発者ごと(端末ごと)に逐一設定してやる必要があることに注意しましょう。

C++/CLI のインテリセンス

VC 2010ではC++/CLI言語のインテリセンスが動作しません(バグではなく仕様)。VC 2012ではちゃんと復活しています。なんで2010で一度死んだのかは不明ですが、C++0x対応やコンパイル前文法チェック機構の強化などを優先した関係上、C++のインテリセンスを再設計する必要があって、その余波でC++/CLI対応が後回しになったのかもしれません。ちなみにVC 2012では Windows ストア アプリ(WinRT)用に新たに C++/CX 拡張モードが追加されています。なお、C++/CX と C++/CLI は混在できません。

従来のWin32ネイティブ業務アプリをWPFに移行したいと考えたとき、全部をWPFに置き換えるのは現実的にいって不可能なので(WPF+C#自体は生産性は高いけどC#C++ほどきめ細かい制御ができないし、逆にネイティブC/C++GUIコード資産をWPFから使うにはあまりに制約が多い上に労多くして実りが少ない)、普通はHwndSourceとか使ってMFCアプリ内にWPFコントロールを埋め込む方法をとって徐々にUI部品などから.NETへ移行していく、またパフォーマンスが要求される部分だけC++実装を残しておいてC++/CLIでマネージインターフェイスを書く、という方法をとると思うんですが、肝心のC++/CLI言語に対するIDEサポートがおざなりだと致命的になります。C++/CLIは安易に使うと(ネイティブC++以上に)地獄を見るという、完全にマニア向けの言語ですが、MSにはもっとC++/CLIを使った相互運用のメリット・デメリットや具体的シナリオを説明するページを作成して欲しいです。

MFCのCFileDialog::SetDefExt()の引数の型について

(これは2010-06-05に書いた故OCNブログの記事を移植したものです)

バグの発見、MS Connect への報告と顛末

Visual Studio 2008/2010 の MFC 9.0/10.0 では、CFileDialog::SetDefExt() の引数のが間違っていて、本来 LPCTSTR (LPCWSTR for UNICODE / LPCSTR for MBCS) とすべきところを LPCSTR (MBCS) としてしまっています。
OS が Vista 以降でなおかつ Vista スタイルのファイル ダイアログの場合は、関数内部で LPCSTR 引数を CStringW 変数で受け取って変換しているんですが、OS が XP 以前もしくは非 Vista スタイルのレガシーダイアログの場合は、受け取った LPCSTR ポインタをそのまま SendMessage() に渡しています。
したがって、プロジェクト設定で Unicode 文字セットを使用するようにしている場合、SendMessage() は SendMessageA() ではなく SendMessageW() に置き換わり、非 Vista スタイルで素直に LPCSTR を渡してしまうと文字化けが発生します。
逆に Vista スタイルの場合、LPCWSTR を LPCSTR に強制キャストして渡してしまうと、CStringW の LPCSTR を受け取るコンストラクタが誤認するので、文字化けが発生します。
実際には、単に afxdlgs.h および dlgfile.cpp の関数インターフェイスが間違っているだけなので、非 Vista スタイルであれば LPCTSTR を無理やり LPCSTR に再解釈キャストしてから渡してしまえば(つまりコンパイラをだましてしまえば)一応正常に動作するんですが、Vista スタイルを使う場合は絶対に SetDefExt() に LPCWSTR を使うことはできません。

昔(2010年頃)、下記の URL で MS Connect 宛てに問題を報告したんですが、Connect の日本語版が米本国のサイトに統合されたときに消されてしまったようです(バグトラッキングを消去するなんて最低の行為ですね……)。確かそのときのタイトルは「MFCのCFileDialog::SetDefExt()の引数の型が不適切」というものでした。

当時 VS 2010 がまだベータの頃、VS 2008 で問題を発見して報告したんですが、MS からの回答は下記のとおりでした。

"To clarify, most likely this issue will be fixed in the next major release, e.g. Visual Studio 2012."

なん……だと

要するに、「例えば」VS 2012で「たぶん」修正されるというだけの回答でした。

その後のバグ修正状況と回避策

Visual Studio 2012 の MFC 11.0 で、確かに件の問題点は修正されていることを確認しました。MSDNにも記載があります。

ただしプロジェクト ラウンドトリップ機能を使うなどして MFC 9.0/10.0 と 11.0 以降をいったりきたりする可能性がある場合、SetDefExt() を呼び出す箇所では下記のような対処をしておく必要がありそうです。MFC 8.0 以前はどうなっていたのか調べていないので未対応です*1

#if (0x0900 <= _MFC_VER) && (_MFC_VER < 0x0B00)
#pragma message("MFC 9.0 / 10.0 CFileDialog::SetDefExt()")
if (m_bVistaStyle)
{
    this->SetDefExt(CStringA(strExt));
}
else
{
    this->SetDefExt(reinterpret_cast<LPCSTR>(static_cast<LPCTSTR>(strExt)));
}
#else
this->SetDefExt(strExt);
#endif

サンプル

かつて MS Connect にアップロードしたサンプルコードを、とりあえず VS 2008/2010/2012/2013 でビルド・比較できるようにしたものを下記にさらしておきます。
「ファイルの種類を変更すると、入力したファイル名の拡張子部分を自動的に更新するファイル ダイアログ」とか作るとき、とりあえずの回避方法として使ってやってください。

ちなみに、CFileDialog::SetControlText()は非Vistaスタイルのレガシーダイアログ専用機能であり、Vistaスタイルのダイアログではうまく動作しない模様です。ただしファイル名入力欄のテキストボックス文字列を設定する場合に関しては、代わりにIFileDialog::SetFileName()を使えばいいらしいです。実はVistaダイアログはCOMコンポーネントになっていて、VS 2008以降のCFileDialogはコンストラクタの引数フラグによってWin32 APIベースの旧ダイアログとCOMベースの新ダイアログを切り替えるラッパーになっています。だったらVistaスタイルのダイアログでもSetControlText()が動作するようにうまくラップしてくれよと言いたいところですが……

バグを見つけたら報告しましょう

MS Connect はいろいろ問題点もありますが、真面目に報告すればちゃんとリプライが来ますし、MSが修正するに足るバグだと判断すれば次期バージョンで修正してくれます。Visual Studio .NET Framework をより良くしたいという志があるのであれば、「いつか誰かが報告/修正してくれるだろう」という楽観的な態度でただ静観しているのではなく、積極的にバグ報告して改善に貢献したほうがよいです。ただし Connect では基本的に英語でコミュニケーションをとることになるので覚悟しましょう。日本のオペレーターが、こちらと米本国チームとのやりとりを翻訳して仲介してくれることもありますが、テクニカルライティングを知らないせいか翻訳がグダグダなことが多いので、英語で直接やりとりしたほうが手っ取り早いです*2

例えば下記の問題点は Visual C++ 2012 Update 4 で見つけたものですが、VC 2013 Update 2 ではすでに修正されていました。

なおVisual Studio 2013ではCommunityエディションが追加され、個人開発者や教育機関などであればATL/MFCが無償で利用できるようになりました。すでにレガシーAPIとなっているATL/MFCによる開発案件というのは今後積極的に増えることはないと思いますが、無償化によってATL/MFCのバグ報告が活発になり、品質が向上することを期待したいものです。

2015-11-06追記:
Windows 10には、非Vistaスタイルのレガシーダイアログがまともにリサイズできないデグレードがあるようです。レガシーダイアログを使っているアプリはまだ存在するし、古くてメンテナンスされていないクローズドソースのアプリだとお手上げなので、MSはデグレードを認めてさっさと直すべきでしょう。Windows 10はデグレードのひどい極めてお粗末なOSです。

*1:昔学生の頃に買った VS 2003、VS 2005 のインストール ディスクは一応持っているので調べようと思えば調べられるんですが、今更調べても需要がないのでやりません。

*2:そもそも英語のリーディング・ライティングスキルがないのに、まともなプログラミングなんてできるはずがありませんが……

Effects10, Effects11の実行時コンパイル

(これは2013-05-11に書いた故OCNブログの記事を移植したものです)

以前書いた、「Effects11 for Windowsストア アプリ」の記事とは趣旨が逆行する話ですが、一応参考程度に書き残しておきます。デスクトップ アプリでは実行時コンパイルを使うこともできますが、いろいろ問題点(特にランタイム再配布に関するもの)があるので、極力実行時コンパイルは避けて、エフェクトやシェーダーのコンパイルには fxc.exe を使ってオフラインで行なうようにしておいたほうがいいです。

今回のサンプルは単純なコンソール アプリです。下記の環境で検証しています。
D3DEffectLoadTest.zip

昔(2010年頃)、Effects10を使っていたコードをEffects11に移行したときにハマったことがあったのですが、D3D10&D3DX10&Effects10と、D3D11&D3DX11&Effects11には細かい違いが結構あります。単純な置き換えでは済まない部分もあるし、紛らわしい部分もかなりあります。

Effects10 Effects11
対応するDirect3D API Direct3D 10.x Direct3D 11.x
使用可能なプロファイル名 fx_4_0, fx_4_1 fx_5_0
使用可能なテクニック technique10 technique10, technique11
外部ソースファイルからの直接生成 D3DX10CreateEffectFromFile() N/A
メモリ上ソースファイルからの直接生成 D3DX10CreateEffectFromMemory() N/A
ソースファイルのコンパイル D3DX10CompileFromFile() D3DX11CompileFromFile()
コンパイル済みバイトコードからの生成 D3D10CreateEffectFromMemory() D3DX11CreateEffectFromMemory()

特にまぎらわしいのが、下記2つです。

  • D3DX10CreateEffectFromMemory()
  • D3DX11CreateEffectFromMemory()

名前は似てますが、前者はメモリ上ソースファイルからのエフェクト生成、後者はメモリ上バイトコードからの生成であり、機能が全然違うので注意が必要です。

前者はFromSourceFileOnMemory()、後者はFromBytecodeOnMemory()にして欲しかった……

エフェクトのプロファイル名というのは、エフェクト ファイルをコンパイルするときに必要となるバージョンのようなもので、各シェーダーステージのプロファイル(vs_4_0やgs_5_0など)とは意味が異なります。fx_5_0を使っていても、vs_4_0_level_9_1などのダウンレベル プロファイルもきちんと使えます。

なお、D3DXはWindows SDK 8.0以降では廃止されていて、D3DCompilerランタイムもろともWindowsストア アプリ開発で使用できない(DLLをリンクするとストア アプリが起動できなくなる*1)ので、今後はできるかぎり使用を避けたほうが良いのですが、ディレクトリの優先順位を

Windows SDK 8.x>旧DirectX SDK June 2010

のように設定することで、Visual C++ 2012/2013コンパイラでもD3DXライブラリを補助的に使うことはできます。要するに、D3D9/D3D10.x/D3D11.xコア ライブラリはWindows SDK 8.xのものを使用して、D3DX9/D3DX10/D3DX11ライブラリは旧DXSDKのものを使うというわけです。これにより、デスクトップ アプリではD3D 11.1&D2D 1.1とD3DX11、もしくはD3D 11.2&D2D 1.2とD3DX11を共存させることも一応できます。

D3DX MathからDirectXMathへの移行は(演算子オーバーロードがなくなっていることを除いて)比較的簡単だと思います。DirectXTKのSimpleMathライブラリを使うとさらに簡単になるでしょう。D3DX TextureからDirectXTexへの移行も、互換機能がだいたい用意されているようなので、注意深く実装すれば大丈夫でしょう。しかし、D3DX Mesh/Sprite/EffectsなどからDirectXTKへの移行はかなり大変な作業になることが予想されます。代替機能がないものも多数あります。また、DirectXTKはD3D11専用で、D3D9/D3D10からは使用できません。実際にDirectXTex/DirectXTKへ移行するか、それともそれらに頼らずD3DXを自前のライブラリで置き換えるか、それを決めるまでの暫定的対処としてしばらくD3DXライブラリを使い続けるという手もあります。

ちなみにSMAAのソースに付属するDirect3D 10サンプルコードではEffects10を使っているのですが、FXLVM(Effects Language Virtual Machine?)が1か所だけ使用されています。Effects11はFXLVMをサポートしないので、移植しようとするとき変更が必要になります。

*1:D3DCompilerランタイムに関してはWindows 8.1ストア アプリにおいて制限付きながら利用できるようになりました。

Effects11 for Windowsストア アプリ

(これは2013-04-13に書いた故OCNブログの記事を加筆修正したものです)

今回は旧D3DXライブラリの歴史にちょっとだけ触れるので、前置きが長いです。

Windowsストア アプリを開発できるVisual Studio 2012用のDirectX SDKWindows SDK 8.0)では、これまでDirect3D開発者にとっておなじみだったD3DXライブラリがほぼ完全に廃止されています。D3DXのピークはDirect3D 9用のD3DX9で、数学関数などのルーチン系だけでなくフォントやスプライト、エフェクト フレームワーク(Effects9)、果てはスキンメッシュやアニメーション コントローラー、GI(PRT)用のSH計算関数までデフォルトで用意されている充実っぷりでした。時は流れ、プログラマブル シェーダーが必須となったDirect3D 10では、D3DX10が大幅に弱体化し、スキンメッシュやアニメーション コントローラーがなくなった代わりに、エフェクト フレームワーク(Effects10)がコアAPIに組み込まれました。しかし続くDirect3D 11ではD3DX11がさらに弱体化し、ついにエフェクトもビルド済みライブラリの形では提供されなくなり、スタティックライブラリ用のソースコード(Effects11)のみがDirectX SDKのサンプルとして付属するだけになりました。

デスクトップ アプリの開発では、Visual Studio 2012/2013において、Visual C++ 2010コンパイラWindows SDK 7.x+DirectX SDK June 2010などの組み合わせを引き続き使用するように設定することもできます(ただしWindows SDK 7.xを使用する場合、Windows SDK 8.0のDirectX 11.1やDirect2D 1.1は使えません。ツールセット設定やディレクトリ優先順位を調整して、D3DコアライブラリはWindows SDK 8.0/8.1を使い、DirectX SDK June 2010のD3DXのみ補助的に使う、ということはできます)。しかし、Windows 8/RT向けストア アプリの開発ではVS 2012とWindows SDK 8.0の組み合わせが必須となっており、またD3DXランタイムおよびD3DCompilerランタイムが一切使用できません(これらのランタイムをリンクしてしまうとアプリが起動不能になる)。HLSLの実行時コンパイルをストア アプリでサポートしない理由などは下記で言及されてます。

ちなみにNVIDIA C for Graphics(Cg)のランタイムは、実はToolkitバージョン3.1時点でD3DXとD3DCompilerのランタイムに依存しているため、Cgランタイムを使ったストア アプリは現時点では作れません。

代わりにストア アプリでのDirectX 11補助ライブラリとして、CodePlexDirectX Tool Kit(DirectXTK, DXTK)というのが公開されているのですが、こいつは(少なくとも自分にとっては)なんとも使いにくいライブラリになってしまっています。従来のエフェクト フレームワークでは、エフェクトファイル(.fxファイル)に複数のシェーダー関数やステートを記述して、組み合わせをpassとtechniqueの形で階層化しパイプラインを構築するということが簡単にできていたのですが、DXTKのEffectはC++コードでパイプラインをガリガリ組み立てていくという、なんとも原始的なことをやってます。最初から用意されている出来合いの基本シェーダーだけを使うのであればそれでもいいかもしれませんが、試行錯誤しながらパイプラインをガンガン書き直す場合は辛いです。なにより今までのエフェクトファイルの資産がほとんど使えず、エフェクトのサブセットフォーマットすらサポートされないので、パイプラインおよびシェーダーステージごとに分割していかなければならないのはかなりの苦痛。

そこで自分が考えたのは、

「Effects11はソース提供されてるんだから、ストア アプリで使えるように修正したらいいじゃない!!」

結論から言うと、わりと簡単にできます。ただしストア アプリ対応のためにD3DCompilerランタイムへの依存を断つといくつかの制限が発生するため、エフェクト フレームワークのリフレクション機能をフル活用しているようなプログラムに対してはコード修正が必要です。が、エフェクトファイルによるパイプラインの構築という最大のメリットがそのまま活かせるのは大きいでしょう。エフェクトファイルではcs_5_0のようなDirectX 11対応H/W用プロファイルだけでなく、vs_4_0_level_9_1やgs_4_0のようなダウンレベルH/W用のプロファイルも使えるので、Surface RT向けアプリ(ARM用アプリ)の開発にも使えるはずです。またD3DCompilerへの依存を断つことで、DirectXエンドユーザーランタイムをインストールする必要がなくなるため、従来のWindows 7向けデスクトップ アプリでこの制限付きEffects11を心置きなく使えるというメリットもあります。

%ProgramFiles(x86)%\Microsoft DirectX SDK (June 2010)\Documentation\License Agreements\DirectX SDK EULA.txt

を読むかぎり、Effects11に関しては改変・再頒布して良いらしいので、ストア アプリでも使えるように修正したCompact Effects11と、そのCompact Effects11を使ったストア アプリのサンプルをアップしておきます。利用は自己責任で。サンプルのほうはぶっちゃけVisual Studio 2012のDirect3DアプリのテンプレートをEffects11使って手直ししただけですが、ID3DX11EffectTechniqueは普通に使えることが実証されました。

なお、サンプルを見れば分かりますが、特に大きな制限として、

(1) D3DCompilerランタイムを使用した、エフェクトの実行時コンパイルができない
(2) 頂点シェーダーBLOBのリフレクションが効かない
(3) 名前によるエフェクト変数(シェーダー定数)の参照と更新はできない
(4) ステート構文によるSamplerStateの設定はできない
(5) 動的シェーダーリンク(Dynamic Shader Linkage)はローレベルAPIで対処

などがあります。

(1) はD3DX11CompileFromFile()が使えないということですが、コンパイルVisual Studioのビルドプロセスで行なって、実行時にコンパイル済みバイトコードをD3DX11CreateEffectFromMemory()に食わせてやることで対処します。VS 2012/2013であれば.fxファイルや.hlslファイルの単純なコンパイル設定はVisual Studio IDEに専用のツールが組み込まれているので簡単ですが(VC++のプロパティ設定UIが使える)、以前のバージョンのVisual Studioでデスクトップ アプリを開発する場合や、1つのファイルに対してオプションの異なる複数のコンパイル処理を走らせたりする場合はコマンドラインでfxc.exeを使って別途エフェクトファイルをコンパイルするか、カスタムビルドツール設定でのコマンドライン指定が要ります(※後述)。

(2) はID3DX11EffectPass::GetDesc()によるバイトコードの取得が使えないということですが、頂点シェーダーバイトコードのバイナリデータがないとID3D11Device::CreateInputLayout()による頂点入力レイアウトの作成ができません。エフェクトファイル本体のコンパイルとは別に頂点シェーダーだけエントリーポイント関数名を指定してコンパイルし、バイトコードを事前作成することで対処します。これも(1)同様です。

(3) はID3DX11EffectVectorVariable::SetFloatVector()などが一律使えないということですが、代わりにcbufferを使って、C++による明示的な定数バッファの作成と更新を行なうことで対処します。サンプラー変数やテクスチャなどのリソース変数(ID3DX11EffectSamplerVariable, ID3DX11EffectShaderResourceVariable)に関しても同様です。まぁヘタにエフェクト変数に頼りすぎるとDirect3Dデバッグレイヤーによるエラーレポートの解読が困難になるなどの問題をいろいろ引き起こすことがあるので、この際あきらめましょう。uniform変数ごとの名前アクセスが主流であるGLSLから移植したりする際はこのあたりに注意が必要です(OpenGL 3.1やES 3.0では逆に定数バッファ相当機能のUBOが追加されているので、cbufferとUBOを使えばDirect3D/OpenGL間でデータ構造の共有もしやすくなります)。

(4) はエフェクトファイルでなくC++でステートオブジェクトを作成・設定することで対処します。なお、サンプラーステートはC++で明示的作成する必要がありますが、レンダリングステートは従来通りエフェクトファイル側で書けます。

(5) はパフォーマンス損失を抑えつつシェーダーの組み合わせ爆発を防ぐために導入されたDirectX 11(シェーダーモデル5.0)の新機能で、シェーダープログラムでinterface(仮想メソッド)が使えるというとんでもない代物なのですが、(3)同様にID3DX11EffectInterfaceVariable::SetClassInstance()などはD3DCompilerのリフレクション(D3DReflect()関数)に依存しているため使えません。そのため、インスタンス テーブル用の定数バッファを明示的に宣言したり、ID3D11DeviceContext::XXSetShader()メソッドに対して明示的にID3D11ClassInstance配列を渡したり、といった工夫が必要になります(ちなみに動的シェーダーリンク目的以外でも、HLSLでclassキーワードを使って単純なクラスを定義することはできます)。ID3DX11Effect::GetClassLinkage()やID3D11ClassLinkage::GetClassInstance()に関しては普通に使えるようなので、Effects11を一切使わない場合と比べて多少は楽になる部分もありますが、ID3DX11EffectPass::Apply()はID3D11ClassInstanceを受け取れないのでやはりトリックが必要になります。実は、

// FX 側:
PixelShader g_psHyperShader = CompileShader(ps_5_0, psHyperShader());
// C++ 側:
m_pEffect->GetVariableByName("g_psHyperShader")->AsShader()->GetPixelShader(0, &pPixelShader);

という感じで特定のシェーダーステージのインターフェイスを取得する機能は動作するので、いったん動的シェーダーリンクを使わないダミーシェーダーを含んだエフェクトのパスを適用した後、特定のシェーダーステージだけ後から設定し直すということはできるんですが、複数のインターフェイスを使う場合は結局ID3D11ShaderReflection::GetNumInterfaceSlots()やID3D11ShaderReflectionVariable::GetInterfaceSlot()を使ってリフレクション経由でインターフェイス スロット情報を調べる必要があるため、動的シェーダーリンクを使うシェーダーだけはエフェクトとは別にコンパイルするようにしたほうが良いでしょう。

長々と書きましたが、D3DCompilerランタイムが使えないことによる制約はかなり大きいです。エフェクト フレームワークのほとんどの便利機能が使えなくなります。ただDXTKのEffectを使うよりは生産性が高くなることは間違いないし、これまでのエフェクトファイルの資産がある程度使えるのはありがたいです。

DirectX 12ではシェーダーステージの追加だけでなく、シェーダー自体の在り方もまたガラッと変わる可能性はあるので、Effects11を更新してEffects12に、ってわけにはいかないと思います。そもそも新しいD3DCompilerがリリースされても、DirectX 12対応のエフェクトファイル仕様(fx_6_0?, technique12など)はもう現れないでしょう。ただストア アプリの場合、DirectX 11.1/Direct2D 1.1や、DirectX 11.2/Direct2D 1.2テクノロジーにOSレベルでがっつり依存しているフレームワーク(&サンドボックス)内でしか動作できないため、DirectX 12がリリースされてもすぐに対応できるわけじゃないと思います(DirectX 12はXbox Oneにはプラットフォーム更新プログラムとして必ず供給されるでしょうが、Windows 8.xや7にバックポートされる可能性は低く、当然DirectX 12対応のストア アプリ/デスクトップ アプリはWindows 10でしか動作しないように思われます)。

ちなみにコンピュート シェーダー用のエクステンションであるD3DCSX拡張ライブラリは一応D3DXの仲間なのですが、例外的にWindows SDK 8.0/8.1にも同梱されています。ただし、これを使うとd3dcsx_46.dll/d3dcsx_47.dllに依存してしまうことに注意が必要。また、ストア アプリでは使えません。

カスタム ビルド ツールによるシェーダーコンパイル

カスタム ビルド ツールを指定するとき、例えば.fxファイルに対して下記のような「コマンド ライン」を指定します。

@echo off
setlocal EnableDelayedExpansion
echo Now compiling effect file by strict mode...
set varDefs=/D DOWN_SAMPLED_TEX_SIZE=128 /D COMPUTING_TEMP_WORK_SIZE=256
"%DXSDK_DIR%Utilities\bin\x86\fxc.exe" /nologo /Ges MyHLSL\MyShader.fx /Fo "$(OutDir)MyShader.fxbin" /T fx_5_0 %varDefs%
if "!ERRORLEVEL!"  neq "0" ( exit !ERRORLEVEL! )
"%DXSDK_DIR%Utilities\bin\x86\fxc.exe" /nologo /Ges MyHLSL\VSInOutStruct.hlsli /Fo "$(OutDir)MyVertexXX.vsbin" /E vsDummyLayoutXX /T vs_4_0_level_9_1
if "!ERRORLEVEL!"  neq "0" ( exit !ERRORLEVEL! )
endlocal

各オプションの意味は、fxc.exeのヘルプに書いてあります。

VS 2012/2013の場合は起動するだけで「%ProgramFiles(x86)%\Windows Kits\8.0\bin\x86」もしくは「%ProgramFiles(x86)%\Windows Kits\8.1\bin\x86」へのパスが通っているので、上記の「%DXSDK_DIR%Utilities\bin\x86\...」というパス指定は不要となり、「fxc.exe /nologo ...」でOKです(以前のバージョンのDirectX SDKのインストールも不要です)。

注意点としては、fxc.exeを実行するたびにERRORLEVELをちゃんとチェックすること、入出力に相対パスを指定するときはVC++プロジェクト ファイル(.vcproj)からの相対パスを記述すること。また、今回の例でいうと、「出力ファイル」に「$(OutDir)MyShader.fxbin」を、「追加の依存ファイル」に「MyHLSL\VSInOutStruct.hlsli;」を指定しておくと、ソースファイル.fxやヘッダーファイル.hlsliの変更、プライマリー出力ファイル.fxbinの不在を自動検出することができるようになります。

なお、「構成プロパティ」→「全般」→「クリーン時に削除する拡張子」に、*.fxbinや*.vsbinを指定しておくとさらに確実になるかもしれません。上記例の.fxbinや.vsbinは自分が勝手に付けたシェーダーバイトコード用拡張子です。VS 2012/2013の組み込みビルド ツールでは、シェーダーバイトコードの拡張子は.cso(Compiled Shader Object?)がデフォルトのようですが、あえて区別を付けるために分けています。

ちなみにVS 2012/2013でHLSLのシンタックス ハイライト(色分け)が効くようになる拡張子は、.fx, .hlsl, .hlsliだけのようで、「ツール」→「オプション」→「テキスト エディター」→「ファイル拡張子」で追加することもできない模様。

なお、自分はCg(.cg, .cgfx)、OpenCL(.cl)、GLSL(.vert, .frag, .glsl)のソースファイルをVisual Studioで編集する際はVC++エディタに関連付けていますが、float4やvec4などが色分けされないとちょっと不便。IDEの設定ファイルをいじると、ユーザー定義型をキーワードとして色分けすることもできるようになるんですが、わざわざそこまでするのも面倒……どのみちインテリセンスは効かないので、愛用のテキストエディタであるMIFESで編集したほうがよかったりします。

Windows 8.1のD3DCompiler

なおWindows 8.1DirectX 11.2では分割コンパイルのサポートに伴って、どうやらD3DCompilerのAPIの一部がストア アプリでもエンドユーザー環境で再び使えるようになっているらしいです。

復活した詳しい経緯は調べてませんが、たぶんDirectXのパワーユーザーである海外のハイエンドゲーム開発者から実行時コンパイルに関する強い要望もあったんだと思います(現にCrysisとかはシェーダーの実行時コンパイルを使って実行環境や設定に合わせて最適化してるっぽい)。個人的にはDynamic Shader Linkage用のリフレクション機能は便利だとは思うけど、それ以外は特に無くても困らないと思うんですが。また、HLSL shader compiler sampleなるものも公開されています。DirectX 11.2を採用しているXbox Oneでも同様にD3DCompilerが使えるようになる模様。まだ試してはいませんが、つまりWindows 8.1Xbox OneではVisual Studio 2013/Windows SDK 8.1に対応する最新のD3DCompilerランタイムが標準インストールされていて、デスクトップ アプリ同様にDynamic Shader LinkageはもちろんEffects11の全機能が使えるってことでしょう。ただ、Win7Win8.0などの旧環境でも(D3DCompilerランタイムの追加インストールをすることなく)そのまま動作するようにしたければ、今回紹介したCompact Effects11を使う意義はありそうです。

オリジナルのEffects11に関して

今回ベースにしたEffects11は、DirectX SDK June 2010に付属していたソースコード+プロジェクトです。なお、Effects11自体は一応CodePlexに移管されているようです。ただしD3DCompilerに依存する従来のコードをそのまま使っているらしく、Win8.0ストアアプリには使えない模様(ストアに提出できない)。いずれにせよ、将来的にDirectX 12のシェーダーモデル6.0対応fxc.exeではEffects12やエフェクトファイルの新しいプロファイル(fx_6_0?)やテクニック(technique12)が提供されない可能性が高いので、それを覚悟して使う必要があります。

Photoshopでゴッドレイ

(これは2012-06-07に書いた故OCNブログの記事を移植したものです)

ゴッドレイっていうのはいわゆるエンジェルラダー(天使の梯子)とかのアレです。雲間などから光条が漏れているような効果のことです。英語では"God rays"とか"Crepuscular rays"と言うそうです。LightWave v9にはゴッドレイそのものの機能がない、というかボリュームライト*1を使っただけでは画面が白飛びするので、Photoshopでスクリーンスペースのポストエフェクトをかけようと思って真っ先に思いついたのが「ぼかし(放射状)」なんですが、いまいち思った通りの結果になりません。で、「Photoshop ゴッドレイ」でGoogle検索したんですが、これまたあんまり良いのが引っかかりません。代わりにVueっていう景観作成ソフト関連の記事が引っかかります。「Photoshop 差し込む光」で検索すると、よさげな解説サイトがありました。やっぱりそこでも「ぼかし(放射状)」を使っていましたが、キーとなるのは「種類」を「ズーム」にすること。デフォルトの「回転」だと思ったようなぼかし結果になりませんので注意。

・適用前:
https://sygh-jp.github.io/content_hosting/illust/godray_making1.png

画像右上の黄色の部分が別レイヤーになっているんですが、このレイヤーに放射ぼかしを適用し、加算合成設定にして、何回かコピーします。

・適用後:
https://sygh-jp.github.io/content_hosting/illust/godray_making2.png

今回のは放射が弱いので、ゴッドレイというよりはむしろブルームエフェクトです……
ぼかす前の黄色の部分の形状を工夫すれば、もっと光が差し込んでいるような感じにもできるんじゃないでしょうか。

余談

ちなみに2012年当時使っていたCPUはAMD Athlon 64 X2 5600+で、キッチン(3Dモデルデータ)のほうは反射レイトレ+ラジオシティレンダリングしたんですが、出力解像度を5,000ピクセル×3,750ピクセルアンチエイリアスを中程度(エンハンス)設定にして1時間半くらいかかりました。ポリエチレン製のまな板に表面下散乱(SSS)を適用したかったんですが、いまいち思った通りの設定にできなかったというかものすごいレンダリング時間がかかりそうだったのであきらめました。その後Core-i7 4770KとLightWave 11を入手し、もっと高速にレンダリングできる環境を構築したので、積極的に使い込んでいきたいところです。

*1:LightWave日本語版では「ヴォリュームライト」表記になっています。