GodotにEffekseerを導入して、発射エフェクトを実装する

Godot Engine Advent Calendar 2024の2日目の記事です

本記事では以下について解説します。

  • GodotのプロジェクトにEffekseerのプラグインを導入する手順
  • エフェクトの素材を改造してゲームに組み込む方法

こんな発射エフェクトを作ります

Effekseerプラグインを導入する

Effekseerのプラグインをプロジェクトに追加します。

プラグインを入手する

こちらからプラグインを入手できます。

github.com

最新バージョンのAssetsのzipをダウンロードします。

中身のaddonsフォルダをプロジェクトにコピーします。
res://addons/effekseer ができるようにします。

これでプラグインのインストールはできました。

プラグインを有効化する

インストールが完了したら、次はGodotで使えるようにします。

プロジェクト設定のプラグインタブからEffekseerを有効化します(有効チェックを入れる)。

これでプラグインの導入は完了です!

エフェクトを探す

https://booth.pm/ja/browse/%E7%B4%A0%E6%9D%90%EF%BC%88%E3%81%9D%E3%81%AE%E4%BB%96%EF%BC%89?q=Effekseer

かっこいいエフェクトが沢山ありますね。ぜひ購入して使わせていただきましょう!

ただ気になるのは、エフェクトの素材はBurst系が多いということです。 例えばアクションゲームで、炎の爆発で敵にダメージを与えるような魔法を作りたいとします。 自分から発射した炎弾が飛んでいき、何かに命中したら爆発するようにしたいです。

炎弾が飛んでいき、敵に命中すると爆発する例

爆発(Burst)はエフェクト素材を使うとして、発射物(Projectile)は自分で何とかしないといけません。 今回はこの発射物エフェクトをEffekseerで作って、Godotで実装するところまで解説したいと思います。

発射物(Projectile)を作る

Effekseerのエディタが無ければダウンロードしておきましょう。 https://effekseer.github.io/jp/

この記事ではこちらの無料のエフェクトを使います。 https://github.com/effekseer/EffectMaterials/

ef_fire01.efkefc

こちらのBurst系エフェクトをProjectile系に改造していきます。

  • 全体的にノードを削減する
    • シンプルにします。沢山撃てるように負荷削減の効果もあります。
  • 炎パーティクルが無限に放出されるようにする
    • 停止命令を受けるまで、エフェクトが勝手に終了しなくなります。
  • 炎パーティクルの「位置への影響」を「生成時のみ」にします
    • 弾の炎が生成位置に残されるようになります
  • 上下方向の移動を少し弱める
    • 好みによります。なるべく弾の軌跡を残したいので。
  • いろいろ微調整
ef_fire01_projectile.efkefc (停止状態)
ef_fire01_projectile.efkefc (飛行状態)

※振る舞いパネルの「移動速度」を設定すると、ルートの移動をシミュレートできます。

Godotに実装する

エフェクトのインポート

Godotに作成したエフェクトをインポートします。
EffekseerEmitter3Dで試しに再生してみます。再生ボタンを押すとエディタビュー上で再生できます。

位置を動かすと炎が残される

炎弾の実装

次に炎弾のシーン(fireball.tscn)を作ってみましょう。

  • CharacterBody3Dをルートにしてシーンを作成します。
  • CollisionShape3Dを追加してShapeにSphereShape3Dを設定します。
  • EffekseerEmitter3Dを追加してEffectにef_fire01_projectile.efkefcを設定します。
  • 今回解説しませんが、爆発時のヒットエリア用にArea3Dを追加しています。

炎弾を制御するスクリプトはこちら。

extends CharacterBody3D

const damage_point = 50  # ヒット時の爆発ダメージ
const speed = 10.0  # 飛行スピード
const lifetime = 3.0  # 飛行限度時間

@onready var _lifecount = 0.0
@onready var _emitter: EffekseerEmitter3D  = $Effect
@onready var _burst_area = $BurstArea    # 爆発の範囲
@onready var _burst_effect = load("res://effects/ef_fire01.efkefc")  # 爆発エフェクト

func _ready():
    # 終了したらfireballシーンを破棄
    _emitter.finished.connect(queue_free)

func _physics_process(delta: float) -> void:
    # 一定時間飛行したら終了する
    _lifecount += delta
    if _lifecount >= lifetime:
        stop()
        return

    # 一定のスピードで前進する
    velocity = transform.basis.z * speed * delta
    var collision = move_and_collide(velocity)
    if collision:  # 何かにぶつかったら爆発する
        burst()

func stop():
    set_physics_process(false)  # 物理更新も停止する
    _emitter.stop_root()  # ルートを停止してなめらかに消えるようにする

func burst():
    # 炎弾エフェクトは停止して、爆発エフェクトを再生
    stop()
    _emitter.effect = _burst_effect
    _emitter.play()

    # おまけ:エネミーにダメージを与える処理
    # for body in _burst_area.get_overlapping_bodies():
    #   if body is EnemyController:
    #       body.damage(damage_point)

炎弾を発射するスクリプトはこちら。

@onready var _fireball := load("res://fireball.tscn")

func spawn_shot():
    var instance = _fireball.instantiate()
    instance.rotation = global_rotation  # 向いている方向を前に設定
    instance.position = global_position + Vector3(0, 0.5, 0)  # 位置を設定。高さ調整
    get_viewport().add_child(instance)  # 

