2014年12月14日日曜日

Rendering a Christmas Star in Rust

この記事はRust Advent Calendar 2014の12/14担当分である。Rustには前から関心はあったのにあまり大したものは作ったことがなかったので、Cargoの使い方やらも学びつつglutin使って簡単なOpenGL(GLSL)アプリを作ることにした。

できたもの

よく考えたらAdvent Calendarってクリスマスにちなんだイベントなのでクリスマスっぽいものをレンダリングすることにした。対象はクリスマスツリーの頂点にあるあの星だ。正式にはなんていうかよくわからない(ベツレヘムの星?)のでとりあえずChristmas Starと呼んでる。できたものがコレ。

3Dでレンダリングされている星だ。右上に表示されている赤丸が光源位置で、キーボードの矢印キーで動かすことができる。もちろん光源を動かすと星の照らされ具合も変わる。ただし基本的な光源処理しかしてないためかいまいち不自然な光り方になっている。

ビルド方法

標準的なCargoパッケージとして作ってるので、Githubに置いたソースを落としてきてcargo runすれば走らせられる。…という説明で終わらせてしまうと不親切なのでもう少し詳細に書く(ただし対象はWindows 8.1 x64環境)。

何も入ってないまっさらな状態からのビルド方法は下記の通り。

  1. Rustのインストール

    公式サイトからインストーラ落としてきて起動して手順に従う

  2. Cargoのインストール

    ビルド済みバイナリを落としてきて適当に解凍してbinにパスを通す

  3. mingw-w64のインストール

    mingw-w64-install.exeを落としてきて下図の設定でインストールする。

    インストール終わったらC:\Program Files\mingw-w64\x86_64-4.9.2-posix-seh-rt_v3-rev0\mingw64\binにパスを通す。

  4. Cargoでビルド

    ここまで来たらあとはソースを落としてきてcargo buildでビルド、cargo runで実行できる

なお、現在動作を確認した環境はOSがWindows 8.1 x64で、使ったrustcは0.13.0-nightly (6f4c11be3 2014-12-05 20:23:10 +0000)、cargoは0.0.1-pre-nightly (da789a6 2014-11-30 08:14:16 +0000)となっている。Rustは正式リリース前でまだ色々変化してるため全く同じ環境でないとビルド通らないかも。その場合はcargo updateで依存crateをアップデートするとビルドが通る場合が多い。

Crate構成

今回作成したchristmas_starの構成は下記の通り。

  • main

    メイン関数のあるモジュール。

  • game

    操作可能でかつ描画されるオブジェクトのtraitが含まれるモジュール。ゲームじゃないから別の名前にしたいけど他の適切な名前が思いつかなかった。

  • glutil

    シェーダを読んだりコンパイル・リンクしたりといった各種関数が含まれているモジュール。

  • control

    ユーザーのキー操作やらをハンドルするモジュール。

  • christmas_star

    クリスマスっぽい星を描画するオブジェクトのモジュール。モデル生成時にスパイクの長さやらが変更できるようになっている。

  • light

    光源オブジェクトのモジュール。

    • directional

      平行光源のモジュール。今回は平行光源自体も描画したいので描画用のシェーダがモジュールに含まれている。

今回はCargoがどんなもんか調べる意図もあったのでわざと細かくモジュールを分けている。とはいえ、Rustはアクセス制御がモジュール間でpublicかprivateかしか選択肢が無いため、モジュールは比較的細かく分割した方が内部構造の隠蔽がはかどると思う。

コード解説

