MiEngine:尝试制作模块化网格特效编辑器
MiEngine:モジュール式のメッシュエフェクトエディタを作ってみる
MiEngineとは?:DirectX11とC++を使用して個人開発している自作ゲームエンジンです。
やりたかったこと
UnityのParticleSystemコンポーネントみたいな雰囲気で、メッシュエフェクトが作りたい。
要は↓のようにモジュールのオンオフとパラメータ調整でエフェクトを表現できるようになりたい。
(なお、ここで言うメッシュエフェクトとは、斬撃や衝撃波のように《メッシュ》×《テクスチャ》で表現するものをイメージしている)
自作エンジンにて、Unityを参考にしたシンプルなParticleSystemエディターは作ることが出来たので、それを参考にしつつメッシュエフェクトを表現するために必要なモジュールを考えていく。

自作のParticleSystemエディター
※なお、本記事はゲームプログラマー目線でゲーム内で簡単にそれっぽいエフェクトを作ることを目的とした内容です。本格的なエフェクトエディタを作ろうとしているわけじゃないので注意。
エディタの構成要素をざっくりと
今回のエディタは、エフェクトを構成する各要素を独立したモジュールとして扱い、そのオンオフやパラメータを変更することで見た目を調整できるように作成しました。
1つの共通シェーダーに処理をまとめ、設定値を渡すことで見た目を切り替えられるように実装。(詳しくは後述)
エディタ画面は、「エフェクトアセットのリスト」「プレビュー」「構成モジュールのパラメータ」 の主に3要素で構成し、編集がしやすいように オートセーブ や マウスによるカメラ回転 などを実装。

また、今回の目標はあくまで「必要な要素を選び、パラメータを調整するだけでそれっぽいエフェクトが作れる」エディタを作ることなので、機能は最小限にした。
(↑を見ると分かると思うが、プレビュー用の一時オブジェクトもシーンにそのまま生成している。サイズ感を見れるので逆に便利だったり)
必須モジュール一覧
ParticleSystemも踏襲しながら、必須となる2つのモジュールをまずは作成。
Main
エフェクトの再生期間、ループ再生するか、自動で最初から再生するか、などなど。

Mainモジュール
Renderer
ParticleSystemと最も差別化したかった要素として、1つのメッシュを中心にエフェクトを構成する思想がまずあったので、メッシュ用のモデルパス、テクスチャパスは導入。
他にはビルボード、ブレンドステート、サンプラーステート等々、表現の幅が広がりそうなものを色々と。

Rendererモジュール
主要なモジュール一覧
Transform
衝撃波や瞬間的に出現する被弾エフェクトなどと相性が良い気がする。
カーブキーを作成し、Durationに沿って基準値から変動するようにしてみた。

Transformモジュール
Flipbook(スプライトアニメーション)
衝撃波や星のきらめきなどなど。色んなエフェクトで使えるモジュール。
エフェクトのDurationに合わせて再生するモードと、速度を指定して再生する2種類のモードがあるとエフェクトの幅が広がる。前者は瞬間的なエフェクト、後者はループ再生するエフェクトに向いている気がする。
ibisPaintで作成したテクスチャをいくつか使ってみたが、動かしてみると割と見れる。

Flipbookモジュール
Transform x Flipbook のエフェクト制作例
ちょっとしたTransformの変動とFlipbookアニメーションで、制作ゲームでは衝撃波エフェクトとマズルフラッシュを作成してみた。

衝撃波とマズルフラッシュエフェクト(衝撃波は円柱、マズルフラッシュは平面メッシュを使用)

衝撃波のテクスチャ
UV Scroll
テクスチャを指定の方向に動かすだけのモジュール。やってることは超単純だが、斬撃などはシンプルな表現ならこれだけでできる。
テクスチャを繰り返し表示したくない場合は、RendererモジュールのサンプラーステートをClampにしておけば良い。

UVScrollモジュール
Gradient
グラデーション色を付けるモジュール。メッシュのUV位置や時間経過によるグラデーションがつけられる。
色彩の変化だけじゃなく、斬撃エフェクトの端のアルファ値を下げるなど、パッと見た時のエフェクトが途切れる違和感を無くす用途にも使える。

Gradientモジュール
UVScroll x Gradient のエフェクト作成例
この2つ以外にもTransformモジュールの回転だったりも少し加えてはいるが、この2つのモジュールでシンプルな斬撃エフェクトを作ることが出来た。
メッシュエフェクトエディタを作りたかった目的の6割くらいはこいつと言っても過言ではないので、それっぽく出来てかなり満足。

斬撃エフェクト

メッシュはこんな形をしている

ibisPaintで地道に描いて作成した斬撃テクスチャ
あると表現の幅が広がるかも?なモジュール一覧
Wave
名の通りUVを揺らすモジュール。水球だったり揺らぎが欲しいエフェクトに使えそう。今回制作したゲームでは、はっきりとしたエフェクトが多かったのであまり使わなかった。ファンタジー系のゲームで魔法陣などを作るときには重宝しそう。

Waveモジュール
Wave のエフェクト作成例
情報量が多すぎて本番では使わなかったビームエフェクト。Waveモジュールを使うと他のエフェクトよりも動きが複雑になる分、シンプルすぎる自作エフェクトの中では少し浮いてしまうのが悲しい。

