C/C++のMessagePackライブラリMPackを使う

はじめに

MessagePackはオブジェクトシリアライズ用の軽量なバイナリフォーマットです。
テキストフォーマットであるJSONの代わりに用いられることが多く、ソケット通信やファイル記録など使用することができます。

C/C++向けのMessagePackライブラリはいくつか存在しますが、今回はMPackを紹介したいと思います。

github.com

Mpackは次の特徴があります。

使い方

使い方については基本的にはGitHubのREADME.mdに書かれていますが、ゼロアロケーションで使う方法を備忘録的に書き残します。

サンプルコード

#include <stdio.h>
#include <memory>
#include <array>
#include <string_view>
#include <iostream>
#include <iomanip>
#include "mpack/mpack.h"

int main()
{
    std::array<uint8_t, 1024> buffer;

    {
        printf("Writing MsgPack.\n");

        // ライター初期化
        mpack_writer_t writer;
        mpack_writer_init(&writer, (char*)buffer.data(), buffer.size());

        // 整数書き出し
        mpack_write_uint(&writer, 123);

        // 実数書き出し
        mpack_write_float(&writer, 1.23f);

        // ブール値書き出し
        mpack_write_bool(&writer, true);

        // 文字列書き出し
        mpack_write_cstr(&writer, "hogehoge");

        // 配列書き出し
        mpack_start_array(&writer, 3);
        for (size_t i = 0; i < 3; i++) {
            mpack_write_uint(&writer, i);
        }
        mpack_finish_array(&writer);

        // 連想配列書き出し
        mpack_start_map(&writer, 3);
        for (size_t i = 0; i < 3; i++) {
            static const char* names[] = {"foo", "bar", "baz"};
            mpack_write_cstr(&writer, names[i]);
            mpack_write_uint(&writer, i);
        }
        mpack_finish_map(&writer);

        // 出力データサイズ
        size_t outputSize = mpack_writer_buffer_used(&writer);

        // 書き出し終了
        mpack_writer_destroy(&writer);

        // バイナリ表示
        std::cout << std::hex;
        for (size_t i = 0; i < outputSize; i++) {
            std::cout << std::setfill('0') << std::setw(2) << (int)buffer[i] << " ";
        }
        std::cout << std::dec << std::endl;
    }

    {
        printf("Reading MsgPack.\n");

        // リーダー初期化
        mpack_reader_t reader;
        mpack_reader_init_data(&reader, (const char*)buffer.data(), buffer.size());

        // 整数読み出し
        mpack_tag_t intTag = mpack_read_tag(&reader);
        std::cout << mpack_tag_uint_value(&intTag) << std::endl;

        // 実数読み出し
        mpack_tag_t floatTag = mpack_read_tag(&reader);
        std::cout << mpack_tag_float_value(&floatTag) << std::endl;

        // ブール値読み出し
        mpack_tag_t boolTag = mpack_read_tag(&reader);
        std::cout << std::boolalpha << mpack_tag_bool_value(&boolTag) << std::endl;

        // 文字列読み出し
        mpack_tag_t strTag = mpack_read_tag(&reader);
        size_t strSize =  mpack_tag_str_length(&strTag);
        std::string_view str(mpack_read_bytes_inplace(&reader, strSize), strSize);
        std::cout << str << std::endl;
        mpack_done_str(&reader);

        // 配列読み出し
        mpack_tag_t arrayTag = mpack_read_tag(&reader);
        size_t arraySize = mpack_tag_array_count(&arrayTag);
        std::cout << "[";
        for (size_t i = 0; i < arraySize; i++) {
            if (i > 0) std::cout << ", ";

            mpack_tag_t intTag = mpack_read_tag(&reader);
            std::cout << mpack_tag_uint_value(&intTag);
        }
        std::cout << "]" << std::endl;
        mpack_done_array(&reader);

        // 連想配列読み出し
        mpack_tag_t mapTag = mpack_read_tag(&reader);
        size_t mapSize = mpack_tag_map_count(&mapTag);
        std::cout << "{";
        for (size_t i = 0; i < mapSize; i++) {
            if (i > 0) std::cout << ", ";

            mpack_tag_t strTag = mpack_read_tag(&reader);
            size_t strSize =  mpack_tag_str_length(&strTag);
            std::string_view str(mpack_read_bytes_inplace(&reader, strSize), strSize);
            std::cout << str << ":";
            mpack_done_str(&reader);

            mpack_tag_t intTag = mpack_read_tag(&reader);
            std::cout << mpack_tag_uint_value(&intTag);
        }
        std::cout << "}" << std::endl;
        mpack_done_map(&reader);

        // 読み出し終了
        mpack_reader_destroy(&reader);
    }

    return 0;
}

