Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGL integration #53

Merged
merged 14 commits into from
Jul 16, 2023
Merged
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
Loading