Web GL with bare Wasm

https://rust-tutorials.github.io/triangle-from-scratch/web_stuff/web_gl_with_bare_wasm.html

이 강좌는 모든 것을 스스로 하는 스타일의 강좌
"Rust for Wasm" 생태계의 대부분은 wasm-bindgen이라는 crate를 사용

새창을 열고 싶다면 winit 또는 sdl2 등의 것들을 사용
웹브라우저에서 무언가를 보여주려면 wasm-bindgen을 사용
일반적으로 wasm-bindgen을 사용하고 있다고 간주


Toolchain Setup

시작하기 전에 올바른 컴파일러와 도구를 사용할 수 있도록 몇 가지 추가 단계를 수행해야 함

Rust를 설치하는 것 외에도 wasm32-unknown-unknown 대상을 설치해야함

rustup target add wasm32-unknown-unknown

GitHub 리포지토리에서 wasm-opt 도구를 얻을 수 있음

The WebAssembly Binary Toolkit (WABT)에서 The WebAssembly Binary Toolkit (WABT)를 얻을 수 있음
프로그램에서 디버깅 기호 등을 제거하여 크기를 상당히 줄일 수 있음

프로그램을 was로 컴파일한 후에는 이를 표시할 방법 필요
ile:// 주소를 사용하여 브라우저에서 로컬 파일을 열 수는 없음
정적 파일을 제공할 수 있는 로컬 서버를 가동하여 화면에 표시
devserver 이용 가능

cargo install devserver

Separate Folder

web_crate 폴더 생성
Cargo.toml 파일 생성

package 설정

[package]
name = "triangle-from-scratch-web-crate"
version = "0.1.0"
authors = ["Lokathor <[email protected]>"]
edition = "2018"
license = "Zlib OR Apache-2.0 OR MIT"

crate-type 설정

wasm 라이브러리를 만들려면 crate 유형이 cdylib가 될 것이라고 Rust에 선언

[lib]
crate-type = ["cdylib"]

release 빌드 설정

릴리스 빌드와 함께 링크 시간 최적화를 수행하기 위해 내용 추가

[profile.release]
lto = "thin"


The Wasm Library

"프로그램"은 실제로 실행 파일로 빌드되지 않음
웹 페이지의 JavaScript가 로드하여 사용할 C 호환 라이브러리로 빌드
이것은 선택적 lib.rs를 사용하여 main.rs를 작성하는 대신 처음부터 코드의 100%를 lib.rs에 넣음

// lib.rs

#[no_mangle]
pub extern "C" fn start() {
  // nothing yet!
}

no_mangle 속성은 Rust가 하는 일반적인 이름 맹글링을 완전히 비활성화
이것은 Rust의 특별한 명명 체계를 모르는 외부 코드에 의해 함수가 호출되는 것을 허용
start() 함수는 extern "C" ABI를 사용한다고 선언해야 함
이것은 JavaScript와 Wasm 간에 통신할 때 올바른 호출 규칙을 제공

JavaScript가 wasm 모듈을 로드할 때 start 함수 호출
일반 프로그램의 메인 기능과 유사


The Web Page

사용자에게 표시하고 wasm을 사용할 웹페이지가 필요
index.html 파일 작성

<html>

<body>
  <canvas width="800" height="600" id="my_canvas"></canvas>
</body>

</html>

로컬 서버를 시작하고 페이지로 이동

화면에 빈 페이지가 표시된 이후
index.html body 안에 hello를 입력하고 저장 하면
변경된 내용이 즉시 반영됨

<html>

<body>
    hello

    <canvas width="800" height="600" id="my_canvas"></canvas>
</body>

</html>

index.html 파일 수정

<html>

<body>
    <canvas width="800" height="600" id="my_canvas"></canvas>

    <script>
        var importObject = {};
    
        const mod_path = 'target/wasm32-unknown-unknown/release/web_crate.wasm';
        WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
          .then(results => {
            results.instance.exports.start();
          });
      </script>
</body>

</html>

Mozilla 개발자 네트워크(MDN) 페이지에서 WebAssembly 코드 로드 및 실행 튜토리얼 참조


Make The Wasm Do Something

wasm이 캔버스를 흰색이 아닌 색상으로 변경하는 코드 작성

