Godot GDEXTENSION build 環境構築のまとめ(その4)

Godot

本稿の内容に関して

本稿は GDEXTENSION の開発環境構築を行った際のメモの共有を意図した、一連の記事の4番目。1番目から順に読まれていくことを想定している。

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

前稿(その3)では Godot 公式が提供する GDEXTENSION サンプルのビルドを行った。本稿では C++ ライブラリとして opencv 4.8.1 をソースコードからビルドするとともに、それを GDEXTENSION に組み込むべく、scons の設定ファイルである Sconstruct の記述に関して検討を行ってゆく。

GDEXTENSION サンプルとしては、mp4 動画を Godot で再生するもの。

C++ライブラリを用いた GDETENSION のビルド

opencv のビルド

方針の検討

GDEXTENSION に opencv ライブラリを組み込むにあたり、以下の選択肢がある。

  • ダイナミックリンクライブラリとして個別ライブラリ (*.dll) を組み込む
  • ダイナミックリンクライブラリとしてビルドした opencv_world ライブラリ (libopencv_world<version>.dll ) を組み込む
  • スタティックリンクライブラリ (*.a) として個別ライブラリを、もしくはスタティックリンクライブラリとしてビルドした opencv_world ライブラリ (libopencv_world<version>.a ) を組み込む

ダイナミックライブラリを選択した場合は、ゲームの配布時において、opencv が生成する多数の lib*.dll を一緒に配布しなければならなず、酷く分かり辛いこととなる。

opencv では多数のライブラリの同梱配布を避けるために BUILD_opencv_world というオプションがあり、これを有効にすれば生成されるダイナミックリンクライブラリ (*.dll) ファイルを libopencv_world<version>.dll の一つにすることができる。しかし、このライブラリは全ての機能を包含するため、非常にファイルサイズが大きく、ゲームの配布時において、大きなファイルの同梱を許容する必要がある。また、一つとはいえ、配布するダイナミックリンクライブラリファイルが増えることは煩わしいかもしれない。

一方スタティックリンクライブラリを選択する場合は、GDEXTENSION にスタティックリンクされるため、ライブラリのダイナミックリンクライブラリ (lib*.dll) を一緒に配布しなくて良く、かつ必要なライブラリのみがリンクされるため、GDEXTENSION のファイルサイズを小さくすることができる。反面スタティックリンクライブラリは単なるオブジェクトファイルのアーカイバであるため、依存関係を明確化しないとリンクが通らない。そしてそうであるにも拘らず、依存関係を整理することが難しいという厄介さがある。また、生成するのにも利用するにも情報が少なく、一般に難易度が高い。

本稿では、windows 環境で MinGW-w64をツールチェーンに用い、opencvをスタティックリンクライブラリとして生成し、それを用いたアプリケーションを scons で GDEXTENSION としてビルドすることを目標とする。

補足:

  • MinGW-w64、スタティックリンクライブラリ、scons の組み合わせはかなりの縛りプレイとなる。自分だけが使うツールを作成するのならば、Visual Studio を使い、ダイナミックリンクライブラリを許容すると難易度が格段に下がることは明記しておく。
  • opencv の場合、依存関係を単純化するため、opencv_world をスタティックリンクライブラリとしてビルドすることもできる。これを用いれば作業を単純化できるが、この手法は他の OSS ライブラリに転用できるわけでもないので、ここではスタティックリンクライブラリとしてビルドした opencv_world を用いない方法の検討を行う。

opencvのソース取得とビルド

opencv のソースコード一式の取得とビルドはコマンドプロンプトで以下の手順で行う。

先ず最初に GitHub から Git を用いてソースコード一式を カレントディレクトリ配下に opencv-4.8.1 というディレクトリを作って、そこに取得する。

c:\dev>git clone --depth 1 https://github.com/opencv/opencv -b 4.8.1 opencv-4.8.1

取得完了後、opencv-4.8.1 ディレクトリに移動し、そこに build ディレクトリを作成する。

c:\dev>cd opencv-4.8.1
c:\dev\opencv-4.8.1>mkdir build

Cmakeは out-of-source をサポートしていて、ソースコードディレクトリとビルド生成物格納ディレクトリを完全に分離することができる。これによりソースコードツリーを汚さず、ビルド生成物を消したい場合は build ディレクトリ配下を消去することにより実現する。

build ディレクトリに移動し、一つ上の CMakeLists.txt を対象に cmake-gui を実行する。

