半透明と半透明の合成の結論

RenderTargetの半透明合成は結局DirectX9のせいでできないということであった。そこでシェーダを使ってムリヤリにでもどうにかならないだろうか、とちょっといじってみた。
まずDXRubyの開発中最新版でWindow.draw_exに:blend=>:noneの指定を追加。透明色も半透明色もそのままピクセルを置く指定だ。Imageにはcopy_rectというメソッドがあるのだが、画面に描く場合にこれをする手段が無かった。
これでどのように描画するのかというと、以下のようにする。描画先のRenderTargetをAとし、描画元の画像をBとする。draw_exしたいという要望だったので対応したやりかたとなっている。

(1) Aと同じサイズの透明なRenderTargetを作る。これをCとする。
(2) もうひとつAと同じサイズのRenderTargetを作る。Dとする。
(3) Cに対してdraw_exでBを描画する。ここで:blend=>:noneする。:blend以外の指定はそのまま使う(noneなのでalphaは無視される)。
(4) シェーダに追加テクスチャとしてCを渡し、D.draw_ex(x, y, A, :shader=>shader, :blend=>:none)とする。このシェーダが理想的な合成をした結果を出力するため、DにAとCの合成結果が描画される。
(5) Dをスクリーンに描画する。

DをまたRenderTargetに描画したいという場合はまた同じことを繰り返す必要がある。あくまでもスクリーン描画は普通なのだ。RenderTargetに描画するときだけ問題になる。
とはいえこんな処理をいちいち書いていては大変なので、AlphaBlendクラスを作ってみた。
とりあえず実行結果はこんな感じ。掲示板にアップいただいた検証コードの改造版。

コードは以下に置いておくが、とりあえずDXRuby1.3.3devをリリースするまで実行するのはちょっと待ってね。

require 'dxruby'

class AlphaBlend
  hlsl = <<EOS
  texture tex0;
  texture tex1;
  float alpha;

  sampler Samp0 = sampler_state
  {Texture =<tex0>;};
  sampler Samp1 = sampler_state
  {Texture =<tex1>;};

  struct PixelIn
  {float2 UV : TEXCOORD0;};
  struct PixelOut
  {float4 Color : COLOR0;};

  PixelOut PS(PixelIn input)
  {
    PixelOut output;
    float4 cd = tex2D( Samp0, input.UV );
    float4 cs = tex2D( Samp1, input.UV );
    cs.a *= alpha;
    output.Color.a = (1.0 - cs.a) * cd.a + cs.a;
    output.Color.rgb = ((cs.rgb * cs.a) + (cd.rgb * cd.a) * (1.0 - cs.a)) / output.Color.a;

    return output;
  }

  technique AlphaBlend
  {pass P0 {PixelShader = compile ps_2_0 PS();}}
EOS

  @@core = Shader::Core.new(
    hlsl,
    {
      :tex1 => :texture,
      :alpha => :float
    }
  )

  def initialize(bg)
    @shader = Shader.new(@@core, "AlphaBlend")
    @result = RenderTarget.new(bg.width, bg.height)
    @rt_image = RenderTarget.new(bg.width, bg.height)
    @bg = bg
    @shader.alpha = 1.0
  end

  def draw_ex(x, y, image, param = {})
    temp = param.dup
    temp[:blend] = :none
    @rt_image.draw_ex(x, y, image, temp)
    @rt_image.update
    @shader.tex1 = @rt_image
    if param[:alpha] == nil
      @shader.alpha = 1.0
    else
      @shader.alpha = param[:alpha] / 255.0
    end
    @result.draw_ex(0, 0, @bg, :shader=>@shader, :blend=>:none)
    @result.update
    return @result
  end
end

image_bg = []
image_bg[0] = Image.load("./sozai/bg_test3.jpg") #背景画像

imaga_0 = Image.new(200, 200, [128, 255,   0,   0]) #半透明レイヤ
imaga_1 = Image.new(200, 200, [255,   0, 255,   0]) #不透明レイヤ

target = RenderTarget.new(800, 600)

map = [[0,0],[0,0]]

Window.resize(800, 600)

target.draw(500, 150, imaga_1,  500)
target.update
ab = AlphaBlend.new(target.to_image) # 引数は背景にしたい絵。
                                     # この例では毎フレーム描くわけじゃないから念のためImageにしとく

#ゲームループ
Window.loop do

  #タイルを敷き詰めて背景を作成
  Window.draw_tile(0, 0, map, image_bg, 0, 0, 4, 3, 0)

  #Windowに直接描画(A)
  Window.draw( 50,  50, imaga_0, 1000)
  Window.draw(150, 150, imaga_1,  500)

  #RenderTargetを経由しての描画(B)←Aと同じ様に描画される事を期待している
  rt = ab.draw_ex(400,  50, imaga_0) # ←合成したRenderTargetが返ってくるのでそれを描画する
  Window.draw(0, 0, rt)

end