Godot GDEXTENSION の WEB エクスポート(その5)

Godot

opencv を使った GDEXTENSION サンプルを WEB エクスポートする

本稿は GDEXTENSION の WEB エクスポートを試した際のメモの共有を意図した一連の記事の5番目で最後。1番目から順に読まれていくことを想定している。

対象としている Godot エンジンのバージョンは 4.2.1。 emscripten のバージョンは 3.1.39。

前稿では、emscripten 向けに boost や Eigen3 といった汎用ライブラリ、vtk, pcl, opencv といった大物 C++ ライブラリのビルドを行った。ただし実際に WEB 上で動作するかは試していない。本稿では opencv を例にとり、 Windows ディスクトップと WEB(HTML5)をターゲットに GDEXTENSION サンプルをビルドする。

GDEXTENSION サンプルとしては、Lenna さんの画像に時間に従って強さが増加するガウシアンブラーをかけてゆく。

ソースコード

“C:\dev\gdextension_sample\sample_02” というディレクトリに GDEXTENSION のサンプルを作成することとする。設置ファイルツリー構造は以下の4つ。

  • gdexample.cpp
  • gdexample.h
  • register_types.cpp
  • register_types.h

各ソースコードファイルの内容は以下とする。

gdexample.h

#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/image_texture.hpp>
#include <opencv2/opencv.hpp>

namespace godot {

class CvBlur : public TextureRect {
    GDCLASS(CvBlur, TextureRect);
    PackedByteArray rawData;
    Ref<Image> image;
    cv::Mat img_org;
	double time_passed;
protected:
    static void _bind_methods();
public:
    CvBlur();
    ~CvBlur();
    void _process(double delta);
    int openImage(const String &p_file);
};

}

#endif

gdexample.cpp

#include "gdexample.h"
#include <godot_cpp/core/class_db.hpp>

#ifdef DEBUG_ENABLED
#include <godot_cpp/variant/utility_functions.hpp> // for "UtilityFunctions::print()".
#endif // DEBUG_ENABLED

#include <filesystem>

using namespace godot;

void CvBlur::_bind_methods()
{
    ClassDB::bind_method(D_METHOD("open_movie", "file_path"), &CvBlur::openImage);
}

CvBlur::CvBlur()
{
	time_passed = 0;
}

CvBlur::~CvBlur()
{
}

int CvBlur::openImage(const String &file_path)
{
#ifdef DEBUG_ENABLED
	UtilityFunctions::print("input strings: ",file_path.utf8().ptr());
#endif // DEBUG_ENABLED

    if (!std::filesystem::is_regular_file(file_path.utf8().ptr())) {

#ifdef DEBUG_ENABLED
        UtilityFunctions::print("%s is not file",std::filesystem::current_path().c_str());
#endif // DEBUG_ENABLED

        return 0;
    }
    cv::Mat img = cv::imread(file_path.utf8().ptr(), -1);
    if(img.empty()) {
#ifdef DEBUG_ENABLED
		UtilityFunctions::print("img.empty(): %s",std::filesystem::current_path().c_str());
#endif // DEBUG_ENABLED
        return -1;
    }
    cv::resize(img, img_org, cv::Size(), 0.5, 0.5);

    rawData.resize(img_org.cols * img_org.rows * 3);
    image = Image::create_from_data(img_org.cols, img_org.rows, false, Image::FORMAT_RGB8, rawData);

    cv::cvtColor(img_org, img_org, cv::COLOR_BGR2RGB);
    memcpy(rawData.ptrw(), (unsigned char *)img_org.data, img_org.cols * img_org.rows * 3);
    image->set_data(img_org.cols, img_org.rows, false, Image::FORMAT_RGB8, rawData);
    set_texture(ImageTexture::create_from_image(image));

    return 1;
}

void CvBlur::_process(double delta) {
    time_passed += delta;
	double sigma = fmod(time_passed, 20.0);
	if (sigma == 0.0)sigma = 0.000001;
	
    if(img_org.empty()) {
        return;
    }
	
    cv::Mat img;
	cv::GaussianBlur(img_org, img, cv::Size(15,15), sigma, sigma);

	memcpy(rawData.ptrw(), (unsigned char *)img.data, img_org.cols * img_org.rows * 3);
    image->set_data(img_org.cols, img_org.rows, false, Image::FORMAT_RGB8, rawData);
    set_texture(ImageTexture::create_from_image(image));
}

