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);