【Godot】Godot向けEffekseerのプラグインを作りました【Effekseer】

はじめに

Godot Engineで使い慣れたエフェクト作成ツールであるEffekseerが使いたくて、Godot上でEffekseerで作成したエフェクトの再生を行うためのプラグインを開発しました。

Effekseerって?

キラキラしたイケてるエフェクトを割と簡単に作れるツールです。

https://effekseer.github.io

f:id:ueshita:20210503164629j:plain
Effekseerサイトトップ

最近バージョン1.60がリリースされましたね。めでたい🎉

www.youtube.com

プロシージャルモデル機能で球とかシンプルな形状のモデルは勿論、ちょっと凝った竜巻形状なんかも生成できるみたいです。

最後のGodot Engine対応をやりました。

Effekseerは他にもマテリアルエディタとか使えるので、ガチ勢の方はめっちゃ作りこむことができます。

f:id:ueshita:20210503180627p:plain

対応してるエンジンも多くて、Godot以外にもUnityとかUE4みたいなメジャーなエンジンはサポートしていて、DXライブラリやCocos2d-xもサポート済、なんとRPGツクールMZには標準搭載されていたりします。EffekseerはOSSなので、有志によってMMDやSiv3Dでも使えるようになっているみたいです。

また、C++APIを叩けばDirectXOpenGLレンダリングすることもできるし、WebAssembly対応もしてるのでJavaScriptやTypeScriptからAPIを叩いてWebGLレンダリングすることもできるので、オレオレエンジン派も安心ですね。

Godot向けプラグイン(アドオン)

ダウンロードページ

次のURLからダウンロードできます。

https://github.com/effekseer/EffekseerForGodot3/releases/

f:id:ueshita:20210503171953j:plain
Godotプラグインのダウンロードページ

プラグインの使い方

https://effekseer.github.io/Help_Godot/ja/how-to-use.html を見てください。

バグを見つけたら

GitHubでIssue報告をお願いします。🙇

プラグイン開発

Effekseerプラグインを開発するときに得たアレコレを書き残します。

EffekseerForGodot3のリポジトリはこちら https://github.com/effekseer/EffekseerForGodot3

GDNative

GodotにはGDNativeという仕組みで、C++等で作ったDLLの関数をGDScriptやC#から呼び出すことができます。

基本的にはこれでEffekseer::Managerにアクセスしています。

レンダリング方法(3D編)

あとはEffekseerRendererを使ってレンダリングするだけです。

しかし問題があって、GodotのユーザーサイドからはOpenGLを直接叩く方法が無いのです。

UnityはネイティブプラグインからDirectXとかOpenGLが叩ける(あれはあれで闇が深い)けど、Godotはそのような方法がエンジン改造をする以外に無いので困りました。

Godotのリファレンスを読み進めていく中、ImmediateGeometry が使えるんじゃないかと思い至りました。ImmediateGeometryはプロシージャルメッシュを毎フレーム生成して3Dシーンで描画するクラスで、頂点データを登録する際のAPI呼び出しのオーバーヘッドはありそうですが、EffekseerRendererから出力される頂点データを描画するにはこれしかなさそうです。

頂点データの転送

ImmediateGeometryに頂点データを転送します。

実際は次のコードのように、ImmediateGeometryが内部で呼び出しているVisualServerのAPIを直接呼び出しています。

auto vs = godot::VisualServer::get_singleton();

// ジオメトリ生成開始
vs->immediate_begin(immediate, godot::Mesh::PRIMITIVE_TRIANGLE_STRIP);

const SimpleVertex* vertices = (const SimpleVertex*)vertexData;
for (int32_t i = 0; i < spriteCount; i++)
{
    // 縮退三角形の端っこ
    vs->immediate_color(immediate, godot::Color());
    vs->immediate_uv(immediate, godot::Vector2());
    vs->immediate_vertex(immediate, ConvertVector3(vertices[i * 4 + 0].Pos));

    // スプライト本体の頂点データ
    for (int32_t j = 0; j < 4; j++)
    {
        auto& v = vertices[i * 4 + j];
        vs->immediate_color(immediate, ConvertColor(v.Col));
        vs->immediate_uv(immediate, ConvertUV(v.UV));
        vs->immediate_vertex(immediate, ConvertVector3(v.Pos));
    }

    // 縮退三角形の端っこ
    vs->immediate_color(immediate, godot::Color());
    vs->immediate_uv(immediate, godot::Vector2());
    vs->immediate_vertex(immediate, ConvertVector3(vertices[i * 4 + 3].Pos));
}

// ジオメトリ生成終了
vs->immediate_end(immediate);

ここでは縮退三角形を生成しています(本当は転送サイズを減らすためにインデックスを使用したい…)

レンダリングが行われるようにする

Instanceに対してinstance_set_baseでImmediateGeometoryのRIDを設定するとレンダリングが行われるようになります。

void RenderCommand::DrawSprites(godot::World* world, int32_t priority)
{
    auto vs = godot::VisualServer::get_singleton();

    vs->instance_set_base(m_instance, m_immediate);
    vs->instance_set_scenario(m_instance, world->get_scenario());
    vs->material_set_render_priority(m_material, priority);
}

レンダリング方法(2D編)

3Dでレンダリングできれば2Dでもいい感じに表示されると思ってたのですが甘かったです。

Godotは3Dと2Dでレンダリングの仕組みが違うのです。 ビューポートを使って3Dと2Dをレイヤー的に重ねることはできますが、2Dに半透明ブレンドしたり加算ブレンドすることが難しくなります。