index.html 파일의 script 부분 수정

<html>

<body>
    <canvas width="800" height="600" id="my_canvas"></canvas>

    <script>
        var gl;
        var canvas;

        function setupCanvas() {
            console.log("Setting up the canvas...");
            let canvas = document.getElementById("my_canvas");
            gl = canvas.getContext("webgl");
            if (!gl) {
                console.log("Failed to get a WebGL context for the canvas!");
                return;
            }
        }

        function clearToBlue() {
            gl.clearColor(0.1, 0.1, 0.9, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
        }

        var importObject = {
            env: {
                setupCanvas: setupCanvas,
                clearToBlue: clearToBlue,
            }
        };

        const mod_path = 'target/wasm32-unknown-unknown/release/web_crate.wasm';
        WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
            .then(results => {
                results.instance.exports.start();
            });
    </script>
</body>

</html>

lib.rs 파일 수정

mod js {
    extern "C" {
        pub fn setupCanvas();
        pub fn clearToBlue();
    }
}

#[no_mangle]
pub extern "C" fn start() {
    unsafe {
        js::setupCanvas();
        js::clearToBlue();
    }
}

rust code build



Workflow Tweaks

wasm 모듈을 다시 빌드하려면 매번

cargo build --release --target wasm32-unknown-unknown

을 수행해야하는 문제 발생

.cargo/config.toml 파일 생성

[build]
target = "wasm32-unknown-unknown"
rustflags = ["-Zstrip=symbols"]

target 은 빌드 타겟 지정
rustflags의 -z는 unstable flag이므로 nightly 빌드 Rust에서 사용
Stable Rust에서는 wabt 툴킷을 사용해야함
Stripping Symbols은 결과물 크기를 줄여줌

HTML 페이지가 자동으로 다시 로드될 때 wasm을 수동으로 다시 빌드해야 하는 부분을 cargo-watch를 이용하여 자동으로 빌드되도록 수정

cargo install cargo-watch
cargo watch -c -x "build --release"

-c 옵션은 리빌드 될때마다 화면을 초기화
-x 옵션은 cargo-watch가 소스코드 수정을 감지했을때마다 "cargo build --release"를 실행시킴


Drawing a Triangle

삼각형을 그리려면 지금 우리가 가지고 있는 것보다 더 많은 wasm/js 상호 작용이 필요

The Rust Code

#[no_mangle]
pub extern "C" fn start() {
  unsafe {
    js::setupCanvas();

    let vertex_data = [-0.2_f32, 0.5, 0.0, -0.5, -0.4, 0.0, 0.5, -0.1, 0.0];
    let vertex_buffer = js::createBuffer();
    js::bindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
    js::bufferDataF32(
      GL_ARRAY_BUFFER,
      vertex_data.as_ptr(),
      vertex_data.len(),
      GL_STATIC_DRAW,
    );

    let index_data = [0_u16, 1, 2];
    let index_buffer = js::createBuffer();
    js::bindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);
    js::bufferDataU16(
      GL_ELEMENT_ARRAY_BUFFER,
      index_data.as_ptr(),
      index_data.len(),
      GL_STATIC_DRAW,
    );

    let vertex_shader_text = "
      attribute vec3 vertex_position;
      void main(void) {
        gl_Position = vec4(vertex_position, 1.0);
      }";
    let vertex_shader = js::createShader(GL_VERTEX_SHADER);
    js::shaderSource(
      vertex_shader,
      vertex_shader_text.as_bytes().as_ptr(),
      vertex_shader_text.len(),
    );
    js::compileShader(vertex_shader);

    let fragment_shader_text = "
      void main() {
        gl_FragColor = vec4(1.0, 0.5, 0.313, 1.0);
      }";
    let fragment_shader = js::createShader(GL_FRAGMENT_SHADER);
    js::shaderSource(
      fragment_shader,
      fragment_shader_text.as_bytes().as_ptr(),
      fragment_shader_text.len(),
    );
    js::compileShader(fragment_shader);

    let shader_program = js::createProgram();
    js::attachShader(shader_program, vertex_shader);
    js::attachShader(shader_program, fragment_shader);
    js::linkProgram(shader_program);
    js::useProgram(shader_program);

    let name = "vertex_position";
    let attrib_location = js::getAttribLocation(
      shader_program,
      name.as_bytes().as_ptr(),
      name.len(),
    );
    assert!(attrib_location != GLuint::MAX);
    js::enableVertexAttribArray(attrib_location);
    js::vertexAttribPointer(attrib_location, 3, GL_FLOAT, false, 0, 0);

    js::clearColor(0.37, 0.31, 0.86, 1.0);
    js::clear(GL_COLOR_BUFFER_BIT);
    js::drawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0);
  }
}

