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,