炎弾を発射して、何かに命中すると爆発する魔法ができました!

おわりに

今回はエフェクトの素材を改造してゲームに組み込む方法について解説しました。 エフェクトを1から作るのは結構気合が要るため、少人数でゲームを制作する場合はこういった方法が役に立つかもしれません。

実戦でGodot Engineを採用する際に気になること

Godot Engineはオープンソースであり非営利のプロジェクトです。 開発はコミュニティによって行われ、コントリビュータ(貢献者)によって実際のコーディングが行われています。

これを聞くと、Godotは責任の所在が不明でアマチュア集団が作っていると勘違いされがちです。 偉い人の中にはこのような疑問を持っている方もいるのではないでしょうか?

  • コンソールでちゃんとリリースできるの?
  • トラブルが起きた時サポートされるの?
  • 突然使えなくなったりしない?

こういった疑問に対して、ある程度回答のようなものができたらと思います。 本記事がGodot採用の一助になれれば幸いです。

コンソールでちゃんとリリースできるの?

結論から言うとできます。現にSwitchなどでGodot製のゲームがいくつもリリースされています。

ただしハードルは少しあります。まずOSSとコンソールプラットフォームは相性が良くないという理由があります。

ゲームやエンジンをコンソール対応するには、コンソールプラットフォームとNDA(秘密保持契約)を結び、提供されるクローズドなSDKを使用して行います。しかしそれを使用したコードもクローズドにすることが契約上求められるため、そのコードをオープンソースにすることができない、という問題が出てきます。

そのためGodotプロジェクト公式ではコンソールに対応することができません。コンソール対応の実装コードをGitHubに上げることはできないですし、Issueを立ててオープンな場で議論したりプルリクエストを出すこともできないのです。GitHubに上がっているソースコードWindows, Linux, macOS, iOS, Androidなど、オープンなプラットフォームに対応したものに限られています。

詳細はこちらの数年前の記事に書かれています。

godotengine.org

余談ですが、数年前に某商用エンジンがオープンソース化する際、コンソールのAPIにアクセスしているクローズドなソースコードもまとめてGitHubに上げてしまうという事件が起きました。界隈ではNDA違反じゃないかと話題になり、その後クローズドなソースコードGitHubから削除されました。

話を戻すと、Godotで作られたゲームをコンソールで動かすには次の2つの方法があります。

  • 他社のコンソールポーティングを使う
  • 自社でコンソール対応を行う

他社のコンソールポーティングを使う

他社が提供するGodotのコンソールポーティングを使う方法です。

GodotはOSSなので、どこの誰でもGodotから派生したエンジンを作って売ることができます。 その流れでGodotを派生させてコンソール向けのポーティングを行い他社にライセンスするビジネスを行う企業がいくつか存在します。 そういった企業はコンソールプラットフォームとNDA契約を結んでおり、クローズドなソースコードを持っています。

現在、コンソールポーティングを行っている代表的な企業はW4 Gamesです。こちらの価格モデルを見ると、プロジェクトの規模にもよりますが、一般的なミドルウェア1つ分くらいの価格で使用できそうな雰囲気です。

また重要な点として、W4 GamesはGodotのコア開発者が立ち上げた会社ということが挙げられます。Godotプロジェクト公式とは異なるのですが、半分公式くらいの立ち位置と考えてもいいのかな?と思われます。

自社でコンソール対応を行う

社内でGodotのコンソール対応を行うというストロングな方法です。

社内でエンジンを派生させることはライセンス上可能です。MITライセンスによる権利表記さえすれば自由に行えます。 社内のエンジニアがコンソール対応することで、ライセンス料なしでコンソールへゲームをリリースすることができます。

エンジニアの人件費があるから実際無料ではないのですが、一回対応してしまえばあとはメンテコストのみでいけます。 大企業なんかはこの方法を選ぶこともあるかもしれません。

トラブルが起きた時サポートされるの?

ゲームエンジンにトラブルはつきものです。トラブルが起きた時に誰に頼ればいいか不明だと怖いですよね。

商用エンジンでトラブルが起きた時は、エンジンの提供元に問い合わせてトラブルを解決できるかもしれません。

しかしOSSのGodotはどうでしょうか? なんとGodotエンジンのライセンスには「このソフトウェアを使ったことで何が起きても開発者に責任はないよ!」と書かれています。

エンジンを開発しているGodotプロジェクトにサポートを頼むことはできません。 トラブルが起きた際は次の2つの選択肢があります。

  • 有料でサポートを行っている企業に頼む
  • 社内でなんとかサポートする

有料でサポートを行っている企業に頼む

先述したコンソールポーティングを行っているW4 Gamesは、コンソール対応だけでなくサポートも行うとのことです。 もちろんこれは有料プランですが、これは責任の所在を明確にしたいというエンタープライズ向けの施策と思われます。

W4 GamesはGodotのコア開発者が設立した企業ですので、強力なサポートが得られるはずです。

社内でなんとかサポートする

Godot Engineに詳しい人を社内で育てる、もしくは外部から詳しい人を雇ったり業務委託するなどが必要になります。

そういった人はトラブルが起きたら、GitHubのIssueを検索したりコミュニティから情報を得たり、 あるいはGodotのソースコードを見て原因を調査したりして問題の対応策を考えます。

