【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されればそれはエンジンの一部になり、あなたはコントリビュータの一員になります。

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

【Godot】ノイズテクスチャから毒の沼を作る

やること

こちらの動画のテクニックをGodotで実装したいと思います。

手順

ノイズテクスチャを用意

動画ではPhotoshopの雲模様フィルタでノイズテクスチャを作成するとありますが、Godotにはノイズテクスチャを生成する機能があります。超便利!

ファイルシステム → フォルダ → 新規リソース から

f:id:ueshita:20220208100118p:plain
ノイズテクスチャ作成

インスペクターからノイズテクスチャにOpenSimplexNoiseをセットします。 Seamlessにもチェックを入れておきます。

f:id:ueshita:20220208220532p:plain

スプライトにノイズテクスチャを設定してみました。

f:id:ueshita:20220208220710p:plain

シェーダを書く

shader_type canvas_item;
render_mode unshaded;

uniform sampler2D gradient;
uniform vec2 size = vec2(512, 512);

void fragment() {
    vec2 uv = UV * size / vec2(512, 512);
    float t0 = texture(TEXTURE, uv).r;
    float t1 = texture(TEXTURE, uv - vec2(TIME * 0.025)).r;
    float t2 = texture(TEXTURE, uv + vec2(TIME * 0.025)).r;
    float t12 = (t1 + t2);
    float offset = (t0 + t12) * 0.5;
    float t3 = texture(TEXTURE, vec2(offset)).r;
    float contrast = t3 * t12;

    vec3 color = texture(gradient, vec2(contrast, 0.0)).rgb;
    COLOR = vec4(color, 1.0);
}

スプライトにマテリアルをセットする

SpriteのMaterialにShaderMaterialをセットし、作成したシェーダをセットします。 これだけでは真っ暗なのでShader Paramを開き、GradientにGradientTextureをセット、さらに内部のGradientにグラデーションパラメータをセットします。

f:id:ueshita:20220209004435p:plain

結果

こんな感じになりました。毒々しい!

f:id:ueshita:20220208222752g:plain

3D対応

3Dもいけます。

f:id:ueshita:20220209010048g:plain

シェーダ

Spatialシェーダ版です。 ライティングをONにしています。

shader_type spatial;

uniform sampler2D noise;
uniform sampler2D gradient;
uniform vec2 size = vec2(512, 512);

void fragment() {
    vec2 uv = UV * size / vec2(512, 512);
    float t0 = texture(noise, uv).r;
    float t1 = texture(noise, uv - vec2(TIME * 0.025)).r;
    float t2 = texture(noise, uv + vec2(TIME * 0.025)).r;
    float t12 = (t1 + t2);
    float offset = (t0 + t12) * 0.5;
    float t3 = texture(noise, vec2(offset)).r;
    float contrast = t3 * t12;

    ALBEDO = texture(gradient, vec2(contrast, 0.0)).rgb;
}

まとめ

シェーダ楽しい✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌

【Godot】Editor拡張する (Inspector Plugin編)

みなさんEditor拡張沼に浸かっていますか?
ゲームエンジンと言ったらEditor拡張ですよね。

Godot Engineも例に漏れず、強力なEditor拡張が行えます。
↓こんなこともできました。

今回は↑のようなインスペクタ拡張を行う方法を紹介したいと思います。

公式の参考ページ

docs.godotengine.org

キャラを増やすボタンをインスペクタに置く

ボタンを押してキャラを増やすシンプルなEditor拡張を行います。

f:id:ueshita:20211001212428g:plain

1. プラグインの体裁を整える

インスペクタを拡張するのでInspector Pluginを作成します。
Inspector PluginをGodotエディタで実行するためには、Godotプラグインの体裁を整える必要があります。
今回はMyPluginという名前で作りました。

まずプロジェクトルートから次のディレクトリを掘ります。

  • res://addons/MyPlugin/

ここに次のファイルを追加します。

  • plugin.cfg
[plugin]

name="MyPlugin"
description="A fantastic editor plugin."
author=""
version="1.0.0"
script="plugin.gd"
  • plugin.gd
tool
extends EditorPlugin

func _enter_tree():
    pass

func _exit_tree():
    pass

そうすると、プロジェクト設定のプラグイン一覧に追加されるので 有効 にチェックを入れます。

f:id:ueshita:20211001214635p:plain

2. Inspector Pluginを作成する

Inspector Pluginのスクリプトを書きます。
今回はインスペクタにカスタムコントロールとしてボタンを追加して、ボタンが押されたらキャラをランダムに配置するようにしました。

  • MyInspectorPlugin.gd
tool
extends EditorInspectorPlugin
class_name MyInspectorPlugin