c:\dev\opencv-4.8.1>cd build
c:\dev\opencv-4.8.1\build>cmake-gui ..

ここで実行している cmake-gui は Cmake に同梱されている GUI ベースの Cmake generator/Configurator。”Generate” のボタンを左クリックすると “Specify the generator for this project” と聞くダイアログが出てくるので以下の設定とする。

  • プルダウンメニュー: MinGW Makefiles
  • ラジオボタン: Use default native compilers

“Finish” ボタンを左クリックすると、generate が始まる。

generate が完了すると下の情報領域に以下のように表示される。

Configuring done
Generating done

この状態で上の “Name/Value” 欄で CMAKE の Config Key を編集できるようになる。右上にある “Grouped”と “Advanced” の二つのチェックを有効にすると、隠れた Key も表示されるとともに、機能グループに分かれて表示されるので見通しが良くなる。

以下の Config Key を編集する。

- BUILD_EXAMPLES -> OFF
- BUILD_JAVA -> OFF
- BUILD_opencv_python_tests -> OFF
- BUILD_opencv_java_bindings_generator -> OFF
- BUILD_opencv_python_bindings_generator -> OFF
- BUILD_opencv_world -> OFF
- BUILD_SHARED_LIBS -> OFF
- BUILD_TESTS -> OFF
- CMAKE_BUILD_TYPE -> Release
- CMAKE_CONFIGURATION_TYPES -> Release
- CMAKE_INSTALL_PREFIX -> C:/dev/tools/mingw64/local/opencv-4.8.1
- OPENCV_GENERATE_PKGCONFIG -> ON
- EXECUTABLE_OUTPUT_PATH -> C:/dev/tools/mingw64/local/opencv-4.8.1/bin
- PKG_CONFIG_EXECUTABLE -> C:/dev/tools/pkg-config/bin/pkg-config.exe
- WITH_CUDA -> OFF
- WITH_EIGEN -> OFF
- WITH_GSTREAMER -> OFF
- WITH_OPENGL -> OFF
- WITH_OBSENSOR -> OFF
- WITH_VTK -> OFF
- WITH_QT -> OFF

CMAKE_INSTALL_PREFIX Key は Cmake の作法で頻出するもので、ビルド生成物をインストールする際のターゲットディレクトリを指定する。

BUILD_SHARED_LIBS の Key はシェアードライブラリ (windows ではダイナミックリンクライブラリを指し、 *.dll という拡張子) を作るかの指定に用いる。これを OFF 指定にすると、スタティックリンクライブラリ (g++ では *.a という拡張子) の生成指定となる。

OPENCV_GENERATE_PKGCONFIG の Key に関しては後述する。

他の Key は opencv 特有のものであるが、ほぼ最小を目指している。

注意:
opencv 4.8.1 では WITH_OBSENSOR Key を有効にすると実行時エラーが発生する。このオプションは OB Sensor をサポートするもので、デフォルトでON なので注意。OB Sensor は使わないので、OFF にする。

設定変更した後は “Generate” ボタンにより確認する。問題無ければ “Configure” ボタンを左クリックし、Configuration を実行する。Makefile はカレントディレクトリに生成されるので、以下のようにビルド及びインストールを実行する。

c:\dev\opencv-4.8.1\build>mingw32-make
c:\dev\opencv-4.8.1\build>mingw32-make install

インストールに成功すると、以下のようなディレクトリ構成になる。

C:\dev\tools\mingw64\local\opencv-4.8.1
├─bin
├─etc
├─include
│  └─opencv2
└─x64
    └─mingw
        ├─bin
        ├─samples
        └─staticlib
            └─pkgconfig

インストール完了すれば、C:\dev\opencv-4.8.1 のディレクトリは消してしまって構わない。消す場合は悲劇を避けるために、コマンドプロンプトからではなくエクスプローラーから GUI で消すことをお勧めする。

opencv サンプルビルド

サンプルの題材に関して

題材は opencv を利用した mp4 動画ファイル再生を行うもの。opencv の Mat 画像を Godot のイメージに変換し、Texture に流し込む。これができれば、opencv を用いた様々な画像変換後に Godot エンジンを用いたリアルタイム表示が実現する。