register_types.h

#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H

void initialize_example_module();
void uninitialize_example_module();

#endif // GDEXAMPLE_REGISTER_TYPES_H

register_types.cpp

#include "register_types.h"

#include "gdexample.h"

#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/godot.hpp>

using namespace godot;

void initialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }

    ClassDB::register_class<CvBlur>();
}

void uninitialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
}

extern "C" {
// Initialization.
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
    godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);

    init_obj.register_initializer(initialize_example_module);
    init_obj.register_terminator(uninitialize_example_module);
    init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);

    return init_obj.init();
}
}

ビルド

SConstruct ファイル

Sconstruct ファイルの例を以下に示す。

#!/usr/bin/env python
import os
import sys
import SCons.Util

project_name = 'libgdexample'
env = SConscript('./godot-cpp-4.2.1-stable/SConstruct')

# add env-define setting for pkg-config
if 'PKG_CONFIG_EXE_DIR' in os.environ:
    env['ENV']['PATH'] += f";{os.environ['PKG_CONFIG_EXE_DIR']}"
else:
    print("Error: PKG_CONFIG_EXE_DIR is NOT set!")
    sys.exit(1)
if 'PKG_CONFIG_PATH' in os.environ:
    env['ENV']['PKG_CONFIG_PATH'] = os.environ['PKG_CONFIG_PATH'].replace('\\', '/')
else:
    print("Error: PKG_CONFIG_PATH is NOT set!")
    sys.exit(1)

if env['platform'] == "windows" and env['use_mingw'] == True and env['arch'] == "x86_64":
    # strip -fno-exceptions from $CXXFLAGS 
    env['CXXFLAGS'] = SCons.Util.CLVar(str(env['CXXFLAGS']).replace("-fno-exceptions", ""))
    print("env['CXXFLAGS']", env['CXXFLAGS'])
    libs_implicit = ['psapi', 'wsock32', 'comdlg32', 'oleAut32', 'uuid']
    env.Append(LINKFLAGS = ['-static-libgcc', '-static-libstdc++', '-static', '-pthread'])

elif env['platform'] == "web" and env['arch'] == "wasm32":
    # strip -fno-exceptions from $CXXFLAGS 
    env['CXXFLAGS'] = SCons.Util.CLVar(str(env['CXXFLAGS']).replace("-fno-exceptions", ""))
    env['CXXFLAGS'] = SCons.Util.CLVar(str(env['CXXFLAGS']).replace("-O3", "-Oz"))
    print("env['CXXFLAGS']", env['CXXFLAGS'])

else:
    print("not suppoted conbination!")
    print("platform",env["platform"])
    print("use_mingw",env["use_mingw"])
    print("use_clang_cl",env["use_clang_cl"])
    print("use_static_cpp",env["use_static_cpp"])
    print("android_api_level",env["android_api_level"])
    print("ios_simulator",env["ios_simulator"])
    print("arch",env["arch"])
    exit()


# make build-option meta data with pkg-config
#pkgs = ['opencv4', 'eigen3']
pkgs = ['opencv4']
env.ParseConfig(f"pkg-config {' '.join(pkgs)} --static --cflags --libs")
print("env['LIBPATH']", env['LIBPATH'])

for path in env['LIBPATH']:
    print("env['LIBPREFIX']", env['LIBPREFIX'])
    print("env['LIBSUFFIX']", env['LIBSUFFIX'])
    print("env['LIBS']", env['LIBS'])

    env['LIBS'] = [env['LIBPREFIX']+i+env['LIBSUFFIX'] \
                   if type(i) is str and \
                   os.path.isfile(os.path.join(path, env['LIBPREFIX']+i+env['LIBSUFFIX'])) \
                   else i for i in env['LIBS']]
try:
    env.Append(LIBS = libs_implicit)
except NameError:
    pass

for i in env['LIBS']:
    print("test3:",type(i), i)

env.Append(CPPPATH=["src/"])
print("env['LIBPATH']", env['LIBPATH'])
print("env['CPPPATH']", env['CPPPATH'])

for i in env['CPPPATH']:
    print("test4:",type(i), i)

sources = Glob("src/*.cpp")
print("CPPDEFINES",env['CPPDEFINES'])
library = env.SharedLibrary(
    "project/bin/{}{}{}".format(project_name,env["suffix"], env["SHLIBSUFFIX"]),
    source=sources,
)
Default(library)

