Skip to content

Commit

Permalink
WebGL integration (#53)
Browse files Browse the repository at this point in the history
Inox2D can finally run on the web with WASM and WebGL!
The most important use-case for Inox2D is now covered :D

Thanks to @adryzz for her
[inox2d-wasm](https://github.com/adryzz/inox2d-wasm) example, I took
inspiration from it for this WebGL example.
  • Loading branch information
Speykious authored Jul 16, 2023
2 parents 921b71d + c7b795f commit 0275d2c
Show file tree
Hide file tree
Showing 15 changed files with 432 additions and 10 deletions.
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ jobs:
- { name: iOS (x86), os: macos-latest, target: "x86_64-apple-ios" }
- { name: iOS (ARM), os: macos-latest, target: "aarch64-apple-ios" }
- { name: Windows (x86), os: windows-latest, target: "x86_64-pc-windows-msvc" }
- { name: WASM, os: ubuntu-latest, target: "wasm32-unknown-unknown" }

steps:
- name: Checkout
Expand Down Expand Up @@ -62,3 +61,26 @@ jobs:
- name: Build Example (WGPU)
if: matrix.renderer == 'WGPU'
run: cargo build --example render_wgpu --no-default-features --features wgpu,owo --all-targets --target=${{ matrix.config.target }}

build-webgl:
name: Build WebGL Example
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
config:
- os: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3

- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Build (OpenGL)
run: cargo build --no-default-features --features opengl --target=wasm32-unknown-unknown

- name: Build WebGL Example
run: cargo build -p render_webgl --no-default-features --target=wasm32-unknown-unknown
18 changes: 14 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,28 @@ pollster = { version = "0.3.0", optional = true }
rayon = "1.7.0"
thiserror = "1.0.39"
tracing = "0.1.37"
wgpu = { version = "0.16.0", optional = true }
wgpu = { version = "0.16.0", optional = true, features = ["webgl"] }

[dev-dependencies]
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
clap = { version = "4.1.8", features = ["derive"] }
glutin = "0.30.6"
glutin-winit = "0.3.0"
raw-window-handle = "0.5.1"

[dev-dependencies]
tracing-subscriber = "0.3.16"
winit = "0.28.2"


[features]
default = ["opengl"]
opengl = ["dep:glow"]
wgpu = ["dep:wgpu", "dep:pollster", "dep:encase", "dep:bytemuck", "glam/bytemuck"]
wgpu = [
"dep:wgpu",
"dep:pollster",
"dep:encase",
"dep:bytemuck",
"glam/bytemuck",
]
owo = ["dep:owo-colors"]

[[example]]
Expand All @@ -45,3 +52,6 @@ required-features = ["opengl"]
[[example]]
name = "render_wgpu"
required-features = ["wgpu"]

[workspace]
members = ["examples/render_webgl"]
2 changes: 2 additions & 0 deletions examples/render_webgl/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"
2 changes: 2 additions & 0 deletions examples/render_webgl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/assets
/dist
3 changes: 3 additions & 0 deletions examples/render_webgl/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.target": "wasm32-unknown-unknown"
}
30 changes: 30 additions & 0 deletions examples/render_webgl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "render_webgl"
version = "0.1.0"
edition = "2021"

[dependencies]
console_error_panic_hook = "0.1.7"
glam = "0.24.0"
glow = "0.12.1"
inox2d = { path = "../../" }
js-sys = "0.3.64"
reqwest = "0.11.18"
tracing = "0.1.37"
tracing-subscriber = "0.3.16"
tracing-wasm = "0.2.1"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
web-time = "0.2.0"
winit = "0.28.2"

[dependencies.web-sys]
version = "0.3.61"
features = [
"Window",
"Location",
"Navigator",
"Element",
"HtmlElement",
"Document",
]
21 changes: 21 additions & 0 deletions examples/render_webgl/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Inox2D: Render WebGL example

![Aka demo picture](https://user-images.githubusercontent.com/13885008/253771145-f3921ffb-6d37-481a-ad26-4a814d070209.png)

## How to build and run

Use [Trunk](https://trunkrs.dev/) to build and run this example.

⚠️ You need to have a puppet file named `puppet.inp` in `examples/render_webgl/assets/` for the project to read. ⚠️

You can get [example models here](https://lunafoxgirlvt.itch.io/inochi-session). They're gonna be in the `.inx` format, so you'll need to convert them to `.inp` by exporting them as puppets from [Inochi Creator](https://lunafoxgirlvt.itch.io/inochi-creator).

```sh
cd examples/render_webgl

# to build
trunk build

# to run directly
trunk serve
```
16 changes: 16 additions & 0 deletions examples/render_webgl/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
<title>Inox2D WebGL Example</title>
<link data-trunk rel="rust" data-wasm-opt="z" data-reference-types>
<link data-trunk rel="copy-dir" href="assets/">
</head>

<body>
</body>

</html>
187 changes: 187 additions & 0 deletions examples/render_webgl/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#[cfg(target_arch = "wasm32")]
mod scene;

#[cfg(target_arch = "wasm32")]
fn create_window(
event: &winit::event_loop::EventLoop<()>,
) -> Result<winit::window::Window, winit::error::OsError> {
use winit::dpi::PhysicalSize;
use winit::platform::web::WindowExtWebSys;
use winit::window::WindowBuilder;

let window = WindowBuilder::new()
.with_resizable(false)
.with_inner_size(PhysicalSize::new(1280, 720))
.build(event)?;

web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| doc.body())
.and_then(|body| {
let canvas = web_sys::Element::from(window.canvas());
canvas.set_id("canvas");
body.append_child(&canvas).ok()
})
.expect("couldn't append canvas to document body");

Ok(window)
}

#[cfg(target_arch = "wasm32")]
fn request_animation_frame(f: &wasm_bindgen::prelude::Closure<dyn FnMut()>) {
use wasm_bindgen::JsCast;
web_sys::window()
.unwrap()
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("Couldn't register `requestAnimationFrame`");
}

#[cfg(target_arch = "wasm32")]
pub fn base_url() -> String {
web_sys::window().unwrap().location().origin().unwrap()
}

#[cfg(target_arch = "wasm32")]
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use std::cell::RefCell;
use std::rc::Rc;

use inox2d::{formats::inp::parse_inp, render::opengl::OpenglRenderer};

use glam::{uvec2, Vec2};
use tracing::info;
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use winit::event::{Event, WindowEvent};

use crate::scene::WasmSceneController;

let events = winit::event_loop::EventLoop::new();
let window = create_window(&events)?;

// Make sure the context has a stencil buffer
let context_options = js_sys::Object::new();
js_sys::Reflect::set(&context_options, &"stencil".into(), &true.into()).unwrap();

let gl = {
let canvas = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
let webgl2_context = canvas
.get_context_with_context_options("webgl2", &context_options)
.unwrap()
.unwrap()
.dyn_into::<web_sys::WebGl2RenderingContext>()
.unwrap();
glow::Context::from_webgl2_context(webgl2_context)
};

info!("Loading puppet");
let res = reqwest::Client::new()
.get(format!("{}/assets/puppet.inp", base_url()))
.send()
.await?;

let model_bytes = res.bytes().await?;
let model = parse_inp(model_bytes.as_ref())?;
let puppet = model.puppet;

info!("Initializing Inox2D renderer");
let window_size = window.inner_size();
let viewport = uvec2(window_size.width, window_size.height);
let mut renderer = OpenglRenderer::new(gl, viewport, &puppet)?;

info!("Uploading model textures");
renderer.upload_model_textures(&model.textures)?;
renderer.camera.scale = Vec2::splat(0.15);
info!("Inox2D renderer initialized");

let scene_ctrl = WasmSceneController::new(&renderer.camera, 0.5);

// Refcells because we need to make our own continuous animation loop.
// Winit won't help us :(
let scene_ctrl = Rc::new(RefCell::new(scene_ctrl));
let renderer = Rc::new(RefCell::new(renderer));
let puppet = Rc::new(RefCell::new(puppet));

// Setup continuous animation loop
{
let anim_loop_f = Rc::new(RefCell::new(None));
let anim_loop_g = anim_loop_f.clone();
let scene_ctrl = scene_ctrl.clone();
let renderer = renderer.clone();
let puppet = puppet.clone();

*anim_loop_g.borrow_mut() = Some(Closure::new(move || {
scene_ctrl
.borrow_mut()
.update(&mut renderer.borrow_mut().camera);

renderer.borrow().clear();
{
let mut puppet = puppet.borrow_mut();
puppet.begin_set_params();
let t = scene_ctrl.borrow().current_elapsed();
puppet.set_param("Head:: Yaw-Pitch", Vec2::new(t.cos(), t.sin()));
puppet.end_set_params();
}
renderer.borrow().render(&puppet.borrow());

request_animation_frame(anim_loop_f.borrow().as_ref().unwrap());
}));
request_animation_frame(anim_loop_g.borrow().as_ref().unwrap());
}

// Event loop
events.run(move |event, _, control_flow| {
// it needs to be present
let _window = &window;

control_flow.set_wait();

match event {
Event::WindowEvent { ref event, .. } => match event {
WindowEvent::Resized(physical_size) => {
// Handle window resizing
renderer
.borrow_mut()
.resize(physical_size.width, physical_size.height);
window.request_redraw();
}
WindowEvent::CloseRequested => control_flow.set_exit(),
_ => scene_ctrl
.borrow_mut()
.interact(&window, event, &renderer.borrow().camera),
},
Event::MainEventsCleared => {
window.request_redraw();
}
_ => (),
}
})
}

#[cfg(target_arch = "wasm32")]
async fn runwrap() {
match run().await {
Ok(_) => tracing::info!("Shutdown"),
Err(e) => tracing::error!("Fatal crash: {}", e),
}
}

#[cfg(target_arch = "wasm32")]
fn main() {
console_error_panic_hook::set_once();
tracing_wasm::set_as_global_default();
wasm_bindgen_futures::spawn_local(runwrap());
}

#[cfg(not(target_arch = "wasm32"))]
fn main() {
panic!("This is a WASM example. You need to build it for the WASM target.");
}
Loading

0 comments on commit 0275d2c

Please sign in to comment.