現時点、Godot エンジンは mp4 動画の再生をサポートしていない。Godot エンジンの開発者たちは MIT ライセンスだけで構成されたゲームエンジンを世に送り出すことを至上命題にしている。残念なことに mp4 や h264/h265 といった比較的モダンな動画フォーマットの再生ライブラリはライセンス的にややこしいものが多く、当分の間 Godot エンジンに mp4 動画再生が組み込まれることは無いだろう。
https://github.com/godotengine/godot-proposals/issues/3286

opencv は ffmpeg や gstreamer といった動画関連ライブラリを用いて mp4 動画の再生を行うことができるが、これらを opencv で利用するためには 動画関連ライブラリ のダイナミックリンクライブラリにPATH を通しておくか、もし Godot エンジンアプリケーションの配布を考えているならば、同梱配布を検討する必要がでてくる。

今回の例でも、opencv_videoio_ffmpeg481_64.dll という ffmpeg のダイナミックリンクライブラリが必要となる。

ここでは先ず、GDEXTENSION としてではなく、普通に Windows アプリケーションとしてopencv ライブラリを用いたサンプルのビルドを試みる。

Cmake を用いた opencv サンプルビルド

opencv のインストール確認を兼ねて、以下の opencv サンプルを Cmake を使いサンプルビルドしてみる。

main.cpp

#include <opencv2/opencv.hpp>
#include <filesystem>
#include <iostream>

// "mov_hts-samp009.mp4" is downloaded from https://www.home-movie.biz/free_movie.html

int main() {
    cv::Mat frame;

    std::string filepath = ".\\mov_hts-samp009.mp4";
    std::cout << filepath << std::endl;

    if (!std::filesystem::is_regular_file(filepath)) {
        std::cout << filepath << " is not found." << std::endl;
        return -1;
    }
	
    cv::VideoCapture cap;
    cap.open(filepath);

    if(!cap.isOpened())
    {
        std::cout << filepath << " is unsupported file." << std::endl;
        return -1;
    }

    while(1) {
        cap.read(frame);
        if (frame.empty()) {
            cap.set(cv::CAP_PROP_POS_FRAMES, 0);
            cap.read(frame);
        }
        cv::resize(frame, frame, cv::Size(), 0.5, 0.5);
        imshow("Video", frame);
        if (cv::waitKey(1) == 'q') break;
    }
    return 0;	
}

このサンプルは mp4 ビデオファイルを読み込み再生するだけの簡単なもの。再生動画ファイルはソースコード埋め込みで “.\mov_hts-samp009.mp4” 固定。

CMakeLists.txt は以下のようになる。

CMakeLists.txt

cmake_minimum_required(VERSION 3.1)
project( prog )

set(OpenCV_DIR C:/dev/tools/mingw64/local/opencv-4.8.1)
find_package( OpenCV REQUIRED )
add_executable( prog src/main.cpp )

target_link_libraries( prog ${OpenCV_LIBS} -mwindows -mwin32 -static-libgcc -static-libstdc++ -static -pthread)

リンクオプション “-mwindows” 及び “-mwin32” に関しては以下を参照。

– 3.18.55 x86 Windows Options
https://gcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/x86-Windows-Options.html

これらを以下のように設置する。

top-directory
│  CMakeLists.txt
│
└─src
        main.cpp

ビルドは以下の手順となる。

top-directory>mkdir build
top-directory>cd build
top-directory\build>cmake -G "MinGW Makefiles" -DOpenCV_STATIC=ON ..
top-directory\build>mingw32-make

特に問題なく、ビルドが成功する。opencv ライブラリを用いたアプリケーションを Cmake を用いてビルドすることは非常に簡単といえる。

ちなみに Cmake 実行時の以下のオプションは、opencv のスタティックリンクライブラリを用いることを意味する。

OpenCV_STATIC=ON

再生動画ファイル “mov_hts-samp009.mp4” は 以下のサイト様よりダウンロードし、top-directory\build ディレクトリに格納する。

SAKURA
https://www.home-movie.biz/free_movie.html

再生には opencv_videoio_ffmpeg481_64.dll が必要なので、以下のように PATH を通しておく。

SET PATH=%PATH%;C:\dev\tools\mingw64\local\opencv-4.8.1\bin

設置後のディレクトリ構成は以下のようになる。

top-directory
│  CMakeLists.txt
│
├─build
│  │  CMakeCache.txt
│  │  cmake_install.cmake
│  │  prog.exe
│  │  Makefile
│  │  mov_hts-samp009.mp4
│  │
│  └─CMakeFiles
│        多数のディレクトリ及び中間生成ファイル
│
└─src
        main.cpp