基本は社内で技術サポートの担当者を立てて、それでどうしても解決しなかったら社外に頼るというのが良いのではないでしょうか?

突然使えなくなったりしない?

Godotが突然使えなくなることはありえないと言えます。

商用ゲームエンジンのように利益を追求する必要はないし、採算が取れなくなって提供が終了するなんてこともありません。 Godotプロジェクトは企業ではなく、株主もいないのである日買収されて使えなくなるなんてこともありません。

一般的にOSSにも権利者はいます。ライセンスのコピーライトに記載されている人物、組織がそれです。 その権利者がライセンスを突然書き換えることは理論上は可能です。

しかし仮にユーザーの不利益になる方向に書き換えられたとしても、それに不服な人は書き換える前のバージョンを使い続けることもできれば、フォークして派生させることもできるのです。そういった理由もありGodotが突然使えなくなるということはあり得ないと考えられます。

Godotの資金繰りって?

Godotは資金繰りが弱くて心配という話を聞きました。

Godotの資金繰りはhttps://fund.godotengine.orgが寄付を集めて行っています。 Godotプロジェクトはここから開発費を受けていることになります。

GodotはOSSなので、基本的にはボランティアが開発を行っているのですが、コア開発者と呼ばれる中心的な人たちも活動しています。 コア開発者はフルタイム(もしくはパートタイム)で作業を行い、プルリクエストをレビューしたりコーディングを行うこともあります。

寄付の額が増えるとこのコア開発者の人数が増やすことができ、新機能の追加や不具合の修正など開発スピードがアップします。 個人でも会社組織でも、応援するという意味で寄付してみませんか?

fund.godotengine.org

(ここのCreditsに日本のゲーム会社のロゴが並ぶ日を願っています。)

【Godot】Assset Libraryにアセットをリリースしてみた話

Godot Advent Calendar 2023の15日目の記事です。

リリースしたもの

FloatableBodyという水中を物体がそれっぽく浮き上がる簡易的な物理システムです。

MITライセンスで無料です!

3D https://raw.githubusercontent.com/ueshita/godot-floatable-body/main/docs/floatable_3d.gif

2D https://raw.githubusercontent.com/ueshita/godot-floatable-body/main/docs/floatable_2d.gif

Asset Libraryのリンク

godotengine.org

日本語ドキュメント

github.com

デモ

Godot1週間ゲームジャム向けにミニゲームを作りました。 godotplayer.com

Asset Libraryでリリースする

この記事で解説するまでもなく、公式ドキュメントが一番詳しいです。 docs.godotengine.org

英語を書く必要はありますが、得意でなければDeepLでなんとかします。

一応審査はあるようです。提出して1~2日で公開されました。

皆さんも便利なものができたら、ぜひリリースしてみましょう。

【Godot】Godot 4.2のコンピュートシェーダで遊ぼう

Godot Advent Calendar 2023の6日目の記事です。

Godot Engine 4.2がリリースされましたね。めでたい!🎉

2023年は4.0から始まり、3回の大型アップデートがありました。

godotengine.org

さて4.2アップデート内容に気になる項目はありましたか?

個人的には沢山ありますが、中でもコンピュートシェーダのところが目を引いたので解説していきたいと思います。

コンピュートシェーダとは名前の通り計算するシェーダです。GPUで処理するので単純で大量の計算をするような用途が得意です。 例えば万単位の大量のオブジェクトを動かしたり、物理シミュレーション、大きい画像の解析処理などに使われます。

コンピュートシェーダの強化

まず経緯を説明します。 Godot 4.0からコンピュートシェーダは使えるようになったのですが、これをリアルタイムレンダリングで活用するにはイマイチでした。 大量のオブジェクトを更新したり、水面の波動を計算してレンダリングに反映させることが難しかったのです。

Godot 4.2より前

Godot 4.2より前のコンピュートシェーダの使用手順は次の通り。

  • RenderingDeviceを作成
  • バッファなどリソースを作成。CPUからデータを転送する
  • コンピュートシェーダで計算処理。結果はGPUに出力される
  • 結果のGPU側のデータをCPU側に転送する

レンダリングではない用途や事前処理系ならこれでも問題ないのですが、コンピュートシェーダの結果を利用してリアルタイムレンダリングに活用しようとすると、RenderingServerへデータを転送する必要があります。これではCPUとGPU間でデータが毎フレーム行ったり来たりするという無駄が発生してしまい、せっかくのパフォーマンスが落ちてしまいますね。

Godot 4.2以降

Godot 4.2ではコンピュートシェーダについて、新しく次の3つのことができるようになりました。

  • RenderingServer内のRenderingDeviceを利用できる機能
  • コンピュート処理とレンダリングを同期する機能
  • コンピュート出力したテクスチャをレンダリングで使う機能

他のエンジンだと普通にできていることだったりするので若干いまさら感ありますが、これでGodotでもまともにリアルタイムレンダリング用途でコンピュートシェーダを使っていけるようになったということです。

コンピュートシェーダの使い方

基本的にはこちらに書いてあります。(英語)

docs.godotengine.org

…が、ここに書かれている方法は従来の使い方で、コンピュートシェーダの出力をレンダリングで活用することはできません。

2023年12月現在、公式ドキュメントは追いついていなさそうです。そこでBastiaanOlij氏が作った公式のデモを見てみましょう。

