0.00
60.0 fps

Robotic Arm Hunting Lights

This shader is a proof of concept to see if I could create a "typical" Shadertoy shader, i.e., a shader that renders a non-trivial animated 3D scene using a ray tracer instead of the commonly used raymarching techniques.

Log in to post a comment.

#version 300 es
precision highp float;

uniform vec2 iResolution;
uniform float iTime;

out vec4 fragColor;

// Robotic Arm. Created by Reinder Nijhoff 2019
// Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
// @reindernijhoff
//
// https://www.shadertoy.com/view/tlSSDV
//
// This shader is a proof of concept to find out if I could 
// create a “typical” Shadertoy shader, i.e. a shader that renders 
// a non-trivial animated 3D scene, by using a ray tracer instead 
// of the commonly used raymarching techniques. 
//
// Some first conclusions:
// 
// - It is possible to visualize an animated 3D scene in a single 
//   shader using ray tracing.
// - The compile-time of this shader is quite long.
// - The ray tracer is not super fast, so it was not possible to cast
//   enough rays per pixel to support global illumination or soft
//   shadows. Here I miss the cheap AO and soft shadow algorithms that
//   are available when raymarching an SDF.
// - Modelling a 3D scene for a ray tracer in code is verbose. It was
//   not possible to exploit the symmetries in the arm and the domain
//   repetition of the sphere-grid that would have simplified the
//   description of an SDF.
// - I ran in GPU-dependent unpredictable precision problems. Hopefully,
//   most problems are solved now. I’m not sure if they are inherent
//   to ray tracing, but I didn’t have these kinds of problems using
//   raymarching before.
//

#define AA 1 // Set AA to 1 if you have a slow GPU
#define PATH_LENGTH 3
#define MAX_DIST 60.
#define MIN_DIST .001

// Global variables
float time;
vec2[2] activeSpheres;
vec2[3] joints;
float joint0Rot;
float jointYRot;

//
// Hash by Dave_Hoskins: https://www.shadertoy.com/view/4djSRW
//
vec2 hash22(vec2 p) {
	vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
    p3 += dot(p3, p3.yzx+33.33);
    return fract((p3.xx+p3.yz)*p3.zy);
}

//
// Ray-primitive intersection routines: https://www.shadertoy.com/view/tl23Rm
//
float dot2( in vec3 v ) { return dot(v,v); }

// Plane 
float iPlane( const in vec3 ro, const in vec3 rd, in vec2 distBound, inout vec3 normal,
              const in vec3 planeNormal, const in float planeDist) {
    float a = dot(rd, planeNormal);
    float d = -(dot(ro, planeNormal)+planeDist)/a;
    if (a > 0. || d < distBound.x || d > distBound.y) {
        return MAX_DIST;
    } else {
        normal = planeNormal;
    	return d;
    }
}

// Sphere: https://www.shadertoy.com/view/4d2XWV
float iSphere( const in vec3 ro, const in vec3 rd, const in vec2 distBound, inout vec3 normal,
               const float sphereRadius ) {
    float b = dot(ro, rd);
    float c = dot(ro, ro) - sphereRadius*sphereRadius;
    float h = b*b - c;
    if (h < 0.) {
        return MAX_DIST;
    } else {
	    h = sqrt(h);
        float d1 = -b-h;
        float d2 = -b+h;
        if (d1 >= distBound.x && d1 <= distBound.y) {
            normal = normalize(ro + rd*d1);
            return d1;
        } else {
            return MAX_DIST;
        }
    }
}

// Capped Cylinder: https://www.shadertoy.com/view/4lcSRn
float iCylinder( const in vec3 oc, const in vec3 rd, const in vec2 distBound, inout vec3 normal,
                 const in vec3 ca, const float ra, const bool traceCaps ) {
    float caca = dot(ca,ca);
    float card = dot(ca,rd);
    float caoc = dot(ca,oc);
    
    float a = caca - card*card;
    float b = caca*dot( oc, rd) - caoc*card;
    float c = caca*dot( oc, oc) - caoc*caoc - ra*ra*caca;
    float h = b*b - a*c;
    
    if (h < 0.) return MAX_DIST;
    
    h = sqrt(h);
    float d = (-b-h)/a;

    float y = caoc + d*card;
    if (y >= 0. && y <= caca && d >= distBound.x && d <= distBound.y) {
        normal = (oc+d*rd-ca*y/caca)/ra;
        return d;
    } else if(!traceCaps) {
        return MAX_DIST;
    } else {
        d = ((y < 0. ? 0. : caca) - caoc)/card;

        if( abs(b+a*d) < h && d >= distBound.x && d <= distBound.y) {
            normal = normalize(ca*sign(y)/caca);
            return d;
        } else {
            return MAX_DIST;
        }
    }
}

