Rust(wasm)⇔JavaScript ファイル読み込みとアニメーション(ゲームループ)

RustでWebGLのプログラムを作っていますが、ファイル読み込み処理が必要になってきました。シェーダーソースコードをRustに埋め込むとエディタで色が変わらない、テクスチャにも対応したい。特殊なサーバー処理なしでファイル操作したいので、JavaScriptのFetch命令でファイル読み込みを実装します。Rust側でできることはRustで実装したいという設計方針のため、比較的簡単な実装、ゲームループでFetchの非同期処理に対応します。ついでにアニメーション処理。RustでPromiseのような処理を実装する知識は……まだない。

 

 

Rust(wasm)でファイル読み込み

シェーダーのソースコードをRustソースコード埋め込みでは何かと不便なので、ファイル読み込みに対応します。ブラウザ上でwasmを実行しているため、ファイルはローカルではなくWebサーバー経由での取得です。

Rust std::fs

Webサーバーからの取得なのでファイルI/Oではなく、ネットワークプログラミングになるのですが、少し調べてみました。
wasmではファイル操作にまだ対応していません。たとえ対応してもブラウザ上で実行されるwasmではセキュリティー上できないと思われます。

    // Err : operation not supported on wasm yet
    let text = std::fs::read_to_string("./data/vshader.vs");
    match text {
        Err(e) => console_log(e.to_string()),
        Ok(c) => console_log(c),
    }

JavaScript Fetch

wasm(Webブラウザ)はネットワーク通信に対応していないため、WebGLと同じようにJavaScript経由にで行います。Webサーバーからのファイル取得には、Fetch関数を使うことにしました。Fetchの詳細は省略。
以下のようなファイル操作クラスとRustから操作するための関数を作成しました。Fetchは非同期処理なので、コールバックやPromiseのような機能をRustで実装したかったのですが知識不足のため、ポーリング処理での終了待ちになりました。

.js ファイル読み込みclass File{
    constructor(path) {
        this.path = path;
        this.buffer = null;
        this.busy_flag = false;//処理中
        this.error = null;
    }

    // 読み込み開始
    read() {
        if( this.busy_flag )return;//既に読み込み開始
        this.busy_flag = true;

        fetch(this.path).then(response => {
            if(response.ok){
                console.log("file read " + this.path);
                return response.arrayBuffer(); }
            else { throw new Error("file error " + this.path);} })
        .then(buffer => {
            this.buffer = buffer;
            this.busy_flag = false; })
        .catch(err =>{ this.error = err; console.log(err); });
    }

    // 読み込み中
    is_busy() {
        return this.busy_flag;
    }

    get_buffer() { return this.buffer;}

    get_byte_size() { return this.buffer.byteLength;}
}


// Fileクラス操作用Rust関数

// return : Handle
function srwfs_create(str_ptr, str_byte_len) {
    let path = wasm_ctx.string_from_utf8(str_ptr,str_byte_len);
    return obj_heap.add_obj( new File(path) );
}

function srwfs_delete( hdl ) {
    obj_heap.take_obj( hdl );
}

// 読み込み開始 srwfs_is_busy()=falseで終了
function srwfs_read( hdl ) {
    obj_heap.get(hdl).read();
}

// 読み込み中
function srwfs_is_busy( hdl ) {
    return obj_heap.get(hdl).busy_flag;
}

// ファイルサイズ (読み込み終了後、取得可能)
function srwfs_get_size( hdl ) {
    if( obj_heap.get(hdl).is_busy() )return 0;
    return obj_heap.get(hdl).get_byte_size();
}

// ファイル取得 srwfs_get_sizeで事前にメモリ確保
function srwfs_get( hdl, buff_ptr, buff_byte_len ) {
    if( obj_heap.get(hdl).is_busy() )return 0;

    let file = obj_heap.get(hdl);
    let size = file.get_byte_size();
    if(buff_byte_len < size )return 0;//用意したバッファが足りない
    let file_buff = new Uint8Array(file.buffer);

    // 事前に必要なサイズを確保したメモリに書き込み
    let buff = wasm_ctx.get_sub_u8( buff_ptr, size );
    buff.set(file_buff);//memcpy
    return buff.byteLength;//書き込んだサイズ
}