github.com

実行してみます。画面をクリックすると波が発生して広がっていくデモでした。

デモの中身を見てみる

こんな感じ(エディタ上で波が起きている…)

デモのコードを見る前に、水面を波が伝わるやり方について簡単に解説します。下の図のように特定の地点の水面の高さをグリッド的に配置しておき、毎フレーム隣接している水面の高さに影響を与えていけばそれっぽい挙動になります。

さてコードはres://water_plane/water_plane.gdなので見てみましょう。コメントは意訳しています。

コンピュートシェーダとバッファの初期化

まず_ready()を見てみます。

var texture : Texture2DRD
var next_texture : int = 0

func _ready():
    # レンダリングスレッドで`_initialize_compute_code`を呼び出す
    RenderingServer.call_on_render_thread(_initialize_compute_code.bind(texture_size))

    # 子のMeshInstance3Dノードからマテリアルを取得
    var material : ShaderMaterial = $MeshInstance3D.material_override
    if material:
        # マテリアルにテクスチャサイズを指定
        material.set_shader_parameter("effect_texture_size", texture_size)
        # マテリアルからTexture2DRDを取得
        texture = material.get_shader_parameter("effect_texture")

RenderingServer.call_on_render_threadはGodot 4.2から使用できる新しいAPIです。 コンピュートシェーダの初期化処理はレンダリングスレッドで行わせるため、このAPI_initialize_compute_codeを登録しています。

また、マテリアルからTexture2DRDを取得しています。ここはあらかじめインスペクタからTexture2DRDが指定されていたものです。別にTexture2DRD.new()で新しく作ってmaterial.set_shader_parameterでもいいと思います。

次に_initialize_compute_codeを含むコンピュートシェーダの初期化周りのコードを見てみます。

var rd : RenderingDevice

var shader : RID  # コンピュートシェーダ
var pipeline : RID  # コンピュートパイプライン

# 波の高さを表すバッファとして使うテクスチャ
# - 1つは現在レンダリングするフレームで使われる
# - 1つは前回レンダリングされたフレームで使われたもの
# - 1つは前々回レンダリングのフレームで使われたもの
var texture_rds : Array = [ RID(), RID(), RID() ]
var texture_sets : Array = [ RID(), RID(), RID() ]

func _create_uniform_set(texture_rd : RID) -> RID:
    var uniform := RDUniform.new()
    uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
    uniform.binding = 0
    uniform.add_id(texture_rd)
    # Even though we're using 3 sets, they are identical, so we're kinda cheating.
    return rd.uniform_set_create([uniform], shader, 0)

func _initialize_compute_code(init_with_texture_size):
    # レンダリングフレーム中にレンダリングスレッドから呼ばれる
    # RenderingServerからRenderingDeviceを借りてくる
    rd = RenderingServer.get_rendering_device()

    # コンピュートシェーダを作成
    var shader_file = load("res://water_plane/water_compute.glsl")
    var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
    shader = rd.shader_create_from_spirv(shader_spirv)
    pipeline = rd.compute_pipeline_create(shader)

    # 波の高さを表すバッファのテクスチャを作成
    var tf : RDTextureFormat = RDTextureFormat.new()
    tf.format = RenderingDevice.DATA_FORMAT_R32_SFLOAT
    tf.texture_type = RenderingDevice.TEXTURE_TYPE_2D
    tf.width = init_with_texture_size.x
    tf.height = init_with_texture_size.y
    tf.depth = 1
    tf.array_layers = 1
    tf.mipmaps = 1
    tf.usage_bits = RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT + RenderingDevice.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT + RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_COPY_TO_BIT

    for i in range(3):
        # RDテクスチャを作成してクリア
        texture_rds[i] = rd.texture_create(tf, RDTextureView.new(), [])
        rd.texture_clear(texture_rds[i], Color(0, 0, 0, 0), 0, 1, 0, 1)
        # テクスチャを参照するUniformSetを作成
        texture_sets[i] = _create_uniform_set(texture_rds[i])

コンピュートシェーダ

本体となるコンピュートシェーダ(water_plane.glsl)のコードがこちらです。

#[compute]
#version 450

// 8x8単位で処理する
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

// 現在のバッファ、前回のバッファ、出力用のバッファ
layout(r32f, set = 0, binding = 0) uniform restrict readonly image2D current_image;
layout(r32f, set = 1, binding = 0) uniform restrict readonly image2D previous_image;
layout(r32f, set = 2, binding = 0) uniform restrict writeonly image2D output_image;

// 制御用の情報
layout(push_constant, std430) uniform Params {
    vec4 add_wave_point;
    vec2 texture_size;
    float damp;
    float res2;
} params;

