1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LWJGL と OpenCV を Clojure で書いていく

Last updated at Posted at 2018-04-16

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~ コマンドが実行できることを確認してください。
$ 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)
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?