LWJGL と OpenCV を Clojure で書いていく
動機
- Lisp っぽいSyntaxで OpenGL とか使ってノベルゲームエンジンを作ってみたい
- JVM 言語で書けば、どこでも動くんじゃないかな?
- ゆくゆくは OpenCV + OpenAL も使って動画ファイルを再生したい
- JavaFX...知らない子ですねぇ
出来たこと
- (OpenCV を Clojure で使うことが出来た)<= まだクロスプラットホームに対応させられていない・・・
- LWJGL を、 Java のチュートリアルを変換することで使うことが出来た
レポジトリはこちらです
実装方法
注意:ここの実装方法は、OpenCV の項をクリアした上でなければ動きません・
Project.clj
ここが一番重要な部分だと思います。僕ではうまく実装することが出来なかったので、[こちら] を参考にさせていただきました。
(おそらく上のページを踏襲してOpenCVも実装すればクロスプラットホームにすることが出来ると思いますが、自分の技術力では出来ませんでした)
また、こちらに関してはまるで何をやっているのかわからないので、解説は出来ません(募:解説)
(require 'leiningen.core.eval)
(def JVM-OPTS
{:common []
:macosx ["-XstartOnFirstThread" "-Djava.awt.headless=true"]
:linux []
:windows []})
(defn jvm-opts
"Return a complete vector of jvm-opts for the current os."
[] (let [os (leiningen.core.eval/get-os)]
(vec (set (concat (get JVM-OPTS :common)
(get JVM-OPTS os))))))
(def LWJGL_NS "org.lwjgl")
;; Edit this to change the version.
(def LWJGL_VERSION "3.1.5")
;; Edit this to add/remove packages.
(def LWJGL_MODULES ["lwjgl"
"lwjgl-assimp"
"lwjgl-bgfx"
"lwjgl-egl"
"lwjgl-glfw"
"lwjgl-jawt"
"lwjgl-jemalloc"
"lwjgl-lmdb"
"lwjgl-lz4"
"lwjgl-nanovg"
"lwjgl-nfd"
"lwjgl-nuklear"
"lwjgl-odbc"
"lwjgl-openal"
"lwjgl-opencl"
"lwjgl-opengl"
"lwjgl-opengles"
"lwjgl-openvr"
"lwjgl-par"
"lwjgl-remotery"
"lwjgl-rpmalloc"
"lwjgl-sse"
"lwjgl-stb"
"lwjgl-tinyexr"
"lwjgl-tinyfd"
"lwjgl-tootle"
"lwjgl-vulkan"
"lwjgl-xxhash"
"lwjgl-yoga"
"lwjgl-zstd"])
;; It's safe to just include all native dependencies, but you might
;; save some space if you know you don't need some platform.
(def LWJGL_PLATFORMS ["linux" "macos" "windows"])
;; These packages don't have any associated native ones.
(def no-natives? #{"lwjgl-egl" "lwjgl-jawt" "lwjgl-odbc"
"lwjgl-opencl" "lwjgl-vulkan"})
(defn lwjgl-deps-with-natives []
(apply concat
(for [m LWJGL_MODULES]
(let [prefix [(symbol LWJGL_NS m) LWJGL_VERSION]]
(into [prefix]
(if (no-natives? m)
[]
(for [p LWJGL_PLATFORMS]
(into prefix [:classifier (str "natives-" p)
:native-prefix ""]))))))))
(def all-dependencies
(into ;; Add your non-LWJGL dependencies here
'[[org.clojure/clojure "1.8.0"]
[org.clojure/core.async "0.4.474"]
;; this is for Linux
[opencv/opencv "4.0.0"]
[opencv/opencv-native "4.0.0"]
]
(lwjgl-deps-with-natives)))
(defproject clj-lwjgl-vplayer "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies ~all-dependencies
:injections [(clojure.lang.RT/loadLibrary org.opencv.core.Core/NATIVE_LIBRARY_NAME)]
:jvm-opts ^:replace ~(jvm-opts)
)
OpenCV
ここ をそのまま使いました。以下は自分がやったことをまとめたものです。
OpenCV を Clojure で使う
- Clojure 環境をセットアップしましょう
Clojureの環境をあなたの開発環境(ex. Mac, Linux)にインストールしてください-
ここ に従って Leiningen をインストールしましょう
それが終わったら、新しく作業フォルダ (例えば simple-sample) を作成し、そこで ~lein run~ コマンドが実行できることを確認してください。
-
ここ に従って Leiningen をインストールしましょう
$ cd path/to/simple-sample
$ lein run
user=>
- OpenCV をダウンロードしてビルドしましょう
以下の例に従って OpenCV をビルドしてください
$ mkdir ~/opt
$ cd ~/opt
$ git clone https://github.com/opencv/opencv.git
$ cd opencv
$ git checkout 3.4.0
$ mkdir build
$ cd build
$ cmake -DBUILD_SHARED_LIBS=OFF ..
$ make -j8
- Leiningen plugin のツールである localrepo をインストールしましょう
$ cd ~/.lein
もし ~/.lein フォルダがない場合には作成してください。
$ mkdir ~/.lein
$ cd ~/.lein
次に、profiles.clj を作成して以下の文を追加してください。
{:user {:plugins [[lein-localrepo "LATEST"]]}}
既にファイルがある場合には追記してください。(以下は例)
{:user {:plugins [[lein-cljsbuild "LATEST"]
[lein-figwheel "LATEST"]
[lein-localrepo "LATEST"] ;; ここ
[luminus/lein-template "2.9.9.2"]]
:dependencies [[org.clojure/tools.nrepl "LATEST"]]}}
次に以下のコマンドを実行してください。
$ lein deps
-
OpenCV を Clojure で使えるようにしましょう
先程 OpenCV をインストールしたフォルダ =~/opt= にある以下のファイルが必要になります。- ./build/bin/opencv-400.jar
- ./build/lib/libopencv_java400.[so/dll/dylib]
Linux で OpenCV をビルドした際には .so 、Mac では dylib 、Windows では dll が見つかると思います。
それぞれの環境で必要になりますので、見つかったそれを使いましょう(逆に言うと、このlibファイルが対応していないOSでは動かないので、つまりこれを用いてクロスプラットフォームなアプリケーションを作るのは難しいということになる・・・のかな?)
これらのファイルを以下の例に従って配置してください。
$ mkdir ~/opencv-to-native
$ cp /path/to/opencv-400.jar ~/opencv-to-native
$ mkdir -p ~/opencv-to-native/native/macosx/x86_64 # 各環境に合わせてください
$ cp /path/to/libopencv_java400.dylib # 各環境に合わせてください
以下の表に従って、nativeフォルダに適切なファイルを設置してください。
| OS | | |
|:---------------+----+---------:|
| Mac | -> | macosx |
| Windows | -> | windows |
| Linux | -> | linux |
| SunOS | -> | solaris |
| Architectures | | |
|:---------------+----+---------:|
| amd64 | -> | x86_64 |
| x86_64 | -> | x86_64 |
| x86 | -> | x86 |
| i386 | -> | x86 |
| arm | -> | arm |
| sparc | -> | sparc |
- native library を jar ファイルにパッケージ化しましょう
以下のコマンドを実行して native-jar を作成してください。
$ cd ~/opencv-to-native
$ jar -cMf opencv-native-400.jar native
これによって以下のような構造が出来上がるはずです。
opencv-to-native
|
|- native
| |- macosx
| |- x86_64
| |- libopencv_java400.dylib
|- opencv-400.jar
|- opencv-native-400.jar
- leiningen に作成した jar ファイルをインストールしましょう
以下のコマンドを実行してください。
$ cd ~/opencv-to-native
$ lein localrepo install opencv-400.jar opencv/opencv 4.0.0
$ lein localrepo install opencv-native-400.jar opencv/opencv-native 4.0.0
- 追記
以上であなたの環境でOpenCVをleiningen から簡単に利用することが出来るようになりました。
以下に使用例として project.clj を紹介します。
(defproject simple-sample "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]
[opencv/opencv "4.0.0"]
[opencv/opencv-native "4.0.0"]])
LWJGL
ここ のコードを素直にClojureに変換しました。
- ネームスペースのあれこれ
必要だったものを適宜追加していきました。
(ns clj-lwjgl-vplayer.learn-opencv.learn-opengl.first
(:import (org.lwjgl.opengl GL GL11)
(org.lwjgl Version)
(org.lwjgl.glfw GLFW Callbacks
GLFWErrorCallback GLFWKeyCallback)))
- パラメータの保持
グローバル変数などはClojureの場合では atom や ref を使うようです。
window に協調性のある ref を使っているのは これを使って Exit 等を判定しているからです。
(defonce param (atom {:width 300 ;; 一回宣言すれば良いので defonce を使っています
:height 300}))
(defonce window (ref 0))
-
run 関数
そのままですね。特に難しいことはないと思いますが、static method の書き方 (Class/static-method) という書き方を思い出すのに少し苦労しました。
loop 関数は Clojure の標準ライブラリにあるので、loop_ としました。 -
init 関数
こちらもほとんど変わりませんが、一部難しかった部分があったので詳しく説明しようと思います。
以下の Java コードを見てください。
glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
}
こちら、lambda 式を使っていますね。これ自体はLispの民としてはとてもうれしいのですが、これを Clojure にどう持ってくるのかが大変難しかったです。
イメージとしてJavaの lambda 式は 無名のクラスにつく特定の関数を作る (Overrideする) といった形で、例えばglfwSetKeyCallback の第2引数は GLFWKeyCallback クラスの Invoke となっているようです。(LWJGLのAPI ドキュメントより)
そのため、Clojure 側としては、
1. GLFWKeyCallback を proxy で実装する
2. Invoke 関数を作る
が必要となります。
つまり以下のようになるわけです。
(proxy [GLFWKeyCallback] []
(invoke [window k scancode action mods]
(when (and (== k GLFW/GLFW_KEY_ESCAPE)
(== action GLFW/GLFW_RELEASE))
(GLFW/glfwSetWindowShouldClose @window true))))
- loop_ 関数 && -main 関数
そのままです。java の ( A | B ) ってビット演算のことなんですね。すっかり忘れていました。
ソース全体
(ns clj-lwjgl-vplayer.learn-opencv.learn-opengl.first
(:import (org.lwjgl.opengl GL GL11)
(org.lwjgl Version)
(org.lwjgl.glfw GLFW Callbacks
GLFWErrorCallback GLFWKeyCallback)))
(defonce param (atom {:width 300
:height 300}))
(defonce window (ref 0))
(defn loop_ []
(GL/createCapabilities)
(GL11/glClearColor 1.0 0.0 0.0 0.0)
(while (not (GLFW/glfwWindowShouldClose @window))
(GL11/glClear
(bit-or GL11/GL_COLOR_BUFFER_BIT GL11/GL_DEPTH_BUFFER_BIT))
(GLFW/glfwSwapBuffers @window)
(GLFW/glfwPollEvents)))
(defn init []
(.set (GLFWErrorCallback/createPrint System/err))
(when (not (GLFW/glfwInit))
(throw (IllegalStateException. (str "Unable to initialize GLFW"))))
(GLFW/glfwDefaultWindowHints)
(GLFW/glfwWindowHint GLFW/GLFW_VISIBLE GLFW/GLFW_FALSE)
(let [width (:width @param)
height (:height @param)]
(dosync
(ref-set window (GLFW/glfwCreateWindow width height "Hello World!" 0 0)))
(when (nil? @window)
(throw (RuntimeException. "Failed to create GLFW window")))
(GLFW/glfwSetKeyCallback
@window
(proxy [GLFWKeyCallback] []
(invoke [window k scancode action mods]
(when (and (== k GLFW/GLFW_KEY_ESCAPE)
(== action GLFW/GLFW_RELEASE))
(GLFW/glfwSetWindowShouldClose @window true)))))
(let [vidmode (GLFW/glfwGetVideoMode (GLFW/glfwGetPrimaryMonitor))]
(GLFW/glfwSetWindowPos
@window
(/ (- (.width vidmode) width) 2)
(/ (- (.height vidmode) height) 2)))
(GLFW/glfwMakeContextCurrent @window)
(GLFW/glfwSwapInterval 1)
(GLFW/glfwShowWindow @window)))
(defn run []
(println "Hello LWJGL " + (Version/getVersion))
(try
(init)
(loop_)
(finally
(Callbacks/glfwFreeCallbacks @window)
(.free (GLFW/glfwSetErrorCallback nil)))))
(defn -main []
(run))
;; (-main)