From f274119da64d9d8b7aac1d3784f6249cbbacbc1c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 18:17:01 -0700 Subject: [PATCH 01/10] Enable depth testing To get the right order, flip the sign of the `z` component in the output of the projection map. --- app-proto/inversive-display/src/main.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 72846fc..5afbb04 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -150,6 +150,9 @@ fn main() { let vertex_array = ctx.create_vertex_array().unwrap(); ctx.bind_vertex_array(Some(&vertex_array)); + // enable depth testing + ctx.enable(WebGl2RenderingContext::DEPTH_TEST); + // set the projection map let focal_length = 3.0_f32; let near_clip = 0.1_f32; @@ -158,8 +161,8 @@ fn main() { let world_to_clip: [f32; 16] = [ focal_length, 0.0, 0.0, 0.0, 0.0, focal_length, 0.0, 0.0, - 0.0, 0.0, (near_clip + far_clip) * depth_inv, -1., - 0.0, 0.0, 2. * near_clip * far_clip * depth_inv, 0.0 + 0.0, 0.0, -(near_clip + far_clip) * depth_inv, -1., + 0.0, 0.0, -2. * near_clip * far_clip * depth_inv, 0.0 ]; ctx.uniform_matrix4fv_with_f32_array(world_to_clip_loc.as_ref(), false, &world_to_clip); From c78a041dc7bc1525fb9686ae36192082bfa5412b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 22:08:34 -0700 Subject: [PATCH 02/10] Write a ray-caster for inversive spheres --- app-proto/inversive-display/src/main.rs | 172 +++++++++++++----------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 5afbb04..12fe9ed 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -8,7 +8,6 @@ // extern crate js_sys; -use std::f64::consts::PI as PI; use sycamore::{prelude::*, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlShader}; @@ -71,8 +70,8 @@ fn main() { console_error_panic_hook::set_once(); sycamore::render(|| { - let turn = create_signal(0.0); - let tip = create_signal(0.0); + let ctrl_x = create_signal(0.0); + let ctrl_y = create_signal(0.0); let display = create_node_ref(); on_mount(move || { @@ -93,18 +92,10 @@ fn main() { WebGl2RenderingContext::VERTEX_SHADER, r##"#version 300 es - in vec3 position; - in vec3 color; - - out vec4 vertexColor; - - uniform mat4 world_to_clip; - uniform mat3 rotation; + in vec4 position; void main() { - vec3 world_pos = rotation * position - vec3(0., 0., 6.); - gl_Position = world_to_clip * vec4(world_pos, 1.); - vertexColor = vec4(color, 1.); + gl_Position = position; } "##, ); @@ -115,12 +106,68 @@ fn main() { precision highp float; - in vec4 vertexColor; - out vec4 outColor; + uniform vec2 resolution; + uniform float shortdim; + + uniform vec2 ctrl; + + struct vecInv { + vec3 sp; + vec2 lt; + }; + + vecInv sphere(vec3 center, float radius) { + return vecInv( + center / radius, + vec2( + 0.5 / radius, + 0.5 * (dot(center, center) / radius - radius) + ) + ); + } + + const float focal_slope = 0.3; + const vec3 light_dir = normalize(vec3(2., 2., 1.)); + void main() { - outColor = vertexColor; + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + vecInv v = sphere(vec3(ctrl, -5.), 1.); + + 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); + float offset = sqrt(1. - adjust); + float u_front = scale * (1. - offset); + float u_back = scale * (1. + offset); + + vec3 color; + if (adjust < 1. && u_front > 0.) { + // the expression for normal needs to be checked. it's + // supposed to give the negative gradient of the lorentz + // product between the impact point vector and the sphere + // vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that + // the result looked good enough for now + vec3 pt_front = u_front * dir; + vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt_front); + + float incidence = dot(normal_front, light_dir); + if (incidence < 0.) { + color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); + } else { + color = mix(vec3(0.4, 0.0, 0.2), vec3(1., 0.8, 1.), incidence); + } + } else { + color = vec3(0.); + } + outColor = vec4(color, 1.); } "##, ); @@ -142,79 +189,38 @@ fn main() { // find indices of vertex attributes and uniforms let position_index = ctx.get_attrib_location(&program, "position") as u32; - let color_index = ctx.get_attrib_location(&program, "color") as u32; - let world_to_clip_loc = ctx.get_uniform_location(&program, "world_to_clip"); - let rotation_loc = ctx.get_uniform_location(&program, "rotation"); + 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"); // 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)); - // enable depth testing - ctx.enable(WebGl2RenderingContext::DEPTH_TEST); - - // set the projection map - let focal_length = 3.0_f32; - let near_clip = 0.1_f32; - let far_clip = 20_f32; - let depth_inv = 1_f32 / (far_clip - near_clip); - let world_to_clip: [f32; 16] = [ - focal_length, 0.0, 0.0, 0.0, - 0.0, focal_length, 0.0, 0.0, - 0.0, 0.0, -(near_clip + far_clip) * depth_inv, -1., - 0.0, 0.0, -2. * near_clip * far_clip * depth_inv, 0.0 + // 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 ]; - ctx.uniform_matrix4fv_with_f32_array(world_to_clip_loc.as_ref(), false, &world_to_clip); + bind_vertex_attrib(&ctx, position_index, 3, &positions); + + // 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)); // set up a repainting routine create_effect(move || { - const VERTEX_CNT: usize = 9; - - // set the vertex positions - let tip_shift = 4.0/3.0 * tip.get() as f32; - let positions: [f32; 3*VERTEX_CNT] = [ - // triangle 1 - 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, - 1.0, -1.0, -1.0, - -1.0, 1.0, -1.0, - // triangle 2 - 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, - -1.0, 1.0, -1.0, - -1.0, -1.0, 1.0, - // triangle 3 - 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, - -1.0, -1.0, 1.0, - 1.0, -1.0, -1.0 - ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); - - // set the vertex colors - let colors: [f32; 3*VERTEX_CNT] = [ - // triangle 1 - 1.0, 0.0, 0.5, - 1.0, 0.0, 0.5, - 1.0, 0.0, 0.5, - // triangle 2 - 0.0, 0.5, 1.0, - 0.0, 0.5, 1.0, - 0.0, 0.5, 1.0, - // triangle 3 - 0.5, 0.0, 1.0, - 0.5, 0.0, 1.0, - 0.5, 0.0, 1.0 - ]; - bind_vertex_attrib(&ctx, color_index, 3, &colors); - - // set the rotation - let angle_val = (2.0*PI*turn.get()) as f32; - let angle_cos = angle_val.cos(); - let angle_sin = angle_val.sin(); - let rotation: [f32; 9] = [ - angle_cos, 0.0, angle_sin, - 0.0, 1.0, 0.0, - -angle_sin, 0.0, angle_cos, - ]; - ctx.uniform_matrix3fv_with_f32_array(rotation_loc.as_ref(), false, &rotation); + // pass the control parameters + ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); @@ -228,15 +234,17 @@ fn main() { canvas(ref=display, width="600", height="600") input( type="range", + min=-1.0, max=1.0, step=0.01, - bind:valueAsNumber=turn + bind:valueAsNumber=ctrl_x ) input( type="range", + min=-1.0, max=1.0, step=0.01, - bind:valueAsNumber=tip + bind:valueAsNumber=ctrl_y ) } } From d2cecf69db88145e6529847055fcf64d4fc72ea3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 23 Aug 2024 00:16:41 -0700 Subject: [PATCH 03/10] Ray-cast a translucent sphere --- app-proto/inversive-display/src/main.rs | 81 ++++++++++++++++--------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 12fe9ed..baf7df6 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -72,6 +72,7 @@ fn main() { sycamore::render(|| { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); + let opacity = create_signal(0.6); let display = create_node_ref(); on_mount(move || { @@ -108,10 +109,17 @@ fn main() { out vec4 outColor; + // view uniform vec2 resolution; uniform float shortdim; + // controls uniform vec2 ctrl; + uniform float opacity; + + // light and camera + const float focal_slope = 0.3; + const vec3 light_dir = normalize(vec3(2., 2., 1.)); struct vecInv { vec3 sp; @@ -128,8 +136,24 @@ fn main() { ); } - const float focal_slope = 0.3; - const vec3 light_dir = normalize(vec3(2., 2., 1.)); + vec4 shade_sphere(vecInv v, vec3 pt) { + // the expression for normal needs to be checked. it's + // supposed to give the negative gradient of the lorentz + // product between the impact point vector and the sphere + // vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that + // the result looked good enough for now + vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt); + + vec3 color; + float incidence = dot(normal_front, light_dir); + if (incidence < 0.) { + color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); + } else { + color = mix(vec3(0.4, 0.0, 0.2), vec3(1.0, 0.8, 1.0), incidence); + } + return vec4(color, opacity); + } void main() { vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; @@ -143,29 +167,20 @@ fn main() { float scale = -b/(2.*a); float adjust = 4.*a*c/(b*b); - float offset = sqrt(1. - adjust); - float u_front = scale * (1. - offset); - float u_back = scale * (1. + offset); - vec3 color; - if (adjust < 1. && u_front > 0.) { - // the expression for normal needs to be checked. it's - // supposed to give the negative gradient of the lorentz - // product between the impact point vector and the sphere - // vector with respect to the coordinates of the impact - // point. i calculated it in my head and decided that - // the result looked good enough for now - vec3 pt_front = u_front * dir; - vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt_front); - - float incidence = dot(normal_front, light_dir); - if (incidence < 0.) { - color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); - } else { - color = mix(vec3(0.4, 0.0, 0.2), vec3(1., 0.8, 1.), incidence); + vec3 color = vec3(0.); + if (adjust < 1.) { + float offset = sqrt(1. - adjust); + float u_front = scale * (1. - offset); + float u_back = scale * (1. + offset); + if (u_back > 0.) { + vec4 sphere_color = shade_sphere(v, u_back * dir); + color = mix(color, sphere_color.rgb, sphere_color.a); + } + if (u_front > 0.) { + vec4 sphere_color = shade_sphere(v, u_front * dir); + color = mix(color, sphere_color.rgb, sphere_color.a); } - } else { - color = vec3(0.); } outColor = vec4(color, 1.); } @@ -192,6 +207,7 @@ fn main() { 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"); + let opacity_loc = ctx.get_uniform_location(&program, "opacity"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); @@ -211,16 +227,17 @@ fn main() { ]; bind_vertex_attrib(&ctx, position_index, 3, &positions); - // 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)); - // set up a repainting routine create_effect(move || { + // 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 control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); + ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); @@ -246,6 +263,12 @@ fn main() { step=0.01, bind:valueAsNumber=ctrl_y ) + input( + type="range", + max=1.0, + step=0.01, + bind:valueAsNumber=opacity + ) } } }); From 2ef0fdd3e2a6e077824c302d70fcf7e5f053bd97 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 23 Aug 2024 12:56:54 -0700 Subject: [PATCH 04/10] Ray-cast two spheres, with hard-coded depth sorting --- app-proto/inversive-display/src/main.rs | 115 ++++++++++++++++++------ 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index baf7df6..1937e2b 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -73,6 +73,7 @@ fn main() { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); let opacity = create_signal(0.6); + let layer_threshold = create_signal(0.0); let display = create_node_ref(); on_mount(move || { @@ -116,11 +117,14 @@ fn main() { // controls uniform vec2 ctrl; uniform float opacity; + uniform int layer_threshold; // light and camera const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); + // --- inversive geometry --- + struct vecInv { vec3 sp; vec2 lt; @@ -136,7 +140,26 @@ fn main() { ); } - vec4 shade_sphere(vecInv v, vec3 pt) { + // --- shading --- + + struct taggedFrag { + vec4 color; + float depth; + }; + + taggedFrag[2] sort(taggedFrag a, taggedFrag b) { + taggedFrag[2] result; + if (a.depth < b.depth) { + result[0] = a; + result[1] = b; + } else { + result[0] = b; + result[1] = a; + } + return result; + } + + taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color) { // the expression for normal needs to be checked. it's // supposed to give the negative gradient of the lorentz // product between the impact point vector and the sphere @@ -147,20 +170,13 @@ fn main() { vec3 color; float incidence = dot(normal_front, light_dir); - if (incidence < 0.) { - color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); - } else { - color = mix(vec3(0.4, 0.0, 0.2), vec3(1.0, 0.8, 1.0), incidence); - } - return vec4(color, opacity); + float illum = mix(0.4, 1.0, max(incidence, 0.0)); + return taggedFrag(vec4(illum * base_color, opacity), -pt.z); } - void main() { - vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; - vec3 dir = vec3(focal_slope * scr, -1.); - - vecInv v = sphere(vec3(ctrl, -5.), 1.); - + // --- ray-casting --- + + 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; @@ -168,18 +184,57 @@ fn main() { float scale = -b/(2.*a); float adjust = 4.*a*c/(b*b); - vec3 color = vec3(0.); if (adjust < 1.) { float offset = sqrt(1. - adjust); - float u_front = scale * (1. - offset); - float u_back = scale * (1. + offset); - if (u_back > 0.) { - vec4 sphere_color = shade_sphere(v, u_back * dir); - color = mix(color, sphere_color.rgb, sphere_color.a); - } - if (u_front > 0.) { - vec4 sphere_color = shade_sphere(v, u_front * dir); - color = mix(color, sphere_color.rgb, sphere_color.a); + return vec2( + scale * (1. - offset), + scale * (1. + offset) + ); + } else { + // these parameters describe points behind the camera, + // so the corresponding fragments won't be drawn + return vec2(-1., -1.); + } + } + + void main() { + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + // initialize two spheres + vecInv v0 = sphere(vec3(0.5, 0.5, -5. + ctrl.x), 1.); + vecInv v1 = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), 1.); + vec3 color0 = vec3(1.0, 0.5, 0.0); + vec3 color1 = vec3(0.0, 0.5, 1.0); + + // cast rays through the spheres + vec2 u0 = sphere_cast(v0, dir); + vec2 u1 = sphere_cast(v1, dir); + + // shade and depth-sort the impact points + taggedFrag front_hits[2] = sort( + sphere_shading(v0, u0[0] * dir, color0), + sphere_shading(v1, u1[0] * dir, color1) + ); + taggedFrag back_hits[2] = sort( + sphere_shading(v0, u0[1] * dir, color0), + sphere_shading(v1, u1[1] * dir, color1) + ); + taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); + + // finish depth sorting + taggedFrag frags_by_depth[4]; + frags_by_depth[0] = front_hits[0]; + frags_by_depth[1] = middle_frags[0]; + frags_by_depth[2] = middle_frags[1]; + frags_by_depth[3] = back_hits[1]; + + // composite the sphere fragments + vec3 color = vec3(0.); + for (int i = 3; i >= layer_threshold; --i) { + if (frags_by_depth[i].depth > 0.) { + vec4 frag_color = frags_by_depth[i].color; + color = mix(color, frag_color.rgb, frag_color.a); } } outColor = vec4(color, 1.); @@ -208,6 +263,7 @@ fn main() { let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); let opacity_loc = ctx.get_uniform_location(&program, "opacity"); + let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); @@ -238,6 +294,7 @@ fn main() { // pass the control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); + ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); @@ -253,22 +310,28 @@ fn main() { type="range", min=-1.0, max=1.0, - step=0.01, + step=0.001, bind:valueAsNumber=ctrl_x ) input( type="range", min=-1.0, max=1.0, - step=0.01, + step=0.001, bind:valueAsNumber=ctrl_y ) input( type="range", max=1.0, - step=0.01, + step=0.001, bind:valueAsNumber=opacity ) + input( + type="range", + max=3.0, + step=1.0, + bind:valueAsNumber=layer_threshold + ) } } }); From 87763fc458c5c25aa74c617a1fb2cac5c09a5f40 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 00:08:22 -0700 Subject: [PATCH 05/10] Ray-caster: tidy up sphere shading --- app-proto/inversive-display/src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 1937e2b..d2bc742 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -166,10 +166,9 @@ fn main() { // vector with respect to the coordinates of the impact // point. i calculated it in my head and decided that // the result looked good enough for now - vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt); + vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); - vec3 color; - float incidence = dot(normal_front, light_dir); + float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); return taggedFrag(vec4(illum * base_color, opacity), -pt.z); } From f1029b31028e1483914f0a8c9dda4759e4c51329 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 01:01:13 -0700 Subject: [PATCH 06/10] Ray-caster: map output into sRGB space Change the base color and default opacity to keep the picture looking broadly the same. --- app-proto/inversive-display/src/main.rs | 35 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index d2bc742..c19e8ab 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -72,7 +72,7 @@ fn main() { sycamore::render(|| { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); - let opacity = create_signal(0.6); + let opacity = create_signal(0.5); let layer_threshold = create_signal(0.0); let display = create_node_ref(); @@ -123,6 +123,33 @@ fn main() { const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); + // --- sRGB --- + + // map colors from RGB space to sRGB space, as specified in the + // sRGB standard (IEC 61966-2-1:1999) + // + // https://www.color.org/sRGB.pdf + // https://www.color.org/chardata/rgb/srgb.xalter + // + // in RGB space, color value is proportional to light intensity, + // so linear color-vector interpolation corresponds to physical + // light mixing. in sRGB space, the color encoding used by many + // monitors, we use more of the value interval to represent low + // intensities, and less of the interval to represent high + // intensities. this improves color quantization + + float sRGB(float t) { + if (t <= 0.0031308) { + return 12.92*t; + } else { + return 1.055*pow(t, 5./12.) - 0.055; + } + } + + vec3 sRGB(vec3 color) { + return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); + } + // --- inversive geometry --- struct vecInv { @@ -203,8 +230,8 @@ fn main() { // initialize two spheres vecInv v0 = sphere(vec3(0.5, 0.5, -5. + ctrl.x), 1.); vecInv v1 = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), 1.); - vec3 color0 = vec3(1.0, 0.5, 0.0); - vec3 color1 = vec3(0.0, 0.5, 1.0); + vec3 color0 = vec3(1., 0.214, 0.); + vec3 color1 = vec3(0., 0.214, 1.); // cast rays through the spheres vec2 u0 = sphere_cast(v0, dir); @@ -236,7 +263,7 @@ fn main() { color = mix(color, frag_color.rgb, frag_color.a); } } - outColor = vec4(color, 1.); + outColor = vec4(sRGB(color), 1.); } "##, ); From e3df765f16fe3ac349674b953ac9ef2cada772d9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 01:38:06 -0700 Subject: [PATCH 07/10] Ray-caster: highlight intersections and cusps --- app-proto/inversive-display/src/main.rs | 71 ++++++++++++++++++++----- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index c19e8ab..e3e7135 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -72,6 +72,8 @@ fn main() { sycamore::render(|| { 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); let opacity = create_signal(0.5); let layer_threshold = create_signal(0.0); let display = create_node_ref(); @@ -116,12 +118,14 @@ fn main() { // controls uniform vec2 ctrl; + uniform vec2 radius; uniform float opacity; uniform int layer_threshold; // light and camera const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); + const float ixn_threshold = 0.005; // --- sRGB --- @@ -170,13 +174,15 @@ fn main() { // --- shading --- struct taggedFrag { + int id; vec4 color; - float depth; + vec3 pt; + vec3 normal; }; taggedFrag[2] sort(taggedFrag a, taggedFrag b) { taggedFrag[2] result; - if (a.depth < b.depth) { + if (a.pt.z > b.pt.z) { result[0] = a; result[1] = b; } else { @@ -186,7 +192,7 @@ fn main() { return result; } - taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color) { + taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { // the expression for normal needs to be checked. it's // supposed to give the negative gradient of the lorentz // product between the impact point vector and the sphere @@ -197,7 +203,7 @@ fn main() { float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return taggedFrag(vec4(illum * base_color, opacity), -pt.z); + return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); } // --- ray-casting --- @@ -228,23 +234,24 @@ fn main() { vec3 dir = vec3(focal_slope * scr, -1.); // initialize two spheres - vecInv v0 = sphere(vec3(0.5, 0.5, -5. + ctrl.x), 1.); - vecInv v1 = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), 1.); + vecInv v [2]; + v[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); + v[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); vec3 color0 = vec3(1., 0.214, 0.); vec3 color1 = vec3(0., 0.214, 1.); // cast rays through the spheres - vec2 u0 = sphere_cast(v0, dir); - vec2 u1 = sphere_cast(v1, dir); + vec2 u0 = sphere_cast(v[0], dir); + vec2 u1 = sphere_cast(v[1], dir); // shade and depth-sort the impact points taggedFrag front_hits[2] = sort( - sphere_shading(v0, u0[0] * dir, color0), - sphere_shading(v1, u1[0] * dir, color1) + sphere_shading(v[0], u0[0] * dir, color0, 0), + sphere_shading(v[1], u1[0] * dir, color1, 1) ); taggedFrag back_hits[2] = sort( - sphere_shading(v0, u0[1] * dir, color0), - sphere_shading(v1, u1[1] * dir, color1) + sphere_shading(v[0], u0[1] * dir, color0, 0), + sphere_shading(v[1], u1[1] * dir, color1, 1) ); taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); @@ -255,10 +262,32 @@ fn main() { frags_by_depth[2] = middle_frags[1]; frags_by_depth[3] = back_hits[1]; + // highlight intersections and cusps + for (int i = 3; i >= 1; --i) { + // intersections + taggedFrag frag0 = frags_by_depth[i]; + taggedFrag frag1 = frags_by_depth[i-1]; + float ixn_sin = length(cross(frag0.normal, frag1.normal)); + vec3 disp = frag0.pt - frag1.pt; + float ixn_dist = max( + abs(dot(frag1.normal, disp)), + abs(dot(frag0.normal, disp)) + ) / ixn_sin; + float ixn_highlight = 1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); + frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); + + // cusps + float cusp_cos = abs(dot(dir, frag0.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); + float cusp_highlight = 1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); + } + // composite the sphere fragments vec3 color = vec3(0.); for (int i = 3; i >= layer_threshold; --i) { - if (frags_by_depth[i].depth > 0.) { + if (frags_by_depth[i].pt.z < 0.) { vec4 frag_color = frags_by_depth[i].color; color = mix(color, frag_color.rgb, frag_color.a); } @@ -288,6 +317,7 @@ fn main() { 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"); + let radius_loc = ctx.get_uniform_location(&program, "radius"); let opacity_loc = ctx.get_uniform_location(&program, "opacity"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); @@ -319,6 +349,7 @@ fn main() { // pass the control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); + ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); @@ -346,6 +377,20 @@ fn main() { step=0.001, bind:valueAsNumber=ctrl_y ) + input( + type="range", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_x + ) + input( + type="range", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_y + ) input( type="range", max=1.0, From 25da6ca06264b52cf5050d5b27664e081959f36b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:00:50 -0700 Subject: [PATCH 08/10] Ray-caster: adjust opacity of highlighting --- app-proto/inversive-display/src/main.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index e3e7135..b120f1a 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -75,6 +75,7 @@ fn main() { let radius_x = create_signal(1.0); 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 display = create_node_ref(); @@ -120,6 +121,7 @@ fn main() { uniform vec2 ctrl; uniform vec2 radius; uniform float opacity; + uniform float highlight; uniform int layer_threshold; // light and camera @@ -273,14 +275,14 @@ fn main() { abs(dot(frag1.normal, disp)), abs(dot(frag0.normal, disp)) ) / ixn_sin; - float ixn_highlight = 1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist); + float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); // cusps float cusp_cos = abs(dot(dir, frag0.normal)); float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); - float cusp_highlight = 1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); } @@ -319,6 +321,7 @@ fn main() { let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); let radius_loc = ctx.get_uniform_location(&program, "radius"); 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"); // create a vertex array and bind it to the graphics context @@ -351,6 +354,7 @@ fn main() { ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); 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); // clear the screen and draw the scene @@ -397,6 +401,12 @@ fn main() { step=0.001, bind:valueAsNumber=opacity ) + input( + type="range", + max=1.0, + step=0.001, + bind:valueAsNumber=highlight + ) input( type="range", max=3.0, From 766d56027c566265ed3b26126ddaa623d142184d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:26:53 -0700 Subject: [PATCH 09/10] Ray-caster: move shaders to separate files This properly reflects the modularity of the code, and it simplifies indentation and syntax highlighting. --- app-proto/inversive-display/src/identity.vert | 7 + .../inversive-display/src/inversive.frag | 187 ++++++++++++++++ app-proto/inversive-display/src/main.rs | 200 +----------------- 3 files changed, 196 insertions(+), 198 deletions(-) create mode 100644 app-proto/inversive-display/src/identity.vert create mode 100644 app-proto/inversive-display/src/inversive.frag diff --git a/app-proto/inversive-display/src/identity.vert b/app-proto/inversive-display/src/identity.vert new file mode 100644 index 0000000..183a65f --- /dev/null +++ b/app-proto/inversive-display/src/identity.vert @@ -0,0 +1,7 @@ +#version 300 es + +in vec4 position; + +void main() { + gl_Position = position; +} \ No newline at end of file diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag new file mode 100644 index 0000000..a4cbe8d --- /dev/null +++ b/app-proto/inversive-display/src/inversive.frag @@ -0,0 +1,187 @@ +#version 300 es + +precision highp float; + +out vec4 outColor; + +// view +uniform vec2 resolution; +uniform float shortdim; + +// controls +uniform vec2 ctrl; +uniform vec2 radius; +uniform float opacity; +uniform float highlight; +uniform int layer_threshold; + +// light and camera +const float focal_slope = 0.3; +const vec3 light_dir = normalize(vec3(2., 2., 1.)); +const float ixn_threshold = 0.005; + +// --- sRGB --- + +// map colors from RGB space to sRGB space, as specified in the sRGB standard +// (IEC 61966-2-1:1999) +// +// https://www.color.org/sRGB.pdf +// https://www.color.org/chardata/rgb/srgb.xalter +// +// in RGB space, color value is proportional to light intensity, so linear +// color-vector interpolation corresponds to physical light mixing. in sRGB +// space, the color encoding used by many monitors, we use more of the value +// interval to represent low intensities, and less of the interval to represent +// high intensities. this improves color quantization + +float sRGB(float t) { + if (t <= 0.0031308) { + return 12.92*t; + } else { + return 1.055*pow(t, 5./12.) - 0.055; + } +} + +vec3 sRGB(vec3 color) { + return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); +} + +// --- inversive geometry --- + +struct vecInv { + vec3 sp; + vec2 lt; +}; + +vecInv sphere(vec3 center, float radius) { + return vecInv( + center / radius, + vec2( + 0.5 / radius, + 0.5 * (dot(center, center) / radius - radius) + ) + ); +} + +// --- shading --- + +struct taggedFrag { + int id; + vec4 color; + vec3 pt; + vec3 normal; +}; + +taggedFrag[2] sort(taggedFrag a, taggedFrag b) { + taggedFrag[2] result; + if (a.pt.z > b.pt.z) { + result[0] = a; + result[1] = b; + } else { + result[0] = b; + result[1] = a; + } + return result; +} + +taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { + // the expression for normal needs to be checked. it's supposed to give the + // negative gradient of the lorentz product between the impact point vector + // and the sphere vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that the result looked good + // enough for now + vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); + + float incidence = dot(normal, light_dir); + float illum = mix(0.4, 1.0, max(incidence, 0.0)); + return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); +} + +// --- ray-casting --- + +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) + ); + } else { + // these parameters describe points behind the camera, so the + // corresponding fragments won't be drawn + return vec2(-1., -1.); + } +} + +void main() { + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + // initialize two spheres + vecInv v [2]; + v[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); + v[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); + vec3 color0 = vec3(1., 0.214, 0.); + vec3 color1 = vec3(0., 0.214, 1.); + + // cast rays through the spheres + vec2 u0 = sphere_cast(v[0], dir); + vec2 u1 = sphere_cast(v[1], dir); + + // shade and depth-sort the impact points + taggedFrag front_hits[2] = sort( + sphere_shading(v[0], u0[0] * dir, color0, 0), + sphere_shading(v[1], u1[0] * dir, color1, 1) + ); + taggedFrag back_hits[2] = sort( + sphere_shading(v[0], u0[1] * dir, color0, 0), + sphere_shading(v[1], u1[1] * dir, color1, 1) + ); + taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); + + // finish depth sorting + taggedFrag frags_by_depth[4]; + frags_by_depth[0] = front_hits[0]; + frags_by_depth[1] = middle_frags[0]; + frags_by_depth[2] = middle_frags[1]; + frags_by_depth[3] = back_hits[1]; + + // highlight intersections and cusps + for (int i = 3; i >= 1; --i) { + // intersections + taggedFrag frag0 = frags_by_depth[i]; + taggedFrag frag1 = frags_by_depth[i-1]; + float ixn_sin = length(cross(frag0.normal, frag1.normal)); + vec3 disp = frag0.pt - frag1.pt; + float ixn_dist = max( + abs(dot(frag1.normal, disp)), + abs(dot(frag0.normal, disp)) + ) / ixn_sin; + float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); + frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); + + // cusps + float cusp_cos = abs(dot(dir, frag0.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); + } + + // composite the sphere fragments + vec3 color = vec3(0.); + for (int i = 3; i >= layer_threshold; --i) { + if (frags_by_depth[i].pt.z < 0.) { + vec4 frag_color = frags_by_depth[i].color; + color = mix(color, frag_color.rgb, frag_color.a); + } + } + outColor = vec4(sRGB(color), 1.); +} \ 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 b120f1a..4575e7a 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -95,208 +95,12 @@ fn main() { let vertex_shader = compile_shader( &ctx, WebGl2RenderingContext::VERTEX_SHADER, - r##"#version 300 es - - in vec4 position; - - void main() { - gl_Position = position; - } - "##, + include_str!("identity.vert"), ); let fragment_shader = compile_shader( &ctx, WebGl2RenderingContext::FRAGMENT_SHADER, - r##"#version 300 es - - precision highp float; - - out vec4 outColor; - - // view - uniform vec2 resolution; - uniform float shortdim; - - // controls - uniform vec2 ctrl; - uniform vec2 radius; - uniform float opacity; - uniform float highlight; - uniform int layer_threshold; - - // light and camera - const float focal_slope = 0.3; - const vec3 light_dir = normalize(vec3(2., 2., 1.)); - const float ixn_threshold = 0.005; - - // --- sRGB --- - - // map colors from RGB space to sRGB space, as specified in the - // sRGB standard (IEC 61966-2-1:1999) - // - // https://www.color.org/sRGB.pdf - // https://www.color.org/chardata/rgb/srgb.xalter - // - // in RGB space, color value is proportional to light intensity, - // so linear color-vector interpolation corresponds to physical - // light mixing. in sRGB space, the color encoding used by many - // monitors, we use more of the value interval to represent low - // intensities, and less of the interval to represent high - // intensities. this improves color quantization - - float sRGB(float t) { - if (t <= 0.0031308) { - return 12.92*t; - } else { - return 1.055*pow(t, 5./12.) - 0.055; - } - } - - vec3 sRGB(vec3 color) { - return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); - } - - // --- inversive geometry --- - - struct vecInv { - vec3 sp; - vec2 lt; - }; - - vecInv sphere(vec3 center, float radius) { - return vecInv( - center / radius, - vec2( - 0.5 / radius, - 0.5 * (dot(center, center) / radius - radius) - ) - ); - } - - // --- shading --- - - struct taggedFrag { - int id; - vec4 color; - vec3 pt; - vec3 normal; - }; - - taggedFrag[2] sort(taggedFrag a, taggedFrag b) { - taggedFrag[2] result; - if (a.pt.z > b.pt.z) { - result[0] = a; - result[1] = b; - } else { - result[0] = b; - result[1] = a; - } - return result; - } - - taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { - // the expression for normal needs to be checked. it's - // supposed to give the negative gradient of the lorentz - // product between the impact point vector and the sphere - // vector with respect to the coordinates of the impact - // point. i calculated it in my head and decided that - // the result looked good enough for now - vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); - - float incidence = dot(normal, light_dir); - float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); - } - - // --- ray-casting --- - - 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) - ); - } else { - // these parameters describe points behind the camera, - // so the corresponding fragments won't be drawn - return vec2(-1., -1.); - } - } - - void main() { - vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; - vec3 dir = vec3(focal_slope * scr, -1.); - - // initialize two spheres - vecInv v [2]; - v[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); - v[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); - vec3 color0 = vec3(1., 0.214, 0.); - vec3 color1 = vec3(0., 0.214, 1.); - - // cast rays through the spheres - vec2 u0 = sphere_cast(v[0], dir); - vec2 u1 = sphere_cast(v[1], dir); - - // shade and depth-sort the impact points - taggedFrag front_hits[2] = sort( - sphere_shading(v[0], u0[0] * dir, color0, 0), - sphere_shading(v[1], u1[0] * dir, color1, 1) - ); - taggedFrag back_hits[2] = sort( - sphere_shading(v[0], u0[1] * dir, color0, 0), - sphere_shading(v[1], u1[1] * dir, color1, 1) - ); - taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); - - // finish depth sorting - taggedFrag frags_by_depth[4]; - frags_by_depth[0] = front_hits[0]; - frags_by_depth[1] = middle_frags[0]; - frags_by_depth[2] = middle_frags[1]; - frags_by_depth[3] = back_hits[1]; - - // highlight intersections and cusps - for (int i = 3; i >= 1; --i) { - // intersections - taggedFrag frag0 = frags_by_depth[i]; - taggedFrag frag1 = frags_by_depth[i-1]; - float ixn_sin = length(cross(frag0.normal, frag1.normal)); - vec3 disp = frag0.pt - frag1.pt; - float ixn_dist = max( - abs(dot(frag1.normal, disp)), - abs(dot(frag0.normal, disp)) - ) / ixn_sin; - float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); - frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); - frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); - - // cusps - float cusp_cos = abs(dot(dir, frag0.normal)); - float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); - float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); - frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); - } - - // composite the sphere fragments - vec3 color = vec3(0.); - for (int i = 3; i >= layer_threshold; --i) { - if (frags_by_depth[i].pt.z < 0.) { - vec4 frag_color = frags_by_depth[i].color; - color = mix(color, frag_color.rgb, frag_color.a); - } - } - outColor = vec4(sRGB(color), 1.); - } - "##, + include_str!("inversive.frag"), ); let program = ctx.create_program().unwrap(); ctx.attach_shader(&program, &vertex_shader); From b9587872d30249dffa2c073857ce1663dd8ef979 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:31:22 -0700 Subject: [PATCH 10/10] Ray-caster: don't bother clearing the screen The ray-caster triangles cover the whole viewport, so they'll completely overdraw the previous frame. --- app-proto/inversive-display/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 4575e7a..186facf 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -161,9 +161,7 @@ fn main() { ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); - // clear the screen and draw the scene - ctx.clear_color(0.0, 0.0, 0.0, 1.0); - ctx.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); }); });