Compare commits

...

6 Commits

Author SHA1 Message Date
Aaron Fenyes
2efc08d6c0 Enable focus for tabs and display
You can now switch tabs from the keyboard using the usual radio button
interaction.
2024-09-09 02:15:04 -07:00
Aaron Fenyes
69ab888d5b Simplify control labeling 2024-09-09 00:32:29 -07:00
Aaron Fenyes
0173b63e19 Add picture plane to circles in triangle 2024-09-08 23:43:26 -07:00
Aaron Fenyes
b289d2d4c3 Distinguish odd layer counts in debug mode
The low-curvature construction admits odd layer counts.
2024-09-08 23:31:48 -07:00
Aaron Fenyes
163361184b Ray-caster: avoid roundoff error in quadratic equation 2024-09-08 23:00:28 -07:00
Aaron Fenyes
ab830b194e Use circles in triangle as low-curvature construction
In the process, add a way to build a sphere by offset and curvature.
2024-09-06 19:01:18 -07:00
4 changed files with 182 additions and 77 deletions

View File

@ -14,10 +14,15 @@ body {
canvas { canvas {
float: left; float: left;
background-color: #020202; background-color: #020202;
border: 1px solid #555;
border-radius: 10px; border-radius: 10px;
margin-top: 5px; margin-top: 5px;
} }
canvas:focus {
border-color: #aaa;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -29,7 +34,12 @@ canvas {
} }
input[type="radio"] { input[type="radio"] {
display: none; appearance: none;
width: 0px;
height: 0px;
padding: 0px;
margin: 0px;
outline: none;
} }
.tab-pane > label { .tab-pane > label {
@ -46,12 +56,16 @@ input[type="radio"] {
background-color: #555; background-color: #555;
} }
.tab-pane > label:has(:focus-visible) {
outline: medium auto currentColor;
}
.tab-pane > label:hover:not(:has(:checked)) { .tab-pane > label:hover:not(:has(:checked)) {
border-color: #bbb; border-color: #bbb;
background-color: #333; background-color: #333;
} }
label { .control > span {
width: 170px; width: 170px;
} }

View File

@ -1,5 +1,6 @@
use nalgebra::DVector; 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<f64> { pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector<f64> {
let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z;
DVector::from_column_slice(&[ 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 / radius,
0.5 * (center_norm_sq / radius - 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<f64> {
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)
])
} }

View File

@ -110,23 +110,37 @@ taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) {
// --- ray-casting --- // --- 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) { vec2 sphere_cast(vecInv v, vec3 dir) {
float a = -v.lt.s * dot(dir, dir); float a = -v.lt.s * dot(dir, dir);
float b = dot(v.sp, dir); float b = dot(v.sp, dir);
float c = -v.lt.t; float c = -v.lt.t;
float scale = -b/(2.*a);
float adjust = 4.*a*c/(b*b); float adjust = 4.*a*c/(b*b);
if (adjust < 1.) { if (adjust < 1.) {
float offset = sqrt(1. - adjust); // as long as `b` is non-zero, the linear approximation of
return vec2( //
scale * (1. - offset), // a*u^2 + b*u + c
scale * (1. + offset) //
); // 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 { } else {
// these parameters describe points behind the camera, so the // the line through `dir` misses the sphere completely
// corresponding fragments won't be drawn
return vec2(-1., -1.); return vec2(-1., -1.);
} }
} }
@ -179,15 +193,13 @@ void main() {
if (debug_mode) { if (debug_mode) {
// at the bottom of the screen, show the color scale instead of the // at the bottom of the screen, show the color scale instead of the
// layer count // 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 // convert number to color
vec3 color; ivec3 bits = layer_cnt / ivec3(1, 2, 4);
if (layer_cnt % 2 == 0) { vec3 color = mod(vec3(bits), 2.);
ivec3 bits = layer_cnt / ivec3(2, 4, 8); if (layer_cnt % 16 >= 8) {
color = mod(vec3(bits), 2.); color = mix(color, vec3(0.5), 0.5);
} else {
color = vec3(0.5);
} }
outColor = vec4(color, 1.); outColor = vec4(color, 1.);
return; return;

View File

@ -10,7 +10,6 @@
extern crate js_sys; extern crate js_sys;
use core::array; use core::array;
use nalgebra::{DMatrix, DVector}; use nalgebra::{DMatrix, DVector};
use std::f64::consts::FRAC_1_SQRT_2;
use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}};
use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation};
@ -86,30 +85,61 @@ fn bind_vertex_attrib(
fn push_gen_construction( fn push_gen_construction(
sphere_vec: &mut Vec<DVector<f64>>, sphere_vec: &mut Vec<DVector<f64>>,
color_vec: &mut Vec<[f32; 3]>,
construction_to_world: &DMatrix<f64>, construction_to_world: &DMatrix<f64>,
ctrl_x: f64, ctrl_x: f64,
ctrl_y: f64, ctrl_y: f64,
radius_x: f64, radius_x: f64,
radius_y: 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_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, 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.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.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));
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( fn push_low_curv_construction(
sphere_vec: &mut Vec<DVector<f64>>, sphere_vec: &mut Vec<DVector<f64>>,
color_vec: &mut Vec<[f32; 3]>,
construction_to_world: &DMatrix<f64>, construction_to_world: &DMatrix<f64>,
curv_x: f64, off1: f64,
curv_y: 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])); // push spheres
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])); let a = 0.75_f64.sqrt();
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.0, 0.0, 0.0, 1.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_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)] #[derive(Clone, Copy, PartialEq)]
@ -136,8 +166,12 @@ fn main() {
// controls for low-curvature example // controls for low-curvature example
let low_curv_controls = create_node_ref(); let low_curv_controls = create_node_ref();
let curv_x = create_signal(0.0); let curv1 = create_signal(0.0);
let curv_y = 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 // shared controls
let opacity = create_signal(0.5); let opacity = create_signal(0.5);
@ -168,8 +202,12 @@ fn main() {
radius_y.track(); radius_y.track();
// track controls for low-curvature example // track controls for low-curvature example
curv_x.track(); curv1.track();
curv_y.track(); curv2.track();
curv3.track();
off1.track();
off2.track();
off3.track();
// track shared controls // track shared controls
opacity.track(); opacity.track();
@ -199,17 +237,10 @@ fn main() {
} }
}); });
// list construction elements // create list of construction elements
const SPHERE_MAX: usize = 200; const SPHERE_MAX: usize = 200;
let mut sphere_vec = Vec::<DVector<f64>>::new(); let mut sphere_vec = Vec::<DVector<f64>>::new();
let color_vec = vec![ let mut color_vec = Vec::<[f32; 3]>::new();
[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],
];
// timing // timing
let mut last_time = 0.0; let mut last_time = 0.0;
@ -365,17 +396,21 @@ fn main() {
// update the construction // update the construction
sphere_vec.clear(); sphere_vec.clear();
color_vec.clear();
match tab_selection.get() { match tab_selection.get() {
Tab::GenTab => push_gen_construction( Tab::GenTab => push_gen_construction(
&mut sphere_vec, &mut sphere_vec,
&mut color_vec,
&construction_to_world, &construction_to_world,
ctrl_x.get(), ctrl_y.get(), ctrl_x.get(), ctrl_y.get(),
radius_x.get(), radius_y.get() radius_x.get(), radius_y.get()
), ),
Tab::LowCurvTab => push_low_curv_construction( Tab::LowCurvTab => push_low_curv_construction(
&mut sphere_vec, &mut sphere_vec,
&mut color_vec,
&construction_to_world, &construction_to_world,
curv_x.get(), curv_y.get() off1.get(), off2.get(), off3.get(),
curv1.get(), curv2.get(), curv3.get(),
) )
}; };
@ -447,46 +482,42 @@ fn main() {
} }
} }
div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } 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) { div(ref=gen_controls) {
div(class="control") { label(class="control") {
label(for="ctrl-x") { "Sphere 0 depth" } span { "Sphere 0 depth" }
input( input(
type="range", type="range",
id="ctrl-x",
min=-1.0, min=-1.0,
max=1.0, max=1.0,
step=0.001, step=0.001,
bind:valueAsNumber=ctrl_x bind:valueAsNumber=ctrl_x
) )
} }
div(class="control") { label(class="control") {
label(for="ctrl-y") { "Sphere 1 depth" } span { "Sphere 1 depth" }
input( input(
type="range", type="range",
id="ctrl-y",
min=-1.0, min=-1.0,
max=1.0, max=1.0,
step=0.001, step=0.001,
bind:valueAsNumber=ctrl_y bind:valueAsNumber=ctrl_y
) )
} }
div(class="control") { label(class="control") {
label(for="radius-x") { "Sphere 0 radius" } span { "Sphere 0 radius" }
input( input(
type="range", type="range",
id="radius-x",
min=0.5, min=0.5,
max=1.5, max=1.5,
step=0.001, step=0.001,
bind:valueAsNumber=radius_x bind:valueAsNumber=radius_x
) )
} }
div(class="control") { label(class="control") {
label(for="radius-y") { "Sphere 1 radius" } span { "Sphere 1 radius" }
input( input(
type="range", type="range",
id="radius-y",
min=0.5, min=0.5,
max=1.5, max=1.5,
step=0.001, step=0.001,
@ -495,72 +526,105 @@ fn main() {
} }
} }
div(ref=low_curv_controls) { div(ref=low_curv_controls) {
div(class="control") { label(class="control") {
label(for="curv-x") { "Sphere 0 curvature" } span { "Sphere 1 offset" }
input( input(
type="range", type="range",
id="curv-x", min=-1.0,
min=0.0, max=1.0,
max=2.0,
step=0.001, step=0.001,
bind:valueAsNumber=curv_x bind:valueAsNumber=off1
) )
} }
div(class="control") { label(class="control") {
label(for="curv-y") { "Sphere 1 curvature" } 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( input(
type="range", type="range",
id="curv-y",
min=0.0, min=0.0,
max=2.0, max=2.0,
step=0.001, step=0.001,
bind:valueAsNumber=curv_y 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
) )
} }
} }
div(class="control") { label(class="control") {
label(for="opacity") { "Opacity" } span { "Opacity" }
input( input(
type="range", type="range",
id="opacity",
max=1.0, max=1.0,
step=0.001, step=0.001,
bind:valueAsNumber=opacity bind:valueAsNumber=opacity
) )
} }
div(class="control") { label(class="control") {
label(for="highlight") { "Highlight" } span { "Highlight" }
input( input(
type="range", type="range",
id="highlight",
max=1.0, max=1.0,
step=0.001, step=0.001,
bind:valueAsNumber=highlight bind:valueAsNumber=highlight
) )
} }
div(class="control") { label(class="control") {
label(for="turntable") { "Turntable" } span { "Turntable" }
input( input(
type="checkbox", type="checkbox",
id="turntable",
bind:checked=turntable bind:checked=turntable
) )
} }
div(class="control") { label(class="control") {
label(for="layer-threshold") { "Layer threshold" } span { "Layer threshold" }
input( input(
type="range", type="range",
id="layer-threshold",
max=5.0, max=5.0,
step=1.0, step=1.0,
bind:valueAsNumber=layer_threshold bind:valueAsNumber=layer_threshold
) )
} }
div(class="control") { label(class="control") {
label(for="debug-mode") { "Debug mode" } span { "Debug mode" }
input( input(
type="checkbox", type="checkbox",
id="debug-mode",
bind:checked=debug_mode bind:checked=debug_mode
) )
} }