// Capped Cone: https://www.shadertoy.com/view/llcfRf
float iCone( const in vec3 oa, const in vec3 rd, const in vec2 distBound, inout vec3 normal,
             const in vec3 pb, const in float ra, const in float rb ) {
    vec3  ba = pb;
    vec3  ob = oa - pb;
    
    float m0 = dot(ba,ba);
    float m1 = dot(oa,ba);
    float m2 = dot(ob,ba); 
    float m3 = dot(rd,ba);

    //caps - only top cap needed for scene
    if (m2 > 0. && dot2(ob*m3-rd*m2) < (rb*rb*m3*m3) ) {
        float d = -m2 / m3;
        if (d > distBound.x && d < distBound.y) {
            normal = ba*inversesqrt(m0);
            return d;
        }
    }
    
    // body
    float m4 = dot(rd,oa);
    float m5 = dot(oa,oa);
    float rr = ra - rb;
    float hy = m0 + rr*rr;

    float k2 = m0*m0    - m3*m3*hy;
    float k1 = m0*m0*m4 - m1*m3*hy + m0*ra*(rr*m3*1.0        );
    float k0 = m0*m0*m5 - m1*m1*hy + m0*ra*(rr*m1*2.0 - m0*ra);

    float h = k1*k1 - k2*k0;
    if( h < 0. ) return MAX_DIST;

    float t = (-k1-sqrt(h))/k2;

    float y = m1 + t*m3;
    if (y > 0. && y < m0 && t >= distBound.x && t <= distBound.y) {
        normal = normalize(m0*(m0*(oa+t*rd)+rr*ba*ra)-ba*hy*y);
        return t;
    } else {   
        return MAX_DIST;
    }
}

// Box: https://www.shadertoy.com/view/ld23DV
float iBox( const in vec3 ro, const in vec3 rd, const in vec2 distBound, inout vec3 normal, 
            const in vec3 boxSize ) {
    vec3 m = sign(rd)/max(abs(rd), 1e-8);
    vec3 n = m*ro;
    vec3 k = abs(m)*boxSize;
	
    vec3 t1 = -n - k;
    vec3 t2 = -n + k;

	float tN = max( max( t1.x, t1.y ), t1.z );
	float tF = min( min( t2.x, t2.y ), t2.z );
	
    if (tN > tF || tF <= 0.) {
        return MAX_DIST;
    } else {
        if (tN >= distBound.x && tN <= distBound.y) {
        	normal = -sign(rd)*step(t1.yzx,t1.xyz)*step(t1.zxy,t1.xyz);
            return tN;
        } else if (tF >= distBound.x && tF <= distBound.y) {
        //	normal = sign(rd)*step(t1.yzx,t1.xyz)*step(t1.zxy,t1.xyz);
            return tF;
        } else {
            return MAX_DIST;
        }
    }
}

//
// Ray tracer helper functions
//
vec3 FresnelSchlick(vec3 SpecularColor, vec3 E, vec3 H) {
    return SpecularColor + (1. - SpecularColor) * pow(1.0 - max(0., dot(E, H)), 5.);
}

vec2 randomInUnitDisk(const vec2 seed) {
    vec2 h = hash22(seed) * vec2(1.,6.28318530718);
    float phi = h.y;
    float r = sqrt(h.x);
	return r*vec2(sin(phi),cos(phi));
}

//
// Sphere functions
//
vec2 activeSphereGrid(float t) {
  vec2 p = randomInUnitDisk(vec2(floor(t),.5));
  return floor(p * 8.5 + 1.75*normalize(p));
}

vec3 sphereCenter(vec2 pos) {
    vec3 c = vec3(pos.x, 0., pos.y)+vec3(.25,.25,.25);
    c.xz += .5*hash22(pos);
	return c;
}

vec3 sphereCol(in float t) {
    return normalize(.5 + .5*cos(6.28318530718*(1.61803398875*floor(t)+vec3(0,.1,.2))));
}