void main() {
    ivec2 tl = ivec2(0, 0);
    ivec2 size = ivec2(params.texture_size.x - 1, params.texture_size.y - 1);
    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);

    // 領域外の読み書きをしないようにする処理
    if ((uv.x > size.x) || (uv.y > size.y)) {
        return;
    }

    // バッファから読み込む
    float current_v = imageLoad(current_image, uv).r;
    float up_v = imageLoad(current_image, clamp(uv - ivec2(0, 1), tl, size)).r;
    float down_v = imageLoad(current_image, clamp(uv + ivec2(0, 1), tl, size)).r;
    float left_v = imageLoad(current_image, clamp(uv - ivec2(1, 0), tl, size)).r;
    float right_v = imageLoad(current_image, clamp(uv + ivec2(1, 0), tl, size)).r;
    float previous_v = imageLoad(previous_image, uv).r;

    // 前回の波の高さと周囲の波の高さから、新しい波の高さを計算する
    float new_v = 2.0 * current_v - previous_v + 0.25 * (up_v + down_v + left_v + right_v - 4.0 * current_v);
    new_v = new_v - (params.damp * new_v * 0.001);

    // 新しい衝撃を加える
    if (params.add_wave_point.z > 0.0 && uv.x == floor(params.add_wave_point.x) && uv.y == floor(params.add_wave_point.y)) {
        new_v = params.add_wave_point.z;
    }

    if (new_v < 0.0) {
        new_v = 0.0;
    }
    vec4 result = vec4(new_v, new_v, new_v, 1.0);

    // 結果をバッファに書き込む
    imageStore(output_image, uv, result);
}

コンピュートシェーダの拡張子はglslです。なぜか現時点でGodotのエディタでは開くことができないです。(ナンデ?) コンパイルが成功したということは教えてくれますが、編集するにはVSCodeなど別のエディタを使いましょう。

コンピュート処理の実行とバッファの更新

さて実際にコンピュートシェーダを実行するコード周りを見ていきましょう。

func _process(delta):
    # 今回使用するテクスチャの番号を更新する
    next_texture = (next_texture + 1) % 3

    # 描画に使用するテクスチャを更新する
    # `_initialize_compute_code` はまだ実行されていないかもしれないので、最初のフレームは空のRIDになることに注意
    if texture:
        texture.texture_rd_rid = texture_rds[next_texture]

    # `render_process`をレンダリングスレッドで実行する
    # `_render_process`は並列に実行される可能性があるため、必要なパラメータを引数として渡しています
    RenderingServer.call_on_render_thread(_render_process.bind(next_texture, add_wave_point, texture_size, damp))

_render_processはおそらく描画前にレンダリングスレッドで呼ばれるため、描画と同期してコンピュートシェーダは実行されます。

func _render_process(with_next_texture, wave_point, tex_size, damp):
    # シェーダ内で使用している構造体を作る
    var push_constant : PackedFloat32Array = PackedFloat32Array()
    push_constant.push_back(wave_point.x)
    push_constant.push_back(wave_point.y)
    push_constant.push_back(wave_point.z)
    push_constant.push_back(wave_point.w)

    push_constant.push_back(tex_size.x)
    push_constant.push_back(tex_size.y)
    push_constant.push_back(damp)
    push_constant.push_back(0.0)

    # Dispatchグループのサイズを計算
    # シェーダ内でlocal_size_x,local_size_yの数値で割る
    var x_groups = (tex_size.x - 1) / 8 + 1
    var y_groups = (tex_size.y - 1) / 8 + 1

    var next_set = texture_sets[with_next_texture]
    var current_set = texture_sets[(with_next_texture - 1) % 3]
    var previous_set = texture_sets[(with_next_texture - 2) % 3]

    # シェーダ内部で使用するUniformSetを指定してコンピュートシェーダを実行
    var compute_list := rd.compute_list_begin()
    rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
    rd.compute_list_bind_uniform_set(compute_list, current_set, 0)
    rd.compute_list_bind_uniform_set(compute_list, previous_set, 1)
    rd.compute_list_bind_uniform_set(compute_list, next_set, 2)
    rd.compute_list_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4)
    rd.compute_list_dispatch(compute_list, x_groups, y_groups, 1)
    rd.compute_list_end()

    # 今回は同期のためのバリアは必要ない(Godotが描画で使う際に自動的に内部のバリアが使われるため)
    # ただし、コンピュートシェーダーの出力を別のコンピュートシェーダーの入力として使いたい場合は、バリアを追加する必要がある
    #rd.barrier(RenderingDevice.BARRIER_MASK_COMPUTE)

コンピュートシェーダとバッファの破棄

コンピュートシェーダのリソースの破棄は手動で行う必要があります。そしてまたレンダリングスレッドで破棄する必要があります。少し面倒ですね。

func _exit_tree():
    # 描画用のテクスチャに無効なRIDを指定して参照されないようにする
    if texture:
        texture.texture_rd_rid = RID()

    # レンダリングスレッドで`_free_compute_resources`を呼び出す
    RenderingServer.call_on_render_thread(_free_compute_resources)

func _free_compute_resources():
    # バッファ用テクスチャを破棄 (UniformSetは自動的に解放されるらしい)
    for i in range(3):
        if texture_rds[i]:
            rd.free_rid(texture_rds[i])

    # コンピュートシェーダを破棄 (Pipelineは自動的に解放されるらしい)
    if shader:
        rd.free_rid(shader)

コンピュート結果をレンダリングで使う

コンピュートシェーダで計算した波の高さを水面のレンダリングで活用します。 シェーダソースはwater_shader.gdshaderです。これは普通のGodotシェーダですね。

shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;

uniform vec3 albedo : source_color;
uniform float metalic : hint_range(0.0, 1.0, 0.1) = 0.8;
uniform float roughness : hint_range(0.0, 1.0, 0.1) = 0.2;
uniform sampler2D effect_texture;  // コンピュート結果のバッファをテクスチャとして使う
uniform vec2 effect_texture_size;  // コンピュート結果のバッファのテクスチャサイズ