設置

ソースコードと SConstruct ファイルを以下のように設置する。

C:\dev\gdextension_sample\sample_02
│  SConstruct
│
└─src
        gdexample.cpp
        gdexample.h
        register_types.cpp
        register_types.h

godot-cpp はサブモジュールとして直下の ‘./godot-cpp-4.2.1-stable に設置する。

Microsoft Windows [Version 10.0.22631.2861]
(c) Microsoft Corporation. All rights reserved.

c:\Users\user>mingw64.bat
c:\dev>cd gdextension_sample

c:\dev\gdextension_sample>cd sample_02
c:\dev\gdextension_sample\sample_02>git clone --depth 1 https://github.com/godotengine/godot-cpp.git -b godot-4.2.1-stable godot-cpp-4.2.1-stable

この結果ディレクトリ構造は以下のようになる。

C:\dev\gdextension_sample\sample_02
│
│  SConstruct
│
├─godot-cpp-4.2.1-stable
│  ※多数のファイル
│
└─src
        gdexample.cpp
        gdexample.h
        register_types.cpp
        register_types.h

以上でビルドの準備は整った。これを Windows ターゲット/WEB(HTML5)ターゲット、デバッグビルド/リリースビルドの組み合わせで合計4回ビルドを行う。

Windows ターゲット、WEB(HTML5)ターゲットではコマンドプロンプトを別に開き、それぞれの環境設定用バッチファイルを実行する必要がある。

Windows プラットフォーム向けビルド

環境設定

新しいコマンドプロンプトを開き以下を実行する。

Microsoft Windows [Version 10.0.22631.3007]
(c) Microsoft Corporation. All rights reserved.

C:\Users\user>.\mingw64.bat

c:\dev>cd gdextension_sample\sample_02

リリースビルド

Windows ターゲットのリリースビルドは以下を実行する。

c:\dev\gdextension_sample\sample_02>scons platform=windows use_mingw=yes target=template_release

ビルドが成功すると以下の生成物を得る。

C:\dev\gdextension_sample\sample_01\project\bin\libgdexample.windows.template_release.x86_64.dll

デバッグビルド

Windows ターゲットのデバッグビルドは以下を実行する。

c:\dev\gdextension_sample\sample_02>scons platform=windows use_mingw=yes target=template_debug

ビルドが成功すると以下の生成物を得る。

C:\dev\gdextension_sample\sample_02\project\bin\libgdexample.windows.template_debug.x86_64.dll

WEB(HTML5)向けビルド

環境設定

新しいコマンドプロンプトを開き以下を実行する。

Microsoft Windows [Version 10.0.22631.3007]
(c) Microsoft Corporation. All rights reserved.

C:\Users\kano>.\emsdk

c:\dev>cd gdextension_sample\sample_02

リリースビルド

WEB(HTML5)ターゲットのリリースビルドは以下を実行する。

c:\dev\gdextension_sample\sample_01>scons platform=web target=template_release

ビルドが成功すると以下の生成物を得る。

C:\dev\gdextension_sample\sample_01\project\bin\libgdexample.web.template_release.wasm32.wasm

デバッグビルド

WEB(HTML5)ターゲットのデバッグビルドは以下を実行する。

c:\dev\gdextension_sample\sample_01>scons platform=web target=template_debug

ビルドが成功すると以下の生成物を得る。

C:\dev\gdextension_sample\sample_01\project\bin\libgdexample.web.template_debug.wasm32.wasm

Godot エンジンプロジェクトの作成

Godot エンジンプロジェクトの作成

先ず Godot エンジンプロジェクト用のディレクトリを作成する。新しいコマンドプロンプトを開き、以下を実行する。

Microsoft Windows [Version 10.0.22631.3007]
(c) Microsoft Corporation. All rights reserved.

C:\Users\user>.\mingw64.bat

c:\dev>cd demos
c:\dev\demos>mkdir demo_06
c:\dev\demos>cd demo_06

空の Godot エンジンプロジェクトを作成。

c:\dev\demos\demo_06>copy nul .\project.godot

bin ディレクトリを作成し、GDEXTENSION のダイナミックリンクライブラリを設置する。

c:\dev\demos\demo_06>mkdir bin
c:\dev\demos\demo_06>cd bin
c:\dev\demos\demo_06\bin>copy C:\dev\gdextension_sample\sample_02\project\bin\*.dll .
c:\dev\demos\demo_06\bin>copy C:\dev\gdextension_sample\sample_02\project\bin\*.wasm .