//
// Inverse Kinematics
//
// Very hacky, analytical,  inverse kinematics. I came up with the algorithm myself;
// Íñigo Quílez can probably implement it without using trigonometry:
// https://iquilezles.org/articles/noacos
//
void initDynamics() {
    time = iTime * .25;

    activeSpheres[0] = activeSphereGrid(time);
    activeSpheres[1] = activeSphereGrid(time+1.);

    vec3 ta0 = sphereCenter(activeSpheres[0]);
    vec3 ta1 = sphereCenter(activeSpheres[1]);

    float taa0 = atan(-ta0.z, ta0.x);  
    float taa1 = atan(-ta1.z, ta1.x);

    if (abs(taa0-taa1) > 3.14159265359) {
        taa1 += taa1 < taa0 ? 2. * 3.14159265359 : -2. * 3.14159265359;  
    }
    jointYRot = mix(taa0, taa1, clamp(fract(time)*2.-.5,0.,1.));    

    float tal = mix(length(ta0), length(ta1), clamp(fract(time)*2.5-1.,0.,1.));

    vec2 target = vec2(tal,.5-.5*smoothstep(.35,.4,abs(fract(time)-.5)));  

    float c0 = length(target);
    float b0 = min(11., 4. + 2. * c0 / 11.);

    vec2 sd = normalize(target);
    float t0 = asin(sd.y)+acos(-(b0*b0-25.-c0*c0)/(10.*c0));

    joints[0] = vec2(5. * cos(t0), 5.* sin(t0));
    joint0Rot = t0;

    sd = normalize(target-joints[0]);  
    float c1 = min(6., distance(joints[0], target));
    const float b1 = 2.;  

    float t1 = asin(sd.y) * sign(sd.x) + acos(-(b1*b1-16.-c1*c1)/(8.*c1));
    t1 += sd.x < 0. ? 3.1415 : 0.;
    joints[1] = joints[0] + 4. * vec2(cos(t1),sin(t1));
    joints[2] = target;
}

//
// Scene description
//
vec3 opU( const in vec3 d, const in float iResult, const in float mat ) {
	return (iResult < d.y) ? vec3(d.x, iResult, mat) : d;
}
      
vec3 iPlaneInt(vec3 ro, vec3 rd, float d) {
    d = -(ro.y - d) / rd.y;
    return ro + d * rd;
}

vec3 traceSphereGrid( in vec3 ro, in vec3 rd, in vec2 dist, out vec3 normal, const int maxsteps ) {  
	float m = 0.;
    if (ro.y < .5 || rd.y < 0.) {
        vec3 ros = ro.y < .5 ? ro : iPlaneInt(ro, rd, .5);
        if (length(ros.xz) < 11.) {
            vec3 roe = iPlaneInt(ro, rd,rd.y < 0. ?0.:.5);
            vec3 pos = floor(ros);
            vec3 rdi = 1./rd;
            vec3 rda = abs(rdi);
            vec3 rds = sign(rd);
            vec3 dis = (pos-ros+ .5 + rds*.5) * rdi;
            bool b_hit = false;

            // traverse grid in 2D
            vec2 mm = vec2(0);
            for (int i = 0; i<maxsteps; i++) {
                float l = length(pos.xz+.5);
                if (pos.y > .5 || pos.y < -1.5 || l > 11.) {
                    break;
                }
                else if ( l > 2. && pos.y > -.5 && pos.y < 1.5 ) {
                    float d = iSphere(ro-sphereCenter(pos.xz), rd, dist, normal, .25);
                    if (d < dist.y) {
                        m = 2.;
                        dist.y = d;
                        break;
                    }
                }	
                vec3 mm = step(dis.xyz, dis.yxy) * step(dis.xyz, dis.zzx);
                dis += mm*rda;
                pos += mm*rds;
            }
        }
    }
	return vec3(dist, m);
}

vec3 rotateY( const in vec3 p, const in float t ) {
    float co = cos(t);
    float si = sin(t);
    vec2 xz = mat2(co,si,-si,co)*p.xz;
    return vec3(xz.x, p.y, xz.y);
}

