euphonictechnologies’s diary

Haskell超初心者の日記です。OCamlが好きです。

follow us in feedly

Haskell - GHC for iOS : OpenGLで3Dグラフィックスを表示してみる、のタッチ操作編

前回までのあらすじ

blog.euphonictech.com

今回は

やっつけハックですが、タッチに反応するようにしてみます。

完成図

画面をうりうりすると立方体がくるくるします。

何が必要?

XcodeでObjective-C側にタッチに反応するためのメソッドを書く必要があります。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

それぞれ読んで字のごとく、touchesBegan -> touchesMoved -> touchesEnded の順番です。touchedMovedはタッチされている間ずっと細かく呼び出され続けます。

どうするの?

Objective-C側のtouchesBegan, touchesMoved, touchesEndedはタッチされた座標とタッチ開始時の座標の2つをメンバ変数に更新するようにします。描画に反映させるのはdrawFrame関数にそれらの座標をすべて渡して画面に反映させます。つまり、新たに4つの変数をdrawFrame関数に渡せるようにします。

変更点は

まず、Objective-C側は上記3メソッドとメンバ変数を用意します。

@interface GameViewController () {
    float ox, oy;
    float x, y;
}

#pragma mark - Touches

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    CGPoint loc = [self getPositionFromTouch:[touches anyObject]];
    ox = loc.x;
    oy = loc.y;
    x = loc.x;
    y = loc.y;
    touchesBegan(loc.x, loc.y);
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    CGPoint loc = [self getPositionFromTouch:[touches anyObject]];
    x = loc.x;
    y = loc.y;
    touchesMoved(loc.x, loc.y, ox, oy);
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    CGPoint loc = [self getPositionFromTouch:[touches anyObject]];
    x = 0;
    y = 0;
    touchesEnded(loc.x, loc.y, ox, oy);
}

こんな感じです。 main.mに、c_mainで受け取るdrawFrame関数の受け取る引数を増やします。

void (*drawFrame)(double, double, double, double, double, double, double);

void c_main(void (*_drawFrame)(double, double, double, double, double, double, double)) {
...
}

で、呼び出し方をGameViewController.m内で変えます。

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    extern void (*drawFrame)(double, double, double, double, double, double, double);
    drawFrame(self.timeSinceLastUpdate, self.view.bounds.size.width, self.view.bounds.size.height, x, y, ox, oy);
}

Haskell側の変更

Haskell側で必要な変更は

  • 指でうねうねして回した分を指を離した後も覚えておくためのDoubleを入れるIORef2つ
  • そのIORef2つとDouble4つをdrawFrameの引数に加える。
  • モデルを指の座標に反応して回転させる

です。

まずはIORefを2つ足します

どっかで覚えておかないといけないので、回転量を記憶しているrotRefと同じようにmain内でIORefとして定義します。

main = do
    putStrLn "Haskell start"
    rotRef <- newIORef 0.0
    deltaXRef <- newIORef 0.0
    deltaYRef <- newIORef 0.0

    drawFrame <- mkDrawFrame $ drawFrame rotRef deltaXRef deltaYRef
    c_main drawFrame

とします。

引数にたくさん足す

これは簡単。あわせてFFIを通してexposeする定義も変更します。

foreign import ccall safe "c_main" c_main :: FunPtr (Double -> Double -> Double -> Double -> Double -> Double -> Double -> IO ()) -> IO ()
foreign import ccall "wrapper" mkDrawFrame :: (Double -> Double -> Double -> Double -> Double -> Double -> Double -> IO ())
                                           -> IO (FunPtr ((Double -> Double -> Double -> Double -> Double -> Double -> Double -> IO ())))

...

drawFrame :: IORef Double
          -> IORef Double
          -> IORef Double
          -> Double
          -> Double
          -> Double
          -> Double
          -> Double
          -> Double
          -> Double
          -> IO()
drawFrame rotRef deltaXRef deltaYRef timeSinceLastUpdate width height touchX touchY origTouchX origTouchY = do

これはキモいですね。

モデルを指に合わせて回転させる

今回は楽をしましょう。単純にヨー軸、ピッチ軸(といいますが、立方体なのでどの軸でもいいです)周りに指の座標のxとyのデルタだけ回転させましょう。drawFrameはタッチ開始座標と現在タッチ座標を引数として受け取るので、それを使いましょう。

    -- Touch
    modifyIORef deltaXRef $ \x -> if touchX == 0 || (touchX - origTouchX) == 0 then x else touchX - origTouchX
    modifyIORef deltaYRef $ \y -> if touchY == 0 || (touchY - origTouchY) == 0 then y else touchY - origTouchY
    yaw <- readIORef deltaXRef
    pitch <- readIORef deltaYRef

    let (modelViewProjectionMatrix, normalMatrix) = getMatrices aspect rotation yaw pitch

こうして、deltaXRefとdeltaYRefに覚えさせておきますそうすると指を離した後もその回転具合を覚えておけます。 この回転具合をgetMatricesに渡してmodelViewProjectionMatrixに反映させます。

getMatrices :: Double -> Double -> Double -> Double -> (M.Matrix Double, M.Matrix Double)
getMatrices aspect rotation yaw pitch =
    (modelViewProjectionMatrix, normalMatrix)
    where
        modelViewProjectionMatrix = modelViewMatrix * projectionMatrix
        normalMatrix = M.transpose $ MH.inverse $ M.submatrix 1 3 1 3 modelViewMatrix
        projectionMatrix = MH.matrixPerspective (MH.radiansFromDegrees 65.0) aspect 0.1 100.0
        modelViewMatrix = yawRotate * pitchRotate * modelViewMatrixE * baseModelViewMatrix
        yawRotate = MH.matrixMakeRotation (MH.radiansFromDegrees pitch) 0.0 1.0 0.0
        pitchRotate = MH.matrixMakeRotation (MH.radiansFromDegrees yaw) 0.0 0.0 1.0
        baseModelViewMatrix = MH.matrixRotate (MH.matrixTranslation 0.0 0.0 (-4.0)) rotation 0.0 1.0 0.0
        modelViewMatrixE = MH.matrixRotate (MH.matrixTranslation 0.0 0.0 1.5) rotation 1.0 1.0 1.0

yawRotatepitchRotateという変数を用意しました。それらはある特定の軸周りに与えられた度数だけ回転させます。画面は上下にプラスマイナス300とか400ピクセル、左右にプラスマイナス200とかピクセルありますから、度数で考えるのが良さそうです。その2つの回転行列をmodelViewMatrixに左からかけておきます。yawとpitchの順番は当然ですがどっちでも良いです。

これで回転をモデルに反映させられました。

完成です!

これで指に反応することができるように成りました。指に反応してモデルを色々できるとできることが広がりますね。ちょっとしたゲームぐらいなら作れそうです。

まとめ

これでとりあえずiOSでHaskellを使ってOpenGLで描画することができることが検証できたと思います。今度はもっと実用的な使い方を模索してみようかと思います。たとえば、汎用的なアルゴリズムとかルーチンをHaskellで書いて、どうしてものところだけSwiftで書くとか。

今回の作業の結果はGithubにあります。

github.com