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