- Author
-
しのかろ(Twitter@shinokaro)
- URL
- 加筆修正
-
2013-12-26
DXRuby Advent Calendar 2013 の21日目は“しのかろ”が記事をお送りします。前日はaotakさんが ゲーム作りました という記事を書かれました。内容は、実際にゲームを作る上での思考過程についてでした。
ゲームを完成させるには他の作品ジャンルに比べて時間が掛かります。このため、ゲーム製作の経験を積むことが難しくなります。そこで、ゲーム製作事例の記事が重要になります。他のゲーム製作事例を記事として読むことでゲーム製作の知見を得ることが出来ます。
ゲーム製作に関する文章情報は公開されない傾向にあると思います、そういった点でaotakさんの記事は貴重なものだと思います。
さて、私の記事は、DXRubyにあるShaderの使い方についてです。Shaderを使用すればDirectXの力を引き出すことができます。これは、DXRubyを使う理由の一つになります。ただし、Shaderを使うためにはHLSLという別の言語、実装を学ぶ必要があります。
HLSLとはHigh Level Shader Languageの事です。これは、マイクロソフトによって作成されたDirectXにおけるシェーディングのための言語です。Shderの単語がShadingとされることもあり、略称というより固有名詞なのだと思います。
HLSLについての解説は本稿では行いません。なぜならHLSLは一つの言語ですので簡単には説明できないのです。HLSLの言語仕様は公開されていますので、その情報源を示しておきます。
自己学習にあたっては、注意すべき点があります。グラフィック・ハードウェア、GPUと呼ばれるものの概念と実装について知っておく必要があるでしょう。なぜなら、実際のハードウェアーの仕組みを前提としてHLSLが設計されているためです。
次に、Shadingについて説明します。Shadingとは絵における濃淡、陰影表現の事を指します。濃淡、陰影を通じて立体を表現する、転じて3DCGにおける立体描画技術を指す、ということなのだと思われます。
よって、Shaderとは3DCGにおける描画データーを変化させる仕組み、となるでしょう。描画データーとは頂点情報、面情報、テクスチャー画像、ボーン、モーションデーターなどがあげられます。Shaderはこれらのデーターを変化、つまり計算して書き換える仕組みです。そして、その仕組みの利用の仕方を記述するのがShader Languageということになります。Shader Languageはいくつか種類がありますが、DirectXで利用できる言語はHLSLとなります。
Shaderの記述言語は3DCGを描画するハードウェアーと密接に関連しています。そして、そのハードウェアーはメーカー、製品ごとに全く違います。ですから、言語の仕様が統一されていても、そのままハードウェアーで動作することは不可能です。極端な話、ゲームプレイヤーのPC毎にハードウェアーの内容が違うのです。この問題を解決するために、HLSLは実行時に個別のハードウェアーのためにコンパイルを行います。
こうすることで、統一したプログラムの記述、個別のハードウェアー、ハードウェアー言語による高速処理を実現します。
今回はDXRubyのShaderを使ってスプライトを半透明にする事例を説明します。これを取り上げた理由はDXRubyのSpriteではShaderを適用すると、Spriteのalphaが反映されないからです。
これは問題になりません。なぜなら、Shaderを利用すれば画像へのエフェクトを全て行えます。半透明など簡単なことです。
DXRubyからShaderを使用するには幾つかの手間が必要です。値を代入すると、すぐに結果が出るDXRubyスタイルからすると面倒に感じるかもしれません。
最初にHLSLで書かれたコードを用意します。これは、文字列として扱います。次に、このコードを引数としてDXRuby::Shader::Coreに与えてShader::Coreオブジェクトを作成します。
なぜ、DXRuby::Sprite#shader=“HLSL_Code”ではいけないのでしょうか?その理由は、Shade::Coreは使い回しが出来るからです。つまり、SpriteごとにShader::Coreを生成する必要はありません。Shader::Coreは使用するエフェクトごとに生成して保持しておけばよいのです。HLSLのコンパイルには時間が掛かります。フレームレートを維持しなければならないゲームでは、使用前に準備する必要があるでしょう。
次に、先ほど作成したShader::Coreオブジェクトを引数としてDXRuby::Shaderに与えてShaderオブジェクトを作成します。これを、DXRuby::Spriteオブジェクトに渡してShaderが動作状態になります。
ここで、DXRuby::Sprite#shader=Shader_objectではないのは、なぜでしょうか?その理由は、Shader::Coreがプログラムコードであり、Shaderはコード実行用リソースを確保しているからです。
この仕組み(手間)のおかげで、“単一のコード、複数の実行環境”を作り出すことが出来ます。こうして、目的のエフェクト処理を指定しパラメーターを個別に与えることで、それぞれ違う結果を得られます。
しかし、この仕組みを逆手に使う方法も考えられます。例として、あえてShaderオブジェクトを共有(使いまわし)することです。パラメーターも共通のShaderですから、共有しているSprite達は全く同じエフェクトが掛かることでしょう。
こういった処理の管理は、ゲームデザインと密接に関連しています。“コードの選択”を考える必要がありますし、それを行うのがゲームプログラマーの仕事です。
DXRubyのShader::Coreを継承してMyShaderCoreクラスを作成します。
今回は実装にあたって、シングルトン・パターンを適用します。そのためにRubyに標準添付されている拡張ライブラリーsingletonを使用します。この場合に起きる問題は、DxRubyスターターキットでは動作しなくなることです。このため、ゲーム配布に当たっては自分でパッケージを作ることになるでしょう。
この辺の話は、DXRuby Advent Calendar 2013 24日目、aotakさんの記事{“DXRubyで作ったゲームを配布する”}[http://blog.aotak.me/post/71009543722/publish-dxruby-game] を参考にするとよいでしょう。
require 'singleton' class MyShaderCore < ::DXRuby::Shader::Core include Singleton # このクラスはただ一つのオブジェクトを生成するため引数を取らない。 # つまり、initializeメソッドに具体的な内容を書き込むことになる。 def initialize # ヒアドキュメントを書き出す際に << ではなく <<- を使用すると終端行でインデントが使用できる。 # 識別子には自由な名前をつけることが出来るので、ここでは HLSL とする。 # 終端行は当然 HLSL と書き、インデントも行う。 # さらに、開始ラベル <<-HLSL は“式”なので下記のような記述が出来る。 super *[<<-HLSL, {alpha: :float}] // ヒアドキュメント内はDirectXに渡すコードとなる。 // つまり、コメントの記述がC, C++形式になる。 // これは、Rubyコード中にHLSLを記述するときに間違いやすい。 float alpha; texture tex0; sampler Samp = sampler_state { Texture =<tex0>; }; float4 PS(float2 input : TEXCOORD0) : COLOR0 { float4 color = tex2D( Samp, input); float4 result = { color.r, color.g, color.b, alpha }; return result; } // /* 一つのシェーダープログラムを分岐させることで、複数の機能をもたせることが出来る。 ただし、シェーダー・プログラムのステップ数の制限など使い回しには限界がある。 */ technique { pass { PixelShader = compile ps_2_0 PS(); } } HLSL end # このクラスについては、コードはこれで終わりである。 # HLSLコードを別ファイルにするアイデアもある。 # その場合に起きる問題は、パラメータの指定をどうするかである。 # 対処法としてはHLSLコードとパラメーターをYAMLで記述しておく。 # これを、読み込みメタプログラミングでMyShaderCoreクラスを生成する。 end
次にMyShderCoreを利用するためのMySpriteクラスを作成します。
DXRubyのSpriteをそのまま使ってもよいのですが、専用クラスを作ることで利便性を向上させます。主な目的は自作Spriteの設定をShaderパラメーターに反映させることです。
class MySprite < ::DXRuby::Sprite # DXRuby::Shader::Coreを生成後、DXRuby::Spriteにセットする前にDXRuby::Shaderに # セットする必要がある。これをまとめて行うメソッドを定義する。 # DXRuby::Shaderオブジェクトを使いまわす場合も想定されるので shader= を # オーバーライドすることは避ける。 def shader_core=(core) self.shader= Shader.new(core) end # shaderへのdelegate # technigueの指定はSpriteに対するもの、という考えから def technique self.shader.technique end def technique=(val) self.shader.technique= val end # shaderのパラメーターへのdelegateは推奨できない。 # 予測される事態として自作スプライトとのメソッド名の衝突が考えられる。 # つまり、メソッド名とメソッドが行う事を考慮すべきである。 # def shader_parameter=(val) # self.shader.parameter # end # 画素情報の値が0~255の8Bitというのは旧来の決まりごとである。 # Shaderでは0.0~1.0のフロートで取り扱う。 # 今後、未来においては画素情報の割り当てビットは不定(実装依存)であるし # 画像処理の数式からいえばフロートのまま記述できたほうが簡単になる。 def f_alpha self.alpha / 255.0 end def f_alpha=(a) self.alpha= a * 255 end # draw時にスプライトのalpha値をshaderに適用する。 # 毎回、適用するのは無駄に思えるかもしれない。 # しかし、アクセッサに適用機能を持たせると、アクセッサを継承先で # オーバーライドしたときにsuperが呼ばれる保障はない。 # 安全性を取って描画時に適用処理を行う。 def draw self.shader.alpha= self.f_alpha super end end
DXRubyを通じてHLSLを利用することでRubyの環境からOSの保護を受けつつドライバー、ハードウェアーを直接コントロールする事ができます。このような手段が用意さていればRubyからもヘテロジニアス・システム・アーキテクチャーを利用すること出来ます。
そういった場合にHLSLのような別の言語を使用することに抵抗があるかもしれません。しかし、RubyであればHLSLをRubyで記述するDSLを実装する事も考えられるでしょう。そのようなことを可能にするのがRubyの強みだと考えます。
以上、DXRubyからのHLSL使用方法でした。明日は土井ヴぃさんが記事を書きます。
長文、ご拝読ありがとうございました。