2014/01/15(水) [n年前の日記]
#2 [dxruby] DXRuby+Shaderで床ラスタースクロールっぽいことができたような気がする
Shader利用サンプルの sample_spehari.rb を弄って、望んだ感じの床ラスタースクロール、っぽいことができたような気がする。たぶん。
sample_spehari.rb を眺めていて、気づいた点を列挙。
こんな感じに。
STGサンプルに組み込んでみたら、こんな感じ。 どう見ても、 _「お願い、私の星を助けて…」 ですな。アレも、こんな感じのことをしてたのかしら。
長いけど、一応ソースも貼っときます。
_floorrasterscroll.rb
今回も、単体で実行しても動くけど、他のファイルから呼んでも使えるようにしておきました。本体ファイル?と同じフォルダに floorrasterscroll.rb を入れといて、以下のような記述で使えるかなと…。
サンプルを弄って作っただけなので、これもサンプル同様、Public Domain にしておきます。それと、画像は以下からDLして使ってみてください。
_cloud.png (320x320 dot)
もやもやした模様ではよく分からないでしょうから、あの有名な、 _lena様画像 も置いときますね…。
_lena.png (160x160 dot)
使うと、以下のような感じになります。
やっぱり、lena様のような画像で実験すると、なんだかやる気がでてくるなあ。
sample_spehari.rb を眺めていて、気づいた点を列挙。
- Shaderは、スクロール処理をしていない。あくまで、渡された画像を透視変換して描画することのみに専念してる。
- するとスクロール処理はどうやってるかというと…。縦横スクロールさせた後の画像を、Shader に渡すことでやっていた。
- 縦横スクロールは、元画像と、RenderTarget#drawTile() を使って行う。RenderTarget に描いた画像を Shader に渡す感じ。
こんな感じに。
STGサンプルに組み込んでみたら、こんな感じ。 どう見ても、 _「お願い、私の星を助けて…」 ですな。アレも、こんな感じのことをしてたのかしら。
長いけど、一応ソースも貼っときます。
_floorrasterscroll.rb
require 'dxruby' # # Shaderを使って床ラスタースクロールっぽい処理をするクラス # class FloorRasterScroll @@hlsl = <<EOS float floor_distance; float screen_distance; float x_diff; float y_diff; float alpha; texture tex0; sampler Samp0 = sampler_state { Texture =<tex0>; MipFilter = LINEAR; MinFilter = LINEAR; MagFilter = LINEAR; AddressU = WRAP; AddressV = WRAP; }; float4 PS(float2 input : TEXCOORD0) : COLOR0 { float4 output; input.y = screen_distance * floor_distance / -(input.y + y_diff); input.x = (input.x - x_diff) * -input.y / screen_distance; output = tex2D( Samp0, input ); output.a = alpha; return output; } technique FloorRasterScroll { pass P0 { PixelShader = compile ps_2_0 PS(); } } EOS attr_accessor :core, :shader, :image attr_accessor :w, :h attr_accessor :tilew, :tileh attr_accessor :rt_w, :rt_h, :rt_scaley attr_accessor :rt attr_accessor :floor_dist, :scr_dist, :x_diff, :y_diff, :alpha # # 初期化処理 # # @param [Image] image 使用する画像 # @param [float] floor_dist 床までの距離。1.0前後が目安 # @param [float] scr_dist 画面までの距離。1.0以下が目安 # @param [float] y_diff 視線方向を上下にずらす量。0.0から1.0の範囲 # @param [float] alpha 透明度。0.0で透明。1.0で不透明 # @param [int] w 描画横幅。nil ならウインドウ横幅 # @param [int] h 描画縦幅。nil ならウインドウ縦幅 # def initialize(image, floor_dist=0.3, scr_dist=0.3, y_diff=0.1, alpha=1.0, w=nil, h=nil) self.core = Shader::Core.new(@@hlsl,{ :floor_distance=>:float, :screen_distance=>:float, :x_diff=>:float, :y_diff=>:float, :alpha=>:float }) self.shader = Shader.new(self.core, "FloorRasterScroll") self.image = image self.w = (w == nil)? Window.width : w self.h = (h == nil)? Window.height : h self.floor_dist = floor_dist self.scr_dist = scr_dist self.y_diff = y_diff self.alpha = alpha # 画面を覆い隠すタイル数を求める self.tilew = self.w.quo(self.image.width).ceil self.tileh = self.h.quo(self.image.height).ceil # RenderTarget の必要サイズを求める self.rt_w = self.image.width * self.tilew self.rt_h = self.image.height * self.tileh # y方向で拡大縮小して描画縦横幅に合わせる self.rt_scaley = self.h.quo(2).quo(rt_h) # Rendertarget生成 self.rt = RenderTarget.new(rt_w, rt_h) # 画面の真ん中に消失点?を持ってくるための補正値を算出 self.shader.x_diff = 0.5 * self.w.quo(self.rt_w) end # # 描画処理 # # @param [int] x 描画x座標 # @param [int] y 描画y座標 # @param [float] scrl_x 横方向スクロール位置 # @param [float] scrl_z 奥行方向スクロール位置 # @param [Hash] opts オプション # @option opts [float] floor_dist 床との距離。1.0前後が目安 # @option opts [float] scr_dist 画面との距離。1.0以下が目安 # @option opts [float] y_dist 視線方向を上下にずらす量。0.0 - 1.0 の範囲 # @option opts [int] z 描画奥行情報 # @option opts [float] alpha 透明度。0.0で透明。1.0で不透明 # @option opts [Boolean] vflip trueなら上下判定。falseなら反転しない # def draw(x, y, scrl_x, scrl_z, opts={}) self.floor_dist = opts[:floor_dist] if opts.has_key?(:floor_dist) self.scr_dist = opts[:scr_dist] if opts.has_key?(:scr_dist) self.y_diff = opts[:y_diff] if opts.has_key?(:y_diff) self.alpha = opts[:alpha] if opts.has_key?(:alpha) self.shader.floor_distance = self.floor_dist self.shader.screen_distance = self.scr_dist self.shader.y_diff = self.y_diff self.shader.alpha = self.alpha vflip = 1 vflip *= -1 if opts.has_key?(:vflip) and opts[:vflip] == true z = 0 z = opts[:z] if opts.has_key?(:z) # Shader が参照する画像を RenderTarget に作成 self.rt.draw_tile(0, 0, [[0]], [self.image], scrl_x, scrl_z, self.tilew, self.tileh).update # Shader を使って透視変換しつつ描画 Window.drawEx(x, y, self.rt, :shader=>self.shader, :scaley=>self.rt_scaley * vflip, :centery=>0, :z=>z) end # # 動作確認用。グリッド画像を生成して返す # # @param [int] w 画像横幅 # @param [int] h 画像縦幅 # @return [Image] 生成した画像 # def self.get_grid_image(w, h) image = Image.new(w, h, [0, 255, 0]) image.box_fill(0, 0, w / 2 - 1, h / 2 - 1, [150,250,150]) image.box_fill(w / 2, 0, w - 1, h / 2 - 1, [100,250,100]) image.box_fill(0, h / 2, w / 2 - 1, h - 1, [200,250,200]) image.box_fill(w / 2, h / 2, w - 1, h - 1, [0,220,0]) return image end end # ---------------------------------------- if $0 == __FILE__ # 動作テスト # 画像を読み込む # imgfilename = "lena.png" imgfilename = "cloud.png" if File.file?(imgfilename) image = Image.load(imgfilename) else # 画像ファイルが見つからなければ、タイル画像を作成 image = FloorRasterScroll.get_grid_image(160, 160) end test_mode = 1 case test_mode when 0 # ---------------------------------------- # シンプルな使い方の例 bg = FloorRasterScroll.new(image) x = 0 z = 0 Window.loop do break if Input.keyPush?(K_ESCAPE) x += 8 z += 5 bg.draw(0, 240, x, z) end when 1 # ---------------------------------------- # 動作テスト。各パラメータを細かく変更しながら描画してみる bg = FloorRasterScroll.new(image) # 床用 bg2 = FloorRasterScroll.new(image) # 天井用 x = 0 z = 0 x_spd = 8 z_spd = 5 floor_dist = 0.3 scr_dist = 0.3 y_diff = 0.1 font = Font.new(16) fade_enable = false count = 0 Window.loop do break if Input.keyPush?(K_ESCAPE) # 速度リセット if Input.keyDown?(K_C) x_spd = 0 z_spd = 0 end # 横方向スクロール速度を変更 x_spd += 0.1 * Input.x # 奥行方向スクロール速度を変更 z_spd -= 0.1 * Input.y # 床との距離を変更 floor_dist += 0.01 if Input.keyDown?(K_W) floor_dist -= 0.01 if Input.keyDown?(K_S) # 画面までの距離を変更 scr_dist += 0.005 if Input.keyDown?(K_D) scr_dist -= 0.005 if Input.keyDown?(K_A) scr_dist = 0.05 if scr_dist < 0.05 # 視線を上下にずらす値を変更 y_diff += 0.005 if Input.keyDown?(K_R) y_diff -= 0.005 if Input.keyDown?(K_F) # 透明度を変更 fade_enable = !fade_enable if Input.keyPush?(K_Q) alpha = (fade_enable)? Math.sin(3 * count * Math::PI / 180.0).abs : 1.0 # スクロール位置変更 x += x_spd z += z_spd # 描画 bg.draw(0, 240, x, z, :vflip=>false, :floor_dist=>floor_dist, :scr_dist=>scr_dist, :y_diff=>y_diff, :z=>-255, :alpha=>alpha) bg2.draw(0, 240, x, -z, :vflip=>true, :floor_dist=>floor_dist, :scr_dist=>scr_dist, :y_diff=>y_diff, :z=>-255, :alpha=>alpha) # 説明文を描画 tx = 4 h = 20 Window.drawFont(tx, h * 0, "RIGHT , LEFT : x speed = #{x_spd}", font) Window.drawFont(tx, h * 1, "UP , DOWN : z speed = #{z_spd}", font) Window.drawFont(tx, h * 2, "W , S : floor dist = #{floor_dist}", font) Window.drawFont(tx, h * 3, "D , A : screen dist = #{scr_dist}", font) Window.drawFont(tx, h * 4, "R , F : y_diff = #{y_diff}", font) Window.drawFont(tx, h * 5, "Q : Fade in/out", font) Window.drawFont(tx, h * 6, "C : Speed clear", font) count += 1 end end end動作テスト部分のほうが、長いという…。
今回も、単体で実行しても動くけど、他のファイルから呼んでも使えるようにしておきました。本体ファイル?と同じフォルダに floorrasterscroll.rb を入れといて、以下のような記述で使えるかなと…。
# 床ラスター処理のテスト require 'dxruby' require_relative 'floorrasterscroll' image = Image.load("lena.png") bg = FloorRasterScroll.new(image) x = 0 z = 0 Window.loop do break if Input.keyPush?(K_ESCAPE) x += 8 z += 4 bg.draw(0, 240, x, z) end
サンプルを弄って作っただけなので、これもサンプル同様、Public Domain にしておきます。それと、画像は以下からDLして使ってみてください。
_cloud.png (320x320 dot)
もやもやした模様ではよく分からないでしょうから、あの有名な、 _lena様画像 も置いときますね…。
_lena.png (160x160 dot)
使うと、以下のような感じになります。
やっぱり、lena様のような画像で実験すると、なんだかやる気がでてくるなあ。
◎ 透視変換で頭が混乱してきたのでメモ。 :
透視変換をするShader ―― HLSLを書くあたりで、ちょっとハマった。なんだかややこしいので、一応図解してメモ。
まず、画面座標は、縦横 0.0 〜 1.0 の範囲になってる、と考える。また、元画像は、床に、つまり平面に、ずらりと並んでると考える。z値として得た値は、uv値のvとして使う、とでも言えばいいのか…。 もしかしたら v の方向は逆かもしれないけど。上が1.0なのか、下が1.0なのか、よく分かってません…。
真横から見た状態を考える。 作図ツールの関係で、上方向が +y になってるけど、プログラム上では、下方向が +y なので、ちと注意。
値が分かってるものと、値が分かってないものを、分類してみる。
(sz, sy) が作る三角形と (pz, py) が作る三角形は相似なので、以下が成り立つ。
さらに、真上から見た状態を考える。
これも、値が分かってるものと、分かってないものを、分類してみる。
で、注意点が。
sx は input.x から求まるのだけど、input.x の範囲は、画面左から右に向かって 0.0〜1.0 なので、真ん中は 0.5。なので、sx = input.x - 0.5 として、ずらしてやらないといけない。
sy は…。今回は、床を表示したいなーと思ってるので、画面の下半分だけ考えればいいよなと。だったら、sy = input.y でいいかなと。画面の上半分も含めて何かしたいなら、sy = input.y - 0.5 になるのだろうけど。
そんな感じで、HLSL内では、以下のようなソレを書けば良さそう。
まず、画面座標は、縦横 0.0 〜 1.0 の範囲になってる、と考える。また、元画像は、床に、つまり平面に、ずらりと並んでると考える。z値として得た値は、uv値のvとして使う、とでも言えばいいのか…。 もしかしたら v の方向は逆かもしれないけど。上が1.0なのか、下が1.0なのか、よく分かってません…。
真横から見た状態を考える。 作図ツールの関係で、上方向が +y になってるけど、プログラム上では、下方向が +y なので、ちと注意。
値が分かってるものと、値が分かってないものを、分類してみる。
- sy は、画面上の座標。HLSL 内の input.y が、そのものズバリなので、これは値が分かってる。
- sz は、画面までの距離。これはテキトーに決めるので、値が分かってる。画面の縦横が 0.0 〜 1.0 なので、sz も 0.n みたいな値になりそう。
- py は、床までの距離。これもテキトーに決めるので、値が分かってる。おそらくこれも、値が 0.n ぐらいになりそう。
- pz は、参照する画像の、v値。これが分からない。
(sz, sy) が作る三角形と (pz, py) が作る三角形は相似なので、以下が成り立つ。
sz : sy = pz : py sz * py = sy * pz pz = (sz * py) / syこれで、参照する画像の v 値は得られるので、HLSL内の input.y に代入してやればいい。はず。
さらに、真上から見た状態を考える。
これも、値が分かってるものと、分かってないものを、分類してみる。
- sx は、画面上の座標。HLSL内の input.x から得られる。これは値が分かってる。
- sz は、画面までの距離。これはテキトーに決めるので、値が分かってる。
- pz は、参照する画像の v値。これはさっき求めたから、値が分かってる。
- px は、参照する画像の u値。これが分からない。コイツを求めたい。
sz : sx = pz : px sz * px = sx * pz px = sx * pz / sz
で、注意点が。
sx は input.x から求まるのだけど、input.x の範囲は、画面左から右に向かって 0.0〜1.0 なので、真ん中は 0.5。なので、sx = input.x - 0.5 として、ずらしてやらないといけない。
sy は…。今回は、床を表示したいなーと思ってるので、画面の下半分だけ考えればいいよなと。だったら、sy = input.y でいいかなと。画面の上半分も含めて何かしたいなら、sy = input.y - 0.5 になるのだろうけど。
そんな感じで、HLSL内では、以下のようなソレを書けば良さそう。
sz = 画面までの距離 py = 床までの距離 input.y = sz * py / input.y input.x = (input.x - 0.5) * input.y / szと思ったけど、試してみたら上下が逆になった。やっぱり v値は、上が1.0なのかもしれない…? そんなわけで、ちょっと修正。
input.y = sz * py / -input.y input.x = (input.x - 0.5) * -input.y / sz見た目はそれらしくなったから、これでいいか…。
◎ 天井はどうしてるかというと。 :
天井は、床を上下反転して描画してるだけです。Window.drawEx() で描画する際に、:scaley => -n を渡してるだけ。
[ ツッコむ ]
以上です。