思った以上にコードの量が多くなったのでここでは一部だけ解説する。ソース全体はGithubにあるのでそっちを見てもらえれば。

  • main

    mainではウィンドウを生成した後ウィンドウイベントを処理しつつゲームループを回している。

    fn process_main_loop(window: &glutin::Window, obj_list: &mut Vec<&mut game::Object>) {
        let mut cs = control::State::new(); 
        while !window.is_closed() {
            // process window evets
            for ev in window.poll_events() {
                match ev {
                    glutin::Event::KeyboardInput(elem_state, _, key_code) => cs.handle_key_input(elem_state, key_code),
                    _ => (),
                }
            }
            // if cs.moving() {
            //     println!("control state: {}", cs);
            // }
            // free CPU.
            // We should be checking the elapsed time to see how long we can wait here.
            std::io::timer::sleep(std::time::duration::Duration::milliseconds(8));
    
            // update all
            for o in obj_list.iter_mut() {
                o.update(&cs)
                    .unwrap_or_else(|e| panic!("Error when updating: {}", e));
            }
    
            // draw all
            clear_screen();
            for o in obj_list.iter() {
                o.draw()
                    .unwrap_or_else(|e| panic!("Error when drawing: {}", e));
            }
            unsafe { gl::Flush(); }
            window.swap_buffers();
        }
    }
    
    fn main() {
        let builder = glutin::WindowBuilder::new();
        let r = builder.with_dimensions(300, 300)
            .with_title("rust glsl sample".to_string())
            .build();
        let window = r.unwrap_or_else(|e| panic!("Error while building window: {}", e));
        unsafe { window.make_current() };
        gl::load_with(|symbol| window.get_proc_address(symbol));
    
        let mut obj = christmas_star::ChristmasStar::new();
        obj.init()
            .unwrap_or_else(|e| panic!("ChristmasStar init failed: {}", e));
        // need an indent here because obj_list will own the obj
        {
            let mut obj_list : Vec<&mut game::Object> = Vec::new();
            obj_list.push(&mut obj);
            process_main_loop(&window, &mut obj_list);
        }
        obj.close();
    }

    この中で注目したいのはmain関数内でobj_listを生成しているあたりだ。他の言語であればobj_listを生成してobjを追加するところでブロックを分ける必要ないのにRustの場合は必要になっている。

    この理由はobjをobj_listに渡した瞬間obj_listがobjのownerとなるからだ。obj_listがobjのownerになると、obj_listが完全に消えたことが保証できないかぎりobj.close()が呼べなくなってしまう(close関数はobjをmutateするため)。実際ここで中括弧を外すと下のコンパイルエラーが出る。

       Compiling christmas_star v0.0.1 (file:///C:/.../christmas_star)
    C:\...\christmas_star\src\main.rs:72:5: 72:8 error: cannot borrow `obj` as mutable more than o
    nce at a time
    C:\...\christmas_star\src\main.rs:72     obj.close();
                                             ^~~
    C:\...\christmas_star\src\main.rs:69:28: 69:31 note: previous borrow of `obj` occurs here; the
     mutable borrow prevents subsequent moves, borrows, or modification of `obj` until the borrow ends
    C:\...\christmas_star\src\main.rs:69         obj_list.push(&mut obj);
                                                                        ^~~
    C:\...\christmas_star\src\main.rs:73:2: 73:2 note: previous borrow ends here
    C:\...\christmas_star\src\main.rs:54 fn main() {
    ...
    C:\...\christmas_star\src\main.rs:73 }

    スコープを限定すればobj_listの生存期間は保証できるのでここではブロックを分けて対処した。他にもしかしたらうまい方法があるかも。

  • christmas_star

    christmas_starは初期化をしている部分だけを解説する。ここではシェーダソースを読みこんでコンパイル・リンクしたあと、GPU側のリソースを初期化している。

    impl ChristmasStar {
    ...
        pub fn init(&mut self) -> Result<(), String> {
            let vss = include_str!("vertex.glsl");
            let fss = include_str!("fragment.glsl");
            let vs = try!(glutil::compile_shader(vss.as_slice(), gl::VERTEX_SHADER));
            let fs = try!(glutil::compile_shader(fss.as_slice(), gl::FRAGMENT_SHADER));
            let prog = try!(glutil::link_program(vs, fs));
    
            // remove shaders since we've finished linking it
            glutil::remove_shader(prog, vs);
            glutil::remove_shader(prog, fs);
     
            let (vao, vbo, ind_num) = try!(init_buffers(&self.geometry));
    
            let r = &mut self.resource;
            r.shader_program = prog;
            r.vao = vao;
            r.vbo = vbo;
            r.indice_num = ind_num;
    
            try!(self.directional.init());
    
            Ok(())
        }
    ...
    }
    

    注目したいのはinclude_strというマクロだ。GLSLはソースのプリコンパイルができないので実行時にソースを渡してコンパイルする必要があるんだけど、include_strマクロを使えば外部のテキストファイルを簡単に埋め込んでコンパイルすることができる。こういう機能は他の言語だとあまり見たことがなかった(知らないだけ?)ので素直に感心した。

雑感

今回Rustで書いてみて色々思ったことを連ねていく。

  • エラー処理の方法が色々あってよくわからない

    RustのライブラリをつかうとResultという型が返ってくることがよくあって、こいつをどう扱えばいいのかがちょっとわかりづらい。ドキュメントを見るとエラー処理用の関数が大量に用意されているものの推奨するエラー処理方法が書かれていないのだ。

    今回書いてみた感じ自分の中では以下のルールがぼんやりとできている。

    1. Resultを返す関数を使うときはすべてtryマクロで囲って上位に返す(tryマクロ使えばmatch書かずに済む)
    2. 自作関数で失敗しうるものは全てResultを返す
    3. トップレベル(main関数)ではtryマクロが使えないので、返ってきたResultはunwrap_or_elseを呼んでもしエラーがあればpanicでエラー出力する

    基本的には「いちいち自分でmatch処理を書くのツラいのでできるだけmatchせずに済むように」が方針になっている。try-catchが無くて関数から多値を返すことができるあたりGoと似ているので、Goと同じようにエラーを上位に返すやり方が適切に感じられる。

  • キャストの方法がよくわからない

    ループ変数(i32)を浮動小数点(f32)にキャストするときにto_f32という関数を使ったんだけど、こいつがOptionを返してくるのでキャスト失敗に備えなきゃいけないの? という疑問がわいてしまった。あとは「let stride = vertice_size as i32」みたいな書き方もできてしまうためasを使ったほうがいいのかto_**関数を使った方がいいのかよくわからない。現状は混在させて書いている。

  • ダウンキャストできない?

    当初はgame::ObjectをDrawableとMovableという2つのtraitに分けて管理しようとしていたのだけど、ダウンキャストがうまく行かないことがわかって断念した。Algebraic Data Type(というかenum)を上手く使えばできる気はしてて、ただドキュメントが少なく茨の道っぽかったのでできていない。

  • lifetimeはハマる

    ダウンキャストとの絡みでenumとtraitまわりを色々頑張ってみたものの最終的にはコンパイルを通すことすらできなかった。大体がlifetimeにまつわるエラーで、エラーの内容で検索しても残念ながら情報がほとんど無い。lifetimeまわりの書き方はもうちょっとドキュメントが充実してこないとハマると思う。

総じてドキュメントがあればどうにかなることが多い中、lifetimeまわりだけは難易度が高く初見殺しになりそうだと思った。

昔のRustはポインタが数種類あったり(関連する記号が'@'、'~'、'&'、'*'と4つあった)ビルドもMakefileでやってたりと、正直このままじゃ流行らねえわ…と思いながら追っていた。ところが最近は1.0に向けて言語仕様もシンプルになり、Cargoのおかげでビルドが簡単になりといい感じである。個人的にRustはC/C++を駆逐すべく頑張って欲しいので今後もちょくちょく書いていきたい。

0 件のコメント:

コメントを投稿