// 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); return max(sdcube, -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.0001, 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); }