The JavaScript Code

body의 script 내용 수정

var gl;
        var canvas;
        var wasm_memory;
        var js_objects = [null];

        const decoder = new TextDecoder();

        function setupCanvas() {
            console.log("Setting up the canvas.");
            let canvas = document.getElementById("my_canvas");
            gl = canvas.getContext("webgl");
            if (!gl) {
                console.log("Failed to get a WebGL context for the canvas!");
                return;
            }
        }

        function clearToBlue() {
            gl.clearColor(0.1, 0.1, 0.9, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
        }

        var importObject = {
            env: {
                setupCanvas: setupCanvas,

                attachShader: function (program, shader) {
                    gl.attachShader(js_objects[program], js_objects[shader]);
                },
                bindBuffer: function (target, id) {
                    gl.bindBuffer(target, js_objects[id]);
                },
                bufferDataF32: function (target, data_ptr, data_length, usage) {
                    const data = new Float32Array(wasm_memory.buffer, data_ptr, data_length);
                    gl.bufferData(target, data, usage);
                },
                bufferDataU16: function (target, data_ptr, data_length, usage) {
                    const data = new Uint16Array(wasm_memory.buffer, data_ptr, data_length);
                    gl.bufferData(target, data, usage);
                },
                clear: function (mask) {
                    gl.clear(mask)
                },
                clearColor: function (r, g, b, a) {
                    gl.clearColor(r, g, b, a);
                },
                compileShader: function (shader) {
                    gl.compileShader(js_objects[shader]);
                },
                createBuffer: function () {
                    return js_objects.push(gl.createBuffer()) - 1;
                },
                createProgram: function () {
                    return js_objects.push(gl.createProgram()) - 1;
                },
                createShader: function (shader_type) {
                    return js_objects.push(gl.createShader(shader_type)) - 1;
                },
                drawElements: function (mode, count, type, offset) {
                    gl.drawElements(mode, count, type, offset);
                },
                enableVertexAttribArray: function (index) {
                    gl.enableVertexAttribArray(index)
                },
                getAttribLocation: function (program, pointer, length) {
                    const string_data = new Uint8Array(wasm_memory.buffer, pointer, length);
                    const string = decoder.decode(string_data);
                    return gl.getAttribLocation(js_objects[program], string);
                },
                linkProgram: function (program) {
                    gl.linkProgram(js_objects[program]);
                },
                shaderSource: function (shader, pointer, length) {
                    const string_data = new Uint8Array(wasm_memory.buffer, pointer, length);
                    const string = decoder.decode(string_data);
                    gl.shaderSource(js_objects[shader], string);
                },
                useProgram: function (program) {
                    gl.useProgram(js_objects[program]);
                },
                vertexAttribPointer: function (index, size, type, normalized, stride, offset) {
                    gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
                },
            }
        };

        const mod_path = 'target/wasm32-unknown-unknown/release/web_crate.wasm';
        WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
            .then(results => {
                console.log("Wasm instance created.");
                // assign the memory to be usable by the other functions
                wasm_memory = results.instance.exports.memory;
                // start the wasm
                results.instance.exports.start();
            });

Wasm Startup

마지막으로 시작 코드에 대해 한 가지 더 변경해야 함

const mod_path = 'target/wasm32-unknown-unknown/release/triangle_from_scratch_web_crate.wasm';
WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
  .then(results => {
    console.log("Wasm instance created.");
    // assign the memory to be usable by the other functions
    wasm_memory = results.instance.exports.memory;
    // start the wasm
    results.instance.exports.start();
  });

결과를 받은 후, export된 메모리를 wasm_memory 값에 할당
이러면 javascript에서 rust 함수를 호출 할 수 있음

좋은 웹페이지 즐겨찾기