commit 528b97bccbcfd4f853450c1fae8d212fb8a6990d Author: James Martin Date: Wed May 19 21:06:49 2021 -0700 My first ray marching implementation, which I've been working on for a few days. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b483f85 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Shadertoy Shaders +These are version-controlled copies of [my Shadertoy shaders](https://www.shadertoy.com/user/jamestmartin). + +[Shadertoy](https://www.shadertoy.com/) is a website for writing GLSL fragment shaders, +which are functions that take a pixel location as input and return a color as output. +Through a lot of math, this is all you need to display all sorts of cool stuff to the screen using the GPU. + +Only publically-shown shaders will show up on my profile, +but seeing as I haven't published any finished products yet, +all of my shaders are unlisted and will not show up. +I will, however, contain a link to each shader on Shadertoy at the top of the file, +so it will still be possible to view each shader through this repository. diff --git a/ray_march 2021-05-14.frag b/ray_march 2021-05-14.frag new file mode 100644 index 0000000..acdf4b1 --- /dev/null +++ b/ray_march 2021-05-14.frag @@ -0,0 +1,432 @@ +// https://www.shadertoy.com/view/NsjSDK + +/// Settings + +// Use a fixed-point calculations while ray marching +// to prevent cumulative floating-point addition error. +// I originally implemented this to fix an artifact on the shadow side of the cube, +// and it worked, but I appear to have accidentally fixed the issue +// with the floating-point version too since then. +// Neither one appears to be more performant at this point, +// but I'd lean towards using the fixed point version to avoid any more +// of the aforementioned artifacts. +#define FIXED_POINT_MARCH +// 4x supersampling +#define SUPERSAMPLING +// All perspective introduces some form of distortion of the image. +// Non-curvilinear perspective preserves x/y parallel lines, +// whereas curvilinear perspective preserves the distances to objects. +// I prefer curvilinear perspective when the camera is zoomed in +// to minimize distortion. +// If you set the FOV to very high (up to 180 degrees), the scene will be +// cut off in a circle, just as a byproduct of how curvilinear perspective works. +// (It projects the surface of half the surface of a sphere around the camera onto a circle, +// whereas linear perspective projects onto a plane in front of the camera.) +#define CURVILINEAR +// Very large view distance, making the background of the scene much nicer, +// and preventing the shadows from getting cut off. +// However, rays sailing off into infinity is very slow +// (because the ground plane is infinite, the ray march distance interval +// doesn't increase very quickly or even shrinks, meaning each ray +// that's not substantially above the horizon takes nearly MAX_STEPS), +// so this option is not recommended in combination with supersampling +// unless you're rendering a still image. +//#define LONG_VIEW +// The width of the field of view. (180 degrees / FOV) +// Larger numbers suffer from less distortion at the edges of the image, +// but make the scene appear more zoomed-in. +#define FOV 3.2 +// The distance of the camera from the scene. +// The ball sails around in a circle with a distance of 5 and radius 4, +// so increasing this distance will result in the ball passing through the camera. +#define CAMERA_DISTANCE 3.99 + + +// The minimum distance resolution used by the fixed-point ray marching algorithm +// is 1/ this number. You can get away with as low as 1024 without much noticable difference. +// You'd think a very high resolution would mean that the algorithm runs slower, +// but in practice the distance to the object halves each iteration, so +// the number of iterations is more-or-less logarithmic with regards to this number. +// The only place it's slower is when a ray passes next to the edges of an object, +// but that's what the resolution is *for*: preventing graphical artifacts at the edges of objects. +#define FIXED_POINT_RESOLUTION 4096 +#define FIXED_POINT_RESOLUTION_F 4096. +// The minimum distance from a surface that counts as a collision by the ray marching algorithm. +#ifdef FIXED_POINT_MARCH +#define MIN_DISTANCE (1./FIXED_POINT_RESOLUTION_F) +#else +// Any value much lower than this breaks the algorithm due to rounding error. +#define MIN_DISTANCE 0.001 +#endif + +#ifdef LONG_VIEW +// The greatest view distance that I can tell makes any difference at 800x450. +// My GPU runs this at 60 FPS with supersampling disabled at 800x450. +#define MAX_STEPS 3000 +// ^ the maximum number of iterations taken by ray marching +#define MAX_DISTANCE 400. +#define FIXED_POINT_MAX_DISTANCE (400 * FIXED_POINT_RESOLUTION) +#else +// The shortest view distance which I don't think cuts off *too* much of the stage. +// The ideal would be to get my GPU to run this at 60 FPS at 1920x1080 with supersampling, +// but so far I haven't been able to optimize it to that point, +// and I'm not sure I even *could*, with my GPU and as slow of an algorithm as ray marching. +#define MAX_STEPS 200 +#define MAX_DISTANCE 20. +#define FIXED_POINT_MAX_DISTANCE (20 * FIXED_POINT_RESOLUTION) +#endif + +#define PI 3.14159 + +// problems to figure out: raymarching performance, numerical precision, math + +// sRGB to linear RGB and vice versa +vec3 from_srgb(vec3 srgb) { + // approximation + return vec3(pow(srgb.x, 1./2.2), pow(srgb.y, 1./2.2), pow(srgb.z, 1./2.2)); +} + +vec3 from_linear(vec3 linear_rgb) +{ + // copied from somewhere on the internet + bvec3 cutoff = lessThan(linear_rgb, vec3(0.0031308)); + vec3 higher = vec3(1.055)*pow(linear_rgb, vec3(1.0/2.4)) - vec3(0.055); + vec3 lower = linear_rgb * vec3(12.92); + + return mix(higher, lower, cutoff); +} + +vec3 to_polar(vec3 cart) { + float r = length(cart); + return vec3(r, acos(cart.y / r), atan(cart.z, cart.x)); +} + +vec3 from_polar(vec3 pol) { + float xz = sin(pol.y); + return pol.x * vec3(cos(pol.z) * xz, cos(pol.y), sin(pol.z) * xz); +} + +vec3 realign(vec3 axis, vec3 vec) { + vec3 axis_p = vec3(0., to_polar(axis).yz); + vec3 vec_p = to_polar(vec); + return from_polar(vec_p - axis_p); +} + +// Signed distance functions of various shapes. +// These are fundamental to how ray marching works, +// but I will not be explaining how they (or ray marching) works any further. +float sdf_segment(vec3 s1, vec3 s2, vec3 p) { + // Translate relative to s1. + vec3 s1s2 = s2 - s1; + vec3 s1p = p - s1; + vec3 s_direction = normalize(s1s2); + // Distance along s nearest to p. + float h = dot(s_direction, s1p); + // Point along s nearest to p. + vec3 s1x = s_direction * clamp(h, 0., length(s1s2)); + // Distance from p to x. + return distance(s1p, s1x); +} + +float sdf_plane(mat4 trans, vec3 pos) { + vec4 p1w = trans * vec4(0., 0., 0., 1.); + vec4 p2w = trans * vec4(1., 0., 0., 1.); + vec4 p3w = trans * vec4(0., 0., 1., 1.); + vec3 p1 = p1w.xyz / p1w.w; + vec3 p2 = p2w.xyz / p2w.w; + vec3 p3 = p3w.xyz / p3w.w; + vec3 n = normalize(cross(p2 - p1, p3 - p1)); + vec3 rp = pos - p1; + return dot(rp, n); +} + +float sdf_pill(vec3 s1, vec3 s2, float radius, vec3 p) { + return sdf_segment(s1, s2, p) - radius; +} + +float sdf_sphere(vec3 sphere_pos, float sphere_radius, vec3 pos) { + return distance(pos, sphere_pos) - sphere_radius; +} + +float sdf_cube( + mat4 trans, + vec3 pos +) { + // positive faces of cube + mat4 f1 = trans * mat4( + 0., 1., 0., 0., + 1., 0., 0., 0., + 0., 0., 1., 0., + 0.5, 0., 0., 1. + ); + // WTF: why does this y have to be negative? + mat4 f2 = trans * mat4( + 1., 0., 0., 0., + 0., 1., 0., 0., + 0., 0., 1., 0., + 0., -0.5, 0., 1. + ); + mat4 f3 = trans * mat4( + 1., 0., 0., 0., + 0., 0., 1., 0., + 0., 1., 0., 0., + 0., 0., 0.5, 1. + ); + + // signed distances from positive cube faces + float df1 = sdf_plane(f1, pos); + float df2 = sdf_plane(f2, pos); + float df3 = sdf_plane(f3, pos); + + // distances between parallel cube faces + float lpf1 = length(trans * vec4(1., 0., 0., 0.)); + float lpf2 = length(trans * vec4(0., 1., 0., 0.)); + float lpf3 = length(trans * vec4(0., 0., 1., 0.)); + + // signed distances from both parallel cube faces + // * if positive, positive distance from plane + // * if negative: + // * remeasure difference from opposite face (which is +1 away) + // * if distance from opposite face is positive, then we are in the cube + // * otherwise we are outside the cube + + float dpf1 = max(df1, -(df1 + lpf1)); + float dpf2 = max(df2, -(df2 + lpf2)); + float dpf3 = max(df3, -(df3 + lpf3)); + + vec3 dpfs = vec3(dpf1, dpf2, dpf3); + + // if the point is outside the cube, then a point within two faces adds 0 distance; + // if the point is inside the cube, then a point within the faces does add distance. + if (all(lessThan(dpfs, vec3(0.)))) { + // the point is inside the cube. + return -length(dpfs); + } + + return length(max(dpfs, 0.)); +} + +float df_cube_minus_sphere(mat4 trans, float sphere_radius, vec3 pos) { + // TODO: SDF for this object + float sdcube = sdf_cube(trans, pos); + if (sdcube >= MIN_DISTANCE) { + return sdcube; + } + + // we're inside the cube + vec3 sphere_pos = (trans * vec4(0., 0., 0., 1.)).xyz; + float sdsphere = sdf_sphere(sphere_pos, sphere_radius, pos); + if (sdsphere >= MIN_DISTANCE) { + // inside the cube but outside the sphere + return 0.; + } + + // we're inside the sphere subtracted from the cube, + // so our distance is the distance to the sphere. + + return abs(sdsphere); +} + +float distance_to_objects(out int material, vec3 pos) { + pos.z -= CAMERA_DISTANCE; + //pos = realign(vec3(1., 0., 0.), pos); + //pos = from_polar(to_polar(pos)); + + vec3 sphere_pos = 5.0 * vec3(sin(iTime), 0.0, cos(iTime)); + float sphere_radius = 1.0; + float sphere_dist = abs(sdf_sphere(sphere_pos, 1.0, pos)); + + mat4 ground_trans = mat4( + 1., 0., 0., 0., + 0., 1., 0., 0., + 0., 0., 1., 0., + 0., -1., 0., 1. + ); + float ground_dist = abs(sdf_plane(ground_trans, pos)); + + vec3 pill_pos1 = vec3(-1.0, -0.5, 2.7); + vec3 pill_pos2 = vec3(0.4, -0.9, 2.5); + float pill_radius = 0.1; + float pill_dist = abs(sdf_pill(pill_pos1, pill_pos2, pill_radius, pos)); + + mat4 cube_trans = mat4( + 0.6, 0., 0., 0., + 0., 0.6, 0., 0., + 0., 0., 0.6, 0., + 0.2 - 1.3 * sin(iTime/2.), 0.3 - 0.3 * cos(iTime/3.), 2.5 + 0.2 * sin((iTime+0.5)*2.), 1. + ); + float cube_angle = iTime; + mat4 cube_rotate = mat4( + cos(cube_angle), -sin(cube_angle), 0., 0., + sin(cube_angle), cos(cube_angle), 0., 0., + 0., 0., 1., 0., + 0., 0., 0., 1. + ); + //float cube_dist = abs(sdf_cube(cube_trans, pos)); + float cube_dist = df_cube_minus_sphere(cube_trans * cube_rotate, 0.39, pos); + + // HACK: What function generalizes this pattern? + // Obviously min for the distances, but what about the materials? + if (sphere_dist < ground_dist && sphere_dist < pill_dist && sphere_dist < cube_dist) { + material = 0; + return sphere_dist; + } + if (ground_dist < pill_dist && ground_dist < cube_dist) { + material = 1; + return ground_dist; + } + if (pill_dist < cube_dist) { + material = 2; + return pill_dist; + } + material = 3; + return cube_dist; +} + +vec3 material_color(int mat) { + if (mat == 0) { + return from_srgb(vec3(0.0, 0.19, 0.56)); + } + if (mat == 1) { + return vec3(1.0, 1.0, 1.0); + } + if (mat == 2) { + return from_srgb(vec3(0.50, 1.00, 0.40)); + } + if (mat == 3) { + return from_srgb(vec3(0.83, 0.69, 0.22)); + } + return vec3(1.0, 0.0, 1.0); +} + +#ifndef FIXED_POINT_MARCH +float ray_march(out int mat, vec3 ray_origin, vec3 ray_direction) { + vec3 position_along_ray = ray_origin; + float dist = 1./0.; + mat = -1; + for (int i = 0; i < MAX_STEPS && distance(ray_origin, position_along_ray) < MAX_DISTANCE && dist > MIN_DISTANCE; i++) { + dist = distance_to_objects(mat, position_along_ray); + position_along_ray += ray_direction * dist; + } + if (distance(ray_origin, position_along_ray) > MAX_DISTANCE) { + return 1.0/0.0; + } + return distance(ray_origin, position_along_ray); +} +#else +float ray_march(out int mat, vec3 ray_origin, vec3 ray_direction) { + int travel_dist = 0; + int delta = 1; + for (int i = 0; i < MAX_STEPS && delta > 0; i++) { + float dist = distance_to_objects(mat, ray_origin + ray_direction * float(travel_dist) / FIXED_POINT_RESOLUTION_F); + delta = min(int(floor(dist * FIXED_POINT_RESOLUTION_F)), FIXED_POINT_MAX_DISTANCE - travel_dist); + travel_dist += delta; + } + if (travel_dist >= FIXED_POINT_MAX_DISTANCE) { + return 1./0.; + } + return float(travel_dist) / FIXED_POINT_RESOLUTION_F; +} +#endif + +vec3 normal(vec3 pos) { + // Magic number determined by tinkering with it until stuff worked. + vec2 delta = vec2(0.00025, 0.); + int _mat = -1; + float dist = distance_to_objects(_mat, pos); + vec3 dq = (dist - vec3( + distance_to_objects(_mat, pos - delta.xyy), + distance_to_objects(_mat, pos - delta.yxy), + distance_to_objects(_mat, pos - delta.yyx) + )) / delta.x; + + return normalize(dq); +} + +float light(vec3 pos) { + vec3 source = vec3(0.5, 1.5, -1.0); + float intensity = dot(normalize(source - pos), normal(pos)); + vec3 direction = normalize(source - pos); + int _mat = -1; + // 10 is a magic number determined by experimentation. + // Anything less generates noticable artifacts due to rounding error (depending on on MIN_DISTANCE). + if (ray_march(_mat, pos + direction * (10. * MIN_DISTANCE), direction) < distance(source, pos)) { + intensity *= 0.1; + } + if (intensity < 0.005 && length(pos) < 1./0.) { + return 0.005; + } + return clamp(intensity, 0., 1.); +} + +vec3 project_spherical(vec2 pos) { + return vec3(pos, sqrt(1.0 - pow(pos.x, 2.0) - pow(pos.y, 2.0))); +} + +vec3 project_planar(vec2 pos) { + return normalize(vec3(pos.x, pos.y, 1.0)); +} + +// Cast a ray in a direction and figure out the color of the material. +vec3 color_ray(vec3 dir) { + int mat = -1; + vec3 pos = vec3(0.); + vec3 color = vec3(0.); + //for (int i = 1; i < 3; i++) { + int i = 1; + float dist = ray_march(mat, vec3(0.0), dir); + pos += dir * dist; + color += material_color(mat) * light(pos) / float(i); + // dir = reflect(dir, normal(pos)); + // pos += 10. * MIN_DISTANCE * dir; + //} + return color; +} + +vec3 color_frame(vec2 loc) { + #ifdef CURVILINEAR + vec3 dir = project_spherical(loc); + #else + vec3 dir = project_planar(loc); + #endif + return color_ray(dir); +} + +// 4x the samples means 4x better-looking edges and 4x slower. +vec3 supersample(vec2 loc, float increment) { + increment /= 2. * FOV; + vec2 d = vec2(increment, -increment); + // As far as I can tell, antialiasing algorithms usually put the samples at strange locations, + // but I neither know why nor what the actual offsets are. + return (color_frame(loc + d.xx) + color_frame(loc + d.xy) + color_frame(loc + d.yx) + color_frame(loc + d.yy)) / 4.; +} + +void mainImage(out vec4 frag_color, in vec2 frag_coord) { + // Scale from pixel coordinates to scene coordinates. + vec2 uv = (frag_coord / iResolution.xy * 2.0) - 1.0; + vec2 loc; + // Cut off the part of the scene which doesn't fit due to the aspect ratio. + if (iResolution.x > iResolution.y) { + loc = vec2(uv.x, uv.y * iResolution.y / iResolution.x); + } else { + loc = vec2(uv.x * iResolution.x / iResolution.y, uv.y); + } + + vec2 xy = loc / FOV; + + #ifdef SUPERSAMPLING + // FIXME: 1/iResolution.x is incorrect if the scene is larger than it is tall. + vec3 color = supersample(xy, 1. / iResolution.x); + #else + vec3 color = color_frame(xy); + #endif + int mat = -1; + //color = vec3(distance_to_objects(mat, 5. * project_spherical(xy)) / MAX_DISTANCE); + //color = vec3(ray_march(mat, vec3(0.), project_spherical(xy)) / MAX_DISTANCE); + vec3 dir = project_spherical(xy); + float dist = ray_march(mat, vec3(0.), dir); + //color = material_color(mat) * dist / MAX_DISTANCE; + //color = (normal(dist * dir) + vec3(1.0)) / 2.; + + frag_color = vec4(from_linear(color), 0.0); +}