vec3 worldhit( const in vec3 ro, const in vec3 rd, const in vec2 dist, out vec3 normal ) {
    vec3 d = vec3(dist, 0.);
    
    d = traceSphereGrid(ro, rd, d.xy, normal, 10);
    
    d = opU(d, iPlane   (ro, rd, d.xy, normal, vec3(0,1,0), 0.), 1.);
    d = opU(d, iCone    (ro-vec3(0,.2,0), rd, d.xy, normal, vec3(0,.2,0), 1.5, 1.4), 4.);
    d = opU(d, iCylinder(ro, rd, d.xy, normal, vec3(0,.2,0), 1.5, false), 4.);
    
    float dmax = d.y;
    vec3 roa = rotateY(vec3(ro.x, ro.y-1., ro.z), jointYRot);    
    vec3 rda = rotateY(rd, jointYRot); 
    
    vec3 bb = vec3(.5*max(joints[1].x,joints[2].x), joints[0].y*.5, .0);
    vec3 bbn;
    
    if (iBox(roa-bb, rda, vec2(0,100), bbn, bb+vec3(.75,.75,.8)) < 100.) {
	    vec3 dr = vec3(-sin(joint0Rot), cos(joint0Rot), 0);
        vec2 j21 = joints[2]-joints[1];
        
        for (int axis=0; axis<=1; axis++) {
            float a = axis == 0 ? -1. : 1.;
            d = opU(d, iCylinder(roa-vec3(0,0,a*.67), rda, d.xy, normal, vec3(0,0,-a*.2),.55, true), 3.);
            d = opU(d, iCylinder(roa-vec3(0,0,a*.58)-.4*dr, rda, d.xy, normal, vec3(joints[0],-a*.24)-.24*dr,.07, false), 4.);
            d = opU(d, iCylinder(roa-vec3(0,0,a*.58)+.4*dr, rda, d.xy, normal, vec3(joints[0],-a*.24)+.24*dr,.07, false), 4.);
            d = opU(d, iCylinder(roa-vec3(joints[0],a*.45), rda, d.xy, normal, vec3(0,0,-a*.2),.35, true), 3.);
            d = opU(d, iCylinder(roa-vec3(joints[1],a*.29), rda, d.xy, normal, vec3(0,0,-a*.08),.25, true), 3.);
            d = opU(d, iCylinder(roa-vec3(joints[1],a*.24), rda, d.xy, normal, vec3(j21,a*.08),.03, false), 4.);
        }

        vec2 j10 = joints[1]-joints[0];
        d = opU(d, iCylinder(roa-vec3(0,0,-.72), rda, d.xy, normal, vec3(0,0,1.44),.5, true), 5.);
        d = opU(d, iBox     (roa+vec3(0,.5,0), rda, d.xy, normal, vec3(.5,.5,.47)), 5.);
        d = opU(d, iCone    (roa-vec3(joints[0],0), rda, d.xy, normal, vec3(j10,0),.25, .15), 5.);
        d = opU(d, iCylinder(roa-vec3(joints[0],-.5), rda, d.xy, normal, vec3(0,0,1.),.3, true), 5.);
        d = opU(d, iCylinder(roa-vec3(joints[1],-.35), rda, d.xy, normal, vec3(0,0,.7),.2, true), 5.);
        d = opU(d, iCylinder(roa-vec3(joints[2],-.4), rda, d.xy, normal, vec3(0,0,.8),.2, true), 3.);
        d = opU(d, iSphere  (roa-vec3(joints[2],0), rda, d.xy, normal, .32), 5.);
        d = opU(d, iCylinder(roa-vec3(joints[2],0), rda, d.xy, normal, vec3(0,-.5,0),.06, true), 3.);

        if (d.y < dmax) {
            normal = rotateY(normal, -jointYRot);
        }
    }    
    return d;
}

float shadowhit( const vec3 ro, const vec3 rd, const float dist) {
    vec3 normal;
    float d = traceSphereGrid( ro, rd, vec2(.3, dist), normal, 4).y;
    d = min(d, iCylinder(ro, rd, vec2(.3, dist), normal, vec3(0,.2,0), 1.5, false));
    return d < dist-0.001 ? 0. : 1.;
}

//
// Simple ray tracer
//
float getSphereLightIntensity(float num) {
    return num > .5 ?
        clamp(fract(time)*10.-1., 0., 1.) :
		max(0., 1.-fract(time)*10.); 
}

