~luyu/comp5411-rendering-project

da0a6999ac893e9df1077d4553d3260d51e54801 — Luyu Cheng 2 months ago 4f96fc8
refactor: extract model and common GL patterns
3 files changed, 254 insertions(+), 251 deletions(-)

A src/gl.rs
M src/lib.rs
A src/model.rs
A src/gl.rs => src/gl.rs +102 -0
@@ 0,0 1,102 @@
use web_sys::{WebGl2RenderingContext, WebGlProgram, WebGlShader};

macro_rules! get_attribute_location {
    ($context:ident, $program:ident, $name:expr) => {
        (match $context.get_attrib_location(&$program, $name) {
            location if location >= 0 => Some(location as u32),
            _ => None,
        })
        .ok_or(format!(
            "Failed to get the location of attribute \"{}\"",
            $name
        ))?;
    };
}

macro_rules! get_uniform_location {
    ($context:ident, $program:ident, $name:expr) => {
        $context
            .get_uniform_location(&$program, $name)
            .ok_or(format!("Failed to get the location of uniform {}", $name))
    };
}

macro_rules! prepare_buffer {
    ($context:ident, $array_buffer_type:expr, $data:expr, $js_array_type:ident) => {{
        let buffer = $context
            .create_buffer()
            .ok_or(format!("Failed to create buffer for {}", stringify!($data)))?;
        $context.bind_buffer($array_buffer_type, Some(&buffer));
        // Note that `Float32Array::view` is somewhat dangerous (hence the
        // `unsafe`!). This is creating a raw view into our module's
        // `WebAssembly.Memory` buffer, but if we allocate more pages for ourself
        // (aka do a memory allocation in Rust) it'll cause the buffer to change,
        // causing the `Float32Array` to be invalid.
        //
        // As a result, after `Float32Array::view` we have to be very careful not to
        // do any memory allocations before it's dropped.
        unsafe {
            let array_buffer_view = js_sys::$js_array_type::view(&$data);
            $context.buffer_data_with_array_buffer_view(
                $array_buffer_type,
                &array_buffer_view,
                WebGl2RenderingContext::STATIC_DRAW,
            );
        }
        buffer
    }};
}

pub(crate) use get_attribute_location;
pub(crate) use get_uniform_location;
pub(crate) use prepare_buffer;

pub fn compile_shader(
    context: &WebGl2RenderingContext,
    shader_type: u32,
    source: &str,
) -> Result<WebGlShader, String> {
    let shader = context
        .create_shader(shader_type)
        .ok_or_else(|| String::from("Unable to create shader object"))?;
    context.shader_source(&shader, source);
    context.compile_shader(&shader);

    if context
        .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
        .as_bool()
        .unwrap_or(false)
    {
        Ok(shader)
    } else {
        Err(context
            .get_shader_info_log(&shader)
            .unwrap_or_else(|| String::from("Unknown error creating shader")))
    }
}

