スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

DXライブラリで炎エフェクト

Привет、ペチコです
TDU2でよくロシア人と間違えられるので、前世はロシア人ではないかと思い始めています

ロシア語全くわかりませんけど


SkyrimとかFalloutをやってると炎の表現ってどうやるんだろうって気になりました
パーティクルを使って云々と概要は知っていても実際の手順は皆目見当も付かないので調べてみました

Fallout NVの炎


途中でワイヤフレームに切り替えてみましたがよくわかりません
なのでこのシーンの炎を切り出してみると、こんなカンジでした

Fo_NV_Flame
無理矢理抜き出したのでパースが狂っていますが、大小の平面ポリゴンが奥行きをずらして並んでます
これを踏まえて実際の画面を見てみると、なんとなくやり方がわかってくるような気がします
あくまで気がするだけです

次はテクスチャを覗いてみます
Fo_NV_FlameTexture
上半分がテクスチャ、下半分がアルファチャンネルです
実際は512x256のアルファ付き横長テクスチャになってました
そのまま載せる訳には参りませんのでこの画像は大きくなりません

動画で見てみるとテクスチャアニメーションはしていないので、各ポリゴンに別々の形の炎を割り振っているだけみたいですね


これを真似て作ってみようと思います
テクスチャはさっきのをお借りしました

flame_effect.cpp
#include "DxLib.h"

#define ArrayLength(array) (sizeof(array) / sizeof(array[0]))

// 関数
void Init_Particle( VECTOR position ); // パーティクル初期化
void Set_Particle( void ); // パーティクルセット

// パーティクル構造体
struct P_FLAMES
{
// 座標
VECTOR pos ;
// スケール(0:小 1:中 2:大)
int iscale ;
// スケール
float scale ;
// y軸移動
float ymove ;
// z軸回転
float rotate ;
// 透明度
float alpha ;
//テクスチャアニメーション
int anim ;
} ;
// パーティクル実体
P_FLAMES flames[30];

// 頂点のデータをセットアップ
VERTEX3DSHADER Vertex[ ArrayLength(flames)*6 ];

void Init_Particle( VECTOR position )
{
//パーティクル初期配置
for(int i=0;i<ArrayLength(flames);i++){
//flames[i].iscale = rand()%3;
flames[i].iscale = i%3;
flames[i].scale = (float)(flames[i].iscale+1)*0.1f;
if(flames[i].iscale==0){
flames[i].pos.x = position.x -30.0f+(float)(rand()%61);
}else if(flames[i].iscale==1){
flames[i].pos.x = position.x -25.0f+(float)(rand()%51);
}else{
flames[i].pos.x = position.x -5.0f+(float)(rand()%11);
}
//flames[i].pos.x = position.x -50.0f+(float)(rand()%101);
flames[i].pos.y = position.y;
flames[i].pos.z = ((float)ArrayLength(flames)/2.0f*2.0f)-(float)i*1.0f;
if(flames[i].pos.x<position.x){
flames[i].rotate = 3.14159f / 180.0f * 30.0f;
}else{
flames[i].rotate = 3.14159f / 180.0f * -30.0f;
}
flames[i].ymove = 0.0f;
flames[i].alpha = (float)(rand()%100+1)/100.0f;
flames[i].anim = rand()%8;
}
Set_Particle();
}

