📚

WebGLのチュートリアルをRustのWebAssemblyで書く 〜その2〜

2023/02/05に公開

https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Adding_2D_content_to_a_WebGL_context

Cargo.toml

web-sysのfeaturesのみ抜粋します

...
[dependencies.web-sys]
version = "0.3.60"
features = [
  "console",
  "Window",
  "Document",
  "HtmlElement",
  "Headers",
  "Request",
  "RequestInit",
  "RequestMode",
  "Response",
  "HtmlCanvasElement",
  "WebGl2RenderingContext",
  "WebGlShader",
  "WebGlProgram",
  "WebGlUniformLocation",
  "WebGlBuffer",
  "WebGlTexture",
]

rs

GLMのような機能を使うためnalgebra_glmを使います。

use nalgebra_glm as glm;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::WebGl2RenderingContext as GL;

WebGLプログラムを構造体にします。今回使うのはPositionだけですが、後々のためにそのほかもOption型で入れておきます

pub struct ProgramInfo {
    program: web_sys::WebGlProgram,
    attrib_locations: AttribLocations,
    uniform_locations: UniformLocations,
}

struct AttribLocations {
    vertex_position: Option<i32>,
    vertex_color: Option<i32>,
    texture_coord: Option<i32>,
    vertex_normal: Option<i32>,
}

struct UniformLocations {
    projection_matrix: Option<web_sys::WebGlUniformLocation>,
    model_view_matrix: Option<web_sys::WebGlUniformLocation>,
    u_sampler: Option<web_sys::WebGlUniformLocation>,
    normal_matrix: Option<web_sys::WebGlUniformLocation>,
}

impl ProgramInfo {
    pub fn new(gl: &GL, shader_program: web_sys::WebGlProgram) -> Self {
        ProgramInfo {
            attrib_locations: AttribLocations {
                vertex_position: Some(gl.get_attrib_location(&shader_program, "aVertexPosition")),
                vertex_normal: Some(gl.get_attrib_location(&shader_program, "aVertexNormal")),
                vertex_color: Some(gl.get_attrib_location(&shader_program, "aVertexColor")),
                texture_coord: Some(gl.get_attrib_location(&shader_program, "aTextureCoord")),
            },
            uniform_locations: UniformLocations {
                projection_matrix: gl.get_uniform_location(&shader_program, "uProjectionMatrix"),
                model_view_matrix: gl.get_uniform_location(&shader_program, "uModelViewMatrix"),
                normal_matrix: gl.get_uniform_location(&shader_program, "uNormalMatrix"),
                u_sampler: gl.get_uniform_location(&shader_program, "uSampler"),
            },
            program: shader_program,
        }
    }
}

同様にWebGLバッファーについても下記の構造体を用意しておきます

struct Buffers {
    position: Option<web_sys::WebGlBuffer>,
    color: Option<web_sys::WebGlBuffer>,
    texture_coord: Option<web_sys::WebGlBuffer>,
    indices: Option<web_sys::WebGlBuffer>,
    normal: Option<web_sys::WebGlBuffer>,
}

shaderを読み込むのにjsのfetchのような関数を書きました

async fn fetch(url: &str) -> Result<JsValue, JsValue> {
    let mut opts = web_sys::RequestInit::new();
    opts.method("GET");
    opts.mode(web_sys::RequestMode::Cors);

    let request = web_sys::Request::new_with_str_and_init(url, &opts)?;
    request.headers().set("Accept", "application/text")?;
    let window = window()?;
    let resp: web_sys::Response = JsFuture::from(window.fetch_with_request(&request))
        .await?
        .dyn_into()?;
    let text = JsFuture::from(resp.text()?).await?;
    Ok(text)
}

fn window() -> Result<web_sys::Window, JsValue> {
    web_sys::window().ok_or_else(|| JsValue::from_str("no window exists"))
}

シェーダーを書いて静的ファイルとして置いておきます

static/glsl/square.vert

attribute vec4 aVertexPosition;

uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

void main() {
  gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
}

static/glsl/square.frag

void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

シェーダーの初期化関数とローダー関数です

fn init_shader_program(
    gl: &GL,
    vs_source: &str,
    fs_source: &str,
) -> Result<web_sys::WebGlProgram, String> {
    let vertex_shader = load_shader(gl, GL::VERTEX_SHADER, vs_source)?;
    let fragment_shader = load_shader(gl, GL::FRAGMENT_SHADER, fs_source)?;

    let shader_program = gl
        .create_program()
        .ok_or_else(|| String::from("Unknown error creating program object"))?;

    gl.attach_shader(&shader_program, &vertex_shader);
    gl.attach_shader(&shader_program, &fragment_shader);
    gl.link_program(&shader_program);

    if gl
        .get_program_parameter(&shader_program, GL::LINK_STATUS)
        .as_bool()
        .unwrap_or(false)
    {
        Ok(shader_program)
    } else {
        Err(gl
            .get_program_info_log(&shader_program)
            .unwrap_or_else(|| String::from("Unknown error creating program object")))
    }
}

fn load_shader(gl: &GL, type_: u32, source: &str) -> Result<web_sys::WebGlShader, String> {
    let shader = gl
        .create_shader(type_)
        .ok_or_else(|| String::from("Unable to create shader object"))?;
    gl.shader_source(&shader, source);
    gl.compile_shader(&shader);
    if gl
        .get_shader_parameter(&shader, GL::COMPILE_STATUS)
        .as_bool()
        .unwrap_or(false)
    {
        Ok(shader)
    } else {
        Err(gl
            .get_shader_info_log(&shader)
            .unwrap_or_else(|| String::from("Unknown error creating shader")))
    }
}

