euphonictechnologies’s diary

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

follow us in feedly

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

前回までのあらすじ

Haskell - GHC for iOS : OpenGLで3Dグラフィックスを表示してみる、の準備 - euphonictechnologies’s diary

前回はあらすじ紹介みたいな感じでぜんぜん本題に入りませんでした。今回はコードを書いていきたいと思います。

大事なところ - Haskellで全てはかけません

HOCとかいろいろあるようなのですが、現状ではiPhoneで動くHaskellのコードは書けないようです。HOCはHaskell to Objective-C bindingということでCocoaアプリを書くことができるようですが、残念ながらTemplateHaskellを駆使して書かれているので、TemplateHaskellが使えないghc-iosでは使うことができません。下記に参考リンクを載せておきますので、興味のある方はご覧ください。

今回は描画のコードを書きます

基本的には書かれているObjective-Cのコードを置き換えていこうと思います。土台となるのはghc-iosのリポジトリに含まれている ghc-ios/TestApp · GitHubです。この中のhaskell.hsというファイルがHaskell部分なのですが、以下のようになっています。

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign.Ptr
...

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

main = do
    putStrLn "Haskell start"
    angleRef <- newIORef 0
    allocaArray 8 $ \vBox -> do
        pokeArray (vBox :: Ptr CFloat) [
                -0.5, -0.33,
                ...
            ]

        drawFrame <- mkDrawFrame $ do
            ...
            drawArrays TriangleStrip 0 4
            ...

        c_main drawFrame

省略をした部分は...としてあります。

mainはmain関数です。ここがプログラムのエントリポイントになります。なのでObjCのコードでmain関数は記述しません。Haskellのレイヤからブートストラップします。

基本的にはこのファイルは2つの関数をインポートするという宣言があります。

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

この2つです。1つ目の関数はObjC側の入口となる関数、c_mainです。

2つ目の関数はmkDrawFrame関数です。この関数はforeign import call "wrapper"と定義されています。簡単に言うとこれを使うとコールバックルーチンをcに渡すことができるスタブファクトリmkDrawFrameをHaskellのコード内で利用可能にします。このmkDrawFrameはIO()を受け取ってIO()を実行する関数のポインタを作り出します。

具体的にHaskellレイヤとCレイヤの関連を前回示した成果物の中から見てみます。

OGLWHaskellTest/Main.hs at master · ysnrkdm/OGLWHaskellTest · GitHub

{-# LANGUAGE ForeignFunctionInterface #-}
...
foreign import ccall safe "c_main" c_main :: FunPtr (Double -> Double -> Double -> IO ()) -> IO ()
foreign import ccall "wrapper" mkDrawFrame :: (Double -> Double -> Double -> IO ()) -> IO (FunPtr ((Double -> Double -> Double -> IO ())))
...
main = do
    putStrLn "Haskell start"
    rotRef <- newIORef 0.0

    drawFrame <- mkDrawFrame $ drawFrame rotRef
    c_main drawFrame

OGLWHaskellTest/main.m at master · ysnrkdm/OGLWHaskellTest · GitHub

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

void c_main(void (*_drawFrame)(double, double, double)) {
    NSLog(@"%@", [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"]);
    int argc_ = 1;
    char* argv_[2];
    printf("c_main\n");
    drawFrame = _drawFrame;
    printf("c_main continues...\n");
    argv_[0] = "dummy";
    argv_[1] = "";
    @autoreleasepool {
        UIApplicationMain(argc_, argv_, nil, NSStringFromClass([AppDelegate class]));
    }
}

まず、Haskell側のMain.hsのmain関数が呼ばれ、起動します。main関数はmkDrawFrame関数を使ってdrawFrame関数へのポインタを作り出し、Objective-C側のc_main関数をその関数ポインタを渡しながらコールします。 c_main関数はdrawFrame関数ポインタを受け取ってグローバルに貼り付けてからUIApplicationMainを呼び出してCocoa Touchのアプリケーションを開始します。デバッグプリントを抜くとこんな感じのことをやっています。 さて、ここでdrawFrameがグローバルに利用可能になったので、その中に描画処理をすべて書いて、今あるObjective-Cの描画コードを置き換えれば今回の目標は達成です。描画はOGLWHaskellTest/GameViewController.m at master · ysnrkdm/OGLWHaskellTest · GitHubの中で行われているので、早速重要な部分を見てみます。

GameViewController.mを見てみると

GameViewController.mのオリジナルバージョンをGistにアップロードしてみました。テンプレートから始めるとこのファイルが得られるはずです。これをHaskellに移植していく、というのがこのエントリの目的です。

Original version: Original version of GameViewController.m in OpenGL project made by Xcode template

描画部分を抜き出してみるとこうなります:

- (void)update
{
    float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height);
    GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 100.0f);
    
    self.effect.transform.projectionMatrix = projectionMatrix;
    
    GLKMatrix4 baseModelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -4.0f);
    baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, _rotation, 0.0f, 1.0f, 0.0f);
    
    // Compute the model view matrix for the object rendered with GLKit
    GLKMatrix4 modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -1.5f);
    modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, _rotation, 1.0f, 1.0f, 1.0f);
    modelViewMatrix = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix);
    
    self.effect.transform.modelviewMatrix = modelViewMatrix;
    
    // Compute the model view matrix for the object rendered with ES2
    modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, 1.5f);
    modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, _rotation, 1.0f, 1.0f, 1.0f);
    modelViewMatrix = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix);
    
    _normalMatrix = GLKMatrix3InvertAndTranspose(GLKMatrix4GetMatrix3(modelViewMatrix), NULL);
    
    _modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix);
    
    _rotation += self.timeSinceLastUpdate * 0.5f;
}
 
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(0.65f, 0.65f, 0.65f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glBindVertexArrayOES(_vertexArray);
    
    // Render the object with GLKit
    [self.effect prepareToDraw];
    
    glDrawArrays(GL_TRIANGLES, 0, 36);
    
    // Render the object again with ES2
    glUseProgram(_program);
    
    glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, _modelViewProjectionMatrix.m);
    glUniformMatrix3fv(uniforms[UNIFORM_NORMAL_MATRIX], 1, 0, _normalMatrix.m);
    
    glDrawArrays(GL_TRIANGLES, 0, 36);
}

