SDL2 イメージ操作

コードを載せる

import SDL.Video (Renderer)
import SDL.Video.Renderer (Surface,Texture,SurfacePixelFormat(..),Rectangle(..),PixelFormat(..)
                          ,rendererDrawColor,clear,destroyTexture
                          ,createTextureFromSurface,copyEx,present
                          ,lockSurface,unlockSurface,surfacePixels,surfaceFormat,createRGBSurfaceFrom)
import SDL.Vect (Point(P),V2(..),V4(..))
import SDL.Internal.Numbered (fromNumber)
import qualified SDL.Raw.Types as SDLT
import Foreign.Ptr (castPtr)
import Foreign.ForeignPtr (newForeignPtr_)
import Foreign.Storable (peek)
import qualified Data.Vector.Storable.Mutable  as VM
import System.Random.Shuffle (shuffleM)
import Data.Word (Word8)

draw :: Renderer -> [Surface] -> IO ()
draw re imageS = do
  imageTO <- mapM (createTextureFromSurface re) imageS
  let imageS0 = head imageS

  -- メモリ操作のため surfaceをロックする
  lockSurface imageS0

  -- surfaceのPixelForat情報があるポインタを取得
  SurfacePixelFormat pointerPixFormat <- surfaceFormat imageS0

  -- ポインタ(アドレス)の情報を読む
  surPixFormat <- peek pointerPixFormat

  -- このPixelFormatは SDL.Raw.Type で定義されてゐるもので
  -- pixelFormatFormat といふデータに SDL.Video.Renderer で定義される PixelFormat の情報が
  -- Word32といふ型で格納されてゐる
  -- これは print で表示すると 整数なのだが これを fromNumber で PixelFormatの形式に變換する
  let sPixFormat = fromNumber (SDLT.pixelFormatFormat surPixFormat) :: PixelFormat

  -- イメージのsurfaceのピクセル情報が格納されてゐるアドレスを得る(ポインタ)
  pointer0 <- surfacePixels imageS0

  -- データからsurfaceをつくるためには IOVector Word8 といふ型でデータを保持しなければならない
  -- IOVector は MVector (PrimState IO) と同義であり
  -- surfacePixels函數で得られる Ptr () を castPtr で Ptr Word8 へ變換し
  -- Ptr Word8 を newForeignPtr_ で ForeignPtr Word8 へ變換する
  frPointer <- newForeignPtr_ (castPtr pointer0)

  -- イメージの1ピクセルは 4つのWord8(0〜255)のデータで表される
  -- Word8の総数は 4*64*64 となる (64*64ピクセルだから)
  -- この長さを ポインタを起点としたアドレスから讀み込み MVector型のデータを得る
  let mvector = VM.unsafeFromForeignPtr0 frPointer (4*64*64) :: (VM.MVector (VM.PrimState IO) Word8)
  -- ロックを解除
  unlockSurface imageS0

  -- mvector のクローンを作成
  -- mvectorを書き替えると imageS0 の内容がそのまま書き替はる
  -- surface が變更されると 表示してゐたtextureも變はる
  mvector2 <- VM.clone mvector

  -- 4x4のピクセルを單位として ランダムにピクセル情報を變更する
  -- 水の中にあるやうな効果を出すことができた
  mapM_ (\y -> mapM_ (\x -> sfl4x4 mvector2 (V2 x y)) [0..15]) [0..15]

  -- MVector Word8 型のデータ, surfaceのサイズ, ピッチ(1行のピクセル(64px)のバイト數),
  -- そして 先程求めたsPixFormat (このイメージはABGR8888だった)を使い 新たなsurfaceをつくる
  newImageS0 <- createRGBSurfaceFrom mvector2 (V2 64 64) (4*64) sPixFormat

  newImageT <- createTextureFromSurface re newImageS0
  let imageTextures = imageTO ++ [newImageT]
  initDraw re
  imageDraw re imageTextures 
  mapM_ destroyTexture imageTextures 
  present re

きっかけは ブロックを使った操画(ゲーム)を考へてゐて そのブロックのイメージを 「もうちょっと柔らかいものにしたいな〜」 といふことだった
BABA IS YOU といふ 超名作ゲームのブロックの ほわほわする感じが すごく良くて そんなやうなイメージが創れたらいいな〜 といふ 安直な思ひがあった
しかし おそらく それをやるためには イメージを大量に用意して アニメーションさせるのではなく イメージそれ自體にアクセスして その情報を變へるようなことが必要だ と考へた
イメージにアクセスするためには SDL2の場合 surfaceの情報を取得する必要がある
SDL.Video.Renderer のドキュメントを見ると

surfacePixels :: MonadIO m => Surface -> m (Ptr ())

とあり
Obtain the pointer to the underlying pixels in a surface. You should bracket this call with lockSurface and unlockSurface, respectively.
と説明されてゐる
要は 「ほら 情報にアクセスできるポインタだよ これで後はよろしく」
みたいな感じだ
何の例もない
どうすればいいか どこにも書いてゐない

ちなみに 仮にデータが得られたとして それを どう使ふのかといへば

createRGBSurfaceFrom
  :: (Functor m, MonadIO m)
=> IOVector Word8  --(The existing pixel data)
 -> V2 CInt  --(The size of the surface)
 -> CInt  --(The pitch - the length of a row of pixels in bytes)
 -> PixelFormat  --(The bit depth,red,green,blue and alpha mask for the pixels)
 -> m Surface

最終的に IOVector Word8 といふ型でデータが供給できればよい
んで Data.Vector.Storable.Mutable のドキュメントを見ると

unsafeFromForeignPtr0
  :: Storable a
=> ForeignPtr a
 -> Int
 -> MVector s a