実行すると以下のように動画がループ再生される。音声は無い。

“q” キーを押下すると停止する。

scons を用いた opencv サンプルのビルド

GDEXTENSION は scons でビルドする方法が用意されているので、opencv のライブラリを用いたサンプルも scons を用いてビルドできると都合が良い。

前項の C++ サンプルを scons を用いてビルドできるかを試す。ここで opencv ライブラリをビルドした際に生成された以下のファイルを見てみる。

C:\dev\tools\mingw64\local\opencv-4.8.1\x64\mingw\staticlib\pkgconfig\opencv4.pc

このファイルは opencv の Cmake のコンフィギュレーションで以下のオプションを設定した場合に生成される。

OPENCV_GENERATE_PKGCONFIG=ON

このファイルは pkg-config の入力ファイルとしてビルドオプションを生成するために用いられ、中身は以下のようになっている。

opencv4.pc

# Package Information for pkg-config

prefix=C:/dev/opencv-4.8.1/build/install
exec_prefix=${prefix}
libdir=${exec_prefix}/x64/mingw/staticlib
includedir=${prefix}/include

Name: OpenCV
Description: Open Source Computer Vision Library
Version: 4.8.1
Libs: -L${exec_prefix}/x64/mingw/staticlib -lopencv_gapi481 -lopencv_highgui481 -lopencv_ml481 -lopencv_objdetect481 -lopencv_photo481 -lopencv_stitching481 -lopencv_video481 -lopencv_calib3d481 -lopencv_features2d481 -lopencv_dnn481 -lopencv_flann481 -lopencv_videoio481 -lopencv_imgcodecs481 -lopencv_imgproc481 -lopencv_core481
Libs.private: -L${exec_prefix}/x64/mingw/staticlib -llibprotobuf -lade -llibjpeg-turbo -llibwebp -llibpng -llibtiff -llibopenjp2 -lIlmImf -lzlib -lquirc -lwsock32 -lcomctl32 -lgdi32 -lole32 -lsetupapi -lws2_32 -lpthread
Cflags: -I${includedir}

opencv をスタティックリンクライブラリとしてビルドすると主に以下の命名規則のスタティックリンクライブラリが多数生成される。

libopencv_*<version>.a 

スタティックリンクライブラリを用いてビルドを行う場合、リンクでは依存関係において依存する方を先に指定する必要がある。もし、正しい順番に指定しなかった場合、依存関係が解決できず多量のエラーが出てくることになる。opencv4.pc は正しい依存関係をライブラリ利用者に提供する。

実際に pkg-config を試してみる。

c:\dev>pkg-config c:\opencv-4.8.1\build\install\x64\mingw\staticlib\pkgconfig\opencv4.pc --static --cflags --libs
-IC:/dev/opencv-4.8.1/build/install/include  -LC:/dev/opencv-4.8.1/build/install/x64/mingw/staticlib -lopencv_gapi481 -lopencv_highgui481 -lopencv_ml481 -lopencv_objdetect481 -lopencv_photo481 -lopencv_stitching481 -lopencv_video481 -lopencv_calib3d481 -lopencv_features2d481 -lopencv_dnn481 -lopencv_flann481 -lopencv_videoio481 -lopencv_imgcodecs481 -lopencv_imgproc481 -lopencv_core481 -llibprotobuf -lade -llibjpeg-turbo -llibwebp -llibpng -llibtiff -llibopenjp2 -lIlmImf -lzlib -lquirc -lwsock32 -lcomctl32 -lgdi32 -lole32 -lsetupapi -lws2_32 -lpthread

-I, -L, そして多数の -l オプションが生成されたことが分かる。

linux 系の shell (bashなど) では以下のような記述により pkg-config の出力を直接 g++ の引数に利用することができ、便利。

$ g++ sample.cpp -o sample -std=c++17 `pkg-config opencv4.pc --static --cflags --libs`

しかし windows DOS ではプログラムの実行結果を、別のプログラムの引数に埋め込む簡単な記述方法が無い。従って コマンドプロンプト を用いたビルド処理では、pkg-config は今一つ利便性に欠ける。

幸いなことに scons では以下のように pkg-config のような外部コマンドを用いて、コンフィグファイルのパース結果をビルドオプションに反映させることができる。

scons 記述例

 ParseConfig(f"pkg-config {opencv_pc_file} --static --cflags --libs")