/// rust(wasm)から呼ばれる関数
/// WebAssembly.instantiate()へ渡す
export const imports = {
    env: {
        srwfs_create : srwfs_create,
        srwfs_delete : srwfs_delete,
        srwfs_read : srwfs_read,
        srwfs_is_busy : srwfs_is_busy,
        srwfs_get_size : srwfs_get_size,
        srwfs_get : srwfs_get,
    },
}
.rs ファイル読み込みpub struct File {
    hdl : Handle,
}

/// ファイル
impl File {
    pub fn new<T : GetStrPtr>(s : T) -> File {
        File{ hdl : unsafe{ srwfs_create(s.get_ptr(), s.get_size()) },}
    }
    pub fn new_read<T : GetStrPtr>( path : T ) -> File {
        let mut f = File::new(path);
        f.read();
        f
    }
    pub fn read(&mut self) {
        unsafe{ 
            srwfs_read(self.hdl )
        }
    }

    pub fn is_busy(&self) -> bool {
         unsafe{ srwfs_is_busy(self.hdl) != 0 }
    }

    pub fn get_size(&self) -> usize {
        unsafe { srwfs_get_size(self.hdl) }
    }

    pub fn get_vec8(&self) -> Vec<u8> {
        let size = self.get_size();
       let mut vec = Vec::<u8>::new();
        vec.reserve(size);
        unsafe { // Vec<u8>の中身を直接いじる
            let len = srwfs_get(self.hdl,
                            vec.as_mut_ptr() as usize,
                            vec.capacity() as usize );
            if len == size { vec.set_len(len); }
        }
        vec
    }
}
impl Drop for File {
    fn drop(&mut self){
        unsafe { srwfs_delete(self.hdl); }
    }
}
間違った使い方

ブラウザでのwasmとJavaScriptはシングルスレッドなので

  let file = File::new_read("./data/vshader.vs");
  while file.is_busy() {}
// へんじがない。

こうするとブラウザが応答しなくなり、ブラウザに警告されます。ファイルの取得を開始した後は、Rustの関数をreturnさせJavaScriptに制御を戻す必要があります。この対応のために、一定間隔でゲーム処理や描画処理を呼び出す「ゲームループ」を実装します。ゲームループができればアニメーションにも対応。

ゲームループ

ゲームプログラムでよく採用されるゲームループをJavaScriptで実装して、定期的にRust関数を呼び出します。setTimeoutだとブラウザの画面更新タイミングと重なった時に一瞬処理落ちのような症状が出るため、画面更新直後にコールバックするrequestAnimationFrameを使います。あとフレームスキップにも対応しました。

.js ゲームループlet UpdateFPS = 30;
let UpdateStepTime = 1000/UpdateFPS;//ms
let UpdateLastTime = performance.now();
let UpdateFrameSkipMax = 4;

function main()
{
    // いろいろ

    // ゲームループ開始
    anime_frame = requestAnimationFrame(update);
}

// ゲームループ処理
function update( now_time ) {
    let time = now_time - UpdateLastTime;

    for(let cnt = 0; cnt < UpdateFrameSkipMax; ++cnt) {
        if( time < 0){
             break;// 更新成功
        }
        // 更新処理   
        wasm_ctx.rust.rust_step();
        time -= UpdateStepTime;
  
        // time > 0 遅れ発生 再度実行して遅れを取り戻す
    } 
    // 描画   
    wasm_ctx.rust.rust_render();

    // 次のフレームへ
    UpdateLastTime = now_time;
    anime_frame = requestAnimationFrame(update);
}

状態遷移