varying vec2 uv_tangent;
varying vec2 uv_binormal;

void vertex() {
    vec2 pixel_size = vec2(1.0, 1.0) / effect_texture_size;

    uv_tangent = UV + vec2(pixel_size.x, 0.0);
    uv_binormal = UV + vec2(0.0, pixel_size.y);
}

void fragment() {
    // その場所の波の高さと、隣接の波の高さを読み込む
    float f1 = texture(effect_texture, UV).r;
    float f2 = texture(effect_texture, uv_tangent).r;
    float f3 = texture(effect_texture, uv_binormal).r;

    // 波の高さを水面の傾きとして法線を計算(ライティングはGodotにおまかせ)
    vec3 tangent = normalize(vec3(1.0, 0.0, f2 - f1));
    vec3 binormal = normalize(vec3(0.0, 1.0, f3 - f1));
    NORMAL_MAP = normalize(cross(binormal, tangent)) * 0.5 + 0.5;

    ALBEDO = albedo.rgb;
    METALLIC = metalic;
    ROUGHNESS = roughness;
    SPECULAR = 0.5;
}

まとめ

Godot 4.2以降のコンピュート処理の結果をレンダリングで使う方法のまとめです

  • コンピュート関連の初期化、更新処理、終了処理はRenderingServer.call_on_render_threadレンダリングスレッドから実行する
  • コンピュート処理を行うためのRenderingDeviceはRenderingServer.get_rendering_device()から取得する
  • コンピュート結果のテクスチャはTexture2DRDtexture_rd_ridにセットする

草を靡かせてみる

ここからは応用編です。

こちらの記事を参考に。Godotで似たようなことをしてみましょう。

nagakagachi.hatenablog.com

完成したものがこちら

youtu.be

GitHubでGodotプロジェクト一式を公開しています。

github.com

完成物を少し解説

草の動き(コンピュート処理)

基本的に波を更新する部分は元と同じですが、そのままだと草がプルプルしすぎてキモイのでもう少し自然に動くように調整しています。

  • 動きの加速度を調整して草がゆっくりと動くようにする
  • 草の動きはそこまで広がらないので減衰しやすくする
  • プレイヤーのコライダーで継続的に力が加わるようにする

草のレンダリング

  • 草のレンダリングはMeshMultiInstance3Dを使用
  • 草1本の三角錐のメッシュを大量にインスタンス描画する
  • コンピュート結果のテクスチャを頂点シェーダで使い草に動きを付ける

草1本のベースメッシュを生成して、MultiMeshのインスタンスを量産するコードは次の通りです。

func _initialize_mesh() -> void:
    # 草1本を表現する三角錐の細長いメッシュを作る
    var st = SurfaceTool.new()
    st.begin(Mesh.PRIMITIVE_TRIANGLES)

    for i in range(3):
        var t1 := i * PI * 2 / 3
        var t2 := (i + 1) * PI * 2 / 3
        var v0 := Vector3(0.0, 1.0, 0.0)
        var v1 := Vector3(cos(t1), 0.0, sin(t1)) * 0.04
        var v2 := Vector3(cos(t2), 0.0, sin(t2)) * 0.04
        st.set_normal((v2 - v0).normalized().cross((v1 - v0).normalized()))
        st.add_vertex(v0)
        st.add_vertex(v1)
        st.add_vertex(v2)

    instance.multimesh.mesh = st.commit()

    # Area3D内を埋め尽くす草のインスタンスを作成する
    var box_shape: BoxShape3D = shape
    var area_size := box_shape.size
    var area_step := Vector3(area_size.x / count_x, 0.0, area_size.z / count_z)
    var area_offset := -area_size / 2 + Vector3(area_step.x * 0.5, 0.0, area_step.z * 0.5)

    instance.multimesh.instance_count = count_x * count_z
    for z in range(count_z):
        for x in range(count_z):
            var index := x + z * count_x
            # 草1本のトランスフォームを作成
            var xform := Transform3D()
            # 草が生える位置の中心を計算
            xform.origin = area_offset + Vector3(x * area_size.x / count_x, 0.0, z * area_size.z / count_z)
            # ランダムで草が生える位置を散らす
            xform.origin += area_step * Vector3(randf_range(-0.5, 0.5), 0.5, randf_range(-0.5, 0.5))
            # ランダムで草の長さを散らす
            xform.basis = xform.basis.scaled(Vector3.ONE * randf_range(0.5, 0.8))
            # ランダムで草が生える角度を散らす
            xform.basis *= Basis.from_euler(Vector3(randf_range(-PI/16, PI/16), randf_range(-PI, PI), randf_range(-PI/16, PI/16)), EULER_ORDER_YXZ)
            # インスタンスに作成したトランスフォームを指定
            instance.multimesh.set_instance_transform(index, xform)

まとめ

コンピュートシェーダ楽しい ✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌

草の実装について、YouTubeを探すと詳しい動画がいくつか見つかります。 www.youtube.com

【Godot】EffekseerプラグインをGodot4に対応しました【Effekseer】

はじめに

リリース自体は結構前に行っていたのですが、いろいろ忙しくて告知をしていないことに気づきまして…。 改めて告知したいと思います。

Effekseerって?

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

https://effekseer.github.io