この処理は、具体的には以下のリストに要素を追加する。

$CPPPATH # インクルードファイルパス: g++ の場合は -I オプションの要素
$LIBPATH # ライブラリのパス: g++ の場合は -L オプションの要素
$LIBS    # リンクするライブラリ: g++ の場合は -l オプションの要素

尚、scons は env = Environment() として環境変数を取り入れる際、不要な PATH 設定を反映させない仕様となっている。scons で pkg-config を使うためには pkg-config の実行ファイルの PATH を scons に知らせる必要がある。例えば以下のようにすれば環境変数の PATH 全てを反映させることができる。

scons 記述例

env = Environment(tools = ['mingw'], ENV = {'PATH' : os.environ['PATH']})

しかし この方法では色々副作用が発生(例えば scons が使う環境変数 TMP、TEMP を管理者権限の必要な PATH に書き換えてしまう結果 scons の実行に管理者権限が必要になってしまうなど)するので推奨しない。副作用を避けるために、以下のように必要な PATH を個別に追加していくほうが安全。

scons 記述例

env = Environment(tools = ['mingw'])

# for pkg-config
pkg_config_dir = 'c:/dev/tools/pkg-config/bin'
env['ENV']['PATH'] += f";{pkg_config_dir}"

以上を踏まえて SConstruct を作成する。

SConstruct

import os
import sys

env = Environment(tools = ['mingw'])

# for pkg-config
pkg_config_dir = 'c:/dev/tools/pkg-config/bin'
env['ENV']['PATH'] += f";{pkg_config_dir}"

env.Append(CXXFLAGS = ["-std=c++0x"])
env.Append(LINKFLAGS = ["-mwindows", "-mwin32", "-static-libgcc", "-static-libstdc++",
                        "-static", "-pthread"])

# for opencv
opencv_pc_file='c:/dev/opencv-4.8.1/build/install/x64/mingw/staticlib/pkgconfig/opencv4.pc'
env.ParseConfig(f"pkg-config {opencv_pc_file} --static --cflags --libs")

sources = Glob("src/*.cpp")
env.Program("prog", source=sources)

この SConstruct を用いて以下のようにビルドしてみる。

c:\dev\opencv_sample\scons

残念なことに大量のリンクエラーが発生する。リンクエラーは主に以下の二種。

  • “liblib*.a” というライブラリが見つからない
  • Windows SDKへ依存するシンボル参照が見つからない

“liblib*.a” というライブラリが見つからないのは scons の仕様による。scons の $LIBS 指定において、例えば “libpng” を指定した場合、期待する動作は LIBPREFIX である “lib” と LIBSUFFIX である “.a” を追加した “liblibpng.a” というファイル名のライブラリをリンクするべく g++ に “-llibpng” というオプションを渡すというものだが、現状の scons の仕様では先頭の “lib” を削除して g++ に “-lpng” というオプションを渡してしまう。その結果 g++ は “libpng.a” というファイルを探し、見つけられないのでリンクエラーとなる。

対策は色々考えられるが、例えば以下のサンプルコードのように、LIBS に “lib*.a” という本来のファイル名 (例えば “liblibpng.a”) を再設定し、scons により LIBPREFIX と LIBSUFFIX を剝がされることにより、正しい -l オプションになるようにする。

SConstruct(変更部分抜粋)

env.ParseConfig(f"pkg-config {opencv_pc_file} --static --cflags --libs")
for path in env['LIBPATH']:
    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']]

このようにすると、”liblibpng.a” というライブラリであっても、LIBPREFIX と LIBSUFFIX を剝がしたのち、”-llibpng” というオプションが生成されるようになる。

別解として、例えば以下のコードのようにライブラリをフルパスで示す方法もある。

SConstruct(変更部分抜粋)

