CustomRenderTarget

では新機能について説明。

CustomRenderTarget

リファレンスにも書いてあるけども。RenderTargetを継承して作られている。現在はまだ残っているがdraw系メソッドは呼び出しても何も起こらない。消す予定。ユーザはCustomRenderTargetを継承してcustom_renderメソッドを自分で実装して使う。custom_renderに渡される引数は低レベルインターフェイスモジュールで、こいつを経由して低レベルの描画機能を呼び出す。

VertexBuffer

頂点バッファ。頂点のフォーマットをnewに渡して生成する。頂点情報の値は配列にrubyオブジェクトでIntegerやFloatを詰めてVertexBuffer#vertices=に渡す。こいつをcustom_render内で使う。


ということで以下もうちょい具体的な話。

サンプル

1.5.21devのサンプルにも含めておいたsample_1.rbが最低限のコードとなる。この機能を扱うにはシェーダが必須。

require 'dxruby'

class RenderTargetSample < CustomRenderTarget
  # 描画用HLSL
  hlsl = <<EOS
  struct VS_INPUT // 頂点シェーダの入力フォーマット
  {
    float4 vPosition    : POSITION0;    //頂点座標
  };

  struct VS_OUTPUT // 頂点シェーダの出力フォーマット
  {
    float4 vPosition    : POSITION0;    //頂点座標
  };

  VS_OUTPUT VS(VS_INPUT v) // 頂点シェーダ
  {
    VS_OUTPUT output;
    output.vPosition = v.vPosition; // 座標はそのまま出力
    return output;
  }

  struct PS_INPUT // ピクセルシェーダの入力フォーマット
  { // ピクセルシェーダの入力無し
  };

  struct PS_OUTPUT // ピクセルシェーダの出力フォーマット
  {
    float4 vColor       : COLOR;     //最終的な出力色
  };

  PS_OUTPUT PS(PS_INPUT p) // ピクセルシェーダ
  {
    PS_OUTPUT output;
    output.vColor = float4(1.0, 1.0, 1.0, 1.0); // 白固定(RGBA)
    return output;
  }

  technique // 使うシェーダの設定
  {
    pass
    {
      VertexShader = compile vs_2_0 VS();
      PixelShader = compile ps_2_0 PS();
    }
  }
EOS

  # Shader::Core生成
  @@core = Shader::Core.new(hlsl)

  def initialize(*)
    super
    @shader = Shader.new(@@core)

    # 頂点バッファ
    @vertex_buffer = VertexBuffer.new([
      [D3DDECLTYPE_FLOAT3, D3DDECLUSAGE_POSITION, 0],
    ])

    # 頂点バッファにデータを設定
    @vertex_buffer.vertices = [-1.0,  1.0, 0.0, # 頂点1(xyz)
                               -1.0, -1.0, 0.0, # 頂点2(xyz)
                                1.0,  1.0, 0.0] # 頂点3(xyz)
  end

  # CustomRenderTargetの描画メソッド
  def custom_render(o) # oは低レベルインターフェイスオブジェクト。custom_renderの中でのみ使える。
    # ビューポート設定
    o.set_viewport(0, 0, width, height, 0, 1)

    # 描画開始
    o.begin_scene

    # 使うシェーダ選択
    o.using_shader(@shader) do
      # 使う頂点バッファ選択
      o.set_stream(@vertex_buffer)

      # 描画
      o.draw_primitive(D3DPT_TRIANGLELIST, @vertex_buffer.vertex_count / 3)
    end

    # 描画終了
    o.end_scene
  end
end

# 画面サイズ
Window.width, Window.height = 320, 240

# RenderTargetSampleオブジェクト生成
rtsmpl = RenderTargetSample.new(Window.width, Window.height)

Window.loop do
  # 画面に描画
  Window.draw(0, 0, rtsmpl)
end

これを実行すると以下のような画像が表示される。

この記事ではこのコードでなぜこの結果になるのか、をおおまかに解説する。細かいことは後日。

頂点バッファ

サンプルでは頂点バッファに

      [D3DDECLTYPE_FLOAT3, D3DDECLUSAGE_POSITION, 0],

とフォーマットを宣言していて、これはつまり単精度浮動小数点数3個で座標を表現していますよ、という意味である。んで頂点データは

    # 頂点バッファにデータを設定
    @vertex_buffer.vertices = [-1.0,  1.0, 0.0, # 頂点1(xyz)
                               -1.0, -1.0, 0.0, # 頂点2(xyz)
                                1.0,  1.0, 0.0] # 頂点3(xyz)

となっていて、3個の値で表現されるxyz座標が3セット。これで三角形を表現している。Rubyには単精度浮動小数点数が存在しないので、FloatなりIntegerなり適当に渡せば変換されるようになっている。GPUは三角形を描画する機能を実装したハードウェアであるので、基本的に頂点3つで三角形を表現する。
座標系は3Dの場合はDirectXでは一般的に、xは右方向に+、yは上方向に+、zは奥方向に+となる。頂点バッファに詰める情報は基本的にはローカル座標系(モデルの基点を(0,0,0)とした座標系)なのだが、それはそれとして、頂点123が左上、左下、右上を意味することはわかるだろう。

頂点シェーダ

