今回は、猫マップを用いて炎のエフェクトを作ります。
普通にやるのはもんしょさんの所などでやられているので、
今回は地面に焦げ後をつけてみました。
上に出ている画像は、左から、
1. | 猫マップで作られた火種。 |
2. | 環境マップとして3Dに張った燃えている部分のテクスチャ。 |
3. | 燃えている部分。 |
4. | 16箱サンプリングでぼかして作ったこげ後マップ。 |
です。
右ドラッグでカメラを回せたり、ホイールでズームが変えられます。
ソースと実行ファイルは、次のものです。
まぁ、いつものように適当にファイルが入っています。
BurnV.cg | 炎を作る。 |
BurnP.cg | 炎を作る。 |
BlackV.cg | 焼け跡の黒い部分を作成する。 |
vs.cg | 地面の描画。 |
ps.cg | 地面の描画。 |
draw.cpp | メインの描画部分。 |
draw.h | 描画の各関数の定義。 |
bg.cpp | 空の描画。 |
load.cpp | メッシュのロード。 |
load.h | ロードのインターフェイス。 |
main.h | 基本的な定数など。 |
main.cpp | 描画に関係しないシステム的な部分。 |
あと、実行ファイル及び、プロジェクトファイルが入っています。
さて、今回のレンダリングですが、1フレームに6回のレンダリングをしています。
大雑把に説明しましょう。
最初に火種を猫マップで作成します。
次に、燃える場所を描きこんだマップ(「t-pot」ってかいてあるやつね)を環境マップのように3次元のモデルに張り込みます。 その張り込んだテクスチャと火種を乗算合成することによって、今回の燃え具合を決定します。
次のプロセスは、上に昇る炎を作ることです。今作った燃え具合のテクスチャと、前のフレームの炎の画像を加算合成します。 ただし、前のフレームの画像は、少し上にずらして色の強さも少々減算します。前のフレームの画像が残りつつ上に上がる効果で、 あたかも炎が燃え上がる画像が出来ます。
今できた炎は少し置いておいて、次に炎の周りのこげ後を作ります。
こげ跡は、燃える場所を描きこんだマップを16箱サンプリングして、ぼかした画像を使います。
このレンダリングは、毎回やることは無いのですが、炎が動的に変化するときを考えて、毎回行いました。
次に、今できたこげ跡のテクスチャを通常のレンダリングの結果に減算合成してこげ跡を作ります。 完全に黒くするより、少し色が残っていたほうが綺麗かなぁと思って、薄く減算しています。
最後に合成です。こげ跡のついた地形と炎のテクスチャを2次元的に加算すれば、焼け焦げた景色の出来上がりです。
一つ目のレンダリングが猫マップです。火種のテクスチャーを並び替えて、動きのある火種を作ります。
レンダリングするサーフェスpCatTextureSurface[id]を2枚用意して、
レンダリング先と元になるテクスチャを入れ替えながらレンダリングします。
draw.c 0279: //----------------------------------------------------------------------------- 0280: // CatMap 0281: //----------------------------------------------------------------------------- 0282: { 0283: lpD3DDev->SetRenderTarget(pCatTextureSurface[id], NULL); 0284: 0285: lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_SELECTARG1); 0286: lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0287: lpD3DDev->SetTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_POINT ); 0288: lpD3DDev->SetTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_POINT ); 0289: float size = 128.0f; 0290: TLVERTEX Vertex[] = { 0291: // x y z rhw tu tv 0292: {0.0f*size, 0.0f*size,0, 1, 0.0f, 0.0f,}, 0293: {0.5f*size, 0.0f*size,0, 1, 1.0f, 0.5f,}, 0294: {0.0f*size, 1.0f*size,0, 1, 1.0f, 1.0f,}, 0295: 0296: {0.5f*size, 0.0f*size,0, 1, 0.0f, 0.5f,}, 0297: {1.0f*size, 0.0f*size,0, 1, 1.0f, 1.0f,}, 0298: {0.0f*size, 1.0f*size,0, 1, 0.0f, 1.0f,}, 0299: 0300: {0.0f*size, 1.0f*size,0, 1, 0.0f, 0.0f,}, 0301: {1.0f*size, 0.0f*size,0, 1, 1.0f, 0.0f,}, 0302: {0.5f*size, 1.0f*size,0, 1, 1.0f, 0.5f,}, 0303: 0304: {0.5f*size, 1.0f*size,0, 1, 0.0f, 0.5f,}, 0305: {1.0f*size, 0.0f*size,0, 1, 0.0f, 0.0f,}, 0306: {1.0f*size, 1.0f*size,0, 1, 1.0f, 1.0f,}, 0307: }; 0308: 0309: if(init){ 0310: lpD3DDev->SetTexture( 0, pCatSeedTexture); // 初期化 0311: }else{ 0312: lpD3DDev->SetTexture( 0, pCatTexture[1-id] ); // それ以外 0313: } 0314: lpD3DDev->SetVertexShader( FVF_TLVERTEX ); 0315: lpD3DDev->SetPixelShader(0); 0316: lpD3DDev->DrawPrimitiveUP( D3DPT_TRIANGLELIST, 4, Vertex, sizeof( TLVERTEX ) ); 0317: }
最初のフレームだけ元絵を使って、後のフレームでは、前の結果のテクスチャを使用します。
次に3次元的に燃えている場所を描いたテクスチャをレンダリングします。
実は地面として描いているテクスチャと燃える場所のテクスチャーのUV値は一致するように画像を描きましたので、
テクスチャ座標をそのまま出力すれば、レンダリングされます。
draw.c 0372: lpD3DDev->SetRenderTarget(pEnvTextureSurface, lpZbuffer ); 0373: lpD3DDev->Clear(0,NULL,D3DCLEAR_ZBUFFER, 0,1.0f,0); 0374: pVertexProgramContainer->SetShaderActive(); 0375: pPixelProgramContainer->SetShaderActive(); 0376: DrawModel(lpD3DDev, mat, 1);
モデルの描画も特に変わったことはありません。 しいていえば、モデルの描画はテクスチャを変えて使いまわすので、 今回は、テクスチャ0に燃える場所のレジスタ、テスクチャ1はなし(NULL)を指定することぐらいです。
draw.c 0234: //----------------------------------------------------------------------------- 0235: // Name: DrawModel 0236: // Desc: モデルの描画 0237: // type 0 普通のデーカルのレンダリング 0238: // type 1 焦げ跡のテクスチャをマッピングする 0239: //----------------------------------------------------------------------------- 0240: VOID DrawModel(LPDIRECT3DDEVICE8 lpD3DDev, D3DXMATRIX &mVP, DWORD type) 0241: { 0242: DrawBg(lpD3DDev, mVP, 0==type); 0243: 0244: D3DXMATRIX m; 0245: D3DXMatrixTranspose( &m, &mVP ); 0246: pVertexProgramContainer->SetShaderConstant( vertex_mat_iter, &m ); 0247: 0248: lpD3DDev->SetStreamSource(0, mesh.pVB, sizeof(D3D_CUSTOMVERTEX)); 0249: lpD3DDev->SetIndices(mesh.pIndex,0); 0250: 0251: switch(type){ 0252: case 1: 0253: pPixelProgramContainer->SetTexture(tex0_iter, pBurnTexture); 0254: pPixelProgramContainer->SetTexture(tex1_iter, NULL); 0255: break; 0256: default: 0257: pPixelProgramContainer->SetTexture(tex0_iter, mesh.pTextures[0]); 0258: pPixelProgramContainer->SetTexture(tex1_iter, pBlackTexture); 0259: break; 0260: } 0261: 0262: lpD3DDev->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0263: mesh.pSubsetTable[0].VertexStart, 0264: mesh.pSubsetTable[0].VertexCount, 0265: mesh.pSubsetTable[0].FaceStart * 3, 0266: mesh.pSubsetTable[0].FaceCount); 0267: }
頂点シェーダ、ピクセルシェーダは簡単なものです。
座標変換は普通のワールドから射影空間への変換で、テクスチャ座標はそのままコピーします。
vs.cg 0005: // ---------------------------------------------------------------------------- 0006: // 入力データ 0007: // ---------------------------------------------------------------------------- 0008: struct appdata { 0009: float4 position : POSITION; 0010: float4 texcoord0 : TEXCOORD0; 0011: }; 0012: // ---------------------------------------------------------------------------- 0013: // 頂点シェーダプログラム 0014: // ---------------------------------------------------------------------------- 0015: vf20 main(appdata I 0016: , uniform float4x4 worldviewproj_matrix 0017: ) { 0018: vf20 O; 0019: 0020: // よくある計算 0021: O.HPOS = mul(worldviewproj_matrix, I.position); 0022: 0023: // テクスチャ座標 0024: O.TEX0 = I.texcoord0; 0025: O.TEX1 = I.texcoord0; 0026: 0027: return O; 0028: }
ピクセルシェーダは、テクスチャ0の画像に対して、テクスチャ1の画像を薄く減算しますが、 今回はテクスチャ1の内容が無いので、テクスチャ1の値がそのまま出力されます。
ps.cg 0001: // ---------------------------------------------------------------------------- 0002: // 入力データ 0003: // ---------------------------------------------------------------------------- 0004: struct myVertexOut { 0005: float4 lightVec : COLOR0; 0006: float4 texCoord0 : TEXCOORD0; 0007: float4 texCoord1 : TEXCOORD1; 0008: }; 0009: 0010: // ---------------------------------------------------------------------------- 0011: // 出力データ 0012: // ---------------------------------------------------------------------------- 0013: struct myFragment { 0014: float4 col : COLOR; 0015: }; 0016: 0017: // ---------------------------------------------------------------------------- 0018: // ピクセルシェーダプログラム 0019: // ---------------------------------------------------------------------------- 0020: myFragment main(myVertexOut I 0021: , uniform sampler2D tex0 0022: , uniform sampler2D tex1 0023: ) { 0024: myFragment O; 0025: 0026: // 2つめのテクスチャーを薄くして減算する 0027: O.col = tex2D(tex0)-0.3f*tex2D(tex1); 0028: 0029: return O; 0030: }
ピクセルシェーダは、テクスチャ0の画像に対して、テクスチャ1の画像を薄く減算しますが、 今回はテクスチャ1の内容が無いので、テクスチャ1の値がそのまま出力されます。
実は、この段階では、まだ猫マップの結果は合成しません。
次の段階で合成します。
では、炎のテクスチャを完成させます。
この段階では、テクスチャを3枚使います。
1つは前の段階の3次元的な燃えている場所を示したテクスチャ、
2つめは、猫マップの結果、もう一つが前のフレームの炎のテクスチャーです。
このレンダリングターゲットも2枚用意して、1フレームごとにテクスチャと役割を切り替えて使います。
draw.c 0378: //----------------------------------------------------------------------------- 0379: // 火の効果の作成 0380: //----------------------------------------------------------------------------- 0381: lpD3DDev->SetRenderTarget(pFireTextureSurface[id], NULL); 0382: lpD3DDev->Clear(0,NULL,D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0),1.0f,0); 0383: 0384: if(!init){ 0385: lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_SELECTARG1); 0386: lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0387: lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP, D3DTOP_SELECTARG1); 0388: lpD3DDev->SetTextureStageState(1,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0389: lpD3DDev->SetTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_POINT ); 0390: lpD3DDev->SetTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_POINT ); 0391: lpD3DDev->SetTextureStageState(1, D3DTSS_MAGFILTER, D3DTEXF_POINT ); 0392: lpD3DDev->SetTextureStageState(1, D3DTSS_MINFILTER, D3DTEXF_POINT ); 0393: lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); 0394: lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); 0395: lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); 0396: lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); 0397: TLVERTEX Vertex[] = { 0398: // x y z rhw tu tv 0399: { -1, 1, 0.1f, 1.f, 0+0.5f/512.0f, 1+0.5f/512.0f,}, 0400: { 1, 1, 0.1f, 1.f, 1+0.5f/512.0f, 1+0.5f/512.0f,}, 0401: { 1, -1, 0.1f, 1.f, 1+0.5f/512.0f, 0+0.5f/512.0f,}, 0402: { -1, -1, 0.1f, 1.f, 0+0.5f/512.0f, 0+0.5f/512.0f,}, 0403: }; 0404: 0405: pBurnVC->SetShaderActive(); 0406: pBurnPC->SetShaderActive(); 0407: pBurnPC->SetTexture(BurnTex0_iter, pFireTexture[1-id]); 0408: pBurnPC->SetTexture(BurnTex1_iter, pEnvTexture); 0409: pBurnPC->SetTexture(BurnTex2_iter, pCatTexture[id]); 0410: lpD3DDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex, sizeof( TLVERTEX ) ); 0411: }
頂点シェーダは2次元的な合成なので、座標変換はせずにそのまま座標をコピー、
テクスチャ座標は前のフレームの画像を少し上にあげて出力します。上げ具合は適当です。
なお、作ってみて、上下が入れ替わっていたものがあったので、ここで修正しました。
BurnV.cg 0005: // ---------------------------------------------------------------------------- 0006: // 入力データ 0007: // ---------------------------------------------------------------------------- 0008: struct appdata { 0009: float4 position : POSITION; 0010: float4 texcoord0 : TEXCOORD0; 0011: }; 0012: // ---------------------------------------------------------------------------- 0013: // 頂点シェーダプログラムm 0014: // ---------------------------------------------------------------------------- 0015: vf20 main(appdata I 0016: , uniform float4x4 worldviewproj_matrix 0017: ) { 0018: vf20 O; 0019: 0021: O.HPOS = I.position; 0022: 0023: // 1フレ前の画像 0024: O.TEX0 = I.texcoord0; 0025: O.TEX0.y =1-I.texcoord0.y+4.0f/512.0f; 0026: // 燃えている部分のマップ 0027: O.TEX1 = I.texcoord0; 0028: O.TEX1.y = 1-I.texcoord0.y; 0029: // 猫マップ 0030: O.TEX2 = I.texcoord0; 0031: 0032: return O; 0033: }
ピクセルシェーダはそれぞれのテクスチャーを組み合わせます。
先ず、前のフレームの画像は、読み込んだ直後に減算します。
猫マップと燃えあとは、乗算して模様付けした後に前のフレームの画像と合成します。
BurnP.cg 0005: fragout main(vf20 I 0006: , uniform sampler2D tex0 0007: , uniform sampler2D tex1 0008: , uniform sampler2D tex2 0009: ) { 0010: fragout O; 0011: 0012: float4 LastResult = uclamp(tex2D(tex0) 0013: - float4(0.15, 0.15, 0.15, 0.0f)); 0014: float4 NewPoint = tex2D(tex1); 0015: float4 Fire = tex2D(tex2); 0016: 0017: O.col = Fire * NewPoint + LastResult; 0018: 0019: return O; 0020: }
16箱サンプリングでぼかした焦げ跡のテクスチャを作ります。
テクスチャステージステートの設定を万万やっていますが、4つ設定しているだけで、特にすごい設定をしている訳ではありません。
基本的には4枚のテクスチャを張るだけです。
draw.c 0319: //----------------------------------------------------------------------------- 0320: // 焦げ跡のテクスチャの生成 0321: //----------------------------------------------------------------------------- 0322: lpD3DDev->SetRenderTarget(pBlackTextureSurface, NULL ); 0323: lpD3DDev->Clear(0,NULL,D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0),1.0f,0); 0324: lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_SELECTARG1); 0325: lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP, D3DTOP_ADD); 0326: lpD3DDev->SetTextureStageState(2,D3DTSS_COLOROP, D3DTOP_ADD); 0327: lpD3DDev->SetTextureStageState(3,D3DTSS_COLOROP, D3DTOP_ADD); 0328: lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0329: lpD3DDev->SetTextureStageState(1,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0330: lpD3DDev->SetTextureStageState(2,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0331: lpD3DDev->SetTextureStageState(3,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0332: lpD3DDev->SetTextureStageState(1,D3DTSS_COLORARG2, D3DTA_CURRENT); 0333: lpD3DDev->SetTextureStageState(2,D3DTSS_COLORARG2, D3DTA_CURRENT); 0334: lpD3DDev->SetTextureStageState(3,D3DTSS_COLORARG2, D3DTA_CURRENT); 0335: lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); 0336: lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); 0337: lpD3DDev->SetTextureStageState(2,D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); 0338: lpD3DDev->SetTextureStageState(3,D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); 0339: lpD3DDev->SetTextureStageState(0,D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); 0340: lpD3DDev->SetTextureStageState(1,D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); 0341: lpD3DDev->SetTextureStageState(2,D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); 0342: lpD3DDev->SetTextureStageState(3,D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); 0343: 0344: TLVERTEX BlackVertex[] = { 0345: // x y z rhw tu tv 0346: { -1, 1, 0.1f, 1.f, 0, 0,}, 0347: { 1, 1, 0.1f, 1.f, 1, 0,}, 0348: { 1, -1, 0.1f, 1.f, 1, 1,}, 0349: { -1, -1, 0.1f, 1.f, 0, 1,}, 0350: }; 0351: 0352: pBlackVC->SetShaderActive(); 0353: lpD3DDev->SetTexture(0, pBurnTexture); 0354: lpD3DDev->SetTexture(1, pBurnTexture); 0355: lpD3DDev->SetTexture(2, pBurnTexture); 0356: lpD3DDev->SetTexture(3, pBurnTexture); 0357: lpD3DDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, BlackVertex, sizeof( TLVERTEX ) ); 0358: lpD3DDev->SetTexture(0, NULL); 0359: lpD3DDev->SetTexture(1, NULL); 0360: lpD3DDev->SetTexture(2, NULL); 0361: lpD3DDev->SetTexture(3, NULL);
頂点シェーダで縦横ずらして表示します。
BlackV.cg 0005: // ---------------------------------------------------------------------------- 0006: // Input datas 0007: // ---------------------------------------------------------------------------- 0008: struct appdata { 0009: float4 position : POSITION; 0010: float4 texcoord0 : TEXCOORD0; 0011: }; 0012: // ---------------------------------------------------------------------------- 0013: // Vertex shader program 0014: // ---------------------------------------------------------------------------- 0015: vf20 main(appdata I 0016: , uniform float4x4 worldviewproj_matrix 0017: ) { 0018: vf20 O; 0019: 0020: float4 ofset0 = {-1.0f/512.0f, -1.0f/512.0f, 0.0f, 0.0f}; 0021: float4 ofset1 = {+1.0f/512.0f, -1.0f/512.0f, 0.0f, 0.0f}; 0022: float4 ofset2 = {-1.0f/512.0f, +1.0f/512.0f, 0.0f, 0.0f}; 0023: float4 ofset3 = {+1.0f/512.0f, +1.0f/512.0f, 0.0f, 0.0f}; 0024: 0026: O.HPOS = I.position; 0027: 0028: O.TEX0 = I.texcoord0 + ofset0; 0029: O.TEX1 = I.texcoord0 + ofset1; 0030: O.TEX2 = I.texcoord0 + ofset2; 0031: O.TEX3 = I.texcoord0 + ofset3; 0032: 0033: return O; 0034: }
それぞれ別方向にずらしています。
焦げ跡は、減算してレンダリングするだけです。
3Dの地面のモデルに張ります。燃える場所を3Dに起こしたときと同じシェーダを使います。今回は、減算の項も効くのを忘れてはいけません。
draw.c 0416: lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer ); 0417: lpD3DDev->Clear(0,NULL,D3DCLEAR_ZBUFFER, 0,1.0f,0); 0418: 0419: pVertexProgramContainer->SetShaderActive(); 0420: pPixelProgramContainer->SetShaderActive(); 0421: lpD3DDev->SetTextureStageState(1, D3DTSS_MAGFILTER, D3DTEXF_LINEAR ); 0422: lpD3DDev->SetTextureStageState(1, D3DTSS_MINFILTER, D3DTEXF_LINEAR ); 0423: DrawModel(lpD3DDev, mat, 0);
最後に炎を合成しますが、2次元的に加算で張るだけです。
draw.c 0427: lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_SELECTARG1); 0428: lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE); 0429: lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP, D3DTOP_DISABLE); 0430: lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); 0431: float size = 512.0f; 0432: TLVERTEX Vertex[4] = { 0433: // x y z rhw tu tv 0434: { 0, 0,0, 1, 0, 0,}, 0435: {size, 0,0, 1, 1, 0,}, 0436: {size, size,0, 1, 1, 1,}, 0437: { 0, size,0, 1, 0, 1,}, 0438: }; 0439: lpD3DDev->SetTexture( 0, pFireTexture[id] ); 0440: lpD3DDev->SetVertexShader( FVF_TLVERTEX ); 0441: lpD3DDev->SetPixelShader(0); 0442: lpD3DDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex, sizeof( TLVERTEX ) ); 0443: lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
てなわけで、炎を作りました。
もうこれだけでは目新しくありませんが、マップを用意するだけで燃やせるので、バンバン燃やしていきましょう。