From e3df765f16fe3ac349674b953ac9ef2cada772d9 Mon Sep 17 00:00:00 2001
From: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Date: Sat, 24 Aug 2024 01:38:06 -0700
Subject: [PATCH] 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,