ファイル読み込みとゲームループができたので、次は、ファイル読み込み開始⇒読み込み待ち⇒構築処理⇒更新処理、といった状態遷移処理が必要です。

match

switch/case……じゃなくてmatchで実装した場合、簡単ですが状態の追加を行う場合、enumの追加、case文の追加が必要になります。ちょっと面倒。

.rs matchで状態遷移enum State{
    Init = 0,
    Wait = 1,
    Setup = 2,
    Step = 3,
}

struct Game{
     state : State,
}

impl Game{
    fn update(&mut self) {
        match self.state {
            State::Init => self.init(),
            State::Wait => self.wait(),
            State::Setup => self.setup(),
            State::Step => self.step(),
        }
    }
}
メソッド関数ポインタを利用した状態遷移

Rustの関数ポインタを利用してみました。状態を追加する場合、メソッド追加のみ。enumが便利です。

.rs 関数ポインタで状態遷移// Game構造体メソッド関数ポインタ
type FuncPtr = fn(&mut Game) -> State; 

enum State {
    Next(FuncPtr),
    Continue,
}
struct Game {
    func : Option<FuncPtr>,
}
impl Game {
    fn new() -> Game {
        Game{ func : Some(Self::init), }
    }
    fn update(&mut self) {
        if let Some(func) = self.func {
            match (func)(self) {
                State::Next(next) => self.func = Some(next),
                State::Continue => {},
            }
        }
    }
    fn init(&mut self) -> State {
        //初期化
        State::Next(Self::wait)
    }
    fn wait(&mut self) -> State {
        if 読み込み中 { return State::Continue; }
        State::Next(Self::setup)
    }
    fn setup(&mut self) -> State {
        // 構築
        State::Next(Self::step)
    }
    fn step(&mut self) -> State {
        // 更新処理
        State::Continue
    }
}

回転するポリゴン&ファイル読み込み

これまで作成したものを使って、回転アニメーションするポリゴン表示、シェーダーソースコードをファイル読み込みするプログラムを作成しました。それと、各処理のモジュール化も行いました。RustとJSのバインド(メモリとオブジェクト操作関連)、WebGLバインダー、ファイル操作をRustモジュールとそれに対応したjsファイルに分割しています。単純なRustとwasm+JavaScriptとの連携ということでsrwlib(simple rust+wasm library)と名付けてます。

実行
□rust-webgl-file-loop

Rustプロジェクトとソースコードです。Windowsコマンドスクリプトでのビルドになります。

“Rust(wasm) ファイル読み込みとゲームループ” をダウンロード rust_webgl_file_loop.zip – 4 回のダウンロード – 43 KB

ソースコード モジュール以外

メインソースのlib.rsとindex.jsです。

index.jsimport * as srwbind from "./srwlib/srwbind.js";// simple rust wasm binder
import * as srwfs from "./srwlib/srwfs.js";// simple rust wasm file system
import * as srwgl from "./srwlib/srwgl.js";// simple rust wasm webgl binder

export function merge_imports(src, dest) {
    for ( let n in dest.env ) {
        src.env[n] = dest.env[n];
    }
}
let imports = { env:{}, };
merge_imports(imports, srwbind.imports);
merge_imports(imports, srwfs.imports);
merge_imports(imports, srwgl.imports);

// wasm読み込み
fetch('./rust.wasm')
    .then(response => response.arrayBuffer())
    .then(bytes => WebAssembly.instantiate(bytes, imports))
    .then(results => { main(results.instance); })
    .catch(err=>console.log(err));

let UpdateFPS = 30;
let UpdateStepTime = 1000/UpdateFPS;//ms
let UpdateLastTime = performance.now();
let UpdateFrameSkipMax = 4;

