夏休みに作ってたゲームの動画 pic.twitter.com/MIdCFh47PT
— hacca (@hacca_os) November 26, 2021
今年の夏休みはSiv3Dを使ってゲーム(以下INSIDER)を作りました。
描画や処理の最適化だったり、いい感じな表現を実現するのに役立った機能をまとめておきます。というよりSiv3Dの描画能力を最大限活用するためには絶対知っておいた方がいい機能だと思います。
※制作に使用したバージョンはOpenSiv3D0.4.3で、記事もそれにならっています。
#TextureAsset
TextureAssetを使うと特定のファイルの中の画像を読み込んでファイル名で管理することができます。テクスチャの管理が楽になります。
例
Array <FilePath> files = FileSystem::DirectoryContents(U"ファイルパス");
for (auto&& it = files.begin(); it != files.end(); ++it) {
if (FileSystem::Extension(*it) == U"png") {
TextureAsset::Register(FileSystem::BaseName(*it), *it, AssetParameter::LoadAsync());
}
}
#HashTable
ブロックを管理するとき、Gridを使うと第1象限以外の座標や空のブロックの管理が少し面倒です。代わりに存在するブロックだけをHashTableで管理すると余計な処理が不要になります。
ファイルに出力するときイテレータを使えばキーを取得できます。
例
class block{
public:
String type;
int option;
};
//Formatを使って座標をキーにします
HashTable<String, block> blocks;
Stringをキーにして実装しましたがVec2をキーにしたほうが良いかも
#RenderTexture
RenderTextureはSiv3Dの描画順に関する問題を一気に解決してくれます。
そのままではレイヤー管理には使えないのでRenderStateを設定します。
例
RenderTexture RT(1920,1080);
BlendState bs { BlendState::Default };
bs.srcAlpha = Blend::SrcAlpha;
bs.dstAlpha = Blend::DestAlpha;
bs.opAlpha = BlendOp::Max;
RT.clear(ColorF(0, 0, 0, 0));
{
ScopedRenderTarget2D target(RT);
ScopedRenderStates2D blend(bs);
}
前提としてINSIDERのブロックの描画はこんな感じになっています↓
キャラクターを描画するためにはブロックを前後に分割する必要がありますが、RenderTextureを使わない実装は「ブロックの描画位置の計算→描画順に並び替え→後ろの描画→(キャラクター描画)→前の描画」となり描画に関わるループが増えます。
ここでRenderTextureを使うと描画位置の計算と一緒にブロックの前後を描画できます。処理は「並び替え→位置計算&描画」となりループが大幅に減ります。
処理と描画を分けつつ、計算量も減らせます。
RenderStateを使ってdstのα値を使うようにすると簡単にクリッピングマスクを実装できます。逆にsrcのα値を引いて、くりぬいたような表現もできます。懐中電灯のような表現に使えるかも。
#カスタムピクセルシェーダ
カスタムピクセルシェーダを使うと表現の幅がぐっと広がります。広がりますが正直自分でもよくわからないまま書いてよく分からないまま動いてるので、不備があったら教えてもらえると助かります。
定数バッファの記述は省略します。
例1 射影変換
ver 0.6からは普通に使えるようになったのでおとなしくそっちを使ったほうがいい。
それと平行に変形する場合など、y座標が同じだと分母が0になってしまうのでこのコードでは対応できません。
Texture2D g_texture0 : register(t0);
SamplerState g_sampler0 : register(s0);
Texture2D g_texture1 : register(t1);
SamplerState g_sampler1 : register(s1);
cbuffer PSConstants2D : register(b0)
{
float4 g_colorAdd;
float4 g_sdfParam;
float4 g_internal;
}
cbuffer InputFITQUAD : register(b1)
{
//4点を時計回りに指定
float2 xy1;
float2 xy2;
float2 xy3;
float2 xy4;
}
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
float Func(float2 Pn, float2 Pn2)
{
return Pn.x * Pn2.y - Pn2.x * Pn.y;
}
float4 PS(PSInput input) : SV_TARGET
{
float x1 = xy1.x;
float y1 = xy1.y;
float x2 = xy2.x;
float y2 = xy2.y;
float x3 = xy3.x;
float y3 = xy3.y;
float x4 = xy4.x;
float y4 = xy4.y;
float X1 = 0;
float Y1 = 0;
float X2 = x2 - x1;
float Y2 = y2 - y1;
float X3 = x3 - x1;
float Y3 = y3 - y1;
float X4 = x4 - x1;
float Y4 = y4 - y1;
float2 P1 = float2(X1, Y1);
float2 P2 = float2(X2, Y2);
float2 P3 = float2(X3, Y3);
float2 P4 = float2(X4, Y4);
float alpha = Func(P4, P2) - Func(P3, P2);
float beta = Func(P4, P2) * x3 - Func(P3, P2) * x4;
float gamma = Func(P4,P2) * y3 - (X3 * Y2 - X2 * Y3) * y4;
float gover = Func(P4, P3) * (Y3 * gamma - y2 * Y3 * alpha) - Func(P2, P3) * (Y3 * gamma - y4 * Y3 * alpha);
float gunder = Func(P4, P3) * (y2 * Y3 * beta - x2 * Y3 * gamma) - Func(P2, P3) * (y4 * Y3 * beta - x4 * Y3 * gamma);
float g = gover / gunder;
float h = -(alpha + beta * g) / gamma;
float aover = Y3 * gamma - y2 * Y3 * alpha - (y2 * Y3 * beta - x2 * Y3 * gamma) * g;
float aunder = Func(P2, P3) * gamma;
float a = aover / aunder;
float dover = Func(P4, P3) * Y2;
float dunder = Func(P4, P2) * Y3;
float d = (dover / dunder) * a;
float b = -(X3 / Y3) * a;
float e = -(X2 / Y2) * d;
float c = -a * x1 - b * y1;
float f = -d * x1 - e * y1;
float x0to1 = (a * input.position.x + b * input.position.y + c) / (g * input.position.x + h * input.position.y + 1);
float y0to1 = (d * input.position.x + e * input.position.y + f) / (g * input.position.x + h * input.position.y + 1);
float4 ret = float4(0, 0, 0, 0);
if (0 <= x0to1 && x0to1 <= 1 && 0 <= y0to1 && y0to1 <= 1)
ret = g_texture1.Sample(g_sampler1, float2(1, 1) - float2(x0to1, y0to1));
return (ret);
}
例2 ブラーエフェクト
なるべくfor文を避けてます。
Texture2D g_texture0 : register(t0);
SamplerState g_sampler0 : register(s0);
cbuffer PSConstants2D : register(b0)
{
float4 g_colorAdd;
float4 g_sdfParam;
float4 g_internal;
}
cbuffer InputBLUR : register(b1)
{
//どれくらいぼかすか
float shift;
}
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
float4 PS(PSInput input) : SV_TARGET
{
float rate = 1;
const float qshiftx = shift / 1080.0f * rate;//1px
const float qshifty = shift / 1080.0f * rate;//1px
float4 Color_Color = float4(0, 0, 0, 0);
float nota0 = 0;
float Color_Conce = 0;
float4 Ov = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 0, qshifty * -1));
if(Ov.a != 0)
Color_Color.rgb += Ov.rgb, nota0 += 1;
Color_Conce += Ov.a;
float4 OvR = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 1, qshifty * -1));
if (OvR.a != 0)
Color_Color.rgb += OvR.rgb, nota0 += 1;
Color_Conce += OvR.a;
float4 OvL = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * -1, qshifty * -1));
if (OvL.a != 0)
Color_Color.rgb += OvL.rgb, nota0 += 1;
Color_Conce += OvL.a;
float4 Mi = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 0, qshifty * 0));
if (Mi.a != 0)
Color_Color.rgb += Mi.rgb * 2, nota0 += 2;
Color_Conce += Mi.a;
float4 MiR = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 1, qshifty * 0));
if (MiR.a != 0)
Color_Color.rgb += MiR.rgb, nota0 += 1;;
Color_Conce += MiR.a;
float4 MiL = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * -1, qshifty * 0));
if (MiL.a != 0)
Color_Color.rgb += MiL.rgb, nota0 += 1;
Color_Conce += MiL.a;
float4 Ud = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 0, qshifty * 1));
if (Ud.a != 0)
Color_Color.rgb += Ud.rgb, nota0 += 1;
Color_Conce += Ud.a;
float4 UdR = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 1, qshifty * 1));
if (UdR.a != 0)
Color_Color.rgb += UdR.rgb, nota0 += 1;
Color_Conce += UdR.a;
float4 UdL = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * -1, qshifty * 1));
if (UdL.a != 0)
Color_Color.rgb += UdL.rgb, nota0 += 1;
Color_Conce += UdL.a;
float4 Color;
Color.rgb = Color_Color.rgb / nota0;
Color.a = Color_Conce / 9.0;
return (Color);
}
例3 放射ぼかし
Texture2D g_texture0 : register(t0);
SamplerState g_sampler0 : register(s0);
cbuffer PSConstants2D : register(b0)
{
float4 g_colorAdd;
float4 g_sdfParam;
float4 g_internal;
}
cbuffer InputBLUR : register(b1)
{
float shift;
}
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
float4 PS(PSInput input) : SV_TARGET
{
float maxLong = distance(float2(960, 540), float2(1920, 1080)); //最大値
float Long = distance(input.position.xy, float2(960, 540)); //中心からの距離
float2 unitVector = (input.position.xy - float2(960, 540)) / Long; //中心からピクセルへの単位ベクトル
float rate = Long / maxLong;
const float qshiftx = shift / 1080.0f * rate;//1px
const float qshifty = shift / 1080.0f * rate;//1px
float4 Color_Color = float4(0, 0, 0, 0);
float nota0 = 0;
float Color_Conce = 0;
float4 Ov = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 0, qshifty * -1));
if(Ov.a != 0)
Color_Color.rgb += Ov.rgb, nota0 += 1;
Color_Conce += Ov.a;
float4 OvR = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 1, qshifty * -1));
if (OvR.a != 0)
Color_Color.rgb += OvR.rgb, nota0 += 1;
Color_Conce += OvR.a;
float4 OvL = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * -1, qshifty * -1));
if (OvL.a != 0)
Color_Color.rgb += OvL.rgb, nota0 += 1;
Color_Conce += OvL.a;
float4 Mi = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 0, qshifty * 0));
if (Mi.a != 0)
Color_Color.rgb += Mi.rgb * 2, nota0 += 2;
Color_Conce += Mi.a;
float4 MiR = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 1, qshifty * 0));
if (MiR.a != 0)
Color_Color.rgb += MiR.rgb, nota0 += 1;;
Color_Conce += MiR.a;
float4 MiL = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * -1, qshifty * 0));
if (MiL.a != 0)
Color_Color.rgb += MiL.rgb, nota0 += 1;
Color_Conce += MiL.a;
float4 Ud = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 0, qshifty * 1));
if (Ud.a != 0)
Color_Color.rgb += Ud.rgb, nota0 += 1;
Color_Conce += Ud.a;
float4 UdR = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * 1, qshifty * 1));
if (UdR.a != 0)
Color_Color.rgb += UdR.rgb, nota0 += 1;
Color_Conce += UdR.a;
float4 UdL = g_texture0.Sample(g_sampler0, input.uv + float2(qshiftx * -1, qshifty * 1));
if (UdL.a != 0)
Color_Color.rgb += UdL.rgb, nota0 += 1;
Color_Conce += UdL.a;
float4 Color;
Color.rgb = Color_Color.rgb / nota0;
Color.a = Color_Conce / 9.0;
return (Color);
}
例4 縁取り
処理が重たくなる。
Texture2D g_texture0 : register(t0);
SamplerState g_sampler0 : register(s0);
cbuffer PSConstants2D : register(b0)
{
float4 g_colorAdd;
float4 g_sdfParam;
float4 g_internal;
}
cbuffer InputEDGE : register(b1)
{
float thickness;
float R;
float G;
float B;
}
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
float4 PS(PSInput input) : SV_TARGET
{
bool ok = false;
float2 unit;
float4 ctest;
float _thickness = thickness;
float _thickness_half = -thickness / 2;
[loop]
for (float x = 0; x < thickness; ++x)
{
[loop]
for (float y = 0; y < thickness; ++y)
{
unit.x = (_thickness_half + x) / 1920.0f;
unit.y = (_thickness_half + y) / 1080.0f;
ctest = g_texture0.Sample(g_sampler0, input.uv + unit);
if (ctest.a != 0)
{
ok = true;
}
}
}
float4 color = g_texture0.Sample(g_sampler0, input.uv);
if (ok && color.a == 0)
{
color = float4(R, G, B, 1);
}
return (color);
}
重ね掛けするときはその組み合わせのシェーダをちゃんと書いて一回で済むようにしたほうがよさそう?
ほかにも色収差とか、ある程度自分で書けるといろいろな表現が可能になって楽しいです。
#おまけ
Siv3Dにはテキストを指定した矩形に収まるよう描画できる関数があります。しかし、収まりきらない場合、最後のほうは三点リーダーになって描画されません。メッセージログの描画など、横幅は決まっているけれど高さはわからない場合、簡単に矩形を取得できます。
例
Font font(60);
String text;
double w = 横幅;
h = std::ceil(font(text).region().w / w) * font(_text).region().h;
font(text).draw(Rect(0, 0, w, h));
#終わりに
入門編のつもりで書き始めたつもりですがチュートリアル読むのが一番手っ取り早いです。なのでこの記事はそこそこ分かってきて、もうちょっと表現凝ってみたい段階の人の参考になったらいいかなと思います。
ずっとお世話になってるライブラリなのでこれからはおまけに書いた小手先テクだったり、あとはRenderTextureだったり、RenderStateのクリッピングマスクやくり抜きなど、チュートリアルの充実に寄与できたら良いな。