c:\dev\demos\demo_06\bin ディレクトリに以下のような gdextension ファイルを作成する。

c:\dev\demos\demo_06\bin\gdexample.gdextension

[configuration]

entry_symbol = "example_library_init"
compatibility_minimum = "4.1"

[libraries]
windows.debug.x86_64   = "res://bin/libgdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdexample.windows.template_release.x86_64.dll"
web.debug.wasm32 = "res://bin/libgdexample.web.template_debug.wasm32.wasm"
web.release.wasm32 = "res://bin/libgdexample.web.template_release.wasm32.wasm"

opencv サンプル用の画像に wikipedia にある lenna さんの画像を拝借する。

https://en.wikipedia.org/wiki/File:Lenna_(test_image).png

ここまでの手順で、ディレクトリ構成は以下のようになる。

C:\dev\demos\demo_06
│  project.godot
│  Lenna_(test_image).png  
│
└─bin\
        gdexample.gdextension
        libgdexample.windows.template_debug.x86_64.dll"
        libgdexample.windows.template_release.x86_64.dll"
        libgdexample.web.template_debug.wasm32.wasm"
        libgdexample.web.template_release.wasm32.wasm"

Godot エンジンプロジェクトを起動する。

c:\dev\demos\demo_05>godot.windows.editor.x86_64.exe -e .

名無しのプロジェクトが起動する。

GDEXTENSION が読み込まれている場合は、左下のリソースツリーに gdexample.gdextension が存在する。

res://
└─bin/
        gdexample.gdextension

シーンに Node2D を作成し、名前を “Prototype” に変更する。

“Prototype” の下に 今回作成した GDEXTENSION である “CvBulr” を作成する。

Prototype にスクリプトをアタッチする。スクリプトの内容は以下とする。

CvBulr.gd

extends CvBulr

func _ready():
	var res = open_movie("./Lenna_(test_image).png")
	if res == 1:
		print("open success")
	else:
		print("open failure")

Ctrl-“S” とし、このシーンを ”Prototype.tscn” として保存する。

上部メニューの「プロジェクト」の「プロジェクトの設定」を選択すると「プロジェクトの設定」が開く。「一般」の「実行」でメインシーンを “res://Prototype.tscn” に設定する。

再度 Ctrl-“S” とし、保存する。

以上の手順で Godot エンジンプロジェクトの作成は完了。

Godot エンジンエディッタでの実行

“F5” を押下することによりメインシーンが再生される。

画像サイズは 256 x 256 ピクセル。最初は小さいながらも明瞭な画像であるが、時間経過とともにぼかしがはいっていくように変化してゆく。ある程度時間がたつと再び明瞭な画像に戻る。

WEB(HTML5)エクスポート

WEB エクスポートでの GDEXTENSION で使用するリソースファイルに関しては注意が必要。Godot エンジンでは画像などのリソースファイルは pck ファイルの中に built-in リソースとして組み込まれる。これらのリソースは GDScript からは “res://~” という形でアクセスできる。しかし、GDEXTENSION の C/C++ ドメインから標準的な C/C++ のファイル I/O を用いて “res://~” という形式でアクセスする簡単な方法はない。なので、Windows ターゲットの場合は Windows のファイルシステムを直接参照している。問題なのは WEB(HTML5)の実行環境では C/C++ ドメインから見えるファイルシステムが WASM の仮想ファイルシステムになるので、そこにリソースファイルを送り込まなくてはならないこと。GDEXTENSION では WEB サーバー上のファイルを、WASM の仮想ファイルシステムにプリロードする engine.preloadFile() という javasccript api を用意してくれているので、これを使用する。
https://docs.godotengine.org/en/3.0/getting_started/workflow/export/customizing_html5_shell.html
https://godotforums.org/d/24848-how-to-load-a-pck-file/8

前置きが長くなったが、WEB エクスポートの手順を見てみよう。

今回のサンプルの場合 Lenna_(test_image).png は pck ファイルの不要なサイズ増加を避けるため、built-in リソースから除外すべく res:// からいったん外す。そして後述するように、WEB エクスポートリソースとして別途組み込む。