2DシーンではCanvasItemを使って描画します。ImmediageGeometoryは3DのInstanceには設定できますが、2DのCanvasItemには設定することができないので、別の方法を探す必要があります。

頂点データの転送

幸いCanvasItemには canvas_item_add_triangle_array で頂点データが指定できるので、これを使用して頂点データの転送が出来そうです。

auto vs = godot::VisualServer::get_singleton();

godot::PoolIntArray indexArray;
godot::PoolVector2Array pointArray;
godot::PoolColorArray colorArray;
godot::PoolVector2Array uvArray;

indexArray.resize(spriteCount * 6);
pointArray.resize(spriteCount * 4);
colorArray.resize(spriteCount * 4);
uvArray.resize(spriteCount * 4);

// Generate index data
int* indices = indexArray.write().ptr();

for (int32_t i = 0; i < spriteCount; i++)
{
    indices[i * 6 + 0] = i * 4 + 0;
    indices[i * 6 + 1] = i * 4 + 1;
    indices[i * 6 + 2] = i * 4 + 2;
    indices[i * 6 + 3] = i * 4 + 3;
    indices[i * 6 + 4] = i * 4 + 2;
    indices[i * 6 + 5] = i * 4 + 1;
}

// Copy vertex data
godot::Vector2* points = pointArray.write().ptr();
godot::Color* colors = colorArray.write().ptr();
godot::Vector2* uvs = uvArray.write().ptr();

const SimpleVertex* vertices = (const SimpleVertex*)vertexData;
for (int32_t i = 0; i < spriteCount; i++)
{
    for (int32_t j = 0; j < 4; j++)
    {
        auto& v = vertices[i * 4 + j];
        points[i * 4 + j] = ConvertVector2(v.Pos, baseScale);
        colors[i * 4 + j] = ConvertColor(v.Col);
        uvs[i * 4 + j] = ConvertUV(v.UV);
    }
}

vs->canvas_item_add_triangle_array(canvas_item, indexArray, pointArray, colorArray, uvArray);

レンダリングが行われるようにする

親ノードを設定してマテリアルを指定すると描画されるようになります。 またEffekseerRendererが出力する頂点データは絶対座標のため、親ノードのトランスフォームの影響を受けないように逆行列を指定します。

void EffekseerGodot::RenderCommand2D::DrawSprites(godot::Node2D* parent)
{
    auto vs = godot::VisualServer::get_singleton();

    vs->canvas_item_set_parent(m_canvasItem, parent->get_canvas_item());
    vs->canvas_item_set_transform(m_canvasItem, parent->get_global_transform().affine_inverse());
    vs->canvas_item_set_material(m_canvasItem, m_material);
}

マテリアル

Effekseerの想定したレンダリングが行われるように、Godotシェーダを生成しています。

Godotの描画ステートはシェーダ内で定義する仕様なので、ステートの組み合わせ分だけシェーダを生成しています。

static const char* ShaderType3D = 
    "shader_type spatial;\n";

static const char* ShaderType2D = 
    "shader_type canvas_item;\n";

static const char* BlendMode[] = {
    "",
    "render_mode blend_mix;\n",
    "render_mode blend_add;\n",
    "render_mode blend_sub;\n",
    "render_mode blend_mul;\n",
};
static const char* CullMode[] = {
    "render_mode cull_back;\n",
    "render_mode cull_front;\n",
    "render_mode cull_disabled;\n",
};
static const char* DepthTestMode[] = {
    "render_mode depth_test_disable;\n",
    "",
};
static const char* DepthWriteMode[] = {
    "render_mode depth_draw_never;\n",
    "render_mode depth_draw_always;\n",
};

bool Shader::Compile(RenderType renderType, const char* code, std::vector<ParamDecl>&& paramDecls)
{
    auto vs = godot::VisualServer::get_singleton();

    auto& shader = m_internals[(int)renderType];
    shader.paramDecls = std::move(paramDecls);

    godot::String baseCode = code;

#define COUNT_OF(list) (sizeof(list) / sizeof(list[0]))
    if (renderType == RenderType::CanvasItem)
    {
        for (size_t bm = 0; bm < COUNT_OF(BlendMode); bm++)
        {
            godot::String fullCode;
            fullCode += ShaderType2D;
            fullCode += BlendMode[bm];
            fullCode += baseCode;

            shader.rid[0][0][0][bm] = vs->shader_create();
            vs->shader_set_code(shader.rid[0][0][0][bm], fullCode);
        }
    }
    else
    {
        for (size_t dwm = 0; dwm < COUNT_OF(DepthWriteMode); dwm++)
        {
            for (size_t dtm = 0; dtm < COUNT_OF(DepthTestMode); dtm++)
            {
                for (size_t cm = 0; cm < COUNT_OF(CullMode); cm++)
                {
                    for (size_t bm = 0; bm < COUNT_OF(BlendMode); bm++)
                    {
                        godot::String fullCode;
                        fullCode += ShaderType3D;
                        fullCode += DepthWriteMode[dwm];
                        fullCode += DepthTestMode[dtm];
                        fullCode += CullMode[cm];
                        fullCode += BlendMode[bm];
                        fullCode += baseCode;

                        shader.rid[dwm][dtm][cm][bm] = vs->shader_create();
                        vs->shader_set_code(shader.rid[dwm][dtm][cm][bm], fullCode);
                    }
                }
            }
        }
    }
#undef COUNT_OF
    return true;
}