sdl2rでゲームライブラリを作る(8)

ま、とりあえず最低限の入力はできてるので次、RenderTargetクラスを作ろう。SDL2に興味を持った理由は大きいのがGPUを使った高速描画だったのだが、RenderTargetテクスチャが扱えることも大きな要因であった。

RenderTargetテクスチャ

SDL2ではSDL_WindowオブジェクトにSDL_Rendererオブジェクトを割り当てることができるが、SDL_Rendererのデフォルト描画先はSDL_Windowのフレームバッファである。この描画先設定を変更してSDL_Textureに描画できるようにする機能がある。
SDL_Textureを描画先に設定するには、SDL_Textureの生成時にSDL_TEXTUREACCESS_TARGETフラグを指定しておく必要がある。SDL_TEXTUREACCESS_TARGETを指定するとそのテクスチャはロックができなくなるが、代わりにRendererで描画することができるようになる。DXRubyのRenderTargetクラスもDirectXの同様の機能を使っている。
ロックできないのでImageクラスのようにピクセルを操作することはできないが、保持しているのはテクスチャなので、Imageと同じように画面や他のRenderTargetに描画することができる。

RenderTargetクラス

このように作る。

module DXRuby
  class RenderTarget
    attr_accessor :_texture

    def initialize(w, h, bgcolor=[0, 0, 0, 0])
      @_reservation = []
      @_bgcolor = DXRuby._convert_color_dxruby_to_sdl(bgcolor)
      return if w == 0 and h == 0
      @_texture = SDL.create_texture(Window._renderer, SDL::PIXELFORMAT_RGBA8888, SDL::TEXTUREACCESS_TARGET, w, h)
    end

    def draw_box_fill(x1, y1, x2, y2, color, z=0)
      tmp = DXRuby._convert_color_dxruby_to_sdl(color)
      prc = ->{
        SDL.set_render_draw_blend_mode(@_renderer, SDL::BLENDMODE_BLEND)
        SDL.set_render_draw_color(@_renderer, *tmp)
        SDL.render_fill_rect(@_renderer, SDL::Rect.new(x1, y1, x2 - x1 + 1, y2 - y1 + 1))
      }
      @_reservation << [z, prc]
    end

    def draw(x, y, image, z=0)
      prc = ->{
        image._create_texture unless image._texture
        SDL.set_texture_blend_mode(image._texture, SDL::BLENDMODE_BLEND)
        SDL.render_copy(Window._renderer, image._texture, nil, SDL::Rect.new(x, y, image.width, image.height))
      }
      @_reservation << [z, prc]
    end

    def clear
      SDL.set_render_target(Window._renderer, @_texture)
      SDL.set_render_draw_color(Window._renderer, *@_bgcolor)
      SDL.set_render_draw_blend_mode(Window._renderer, SDL::BLENDMODE_NONE)
      SDL.render_fill_rect(Window._renderer, nil)
    end

    def update
      self.clear
      @_reservation.sort_by!{|v|v[0]}.each{|v|v[1].call}
      @_reservation.clear
    end

    def width
      SDL.query_texture(@_texture)[2]
    end

    def height
      SDL.query_texture(@_texture)[3]
    end

    def bgcolor
      DXRuby._convert_color_sdl_to_dxruby(@_bgcolor)
    end

    def bgcolor=(bgcolor)
      @_bgcolor = DXRuby._convert_color_dxruby_to_sdl(bgcolor)
    end
  end
end

基本的にはWindowでやっていた描画とほぼ同じである。ポイントとしてはSDL.set_render_targetを呼んで描画先を切り替えるところ。SDL2のレンダリング関連の機能はsetなんちゃらで色々変更することができるが、それ以降の操作すべてに影響するので、何かするたびに元に戻すか、毎回指定してやる必要がある。まあ、毎回指定しておけばよい。全体に影響する設定を変更しながら描画メソッドを発行していくのはOpenGL的な雰囲気がある。

Windowモジュール

SDL2ではset_render_targetの引数にnilを渡すことで描画先をデフォルト、つまりWindowのフレームバッファに変更することができる。これを利用して、@_textureにnilを設定したRenderTargetオブジェクトをWindow内部で保持し、画面への描画はそっちに任せるようにする。ただし、それを描画元に使うことはできないので、基本的にはユーザから見えないようにするべきだが、そういうRenderTargetオブジェクトを生成するためにnewの引数のwとhを0にできるようにしてしまったので、0指定でユーザが簡単に画面へ描画する用のRenderTargetオブジェクトを作れてしまう。これは正直あまりよろしくないが、まあ、どのようにブロックしたところでRubyで書いている以上、それを防ぐことはできないと思うので、諦めることにする。
Windowモジュールはこんな感じになる。

module DXRuby
  module Window
    def self.draw_box_fill(x1, y1, x2, y2, color, z=0)
      @_render_target.draw(x1, y1, x2, y2, color, z)
    end

    def self.draw(x, y, image, z=0)
      @_render_target.draw(x, y, image, z)
    end

    def self.loop
      timer = FPSTimer.instance
      timer.reset
      SDL.set_window_size(@_window, @_width, @_height)
      SDL.show_window(@_window)
      Kernel.loop do
        timer.wait_frame do
          return if Input.update

          yield

          @_render_target.update
          SDL.render_present(@_renderer)
        end
      end
    end
  end
end

おしまい

ちなみに、Window内部に画面描画用RenderTargetを一つ持つというのはDXRubyも同様の作りになっている。同じハードウェアを使うわけだからDirectXOpenGLもできることは大差ないのだなあー、と思う瞬間である。
今回までのコードはこちら
なんか長くなってきたのでそろそろgithubリポジトリ作るかなーという感じになってきた。でもその前にもうちょい機能を追加しておきたいなあ。