ビームエフェクト
Fresnel
視線と法線の角度から縁を強調する演出的なFresnel表現ができる。
球形のメッシュと合わせて使うとエネルギー球が作れる。光らせられるのはあくまでメッシュの縁なので、テクスチャで大きく形作っているエフェクトには向かないかもしれない。

Fresnelモジュール
Fresnel のエフェクト作成例
パワーをチャージした際によく見る気がするエフェクト。

エネルギー球エフェクト
全部1シェーダーにまとめてみる
各モジュールは比較的単純な演算で構成されているため、今回はすべてを1つのシェーダーにまとめた。
未使用モジュールの演算も一部残るが、個人制作なのでシェーダーを個別に管理する手間を減らせるメリットを優先。
各モジュールの有効・無効は、使用可否のフラグや、演算結果に影響を与えない初期値を設定することで切り替えている。
(例えばUVスクロールならオフセットを0に、Waveなら振幅を0にすることで、シェーダー内で演算が行われても見た目に影響しないようにした)
ちなみにGradientには、UVスクロールなどの影響を受ける前のUV座標を使用している。これによってテクスチャが移動してもグラデーションの位置は変わらず、斬撃エフェクトなどの端を自然にフェードさせられる。
float4 main(PS_INPUT ps_in) : SV_TARGET
{
const float2 originalUv = ps_in.texcoord;
float2 baseUv = ps_in.texcoord * (1 - g_UseWorldProjection) + float2(ps_in.posW.x, ps_in.posW.y) * g_UseWorldProjection;
float2 uv = baseUv;
// UVにTiling・Scrollを適用
uv = uv * g_UVTiling + g_UVOffset;
// Waveを適用
const float phase = dot(uv, g_UVWaveDirection) * g_UVWaveFrequency + g_EffectTime * g_UVWaveSpeed;
uv += g_UVWaveDirection * sin(phase) * g_UVWaveAmplitude;
// 最後にアトラス上の対象領域へ変換
uv = g_FrameUVRect.xy + uv * g_FrameUVRect.zw;
const float directionLength = length(g_GradientDirection);
const float2 gradientDirection = directionLength > 0.0001f
? g_GradientDirection / directionLength
: float2(0.0f, 1.0f);
// 単位正方形のUVを方向へ射影した範囲を0~1へ正規化する。
const float minProjection = min(0.0f, gradientDirection.x) + min(0.0f, gradientDirection.y);
const float maxProjection = max(0.0f, gradientDirection.x) + max(0.0f, gradientDirection.y);
const float projectionRange = max(maxProjection - minProjection, 0.0001f);
const float gradientPosition = saturate(
(dot(originalUv, gradientDirection) - minProjection) / projectionRange);
const float gradientRange = g_GradientEndPosition - g_GradientStartPosition;
const float gradientT = abs(gradientRange) > 0.0001f
? saturate((gradientPosition - g_GradientStartPosition) / gradientRange)
: step(g_GradientStartPosition, gradientPosition);
const float4 uvGradientColor = lerp(g_GradientStartColor, g_GradientEndColor, gradientT);
float4 color = g_Texture.Sample(g_SamplerState, uv) * g_EffectColor * uvGradientColor;
// Fresnel効果を適用
if (g_UseFresnel != 0)
{
float3 viewDir = normalize(g_EyePosition.xyz - ps_in.posW.xyz);
float3 normal = normalize(ps_in.normal.xyz);
float ndotv = saturate(abs(dot(normal, viewDir)));
float fresnel = 1.0f - ndotv;
float mask = smoothstep(g_FresnelTreshold, 1.0f, fresnel);
color.rgb += g_FresnelColor.rgb * mask * g_FresnelIntensity;
}
color.rgb *= g_RendererIntensity;
if (color.a <= g_AlphaCutoff) discard;
return color;
}
使用した素材作成ツール
メッシュ素材には、フリーで使える 「Effect Mesh Generator」 と Blender を使用。
↓のサイトはとにかくシンプルで、ゲームに使いやすいメッシュ素材が多いのでオススメ。
Blenderはちょっとしたメッシュの変形や、自分で欲しい形を作りたい時などに使った。
(自前で作る場合、ちゃんとUV展開を考えないとテクスチャが綺麗に貼れないので、そのあたりは注意が必要)
あとはibisPaint。フリーでも使えるが、解放されるツールとアニメーション機能もふんだんに使えるようになることを考えれば、月300円はかなり安いと思う。(フリーハンドで書くのが苦手なので、ベジエ曲線機能などはなかなか重宝した)

走った際の土ぼこりエフェクトを描いている風景
最後に
シンプルなエフェクトを作るためのモジュール、動かし方をざっくり書いてみました。
仕組みも見た目も単純ではあるが、実際にゲームに入れてみると手触り感が段違いで満足。(特に剣攻撃なんかは、斬撃エフェクトが無いと当たり判定が分かりづらい部分もあるし)

実際ゲームで使ってみた映像
エフェクトの発生タイミングや大まかな形状まで含めて調整できるようになったことで、ゲーム体験の楽しさを考えるための選択肢がかなり増えた。
本格的なエフェクトエディタとは言えないが、プログラマーがゲームの演出を手軽に試行錯誤するためのツールとしては、なかなか良いものができた気がする。
次のゲーム制作でも活用していきたい。
来源:zenn · 游戏开发社区 · zenn.dev