Sprite3Dと頂点シェーダの連携

マリオカート的な画面を作ろうとすると、少なくとも地面の絵は透視座標変換してパースをつけた描画をする必要がある。これをどのように実現するかというのは大きな問題で、いまどきのPCは速いといってもソフトウェアで計算&描画するのは負荷が大きすぎる。せっかく頂点シェーダが(かなり無理矢理だけど)使えるんだから、それを利用してみたいところだ。
この場合の課題はビルボード的に扱うSprite3Dの座標変換と、地面描画の座標変換のすりあわせだ。なぜならSprite3Dの描画システムは通常の画像描画で、Direct3Dに渡す値はDXRuby内部で算出していて、Spriteのパラメータはスクリーン座標なのだが、頂点シェーダを使う描画の座標はDirect3Dに渡す座標系を算出することになって、ここに違いが発生するからだ。
基本的にはプログラム内ではどちらも同じ座標系(ワールド座標系)で表現して、ビュー行列(カメラ)までは同じものを使って、射影変換行列を細工することでそれぞれ違う座標系を出力するということをする。Sprite側はスクリーン座標に変換するためにビューポート行列も使うから、そこで細工するのもありかもしれない。
ということで作ってみた。

おわかりいただけただろうか。地面の画像の上にSpriteによるビルボードが乗っているように見える。
Sprite側は縦のサイズをランダムで生成していて、新機能のoffset設定を使って画像の真ん中一番下が描画指定位置になるようにずらしているため、サイズの違う画像が地面に乗っているような描画ができている。この場合、Spriteで指定する座標は地面と同じ高さでよくなって、画像のサイズにあわせて座標を計算する必要がなくなるのでラクだ。自分がラクをするために機能が追加されるのはDXRubyの伝統である。
ソースは続きに。
ちょっとヤッツケなのでインターフェイスがばらばらだったりカメラの指定が生のビュー行列だったりしてまだまだ荒削りである。もうちょっと何とかしないとゲームには使えない。

require 'dxruby'

class View
  attr_accessor :m, :sw, :sh, :zn, :zf
  def initialize(zn, zf, target = nil)
    if target == nil
      target = Window
    end
    @m = Matrix.new
    @zn = zn
    @zf = zf
    @sw = target.width
    @sh = target.height
  end
end

class Sprite3D < Sprite
  attr_accessor :v

  def self.set_transform(view)
    @@view = view
    @@proj = Matrix.new([[2.0 * view.zn / view.sw, 0, 0, 0],
                         [0, 2.0 * view.zn / view.sh, 0, 0],
                         [0, 0, view.zf / (view.zf - view.zn), 1],
                         [0, 0, -view.zn * view.zf / (view.zf - view.zn), 0]])
    @@vp = Matrix.new([[ view.sw / 2, 0, 0, 0],
                       [ 0, view.sh / 2, 0, 0],
                       [ 0, 0, -1, 0],
                       [ view.sw / 2, view.sh / 2, 0, 1]])
    @@zn = view.zn
  end

  def initialize(v, image)
    @v = v
    self.image = image
    self.offset_x = nil
    self.offset_y = -image.height
    self.center_y = image.height
  end

  def update
    temp = (Vector.new(0,0,0,1) + @v) * @@view.m * @@proj
    self.scale_x = self.scale_y = @@zn / temp.w
    self.xyz = temp / temp.w * @@vp
  end
end

class Polygon
  attr_accessor :image, :view, :m, :z

  hlsl = <<EOS
  float4x4 g_world, g_view, g_proj;
  texture tex0;

  sampler Samp = sampler_state
  {
   Texture =<tex0>;
  };

  struct VS_OUTPUT
  {
    float4 pos : POSITION;
    float2 tex : TEXCOORD0;
  };

  VS_OUTPUT VS(float4 pos: POSITION, float2 tex: TEXCOORD0)
  {
    VS_OUTPUT output;

    output.pos = mul(mul(mul(pos, g_world), g_view), g_proj);
    output.tex = tex;

    return output;
  }

  float4 PS(float2 input : TEXCOORD0) : COLOR0
  {
    return tex2D( Samp, input );
  }

  technique
  {
   pass
   {
    VertexShader = compile vs_2_0 VS();
    PixelShader = compile ps_2_0 PS();
   }
  }
EOS

  @@core = Shader::Core.new(hlsl, {:g_world=>:float, :g_view=>:float, :g_proj=>:float})

  def initialize(image, view, z)
    @shader = Shader.new(@@core)
    @view = view
    @image = image
    @z = z
    @m = Matrix.new
  end

  def update
    @shader.g_world = @m
    @shader.g_view = @view.m.to_a
    @shader.g_proj = [-2.0 * @view.zn / @view.sw, 0, 0, 0,
                      0, 2.0 * @view.zn / @view.sh, 0, 0,
                      0, 0, -(@view.zf / (@view.zf - @view.zn)), -1,
                      0, 0, -(-@view.zn * @view.zf / (@view.zf - @view.zn)), 0]
  end

  def draw
    Window.draw_shader(-@image.width/2, -@image.height/2, @image, @shader, @z)
  end
end



view = View.new(-300.0, -5000.0)
view.m = Matrix.translation(0, 200, 0) * Matrix.rotation_x(-10) * Matrix.translation(0, 0, -2000)

Sprite3D.set_transform(view)
objects = Array.new(20) {
  Sprite3D.new(Vector.new(0, 0, 0), Image.new(200, rand()*300+100, [rand()*255, rand()*255, rand()*255]))
}

grand = Polygon.new(Image.load("bgimage/BG13a_80.jpg"), view, -3000)

y_angle = 0
Window.loop do
  grand.m = (Matrix.scaling(6,6,1) * Matrix.rotation_x(90) * Matrix.translation(0,0,-800)).to_a

  y_angle += 2
  objects.each_with_index do |s, i|
    s.v = Vector.new(1500, 0, 0) * Matrix.rotation_y(i * 18 + y_angle) + Vector.new(0, 0, -500)
  end

  Sprite.update(objects)
  grand.update
  Sprite.draw(objects)
  grand.draw

  break if Input.key_push?(K_ESCAPE)
end