シェーダモデル1.1シングルパスで醜い加算合成の飽和をなんとかする


左:十分暗い背景上での単純な加算合成 / 中:青空バックに加算合成白飛びしている / 右:なんとかするピクセルシェーダで描画

動機

レーザービームや爆炎や魔法のエフェクトのブルーム等はお手軽に加算合成されることが多いが、合成の対象とする背景が特定の色を伴っている場合、期待どおりの結果を得ることができない。補色を加算合成すれば真っ白になるし、補色でなくとも想定した色が再現されることはなく背景と混じって違った色になってしまう。例えば赤い炎を加算合成するとして、青空をバックにすれば紫になるし、砂漠をバックにすれば色の変化が少なく迫力不足になってしまう。そのためにゲームではそういったエフェクトが発生するときだけ暗くなったり、そもそもゲーム通して終始暗かったりする。

アニメなどでは暗くするわけにもいかないので結構工夫しているようだけれども、ゲームの方はそんなことは無視で白く飛んでてもそれが計算の結果ですと言い張っていていいのか。

目標

  • 背景が明るい場合はその明るさと重なってくるエフェクトの明るさ分だけ暗くして余裕をもたせた上で明るいエフェクトを重ねることで、部分的な色成分の飽和に伴う色相の変化を抑えたい。
  • テクスチャは黒地に光源とブルームのぼかしを加えたもののみ用いalphaチャンネル等に前処理を施すような作業を求めない。
  • シェーダ定数を外からいじることで全体の色や透明度を調整できる。
  • 出来うる限りの高速化と下位互換。

実装

DirectXSDKのEffectEditで動くサンプル。magiccircle.pngここから落としてDirectX9SDK/Samples/Mediaへつっこめ。

float4x3 WorldView  : WORLDVIEW;
float4x4 Projection : PROJECTION;

string XFile = "cube.x";   // Model to load
string BIMG  = "lake.bmp";  // Background image
DWORD  BCLR = 0xff202020;  // Background color (if no image)

float4 Color = float4(1.0,0.6,0.0,1.0);

texture tex0 <string name = "magiccircle.png";>;
sampler Samp = sampler_state
{
    Texture =  <tex0>;
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;
};

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
    float2 Tex: TEXCOORD0;
};

VS_OUTPUT vs(float3 Pos : POSITION)
{
    VS_OUTPUT o;
    Pos.z = 0;
    o.Pos  = mul(float4(mul(float4(Pos,1), (float4x3)WorldView),1), Projection);      
    o.Tex.x = (Pos.x - 1) * 0.5;
    o.Tex.y = (Pos.y - 1) * -0.5;
    return o;
}

technique tec0
{
    pass p0
    {
        VertexShader = compile vs_1_1 vs();
        PixelShader = asm 
            {
            ps_1_1
            def c1,-0.11,-0.11,-0.11,1.0
            def c2, 0,0,0.75, -0.75
            tex t0
            mov r0, t0
            mul r0,r0,r0
            mad r0,r0,r0,c1 // r0 = t0 * t0 * t0 * t0 - 0.11
            lrp r0, c0, t0, r0 // lerp
            add_sat r1.a, t0.b, c2.b
            add r0.a, r1.a, c2.a
            mul r0, r0, c0.a // apply alpha
            };

        PixelShaderConstant1[0] = <Color>;
        Sampler[0] = (Samp);

        LightEnable[0] = False;
        Lighting = False;
        
        AlphaBlendEnable = true;
        BlendOp = add;
        SrcBlend = one; 
        DestBlend = invsrcalpha;
    }
}

ポイント

  • 色成分ごと4乗し原点方向へ押しつぶした分とそのままの分とを指定された色成分でlerpしてブルームの部分の色のコントロールを可能に。
  • アルファのみを別計算しblend計算の方でDestBlendへ当て背景の明るさを適切に削っている。
  • ps1.1 シングルパス

どんどん使って醜い加算合成の飽和をなくそう!