diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3b84e..a562262 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/Cargo.toml b/Cargo.toml index dbaa4af..3904472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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]] @@ -45,3 +52,6 @@ required-features = ["opengl"] [[example]] name = "render_wgpu" required-features = ["wgpu"] + +[workspace] +members = ["examples/render_webgl"] diff --git a/examples/render_webgl/.cargo/config.toml b/examples/render_webgl/.cargo/config.toml new file mode 100644 index 0000000..f4e8c00 --- /dev/null +++ b/examples/render_webgl/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/examples/render_webgl/.gitignore b/examples/render_webgl/.gitignore new file mode 100644 index 0000000..ab67026 --- /dev/null +++ b/examples/render_webgl/.gitignore @@ -0,0 +1,2 @@ +/assets +/dist \ No newline at end of file diff --git a/examples/render_webgl/.vscode/settings.json b/examples/render_webgl/.vscode/settings.json new file mode 100644 index 0000000..c751f3d --- /dev/null +++ b/examples/render_webgl/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.target": "wasm32-unknown-unknown" +} \ No newline at end of file diff --git a/examples/render_webgl/Cargo.toml b/examples/render_webgl/Cargo.toml new file mode 100644 index 0000000..dab09b3 --- /dev/null +++ b/examples/render_webgl/Cargo.toml @@ -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", +] diff --git a/examples/render_webgl/Readme.md b/examples/render_webgl/Readme.md new file mode 100644 index 0000000..1ffee78 --- /dev/null +++ b/examples/render_webgl/Readme.md @@ -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 +``` diff --git a/examples/render_webgl/index.html b/examples/render_webgl/index.html new file mode 100644 index 0000000..d5ca449 --- /dev/null +++ b/examples/render_webgl/index.html @@ -0,0 +1,16 @@ + + + + + + + + Inox2D WebGL Example + + + + + + + + diff --git a/examples/render_webgl/src/main.rs b/examples/render_webgl/src/main.rs new file mode 100644 index 0000000..a405555 --- /dev/null +++ b/examples/render_webgl/src/main.rs @@ -0,0 +1,187 @@ +#[cfg(target_arch = "wasm32")] +mod scene; + +#[cfg(target_arch = "wasm32")] +fn create_window( + event: &winit::event_loop::EventLoop<()>, +) -> Result { + 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) { + 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> { + 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::() + .unwrap(); + let webgl2_context = canvas + .get_context_with_context_options("webgl2", &context_options) + .unwrap() + .unwrap() + .dyn_into::() + .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."); +} diff --git a/examples/render_webgl/src/scene.rs b/examples/render_webgl/src/scene.rs new file mode 100644 index 0000000..82d9949 --- /dev/null +++ b/examples/render_webgl/src/scene.rs @@ -0,0 +1,92 @@ +//! A nice scene controller to smoothly move around in the window. + +use glam::{vec2, Vec2}; +use inox2d::math::camera::Camera; +use tracing::info; +use web_time::Instant; +use winit::event::{ElementState, MouseScrollDelta, WindowEvent}; +use winit::window::Window; + +pub struct WasmSceneController { + // for camera position and mouse interactions + camera_pos: Vec2, + mouse_pos: Vec2, + mouse_pos_held: Vec2, + mouse_state: ElementState, + + // for smooth scrolling + pub scroll_speed: f32, + hard_scale: Vec2, + + // for FPS-independent interactions + start: Instant, + prev_elapsed: f32, + current_elapsed: f32, +} + +impl WasmSceneController { + pub fn new(camera: &Camera, scroll_speed: f32) -> Self { + Self { + camera_pos: camera.position, + mouse_pos: Vec2::default(), + mouse_pos_held: Vec2::default(), + mouse_state: ElementState::Released, + scroll_speed, + hard_scale: camera.scale, + start: Instant::now(), + prev_elapsed: 0.0, + current_elapsed: 0.0, + } + } + + pub fn update(&mut self, camera: &mut Camera) { + // Smooth scrolling + let time_delta = self.current_elapsed - self.prev_elapsed; + camera.scale = camera.scale + time_delta.powf(0.6) * (self.hard_scale - camera.scale); + + // Mouse dragging + if self.mouse_state == ElementState::Pressed { + camera.position = + self.camera_pos + (self.mouse_pos - self.mouse_pos_held) / camera.scale; + } + + // Frame interval + self.prev_elapsed = self.current_elapsed; + self.current_elapsed = self.start.elapsed().as_secs_f32(); + } + + pub fn interact(&mut self, window: &Window, event: &WindowEvent, camera: &Camera) { + match event { + WindowEvent::CursorMoved { position, .. } => { + self.mouse_pos = vec2(position.x as f32, position.y as f32); + + if self.mouse_state == ElementState::Pressed { + window.request_redraw(); + } + } + WindowEvent::MouseInput { state, .. } => { + self.mouse_state = *state; + if self.mouse_state == ElementState::Pressed { + self.mouse_pos_held = self.mouse_pos; + self.camera_pos = camera.position; + } + } + WindowEvent::MouseWheel { delta, .. } => { + // Handle mouse wheel (zoom) + let my = match delta { + MouseScrollDelta::LineDelta(_, y) => *y * 12., + MouseScrollDelta::PixelDelta(pos) => pos.y as f32 * 0.1, + }; + + self.hard_scale *= 2_f32.powf(self.scroll_speed * my * 0.1); + + window.request_redraw(); + } + _ => (), + } + } + + pub fn current_elapsed(&self) -> f32 { + self.current_elapsed + } +} diff --git a/src/render/opengl/mod.rs b/src/render/opengl/mod.rs index 6f2f4a9..5f9be01 100644 --- a/src/render/opengl/mod.rs +++ b/src/render/opengl/mod.rs @@ -9,7 +9,7 @@ use std::ops::Deref; use glam::{uvec2, UVec2, Vec3}; use glow::HasContext; -use tracing::error; +use tracing::{error, debug}; use crate::math::camera::Camera; use crate::model::ModelTexture; @@ -187,6 +187,10 @@ impl OpenglRenderer { textures: Vec::new(), }; + // Set emission strength once (it doesn't change anywhere else) + renderer.bind_shader(&renderer.part_shader); + renderer.part_shader.set_emission_strength(&renderer.gl, 1.); + renderer.resize(viewport.x, viewport.y); unsafe { renderer.attach_framebuffer_textures() }; @@ -201,8 +205,9 @@ impl OpenglRenderer { let shalltexs = decode_model_textures(model_textures); // upload textures - for shalltex in shalltexs { - let tex = texture::Texture::from_shallow_texture(&self.gl, &shalltex)?; + for (i, shalltex) in shalltexs.iter().enumerate() { + debug!("Uploading shallow texture {}", i); + let tex = texture::Texture::from_shallow_texture(&self.gl, shalltex)?; self.textures.push(tex); } diff --git a/src/render/opengl/shader.rs b/src/render/opengl/shader.rs index bb77dd3..4e0e96a 100644 --- a/src/render/opengl/shader.rs +++ b/src/render/opengl/shader.rs @@ -13,6 +13,19 @@ pub(crate) fn compile( unsafe { let program = gl.create_program().map_err(ShaderCompileError)?; + // Use GLSL ES 3.00 on WASM for WebGL + #[cfg(target_arch = "wasm32")] + let (vertex, fragment) = ( + &format!( + "#version 300 es\nprecision highp float;\n{}", + vertex.replace("#version 330", "") + ), + &format!( + "#version 300 es\nprecision highp float;\n{}", + fragment.replace("#version 330", "") + ), + ); + let shader = gl .create_shader(glow::VERTEX_SHADER) .map_err(ShaderCompileError)?; diff --git a/src/render/opengl/shaders.rs b/src/render/opengl/shaders.rs index 37f357c..517a54f 100644 --- a/src/render/opengl/shaders.rs +++ b/src/render/opengl/shaders.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use glam::{Mat4, Vec2, Vec3}; use glow::HasContext; +use tracing::debug; use super::shader::{self, ShaderCompileError}; @@ -17,6 +18,7 @@ pub struct PartShader { u_opacity: Option, u_mult_color: Option, u_screen_color: Option, + u_emission_strength: Option, } impl Deref for PartShader { @@ -29,6 +31,7 @@ impl Deref for PartShader { impl PartShader { pub fn new(gl: &glow::Context) -> Result { + debug!("Compiling Part shader"); let program = shader::compile(gl, PART_VERT, PART_FRAG)?; Ok(Self { @@ -38,6 +41,7 @@ impl PartShader { u_opacity: unsafe { gl.get_uniform_location(program, "opacity") }, u_mult_color: unsafe { gl.get_uniform_location(program, "multColor") }, u_screen_color: unsafe { gl.get_uniform_location(program, "screenColor") }, + u_emission_strength: unsafe { gl.get_uniform_location(program, "emissionStrength") }, }) } @@ -70,6 +74,12 @@ impl PartShader { pub fn set_screen_color(&self, gl: &glow::Context, screen_color: Vec3) { unsafe { gl.uniform_3_f32_slice(self.u_screen_color.as_ref(), screen_color.as_ref()) }; } + + /// Sets the `emissionStrength` uniform of the shader. + #[inline] + pub fn set_emission_strength(&self, gl: &glow::Context, emission_strength: f32) { + unsafe { gl.uniform_1_f32(self.u_emission_strength.as_ref(), emission_strength) }; + } } pub struct PartMaskShader { @@ -89,6 +99,7 @@ impl Deref for PartMaskShader { impl PartMaskShader { pub fn new(gl: &glow::Context) -> Result { + debug!("Compiling Part Mask shader"); let program = shader::compile(gl, PART_VERT, PART_MASK_FRAG)?; Ok(Self { @@ -140,6 +151,7 @@ impl Deref for CompositeShader { impl CompositeShader { pub fn new(gl: &glow::Context) -> Result { + debug!("Compiling Composite shader"); let program = shader::compile(gl, COMP_VERT, COMP_FRAG)?; Ok(Self { @@ -193,6 +205,7 @@ impl Deref for CompositeMaskShader { impl CompositeMaskShader { pub fn new(gl: &glow::Context) -> Result { + debug!("Compiling Composite Mask shader"); let program = shader::compile(gl, COMP_VERT, COMP_MASK_FRAG)?; Ok(Self { diff --git a/src/render/opengl/shaders/basic/basic.frag b/src/render/opengl/shaders/basic/basic.frag index 9efe3da..9e62cfb 100644 --- a/src/render/opengl/shaders/basic/basic.frag +++ b/src/render/opengl/shaders/basic/basic.frag @@ -18,7 +18,7 @@ uniform sampler2D bumpmap; uniform float opacity; uniform vec3 multColor; uniform vec3 screenColor; -uniform float emissionStrength = 1; +uniform float emissionStrength; void main() { // Sample texture diff --git a/src/render/opengl/texture.rs b/src/render/opengl/texture.rs index fc60854..8a7cab9 100644 --- a/src/render/opengl/texture.rs +++ b/src/render/opengl/texture.rs @@ -151,11 +151,17 @@ pub unsafe fn upload_empty( height: u32, ty: u32, ) { + let internal_format = if ty == glow::FLOAT { + glow::RGBA32F + } else { + glow::RGBA8 + } as i32; + gl.bind_texture(glow::TEXTURE_2D, Some(tex)); gl.tex_image_2d( glow::TEXTURE_2D, 0, - glow::RGBA as i32, + internal_format, width as i32, height as i32, 0,