var target: MyCharactor

# 選択されたノード、ファイルがこのスクリプトの対象かどうか判定する
func can_handle(object: Object) -> bool:
    # オブジェクトのクラスがMyCharactorだったら対象とする
    return object is MyCharactor

# インスペクタに表示されるときに1回呼ばれる
func parse_begin(object: Object) -> void:
    # インスペクターに表示しているオブジェクトを保存
    target = object
    # ボタンを作る
    var button = Button.new()
    button.text = "押すと増える"
    # イベント登録
    button.connect("pressed", self, "_button_pressed")
    # UIのカスタムコントロールとして追加
    add_custom_control(button)

# ボタンが押されると呼ばれる
func _button_pressed():
    # エディタのルートノード
    var root = target.get_tree().edited_scene_root
    # キャラをロードしてインスタンス化する
    var chara = load("res://models/Dman.fbx").instance() as Spatial
    # 位置をランダムに配置する
    chara.transform.origin = Vector3(rand_range(-3, 3), rand_range(-3, 3), rand_range(-3, 3))
    # ルートの子として追加
    root.add_child(chara)
    # キャラのオーナーを設定 (Editorでは必要)
    chara.set_owner(root)
    print("増えたぞ")

今回、Insupector Pluginの対象判定のためにクラス型を使っているため、対象になる空のクラスを作りました。

  • MyCharactor.gd
extends Node
class_name MyCharactor

3. 動作確認する

f:id:ueshita:20211001220914p:plain

適当なノードに先ほどの空クラスのスクリプトをアタッチしています。
このノードをクリックするとインスペクタにボタンが追加されていると思います。

このボタンを押しまくると…

f:id:ueshita:20211001221106p:plain

D言語くんが大量増殖しました!

なんか上手くいかないときは、プラグインの有効チェックを外して再度有効にするとリロードが行われます。

インスペクタで3Dレンダリングを行う

EditorInspectorPluginのadd_custom_controlにはControlノードを突っ込めるため、ぶっちゃけ何でもできます。
例えばノードやアセットの3Dプレビュー機能が欲しかったりしたら、ViewportContainerを使って3Dレンダリングを行うこともできます。

f:id:ueshita:20211001223600p:plain

1. Controlシーンを作る

今回はレスポンシブルにするため、ルートノードの型をVBoxContainerにしました。

f:id:ueshita:20211001224349p:plain

ViewportContainer

f:id:ueshita:20211001224645p:plain

  • Stretchにチェックを入れる
  • Min SizeのYを設定しないと、Viewportがゼロサイズになってしまうので適当な値を入れる

Viewport

f:id:ueshita:20211001224836p:plain

  • Own Worldにチェックを入れる

3Dシーン

Camera, DirectionalLight, 適当なキャラを配置します。

2. スクリプトを作る

CharaPreview.gd

動かないのは面白くないので、tool属性を付けたスクリプトをキャラにアタッチしました。

tool
extends Spatial

func _process(delta: float):
    transform = transform.rotated(Vector3.FORWARD, deg2rad(360.0) * delta)

MyInspectorPlugin.gd

作成したシーンをロードしてadd_custom_controlに突っ込むスクリプトを書きます。

tool
extends EditorInspectorPlugin
class_name MyInspectorPlugin

var target: MyCharactor
var scene: Control

# 選択されたノード、ファイルがこのスクリプトの対象かどうか判定する
func can_handle(object: Object) -> bool:
    # オブジェクトのクラスがMyCharactorだったら対象とする
    return object is MyCharactor

# インスペクタに表示されるときに1回呼ばれる
func parse_begin(object: Object) -> void:
    # インスペクターに表示しているオブジェクトを保存
    target = object
    # シーンを追加する
    scene = load("res://addons/MyPlugin/MyInspectorPlugin.tscn").instance()
    # ボタン押下のイベント登録
    #scene.get_node("Button").connect("pressed", self, "_button_pressed")
    # UIのカスタムコントロールとして追加
    add_custom_control(scene)

3. 動作確認する

インスペクタで荒ぶるD言語くんが見れました。

f:id:ueshita:20211001223453g:plain

おわりに

Godot EngineではInspector Pluginでなんでも出来ることが分かりました。
この調子でゲーム制作がどんどん効率化できそうです。

しかし、効率よくゲームを作るためEditor拡張していたら夢中になってしまい、いつまで経ってもゲームが出来上がらないなんてことは避けましょうね。

おまけ

Effekseerプラグインに実例があるので、よかったらご参考ください。 https://github.com/effekseer/EffekseerForGodot3/tree/main/Dev/Godot/addons/effekseer