バッファーの初期化関数です

fn init_buffers(gl: &GL) -> Result<Buffers, JsValue> {
    let position_buffer = init_position_buffer(gl)?;
    Ok(Buffers {
        position: Some(position_buffer),
        color: None,
        texture_coord: None,
        indices: None,
        normal: None,
    })
}

fn init_position_buffer(gl: &GL) -> Result<web_sys::WebGlBuffer, String> {
    let position_buffer = gl
        .create_buffer()
        .ok_or_else(|| String::from("Failed to create buffer"))?;
    gl.bind_buffer(GL::ARRAY_BUFFER, Some(&position_buffer));

    let positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
    gl.buffer_data_with_array_buffer_view(
        GL::ARRAY_BUFFER,
        unsafe { &js_sys::Float32Array::view(&positions) },
        GL::STATIC_DRAW,
    );

    Ok(position_buffer)
}

ドロー関数です

fn draw_scene(
    gl: &GL,
    program_info: &ProgramInfo,
    buffers: &Buffers,
) -> Result<(), String> {
    gl.clear_color(0.0, 0.0, 0.0, 1.0);
    gl.clear_depth(1.0);
    gl.enable(GL::DEPTH_TEST); // Enable depth testing
    gl.depth_func(GL::LEQUAL); // Near things obscure far things

    gl.clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);

    let canvas = gl.canvas().expect("Unable get canvas");
    let canvas = canvas
        .dyn_into::<web_sys::HtmlElement>()
        .expect("Unable to get canvas");

    let field_of_view = (45.0 * std::f32::consts::PI) / 180.0;
    let aspect = canvas.client_width() as f32 / canvas.client_height() as f32;
    let z_near = 0.1;
    let z_far = 100.0;
    let projection_matrix = glm::perspective(aspect, field_of_view, z_near, z_far);
    let model_view_matrix =
        glm::translate(&glm::Mat4::identity(), &glm::TVec3::new(0.0, 0.0, -6.0));

    let normal_matrix = glm::inverse(&model_view_matrix);
    let normal_matrix = glm::transpose(&normal_matrix);

    set_position_attribute(gl, buffers, program_info);

    gl.use_program(Some(&program_info.program));

    if let Some(projection_matrix_location) = &program_info.uniform_locations.projection_matrix {
        gl.uniform_matrix4fv_with_f32_array(
            Some(projection_matrix_location),
            false,
            &projection_matrix.iter().map(|v| *v).collect::<Vec<_>>(),
        );
    }

    if let Some(model_view_matrix_location) = &program_info.uniform_locations.model_view_matrix {
        gl.uniform_matrix4fv_with_f32_array(
            Some(model_view_matrix_location),
            false,
            &model_view_matrix.iter().map(|v| *v).collect::<Vec<_>>(),
        );
    }

    if let Some(normal_matrix_location) = &program_info.uniform_locations.normal_matrix {
        gl.uniform_matrix4fv_with_f32_array(
            Some(normal_matrix_location),
            false,
            &normal_matrix.iter().map(|v| *v).collect::<Vec<_>>(),
        )
    }

    {
        let offset = 0;
        let vertex_count = 4;
        gl.draw_arrays(GL::TRIANGLE_STRIP, offset, vertex_count);
    }

    Ok(())
}

fn set_position_attribute(gl: &GL, buffers: &Buffers, program_info: &ProgramInfo) {
    let num_components = 2;
    //let num_components = 3; // added z-component
    let type_ = GL::FLOAT;
    let normalize = false;
    let stride = 0;
    let offset = 0.0;

    if let Some(position) = &buffers.position {
        gl.bind_buffer(GL::ARRAY_BUFFER, Some(&position));
    }

    if let Some(vertex_position) = program_info.attrib_locations.vertex_position {
        gl.vertex_attrib_pointer_with_f64(
            vertex_position as u32,
            num_components,
            type_,
            normalize,
            stride,
            offset,
        );
        gl.enable_vertex_attrib_array(vertex_position as u32);
    }
}

start関数を修正します

#[wasm_bindgen]
pub async fn start() -> Result<(), JsValue> {

    let vs_source = fetch("./static/glsl/square.vert")
        .await?
        .as_string()
        .unwrap();
    let fs_source = fetch("./static/glsl/square.frag")
        .await?
        .as_string()
        .unwrap();

    let window = window()?;
    let document = window
        .document()
        .ok_or_else(|| JsValue::from_str("should have document"))?;
    let app = document
        .get_element_by_id("app")
        .ok_or_else(|| JsValue::from_str("no #app exists"))?;
    let canvas = document
        .create_element("canvas")?
        .dyn_into::<web_sys::HtmlCanvasElement>()?;
    canvas.set_attribute("width", "640")?;
    canvas.set_attribute("height", "480")?;
    app.append_child(&canvas)?;

    let gl = canvas
        .get_context("webgl2")?
        .ok_or_else(|| JsValue::from_str("fail to get context"))?
        .dyn_into::<web_sys::WebGl2RenderingContext>()?;

    let shader_program = init_shader_program(&gl, &vs_source, &fs_source)?;

    let program_info = ProgramInfo::new(&gl, shader_program);
    let buffers = init_buffers(&gl)?;

    {
        // tutorial 01
        // gl.clear_color(0.0, 0.0, 0.0, 1.0);
        // gl.clear(GL::COLOR_BUFFER_BIT);
    }

    draw_scene(&gl, &program_info, &buffers)?;

    Ok(())
}

Discussion