void Set_Particle( void )
{
for(int i=0;i<ArrayLength(flames);i++){
int arr = i*6;

// 拡大行列
MATRIX ZoomMtx = MGetScale( VGet( flames[i].scale, flames[i].scale, 1.0f ) ) ;
//Y軸回転行列
MATRIX RotateMtx = MGetRotZ( flames[i].rotate );
//平行移動行列
MATRIX MovemMtx = MGetTranslate( VGet( flames[i].pos.x, flames[i].pos.y+flames[i].ymove, flames[i].pos.z ) ) ;

//回転→平行移動
ZoomMtx = MMult(ZoomMtx,RotateMtx);
ZoomMtx = MMult(ZoomMtx,MovemMtx);

//テクスチャアニメーション
float u1 = (float)(flames[i].anim % 4) * 1.0f / 4.0f;
float v1 = (float)(flames[i].anim % 8 / 4) * 1.0f / 2.0f;
float u2 = u1 + 1.0f / 4.0f;
float v2 = v1 + 1.0f / 2.0f;

//アルファ
int alpha = (int)(flames[i].alpha*255.0f);

// 頂点データをセットアップ
Vertex[ arr ].pos = VGet( -100.0f, 100.0f, 0.0f ) ;
// 変換
Vertex[ arr ].pos = VTransform( Vertex[ arr ].pos, ZoomMtx ) ;
Vertex[ arr ].norm = VGet( 0.0f, 0.0f, -1.0f ) ;
Vertex[ arr ].dif = GetColorU8( 255,255,255,alpha ) ;
Vertex[ arr ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ arr ].u = u1 ;
Vertex[ arr ].v = v1 ;
Vertex[ arr ].su = 0.0f ;
Vertex[ arr ].sv = 0.0f ;

Vertex[ arr+1 ].pos = VGet( 100.0f, 100.0f, 0.0f ) ;
// 変換
Vertex[ arr+1 ].pos = VTransform( Vertex[ arr+1 ].pos, ZoomMtx ) ;
Vertex[ arr+1 ].norm = VGet( 0.0f, 0.0f, -1.0f ) ;
Vertex[ arr+1 ].dif = GetColorU8( 255,255,255,alpha ) ;
Vertex[ arr+1 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ arr+1 ].u = u2;
Vertex[ arr+1 ].v = v1 ;
Vertex[ arr+1 ].su = 0.0f ;
Vertex[ arr+1 ].sv = 0.0f ;

Vertex[ arr+2 ].pos = VGet( -100.0f, -100.0f, 0.0f ) ;
// 変換
Vertex[ arr+2 ].pos = VTransform( Vertex[ arr+2 ].pos, ZoomMtx ) ;
Vertex[ arr+2 ].norm = VGet( 0.0f, 0.0f, -1.0f ) ;
Vertex[ arr+2 ].dif = GetColorU8( 255,255,255,alpha ) ;
Vertex[ arr+2 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ arr+2 ].u = u1 ;
Vertex[ arr+2 ].v = v2 ;
Vertex[ arr+2 ].su = 0.0f ;
Vertex[ arr+2 ].sv = 0.0f ;


Vertex[ arr+3 ].pos = VGet( -100.0f, -100.0f, 0.0f ) ;
// 変換
Vertex[ arr+3 ].pos = VTransform( Vertex[ arr+3 ].pos, ZoomMtx ) ;
Vertex[ arr+3 ].norm = VGet( 0.0f, 0.0f, -1.0f ) ;
Vertex[ arr+3 ].dif = GetColorU8( 255,255,255,alpha ) ;
Vertex[ arr+3 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ arr+3 ].u = u1 ;
Vertex[ arr+3 ].v = v2 ;
Vertex[ arr+3 ].su = 0.0f ;
Vertex[ arr+3 ].sv = 0.0f ;

Vertex[ arr+4 ].pos = VGet( 100.0f, 100.0f, 0.0f ) ;
// 変換
Vertex[ arr+4 ].pos = VTransform( Vertex[ arr+4 ].pos, ZoomMtx ) ;
Vertex[ arr+4 ].norm = VGet( 0.0f, 0.0f, -1.0f ) ;
Vertex[ arr+4 ].dif = GetColorU8( 255,255,255,alpha ) ;
Vertex[ arr+4 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ arr+4 ].u = u2 ;
Vertex[ arr+4 ].v = v1 ;
Vertex[ arr+4 ].su = 0.0f ;
Vertex[ arr+4 ].sv = 0.0f ;

Vertex[ arr+5 ].pos = VGet( 100.0f, -100.0f, 0.0f ) ;
// 変換
Vertex[ arr+5 ].pos = VTransform( Vertex[ arr+5 ].pos, ZoomMtx ) ;
Vertex[ arr+5 ].norm = VGet( 0.0f, 0.0f, -1.0f ) ;
Vertex[ arr+5 ].dif = GetColorU8( 255,255,255,alpha ) ;
Vertex[ arr+5 ].spc = GetColorU8( 0, 0, 0, 0 ) ;
Vertex[ arr+5 ].u = u2 ;
Vertex[ arr+5 ].v = v2 ;
Vertex[ arr+5 ].su = 0.0f ;
Vertex[ arr+5 ].sv = 0.0f ;
}
}

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )
{
//ウインドウモード
ChangeWindowMode( TRUE ) ;

// DXライブラリの初期化
if( DxLib_Init() < 0 ) return -1;

// 画面解像度
SetGraphMode( 1024 , 768 , 32 ) ;

//地面
int modelhandle = MV1LoadModel( "ground.mv1" ) ;

// テクスチャを読み込む
int texhandle = LoadGraph( "fire.dds" ) ;

// 頂点シェーダ
int vshandle = LoadVertexShader( "fireVS.vso" ) ;
// ピクセルシェーダ
int pshandle = LoadPixelShader( "firePS.pso" ) ;

//パーティクルの初期化
Init_Particle( VGet(0.0f,0.0f,0.0f) );

// ESCキーが押されるまでループ
while( ProcessMessage() == 0 && CheckHitKey( KEY_INPUT_ESCAPE ) == 0 )
{

// 描画先を裏画面にする
SetDrawScreen( DX_SCREEN_BACK ) ;
// 画面を初期化
ClearDrawScreen() ;

// カメラ視点
SetCameraPositionAndTarget_UpVecY( VGet( 100.0f, 100.0f, -200.0f ), VGet( 0.0f, 50.0f, 0.0f ) ) ;

// Zバッファを有効にする
SetUseZBufferFlag( TRUE );
// シェーダ使用しない
MV1SetUseOrigShader( FALSE ) ;
// 地面の描画
MV1SetPosition( modelhandle, VGet( 0.0f, 0.0f, 0.0f ) ) ;
MV1DrawModel( modelhandle ) ;

// パーティクル
for(int i=0;i<ArrayLength(flames);i++){
//拡大率
if(flames[i].iscale<2){
flames[i].scale *= 0.99f;
}else{
flames[i].scale *= 1.0f + (float)(flames[i].iscale)*0.003f;
}
//回転
//flames[i].rotate += 3.14159f / 180.0f * 6.0f*(1.0f-(float)flames[i].scale+0.1f);
//y移動
flames[i].ymove += ( 3.0f-(float)(flames[i].iscale) )*1.4f;
//透明度
flames[i].alpha -= ( (float)(3-flames[i].iscale) )*0.014f;
//初期化
if(flames[i].alpha<0.0f){
flames[i].ymove=0.0f;
flames[i].alpha=1.0f;
flames[i].anim = (flames[i].anim+1)%8;
flames[i].scale = (float)(flames[i].iscale+1)*0.1f;
}
}
Set_Particle();

// ビュー行列
MATRIX mtx = GetCameraViewMatrix() ;
mtx.m[0][0]=1.0f;mtx.m[0][1]=0.0f;mtx.m[0][2]=0.0f;
mtx.m[1][0]=0.0f;mtx.m[1][1]=1.0f;mtx.m[1][2]=0.0f;
mtx.m[2][0]=0.0f;mtx.m[2][1]=0.0f;mtx.m[2][2]=1.0f;
//mtx = MInverse( mtx ) ;
mtx = MTranspose( mtx ) ;

// 回転を無効にしたビュー行列をc6にセット
SetVSConstFMtx( 6, mtx ) ;

//シェーダ使用
MV1SetUseOrigShader( TRUE ) ;
// 使用するテクスチャを0番にセット
SetUseTextureToShader( 0, texhandle ) ;

// 使用する頂点シェーダーのセット
SetUseVertexShader( vshandle ) ;

// 使用するピクセルシェーダーをセット
SetUsePixelShader( pshandle ) ;

// アルファブレンド
SetDrawBlendMode( DX_BLENDMODE_ALPHA, 128 ) ;
// シェーダーを使用したポリゴンの描画
DrawPolygon3DToShader( Vertex, ArrayLength(flames)*2 ) ;
// アルファブレンド解除
SetDrawBlendMode( DX_BLENDMODE_NOBLEND, 255 ) ;

// 使用したfloat4型定数10~13番の設定を無効化する
ResetVSConstF( 6, 4 ) ;

// Zバッファを無効にする
SetUseZBufferFlag( FALSE );

// 裏画面の内容を表画面に反映させる
ScreenFlip() ;

}

// 読み込んだモデルの削除
MV1DeleteModel( modelhandle ) ;

// 読み込んだシェーダーの削除
InitShader() ;

// DXライブラリの後始末
DxLib_End();

// ソフトの終了
return 0;
}