頂点シェーダのコードはこのようになっている。

  struct VS_INPUT // 頂点シェーダの入力フォーマット
  {
    float4 vPosition    : POSITION0;    //頂点座標
  };

  struct VS_OUTPUT // 頂点シェーダの出力フォーマット
  {
    float4 vPosition    : POSITION0;    //頂点座標
  };

  VS_OUTPUT VS(VS_INPUT v) // 頂点シェーダ
  {
    VS_OUTPUT output;
    output.vPosition = v.vPosition; // 座標はそのまま出力
    return output;
  }

頂点バッファから受け取るデータが入力側で、ピクセルシェーダに渡すのが出力側となる。頂点バッファのデータが座標しかないので受け取る情報は座標のみ、頂点シェーダは座標を出力する必要があるが出力できるものが座標しかないのでこちらも座標のみ。そして、何も計算せず「そのまま」出力する。

頂点シェーダの役目と座標系

通常、頂点シェーダの入力になる頂点バッファの中身はキャラや建物などのモデルデータであり、それらはローカル座標系である。後段にくるピクセルシェーダは画面の画素に対応する。モデルデータと画素の橋渡しをするのが頂点シェーダの役割といえる。実際にはビューポート変換が間に入るので、ビューポート変換直前の値を作るのが頂点シェーダの役割である。
3Dの場合、モデルデータはキャラごとに重なる場所に座標があるので、まずこれをワールド座標系に変換して別の位置にして、更にごにょごにょ(省略)する。最終的に出力される値は、画面の左上を(-1,1,z)、右下を(1,-1,z)とする座標系となる。これをビューポート変換で(0,0)〜(width,height)にして、画面の画素に対応させた上でピクセルシェーダが起動されるわけだ。
今回のサンプルでは頂点シェーダは何もしていないので頂点バッファの値がそのまま出力される。従って、頂点123はそれぞれ「画面の左上隅」「画面の左下隅」「画面の右上隅」を表現することとなる。それがビューポート変換(サンプルのo.set_viewportで定義)されて、実際の画面の隅から隅までの三角形となる。
この場合、出力のzって何に使うの?って話になるが、これはテクスチャのパースと画像のクリッピングに使われる。今回はテクスチャを貼っていないしzは0固定なので関係ないが、後日そのへんもやりたい。

ピクセルシェーダ

  struct PS_INPUT // ピクセルシェーダの入力フォーマット
  { // ピクセルシェーダの入力無し
  };

  struct PS_OUTPUT // ピクセルシェーダの出力フォーマット
  {
    float4 vColor       : COLOR;     //最終的な出力色
  };

  PS_OUTPUT PS(PS_INPUT p) // ピクセルシェーダ
  {
    PS_OUTPUT output;
    output.vColor = float4(1.0, 1.0, 1.0, 1.0); // 白固定(RGBA)
    return output;
  }

ピクセルシェーダは画素1個に対して1回起動される。ので、シェーダの関数は自分がどこに描画しようとしているのかを知る必要が無い。ので座標の入力は無い。入力が何も無いので出力も固定するしかなく、とりあえず白としている。頂点シェーダとビューポート変換により作られた画面左上半分を埋める三角形の画素に対してピクセルシェーダが起動されるので、実行結果は左上半分が白くなる。

custom_render

DXRuby1.5.21devの新機能はDXRuby初の抽象クラスであり、継承してcustom_renderを実装して使う。今回のサンプルでは以下のように実装している。現状ではこれが最低限だが今後もっと減るかもしれないし増えるかもしれない。

  def custom_render(o) # oは低レベルインターフェイスオブジェクト。custom_renderの中でのみ使える。
    # ビューポート設定
    o.set_viewport(0, 0, width, height, 0, 1)

    # 描画開始
    o.begin_scene

    # 使うシェーダ選択
    o.using_shader(@shader) do
      # 使う頂点バッファ選択
      o.set_stream(@vertex_buffer)

      # 描画
      o.draw_primitive(D3DPT_TRIANGLELIST, @vertex_buffer.vertex_count / 3)
    end

    # 描画終了
    o.end_scene
  end

引数のoは低レベルインターフェイス用の無名モジュールである。custom_render以外から呼んで欲しくないという意味を込めて無名かつ引数渡しとなっている。
set_viewportが頂点シェーダのところで書いたビューポート変換定義となる。よほど特殊なことをしない限りこの記述はこのままでよい。であれば省略可能にするのもアリか?
begin_scene、end_sceneはDirectXで必要だから。ほかのRenderTargetをテクスチャとして使いたい場合、それの画像生成はbegin_scene〜end_sceneの外側でやる必要があると思われるのでそういう理由でメソッドが定義されている。
using_shaderはシェーダを使う際に非常に面倒なAPIたたきが発生するのでそれの隠蔽用。set_streamは頂点バッファをストリームソースとして設定する処理を頂点宣言とまとめたもの。draw_primitiveはDirectX9の同名のAPIほぼそのまま。ストリームソースに設定されたバッファを使ってシェーダを駆動して描画する。
という形で、DXRubyの描画と比べると手続きがやや面倒だが、DirectX9そのものと比べるとかなりシンプルになっていて、あと必要なものは数学を駆使した座標計算や色計算となる。とりあえずこの流れさえ理解できればたいがいの物は応用で作れる。

おしまい

頂点シェーダに何を書けば何ができるのか、ピクセルシェーダで何を作れるのか、他、APIレベルでどういうことが可能なのか、というのを今後やっていこうと思う。色んなことができるように作ってある。すべては応用。