diff --git a/app-proto/inversive-display/Cargo.toml b/app-proto/inversive-display/Cargo.toml index bcf6b76..c0cbf3d 100644 --- a/app-proto/inversive-display/Cargo.toml +++ b/app-proto/inversive-display/Cargo.toml @@ -22,6 +22,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } version = "0.3.69" features = [ 'HtmlCanvasElement', + 'Performance', 'WebGl2RenderingContext', 'WebGlBuffer', 'WebGlProgram', diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 51bc0ff..2fcd86e 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -25,7 +25,7 @@ vecInv sphere(vec3 center, float radius) { // construction. the SPHERE_MAX array size seems to affect frame rate a lot, // even though we should only be using the first few elements of each array -const int SPHERE_MAX = 12; +const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; uniform vec3 color_list[SPHERE_MAX]; @@ -40,7 +40,7 @@ uniform vec2 radius; uniform float opacity; uniform float highlight; uniform int layer_threshold; -uniform bool test_mode; +uniform bool debug_mode; // light and camera const float focal_slope = 0.3; @@ -135,36 +135,63 @@ void main() { vec3 dir = vec3(focal_slope * scr, -1.); // cast rays through the spheres - vec2 depth_pairs [SPHERE_MAX]; - taggedFrag frags [2*SPHERE_MAX]; - int frag_cnt = 0; - for (int i = 0; i < sphere_cnt; ++i) { - vec2 hit_depths = sphere_cast(sphere_list[i], dir); - if (!isnan(hit_depths[0])) { - frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); - ++frag_cnt; - } - if (!isnan(hit_depths[1])) { - frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); - ++frag_cnt; - } - } - - // sort the fragments by depth, using an insertion sort - for (int take = 1; take < frag_cnt; ++take) { - taggedFrag pulled = frags[take]; - for (int put = take; put >= 0; --put) { - if (put < 1 || frags[put-1].pt.z >= pulled.pt.z) { - frags[put] = pulled; - break; - } else { - frags[put] = frags[put-1]; + const int LAYER_MAX = 12; + taggedFrag frags [LAYER_MAX]; + int layer_cnt = 0; + for (int id = 0; id < sphere_cnt; ++id) { + // find out where the ray hits the sphere + vec2 hit_depths = sphere_cast(sphere_list[id], dir); + + // insertion-sort the fragments we hit into the fragment list + for (int side = 0; side < 2; ++side) { + float hit_z = -hit_depths[side]; + if (0. > hit_z) { + for (int layer = layer_cnt; layer >= 0; --layer) { + if (layer < 1 || frags[layer-1].pt.z >= hit_z) { + // we're not as close to the screen as the fragment + // before the empty slot, so insert here + if (layer < LAYER_MAX) { + frags[layer] = sphere_shading( + sphere_list[id], + hit_depths[side] * dir, + color_list[id], + id + ); + } + break; + } else { + // we're closer to the screen than the fragment before + // the empty slot, so move that fragment into the empty + // slot + frags[layer] = frags[layer-1]; + } + } + layer_cnt = min(layer_cnt + 1, LAYER_MAX); } } } + /* DEBUG */ + // in debug mode, show the layer count instead of the shaded image + if (debug_mode) { + // at the bottom of the screen, show the color scale instead of the + // layer count + if (gl_FragCoord.y < 10.) layer_cnt = 2 * int(8. * gl_FragCoord.x / resolution.x); + + // convert number to color + vec3 color; + if (layer_cnt % 2 == 0) { + ivec3 bits = layer_cnt / ivec3(2, 4, 8); + color = mod(vec3(bits), 2.); + } else { + color = vec3(0.5); + } + outColor = vec4(color, 1.); + return; + } + // highlight intersections and cusps - for (int i = frag_cnt-1; i >= 1; --i) { + for (int i = layer_cnt-1; i >= 1; --i) { // intersections taggedFrag frag0 = frags[i]; taggedFrag frag1 = frags[i-1]; @@ -187,7 +214,7 @@ void main() { // composite the sphere fragments vec3 color = vec3(0.); - for (int i = frag_cnt-1; i >= layer_threshold; --i) { + for (int i = layer_cnt-1; i >= layer_threshold; --i) { if (frags[i].pt.z < 0.) { vec4 frag_color = frags[i].color; color = mix(color, frag_color.rgb, frag_color.a); diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index ebbf713..f5b42c5 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -9,9 +9,9 @@ extern crate js_sys; use core::array; -use nalgebra::DVector; +use nalgebra::{DMatrix, DVector}; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; -use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; +use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; mod engine; @@ -96,22 +96,58 @@ fn main() { let radius_y = create_signal(1.0); let opacity = create_signal(0.5); let highlight = create_signal(0.2); - let layer_threshold = create_signal(0.0); - let debug_mode = create_signal(false); + 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 || { + ctrl_x.track(); + ctrl_y.track(); + radius_x.track(); + radius_y.track(); + opacity.track(); + highlight.track(); + turntable.track(); + layer_threshold.track(); + debug_mode.track(); + + scene_changed.set(true); + }); + on_mount(move || { // list construction elements - const SPHERE_MAX: usize = 12; + const SPHERE_MAX: usize = 200; let mut sphere_vec = Vec::>::new(); let color_vec = vec![ [1.00_f32, 0.25_f32, 0.00_f32], [0.00_f32, 0.25_f32, 1.00_f32], - [0.25_f32, 0.00_f32, 1.00_f32] + [0.25_f32, 0.00_f32, 1.00_f32], + [0.25_f32, 1.00_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.00_f32, 0.75_f32, 0.50_f32], ]; + // timing + let mut last_time = 0.0; + + // scene parameters + const TURNTABLE_SPEED: f64 = 0.5; + let mut turntable_angle = 0.0; + + /* INSTRUMENTS */ + let performance = window().unwrap().performance().unwrap(); + // get the display canvas let canvas = display .get::() @@ -207,60 +243,110 @@ fn main() { bind_vertex_attrib(&ctx, position_index, 3, &positions); // set up a repainting routine - let (_, start_updating_display, _) = create_raf(move || { - // update the construction - sphere_vec.clear(); - sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get())); - sphere_vec.push(engine::sphere(-0.5, -0.5, -5.0 + ctrl_y.get(), radius_y.get())); - sphere_vec.push(engine::sphere(-0.5, 0.5, -5.0, 0.75)); + 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; - // 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] - ); + // move the turntable + let turntable_val = turntable.get(); + if turntable_val { + turntable_angle += TURNTABLE_SPEED * time_step; } - // 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); + 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; + } + + // set the orientation and translation + let orientation = { + let ang_cos = turntable_angle.cos(); + let ang_sin = turntable_angle.sin(); + DMatrix::from_column_slice(5, 5, &[ + ang_cos, 0.0, ang_sin, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + -ang_sin, 0.0, ang_cos, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; + let translation = { + 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 + ]) + }; + let construction_to_world = &translation * orientation; + + // update the construction + sphere_vec.clear(); + sphere_vec.push(&construction_to_world * engine::sphere(0.5, 0.5, ctrl_x.get(), radius_x.get())); + sphere_vec.push(&construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y.get(), radius_y.get())); + 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)); + + // 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(turntable_val); + } else { + frames_since_last_sample = 0; + mean_frame_interval.set(-1.0); + } }); - - /* - this wastes CPU time by running an animation loop, which updates the - display even when nothing has changed. there should be a way to make - Sycamore do single-frame updates in response to changes, but i - haven't found it yet - */ - start_updating_display(); + start_animation_loop(); }); view! { div(id="app") { + div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } canvas(ref=display, width="600", height="600") div(class="control") { label(for="ctrl-x") { "Sphere 0 depth" } @@ -326,6 +412,14 @@ fn main() { bind:valueAsNumber=highlight ) } + div(class="control") { + label(for="turntable") { "Turntable" } + input( + type="checkbox", + id="turntable", + bind:checked=turntable + ) + } div(class="control") { label(for="layer-threshold") { "Layer threshold" } input(