正直手探りなのでコードは無茶苦茶です
はい、言い訳です。整理整頓ニガテなので綺麗なコードは書けません
インデントずれは直す気がない仕様です
そういえば今まで<とか>を&lt;&gt;で置き換えてなかったからIEだとちゃんと表示されてなかったかもですね


上から適当に説明

ArrayLength(array) (sizeof(array) / sizeof(array[0]))
配列のサイズを取得するようにしましたけど、別にArray_Num=xxみたいにした方が計算が入らない分よかったですね

struct P_FLAMES
パーティクルに必要そうなものを構造体でまとめましたが、無駄な情報を増やすより時間とかフレーム数を保存するようにして、そこから座標計算とかした方が良かったです

粗ばっかり見えてきます

P_FLAMES flames[30];
一つの炎に幾つパーティクルを使うかです。というかここでは一つの炎しか考えてません
実際は幾つか炎が上がる事があるでしょうし、別に炎の数を管理するものを作ると良いと思います

VERTEX3DSHADER Vertex[xx];
で頂点データを作成してます。xx部分は上のパーティクル数*6です(平面ポリゴンに6つの頂点があるため)
同座標の頂点をまとめる、Index版もありますが面倒だったのでこちらを使ってます

void Init_Particle
パーティクルの初期配置関数。表示したい座標を渡してそこに描画するようにしてます
中身はものすごい手探りなのではしょります
時間を取ってないので初期透明度をランダムにすることで、ずれを表現してます