let wasm_ctx;
let anime_frame;
function main(instance)
{
    let canvas = document.getElementById('canvas');
    let glctx = canvas.getContext('webgl');

    // これがないとEdgeで画面が表示されない……またおまえか
    glctx.clear(glctx.COLOR_BUFFER_BIT);
    //  初回のrequestAnimationFrameで
    //  glの画面更新がないとそれ以降画面が更新されない
    
    srwbind.inilialize(instance);
    srwfs.initialize(srwbind);
    srwgl.initialize(srwbind, glctx);

    wasm_ctx = srwbind.get_wasm_context();

     // 終了処理 
    window.addEventListener('beforeunload', function(e){
        cancelAnimationFrame(anime_frame);
        wasm_ctx.rust.rust_exit();

        // 16回リロードで発生するwarning対策 for Firefox
        glctx.getExtension('WEBGL_lose_context').loseContext();
    });

    // 初期化
    wasm_ctx.rust.rust_init();

    // ゲームループ開始
    anime_frame = requestAnimationFrame(update);
}

// ゲームループ処理
function update( now_time ) {
    let time = now_time - UpdateLastTime;

    for(let cnt = 0; cnt < UpdateFrameSkipMax; ++cnt) {
        if( time < 0){
             break;// 更新成功
        }
        // 更新処理   
        wasm_ctx.rust.rust_step();
        time -= UpdateStepTime;
  
        // time > 0 遅れ発生 再度実行して遅れを取り戻す
    } 
    // 描画   
    wasm_ctx.rust.rust_render();

    // 次のフレームへ
    UpdateLastTime = now_time;
    anime_frame = requestAnimationFrame(update);
}
lib.rsmod srwbind;    // RustとJSのバインダー
mod srwgl;      // WebGLバインダー
mod srwfs;      // File System

//use srwgl::*;         //gl????関数
//use srwgl::types::*;  //定数 GL_????
use srwgl::prelude::*;


type MyAppFuncPtr = fn(&mut MyApp) -> MyAppState;
enum MyAppState {
    Error(String),
    Exit,
    Continue,
    Next(MyAppFuncPtr),
    Chain(MyAppFuncPtr),   //続けて実行
}

struct MyApp {
    data : Option<MyData>,
    files : Option<MyFile>,

    func : Option<MyAppFuncPtr>,
}

impl MyApp {
    fn new() -> MyApp {
        MyApp {
            data : None, files : None,
            func : Some(Self::init),
        }
    }
}

struct MyData {
    prog : GLHandle,
    vb : GLHandle,
    ib : GLHandle,
    pos_idx : GLint,
    col_idx : GLint,
    mtx_uidx : GLint,
    proj_uidx : GLint,

    rot : f32,
}

struct MyFile {
    vs_src : srwfs::File,   // シェーダーソースコード
    fs_src : srwfs::File,
}

// シングルトン?
static mut APP : Option< MyApp > = None;

impl MyApp {
    
    // ゲームループ関数
    fn update(&mut self) -> Result<bool,String> {
        loop {
            if let Some(func) = self.func {
                match (func)(self) {
                    MyAppState::Next(next) => self.func = Some(next),
                    MyAppState::Chain(next) => {
                        self.func = Some(next);
                        continue; },
                    MyAppState::Continue => {},
                    MyAppState::Exit => self.func = None,
                    MyAppState::Error(err) => { self.func = None; return Err(err); },
                }
            }
            break;
        }
        Ok(self.func.is_some())//実行中?
    }

     // 初期化 ファイル読み込み開始
    fn init(&mut self) -> MyAppState {
        srwbind::console_log("app init");

        // Err : operation not supported on wasm yet
        /*let text = std::fs::read_to_string("./data/vshader.vs");
        match text {
            Err(e) => println!("{}",e) ,
            //Err(e) => srwbind::console_log(e.to_string()),
            Ok(c) => srwbind::console_log(c),
        }*/

        // ファイル読み込み シェーダーソース
        self.files = Some( MyFile {
            vs_src : srwfs::File::new_read("./data/vshader.vs"),
            fs_src : srwfs::File::new_read("./data/fshader.fs"),
        });

        MyAppState::Next(Self::wait_file)
    }