出力

Writing MsgPack.
7b ca 3f 9d 70 a4 c3 a8 68 6f 67 65 68 6f 67 65 93 00 01 02 83 a3 66 6f 6f 00 a3 62 61 72 01 a3 62 61 7a 02
Reading MsgPack.
123
1.23
true
hogehoge
[0, 1, 2]
{foo:0, bar:1, baz:2}

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

はじめに

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

Effekseerって?

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

https://effekseer.github.io

Effekseerサイトトップ

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

www.youtube.com

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

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

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

対応してるエンジンも多くて、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/

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;
}

【Godot】エラー "Condition "!script_data" is true." を黙らせる

発生した現象

Godot EngineでC++等使ってNativeScriptを実装してゲーム作りをしていると、こんなエラーを見ることがあります。

f:id:ueshita:20210418184021p:plain

実行して戻ってくると。

f:id:ueshita:20210418184058p:plain

エラーが2倍に増える。

めっちゃウザい!

発生条件

  • GDNativeのDLLが2つ以上ある
  • Godotエディタのフォーカスが外れた後、再度フォーカスする

解決方法

ネイティブライブラリ(dllとかsoとか)を指定しているファイルGDNativeLibaryのプロパティのReloadableのチェックを外すだけです。

f:id:ueshita:20210418184620p:plain

原因

GodotのDLLのリロード処理が上手く行われていないんですかね?

Reloadableの詳細は次の通りです。

https://docs.godotengine.org/en/stable/classes/class_gdnativelibrary.html#class-gdnativelibrary-property-reloadable

[原文] If true, the editor will temporarily unload the library whenever the user switches away from the editor window, allowing the user to recompile the library without restarting Godot.

[日本語訳] true の場合、ユーザーがエディタ ウィンドウから離れると、エディタは一時的にライブラリをアンロードし、ユーザーは Godot を再起動せずにライブラリを再コンパイルできます。

ReloadableがOFFになるとエディタ起動中はDLLがロードしっぱなしになるため、GDNativeの変更中はビルドのたびにエディタ終了して再起動する必要がありそうです。 (Unityのネイティブプラグイン開発の悪夢再び…)

ReloadableがONのGDNativeLibraryが1つだけの時はエラー出ないので、GDNativeの変更が終わったらReloadableをOFFにする運用がよさそう?

【Godot】ディザ抜きシェーダで半透明を表現する

f:id:ueshita:20201206133700g:plain

ディザ抜きと呼ばれる手法を使うと半透明の表現ができます。

半透明といえばアルファブレンドですが、最近のゲームではアルファブレンドの代わりとしてディザ抜きがよく用いられています。

ディザ抜き

ディザ抜きはアルファブレンドに比べて若干ジャギジャギ感はありますが、次のメリットがあります。

  • 描画コストが軽い(不透明レンダリングなので)
  • 描画順を考えなくていい
  • 重ねた時に破綻しない
  • 内部のポリゴンが透けて見えない

Godotでディザ抜きをする

Godotでディザ抜きする方法を解説します。

BayerMatrix画像を用意する

ディザ抜きにはBayerMatrixというものを使用します。

0/168/162/1610/16
12/164/1614/166/16
3/1611/161/169/16
15/167/1613/165/16

このパラメータをグレイスケール化した4x4の小さな画像テクスチャを用意します。

f:id:ueshita:20201206134015p:plain

テクスチャのインポート設定を次のように設定します。

  • Compress ModeをLosslessにする
  • FilterをOFFにする

f:id:ueshita:20201206193755p:plain

ディザ抜きシェーダを書く