void Set_Particle
こちらが実際に頂点セット関数です
毎フレーム全頂点をセットするという無理矢理さです
この辺はレジスタや頂点テクスチャを使えばシェーダ内で計算ができるのでもっと速く出来ます
頂点テクスチャ上手く値が取れないのです。どうやるんだろう

ベース頂点はそのまま(-100.0f~100.0f)で拡縮、回転、平行移動を行列にセットして動かしてます
最終的に頂点へVTransformするとそれらの計算結果が反映されます

テクスチャのUVはint型のテクスチャ番号から番号に応じたテクスチャを表示するように設定
使ったのが横長のテクスチャだったので横のみ%4になってます

%については今更説明するようなことでもないのですが一応ご説明
%xはxで割った余りを取得します
例えば%2の場合、2/2=1余り0 3/2=1余り1 4/2=2余り0となり繰り返しの数を表現するのに都合が良いのです
この場合だと横に4つテクスチャが並んでいるので%4でxが幾つになっても0~3の4つの数値を取れるようにしてます

Vertex[ xxx ].dif
の4つめの要素が透明度です
0~255のintなので0.0~1.0のfloat型を255倍するようにしてます
int型のままだとアルファを0.5だけずらしたいとかがやりにくいので構造体のアルファはfloatで持つようにしてます

後はパーティクルの数分ループを回す事で、複数ポリゴンを生成しています


WinMain部分
シェーダで描画しますのでそれ用のあれこれを読み込んで、Init_Particleでパーティクルを原点で初期化してます