Effekseerサイトトップ

現在のメジャーバージョンは1.7です。

www.youtube.com

新機能のトリガーは再生中のエフェクトに変化を加えられるのでオススメです。

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

ダウンロードページ

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

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

プラグインの使い方

https://effekseer.github.io/Help_Godot/v4/ja/index.html を見てくださいね。

バグを見つけたら

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

Godot4で変わったところ

  • 【3D】高度描画パネルが使えるようになりました。Godot3では難しかった機能です!
  • 【3D】モデルの描画にインスタンシングを行うようになりました。高速化です!
  • 【3D/2D】エディタビューポートでのプレビュー機能を追加しました。

プラグイン開発

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

Godot3ではGDNativeでしたが、Godot4はGDExtensionという方法に変わりました。 そのうち詳しく解説したいと思います。

レンダリングの仕組みなどは基本的にはGodot3版と同じです(細かいところは少し違います) ueshita.hatenablog.jp

【Godot】Godot4のGDScriptはどれくらい速くなったのか?

Godot Engine Advent Calendar 2022 7日目の記事になります。

結論

Godot4はGodot3と比較して、GDScriptはだいぶ速くなりました。

解説

参考URL

だいたいこちらに書かれています。

https://godotengine.org/article/gdscript-progress-report-typed-instructions

要約

上記の記事を要約しますと、GDScriptの実行エンジンに型付けされた命令を実装されましたということです。

次の処理内容が高速化されたとのことです。

高速化内 高速化率
計算命令 (+や&など) 約25-50%
配列の添え字(array[index])アクセス 約5-7%
組込型の関数(array.resize(n)等)呼び出し 約70%
ネイティブクラスの関数の呼び出し 約120-150%
組込み関数(absやrandf等)の呼び出し 約25〜50%
イテレーション(for-inループ) 約10~50%

詳細な解説

GDScriptエンジンに型付けされた命令が実装されると何が嬉しいかを解説します。

例えばこのようなGDScriptコードがあったとします。

func add(a: int, b: int) -> int:
    return a + b

変数(引数)aと'b'はint型と明示されていますが、Godot3ではコンパイル時にチェックが走るだけで、実行時は動的型の変数として処理されていました。 Godot4ではこれらが実行時もint型の変数として確定した前提で処理されるようになります。

上記では+演算子の処理内容が次のように異なってきます。

Godot3 Godot4
  1. `a`の型をチェック -> int型
  2. `b`の型をチェック -> int型
  3. int型の加算処理を実行
  1. int型の加算処理を実行

実行時に型チェックが不要になるので、これは速くなりそうですね!

また関数の呼び出し側も明示的に型付けされていれば、引数の型チェックが不要になるためこれも速くなるとのことです。

実測

本当に速くなったのか、実際に計測してみました。 (参考記事も2年前ですし…)

計測環境

計測マシン

使用エンジン

  • Godot3: Godot_v3.5.1-stable_win64.exe
  • Godot4: Godot_v4.0-beta7_win64.exe

計測結果

種類 Godot3.5.1 Godot4.0.beta7 高速化率
int演算 52580 30462 72.608
float演算 48105 27786 73.127
配列アクセス 51241 42845 19.596
組込み関数 130234 99035 31.503
組込み型の関数 78387 54279 44.415
ネイティブクラスの関数 169966 25432 568.316
スクリプトの関数 308276 129272 138.471
スクリプトクラスの関数 206974 95454 116.831
フィボナッチ数列の生成 1323376 633049 109.048

ネイティブクラスの関数呼び出しがえぐいくらい速くなっていることを除いて、前述の高速化内容と概ね一致してそうです。

計測コード

https://gist.github.com/ueshita/7d60a47574113c8e5c1b262f36726a9a

追記 (2023/1/13)

Godot4.0.beta11がリリースされました。 アップデート内容にGDScript関連の多くの改善と最適化が入ったようです。

Many improvements, fixes, and optimizations have been done to GDScript and its runtime environment (GH-62688, GH-64253, GH-69590, GH-69991, GH-70246, GH-70464, GH-70613, GH-70655, GH-70658, GH-70702, GH-70838, GH-70859, GH-71051, GH-71107, and more).

というわけで、同条件でベンチマークを取り直してみました。

種類 Godot4.0.beta7 Godot4.0.beta11 高速化率
int演算 30462 23086 131.95
float演算 27786 17040 163.06
配列アクセス 42845 39787 107.69
組込み関数 99035 81489 121.53
組込み型の関数 54279 53841 100.81
ネイティブクラスの関数 25432 23369 108.83
スクリプトの関数 129272 130316 99.20
スクリプトクラスの関数 206974 92584 103.10
フィボナッチ数列の生成 1323376 661419 95.71

【Godot】エンジンをデバッグビルドしてみよう

Godot Engine Advent Calendar 2022 3日目の記事になります。

はじめに

Godot Engineはオープンソースで開発されているため、GitHubに行けばソースコードにアクセスし放題です。 そこでソースコードを眺めているのも楽しいですが、ちょっといじってみたいと思うときもあると思います。

例えば次のような理由です。

  • Godot Engineのお気持ちをもっと知りたい
  • Godot Engineのバグを調査したい
  • Godot Engineの開発に貢献したい

これらを行うには、エンジンをビルドしてデバッグ実行できるようにする必要がありますね。