これが 私の見たところ このモジュール内で ポインタからMVector をつくる唯一(offsetなしなら)の方法だった
ちなみに つくりたいのは IOVector Word8 である
ここで

type IOVector = MVector RealWorld

と書いてある
また
MVector s a の s は (PrimMonad m => PrimState m) であり
PrimState IO が RealWordl である(らしい) https://stackoverflow.com/questions/8959226/constructing-iovector-from-storable-mvector

この邊の理屈は 正直よく分からんかった
が 先程の

unsafeFromForeignPtr0 :: Storable a => ForeignPtr a -> Int -> MVector s a

の a を Word8 として s を PrimState IO とすれば 返り値の型は MVector RealWorld Word8
すなはち IOVector Wrod8 となる
つまり ForeignPtr Word8 型のポインタと データサイズ(Int)を與へることで
目的の型をもつデータがゲットできる といふわけだ
そこで Ptr () を ForeignPtr Word8 にしなければならない
Foreign.Ptr モジュールのドキュメントによると

castPtr :: Ptr a -> Ptr b

といふのがある
これで () を Word8にできるのでは?
といふことで castPtr を適用し
Foreign.ForeginPtr モジュールにある

newForeignPtr_ :: Ptr a -> IO (ForeignPtr a)

を適用して めでたく ForeginPtr Word8 をつくりあげることができた

さて 一番説明がやっかいさうなのは PixelFormat だ
SDL.Video.Renderer に PixelFormatは定義されてゐて
RGB888 や RGBA8888 ABGR8888 BGRA8888 など いくつものフォーマットがある
最終目的を思ひ出して欲しい
createRGBSurfaceFrom に必要なデータを揃えることだった
IOVector Word8 はゲットしたし イメージサイズも分かってゐる
ピッチといふのは ピクセル一行分が 何バイトに相當するかを示すもので
1ピクセルにつき4バイト必要な RGBA8888 や ABGR8888などは (横の幅(px)) × 4 で求まる
殘るは PixelFormat だけなのだが
色々と適當に フォーマットを試してみて できたイメージの結果を確認していくと
これは ABGR8888 だな といふのが分かる
それ以外のものは 明らかに色が もとのイメージと異なったからだ
(といふことは ある意味 PixelFormatだけ變へてやれば 瞬時に画像の色合いを變へることができるわけだ)
けれども これから色々なイメージを讀み込まうと思ってゐる場合
常に ABGR8888とは限らないだらう
だから イメージの情報を讀み取ることで PixelFormatの情報を得たいわけだ
これに關して

surfaceFormat :: MonadIO m => Surface -> m SurfacePixelFormat

といふ函數が SDL.Video.Renderer で用意されてゐる
しかし 返ってくるのは なぜか PixelFormat ではなく SurfacePixelFormat といふ謎なやつだ
そこの部分のドキュメントを見ると

newtype SurfacePixelFormat

Constructors:
SurfacePixelFormat (Ptr PixelFormat)
となってゐる
Ptrだとおおお?!? すなはち surface の PixelFormat を知りたくて尋ねたら 「ハイ! アドレスだよ! 見といてね?」 なんて言はれ ポカーン とするしかない状況が生じたのだ

ここで メモリのアドレスにあるデータを見る方法が必要になってくる
これは 色々検索して

https://naohaq.github.io/

を見たときに分かったのだが
「peek」といふものだ
Foreign.Storable モジュールに書かれてゐる
やった〜!
といふことで peekを使ひ その内容をPixelFormatとして createRGBSurfaceFrom に入れてみた
が・・・エラー
なんやねん といふことで peekした内容を表示してみたら

PixelFormat {pixelFormatFormat = 376840196, pixelFormatPalette = 0x0000000000000000, pixelFormatBitsPerPixel = 32, pixelFormatBytesPerPixel = 4, pixelFormatRMask = 255, pixelFormatGMask = 65280, pixelFormatBMask = 16711680, pixelFormatAMask = 4278190080}

な〜〜?! SDL.Video.Renderer のドキュメントで
SurfacePixelFormat (Ptr PixelFormat) とあるうち PixelFormatのリンクをクリックすると
全然別のドキュメントに飛ばされる
それが
SDL.Raw.Types だ
ここで定義される PixelFormatは

data PixelFormat

Constructors:

PixelFormat  
pixelFormatFormat :: !Word32     
pixelFormatPalette :: !(Ptr Palette)     
pixelFormatBitsPerPixel :: !Word8    
pixelFormatBytesPerPixel :: !Word8   
pixelFormatRMask :: !Word32  
pixelFormatGMask :: !Word32  
pixelFormatBMask :: !Word32  
pixelFormatAMask :: !Word32

この内容は さきほど プリントさせたものと合致してゐる
んで どないしろっちゅーねん!
まう一度 SDL.Video.Renderer にある PixelFormat のドキュメントとにらめっこした
何か このふたつの PixelFormatには 關聯性がある筈なんだ・・・
ん?

 FromNumber PixelFormat Word32
 ToNumber PixelFormat Word32

Word32?
さういへば SDL.Raw.Types のPixelFormat の最初に Word32の型があったな・・・
それは 意味不明な數字だった・・・
そして fromNumber といふ函數があるらしい・・・
まさか あの數字が ABGR8888 を表はしてるとか???
fromNumber函數は モジュール SDL.Internal.Numbered にあった
これを SDL.Raw.Types の PixelFormat 内にある PixelFormatFormat の數字に適用すると・・・
動いた!!!

Youtube のストリーム配信で 説明しやうとやってみた

www.youtube.com

コードは githubに載せてゐる

github.com

一言いはせてほしい
Haskell は 冒険だ!