次にディザ抜きするシェーダを書きます。

  • フラグメントシェーダでBayerMatrixテクスチャをFRAGCOORDで参照
  • dither値とAlpha値と比較して抜く場合はdiscard構文でフラグメント処理を打ち切る
shader_type spatial;

uniform sampler2D Texture;  // モデルのテクスチャ
uniform sampler2D Bayer;    // 4x4のBayerMatrix画像
uniform float Alpha = 1.0;  // アルファパラメータ (0.0~1.0)

void fragment() {
    float dither = texture(Bayer, FRAGCOORD.xy * 0.25).r;
    if (dither >= Alpha)
        discard;
    ALBEDO = texture(Texture, UV).rgb;
}

シェーダパラメータをスクリプトから設定する

AlphaパラメータをShaderMaterialに設定します。 (TextureとBayerは事前にセットしておきます。)

extends MeshInstance

const DURATION = 1.0
var alpha = 0.0
var fade_mode = 0

func _process(delta):
    # スペースキーを押すとフェードアニメーションを開始
    if Input.is_action_just_pressed("ui_accept"):
        if fade_mode == 0:
            if alpha < 0.5:
                fade_mode = 1
            else:
                fade_mode = -1

    if fade_mode < 0:
        alpha -= delta / DURATION
        if alpha <= 0.0:
            alpha = 0.0
            fade_mode = 0
            
    if fade_mode > 0:
        alpha += delta / DURATION
        if alpha >= 1.0:
            alpha = 1.0
            fade_mode = 0

    # ShaderMaterialに値をセット
    var mat = material_override as ShaderMaterial
    mat.set_shader_param("Alpha", alpha)

参考記事

【Godot】HTTPで画像を取得してテクスチャとして使う

はじめに

ネット上の画像をゲーム内で使いたいみたいな状況があるかもしれません (?)

Unityだと簡単にできるんですが、Godotでは出来るか気になったので実験してみました。

今回はこの3つのクラスを使用します。

手順

ノードを組む

3Dノードを組みます。

四角形メッシュを配置してカメラで向くようにします。 ディレクショナルライトも忘れずに。

f:id:ueshita:20200923194917p:plain

次に、UIノードを組みます。

ボタンを押すとシグナルでスクリプトが呼び出されるようにします。

f:id:ueshita:20200923195132p:plain

スクリプトを書く

extends Spatial

# ueshitaのTwitterアイコンのURL
const url = "https://pbs.twimg.com/profile_images/461385142607429632/c6z5xZS3_400x400.png"

func _ready():
    $HTTPRequest.connect("request_completed", self, "_on_request_completed")

func _on_Button_pressed():
    $HTTPRequest.request(url)

func _on_request_completed(result: int, response_code: int, 
        headers: PoolStringArray, body: PoolByteArray):
    
    print("---- HTTP Response ---")
    print("result: %d" % result)
    print("response_code: %d" % response_code)
    print("headers: ", headers)

    # png画像をイメージにロードする
    var image = Image.new()
    image.load_png_from_buffer(body)
    
    # イメージからテクスチャを作成する
    var texture = ImageTexture.new()
    texture.create_from_image(image)
    
    # マテリアルにセットする (今回はSpatialMaterial)
    var material = $Quad.get_surface_material(0) as SpatialMaterial
    material.set_texture(SpatialMaterial.TEXTURE_ALBEDO, texture)

ボタンが押されると_on_Button_pressedが呼ばれ、HTTPリクエストが開始されます。

今回は自分のTwitterアイコンのURLを指定しています。

成功すると_on_request_completed が呼ばれ、画像ロードからテクスチャの設定が行われます。

実行結果

f:id:ueshita:20200923195007p:plain

やったね!

最後に

プロダクトとして作る場合はエラー処理をちゃんと書いたほうがいいですね。

【WebGL】GPUパーティクル、GPUトレイルの実装

f:id:ueshita:20200922164416p:plain

概要

8月に勢いで実装したWebGL2で動作するモノです。

デモ

リポジトリ

github.com

解説

TransformFeedbackは使用せず、複数のテクスチャをダブルバッファ的に使用してパーティクルの更新を行います。

テクスチャレイアウト