    // ファイル待ち
    fn wait_file(&mut self) -> MyAppState {
        if let Some(files) = &self.files {
            // ファイル読み込み中
            if files.vs_src.is_busy() { return MyAppState::Continue; }
            if files.fs_src.is_busy() { return MyAppState::Continue; }
        }else{
            return MyAppState::Error("files None".to_string());
        }
        //ファイル読み込み終了 次へ
        MyAppState::Chain(Self::setup)
    }

    // GL構築
    fn setup(&mut self) -> MyAppState {
        srwbind::console_log("app setup");
        let files = if let Some(f) = &self.files { f }
            else { return MyAppState::Error("files None".to_string()); };

        // とりあえず utf8不正コードの場合 panic
        let vs_src = String::from_utf8(files.vs_src.get_vec8()).unwrap();
        let fs_src = String::from_utf8(files.fs_src.get_vec8()).unwrap();
        
        let vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, vs_src);
        glCompileShader(vs);
        if GL_FALSE == glGetShaderParameter(vs, GL_COMPILE_STATUS) {
            srwbind::console_log( glGetShaderInfoLog(vs) );
            return MyAppState::Error("vs error".to_string());
        }

        let fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, fs_src);
        glCompileShader(fs);
        if GL_FALSE == glGetShaderParameter(fs, GL_COMPILE_STATUS) {
            srwbind::console_log( glGetShaderInfoLog(fs) );
            return MyAppState::Error("fs error".to_string());
        }

        let prog = glCreateProgram();
        glAttachShader(prog, vs);
        glAttachShader(prog, fs);
        glLinkProgram(prog);
        if GL_FALSE == glGetProgramParameter(prog, GL_LINK_STATUS) {
            srwbind::console_log( glGetProgramInfoLog(prog) );
            return MyAppState::Error("prog error".to_string());
        }
        glDetachShader(prog, vs);
        glDetachShader(prog, fs);
        glDeleteShader(vs);
        glDeleteShader(fs);

        let pos_idx = glGetAttribLocation(prog, "position");
        let col_idx = glGetAttribLocation(prog, "color");
        let mtx_uidx = glGetUniformLocation(prog, "mtxModel");
        let proj_uidx = glGetUniformLocation(prog, "mtxProj");

        let vertex : [GLfloat;21] = [
            // x,y,z  r,g,b,a
            0.0,  0.8, 0.0,  0.0, 1.0, 1.0, 1.0,
            0.8, -0.8, 0.0,  1.0, 0.0, 1.0, 1.0,
            -0.8, -0.8, 0.0, 1.0, 1.0, 0.0, 1.0 ];
            
        let index : [u16;3] = [0, 2, 1];

        let vb = glCreateBuffer();
        glBindBuffer(GL_ARRAY_BUFFER, vb);
        glBufferData(GL_ARRAY_BUFFER, &vertex[..], GL_STATIC_DRAW);

        let ib = glCreateBuffer();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ib);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, &index[..], GL_STATIC_DRAW);

        self.data = Some( MyData {
            prog : prog,
            vb : vb,
            ib : ib,
            pos_idx : pos_idx,
            col_idx : col_idx,
            mtx_uidx : mtx_uidx,
            proj_uidx : proj_uidx,
            rot : 0.0,
        });
        self.files = None;

        MyAppState::Next(Self::step)
    }

    // 更新 時間を進める
    fn step(&mut self) -> MyAppState {
        let data = if let Some(d) = &mut self.data { d }
            else { return MyAppState::Exit };
        
        data.rot += 0.02;
        if data.rot > std::f32::consts::PI*2.0 {
            data.rot -= std::f32::consts::PI*2.0; }

        MyAppState::Continue
    }

    // 描画
    fn render(&self) {
        let data = if let Some(d) = &self.data { d }
                   else { return; };
        glDisable(GL_CULL_FACE);

        glClearColor(0.6, 0.8, 0.9, 1.0);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(data.prog);

        glUniformMatrix4fv(data.proj_uidx, GL_FALSE,
                perspective_fov(60.0*3.14159/180.0,16.0/9.0, 0.1, 100.0) );
        let (sin, cos) = ( data.rot.sin(), data.rot.cos() );
        let mtx : [f32;16] = [
            cos, 0.0, sin, 0.0,
            0.0, 1.0,  0.0, 0.0,
           -sin, 0.0, cos, 0.0,
            1.2, 0.0, -2.5, 1.0 ];
        glUniformMatrix4fv(data.mtx_uidx, GL_FALSE, &mtx[..]);
        
        glBindBuffer(GL_ARRAY_BUFFER, data.vb);
        glEnableVertexAttribArray(data.pos_idx);
        glVertexAttribPointer(data.pos_idx, 3, GL_FLOAT, GL_FALSE, 28, 0);
        glEnableVertexAttribArray(data.col_idx);
        glVertexAttribPointer( data.col_idx, 4, GL_FLOAT, GL_FALSE, 28, 12);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, data.ib);
        glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0);
        
        let mtx : [f32;16] = [
            cos, -sin, 0.0, 0.0,
            sin, cos,  0.0, 0.0,
            0.0, 0.0,  1.0, 0.0,
            -1.2, 0.0, -2.5, 1.0 ];
        glUniformMatrix4fv(data.mtx_uidx, GL_FALSE, &mtx[..]);
        glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0);

        glFlush();
        glFinish();
    }

    // 終了処理
    fn exit(&mut self) {
        srwbind::console_log("app exit");
        let data = if let Some(d) = &self.data { d }
                   else { return; };
        glDisableVertexAttribArray(data.pos_idx);
        glDisableVertexAttribArray(data.col_idx);
        glDeleteBuffer(data.ib);
        glDeleteBuffer(data.vb);
        glDeleteProgram(data.prog);
        self.data = None;              
    }
}