上部メニューの「プロジェクト」の「エクスポート」を選択すると「エクスポート」のダイアログが開くので、「追加」を押下して「Web」を選択すると左ペインで「Web(実行可能)」が選択できるようになる。「Web」を選択すると右ペインに設定が表示される。

ここで、エクスポート先を “c:/dev/demos/export/demo_06/web/sample.html” とする。また、「バリアント」を “Extensions Support” のチェックボックスを ”オン” にする。

左下にある「全てをエクスポート」をマウス左クリックすると、「全てのエクスポート」ダイアログが開く。

ここで、「デバッグ」を選べばデバッグ用にエクスポート、「リリース」を選べばリリース用にエクスポートされる。

エクスポートディレクトリに Lenna_(test_image).png を リソースとして組み込むべく、 C:\dev\demos\export\demo_06\web にコピーする。

C:\dev\demos\export\demo_06\web\ に sample.html が生成されているので、以下のように修正する。

C:\dev\demos\export\demo_06\web\sample.html(修正部分のみ)

        } else {
           setStatusMode('indeterminate');
+         engine.preloadFile('Lenna_(test_image).png').then(() => {
+
           engine.startGame({
               'onProgress': function (current, total) {
                   if (total > 0) {
                       statusProgressInner.style.width = `${(current / total) * 100}%`;
                       setStatusMode('progress');
                       if (current === total) {
                           // wait for progress bar animation
                           setTimeout(() => {
                               setStatusMode('indeterminate');
                           }, 500);
                       }
                   } else {
                       setStatusMode('indeterminate');
                   }
               },
           }).then(() => {
               setStatusMode('hidden');
               initializing = false;
           }, displayFailureNotice);
+
+          });

この修正はWEB 環境で外部ファイルを参照するためのもので、sample.html と同じ WEB ディレクトリにある ‘Lenna_(test_image).png’ というファイルを、emscripten C/C++ 環境から見える仮想ファイルシステムのルートディレクトリ “/” にプリロードしている。

engine.startGame() 実行前に、engine.preloadFile(‘Lenna_(test_image).png’) を実行させる必要がある。もし別のファイルが必要なら、同様に engine.startGame() 実行前に必要ファイルをプリロードさせると良いだろう。

sample.html (プロジェクトごとにファイル名は変わるだろうが)はエクスポートの度に生成されるので、都度修正を行わなければならないことは手間ではある。patch コマンドを導入するなり、エディッタの機能で自動化するなり、もしくはコピペするなり自分なりの手順を確立させる必要がある。

一応本サイトにも設置してみた。ディスクトップの Windows PC で Chrome ブラウザを用いているのなら、再生できると思う。

This sample is using godot engine . License note is in https://godotengine.org/license/.

本稿の振り返り

大規模 C++ ライブラリである opencv の機能を使った GDEXTENSION サンプルが Windows ターゲットでも Web ブラウザでも簡単に動作した。これはかなりエポックメイキングなできごとではないかと思う。

Godot GDEXTENSION は WEB 出力をサポートする。分かりやすいマンマシンインターフェースにリッチな2D/3D可視化、そして emscripten/wasm による C/C++ サポート、更にはWebSocket によるサーバー間通信。GDScript からのバインディングも用意されていて、自作する必要はない。更には C/C++ の長い歴史が提供する膨大な量の強力なライブラリを(努力すれば)使用できる。Godot エンジン GDEXTENSION と WEB の組み合わせはフロントエンドの革新である、今そう思っている。

デバッグは Windows プラットフォームで行えばよい。十分ロジックの検証が行えたなら、WEB(HTML5)の差分コーディングを行う。そうすることにより、WEB アプリケーションとして WEB デバッガでデバッグしなければならない部分を最小化することができる。

Godot エンジン GDEXTENSION を用いて WEB アプリケーションを開発するには、Windows ベースの場合、DOS/コマンドプロンプト、C/C++、MinGW-w64、WASM/emscripten、Cmake、scons、python, C/C++、javascript/HTML などやたら広い技術分野の基本を理解しておく必要がある。これら全てをおさえることは実際問題として容易ではない。しかし希望する C/C++ ライブラリをリンクしてビルドして動作させる方法が確立できれば、後は Godot エンジンと C/C++ の知識だけで勝負できるようになるわけで、多くの開発者が利用可能になるだろう。この一連の記事がその一助になれば嬉しい。

そして Godot エンジン及び GDEXTENSION の発展に役立つことを。

コメント

タイトルとURLをコピーしました