env.ParseConfig(f"pkg-config {opencv_pc_file} --static --cflags --libs")
for path in env['LIBPATH']:
    env['LIBS'] = [env.File(os.path.join(path, 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']]

$LIBS の中にフルパス指定の要素が有る場合、scons はその要素をライブラリではなく、入力オブジェクトファイルとして取り扱う。つまり -l オプションを付けるのではなく、そのままの内容を g++ のコマンドラインに渡す。LIBPREFIX も LIBSUFFIX も関係なくなり、scons は余計なことをしなくなる。g++ もライブラリ探索を行わなくて済むのでコンパイル時間が短くなるかもしれない。代わりに g++ のコマンドラインが猛烈に長くなる。また、$LIBS に指定するライブラリをスタティックリンクライブラリからダイナミックリンクライブライに変更する際、簡単にはいかなくなる。ダイナミックリンクライブラリはスタティックリンクライブラリとは異なり、通常 g++ の入力オブジェクトファイルにはできないことに注意。

どちらの解を選ぶかは好みとケースによる。本稿では前者を選ぶ。

Windows SDK へ依存するシンボル参照が見つからないのは、単純に opencv4.pc の記述にシンボル参照に対応する Windows SDK ランタイムライブラリの指定が無いため。プラットフォームに依存するシステムライブラリはプラットフォームに合わせて別途指定する必要がある。対策としては例えば以下のサンプルコードのように、必要なランタイムライブラリを LIBS に追加すればよい。

SConstruct(変更部分抜粋)

env.Append(LIBS = ["comdlg32", "oleAut32", "uuid"])

リンクエラーとなるシンボルがなんというランタイムライブラリの中にあるのかは一つ一つ探す必要がある。ライブラリの Config以下のサイトなどで調べると、関数名とランタイムライブラリ名の対応が分かるので便利。
/https://www.ni.com/docs/ja-JP/bundle/labwindows-cvi/page/cvi/programmerref/availability_win32_functions.htm

もしくは調査すべき Windows SDK ランタイムライブラリは C:\Windows\System32 のディレクトリにある確率が高いわけだから、このディレクトリにある *.dll ファイルを例えば以下のようなツールで解析していけばよい。
https://www.dependencywalker.com/ https://reverseengineering.stackexchange.com/questions/3101/looking-for-exported-symbols-in-a-dll-with-objdump

実務的な解としては、頻回必要とされるランタイムライブラリは決まっているので、それらを以下の例のように $LIBS に追加してしまえば、この問題に遭遇する可能性が減るだろう。

SConstruct(変更案部分抜粋)

env.Append(LIBS = ["psapi", "wsock32", "comdlg32", "ole32", "oleAut32",
                   "uuid", "advapi32", "opengl32", "glu32"])

ダイナミックリンクライブラリの重複指定も、使われないシンボルを含むライブラリの指定も許される。ただし、このようにしてしまうと 第三者が SConstruct を読んだとき、これらのライブラリが必要なのだと誤解することにはなるだろう。

修正後の SConstruct は例えば以下のようになる。

SConstruct

import os
import sys

env = Environment(tools = ['mingw'])

# for pkg-config
pkg_config_dir = 'c:/dev/tools/pkg-config/bin'
env['ENV']['PATH'] += f";{pkg_config_dir}"

env.Append(CXXFLAGS = ["-std=c++0x"])
env.Append(LINKFLAGS = ["-mwindows", "-mwin32", "-static-libgcc", "-static-libstdc++",
                        "-static", "-pthread"])

# for opencv
opencv_pc_file='C:/dev/tools/mingw64/local/opencv-4.8.1/x64/mingw/staticlib/pkgconfig/opencv4.pc'
env.ParseConfig(f"pkg-config {opencv_pc_file} --static --cflags --libs")

for path in env['LIBPATH']:
    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']]
env.Append(LIBS = ["comdlg32", "oleAut32", "uuid"])

sources = Glob("src/*.cpp")
env.Program("prog", source=sources)

対策後 SConstruct を用いて以下のようにビルドしてみる。

c:\dev\opencv_sample\scons

今度はビルドが成功し、SConstruct と同じディレクトリに prog.exe という実行ファイルが生成される。実行すると、mp4 動画が再生される。

見て頂いて分かるように、python スクリプトの力でねじ伏せたような印象が残る。どうとでもなる反面、このようなややこしい処理を行わなければならないのは、scons の特殊な実装に原因があるのではないかと思わないでもない。

何れにしろ scons を使うならば python の知識はあった方が良い。

opencvライブラリをリンクした GDEXTENSION サンプルのビルド

ソースコード

“C:\dev\gdextension_test2” というディレクトリにgdextensionのサンプルを作成することとする。設置ファイルツリー構造は以下のようになり、設置するファイルは SConstruct と以下のソースコード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 MoviePlayer : public TextureRect {
    GDCLASS(MoviePlayer, TextureRect);
    cv::VideoCapture cap;
    PackedByteArray rawData;
    Ref<Image> image;
    void closeMovie();
	double time_passed;
protected:
    static void _bind_methods();
public:
    MoviePlayer();
    ~MoviePlayer();
    void _process(double delta);
    int openMovie(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> // To use "UtilityFunctions::print()".
#endif // DEBUG_ENABLED
#include <filesystem>

using namespace godot;

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

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

MoviePlayer::~MoviePlayer()
{
    closeMovie();
}

int MoviePlayer::openMovie(const String &file_path)
{
#ifdef DEBUG_ENABLED
    UtilityFunctions::print(file_path.utf8().ptr());
#endif // DEBUG_ENABLED
	if (!std::filesystem::is_regular_file(file_path.utf8().ptr())) {
#ifdef DEBUG_ENABLED
		UtilityFunctions::print(std::filesystem::current_path().c_str());
#endif // DEBUG_ENABLED
        return 0;
	}
	closeMovie();
    cap.open(file_path.utf8().ptr());
    if (!cap.isOpened())
        return 0;
    cv::Mat frame;
	cap.read(frame);
	cv::resize(frame, frame, cv::Size(), 0.5, 0.5);
	
	rawData.resize(frame.cols * frame.rows * 3);
    image = Image::create_from_data(frame.cols, frame.rows, false, Image::FORMAT_RGB8, rawData);
    return 1;
}

void MoviePlayer::closeMovie()
{
    if (cap.isOpened())
        cap.release();
}

void MoviePlayer::_process(double delta) {
    time_passed += delta;
	if (!cap.isOpened())
        return;
    cv::Mat frame;
	cap.read(frame);
	if (frame.empty()) {
		cap.set(cv::CAP_PROP_POS_FRAMES, 0);
		cap.read(frame);
	}
	cv::resize(frame, frame, cv::Size(), 0.5, 0.5);

	cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);
    memcpy(rawData.ptrw(), (unsigned char *)frame.data, frame.cols * frame.rows * 3);
    image->set_data(frame.cols, frame.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<MoviePlayer>();
}

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('c:/dev/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", ""))

#    pkgs = ['opencv4', 'eigen3']
    pkgs = ['opencv4']
    libs_implicit = ['psapi', 'wsock32', 'comdlg32', 'oleAut32', 'uuid']
    env.Append(LINKFLAGS = ['-static-libgcc', '-static-libstdc++', '-static', '-pthread'])

    # make build-option meta data with pkg-config
    env.ParseConfig(f"pkg-config {' '.join(pkgs)} --static --cflags --libs")

    for path in env['LIBPATH']:
        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']]
    env.Append(LIBS = libs_implicit)

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"])
    sys.exit(1)

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

これらを以下のように設置する。

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

コマンドプロンプトを起動した際のディレクトリに設置したMinGW-w64 用の環境設定バッチファイルである %HOMEPATH%\mingw64.bat にpkg-config 用の設定を付け加える。

%HOMEPATH%\mingw64.bat

SET PATH=%PATH%;"C:\Program Files\7-Zip"
SET PATH=%PATH%;C:\dev\tools\pkg-config\bin
SET PATH=C:\dev\tools\Python\Python312\Scripts;C:\dev\tools\Python\Python312;%PATH%
SET PATH=C:\dev\tools\mingw64\bin;%PATH%
SET CMAKE_PREFIX_PATH=C:\dev\tools\mingw64\local
SET PATH=%PATH%;C:\dev\tools\mingw64\local
SET PATH=C:\dev\godot-4.2.1-stable\bin;%PATH%

set PKG_CONFIG_EXE_DIR=C:\dev\tools\pkg-config\bin
set PKG_CONFIG_PATH=""
set PKG_CONFIG_PATH=%PKG_CONFIG_PATH%;C:\dev\tools\mingw64\local\opencv-4.8.1\x64\mingw\staticlib\pkgconfig

cd c:\dev

設置が終われば、新規コマンドプロンプトを開き、以下のようにすることでビルドできる。

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

C:\Users\user>.\mingw64
C:\dev>cd gdextension_test2
c:\dev\gdextension_test>scons platform=windows use_mingw=yes custom_api_file=C:/dev/godot-4.2.1-stable/bin/extension_api.json

初回ビルドでは godot-cpp のフルビルドが行われるため、かなり時間がかかる。

ビルドが成功すると以下のような成果物(および中間成果物)が生成される。

C:\dev\gdextension_test2
│  .sconsign.dblite
│  SConstruct
│
├─project
│  └─bin
│          libgdexample.windows.template_debug.x86_64.dll
│          liblibgdexample.windows.template_debug.x86_64.a
│
└─src
        gdexample.cpp
        gdexample.h
        gdexample.o
        register_types.cpp
        register_types.h
        register_types.o

“libgdexample.windows.template_debug.x86_64.dll” が目的の成果物。

この GDEXTENSION は texture_rect Class を継承する MoviePlayer という node となる。つまり、 texture_rect の機能は全て持つうえで、動画ファイルを開き再生する機能を追加でもつ。

設置

以下のような gdextension ファイルを作成する。

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"

そして Godot エディタプロジェクトを、例えば demo_02 という名前で作成し、demo_02の以下のプロジェクトディレクトリに設置する。

res://
└─bin/
        gdexample.gdextension
        libgdexample.windows.template_debug.x86_64.dll

参考例として “C:\dev\demos\” というディレクトリ配下に “C:\dev\demos\demo_02” という Godot エンジンプロジェクトを作成する。ここでは説明を簡単にするため、コマンドプロンプトを用いた方法を説明する。

先ずディレクトリの作成。

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

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

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

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

c:\dev\demos\demo_02>mkdir bin
c:\dev\demos\demo_02>cd bin
c:\dev\demos\demo_02\bin>copy c:\dev\gdextension_test2\project\bin\*.dll .

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

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"

再生動画ファイル “mov_hts-samp009.mp4” 固定は 以下のサイト様よりダウンロードし、C:\dev\demos\demo_02\bin のディレクトリに格納する。

SAKURA
https://www.home-movie.biz/free_movie.html

再生には opencv_videoio_ffmpeg481_64.dll が必要なので、以下のように PATH を通しておく。

SET PATH=%PATH%;C:\dev\tools\mingw64\local\opencv-4.8.1\bin

設置後のディレクトリ構成は以下のようになる。

C:\dev\demos\demo_02
│  project.godot
│
└─bin\
        gdexample.gdextension
        libgdexample.windows.template_debug.x86_64.dll
        mov_hts-samp009.mp4

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

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

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

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

res://
└─bin/
        gdexample.gdextension

先ずシーンを設定する。メニューの「シーン」を選ぶとメニューが現れるので「新規シーン」を選択する。

左ペインの「シーン」のタブに「ルートノードを生成:」という欄が現れるので、「その他のノード」を選ぶ。

検索用のノードツリーが現れるので、「Node2D」を選択し、下にある「作成」を押下する。

画面が 3D 表示から 2D 表示に切り替わる。

ここで左ペインの「シーン」のタブ直下にある Node2D を選択し、右クリックするとメニューが現れるので、「子ノードを追加」を選択する。

検索用のノードツリーが現れるので、検索欄に “movieplayer” と入力すると、”MoviePlayer” というノードが現れるのが期待値。

“MoviePlayer” を選択し、下部の「作成」を押下すると左ペインのシーンのノードツリーに MoviePlayer のノードが組み込まれる。

res://MoviePlayer.gd というスクリプトをアタッチするダイアログが出るので、「作成」を押下し確定させる。

以下のような MoviePlayer.gd の雛型が生成される。

MoviePlayer.gd を以下のように編集する。

MoviePlayer.gd

extends MoviePlayer

func _ready():
	var res = open_movie("./bin/mov_hts-samp009.mp4")
	if res == 1:
		print("open success")
	else:
		print("open failure")

注意
func _process(delete) の定義は削除する。残してあると オーバーライドされてしまい、GDScript の _process() が有効となり、C++ の _process() メソッドがコールされなくなってしまう。

編集が終わったら Ctrl + “S” を押下してシーンを保存する。

保存が終わったら 中央ペインで Node2D のシーンが選択されていること(一つしかシーンがないので当然選択されているわけだが)を確認し、”F6″ を押下する。

Godot エンジンで動画が再生される。

本稿の振り返りと次稿について

以上、野良ビルドした opencv ライブラリを GDEXTENSION に組み込み、Godot プロジェクト上で実行することができた。ハマりどころ満載で、何も知識がない状態から突撃すると挫折必至であるが、一つ一つの課題は然程難しいわけではない。

次稿(その5)ではコンピュータビジョンで多用される C++ ライブラリのビルドマラソンを行う。

コメント

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