From ab830b194e656522a6d48707de951346a3367cc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 6 Sep 2024 18:57:59 -0700 Subject: [PATCH 1/6] Use circles in triangle as low-curvature construction In the process, add a way to build a sphere by offset and curvature. --- app-proto/inversive-display/src/engine.rs | 15 +++ app-proto/inversive-display/src/main.rs | 135 +++++++++++++++++----- 2 files changed, 121 insertions(+), 29 deletions(-) diff --git a/app-proto/inversive-display/src/engine.rs b/app-proto/inversive-display/src/engine.rs index 7fbcd03..79668bb 100644 --- a/app-proto/inversive-display/src/engine.rs +++ b/app-proto/inversive-display/src/engine.rs @@ -1,5 +1,6 @@ use nalgebra::DVector; +// the sphere with the given center and radius, with inward-pointing normals pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; DVector::from_column_slice(&[ @@ -9,4 +10,18 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect 0.5 / radius, 0.5 * (center_norm_sq / radius - radius) ]) +} + +// the sphere of curvature `curv` whose closest point to the origin has position +// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the +// curvature to zero gives a plane +pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector { + let norm_sp = 1.0 + off * curv; + DVector::from_column_slice(&[ + norm_sp * dir_x, + norm_sp * dir_y, + norm_sp * dir_z, + 0.5 * curv, + off * (1.0 + 0.5 * off * curv) + ]) } \ No newline at end of file diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 1fbef8d..8afe8ef 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -10,7 +10,6 @@ extern crate js_sys; use core::array; use nalgebra::{DMatrix, DVector}; -use std::f64::consts::FRAC_1_SQRT_2; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; @@ -86,30 +85,59 @@ fn bind_vertex_attrib( 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, - curv_x: f64, - curv_y: f64 + off1: f64, + off2: f64, + off3: f64, + curv1: f64, + curv2: f64, + curv3: f64, ) { - sphere_vec.push(construction_to_world * DVector::from_column_slice(&[0.0, -1.0, 0.0, 0.5*curv_x, 0.0])); - sphere_vec.push(construction_to_world * DVector::from_column_slice(&[-FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.5*curv_y, 0.0])); - sphere_vec.push(construction_to_world * engine::sphere(0.5, 0.0, 0.5, FRAC_1_SQRT_2)); - sphere_vec.push(construction_to_world * engine::sphere(-0.5, 0.0, -0.5, FRAC_1_SQRT_2)); + // 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(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([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)] @@ -136,8 +164,12 @@ fn main() { // controls for low-curvature example let low_curv_controls = create_node_ref(); - let curv_x = create_signal(0.0); - let curv_y = create_signal(0.0); + 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); @@ -168,8 +200,12 @@ fn main() { radius_y.track(); // track controls for low-curvature example - curv_x.track(); - curv_y.track(); + curv1.track(); + curv2.track(); + curv3.track(); + off1.track(); + off2.track(); + off3.track(); // track shared controls opacity.track(); @@ -199,17 +235,10 @@ fn main() { } }); - // list construction elements + // create list of construction elements 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, 1.00_f32, 0.00_f32], - [0.75_f32, 0.75_f32, 0.00_f32], - [0.00_f32, 0.75_f32, 0.50_f32], - ]; + let mut color_vec = Vec::<[f32; 3]>::new(); // timing let mut last_time = 0.0; @@ -365,17 +394,21 @@ fn main() { // 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, - curv_x.get(), curv_y.get() + off1.get(), off2.get(), off3.get(), + curv1.get(), curv2.get(), curv3.get(), ) }; @@ -496,25 +529,69 @@ fn main() { } div(ref=low_curv_controls) { div(class="control") { - label(for="curv-x") { "Sphere 0 curvature" } + label(for="off-1") { "Sphere 1 offset" } input( type="range", - id="curv-x", - min=0.0, - max=2.0, + id="off-1", + min=-1.0, + max=1.0, step=0.001, - bind:valueAsNumber=curv_x + bind:valueAsNumber=off1 ) } div(class="control") { - label(for="curv-y") { "Sphere 1 curvature" } + label(for="off-2") { "Sphere 2 offset" } input( type="range", - id="curv-y", + id="off-2", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=off2 + ) + } + div(class="control") { + label(for="off-3") { "Sphere 3 offset" } + input( + type="range", + id="off-3", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=off3 + ) + } + div(class="control") { + label(for="curv-1") { "Sphere 1 curvature" } + input( + type="range", + id="curv-1", min=0.0, max=2.0, step=0.001, - bind:valueAsNumber=curv_y + bind:valueAsNumber=curv1 + ) + } + div(class="control") { + label(for="curv-2") { "Sphere 2 curvature" } + input( + type="range", + id="curv-2", + min=0.0, + max=2.0, + step=0.001, + bind:valueAsNumber=curv2 + ) + } + div(class="control") { + label(for="curv-3") { "Sphere 3 curvature" } + input( + type="range", + id="curv-3", + min=0.0, + max=2.0, + step=0.001, + bind:valueAsNumber=curv3 ) } } From 163361184b4e621bb3f69a87571eaac18092492c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:00:28 -0700 Subject: [PATCH 2/6] Ray-caster: avoid roundoff error in quadratic equation --- .../inversive-display/src/inversive.frag | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 9913b40..c6fed13 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -110,23 +110,37 @@ taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { // --- ray-casting --- +// if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by +// the linear function `b*u + c` +const float DEG_THRESHOLD = 1e-9; + +// the depths, represented as multiples of `dir`, where the line generated by +// `dir` hits the sphere represented by `v`. if both depths are positive, the +// smaller one is returned in the first component. if only one depth is +// positive, it could be returned in either component vec2 sphere_cast(vecInv v, vec3 dir) { float a = -v.lt.s * dot(dir, dir); float b = dot(v.sp, dir); float c = -v.lt.t; - float scale = -b/(2.*a); float adjust = 4.*a*c/(b*b); - if (adjust < 1.) { - float offset = sqrt(1. - adjust); - return vec2( - scale * (1. - offset), - scale * (1. + offset) - ); + // as long as `b` is non-zero, the linear approximation of + // + // a*u^2 + b*u + c + // + // at `u = 0` will reach zero at a finite depth `u_lin`. the root of the + // quadratic adjacent to `u_lin` is stored in `lin_root`. if both roots + // have the same sign, `lin_root` will be the one closer to `u = 0` + float square_rect_ratio = 1. + sqrt(1. - adjust); + float lin_root = -(2.*c)/b / square_rect_ratio; + if (abs(a) > DEG_THRESHOLD * abs(b)) { + return vec2(lin_root, -b/(2.*a) * square_rect_ratio); + } else { + return vec2(lin_root, -1.); + } } else { - // these parameters describe points behind the camera, so the - // corresponding fragments won't be drawn + // the line through `dir` misses the sphere completely return vec2(-1., -1.); } } From b289d2d4c313f180c31ed34babc7aa8b728212d3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:31:48 -0700 Subject: [PATCH 3/6] Distinguish odd layer counts in debug mode The low-curvature construction admits odd layer counts. --- app-proto/inversive-display/src/inversive.frag | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index c6fed13..2e185a5 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -193,15 +193,13 @@ void main() { 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); + if (gl_FragCoord.y < 10.) layer_cnt = int(16. * 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); + ivec3 bits = layer_cnt / ivec3(1, 2, 4); + vec3 color = mod(vec3(bits), 2.); + if (layer_cnt % 16 >= 8) { + color = mix(color, vec3(0.5), 0.5); } outColor = vec4(color, 1.); return; From 0173b63e19e908b6fcdc01a48edd192d9e5b65e6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:43:26 -0700 Subject: [PATCH 4/6] Add picture plane to circles in triangle --- app-proto/inversive-display/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 8afe8ef..c3b4a59 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -123,6 +123,7 @@ fn push_low_curv_construction( // 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)); @@ -132,6 +133,7 @@ fn push_low_curv_construction( // 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]); From 69ab888d5bc643bae89f361be75455da60b30778 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 00:32:29 -0700 Subject: [PATCH 5/6] Simplify control labeling --- app-proto/inversive-display/main.css | 2 +- app-proto/inversive-display/src/main.rs | 75 ++++++++++--------------- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css index 7e8c710..a16d04a 100644 --- a/app-proto/inversive-display/main.css +++ b/app-proto/inversive-display/main.css @@ -51,7 +51,7 @@ input[type="radio"] { background-color: #333; } -label { +.control > span { width: 170px; } diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index c3b4a59..6e6a0e8 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -484,44 +484,40 @@ fn main() { div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } canvas(ref=display, width="600", height="600") div(ref=gen_controls) { - div(class="control") { - label(for="ctrl-x") { "Sphere 0 depth" } + label(class="control") { + span { "Sphere 0 depth" } input( type="range", - id="ctrl-x", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=ctrl_x ) } - div(class="control") { - label(for="ctrl-y") { "Sphere 1 depth" } + label(class="control") { + span { "Sphere 1 depth" } input( type="range", - id="ctrl-y", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=ctrl_y ) } - div(class="control") { - label(for="radius-x") { "Sphere 0 radius" } + label(class="control") { + span { "Sphere 0 radius" } input( type="range", - id="radius-x", min=0.5, max=1.5, step=0.001, bind:valueAsNumber=radius_x ) } - div(class="control") { - label(for="radius-y") { "Sphere 1 radius" } + label(class="control") { + span { "Sphere 1 radius" } input( type="range", - id="radius-y", min=0.5, max=1.5, step=0.001, @@ -530,66 +526,60 @@ fn main() { } } div(ref=low_curv_controls) { - div(class="control") { - label(for="off-1") { "Sphere 1 offset" } + label(class="control") { + span { "Sphere 1 offset" } input( type="range", - id="off-1", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off1 ) } - div(class="control") { - label(for="off-2") { "Sphere 2 offset" } + label(class="control") { + span { "Sphere 2 offset" } input( type="range", - id="off-2", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off2 ) } - div(class="control") { - label(for="off-3") { "Sphere 3 offset" } + label(class="control") { + span { "Sphere 3 offset" } input( type="range", - id="off-3", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off3 ) } - div(class="control") { - label(for="curv-1") { "Sphere 1 curvature" } + label(class="control") { + span { "Sphere 1 curvature" } input( type="range", - id="curv-1", min=0.0, max=2.0, step=0.001, bind:valueAsNumber=curv1 ) } - div(class="control") { - label(for="curv-2") { "Sphere 2 curvature" } + label(class="control") { + span { "Sphere 2 curvature" } input( type="range", - id="curv-2", min=0.0, max=2.0, step=0.001, bind:valueAsNumber=curv2 ) } - div(class="control") { - label(for="curv-3") { "Sphere 3 curvature" } + label(class="control") { + span { "Sphere 3 curvature" } input( type="range", - id="curv-3", min=0.0, max=2.0, step=0.001, @@ -597,49 +587,44 @@ fn main() { ) } } - div(class="control") { - label(for="opacity") { "Opacity" } + label(class="control") { + span { "Opacity" } input( type="range", - id="opacity", max=1.0, step=0.001, bind:valueAsNumber=opacity ) } - div(class="control") { - label(for="highlight") { "Highlight" } + label(class="control") { + span { "Highlight" } input( type="range", - id="highlight", max=1.0, step=0.001, bind:valueAsNumber=highlight ) } - div(class="control") { - label(for="turntable") { "Turntable" } + label(class="control") { + span { "Turntable" } input( type="checkbox", - id="turntable", bind:checked=turntable ) } - div(class="control") { - label(for="layer-threshold") { "Layer threshold" } + label(class="control") { + span { "Layer threshold" } input( type="range", - id="layer-threshold", max=5.0, step=1.0, bind:valueAsNumber=layer_threshold ) } - div(class="control") { - label(for="debug-mode") { "Debug mode" } + label(class="control") { + span { "Debug mode" } input( type="checkbox", - id="debug-mode", bind:checked=debug_mode ) } From 2efc08d6c056f6c130467019a5d7f51338b9c63d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 02:15:04 -0700 Subject: [PATCH 6/6] Enable focus for tabs and display You can now switch tabs from the keyboard using the usual radio button interaction. --- app-proto/inversive-display/main.css | 16 +++++++++++++++- app-proto/inversive-display/src/main.rs | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css index a16d04a..f8f7a96 100644 --- a/app-proto/inversive-display/main.css +++ b/app-proto/inversive-display/main.css @@ -14,10 +14,15 @@ body { canvas { float: left; background-color: #020202; + border: 1px solid #555; border-radius: 10px; margin-top: 5px; } +canvas:focus { + border-color: #aaa; +} + .hidden { display: none; } @@ -29,7 +34,12 @@ canvas { } input[type="radio"] { - display: none; + appearance: none; + width: 0px; + height: 0px; + padding: 0px; + margin: 0px; + outline: none; } .tab-pane > label { @@ -46,6 +56,10 @@ input[type="radio"] { background-color: #555; } +.tab-pane > label:has(:focus-visible) { + outline: medium auto currentColor; +} + .tab-pane > label:hover:not(:has(:checked)) { border-color: #bbb; background-color: #333; diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 6e6a0e8..3dd4653 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -482,7 +482,7 @@ fn main() { } } div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } - canvas(ref=display, width="600", height="600") + canvas(ref=display, width=600, height=600, tabindex=0) div(ref=gen_controls) { label(class="control") { span { "Sphere 0 depth" }