CommonLispとSDLで色々 #1

# CommonLispとSDLで色々 #1

〜SDL_Renderer、SDL_Surface、SDL_Texture周りについて〜

## はじめに

[SDL2のリファレンス](https://sdl2referencejp.osdn.jp/ApiByCategory.html)と[cl-sdl2のリファレンス](https://quickref.common-lisp.net/sdl2.html)をにらめっこしながらCommon Lispでどう書いたら良いのか悩みつつという感じなんですが、まとめを兼ねて色々とやっていきます。

### 環境

- Debian GNU/Linux 10.2
- Embeddable Common Lisp(ECL) 16.1.3

#### ライブラリ
- libsdl2
- libsdl2-image

#### Common Lispパッケージ
- sdl2
- sdl2-image

## SDL_Renderer

10年くらい前にC言語でSDL1系を少し触った感じで、SDL2は色々と変わっていたので、SDLの描画の仕組みを見直してみます。

### ウインドウとレンダラーの作成

SDLでは、

- SDLの初期化
- ウインドウ作成
- レンダラー作成

の3つの処理を行い、初めて描画が可能な状態になります。Common Lispのコードにすると、以下のような形です。

```common-lisp
(sdl2:init :video)
(let* ((window (sdl2:create-window :title "SDL Window"
                                   :x sdl2-ffi:+sdl-windowpos-centered+
                                   :y sdl2-ffi:+sdl-windowpos-centered+
                                   :w 800
                                   :h 600
                                   :flags '(:shown)))
       (renderer (sdl2:create-renderer window nil '(:ACCELERATED))))
  ;; 処理
  )
```

`create-window`によってウインドウを作成し、`create-renderer`に作成したウインドウを渡すことによってレンダラーが作成されます。

`create-window`の`:flags`キーワードに渡せる引数は、[SDLのドキュメント](https://sdl2referencejp.osdn.jp/SDL_WindowFlags.html)に書かれているんですが、Common Lispのためそのまま渡すことはできません。`SDL_WINDOW_SHOWN`という指定は、`:shown`と言った形で、最後の部分にコロンを付けて渡す形になります。例えばOpenGLを有効化したい場合には、次のように指定します。

```common-lisp
:flags `(:shown :opengl)
```

`create-window`の次に呼び出す`create-renderer`は、レンダラーと呼ばれるウインドウの2Dレンダリングコンテキスト(レンダリング情報)を作成します。上記のコードでは、このオプションに`:ACCELERATED`を渡しています。これはハードウェアアクセラレートを有効化する意味を持ちます。オプションは、[RenderingFlags](https://sdl2referencejp.osdn.jp/SDL_RendererFlags.html)に記載されており、どのようなレンダラーを作成するのかを指定することができます。これもcreate-windowに渡したflagsと同様、Common Lispのcl-sdl2では、最後の部分にコロンを付けてキーワードとして指定します。

- :SOFTWARE ソフトウェア(CPU)での描画を行う
- :ACCELERATED 可能ならハードウェア(GPU)で描画を行う
- :PRESENTVSYNC 垂直同期を行う
- :TARGETTEXTURE テクスチャへのレンダリングに対応

### レンダラーへの描画の手順

```common-lisp
(sdl2:render-clear render)
(sdl2:set-render-draw-color renderer 0 0 #xFF #xFF)
(sdl2:render-fill-rect renderer
  (sdl2:make-rect 10 10 780 580))
(sdl2:render-present renderer)
```

レンダラーに対してSDLの描画命令を使って描画を行う際は、最初に`(render-clear render)`を実行し、描画内容をクリアします。`set-render-draw-color`によってどの色でクリアするかを指定することもできますが、何もしなければ黒でクリアされます。ゲームなどの場合で毎回全体を描画しなおす場合には、前回描画下内容をクリアするために必ずこれを呼び出します。

`set-render-draw-color`は描画の色を指定します。`0 0 #xFF #xFF`は、RGBAの並びに対応しており、色の値を0から255(#xFF)の範囲で指定します

- R(赤) = 0
- G(緑) = 0
- B(青) = #xFF
- A(透過値) = #xFF

`render-fill-rect`は、四角形を描画し塗りつぶします。その際の色は直前で`set-render-draw-color`を使って指定した色になります。

`render-fill-rect`の第2引数に渡している`make-rect`を見ます。

```common-lisp
(sdl2:make-rect 10 10 780 580)
```
という箇所は、Rect(矩形)の情報を作成するものです。`make-rect`で作成した矩形情報を`render-fill-rect`に渡す必要があります。

最後の`render-present`はこれまでの描画内容をウインドウに反映します。

まとめると、最小の描画の手順は以下のようなシンプルなものになります。

```common-lisp
(sdl2:render-clear renderer)
;; この間にrendererへの描画処理を記述
(sdl2:render-present renderer)
```

### サンプルコード
https://gitlab.com/hu.moonstone/sdl-playgrounds/tree/master/sdl-window

### 実行結果
![sdl-window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568876/b437e1e1-bd48-3441-2b84-6a7a74b1120d.png)



## SDL_Surface

`create-renderer`で先ほど作成したレンダラーは、ウインドウ内に描画する内容と密接に関わるものでした。ゲームで実際に毎回画面の内容を書き換える際は、書き換えるものと書き換えたくないものが出てくると思います。表示したい情報、ピクセル情報をレンダラーに描画する方法以外で格納しておく方法として、SDL_SurfaceとSDL_Textureというデータ構造が存在します。

### 画像表示
SDL_Surfaceはハードウェアに依存しないピクセル情報を格納します。sdl2-imageパッケージを使うことで利用できる`load-image`で画像をロードすると、SDL_Surfaceデータが返ってきます。

```common-lisp
(let ((image (sdl2-image:load-image "resources/images/lisp.png")))
;; imageはSDL_Surface
```

SDL_Surfaceの情報はコンピューターのメモリ上に確保されるため自由にピクセル単位で情報を書き込むことができます。その反面、速度は遅いです。

SDL_Surfaceの情報は、そのままレンダラーに渡すことができません。一度、SDL_SurfaceからSDL_Textureを作成し、SDL_Textureをレンダラーに書き込むことになります。SDL_Surfaceの情報はコンピューターのメモリ上に確保されますが、SDL_TextureはコンピューターのGPUメモリ上に確保されます。GPU上のメモリは直接操作することができない反面、高速とされています。

- 画像をロード、SDL_Surfaceの作成
- SDL_SurfaceからSDL_Textureを作成
- SDL_Textureをレンダラーに書き込み

という一連の流れをコードにすると、次のような形になります。

```common-lisp
(let* ((image (sdl2-image:load-image "resources/images/lisp.png"))
       (width (sdl2:surface-width image))
       (height (sdl2:surface-height image))
       (tex-image (sdl2:create-texture-from-surface renderer image)))
  (sdl2:query-texture tex-image)
  (sdl2:render-copy renderer tex-image
                    :source-rect (sdl2:make-rect 0 0 width height)
                    :dest-rect (sdl2:make-rect 0 0 width height))
  (format t "width = ~A height = ~A~%" width height)
  (let ((tex-image (sdl2:create-texture-from-surface renderer image)))
    (multiple-value-bind (tex-format tex-access tex-width tex-height)
        (sdl2:query-texture tex-image)
      (format t "width = ~A height = ~A~%" tex-width tex-height))
    (sdl2:render-copy renderer tex-image
                      :source-rect (sdl2:make-rect 0 0 width height)
                      :dest-rect (sdl2:make-rect 0 0 width height))))
```

`create-texture-from-surface`によって、読み込んだ画像から作成したimageというSDL_Surfaceを使いSDL_Textureを作成しています。

`query-texture`は、作成したSDL_Textureからテクスチャの各種情報を得ることができます。`(values texture-format access width height)`という形で返されるため、`multiple-value-bind`を使って取得する必要があります。SDL_Surfaceに対して、`surface-width`、`surface-height`を使っても上記の例では同じ値が返ってきます。

画像のサイズは、SDL_Textureの情報をレンダラーにコピーする際に使うことになります。`render-copy`の第3引数は、コピー元のSDL_Textureのどこの矩形領域を対象するのか、第4引数は、コピー先になるSDL_Rendererのどこの矩形領域に描画を行うのかを指定します。この値を調整することで、読み込んだ画像の左上の4分の1の矩形領域を、レンダラーの画面中央に倍のサイズに引き伸ばして表示すると言った処理も行うことができるようになります。

### サンプルコード
https://gitlab.com/hu.moonstone/sdl-playgrounds/tree/master/sdl-surface

### 実行結果
![sdl-surface.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568876/ca1ba9b9-a598-0603-fc5e-08e23e3e9a56.png)


## SDL_Textureによるレイヤー管理

SDL_Textureは描画する内容を保持しておくことができるため、レイヤーとして利用できます。今までは、ウインドウから作成したレンダラーに対して直接描画したりコピーしたりしていましたが、`set-render-target`を使うことで、描画命令、コピー命令の対象を任意のテクスチャーに変更することができます。スクロールアニメーションさせる縦や横に長い背景と一度描画すれば書き換える必要のないものを直接描画している場合、毎回描画の際に描画命令を使って絵を構築する必要がありますが、別々のテクスチャに用意しておくことで必要だけを書き換えて最終的にレンダラーにコピーすると言った手法が取れるようになります。

```common-lisp
(sdl2:set-render-target renderer texture)
```
第1引数にはウインドウから作成したレンダラーを指定し、第2引数には描画対象にするテクスチャを指定します。このオペレーターに指定するテクスチャは、専用のものを用意する必要があります。`create-texture`を使って専用のテクスチャを作ります。

```common-lisp
(sdl2:create-texture renderer pixel-format access width height)
```
第1引数にはウインドウから作成したレンダラーを指定します。`pixel-format`には、このテクスチャのピクセルの格納順、各色情報のビット数をキーワード形式で指定します。重要なのは3番目のaccessです。ここにtargetキーワードを設定することで、このテクスチャをレンダリング対象として設定することを許可します。

```common-lisp
(sdl2:create-texture renderer :rgba8888 :target 800 600)
```

これによって、800×600のサイズのテクスチャ領域が確保されます。試しに3つのテクスチャ領域を作成し、それぞれに画像を描画し、最後にレンダラーにコピーする処理を行います。


```common-lisp
(let ((offscreen1 (sdl2:create-texture renderer :rgba8888 :target 800 600))
      (offscreen2 (sdl2:create-texture renderer :rgba8888 :target 800 600))
      (offscreen3 (sdl2:create-texture renderer :rgba8888 :target 800 600)))
  ;; 処理
)
```

offscreen1〜3という名前でテクスチャを作成します。最終的にはこれらを1番目から順にレンダラーにコピーしていく形になりますが、offscreen1の上にoffscreen2をコピーすると、offscreen1の描画が全て上書きされてしまうため、重ね合わせが行われた際、どのように色をブレンドするかを設定します。

```common-lisp
(sdl2:set-texture-blend-mode offscreen1 :none)
(sdl2:set-texture-blend-mode offscreen2 :blend)
(sdl2:set-texture-blend-mode offscreen3 :blend)
```

offscreen1はその下に何も描画しないため、ブレンドモードは`none`を設定しています。offscreen2とoffscreen3は、`blend`を設定します。この設定をしていると、重ね合わせた際、そのテクスチャのある色のα値(透過度)が255未満の場合に、下の色とブレンドする処理になります。

具体的なコードを書きます。以下のような処理にします。

- offscreen1にはウインドウから10ピクセル分小さい青く塗りつぶした矩形を表示
- offscreen2にはウインドウ一杯に広げた画像
- offscreen3にはα値を255未満に設定して画面の半分の矩形領域を塗りつぶし

```common-lisp
;; レンダリングターゲットを有効化したテクスチャを3つ作成
(let ((offscreen1 (sdl2:create-texture renderer :rgba8888 :target 800 600))
      (offscreen2 (sdl2:create-texture renderer :rgba8888 :target 800 600))
      (offscreen3 (sdl2:create-texture renderer :rgba8888 :target 800 600)))
  ;; それぞれのテクスチャのブレンドモードを設定
  (sdl2:set-texture-blend-mode offscreen1 :none)
  (sdl2:set-texture-blend-mode offscreen2 :blend)
  (sdl2:set-texture-blend-mode offscreen3 :blend)

  ;; 画像の読み込み
  (let* ((image (sdl2-image:load-image "resources/images/lisp.png"))
         (width (sdl2:surface-width image))
         (height (sdl2:surface-height image))
         ;; 画像のSDL_SurfaceからSDL_Textureを作成
         (tex-image (sdl2:create-texture-from-surface renderer image)))

    ;; レンダリングのターゲットをoffscreen1に設定
    (sdl2:set-render-target renderer offscreen1)
    ;; offscreen1のクリア色を黒、透過なしに設定しクリア
    (sdl2:set-render-draw-color renderer 0 0 0 #xFF)
    (sdl2:render-clear renderer)
    ;; offscreen1に対し、青で透過なしの色で矩形領域を塗りつぶし
    (sdl2:set-render-draw-color renderer 0 0 #xFF #xFF)
    (sdl2:render-fill-rect renderer (sdl2:make-rect 10 10 780 580))

    ;; レンダリングのターゲットをoffscreen2に設定
    (sdl2:set-render-target renderer offscreen2)
    ;; 完全に透明な色でoffscreen2をクリア
    (sdl2:set-render-draw-color renderer 0 0 0 0)
    (sdl2:render-clear renderer)

    ;; 画像をoffscreen2に描画
    (sdl2:render-copy renderer tex-image
                      :source-rect (sdl2:make-rect 0 0 width height)
                      :dest-rect nil)

    ;; レンダリングのターゲットをoffscreen3に設定
    (sdl2:set-render-target renderer offscreen3)
    ;; 完全に透明な色でoffscreen3をクリア
    (sdl2:set-render-draw-color renderer 0 0 0 0)
    (sdl2:render-clear renderer)

    ;; 黄色の透明度(α値)200の矩形領域を描画(画面半分くらい)
    (sdl2:set-render-draw-color renderer #xFF #xFF 0 200)
    (sdl2:render-fill-rect renderer (sdl2:make-rect 20 20 400 560))

    ;; 画像のSurfaceとTextureを解放
    (sdl2:free-surface image)
    (sdl2:destroy-texture tex-image))
  
  ;; レンダリングターゲットをウインドウから作成したレンダラーに設定
  (sdl2:set-render-target renderer nil)
  (sdl2:render-clear renderer)
   
  ;; offscreen1をrendererにコピー
  (sdl2:render-copy renderer offscreen1
                    :source-rect nil
                    :dest-rect nil)

  ;; offscreen2をrendererにコピー
  (sdl2:render-copy renderer offscreen2
                    :source-rect nil
                    :dest-rect nil)

  ;; offscreen3をrendererにコピー
  (sdl2:render-copy renderer offscreen3
                    :source-rect nil
                    :dest-rect nil)

  ;; offscreen1〜offscreen3を解放
  (sdl2:destroy-texture offscreen1)
  (sdl2:destroy-texture offscreen2)
  (sdl2:destroy-texture offscreen3))

;; rendererの描画内容をウインドウに反映
(sdl2:render-present renderer)
;; rendererを解放
(sdl2:destroy-renderer renderer)
```

Rendererへのコピーには、`render-copy`を使います。`:source-rect`、`:dest-rect`にnilを設定している場合は、テクスチャの全領域をrendererの全領域に対してコピーします。

```common-lisp
(sdl2:render-copy renderer texture :source-rect :dest-rect)
```

もし一部だけをコピーしたり、拡大縮小したい場合には、`sdl2:make-rect`を使って矩形領域を指定します。

### サンプルコード
https://gitlab.com/hu.moonstone/sdl-playgrounds/tree/master/sdl-texture

### 実行結果
![sdl-texture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568876/c8514b2d-6186-d75b-4b2d-5c3b85871336.png)


## 参考
- [ECL User's guide](https://common-lisp.net/project/ecl/static/ecldoc/index.html)
- [ANSI Common Lisp(PDF)](https://7chan.org/pr/src/ANSI_Common_Lisp_-_Paul_Graham.pdf)
- [SDL Document](https://wiki.libsdl.org/FrontPage)
- [SDL2.0 日本語マニュアル](https://sdl2referencejp.osdn.jp/index.html)
- [Quickref sdl2](https://quickref.common-lisp.net/sdl2.html)
- [Quickref sdl2-image](https://quickref.common-lisp.net/sdl2-image.html)