本記事はGodot Engineのデバッグビルドをして、デバッグ実行するところまで紹介したいと思います。

基本的には公式ドキュメントに書かれています。より詳しく知りたい時はそちらを参照してもらえればと思います。

環境準備

今回はWindowsVisual Studioを使うことを前提とします。

エンジンをビルドするには次のソフトのインストールが必要です。

インストール済みの場合はスキップしてください。

ある程度の経験者向けのザックリ解説になっているため、不明点などありましたら逐次ググって対処するようお願いします。

Gitのインストール

GitはGitHubからソースコードを入手するために使用します。 今回はCUIで説明するため、Git for Windowsをインストールします。 GUIツールが良ければ、ForkやSourceTreeでも構いません。

Visual Studioのインストール

もし仕事でしたら会社にVisual Studio Professionalを用意してもらいましょう。

個人なら無料でVisual Studio Communityをインストールして使うことができます。

https://visualstudio.microsoft.com/ja/vs/

Pythonのインストール

Godot EngineのビルドするためにSConsと呼ばれるビルドツールが必要です。 SConsをインストールする前にPythonをインストールする必要があります。

https://www.python.org

インストーラでインストールすると、pythonへのPATHがセットされて、どこでもpythonコマンドが使えるようになると思います。

python
Python 3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

Sconsのインストール

Pythonをインストールすると付いてくるpipコマンドを使用してSConsをインストールします。

コマンドプロンプトでインストールを行います。

pip install scons

上手くいくと次のメッセージが出力されると思います。

Collecting scons
  Using cached SCons-4.4.0-py3-none-any.whl (4.2 MB)
Requirement already satisfied: setuptools in c:\devel\python\lib\site-packages (from scons) (65.5.0)
Installing collected packages: scons
Successfully installed scons-4.4.0

Godotのビルド

ビルド手順はGodot3系とGodot4系で微妙に違うため、それぞれ解説したいと思います。

Godot3編

ソースコードを入手する

現在Godot3系は3.xブランチで開発されています。 そのままgit cloneするとGodot4系のmasterブランチが取れてしまいます。(後でブランチを切り替えればいい話ではありますが…)

そこで今回は、3.xブランチを指定してcloneします。

git clone https://github.com/godotengine/godot.git -b 3.x

また、3.5ブランチ、3.4ブランチなども存在しているので、既存のエンジンバージョンに合わせてソースコードを取得することもできます。

エンジンをデバッグビルドする

git cloneして作成されたgodotフォルダに移動してsconsコマンドでビルドします。

cd godot
scons platform=windows vsproj=yes

vsproj=yesを追加するとVisual Studioプロジェクトファイルを作ってくれるため、ぜひ付けましょう。

上手くいけばgodot/binフォルダ内にgodot.windows.tools.64.exeが出力されていると思います。

Godot4編

ソースコードを入手する

現在Godot4系はmasterブランチで開発されています。 そのままgit cloneすればOKです。

git clone https://github.com/godotengine/godot.git

エンジンをデバッグビルドする

Godot3版と違うところは、デバッグビルドすることを示すdev_build=yesを付けることです。

cd godot
scons platform=windows vsproj=yes dev_build=yes

vsproj=yesを追加するとVisual Studioプロジェクトファイルを作ってくれるため、ぜひ付けましょう。

上手くいけばgodot/binフォルダ内にgodot.windows.editor.dev.x86_64.exeが出力されていると思います。

Godotのデバッグ実行

vsproj=yesを付けてsconsすると、godot.slnが作成されています。

こちらをVisual Studioで開いてみましょう。

この状態でデバッグ実行することはできますが、プロジェクトランチャーが起動するだけで、プロジェクトを開いているときのエディタやゲーム実行時のデバッグはできません。(プロジェクトを開いたりゲーム実行するのは別プロセスになるため)

それらを行うにはデバッグオプションのコマンド引数を設定する必要があります。

まずソリューションエクスプローラのgodotを右クリック → プロパティ で次のダイアログが開きます。 コマンド引数のところを変更します。

エディタをデバッグ実行する

プロジェクトを開いたエディタをデバッグするには、次のコマンド引数を設定します。

-e D:\MyProj\project.godot

エディタがデバッグ実行できています。

Visual Studioからブレークポイントを貼ってブレークできることを確認します。

ゲームをデバッグ実行する

プロジェクトのゲーム実行しながらエンジンをデバッグするには、次のコマンド引数を設定します。

--path D:\MyProj

project.godotは付けません。

メインシーンが実行されます。

任意のシーン実行したい時は、次のコマンド引数を指定します。

--path D:\MyProj scene.tscn

その他オプションを使うときは、こちらを参照してください。

まとめ

今回は次の内容を紹介しました。

  • Godot 3.xとGodot 4.xのビルド
  • プロジェクトを開いたエディタのデバッグ
  • ゲーム実行時のエンジンのデバッグ

Godot Engineはオープンソースで活発に開発が進められている、現在勢いのあるゲームエンジンです。

比較的簡単にソースコードを見ていじれる。手元で動かして試せるというエンジンです。

バグを直してGitHubでPull Requestすることもできれば、機能追加して提案することもできます。 もし開発者にMergeされればそれはエンジンの一部になり、あなたはコントリビュータの一員になります。

そんな一歩に繋がる?かもしれない記事でした。