diff --git a/app-proto/inversive-display/src/identity.vert b/app-proto/inversive-display/src/identity.vert deleted file mode 100644 index 183a65f..0000000 --- a/app-proto/inversive-display/src/identity.vert +++ /dev/null @@ -1,7 +0,0 @@ -#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 deleted file mode 100644 index a4cbe8d..0000000 --- a/app-proto/inversive-display/src/inversive.frag +++ /dev/null @@ -1,187 +0,0 @@ -#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 186facf..72846fc 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -8,6 +8,7 @@ // extern crate js_sys; +use std::f64::consts::PI as PI; use sycamore::{prelude::*, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlShader}; @@ -70,13 +71,8 @@ fn main() { console_error_panic_hook::set_once(); 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 highlight = create_signal(0.2); - let layer_threshold = create_signal(0.0); + let turn = create_signal(0.0); + let tip = create_signal(0.0); let display = create_node_ref(); on_mount(move || { @@ -95,12 +91,38 @@ fn main() { let vertex_shader = compile_shader( &ctx, WebGl2RenderingContext::VERTEX_SHADER, - include_str!("identity.vert"), + r##"#version 300 es + + in vec3 position; + in vec3 color; + + out vec4 vertexColor; + + uniform mat4 world_to_clip; + uniform mat3 rotation; + + void main() { + vec3 world_pos = rotation * position - vec3(0., 0., 6.); + gl_Position = world_to_clip * vec4(world_pos, 1.); + vertexColor = vec4(color, 1.); + } + "##, ); let fragment_shader = compile_shader( &ctx, WebGl2RenderingContext::FRAGMENT_SHADER, - include_str!("inversive.frag"), + r##"#version 300 es + + precision highp float; + + in vec4 vertexColor; + + out vec4 outColor; + + void main() { + outColor = vertexColor; + } + "##, ); let program = ctx.create_program().unwrap(); ctx.attach_shader(&program, &vertex_shader); @@ -120,48 +142,80 @@ fn main() { // find indices of vertex attributes and uniforms let position_index = ctx.get_attrib_location(&program, "position") as u32; - 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 highlight_loc = ctx.get_uniform_location(&program, "highlight"); - let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); + 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"); // 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)); - // 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 + // 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 ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); + ctx.uniform_matrix4fv_with_f32_array(world_to_clip_loc.as_ref(), false, &world_to_clip); // 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)); + const VERTEX_CNT: usize = 9; - // 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.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); - ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); + // 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); - // draw the scene + // 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); + + // clear the screen and draw the scene + ctx.clear_color(0.0, 0.0, 0.0, 1.0); + ctx.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); }); }); @@ -171,49 +225,15 @@ fn main() { canvas(ref=display, width="600", height="600") input( type="range", - min=-1.0, max=1.0, - step=0.001, - bind:valueAsNumber=ctrl_x - ) - input( - type="range", - min=-1.0, - max=1.0, - 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 + step=0.01, + bind:valueAsNumber=turn ) input( type="range", max=1.0, - step=0.001, - bind:valueAsNumber=opacity - ) - input( - type="range", - max=1.0, - step=0.001, - bind:valueAsNumber=highlight - ) - input( - type="range", - max=3.0, - step=1.0, - bind:valueAsNumber=layer_threshold + step=0.01, + bind:valueAsNumber=tip ) } }