blackthorn-game-engineの2D描画部分を読んでみた!
blackthornというCommon Lisp向けのゲームエンジンの存在を最近知りました。
MOONGIFTさんの紹介記事。
http://www.moongift.jp/2010/11/20101101100/
これの内部を最近読んでいます。今回は2D描画をどうやって実現してるのかなと思って読んでみました。
このエンジンはアニメーションとかイメージの対応表を記述したconfigファイルを読んで自動的にテクスチャの読み込みなどを実現しているようなので、そのあたりどうやって実現しているのかなぁと。あと、OpenGLをどう操作してるのかというところも注目していきたいです。
読むファイルは
ざっとみた感じだとこの3つだけで十分そうです。
OpenGLの初期化
main.lispのmain関数の冒頭部にこれは記述してありました。
(sdl:with-init () (init-mixer) (game-init *game* :player (hostname) :players (hostnames)) (gl:enable :texture-2d) (gl:enable :blend) (gl:blend-func :src-alpha :one-minus-src-alpha) (gl:clear-color 0 0 0 0) (gl:enable :depth-test) (gl:depth-func :lequal) (gl:matrix-mode :modelview) (gl:load-identity)
このような感じで初期化しているようです。game-initは
(defmethod game-init :before ((game game) &key &allow-other-keys) (apply-screen-next game) (if (game-view game) (window (size (game-view game))) (warn "No view for game ~a: Unable to initialize window.~%" game)))
こんな感じで画面を表示するためのウィンドウを作成していました。
2Dの座標変換
初期化が終わったので、次にGLを使っているところ……。game.lispの画面を描画する関数のrenderにありました。
(defmethod render ((game game) xy zmin zmax) (apply-screen-next game) (activate (game-sheet game)) (with-slots (offset size) (game-view game) (gl:with-pushed-matrix (gl:ortho 0 (x size) (y size) 0 -1 1) (gl:with-primitive :quads (render (game-root game) (+ xy offset) zmin zmax)))))
これはどうやら、GLの座標系は2D描画するのには不便なので、640x480とかそういうピクセル単位の座標に変換しているみたいですね。どうも、ゲーム内部の要素に再帰的にrenderを呼ぶような実装になっていそうです。さしあたって興味は無いのでこれくらいで。
ただ、ここで気になったのは、この範囲で座標変換してしまうと2D、3D混在の画面構成は難しそうだなということ。東方みたいな背景だけ3DのSTGとか作るときに不便そうですね。
テクスチャのロードとか
blackthornでは
- sheetと呼ばれる大きなテクスチャをconfigファイルから動的に生成する
- sheetは各ゲームシーンに多くて一つ。つまりゲーム1つだとテクスチャは多くても数枚。
- 各1枚1枚の画像はimageというクラスが担当。がデータとしてはsheetの座標を持つだけ。
- animeもこのsheetの中で表現する。imageの配列みたいな感じ。
とこういう構成になっているみたいです。これ理解するのに時間がかかりましたね。graphcs.lispの内部はこれを前提にして読むと読みやすくなるかもしれません。
SDLのsurfaceからGLのテクスチャに変換する部分は
(defun load-and-convert-images (sources options) (labels ((color (rgba) (when rgba (destructuring-bind (r g b &optional a) rgba (apply #'sdl:color :r r :g g :b b (if a (list :a a)))))) (point (xy) (when xy (destructuring-bind (x y) xy (funcall #'sdl:point :x x :y y))))) (iter (for source in sources) (assert (probe-file source))) (let* ((images (iter (for source in sources) (for option in options) (collect (sdl-image:load-image source :color-key (color (cdr (assoc :color-key option))) :color-key-at (point (cdr (assoc :color-key-at option))))))) (total-width (ceiling-expt (or (iter (for image in images) (sum (sdl:width image))) 0) 2)) (total-height (ceiling-expt (or (iter (for image in images) (maximize (sdl:height image))) 0) 2)) (surface (sdl:create-surface total-width total-height :bpp 32 :pixel-alpha t))) (iter (with x = 0) (for image in images) (sdl:draw-surface-at-* image x 0 :surface surface) (incf x (sdl:width image))) surface)))
この関数で、一枚の大きなsurfaceを作成します。
つぎに
(defun surface-to-texture (surface) (let ((texture (car (gl:gen-textures 1))) (w (sdl:width surface)) (h (sdl:height surface))) (gl:bind-texture :texture-2d texture) (gl:tex-parameter :texture-2d :texture-min-filter :nearest) (gl:tex-parameter :texture-2d :texture-mag-filter :nearest) (gl:tex-image-2d :texture-2d 0 :rgba w h 0 :rgba :unsigned-byte (sdl-base::with-pixel (pixels (sdl:fp surface)) (sdl-base::pixel-data pixels))) texture)) (defun load-source-to-texture (source &optional options) (surface-to-texture (load-and-convert-images (if (listp source) source (list source)) options)))
これでsurfaceからGLのテクスチャに変換しているようです。あとは描画の部分を理解すればなんとかなるかな。
テクスチャの描画
(defmethod draw ((image image) xy z) (with-slots (size tex-offset tex-size) image (let* ((x1 (truncate (x xy))) (x2 (+ x1 (x size))) (y1 (truncate (y xy))) (y2 (+ y1 (y size))) (tx1 (x tex-offset)) (tx2 (+ tx1 (x tex-size))) (ty1 (y tex-offset)) (ty2 (+ ty1 (y tex-size)))) (gl:tex-coord tx1 ty1) (gl:vertex x1 y1 z) (gl:tex-coord tx2 ty1) (gl:vertex x2 y1 z) (gl:tex-coord tx2 ty2) (gl:vertex x2 y2 z) (gl:tex-coord tx1 ty2) (gl:vertex x1 y2 z))))
こんな感じで普通にテクスチャはってるだけですね。
こんな感じで真似すればGLで2Dゲームはさっくり作れる気がします。