X Y Z W
Tex0(RW) Pos X Pos Y Pos Z Dir XYZ
Tex1(RW) Age Lifetime Seed Vel XYZ

1テクセルが1パーティクルに割り当てられます。

パーティクル

処理は以下の通りです。

  1. perticle-emit
  2. particle-update
  3. particle-render

emitは1ピクセルのポイントスプライトをgl.drawArrays(gl.POINTS, 0, this.newParticleCount);で描画して、指定のテクセルに書き込まれるようにします。

updateは四角形をgl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);してフラグメントシェーダで全パーティクルを更新

renderはgl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, this.maxParticleCount);で全パーティクルを描画(寿命が過ぎたパーティクルは縮退ポリゴンで消去)します。

トレイル(軌跡)

軌跡情報は2Dテクスチャ配列をリングバッファ的に使って保持します。 gl.copyTexSubImage3DでTex0をコピーして更新します。

処理は以下の通りです。

  1. perticle-emit
  2. particle-update
  3. (gl.copyTexSubImage3D)
  4. trail-render

2まではパーティクルと同じ処理です。

トレイルメッシュをgl.drawArraysInstancedインスタンス描画しています。

【Godot】トランジション(画面切り替え)をシェーダで作る

はじめに

ゲームの画面切り替えの際、フェードインやフェードアウトすることがあります。

こうした画面切り替えの演出をトランジションエフェクトといいます。

今回はちょっと凝った切り替えもやってみようと思います。

例えばこんなトランジション。ノベルゲームとかでよく見ますね。

f:id:ueshita:20200917122208g:plain

背景イラストは きまぐれアフター さんからお借りしました。

トランジションエフェクトを実装する

ルール画像

トランジションにはルール画像というモノクロ画像を使います。

今回は For You さんからお借りしました。

f:id:ueshita:20200917114701p:plain

シェーダを記述する

今回はVisualShaderを使いました。ノードベースなシェーダは楽でいいですね。

このシェーダはルール画像をマスクとして使い、黒いところから徐々に透けていくようにsmoothstep関数を使って記述しました。

f:id:ueshita:20200917120150p:plain

一応シェーダコード版も載せておきます。

shader_type canvas_item;

uniform float SoftRange = 0.02;   // 境目を半透明にする幅
uniform float Value = 0.0;        // フェード値
uniform sampler2D Mask;

void fragment()
{
    float rangedValue = Value * (SoftRange * 2.0 + 1.0) - SoftRange;
    float maskValue = texture(Mask, UV).r;
    float minValue = rangedValue - SoftRange;
    float maxValue = rangedValue + SoftRange;
    float alpha = smoothstep(minValue, maxValue, maskValue);
    COLOR = vec4(vec3(0.0), alpha);
}

マテリアルを設定する

背景絵を表示するBGPanelと、トランジション演出を表示するTransitionPanelを設定します。

TransitionPanelにはShaderMaterialを設定し、先ほど作成したシェーダを設定します。

f:id:ueshita:20200917153926p:plain

Shader ParamValueをマウスでドラッグすると、どんな感じになるかプレビューできます。

スクリプトを書く

extends Panel

onready var shader_mat := material as ShaderMaterial

const duration = 1.0   # トランジション時間
var shader_value := 0.0    # シェーダに渡す値
var is_fading := false  # 処理のスイッチ

func _process(delta: float):
    # Spaceキーを押したら開始
    if Input.is_action_just_pressed("ui_accept"):
        is_fading = true
        shader_value = 0.0
    
    # ESCキーを押したらリセット
    if Input.is_action_just_pressed("ui_cancel"):
        is_fading = false
        shader_value = 0.0
    # トランジション(フェード)処理
    if is_fading:
        shader_value = min(1.0, shader_value + delta / duration)
    # シェーダマテリアルに値をセット
    shader_mat.set_shader_param("Value", shader_value)

実行結果

f:id:ueshita:20200917204023g:plain

ほかの例

色々なルール画像を使って面白いトランジションエフェクトができます。

f:id:ueshita:20200917205359g:plain

補足

今回は黒一色に暗転するエフェクトのみの解説でしたが、他の画像やレンダリングしたビューポートをテクスチャとして使うと、クロスフェード的な画面切り替えもできると思います。