MiEngine:尝试制作模块化网格特效编辑器
MiEngine:モジュール式のメッシュエフェクトエディタを作ってみる
MiEngine是什么?:是使用DirectX11和C++个人开发的自制游戏引擎。
想做的事情
想做出类似Unity的ParticleSystem组件那样的氛围的网格特效。
总之,就是希望能像↓那样,通过模块的开关和参数调整来表现特效。
(另外,这里所说的网格特效,指的是像斩击或冲击波那样,用《网格》×《纹理》来表现的效果)
在自制引擎中,已经能够做出参考Unity的简单ParticleSystem编辑器,所以参考它,来思考表现网格特效所需的模块。

自制的ParticleSystem编辑器
※另外,本文是以游戏程序员的视角撰写的,目的是在游戏内轻松做出看起来像那么回事的效果。请注意,这并非要制作一个正式的效果编辑器。
大致梳理编辑器的构成要素
本次的编辑器将构成效果的各个要素作为独立模块来处理,通过切换它们的开关或修改参数来调整外观,我们就是按这样的方式制作的。
将处理集中到一个共通着色器中,通过传入设定值来切换外观,以此方式实现。(详情后述)
编辑器画面主要由「效果资源列表」「预览」「构成模块的参数」这3个要素构成,为了便于编辑,还实现了自动保存以及鼠标旋转相机等功能。

此外,本次的目标终究是制作一个「只需选择必要的要素、调整参数,就能做出看起来像那么回事的效果」的编辑器,因此功能保持在了最小限度。
(看↑应该就能明白,用于预览的临时对象也直接生成在场景中了。因为能看到尺寸感,反而挺方便的)
必需模块一览
沿用 ParticleSystem 的做法,首先创建两个必需的模块。
Main
特效的播放时长、是否循环播放、是否自动从头开始播放等等。

Main 模块
Renderer
作为最想与 ParticleSystem 做出差异化的要素,首先有以单个 mesh 为中心来构成特效的思路,因此引入了用于 mesh 的模型路径和纹理路径。
此外还有 billboard、blend state、sampler state 等等,加入了各种似乎能拓宽表现范围的内容。

Renderer 模块
主要模块列表
Transform
感觉与冲击波、瞬间出现的受击特效等相性很好。
创建曲线关键帧,并尝试让其沿着 Duration 从基准值发生变动。

Transform模块
Flipbook(精灵动画)
冲击波、星星闪烁等等。可用于各种特效的模块。
如果有配合特效 Duration 播放的模式,以及指定速度播放的模式这两种模式,特效的表现范围就会更广。我感觉前者适合瞬间性的特效,后者适合循环播放的特效。
我试着用了几张在 ibisPaint 里制作的纹理,实际动起来看还挺像样的。

Flipbook 模块
Transform x Flipbook 的特效制作示例
通过稍微改变 Transform 和 Flipbook 动画,在制作中的游戏里尝试制作了冲击波特效和枪口闪光。

冲击波与枪口闪光特效(冲击波使用圆柱体,枪口闪光使用平面网格)

冲击波的纹理
UV Scroll
只是一个让纹理朝指定方向移动的模块。做的事情非常简单,但像斩击之类的效果,如果只是简单表现,光靠这个就能做出来。
如果不希望纹理重复显示,只要把 Renderer 模块的采样器状态设为 Clamp 即可。

UVScroll 模块
Gradient
用于添加渐变颜色的模块。可以根据网格的 UV 位置或随时间经过来添加渐变。
不仅可以用于色彩变化,还可以用来降低斩击特效边缘的 alpha 值等,消除第一眼看到时特效中断的违和感。

Gradient 模块
UVScroll x Gradient 的特效制作示例
除此之外还稍微加入了Transform模块的旋转,不过用这两个模块就能做出简单的斩击特效。
可以说制作网格特效编辑器的目的有大约六成就是为了它,所以能做得像模像样,相当满足。

斩击特效

网格的形状是这样的

用ibisPaint一点点画出来的斩击贴图
有了它或许能拓宽表现范围的模块列表
Wave
顾名思义,是让 UV 产生波动的模块。似乎可以用于水球,或需要波动的特效。此次制作的游戏中,清晰明确的特效较多,所以没怎么使用。在奇幻类游戏中制作魔法阵等时,应该会很实用。

Wave 模块
Wave 特效制作示例
信息量过多、在实际成品中未采用的光束特效。使用 Wave 模块时,由于动作比其他特效更复杂,在过于简单的自制特效中会显得有些突兀,这点很遗憾。

光束特效
菲涅尔
可以通过视线与法线的角度来强调边缘,做出具有演出效果的 Fresnel 表现。
与球形网格配合使用时,可以制作出能量球。由于能被发光的只是网格的边缘,因此可能不适合用于主要靠纹理塑造大形状的效果。

Fresnel 模块
Fresnel 效果制作示例
这是充能时经常见到的效果。

能量球效果
试着全部整合到一个着色器中
由于各模块都由相对简单的运算构成,因此这次将全部整合到了一个着色器中。
虽然也会残留一部分未使用模块的运算,但由于是个人制作,因此优先考虑减少单独管理各个着色器所需工夫这一优点。
各模块的启用与禁用,是通过设置表示是否使用的标志,或设置不影响运算结果的初始值来切换的。
(例如 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