while( ProcessMessageのループ部分
最初にカメラの位置を決めたり、Zバッファを有効にしたり、地面を描画したりしてます

// パーティクル
for(int i=0;iこのループで各パーティクルの計算を行ってます
中身はなんとか手を抜こうと適当にやった結果あんまり良くなかったのではしょります
放射状に広がる処理とか入れるとステキかも

MATRIX mtx = GetCameraViewMatrix() ;~
シェーダで描画する前にビュー行列の左上から3x3を単位行列にしてます
こうすることでワールド→ビュー変換の回転が無効になり常にカメラを向くビルボードになります
ただし、ここで全方位ビルボードはダメでした
真上に近い角度から見てもカメラの方を向くので違和感がすごいです
用途に合わせてY軸のみのビルボードにした方がいいと思います

後はアルファブレンドでシェーダ描画
シェーダは何も特別なことはやってません
// flameEffectVS.fx
// 頂点シェーダーの入力
struct VS_INPUT
{
float4 Position : POSITION ; // 座標( VERTEX3DSHADER構造体の pos の値 )
float4 DiffuseColor : COLOR0 ;
float3 Normal : NORMAL0 ; // 法線( VERTEX3DSHADER構造体の norm の値 )
float2 TextureCoord0 : TEXCOORD0 ; // テクスチャ座標0( VERTEX3DSHADER構造体の u, v の値 )
} ;

// 頂点シェーダーの出力
struct VS_OUTPUT
{
float4 ProjectionPosition : POSITION ; // 座標( 射影空間 )
float4 DiffuseColor : COLOR0 ;
float2 TextureCoord0 : TEXCOORD0 ; // テクスチャ座標
} ;


// C++ 側で設定する定数の定義
float4x4 cfViewMatrix : register( c6 ) ; // ワールド座標をビュー座標に変換する行列の転置行列
float4x4 cfProjectionMatrix : register( c2 ) ; // ビュー座標を射影座標に変換する行列の転置行列

// main関数
VS_OUTPUT main( VS_INPUT VSInput )
{
VS_OUTPUT VSOutput ;
float4 lWorldPosition ;
float4 lViewPosition ;

/*
float4x4 bmtx = cfViewMatrix;
bmtx[0][0]=1.0;bmtx[1][0]=0.0;bmtx[2][0]=0.0;
bmtx[0][1]=0.0;bmtx[1][1]=1.0;bmtx[2][1]=0.0;
bmtx[0][2]=0.0;bmtx[1][2]=0.0;bmtx[2][2]=1.0;
*/

// 入力の頂点座標にC++プログラム側で設定した頂点座標を加算する
lWorldPosition = VSInput.Position ;

// 頂点座標をビュー空間の座標に変換する
lViewPosition = mul( lWorldPosition, cfViewMatrix ) ;

// ビュー空間の座標を射影空間の座標に変換する
VSOutput.ProjectionPosition = mul( lViewPosition, cfProjectionMatrix ) ;

// テクスチャ座標はそのまま代入
VSOutput.TextureCoord0 = VSInput.TextureCoord0;

// 頂点カラーはそのまま代入
VSOutput.DiffuseColor = VSInput.DiffuseColor ;

// 関数の戻り値がピクセルシェーダーに渡される
return VSOutput ;
}


// flameEffectPS.fx
// ピクセルシェーダーの入力
struct PS_INPUT
{
float4 DiffuseColor : COLOR0 ;
float2 TextureCoord0 : TEXCOORD0 ; // テクスチャ座標
} ;

// ピクセルシェーダーの出力
struct PS_OUTPUT
{
float4 DrawColor : COLOR0 ; // 描画カラー
} ;


// C++ 側で設定する定数の定義

// 描画するテクスチャ
sampler DiffuseMapTexture : register( s0 ) ;

// main関数
PS_OUTPUT main( PS_INPUT PSInput )
{
PS_OUTPUT PSOutput ;
float4 lTextureColor ;

// テクスチャーの色を取得
lTextureColor = tex2D( DiffuseMapTexture, PSInput.TextureCoord0 ) ;

// 出力する色はテクスチャの色とディフューズカラーを乗算したもの
PSOutput.DrawColor = lTextureColor * PSInput.DiffuseColor;

// 関数の戻り値がラスタライザに渡される
return PSOutput ;
}


頂点シェーダ途中のコメントアウト部分は、シェーダ内でビルボードできる!って考えて書きましたが、なんか意味ないしわかりにくいので止めた名残です

実際のゲームの画面と再現したもので炎の色合いが違ったので、その辺弄ると良いのでしょうがめんどくさくなったので単純出力です


で結果がこちら↓


うん、なんか上手くいきませんでした
この辺はパーティクルの移動部分の調整でどうにでもなるので無視
要はプロはこんなカンジでやってるみたいというのがわかっただけでも収穫なのです

プロの手口(←こう言ったらなんか危ない響きですね)を見るのも面白くて参考になりますね


コメントの投稿

非公開コメント

プロフィール

ペチコートさん

Author:ペチコートさん
大航海時代オンライン
Zephyros、Eurosサーバーに潜伏中

偽ペチコ1号・2号
ペチコの手下達
Zephyrosサーバーに潜伏中

最新記事
月別アーカイブ
カテゴリ
リンク
最新コメント
検索フォーム
QRコード
QR
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。