/// 透視変換行列
pub fn perspective_fov(fov_y: f32, aspect: f32, near_z: f32, far_z: f32 ) -> Vec<f32> {
    let h = f32::cos(fov_y*0.5)/f32::sin(fov_y*0.5);
    let w = h / aspect;
    let z2 = -(far_z + near_z) / ( far_z - near_z );
    let z3 = -2.0*(far_z * near_z) / ( far_z - near_z );
    // z -> -1 ~ +1  note. directx 0 ~ 1
    vec![
          w, 0.0, 0.0,  0.0,
        0.0,   h, 0.0,  0.0,
        0.0, 0.0,  z2, -1.0,
        0.0, 0.0,  z3,  0.0 ]
}

// JavaScriptから呼ばれる関数

// 初期化
#[no_mangle]
fn rust_init() {
    unsafe{ // グローバル変数使用
        APP = Some( MyApp::new() );
    }
}

// requestAnimationFrameで定期的に呼ばれる関数
// 処理落ちした場合、描画せずに再度実行される
#[no_mangle]
fn rust_step()->bool {
    unsafe {
        if let Some(app) = &mut APP {
            match app.update() {
                Err(e) => { srwbind::console_log(e); false },
                Ok(b) => { b },
            }
        }else{ false }
    }  
}

// 描画 
#[no_mangle]
fn rust_render() {
    unsafe {
        if let Some(app) = &mut APP {
            app.render();
        } 
    }
}

#[no_mangle]
fn rust_exit() {
    unsafe {
        if let Some(app) = &mut APP {
            app.exit();
        } 
    }
}

まとめ

ファイル読み込みとゲームループが完成、あとテクスチャができればゲームプログラムの基礎ができそうです。

<< Rust+Wasm+WebGLはじめました 環境構築=>Hello World =>画面クリアまで(Windows10)
<< Rust(wasm)とJavaScript(WebGL)のデータ受け渡し Rust+WebGLでポリゴン描画 (1/2)

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.