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に移植できました。今回はシェーダプログラムのロードとコンパイルを行う部分を移植します。

外観

改めて、元々のXcodeが作ったOpenGLのコードを示します。

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

この中でシェーダプログラムのコードとコンパイルを行っているのは以下の関数群です。

#pragma mark -  OpenGL ES 2 shader compilation
- (BOOL)loadShaders
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
- (BOOL)linkProgram:(GLuint)prog
- (BOOL)validateProgram:(GLuint)prog

その内、実際にはloadShaders以外のすべての関数はloadShadersかその呼び出し先からの呼出なので、実質的にはloadShadersとそのヘルパ関数群です。loadShadersはsetupGL関数の中で以下のように呼び出されています。

- (void)setupGL
{
... 
    [self loadShaders];

この関数だけで、シェーダプログラムをバンドルから読み出し、それを解析し、コンパイルし、必要な物をリンクし、コンパイルされた結果を検証して、OpenGLシステムの中で利用可能にします。そのためのヘルパ関数はcompileShader、linkProgramとvalidateProgramです。

まず最初に、パッケージからファイルを読み出す部分を作る。

    // Create and compile vertex shader.
    vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"];

この部分がバンドルからファイルを読みだしています。当然NSBundleはHaskellからアクセス出来ないので、ブリッジ関数を書いて上げる必要があります。実態は簡単で上記のコードに多少お化粧した程度のものです。

const char *c_pathForResource(char *path, char *ofType) {
    NSString* pathStr = [NSString stringWithUTF8String:path];
    NSString* ofTypeStr = [NSString stringWithUTF8String:ofType];
    NSLog(@"Finding bundle path for %@, %@\n", pathStr, ofTypeStr);
    const char *ret = [[[NSBundle mainBundle] pathForResource:pathStr ofType:ofTypeStr] UTF8String];
    NSLog(@"Found : %s\n", ret);
    return ret;
}

HaskellはNSBundleが使えないのと同じように、NSStringも使えないので、その部分が余計にくっついているだけです。バンドルにはあらゆる種類のファイルが入れられるはずですが、今のところ返り値はconst char *となっています。手抜きですね。

これをHaskell側で使うにはこうします

foreign import ccall safe "c_pathForResource" c_pathForResource :: CString -> CString -> IO Cstring

...
-- Load shaders
    vsFilePath <- withCString "Shader" $
        \shader -> withCString "vsh" $
            \vsh -> c_pathForResource shader vsh

    vsFilePathStr <- peekCString vsFilePath

こんな感じでしょうか。当然vsFilePathの呼び出しはIO()の中で行われています。c_pathForResource(すみません、気持ち悪い名前ですね。直します)

関数はゼロターミネートされた文字列へのポインタを返してくれるので、ポインタをCStringに戻すためにpeekCStringを使っています。

ファイル名と拡張子を渡して関数をコールするためにネストされたwithCString内で呼び出しを行います。withCString関数は最初の引数のStringを次の引数に与えられたラムダの引数にCStringとして渡してラムダを評価しれくれます。

loadShadersのスケルトンをHaskell内に書く

loadShadersが上記の読みだし、コンパイル、リンク、検証であるならば、次のようにかけば良さそうですね。

loadShaders :: IO ()
loadShaders = do
    -- Load shaders
    vsFilePath <- withCString "Shader" $
        \shader -> withCString "vsh" $
            \vsh -> c_pathForResource shader vsh
    fsFilePath <- withCString "Shader" $
        \shader -> withCString "fsh" $
            \fsh -> c_pathForResource shader fsh

    vsFilePathStr <- peekCString vsFilePath
    fsFilePathStr <- peekCString fsFilePath

    putStrLn "Found below files:"
    putStrLn vsFilePathStr
    putStrLn fsFilePathStr

    putStrLn "Loading shaders from above files..."
    program <- LS.loadShaders [
        LS.ShaderInfo GL.VertexShader (LS.FileSource vsFilePathStr) "position" (GL.AttribLocation 0),
        LS.ShaderInfo GL.FragmentShader (LS.FileSource fsFilePathStr) "normal" (GL.AttribLocation 1)]

    putStrLn "Shaders loaded."
    putStrLn $ show program

    GL.currentProgram $= Just program

こんな感じでしょうか。ファイルの読み込み、とLS.loadShadersによるシェーダプログラムのコンパイル、リンク、バリデーションと、最後にGL.currentProgramにセットするところまでです。となると肝はLS.loadShadersということになります。これは独立したモジュールです。

LoadShadersモジュール

Haskell-OpenGLチュートリアルで使われているものを借用し独自に拡張したものを使っています。

これがオリジナルで: Haskell-OpenGL-Tutorial/LoadShaders.hs at master · madjestic/Haskell-OpenGL-Tutorial · GitHub

こちらが私が書いたものです: OGLWHaskellTest/LoadShaders.hs at master · ysnrkdm/OGLWHaskellTest · GitHub

data ShaderSource =
     ByteStringSource B.ByteString
     -- ^ The shader source code is directly given as a 'B.ByteString'.
   | StringSource String
     -- ^ The shader source code is directly given as a 'String'.
   | FileSource FilePath
     -- ^ The shader source code is located in the file at the given 'FilePath'.
   deriving ( Eq, Ord, Show )

getSource :: ShaderSource -> IO B.ByteString
getSource (ByteStringSource bs) = return bs
getSource (StringSource str) = return $ packUtf8 str
getSource (FileSource path) = B.readFile path

この部分が面白い部分の一つで、ひとつのデータ型ですが、複数の方法で構築することができます。関数型言語ではおなじみ代数的データ型というやつですね。

中身については実はあまり特筆すべきことはありません。OpenGLのコマンドを羅列してある似すぎません。

checked :: Show t => (t -> IO ())
        -> (t -> GL.GettableStateVar Bool)
        -> (t -> GL.GettableStateVar String)
        -> String
        -> t
        -> IO ()

ファイルの最後にあるcheckedという関数に与える引数を違えることでコンパイルやリンクを行います。IO()の中なのでこういうヘルパ関数があると便利です。ちなみにコンパイルに失敗するとfailするのでプログラムは死にます。

最後に作った関数をObjective-Cレイヤから呼び出す

実はこれでおしまいです。LoadShadersモジュールにやりたいことが全て詰まっており、それが冒頭にあげた4つの関数

#pragma mark -  OpenGL ES 2 shader compilation
- (BOOL)loadShaders
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
- (BOOL)linkProgram:(GLuint)prog
- (BOOL)validateProgram:(GLuint)prog

を置き換えます。あとはこの4関数をObjective-Cから削除し、HaskellのloadShaders関数の呼び出しに置き換えるだけです。

foreign export ccall loadShaders :: IO()

loadShadersはCの関数で言うとvoid loadShaders()なので、上のような関数です。これをエクスポートしてObjective-C側で呼び出します。

#import "Main_stub.h"
...
- (void)setupGL
{
    [EAGLContext setCurrentContext:self.context];

    loadShaders();

    glEnable(GL_DEPTH_TEST);
}

こんな感じです。Main_stub.hにHaskellからエクスポートした関数の宣言が存在しています。

#include "HsFFI.h"
#ifdef __cplusplus
extern "C" {
#endif
extern void loadShaders(void);
extern void Main_dmcv(StgStablePtr the_stableptr, void* original_return_addr, HsDouble a1, HsDouble a2, HsDouble a3);
#ifdef __cplusplus
}
#endif

もちろん実態は.aファイルの中にあります。これをインクルードして使えるようにすれば単に普通のCの関数のように呼び出すだけです。

f:id:euphonictechnologies:20150301193118g:plain

まとめと次回

これで描画、シェーダプログラムのコンパイルや配置などをHaskellから行うことができるようになりました。 あとは例えばメガデモ的用途であればHaskellでゴリゴリ書いていけばよいです。

例えばCocoa touchを使ってユーザのインタラクションを受け取ろうと思うと、更に作りこまなければなりません。

なので次回はCocoa touchを使ってタッチに反応してみたいと思っています。

さらに、いまは立方体だけですが、Tofu Survivorでもあるまいし立方体だけでは何も作れません。なのでモデルを読み込めるようにもしてみたいところです。