updateとdrawInRectという2つの関数で描画を行っています。updateでは描画に使う行列の計算を行い、drawInRectではOpenGLコマンドを発行し、実際に描画を行っています。

Haskellに移植した後のバージョンでは

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect

の部分で

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

のように呼び出されているのがわかると思います。GLKitを利用しているので、この中で描画コマンドを発行しないと画面に反映されません。なので、1フレーム描画する処理はすべてdrawFrame関数としてHaskell側に書いておいて、Objective-C側はその呼出を行うだけとします。

drawFrame関数をHaskellで書く

drawFrame :: IORef Double -> Double -> Double -> Double -> IO()
drawFrame rotRef timeSinceLastUpdate width height = do
    GL.clearColor $= Color4 0.65 0.65 0.65 1.0
    GL.clear [GL.ColorBuffer,GL.DepthBuffer]

    -- Get, and bind VAO
    [vertexArrayId] <- GL.genObjectNames 1 :: IO [VertexArrayObject]
    vbo <- createVBO gCubeVertexData
    GL.bindVertexArrayObject $= Just vertexArrayId

    -- Calc position, normal and their matrices
    let aspect = (width / height)
    modifyIORef rotRef $ \x -> x + timeSinceLastUpdate * 0.5
    rotation <- readIORef rotRef

    let (modelViewProjectionMatrix, normalMatrix) = getMatrices aspect rotation

    mprog <- (GL.get GL.currentProgram)
    uniformLocate glUniformMatrix4fv (glFloatVectorFromMatrix4 modelViewProjectionMatrix) mprog "modelViewProjectionMatrix"
    uniformLocate glUniformMatrix3fv (glFloatVectorFromMatrix3 normalMatrix) mprog "normalMatrix"

    draw vbo vertexArrayId
    flush

という感じになっています。まず

drawFrame :: IORef Double -> Double -> Double -> Double -> IO()
drawFrame rotRef timeSinceLastUpdate width height = do

この関数は最後のアップデートから何秒経ったかを受け取ります。更に描画に使うビューの高さと幅を受け取ります。rotRefはIORefで書き換え可能な変数としてオブジェクトの回転角度を持っています。この回転角度をビューとオブジェクトの回転に使います。そのときに最後のアップデートから何秒経ったかを使ってrotRefを更新して、回転のアニメーションを描画していきます。この関数は極めて頻繁に呼ばれるので、秒数は極めて短く、少しずつ回転をかけては描画をする、を繰り返してアニメーションを実現しています。

次に

    GL.clearColor $= Color4 0.65 0.65 0.65 1.0
    GL.clear [GL.ColorBuffer,GL.DepthBuffer]

として画面のクリアを行います。画面は灰色でクリアしています。そして次に

    -- Get, and bind VAO
    [vertexArrayId] <- GL.genObjectNames 1 :: IO [VertexArrayObject]
    vbo <- createVBO gCubeVertexData
    GL.bindVertexArrayObject $= Just vertexArrayId

ここでは描画を行うVAO,vertexArrayObjectを作り出します。画面に表示される立方体のモデル情報は座標と法線データの組としてリストに格納されているので、これをVAOに変換する処理をここで行います。次に、描画に使う行列を計算します。

    -- Calc position, normal and their matrices
    let aspect = (width / height)
    modifyIORef rotRef $ \x -> x + timeSinceLastUpdate * 0.5
    rotation <- readIORef rotRef

    let (modelViewProjectionMatrix, normalMatrix) = getMatrices aspect rotation

必要なのはモデルビュープロジェクション行列と法線行列です。モデルビュープロジェクション行列は4x4で、法線行列は3x3です。 行列を計算する前に回転角度の更新も行います。次は描画を行います。

    mprog <- (GL.get GL.currentProgram)
    uniformLocate glUniformMatrix4fv (glFloatVectorFromMatrix4 modelViewProjectionMatrix) mprog "modelViewProjectionMatrix"
    uniformLocate glUniformMatrix3fv (glFloatVectorFromMatrix3 normalMatrix) mprog "normalMatrix"

    draw vbo vertexArrayId
    flush

ここで実際に描画を行い、画面をフラッシュします。描画に必要なシェーダプログラムを読み込み、シェーダプログラムの入力にそれぞれ行列を渡します。このシェーダプログラムはXcodeのOpenGLプロジェクトテンプレートについてきたものをそのまま利用しています。行列をシェーダプログラムに読み込ませた後、draw関数で実際に描画コマンドを発行し、最後に画面のフラッシュを行います。

これが全体的な描画の流れです:

  1. 画面をクリアする
  2. モデルデータからVAOオブジェクトを作り出す
  3. モデル描画に使う行列を計算する
  4. シェーダプログラムに計算した行列を流し込む
  5. 描画コマンドを発行する

と言った流れです。次のエントリでは1つずつ詳しく見ていきます。