// based on the WebGL example in the `wasm-bindgen` guide // // https://rustwasm.github.io/wasm-bindgen/examples/webgl.html // // and this StackOverflow answer by wangdq // // https://stackoverflow.com/a/39684775 // extern crate js_sys; use core::array; use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; use web_sys::{ console, window, KeyboardEvent, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation }; mod engine; fn compile_shader( context: &WebGl2RenderingContext, shader_type: u32, source: &str, ) -> WebGlShader { let shader = context.create_shader(shader_type).unwrap(); context.shader_source(&shader, source); context.compile_shader(&shader); shader } fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, var_name: &str, member_name_opt: Option<&str> ) -> [Option; N] { array::from_fn(|n| { let name = match member_name_opt { Some(member_name) => format!("{var_name}[{n}].{member_name}"), None => format!("{var_name}[{n}]") }; context.get_uniform_location(&program, name.as_str()) }) } // load the given data into the vertex input of the given name fn bind_vertex_attrib( context: &WebGl2RenderingContext, index: u32, size: i32, data: &[f32] ) { // create a data buffer and bind it to ARRAY_BUFFER let buffer = context.create_buffer().unwrap(); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); // load the given data into the buffer. the function `Float32Array::view` // creates a raw view into our module's `WebAssembly.Memory` buffer. // allocating more memory will change the buffer, invalidating the view. // that means we have to make sure we don't allocate any memory until the // view is dropped unsafe { context.buffer_data_with_array_buffer_view( WebGl2RenderingContext::ARRAY_BUFFER, &js_sys::Float32Array::view(&data), WebGl2RenderingContext::STATIC_DRAW, ); } // allow the target attribute to be used context.enable_vertex_attrib_array(index); // take whatever's bound to ARRAY_BUFFER---here, the data buffer created // above---and bind it to the target attribute // // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer // context.vertex_attrib_pointer_with_i32( index, size, WebGl2RenderingContext::FLOAT, false, // don't normalize 0, // zero stride 0, // zero offset ); } fn push_gen_construction( sphere_vec: &mut Vec>, color_vec: &mut Vec<[f32; 3]>, construction_to_world: &DMatrix, ctrl_x: f64, ctrl_y: f64, radius_x: f64, radius_y: f64 ) { // push spheres sphere_vec.push(construction_to_world * engine::sphere(0.5, 0.5, ctrl_x, radius_x)); sphere_vec.push(construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y, radius_y)); sphere_vec.push(construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75)); sphere_vec.push(construction_to_world * engine::sphere(0.5, -0.5, 0.0, 0.5)); sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.15, 1.0, 0.25)); sphere_vec.push(construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25)); // push colors color_vec.push([1.00_f32, 0.25_f32, 0.00_f32]); color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); color_vec.push([0.25_f32, 0.00_f32, 1.00_f32]); color_vec.push([0.25_f32, 1.00_f32, 0.00_f32]); color_vec.push([0.75_f32, 0.75_f32, 0.00_f32]); color_vec.push([0.00_f32, 0.75_f32, 0.50_f32]); } fn push_low_curv_construction( sphere_vec: &mut Vec>, color_vec: &mut Vec<[f32; 3]>, construction_to_world: &DMatrix, off1: f64, off2: f64, off3: f64, curv1: f64, curv2: f64, curv3: f64, ) { // push spheres let a = 0.75_f64.sqrt(); sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.0, 0.0, 1.0)); sphere_vec.push(construction_to_world * engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)); sphere_vec.push(construction_to_world * engine::sphere_with_offset(1.0, 0.0, 0.0, off1, curv1)); sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, a, 0.0, off2, curv2)); sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, -a, 0.0, off3, curv3)); sphere_vec.push(construction_to_world * engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)); sphere_vec.push(construction_to_world * engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)); sphere_vec.push(construction_to_world * engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)); // push colors color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); color_vec.push([1.00_f32, 0.00_f32, 0.25_f32]); color_vec.push([0.25_f32, 1.00_f32, 0.00_f32]); color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); } #[derive(Clone, Copy, PartialEq)] enum Tab { GenTab, LowCurvTab } fn main() { // set up a config option that forwards panic messages to `console.error` #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); sycamore::render(|| { // tab selection let tab_selection = create_signal(Tab::GenTab); // navigation let pitch_up = create_signal(0.0); let pitch_down = create_signal(0.0); let yaw_right = create_signal(0.0); let yaw_left = create_signal(0.0); // controls for general example let gen_controls = create_node_ref(); let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); let radius_x = create_signal(1.0); let radius_y = create_signal(1.0); // controls for low-curvature example let low_curv_controls = create_node_ref(); let curv1 = create_signal(0.0); let curv2 = create_signal(0.0); let curv3 = create_signal(0.0); let off1 = create_signal(1.0); let off2 = create_signal(1.0); let off3 = create_signal(1.0); // shared controls let opacity = create_signal(0.5); let highlight = create_signal(0.2); let turntable = create_signal(false); let layer_threshold = create_signal(0.0); /* DEBUG */ let debug_mode = create_signal(false); /* DEBUG */ /* INSTRUMENTS */ const SAMPLE_PERIOD: i32 = 60; let mut last_sample_time = 0.0; let mut frames_since_last_sample = 0; let mean_frame_interval = create_signal(0.0); // display let display = create_node_ref(); // change listener let scene_changed = create_signal(true); create_effect(move || { // track tab selection tab_selection.track(); // track controls for general example ctrl_x.track(); ctrl_y.track(); radius_x.track(); radius_y.track(); // track controls for low-curvature example curv1.track(); curv2.track(); curv3.track(); off1.track(); off2.track(); off3.track(); // track shared controls opacity.track(); highlight.track(); turntable.track(); layer_threshold.track(); debug_mode.track(); scene_changed.set(true); }); on_mount(move || { // tab listener create_effect(move || { // get the control panel nodes let gen_controls_node = gen_controls.get::(); let low_curv_controls_node = low_curv_controls.get::(); // hide all the control panels gen_controls_node.add_class("hidden"); low_curv_controls_node.add_class("hidden"); // show the selected control panel match tab_selection.get() { Tab::GenTab => gen_controls_node.remove_class("hidden"), Tab::LowCurvTab => low_curv_controls_node.remove_class("hidden") } }); // create list of construction elements const SPHERE_MAX: usize = 200; let mut sphere_vec = Vec::>::new(); let mut color_vec = Vec::<[f32; 3]>::new(); // timing let mut last_time = 0.0; // scene parameters const NAV_SPEED: f64 = 0.4; // in radians per second const TURNTABLE_SPEED: f64 = 0.1; // in radians per second let mut orientation = DMatrix::::identity(5, 5); let mut rotation = DMatrix::::identity(5, 5); let location = { const LEN: f64 = -5.0; const LEN_SQ: f64 = LEN*LEN; DMatrix::from_column_slice(5, 5, &[ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, LEN, 0.0, 0.0, 2.0*LEN, 1.0, LEN_SQ, 0.0, 0.0, 0.0, 0.0, 1.0 ]) }; /* INSTRUMENTS */ let performance = window().unwrap().performance().unwrap(); // get the display canvas let canvas = display .get::() .unchecked_into::(); let ctx = canvas .get_context("webgl2") .unwrap() .unwrap() .dyn_into::() .unwrap(); // compile and attach the vertex and fragment shaders let vertex_shader = compile_shader( &ctx, WebGl2RenderingContext::VERTEX_SHADER, include_str!("identity.vert"), ); let fragment_shader = compile_shader( &ctx, WebGl2RenderingContext::FRAGMENT_SHADER, include_str!("inversive.frag"), ); let program = ctx.create_program().unwrap(); ctx.attach_shader(&program, &vertex_shader); ctx.attach_shader(&program, &fragment_shader); ctx.link_program(&program); let link_status = ctx .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) .as_bool() .unwrap(); let link_msg = if link_status { "Linked successfully" } else { "Linking failed" }; console::log_1(&JsValue::from(link_msg)); ctx.use_program(Some(&program)); /* DEBUG */ // print the maximum number of vectors that can be passed as // uniforms to a fragment shader. the OpenGL ES 3.0 standard // requires this maximum to be at least 224, as discussed in the // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter // here: // // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml // // there are also other size limits. for example, on Aaron's // machine, the the length of a float or genType array seems to be // capped at 1024 elements console::log_2( &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), &JsValue::from("uniform vectors available") ); // find indices of vertex attributes and uniforms let position_index = ctx.get_attrib_location(&program, "position") as u32; let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( &ctx, &program, "sphere_list", Some("sp") ); let sphere_lt_locs = get_uniform_array_locations::( &ctx, &program, "sphere_list", Some("lt") ); let color_locs = get_uniform_array_locations::( &ctx, &program, "color_list", None ); let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); /* DEBUG */ let radius_loc = ctx.get_uniform_location(&program, "radius"); /* DEBUG */ let opacity_loc = ctx.get_uniform_location(&program, "opacity"); let highlight_loc = ctx.get_uniform_location(&program, "highlight"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); ctx.bind_vertex_array(Some(&vertex_array)); // set the vertex positions const VERTEX_CNT: usize = 6; let positions: [f32; 3*VERTEX_CNT] = [ // northwest triangle -1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 0.0, // southeast triangle -1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0 ]; bind_vertex_attrib(&ctx, position_index, 3, &positions); // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { // get the time step let time = performance.now(); let time_step = 0.001*(time - last_time); last_time = time; // get the navigation state let pitch_up_val = pitch_up.get(); let pitch_down_val = pitch_down.get(); let yaw_right_val = yaw_right.get(); let yaw_left_val = yaw_left.get(); let turntable_val = turntable.get(); // update the construction's orientation let ang_vel = { let pitch = pitch_up_val - pitch_down_val; let yaw = yaw_right_val - yaw_left_val; let ang_vel_from_keyboard = if pitch != 0.0 || yaw != 0.0 { NAV_SPEED * Vector3::new(-pitch, yaw, 0.0).normalize() } else { Vector3::zeros() }; let ang_vel_from_turntable = if turntable_val { Vector3::new(0.0, TURNTABLE_SPEED, 0.0) } else { Vector3::zeros() }; ang_vel_from_keyboard + ang_vel_from_turntable }; let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); rotation_sp.copy_from( Rotation3::from_scaled_axis(time_step * ang_vel).matrix() ); orientation = &rotation * &orientation; if scene_changed.get() { /* INSTRUMENTS */ // measure mean frame interval frames_since_last_sample += 1; if frames_since_last_sample >= SAMPLE_PERIOD { mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); last_sample_time = time; frames_since_last_sample = 0; } // find the map from construction space to world space let construction_to_world = &location * &orientation; // update the construction sphere_vec.clear(); color_vec.clear(); match tab_selection.get() { Tab::GenTab => push_gen_construction( &mut sphere_vec, &mut color_vec, &construction_to_world, ctrl_x.get(), ctrl_y.get(), radius_x.get(), radius_y.get() ), Tab::LowCurvTab => push_low_curv_construction( &mut sphere_vec, &mut color_vec, &construction_to_world, off1.get(), off2.get(), off3.get(), curv1.get(), curv2.get(), curv3.get(), ) }; // set the resolution let width = canvas.width() as f32; let height = canvas.height() as f32; ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // pass the construction ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_vec.len() as i32); for n in 0..sphere_vec.len() { let v = &sphere_vec[n]; ctx.uniform3f( sphere_sp_locs[n].as_ref(), v[0] as f32, v[1] as f32, v[2] as f32 ); ctx.uniform2f( sphere_lt_locs[n].as_ref(), v[3] as f32, v[4] as f32 ); ctx.uniform3fv_with_f32_array( color_locs[n].as_ref(), &color_vec[n] ); } // pass the control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); /* DEBUG */ ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); /* DEBUG */ ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); ctx.uniform1i(debug_mode_loc.as_ref(), debug_mode.get() as i32); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); // clear scene change flag scene_changed.set( pitch_up_val != 0.0 || pitch_down_val != 0.0 || yaw_left_val != 0.0 || yaw_right_val != 0.0 || turntable_val ); } else { frames_since_last_sample = 0; mean_frame_interval.set(-1.0); } }); start_animation_loop(); }); view! { div(id="app") { div(class="tab-pane") { label { "General" input( type="radio", name="tab", prop:checked=tab_selection.get() == Tab::GenTab, on:click=move |_| tab_selection.set(Tab::GenTab) ) } label { "Low curvature" input( type="radio", name="tab", prop:checked=tab_selection.get() == Tab::LowCurvTab, on:change=move |_| tab_selection.set(Tab::LowCurvTab) ) } } div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } canvas( ref=display, width=600, height=600, tabindex=0, on:keydown=move |event: KeyboardEvent| { let mut navigating = true; match event.key().as_str() { "ArrowUp" => pitch_up.set(1.0), "ArrowDown" => pitch_down.set(1.0), "ArrowRight" => yaw_right.set(1.0), "ArrowLeft" => yaw_left.set(1.0), _ => navigating = false }; if navigating { scene_changed.set(true); event.prevent_default(); } }, on:keyup=move |event: KeyboardEvent| { let mut navigating = true; match event.key().as_str() { "ArrowUp" => pitch_up.set(0.0), "ArrowDown" => pitch_down.set(0.0), "ArrowRight" => yaw_right.set(0.0), "ArrowLeft" => yaw_left.set(0.0), _ => navigating = false }; if navigating { scene_changed.set(true); event.prevent_default(); } }, on:blur=move |_| { pitch_up.set(0.0); pitch_down.set(0.0); yaw_right.set(0.0); yaw_left.set(0.0); } ) div(ref=gen_controls) { label(class="control") { span { "Sphere 0 depth" } input( type="range", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=ctrl_x ) } label(class="control") { span { "Sphere 1 depth" } input( type="range", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=ctrl_y ) } label(class="control") { span { "Sphere 0 radius" } input( type="range", min=0.5, max=1.5, step=0.001, bind:valueAsNumber=radius_x ) } label(class="control") { span { "Sphere 1 radius" } input( type="range", min=0.5, max=1.5, step=0.001, bind:valueAsNumber=radius_y ) } } div(ref=low_curv_controls) { label(class="control") { span { "Sphere 1 offset" } input( type="range", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off1 ) } label(class="control") { span { "Sphere 2 offset" } input( type="range", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off2 ) } label(class="control") { span { "Sphere 3 offset" } input( type="range", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off3 ) } label(class="control") { span { "Sphere 1 curvature" } input( type="range", min=0.0, max=2.0, step=0.001, bind:valueAsNumber=curv1 ) } label(class="control") { span { "Sphere 2 curvature" } input( type="range", min=0.0, max=2.0, step=0.001, bind:valueAsNumber=curv2 ) } label(class="control") { span { "Sphere 3 curvature" } input( type="range", min=0.0, max=2.0, step=0.001, bind:valueAsNumber=curv3 ) } } label(class="control") { span { "Opacity" } input( type="range", max=1.0, step=0.001, bind:valueAsNumber=opacity ) } label(class="control") { span { "Highlight" } input( type="range", max=1.0, step=0.001, bind:valueAsNumber=highlight ) } label(class="control") { span { "Turntable" } input( type="checkbox", bind:checked=turntable ) } label(class="control") { span { "Layer threshold" } input( type="range", max=5.0, step=1.0, bind:valueAsNumber=layer_threshold ) } label(class="control") { span { "Debug mode" } input( type="checkbox", bind:checked=debug_mode ) } } } }); }