2014/06/24(火) [n年前の日記]
#1 [dxruby] DXRubyで円柱っぽいBG描画をしてみたり
円柱と言うか、パイプと言うか、スペースコロニー内部と言うか、そういう感じのBG描画を DXRuby の Shader を使って試してみたり。
こんな感じになりました。
ソースは以下。
_pipebg.rb
使用画像も置いときます。画像サイズは 960x480ドット。
_bg_960x480.png
他のスクリプトからも呼び出せるようにしておきました。以下は使用例。
_pipebg_test_main.rb
使用画像。
_bg_chip.png
_bg_star.png
以下のような感じの画面になります。
ソースや画像のライセンスは、Public Domain / CC0 ってことで。
一応、マップデータ(Tiled形式。.tmx) その他も含めて、一式置いときます。
_pipe_bg_20140624.zip (120KB)
こんな感じになりました。
ソースは以下。
_pipebg.rb
require 'dxruby' # # パイプ内のような背景描画を行う # class PipeBg @@hlsl = <<EOS float sz; float r; float angle_max; float start_x; float start_y; texture tex0; sampler Samp = sampler_state { Texture =<tex0>; MinFilter = LINEAR; MagFilter = LINEAR; MipFilter = LINEAR; // MinFilter = POINT; // MagFilter = POINT; // MipFilter = POINT; AddressU = BORDER; AddressV = WRAP; }; float4 PS(float2 input : TEXCOORD0) : COLOR0 { input.x = (input.x - 0.5) * (r * cos(atan2(input.y - 0.5, sz))) / sz + start_x + 0.5; input.y = (atan2(input.y - 0.5, sz) / angle_max) + start_y; return tex2D( Samp, input ); } technique { pass { PixelShader = compile ps_2_0 PS(); } } EOS attr_accessor :sz attr_accessor :r attr_accessor :angle_max attr_accessor :start_x attr_accessor :start_y attr_accessor :shader # # コンストラクタ # # @param [Number] sz スクリーンまでの距離 # @param [Number] r 壁までの距離 # @param [Number] start_x 初期位置 x # @param [Number] start_y 初期位置 y # def initialize(sz = 160, r = 280, start_x = 0.0, start_y = 0.0) self.sz = sz self.r = r self.start_x = start_x self.start_y = start_y core = Shader::Core.new(@@hlsl,{ :sz=>:float, :r=>:float, :angle_max=>:float, :start_x=>:float, :start_y=>:float }) self.shader = Shader.new(core) self.shader.sz = self.sz.to_f / (Window.height / 2) self.shader.r = self.r.to_f / (Window.width / 2) self.shader.start_x = self.start_x self.shader.start_y = self.start_y self.angle_max = Math.atan2(Window.height / 2, sz) # self.angle_max = 90.0 * Math::PI / 180.0 self.shader.angle_max = self.angle_max end # # 描画開始位置を更新 # # @param [Number] dx テクスチャ取得位置 x 増分(1.0より小さい値が望ましい) # @param [Number] dy テクスチャ取得位置 y 増分(1.0より小さい値が望ましい) # def update(dx, dy) self.start_x += dx self.start_y += dy self.shader.start_x = self.start_x self.shader.start_y = self.start_y end # # 描画 # # @param [Number] x 描画位置 x # @param [Number] y 描画位置 y # def draw(x, y, img) Window.drawEx(x, y, img, :shader => self.shader) end end if $0 == __FILE__ # ---------------------------------------- # 使用例 font = Font.new(14) img = Image.load("bg_960x480.png") bg = PipeBg.new(200, 360, 0.0, 0.0) Window.loop do break if Input.keyPush?(K_ESCAPE) bg.update(0.0, -0.005) bg.draw(-Window.width / 4, 0, img) s = "#{Window.real_fps.to_i} FPS CPU: #{Window.getLoad.to_i} % x = #{bg.start_x.to_i}" Window.drawFont(4, 4, s, font) end end
使用画像も置いときます。画像サイズは 960x480ドット。
_bg_960x480.png
他のスクリプトからも呼び出せるようにしておきました。以下は使用例。
_pipebg_test_main.rb
# # pipebg.rb の呼び出しテスト # require 'dxruby' require_relative 'pipebg' # 背景マップデータ mapdata = [ [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 2, 1, 2, 2, 2, 1, 1, 2, nil, nil, 2, 1, 2, 2, 2, 1, 1, 2, nil, nil, nil, 2, 1, 1, 1, 1, 1, 1, 1, 1, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 2, nil, nil, 2, 1, nil, 2, 1, 2, nil, 1, 2, nil, nil, 2, 1, 2, nil, nil, 2, 1, 2, nil, nil, 2, 1, 2, nil, nil, nil, 2, 1, 3, 3, 3, 3, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 2, nil, nil, 2, 1, nil, nil, 2, nil, nil, 1, 2, nil, nil, 2, 1, 2, nil, nil, 2, 1, 2, nil, 2, 1, 1, 1, 2, nil, 2, 1, 1, 3, 3, 3, 3, 3, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 2, nil, nil, 2, 1, nil, nil, 2, nil, nil, 1, 2, 2, 2, 1, 1, 2, nil, nil, 2, 1, 2, nil, nil, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 2, nil, nil, 2, 1, nil, 2, 1, 2, nil, 1, 2, nil, 2, 1, 1, 2, nil, nil, 2, 1, 2, nil, nil, 2, 1, 1, 1, 2, 1, nil, nil, nil, nil, 1, 3, 3, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 2, 1, 2, nil, nil, 2, 1, 1, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2, 1, nil, nil, nil, 3, 3, 3, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, nil] ] font = Font.new(14) bg = PipeBg.new(160, 280) imgs = Image.loadTiles("bg_chip.png", 4, 1, true) star_bg = Image.load("bg_star.png") rt = RenderTarget.new(64 * 15, 64 * 8) bg_x, bg_y = 0, 0 Window.loop do break if Input.keyPush?(K_ESCAPE) # 星BG描画 Window.drawTile(0, 0, [[0, 0]], [star_bg], bg_x / 3, 0, 2, 1) # ReanderTargetにタイルBGを描画 rt.drawTile(0, 0, mapdata, imgs, bg_x, bg_y, 15, 8).update # RenderTarget を円柱のように描画 bg.draw(-Window.width / 4, -32, rt) bg_x += 3 bg_y += 2 s = "#{Window.real_fps.to_i} FPS CPU: #{Window.getLoad.to_i} %" Window.drawFont(4, 4, s, font) end
使用画像。
_bg_chip.png
_bg_star.png
以下のような感じの画面になります。
ソースや画像のライセンスは、Public Domain / CC0 ってことで。
一応、マップデータ(Tiled形式。.tmx) その他も含めて、一式置いときます。
_pipe_bg_20140624.zip (120KB)
◎ 処理内容について。 :
どうやって実現したのか自分自身が忘れそうなので、考え方をメモ。
Shader (HLSL) で、Input.x と Input.y の値を変化させてやれば、こういう円柱っぽい表示ができるわけですが…。
とりあえず、横から見た図で考えてみるとして。
点B、原点、点A でできる角度 a は、アークタンジェントを使えば求められます。この場合は、atan2(sy, sz) で得られるはず。
この、角度 a を、テクスチャを参照する際のuv値 ―― Input.y を変化させる値としてそのまま利用すれば、y方向に関しては円柱っぽい見た目になりそうだなと。Input.y は、0,0〜1.0 の値を取るので、角度 a も 0,0〜1.0 に収まるように変換してやればいいわけで。
さらに、角度 a が分かれば、点Aを通る直線と円の交点、のz値も、r * cos(a) で求められます。z値が得られたら、各ラスターを横方向にどれだけ拡大縮小すればいいのか計算可能になるわけで。
今度は上から見た図で考えるとして。
注意点としては…。Input.x も Input.y も、0.0〜1.0 として得られるので、0.5 を引くことで、画面の中心が 0.0 になるようにしてから計算に使ってます。
また、Shader は、与えられた画像 or RenderTarget を円柱っぽく描画する処理しかしていません。スクロール処理は…。
Shader (HLSL) で、Input.x と Input.y の値を変化させてやれば、こういう円柱っぽい表示ができるわけですが…。
とりあえず、横から見た図で考えてみるとして。
- 原点 (0,0) が、視点。
- スクリーンまでの距離が、sz。テキトーに決めておく。
- 青い線で描かれた円が、壁に相当するところ。半径 r の円。
点B、原点、点A でできる角度 a は、アークタンジェントを使えば求められます。この場合は、atan2(sy, sz) で得られるはず。
この、角度 a を、テクスチャを参照する際のuv値 ―― Input.y を変化させる値としてそのまま利用すれば、y方向に関しては円柱っぽい見た目になりそうだなと。Input.y は、0,0〜1.0 の値を取るので、角度 a も 0,0〜1.0 に収まるように変換してやればいいわけで。
さらに、角度 a が分かれば、点Aを通る直線と円の交点、のz値も、r * cos(a) で求められます。z値が得られたら、各ラスターを横方向にどれだけ拡大縮小すればいいのか計算可能になるわけで。
今度は上から見た図で考えるとして。
- z値は ―― 図で言うところの pz 値は、r * cos(a) で得られた。
- sz はテキトーに決めてある。
- sx も HLSL内では Input.x として得られると分かってる。
sx : sz = px : pz sz * px = sx * pz px = sx * pz / szpx が求まったら、それを Input.x に反映してやれば、拡大縮小ができるわけで。
注意点としては…。Input.x も Input.y も、0.0〜1.0 として得られるので、0.5 を引くことで、画面の中心が 0.0 になるようにしてから計算に使ってます。
また、Shader は、与えられた画像 or RenderTarget を円柱っぽく描画する処理しかしていません。スクロール処理は…。
- テクスチャuv値をちょっと弄る。
- もしくは、RenderTarget にスクロールした状態の画を描画しておいて、その RenderTarget を Shader に渡す。
◎ 課題。 :
y値に応じてラスター単位で明るさを変えたら、もうちょっと見た目がリアルっぽくなるのかもしれず。
◎ 実装してみたものの。 :
この手の処理はどうやって実現していたのか、個人的に気になっていたので、今回一応試してみたものの。例えば Unityあたりを使うなら、円柱っぽいモデルを配置・回転させて、それで終わりだろうなと予想するわけで。
3D描画が当たり前の現代においては、こういう処理ができたからと言って、何に使えるのか、どこで使えるのかと言う問題があるなと…。
でもまあ、プログラマーにとっての頭の体操、プログラマー向けのパズル、としてならアリかなと…。
3D描画が当たり前の現代においては、こういう処理ができたからと言って、何に使えるのか、どこで使えるのかと言う問題があるなと…。
でもまあ、プログラマーにとっての頭の体操、プログラマー向けのパズル、としてならアリかなと…。
[ ツッコむ ]
以上です。