pub fn link_program(
    context: &WebGl2RenderingContext,
    vert_shader: &WebGlShader,
    frag_shader: &WebGlShader,
) -> Result<WebGlProgram, String> {
    let program = context
        .create_program()
        .ok_or_else(|| String::from("Unable to create shader object"))?;

    context.attach_shader(&program, vert_shader);
    context.attach_shader(&program, frag_shader);
    context.link_program(&program);

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

M src/lib.rs => src/lib.rs +20 -251
@@ 8,8 8,10 @@ use std::rc::Rc;
use std::vec;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{WebGl2RenderingContext, WebGlProgram, WebGlShader};
use web_sys::WebGl2RenderingContext;

mod gl;
mod model;
mod texture;

/// Defined in https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animation-frames


@@ 22,53 24,6 @@ enum DragMode {
    Rotate,
}

macro_rules! get_attribute_location {
    ($context:ident, $program:ident, $name:expr) => {
        (match $context.get_attrib_location(&$program, $name) {
            location if location >= 0 => Some(location as u32),
            _ => None,
        })
        .ok_or(format!(
            "Failed to get the location of attribute \"{}\"",
            $name
        ))?;
    };
}

macro_rules! get_uniform_location {
    ($context:ident, $program:ident, $name:expr) => {
        $context
            .get_uniform_location(&$program, $name)
            .ok_or(format!("Failed to get the location of uniform {}", $name))
    };
}

macro_rules! prepare_buffer {
    ($context:ident, $array_buffer_type:expr, $data:expr, $js_array_type:ident) => {{
        let buffer = $context
            .create_buffer()
            .ok_or(format!("Failed to create buffer for {}", stringify!($data)))?;
        $context.bind_buffer($array_buffer_type, Some(&buffer));
        // Note that `Float32Array::view` is somewhat dangerous (hence the
        // `unsafe`!). This is creating a raw view into our module's
        // `WebAssembly.Memory` buffer, but if we allocate more pages for ourself
        // (aka do a memory allocation in Rust) it'll cause the buffer to change,
        // causing the `Float32Array` to be invalid.
        //
        // As a result, after `Float32Array::view` we have to be very careful not to
        // do any memory allocations before it's dropped.
        unsafe {
            let array_buffer_view = js_sys::$js_array_type::view(&$data);
            $context.buffer_data_with_array_buffer_view(
                $array_buffer_type,
                &array_buffer_view,
                WebGl2RenderingContext::STATIC_DRAW,
            );
        }
        buffer
    }};
}

#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
    let window = web_sys::window().ok_or("no global `window` exists")?;


@@ 97,62 52,59 @@ pub fn start() -> Result<(), JsValue> {
    context.enable(WebGl2RenderingContext::DEPTH_TEST);
    context.depth_func(WebGl2RenderingContext::LESS);

    let vert_shader = compile_shader(
    let vert_shader = gl::compile_shader(
        &context,
        WebGl2RenderingContext::VERTEX_SHADER,
        include_str!("shader.vert"),
    )?;

    let frag_shader = compile_shader(
    let frag_shader = gl::compile_shader(
        &context,
        WebGl2RenderingContext::FRAGMENT_SHADER,
        include_str!("shader.frag"),
    )?;
    let program = link_program(&context, &vert_shader, &frag_shader)?;
    let program = gl::link_program(&context, &vert_shader, &frag_shader)?;
    context.use_program(Some(&program));

    // let vertices: [f32; 9] = [-0.7, -0.7, 0.0, 0.7, -0.7, 0.0, 0.0, 0.7, 0.0];
    // let cube = Cube::square(0.0);
    let cube = Model::cube();
    let cube = model::Model::cube();

    if cfg!(debug_assertions) {
        cube.debug();
        web_sys::console::log_1(&format!("Model validation: {:?}", cube.check()).into());
    }

    web_sys::console::log_1(&format!("Check: {:?}", cube.check()).into());

    let model_uniform_location = get_uniform_location!(context, program, "model")?;
    let view_uniform_location = get_uniform_location!(context, program, "view")?;
    let projection_uniform_location = get_uniform_location!(context, program, "projection")?;
    let normal_matrix_uniform_location = get_uniform_location!(context, program, "normalMatrix")?;
    let sampler_uniform_location = get_uniform_location!(context, program, "textureSampler")?;
    let model_uniform_location = gl::get_uniform_location!(context, program, "model")?;
    let view_uniform_location = gl::get_uniform_location!(context, program, "view")?;
    let projection_uniform_location = gl::get_uniform_location!(context, program, "projection")?;
    let normal_matrix_uniform_location = gl::get_uniform_location!(context, program, "normalMatrix")?;
    let sampler_uniform_location = gl::get_uniform_location!(context, program, "textureSampler")?;

    let texture = texture::load_texture(&context).ok_or("Failed to load the texture.")?;

    // Get attributes in the program.
    let position_attribute_location = get_attribute_location!(context, program, "position");
    let normal_attribute_location = get_attribute_location!(context, program, "normal");
    let texture_attribute_location = get_attribute_location!(context, program, "textureCoord");
    let position_attribute_location = gl::get_attribute_location!(context, program, "position");
    let normal_attribute_location = gl::get_attribute_location!(context, program, "normal");
    let texture_attribute_location = gl::get_attribute_location!(context, program, "textureCoord");

    let texture_coordinates_buffer = prepare_buffer!(
    let texture_coordinates_buffer = gl::prepare_buffer!(
        context,
        WebGl2RenderingContext::ARRAY_BUFFER,
        cube.texture_coordinates,
        Float32Array
    );
    let position_buffer = prepare_buffer!(
    let position_buffer = gl::prepare_buffer!(
        context,
        WebGl2RenderingContext::ARRAY_BUFFER,
        cube.vertices,
        Float32Array
    );
    let normal_buffer = prepare_buffer!(
    let normal_buffer = gl::prepare_buffer!(
        context,
        WebGl2RenderingContext::ARRAY_BUFFER,
        cube.normals,
        Float32Array
    );
    let index_buffer = prepare_buffer!(
    let index_buffer = gl::prepare_buffer!(
        context,
        WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
        cube.indices,


@@ 391,192 343,9 @@ pub fn start() -> Result<(), JsValue> {
    Ok(())
}

struct Model {
    pub vertices: Vec<f32>,
    pub normals: Vec<f32>,
    pub indices: Vec<u16>,
    pub texture_coordinates: Vec<f32>,
}

impl Model {
    fn cube() -> Model {
        let mut vertices = Vec::with_capacity(4 * 6 * 3);
        let mut normals = Vec::with_capacity(4 * 6 * 3);
        let mut indices = Vec::with_capacity(6 * 6);
        // TODO: generate automatically.
        let mut texture_coordinates = Vec::with_capacity(6 * 4 * 2);
        // Generate four vertices for each face.
        // Therefore, there are 4 * 6 = 24 vertices.
        for i in 0..6 {
            // Fix an axis (x, y, or z).
            let axis0 = i >> 1;
            let axis1 = (axis0 + 1) % 3;
            let axis2 = (axis0 + 2) % 3;
            let mut position = [0.0; 3];
            position[axis0] = if i & 1 == 0 { -1.0 } else { 1.0 };
            // Enumerate 4 combinations of remaining axes (axis 1 and axis 2).
            // The order is in a Z shape: (-1, -1), (-1, 1), (1, -1), (1, 1).
            for j in 0b00..=0b11 {
                position[axis1] = if j & 0b01 == 0 { -1.0 } else { 1.0 };
                position[axis2] = if j & 0b10 == 0 { -1.0 } else { 1.0 };
                // Save the position.
                vertices.extend_from_slice(&position);
                position[axis1] = 0.0;
                position[axis2] = 0.0;
                normals.extend_from_slice(&position);
            }
            // Generate four vertex indices for each face.
            // Because the vertices are in a Z shape, the indices are same.
            indices.extend_from_slice(&[
                i as u16 * 4 + 0,
                i as u16 * 4 + 1,
                i as u16 * 4 + 2,
                i as u16 * 4 + 1,
                i as u16 * 4 + 2,
                i as u16 * 4 + 3,
            ]);
            // Generate texture coordinates for the face.
            texture_coordinates.extend_from_slice(&[0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]);
        }
        Model {
            vertices,
            normals,
            indices,
            texture_coordinates,
        }
    }

    pub fn check(&self) -> bool {
        if self.vertices.len() % 3 != 0
            || self.normals.len() % 3 != 0
            || self.texture_coordinates.len() % 2 != 0
            || self.vertices.len() != self.normals.len()
            || self.vertices.len() / 3 != self.texture_coordinates.len() / 2
        {
            return false;
        }
        for index in &self.indices {
            let i = *index as usize;
            if i >= self.vertices.len() / 3 {
                return false;
            }
        }
        true
    }

    pub fn debug(&self) {
        web_sys::console::log_1(
            &format!("Vertices: {}", pretty_print_packed_array(&self.vertices, 3)).into(),
        );
        web_sys::console::log_1(
            &format!("Normals: {}", pretty_print_packed_array(&self.normals, 3)).into(),
        );
        web_sys::console::log_1(
            &format!(
                "Texture coordinates: {}",
                pretty_print_packed_array(&self.texture_coordinates, 2)
            )
            .into(),
        );
        web_sys::console::log_1(&format!("Indices: {:?}", self.indices).into());
    }

    #[allow(dead_code)]
    fn two_squares() -> Model {
        let mut front = Model::square(1.0);
        let mut back = Model::square(-1.0);
        for idx in back.indices.iter_mut() {
            *idx += (front.vertices.len() / 3) as u16;
        }
        front.vertices.extend_from_slice(&back.vertices);
        front.normals.extend_from_slice(&back.normals);
        front.indices.extend_from_slice(&back.indices);
        front
            .texture_coordinates
            .extend_from_slice(&back.texture_coordinates);
        front
    }

    #[allow(dead_code)]
    fn square(z: f32) -> Model {
        Model {
            vertices: vec![1.0, 1.0, z, 1.0, -1.0, z, -1.0, 1.0, z, -1.0, -1.0, z],
            normals: vec![],
            indices: vec![0, 1, 2, 1, 2, 3],
            texture_coordinates: vec![],
        }
    }
}

fn pretty_print_packed_array(values: &[f32], length: usize) -> String {
    values
        .chunks(length)
        .map(|xs| {
            format!(
                "({})",
                xs.iter()
                    .map(|x| x.to_string())
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        })
        .collect::<Vec<_>>()
        .join(", ")
}

fn request_animation_frame(f: &Closure<FrameRequestCallback>) {
    web_sys::window()
        .unwrap()
        .request_animation_frame(f.as_ref().unchecked_ref())
        .expect("should register `requestAnimationFrame` OK");
}

pub fn compile_shader(
    context: &WebGl2RenderingContext,
    shader_type: u32,
    source: &str,
) -> Result<WebGlShader, String> {
    let shader = context
        .create_shader(shader_type)
        .ok_or_else(|| String::from("Unable to create shader object"))?;
    context.shader_source(&shader, source);
    context.compile_shader(&shader);

    if context
        .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
        .as_bool()
        .unwrap_or(false)
    {
        Ok(shader)
    } else {
        Err(context
            .get_shader_info_log(&shader)
            .unwrap_or_else(|| String::from("Unknown error creating shader")))
    }
}

pub fn link_program(
    context: &WebGl2RenderingContext,
    vert_shader: &WebGlShader,
    frag_shader: &WebGlShader,
) -> Result<WebGlProgram, String> {
    let program = context
        .create_program()
        .ok_or_else(|| String::from("Unable to create shader object"))?;

    context.attach_shader(&program, vert_shader);
    context.attach_shader(&program, frag_shader);
    context.link_program(&program);

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

A src/model.rs => src/model.rs +132 -0
@@ 0,0 1,132 @@
pub struct Model {
    pub vertices: Vec<f32>,
    pub normals: Vec<f32>,
    pub indices: Vec<u16>,
    pub texture_coordinates: Vec<f32>,
}

impl Model {
    pub fn cube() -> Model {
        let mut vertices = Vec::with_capacity(4 * 6 * 3);
        let mut normals = Vec::with_capacity(4 * 6 * 3);
        let mut indices = Vec::with_capacity(6 * 6);
        // TODO: generate automatically.
        let mut texture_coordinates = Vec::with_capacity(6 * 4 * 2);
        // Generate four vertices for each face.
        // Therefore, there are 4 * 6 = 24 vertices.
        for i in 0..6 {
            // Fix an axis (x, y, or z).
            let axis0 = i >> 1;
            let axis1 = (axis0 + 1) % 3;
            let axis2 = (axis0 + 2) % 3;
            let mut position = [0.0; 3];
            position[axis0] = if i & 1 == 0 { -1.0 } else { 1.0 };
            // Enumerate 4 combinations of remaining axes (axis 1 and axis 2).
            // The order is in a Z shape: (-1, -1), (-1, 1), (1, -1), (1, 1).
            for j in 0b00..=0b11 {
                position[axis1] = if j & 0b01 == 0 { -1.0 } else { 1.0 };
                position[axis2] = if j & 0b10 == 0 { -1.0 } else { 1.0 };
                // Save the position.
                vertices.extend_from_slice(&position);
                position[axis1] = 0.0;
                position[axis2] = 0.0;
                normals.extend_from_slice(&position);
            }
            // Generate four vertex indices for each face.
            // Because the vertices are in a Z shape, the indices are same.
            indices.extend_from_slice(&[
                i as u16 * 4 + 0,
                i as u16 * 4 + 1,
                i as u16 * 4 + 2,
                i as u16 * 4 + 1,
                i as u16 * 4 + 2,
                i as u16 * 4 + 3,
            ]);
            // Generate texture coordinates for the face.
            texture_coordinates.extend_from_slice(&[0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]);
        }
        Model {
            vertices,
            normals,
            indices,
            texture_coordinates,
        }
    }

    pub fn check(&self) -> bool {
        if self.vertices.len() % 3 != 0
            || self.normals.len() % 3 != 0
            || self.texture_coordinates.len() % 2 != 0
            || self.vertices.len() != self.normals.len()
            || self.vertices.len() / 3 != self.texture_coordinates.len() / 2
        {
            return false;
        }
        for index in &self.indices {
            let i = *index as usize;
            if i >= self.vertices.len() / 3 {
                return false;
            }
        }
        true
    }

    pub fn debug(&self) {
        web_sys::console::log_1(
            &format!("Vertices: {}", pretty_print_packed_array(&self.vertices, 3)).into(),
        );
        web_sys::console::log_1(
            &format!("Normals: {}", pretty_print_packed_array(&self.normals, 3)).into(),
        );
        web_sys::console::log_1(
            &format!(
                "Texture coordinates: {}",
                pretty_print_packed_array(&self.texture_coordinates, 2)
            )
            .into(),
        );
        web_sys::console::log_1(&format!("Indices: {:?}", self.indices).into());
    }

    #[allow(dead_code)]
    fn two_squares() -> Model {
        let mut front = Model::square(1.0);
        let mut back = Model::square(-1.0);
        for idx in back.indices.iter_mut() {
            *idx += (front.vertices.len() / 3) as u16;
        }
        front.vertices.extend_from_slice(&back.vertices);
        front.normals.extend_from_slice(&back.normals);
        front.indices.extend_from_slice(&back.indices);
        front
            .texture_coordinates
            .extend_from_slice(&back.texture_coordinates);
        front
    }

    #[allow(dead_code)]
    fn square(z: f32) -> Model {
        Model {
            vertices: vec![1.0, 1.0, z, 1.0, -1.0, z, -1.0, 1.0, z, -1.0, -1.0, z],
            normals: vec![],
            indices: vec![0, 1, 2, 1, 2, 3],
            texture_coordinates: vec![],
        }
    }
}

fn pretty_print_packed_array(values: &[f32], length: usize) -> String {
    values
        .chunks(length)
        .map(|xs| {
            format!(
                "({})",
                xs.iter()
                    .map(|x| x.to_string())
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        })
        .collect::<Vec<_>>()
        .join(", ")
}