float getLightIntensity( const vec3 pos, const vec3 normal, const vec3 light, const float intensity) {
    vec3 rd = pos - light;
    float i = max(0., dot(normal, -normalize(rd)) / dot(rd,rd));
    i = i > 0.0001 ? i * intensity * shadowhit(light, normalize(rd), length(rd)) : 0.;
    return max(0., i-0.0001);              
}

vec3 getLighting( vec3 p, vec3 normal ) {
    vec3 l = vec3(0.);
    
    float i = getSphereLightIntensity(0.);
    if (i > 0.) {
	    l += sphereCol(time) * (i * getLightIntensity(p, normal, sphereCenter(activeSpheres[0]), .375));
    } else {    
        i = getSphereLightIntensity(1.);
        if (i > 0.) {
            l += sphereCol(time+1.) * (i * getLightIntensity(p, normal, sphereCenter(activeSpheres[1]), .25));
        }
    }
    
    vec3 robot = mix(sphereCol(time), sphereCol(time-1.), getSphereLightIntensity(0.));
    vec3 lp = rotateY(vec3(joints[2].x, joints[2].y+1.,0), -jointYRot);
    i = getLightIntensity(p, normal, lp, .5);
    i += getLightIntensity(p, normal, vec3(0,2,0), .25);
    l += i * robot;
    
    return l;
}

vec3 getEmissive( in vec2 pos, in float mat ) {
    if (mat > 2.5 ) {
	   return mix(sphereCol(time), sphereCol(time-1.), getSphereLightIntensity(0.));
    } else if (mat > 1.5 ) {
        float li0 = getSphereLightIntensity(0.);
        float li1 = getSphereLightIntensity(1.);
        if (li0 > 0. && pos == activeSpheres[0]) {
            return sphereCol(time) * li0 * 1.25;
        } else if (li1 > 0. && pos == activeSpheres[1]) {
            return sphereCol(time+1.) * li1;
        } else {
            return vec3(0);
        }
    } else {
        return vec3(0);
    }
}

vec3 render( in vec3 ro, in vec3 rd) {
    vec3 col = vec3(1);
    vec3 emitted = vec3(0);
    vec3 normal;
        
    for (int i=0; i<PATH_LENGTH; ++i) {
    	vec3 res = worldhit( ro, rd, vec2(MIN_DIST, MAX_DIST-1.), normal );
		if (res.z > 0.) {
			ro += rd * res.y;

            if (res.z < 3.5) { 
               	vec3 F = FresnelSchlick(vec3(0.4), normal, -rd);
                emitted += (col * (getEmissive(floor(ro.xz), res.z) + .5 * getLighting(ro, normal))) * (1.-F);
                col *= .5 * F;
            } else {
                col *= .15;   
            } 
            
            rd = normalize(reflect(rd,normal));
        } else {
			return emitted;
        }
    }  
    return emitted;
}

mat3 setCamera( in vec3 ro, in vec3 ta, float cr ) {
	vec3 cw = normalize(ta-ro);
	vec3 cp = vec3(sin(cr), cos(cr),0.0);
	vec3 cu = normalize( cross(cw,cp) );
	vec3 cv =          ( cross(cu,cw) );
    return mat3( cu, cv, cw );
}

void main() {
    initDynamics();

    vec2 mo = vec2(.4,-.1);

    vec3 ro = vec3(10.5*cos(1.5+6.*mo.x), 6.+10.*mo.y, 8.5*sin(1.5+6.*mo.x));
    vec3 ta = vec3(ro.x*ro.y*.02, .8, 0);
    mat3 ca = setCamera(ro, ta, 0.);    
    
    vec3 col = vec3(0);
    
#if AA>1
    for( int m=ZERO; m<AA; m++ )
    for( int n=ZERO; n<AA; n++ ) {
        vec2 o = vec2(float(m),float(n)) / float(AA) - 0.5;
        vec2 p = (-iResolution.xy + 2.0*(gl_FragCoord.xy+o))/iResolution.y;
#else    
        vec2 p = (-iResolution.xy + 2.0*gl_FragCoord.xy)/iResolution.y;
#endif
        vec3 rd = ca * normalize( vec3(p.xy,1.6) );  
        col += pow(8. * render(ro, rd), vec3(1./2.2));
#if AA>1
    }
    col /= float(AA*AA);
#endif
    
    col = clamp(col + ((hash22(gl_FragCoord.xy).x-.5)/64.), vec3(0), vec3(1));
    
	fragColor = vec4(col , 1);
}