// Ben Weston - 15/08/2013 /* Eye ball effects: • Ray-marched shape • Ray-traced iris refraction • Fake photon mapping on iris • Subsurface scattering on sclera • HDR reflections with fresnel • Eyelid reflection occlusion • Eyelid ambient occlusion • Procedural textures • Procedural animation */ // KEY CONTROLS - (click on eye to give keyboard focus) const int Key_M = 77; // mouse controls camera / eye direction const int Key_E = 69; // refraction on/off const int Key_P = 80; // photon mapping on/off const int Key_L = 76; // change photon mapping technique (both fake, but one is imitating reality and the other is prettier) const int Key_S = 83; // subsurface scattering on/off const int Key_A = 65; // ambient occlusion on/off const int Key_R = 82; // reflection on/off const int Key_O = 79; // reflection eyelid occlusion on/off const int Key_C = 67; // iris colour const int Key_N = 78; // iris normal // Lights #if (1) // High-contrast light edge-on const vec3 lightDir = vec3(-2,2,.5); const vec3 lightColour = vec3(1.0); const vec3 fillLightDir = vec3(0,1,0); const vec3 fillLightColour = vec3(.65,.7,.8)*.7;//vec3(.15,.2,.25); #else // more neutral "good" lighting (doesn't show off the effects) const vec3 lightDir = vec3(-2,2,-1); const vec3 lightColour = vec3(.83,.8,.78); const vec3 fillLightDir = vec3(0,1,0); const vec3 fillLightColour = vec3(.65,.7,.8); #endif // Constants const float tau = 6.28318530717958647692; // Forward declarations float Noise( in vec3 x ); vec2 Noise2( in vec3 x ); // Gamma correction #define GAMMA (2.2) vec3 ToLinear( in vec3 col ) { // simulate a monitor, converting colour values into light values return pow( col, vec3(GAMMA) ); } vec3 ToGamma( in vec3 col ) { // convert back into colour values, so the correct light will come out of the monitor return pow( col, vec3(1.0/GAMMA) ); } // key is javascript keycode: http://www.webonweboff.com/tips/js/event_key_codes.aspx bool ReadKey( int key, bool toggle ) { float keyVal = texture( iChannel3, vec2( (float(key)+.5)/256.0, toggle?.75:.25 ) ).x; return (keyVal>.5)?true:false; } // ------- EDIT THESE THINGS! ------- // Camera (also rotated by mouse) const vec3 CamPos = vec3(0,0.0,-250.0); const vec3 CamLook = vec3(0,0,0); const float CamZoom = 10.0; const float NearPlane = 0.0; // actually not needed const float drawDistance = 1000.0; const vec3 SkyColour = vec3(.4,.25,.2);//fillLightColour*.5;//vec3(.1,.3,.5); vec3 SkyDome( vec3 rd ) { //the cube maps have lines in, and aren't HDR, so make our own shapes // random variation vec3 result = ToLinear(SkyColour)*2.0*Noise(rd); // square sky-light result = mix( result, vec3(8), smoothstep(.8,1.0,rd.y/max((rd.x+1.0),abs(rd.z))) ); return result; } // Eye params const float IrisAng = tau/12.0; const float PupilAng = (1.6*IrisAng/5.0); const float EyeRadius = 10.0; const float BulgeRadius = 6.0; // used for photon trace, must be bigger than EyeRadius*sin(IrisAng) vec4 ComputeEyeRotation() { vec2 rot; if ( !ReadKey( Key_M, true ) && iMouse.w > .00001 ) rot = .25*vec2(1.0,1.0)*tau*(iMouse.xy-iResolution.xy*.5)/iResolution.x; else { float time = iGlobalTime/2.0; time += Noise(vec3(0,time,0)); // add noise to time (this adds SO MUCH character!) float flick = floor(time)+smoothstep(0.0,0.05,fract(time)); rot = vec2(.2,.1)*tau*(texture( iChannel0, vec2((flick+.5)/256.0,.5), -100.0 ).rb-.5); } return vec4(cos(rot.x),sin(rot.x),cos(rot.y),sin(rot.y)); } vec3 ApplyEyeRotation( vec3 pos, vec4 rotation ) { pos.yz = rotation.z*pos.yz + rotation.w*pos.zy*vec2(1,-1); pos.xz = rotation.x*pos.xz + rotation.y*pos.zx*vec2(1,-1); return pos; } // Shape // This should return continuous positive values when outside and negative values inside, // which roughly indicate the distance of the nearest surface. float Isosurface( vec3 pos, vec4 eyeRotation ) { pos = ApplyEyeRotation(pos,eyeRotation); /* float f = length(pos)-EyeRadius; // f += Noise(pos*3.0)*.008; // cornea bulge float o = EyeRadius*cos(IrisAng)-sqrt(BulgeRadius*BulgeRadius-EyeRadius*EyeRadius*pow(sin(IrisAng),2.0)); float g = length(pos-vec3(0,0,-o))-BulgeRadius; //g += Noise(pos/2.0)*.5; return min(f,g); //return -log(exp(-g*2.0)+exp(-f*2.0))/2.0;*/ vec2 slice = vec2(length(pos.xy),pos.z); float aa = atan(slice.x,-slice.y); float bulge = cos(tau*.2*aa/IrisAng); bulge = bulge*.8-.8; bulge *= smoothstep(tau*.25,0.0,aa); // sharp-edged bulge // if ( aa < IrisAng ) // bulge += cos(tau*.25*aa/IrisAng)*.5; bulge += cos(tau*.25*aa/IrisAng)*.5 * smoothstep(-.02,.1,IrisAng-aa); // slightly softer return length(slice) - EyeRadius - bulge; } float GetEyelidMask( vec3 pos, vec4 eyeRotation ) { vec3 eyelidPos = pos; float eyelidTilt = -.05; eyelidPos.xy = cos(eyelidTilt)*pos.xy + sin(eyelidTilt)*pos.yx*vec2(1,-1); float highLid = tan(max(tau*.05,asin(eyeRotation.w)+IrisAng+.05)); float lowLid = tan(tau*.1); float blink = smoothstep(.0,.02,abs(Noise(vec3(iGlobalTime*.2,0,0))-.5 )); highLid *= blink; lowLid *= blink; return min( (-eyelidPos.z-2.0) - (-eyelidPos.y/lowLid), (-eyelidPos.z-2.0) - (eyelidPos.y/highLid) ); } float GetIrisPattern( vec2 uv ) { return Noise( vec3( 10.0*uv/pow(length(uv),.7), 0 ) ); } // Colour vec3 Shading( vec3 worldPos, vec3 norm, float shadow, vec3 rd, vec4 eyeRotation ) { vec3 view = normalize(-rd); // eyelids - just match BG colour float eyelidMask = GetEyelidMask(worldPos, eyeRotation); if ( eyelidMask < 0.0 || (-worldPos.z-3.0) < (worldPos.x/tan(tau*.23)) ) { return ToLinear(SkyColour); } vec3 pos = ApplyEyeRotation(worldPos,eyeRotation); float lenposxy = length(pos.xy); float ang = atan(lenposxy/(-pos.z)); if ( ang < 0.0 ) ang += tau/2.0; // refract ray vec3 irisRay = ApplyEyeRotation(-view,eyeRotation); vec3 localNorm = ApplyEyeRotation(norm,eyeRotation); float a = dot(irisRay,localNorm); float b = cos(acos(a)*1.33); if ( !ReadKey( Key_E, true ) ) irisRay += localNorm*(b-a); irisRay = normalize(irisRay); // intersect with plane float planeDist = -cos(IrisAng)*EyeRadius; float t = (planeDist-pos.z)/irisRay.z; vec3 ppos = t*irisRay+pos; // polar coord map float rad = length(ppos.xy); float pupilr = EyeRadius*sin(PupilAng); float irisr = EyeRadius*sin(IrisAng); float irisPattern = GetIrisPattern(ppos.xy); // reduce contrast of this now we have actual lighting! /* vec3 iris = mix( mix( vec3(.3,.1,.1)*.5+.5*vec3(.6,.4,.1), vec3(.6,.4,.1), irisPattern ), // hazel mix( vec3(.2,.2,.2)*.5+.5*vec3(.5,.45,.2), vec3(.5,.45,.2), irisPattern ),*/ /* vec3 iris = mix( mix( vec3(.1,.1,.4), vec3(.7,.9,1), irisPattern ), // blue mix( vec3(.1,.1,.4), vec3(.3,.4,.7), irisPattern ),*/ // smoothstep(pupilr*2.0,irisr,rad)); vec3 iris = ToLinear( mix( pow( vec3(.65,.82,.85), 2.0*vec3(1.2-sqrt(irisPattern)) ), vec3(1,.5,.2), .7*pow( mix( smoothstep(pupilr,irisr,rad), Noise(ppos), .7), 2.0) )); if ( ReadKey( Key_C, true ) ) iris = vec3(1); // darken outer iris *= pow( smoothstep( irisr+1.0, irisr-1.5, rad ), GAMMA ); vec3 irisNorm; irisNorm.x = GetIrisPattern(ppos.xy+vec2(-.001,0)) - GetIrisPattern(ppos.xy+vec2(.001,0)); irisNorm.y = GetIrisPattern(ppos.xy+vec2(0,-.001)) - GetIrisPattern(ppos.xy+vec2(0,.001)); // add a radial lump irisNorm.xy += -.01*normalize(ppos.xy)*sin(1.*tau*rad/irisr); irisNorm.z = -.15; // adjust severity of bumps irisNorm = normalize(irisNorm); if ( ReadKey( Key_N, true ) ) irisNorm = vec3(0,0,-1); // lighting // fake photon mapping by crudely sampling the photon density // apply lighting with this modified normal vec3 lightDirN = normalize(lightDir); vec3 localLightDir = ApplyEyeRotation(lightDirN,eyeRotation); vec3 fillLightDirN = normalize(fillLightDir); vec3 localFillLightDir = ApplyEyeRotation(fillLightDirN,eyeRotation); // Bend the light, imitating results of offline photon-mapping // Jimenez's paper makes this seem very complex, because their mapping used a non-flat receiver // but the self-shadowing was negligible, so the main effect was just like premultiplying by a normal // where we'd get better results by using the actual normal. float photonsL, photonsFL; if ( !ReadKey( Key_P, true ) ) { if ( !ReadKey( Key_L, true ) ) { // Nice retro-reflective effect, but not correct vec3 nn = normalize(vec3( ppos.xy, -sqrt(max(0.0,BulgeRadius*BulgeRadius-rad*rad)) )); vec3 irisLDir = localLightDir; vec3 irisFLDir = localFillLightDir; // irisLDir.z = -cos(acos(-irisLDir.z)/1.33); // experiments showed it cuts out at 120 degrees, i.e. 1.33*the usual 90 degree cutoff // irisFLDir.z = -cos(acos(-irisFLDir.z)/1.33); // experiments showed it cuts out at 120 degrees, i.e. 1.33*the usual 90 degree cutoff float d = dot(nn,irisLDir); irisLDir += nn*(cos(acos(d)/1.33) - d); d = dot(nn,irisFLDir); irisFLDir += nn*(cos(acos(d)/1.33) - d); irisLDir = normalize(irisLDir); irisFLDir = normalize(irisFLDir); photonsL = smoothstep(0.0,1.0,dot(irisNorm,irisLDir)); //soften terminator photonsFL = (dot(irisNorm,irisFLDir)*.5+.5); //Seriously, this^ looks really nice, but not like reality. Bah! /* reverse it, to make it look a lot like the accurate version - meh vec3 nn = normalize(vec3( -ppos.xy, -sqrt(max(0.0,BulgeRadius*BulgeRadius-rad*rad)) )); vec3 irisLDir = localLightDir; vec3 irisFLDir = localFillLightDir; float d = dot(nn,irisLDir); irisLDir += nn*(cos(acos(d)/1.33) - d); d = dot(nn,irisFLDir); irisFLDir += nn*(cos(acos(d)/1.33) - d); irisLDir = normalize(irisLDir); irisFLDir = normalize(irisFLDir); float photonsL = smoothstep(0.0,1.0,dot(irisNorm,irisLDir)); // soften the terminator float photonsFL = (dot(irisNorm,irisFLDir)*.5+.5); */ } else { //this is a reasonable match to the dark crescent effect seen in photos and offline photon mapping, but it looks wrong to me. vec3 irisLDir = localLightDir; vec3 irisFLDir = localFillLightDir; irisLDir.z = -cos(acos(-irisLDir.z)/1.5); // experiments showed it cuts out at 120 degrees, i.e. 1.33*the usual 90 degree cutoff irisFLDir.z = -cos(acos(-irisFLDir.z)/1.5); // experiments showed it cuts out at 120 degrees, i.e. 1.33*the usual 90 degree cutoff irisLDir = normalize(irisLDir); irisFLDir = normalize(irisFLDir); photonsL = smoothstep(0.0,1.0,dot(irisNorm,irisLDir)); // soften the terminator photonsFL = (dot(irisNorm,irisFLDir)*.5+.5); // dark caustic ring photonsL *= .3+.7*smoothstep( 1.2, .9, length(ppos.xy/irisr+.2*irisLDir.xy/(irisLDir.z-.05)) ); // photonsFL *= ...; } } else { // no photons photonsL = max( 0.0, dot(irisNorm,localLightDir) ); photonsFL = .5+.5*dot(irisNorm,localLightDir); } vec3 l = ToLinear(lightColour)*photonsL; vec3 fl = ToLinear(fillLightColour)*photonsFL; vec3 ambientOcclusion = vec3(1); vec3 eyelidShadow = vec3(1); if ( !ReadKey( Key_A, true ) ) { // ambient occlusion on fill light ambientOcclusion = mix( vec3(1), ToLinear(vec3(.8,.7,.68)), pow(smoothstep( 5.0, 0.0, eyelidMask ),1.0) ); // shadow on actual light eyelidShadow = mix( vec3(1), ToLinear(vec3(.8,.7,.68)), smoothstep( 2.0, -2.0, GetEyelidMask( worldPos+lightDir*1.0, eyeRotation ) ) ); } fl *= ambientOcclusion; l *= eyelidShadow; iris *= l+fl; // darken pupil iris *= smoothstep( pupilr-.01, pupilr+.5, rad ); // veins float theta = atan(pos.x,pos.y); theta += Noise(pos*1.0)*tau*.03; float veins = (sin(theta*60.0)*.5+.5); veins *= veins; veins *= (sin(theta*13.0)*.5+.5); veins *= smoothstep( IrisAng, tau*.2, ang ); veins *= veins; veins *= .5; vec3 sclera = mix( ToLinear(vec3(1,.98,.96)), ToLinear(vec3(.9,.1,0)), veins ); float ndotl = dot(norm,lightDirN); // subsurface scattering // float subsurface = max(0.0,-2.0*ndotl*EyeRadius); // l = pow(ToLinear(vec3(.5,.3,.25)),vec3(subsurface*.2)); // more intense the further light had to travel // fake, because that^ approximation gives a hard terminator l = pow(ToLinear(vec3(.5,.3,.25)), vec3(mix( 3.0, 0.0, smoothstep(-1.0,.2,ndotl) )) ); if ( ReadKey( Key_S, true ) ) // l = mix( l, vec3(max(0.0,ndotl)), 0.5 ); // else l = vec3(max(0.0,ndotl)); l *= ToLinear(lightColour); fl = ToLinear(fillLightColour)*(dot(norm,fillLightDirN)*.5+.5); fl *= ambientOcclusion; l *= eyelidShadow; sclera *= l+fl; // blend between them float blend = smoothstep(-.1,.1,ang-IrisAng); vec3 result = mix(iris,sclera,blend); // eyelid ambient occlusion/radiosity // if ( !ReadKey( Key_A, true ) ) //result *= mix( vec3(1), ToLinear(vec3(.65,.55,.55)), exp2(-eyelidMask*2.0) ); // result *= mix( vec3(1), ToLinear(vec3(.8,.7,.68)), pow(smoothstep( 5.0, 0.0, eyelidMask ),1.0) ); // bumps - in specular only to help sub-surface scattering look smooth vec3 bumps; bumps.xy = .7*Noise2( pos*3.0 ); bumps.z = sqrt(1.0-dot(bumps.xy,bumps.xy)); bumps = mix( vec3(0,0,1), bumps, blend ); norm.xy += bumps.xy*.1; norm = normalize(norm); float glossiness = mix(.7,1.0,bumps.z); // reflection map float ndoti = dot( view, norm ); vec3 rr = -view+2.0*ndoti*norm; vec3 reflection = SkyDome( rr ); // specular vec3 h = normalize(view+lightDir); float specular = pow(max(0.0,dot(h,norm)),2000.0); // should fresnel affect specular? or should it just be added? reflection += specular*32.0*glossiness*ToLinear(lightColour); // reflection of eyelids //float eyelidReflection = smoothstep( 1.8, 2.0, eyelidMask ); // apply some parallax (subtle improvement when looking up/down at eye) float eyelidReflection = smoothstep( .8, 1.0, GetEyelidMask( normalize(worldPos + rd*2.0)*EyeRadius, eyeRotation ) ); if ( !ReadKey( Key_O, true ) ) reflection *= eyelidReflection; // fresnel float fresnel = mix(.04*glossiness,1.0,pow(1.0-ndoti,5.0)); if ( !ReadKey( Key_R, true ) ) result = mix ( result, reflection, fresnel ); //anti-alias the edge float mask2 = min( eyelidMask, (-worldPos.z-3.0) - (worldPos.x/tan(tau*.23)) ); result = mix( ToLinear(SkyColour), result, smoothstep(.0,.3,mask2) ); return result; } // Precision controls const float epsilon = .003; const float normalPrecision = .1; const float shadowOffset = .1; const int traceDepth = 100; // takes time // ------- BACK-END CODE ------- vec2 Noise2( in vec3 x ) { vec3 p = floor(x.xzy); vec3 f = fract(x.xzy); f = f*f*(3.0-2.0*f); // vec3 f2 = f*f; f = f*f2*(10.0-15.0*f+6.0*f2); vec2 uv = (p.xy+vec2(37.0,17.0)*p.z) + f.xy; vec4 rg = textureLod( iChannel0, (uv+0.5)/256.0, 0.0 ); return mix( rg.yw, rg.xz, f.z ); } float Noise( in vec3 x ) { return Noise2(x).x; } float Trace( vec3 ro, vec3 rd, vec4 eyeRotation ) { float t = 0.0; float dist = 1.0; for ( int i=0; i < traceDepth; i++ ) { if ( abs(dist) < epsilon || t > drawDistance || t < 0.0 ) continue; dist = Isosurface( ro+rd*t, eyeRotation ); t = t+dist; } return t;//vec4(ro+rd*t,dist); } // get normal vec3 GetNormal( vec3 pos, vec4 eyeRotation ) { const vec2 delta = vec2(normalPrecision, 0); vec3 n; // it's important this is centred on the pos, it fixes a lot of errors n.x = Isosurface( pos + delta.xyy, eyeRotation ) - Isosurface( pos - delta.xyy, eyeRotation ); n.y = Isosurface( pos + delta.yxy, eyeRotation ) - Isosurface( pos - delta.yxy, eyeRotation ); n.z = Isosurface( pos + delta.yyx, eyeRotation ) - Isosurface( pos - delta.yyx, eyeRotation ); return normalize(n); } // camera function by TekF // compute ray from camera parameters vec3 GetRay( vec3 dir, float zoom, vec2 uv ) { uv = uv - .5; uv.x *= iResolution.x/iResolution.y; dir = zoom*normalize(dir); vec3 right = normalize(cross(vec3(0,1,0),dir)); vec3 up = normalize(cross(dir,right)); return dir + right*uv.x + up*uv.y; } void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord.xy / iResolution.xy; vec3 camPos = CamPos; vec3 camLook = CamLook; vec2 camRot = .5*tau*(iMouse.xy-iResolution.xy*.5)/iResolution.x; if ( !ReadKey( Key_M, true ) ) camRot = vec2(0,0); camPos.yz = cos(camRot.y)*camPos.yz + sin(camRot.y)*camPos.zy*vec2(1,-1); camPos.xz = cos(camRot.x)*camPos.xz + sin(camRot.x)*camPos.zx*vec2(1,-1); vec4 eyeRotation = ComputeEyeRotation(); if ( Isosurface(camPos, eyeRotation) <= 0.0 ) { // camera inside ground fragColor = vec4(0,0,0,0); return; } vec3 ro = camPos; vec3 rd; rd = GetRay( camLook-camPos, CamZoom, uv ); ro += rd*(NearPlane/CamZoom); rd = normalize(rd); float t = Trace(ro,rd,eyeRotation); vec3 result = ToLinear(SkyColour); if ( t > 0.0 && t < drawDistance ) { vec3 pos = ro+t*rd; vec3 norm = GetNormal(pos,eyeRotation); // shadow test float shadow = 1.0; if ( Trace( pos+lightDir*shadowOffset, lightDir, eyeRotation ) < drawDistance ) shadow = 0.0; result = Shading( pos, norm, shadow, rd, eyeRotation ); // fog // result = mix ( SkyColour, result, exp(-t*t*.0002) ); } fragColor = vec4( ToGamma( result ), 1.0 ); }