//double include guard #ifndef RAY_MARCH_LIB_INCLUDED #define RAY_MARCH_LIB_INCLUDED #include "UnityCG.cginc" // Multi compile stuff #define V_X float3(1, 0, 0) #define V_Y float3(0, 1, 0) #define V_Z float3(0, 0, 1) #define V_XZ float3(1, 0, 1) #define V_XY float3(1, 1, 0) #define V_YZ float3(0, 1, 1) //ambient occlusion quality #ifndef AO_STEPS #define AO_STEPS 5 #endif //normals for lighting #ifndef NORMAL_DELTA #define NORMAL_DELTA 0.001 #endif //normals for reflection angles #ifndef REFL_NORMAL_DELTA #define REFL_NORMAL_DELTA 0.001 #endif #ifndef MAX_REFLECTIONS #define MAX_REFLECTIONS 2 #endif #ifdef USE_DYNAMIC_QUALITY//quality settings as unity material properties int _MaxSteps = 100; float _MaxDist = 100; float _SurfDist = 0.00001; #else//pre compile quality settings #ifndef MAX_STEPS #define MAX_STEPS 256 //256 #endif #ifndef MAX_DIST #define MAX_DIST 128 #endif #ifndef SURF_DIST #define SURF_DIST 0.0001 //#define SURF_DIST 0.00001 #endif #endif #define col(r, g, b) fixed4(r, g, b, 1) struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; float3 vCamPos : TEXCOORD1; float3 vHitPos : TEXCOORD2; }; struct fragOut { fixed4 col : SV_Target; float depth : SV_Depth; }; typedef struct material { fixed4 col; fixed fRough; } material_t; #define DEFMAT {fixed4(.2,.2,.2,1), 1} #define M_RED {fixed4(0.2, 0.001, 0.001, 1), 1} #define M_ORANGE {fixed4(0.2, 0.1, 0.001, 1), 1} #define M_YELLOW {fixed4(0.2, 0.2, 0.001, 1), 1} #define M_GREEN {fixed4(0.001, 0.2, 0.001, 1), 1} #define M_BLUE {fixed4(0.001, 0.001, 0.2, 1), 1} #define M_LIGHT_BLUE{fixed4(0.001, 0.05, 0.2, 1), 1} #define M_MAGENTA {fixed4(0.2, 0.001, 0.2, 1), 1} #define M_PURPLE {fixed4(0.05, 0.001, 0.2, 1), 1} #define M_WHITE {fixed4(0.5, 0.5, 0.5, 1), 1} #define M_MIRROR {fixed4(0.1, 0.1, 0.1, 1), 0} inline material mat(float r, float g, float b, float fRough = 1) { material m = {fixed4(r, g, b, 1), fRough}; return m; } inline material mat(float3 rgb, float fRough = 1) { material m = {fixed4(rgb, 1), fRough}; return m; } //used for lighting a point struct rayData { float dist; int iSteps; material mat; float3 vRayStart; float3 vRayDir; float3 vHit; fixed3 vNorm; bool bMissed; float minDist; float distToMinDist; }; //returned from distance functions, including main scene struct sdfData { float dist; material mat; }; sdfData scene(float3 p); fixed4 lightPoint(rayData r); fixed4 rayMarch(float3 p, float3 d); rayData castRay(float3 p, float3 d, float startDist = 0); v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); #ifdef USE_WORLD_SPACE o.vCamPos = _WorldSpaceCameraPos; o.vHitPos = mul(unity_ObjectToWorld, v.vertex); #else o.vCamPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)); o.vHitPos = v.vertex; #endif return o; } #ifdef USE_REFLECTIONS #define CALC_NORM fragOut frag (v2f i) { float fRayLen = 0;//since last bounce #ifdef CONSTRAIN_TO_MESH float3 vLastBounce = i.vHitPos; fRayLen += length(i.vHitPos - i.vCamPos); #else float3 vLastBounce = i.vCamPos; #endif float3 vRayDir = normalize(i.vHitPos - i.vCamPos);//current direction sdfData point_data; rayData ray; fixed4 col; float colUsed = 0;// what amount of the final colour has been calculated float prevRough = 0; float3 vFirstHit; for (int i = 0; i < MAX_REFLECTIONS+1; i++) { ray = castRay(vLastBounce, vRayDir); if (i == 0) {//before any bounces col = lightPoint(ray); vFirstHit = ray.vHit; } else { float colAmt = colUsed + (prevRough * (1-colUsed)); col = lerp(lightPoint(ray), col, colAmt); colUsed = colAmt; } if (ray.bMissed || ray.mat.fRough > 0.99) { break; } prevRough = ray.mat.fRough; vRayDir = reflect(vRayDir, ray.vNorm); vLastBounce = ray.vHit + vRayDir * 0.01; } #ifdef DISCARD_ON_MISS if (ray.bMissed && i == 0) discard; #endif fragOut o; o.col = col; #ifdef USE_WORLD_SPACE float4 vClipPos = mul(UNITY_MATRIX_VP, float4(vFirstHit, 1)); #else float4 vClipPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(vFirstHit, 1))); #endif o.depth = vClipPos.z / vClipPos.w; #if !defined(UNITY_REVERSED_Z) // basically only OpenGL (unity editor on linux) o.depth = o.depth * 0.5 + 0.5; // remap -1 to 1 range to 0.0 to 1.0 #endif return o; } #else fragOut frag (v2f i) { float3 vRayDir = normalize(i.vHitPos - i.vCamPos); #ifdef CONSTRAIN_TO_MESH //rayData ray = castRay(i.vHitPos, vRayDir, length(i.vHitPos-i.vCamPos)); rayData ray = castRay(i.vCamPos, vRayDir, length(i.vHitPos-i.vCamPos)); //rayData ray = castRay(i.vCamPos, vRayDir, 1); //rayData ray = castRay(i.vCamPos, vRayDir, 0); #else rayData ray = castRay(i.vCamPos, vRayDir); #endif #ifdef DISCARD_ON_MISS if (ray.bMissed) discard; #endif fragOut o; o.col = lightPoint(ray); // writing to depth buffer costs about 1-2 frames at 4k #ifdef USE_WORLD_SPACE float4 vClipPos = mul(UNITY_MATRIX_VP, float4(ray.vHit, 1)); #else float4 vClipPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(ray.vHit, 1))); #endif o.depth = (vClipPos.z / vClipPos.w + 1.0) * 0.5; return o; } #endif //gets normal of a point inline float3 getNormFull(float3 vPos, float fEpsilon = 0.001) { //if epsilon is smaller than 0.001, there are often artifacts const float2 e = float2(fEpsilon, 0); float3 n = scene(vPos).dist - float3( scene(vPos - e.xyy).dist, scene(vPos - e.yxy).dist, scene(vPos - e.yyx).dist); return normalize(n); } //gets normal, provided you have the distance for pos (1 less call to scene()) inline float3 getNorm(float3 vPos, float fPointDist, float fEpsilon = 0.001) { ////if epilon is smaller than 0.001, there are often artifacts const float2 e = float2(fEpsilon, 0); float3 n = fPointDist - float3( scene(vPos - e.xyy).dist, scene(vPos - e.yxy).dist, scene(vPos - e.yyx).dist); return normalize(n); } //marches a ray through the scene once rayData castRay(float3 vRayStart, float3 vRayDir, float startDist) { float fRayLen = startDist;//startDist;// total distance marched / distance from camera float3 vPos; sdfData sdf_data; rayData ray; ray.vRayDir = vRayDir; ray.vRayStart = vRayStart; ray.minDist = 30000.0;// budget "infinity" ray.distToMinDist = 0; #ifdef USE_DYNAMIC_QUALITY for (int i = 0; i < _MaxSteps; i++) #else for (int i = 0; i < MAX_STEPS; i++) #endif { vPos = vRayStart + fRayLen * vRayDir; sdf_data = scene(vPos); #ifdef USE_DYNAMIC_QUALITY if (abs(sdf_data.dist) < _SurfDist) break; #else if (abs(sdf_data.dist) < SURF_DIST) break; #endif fRayLen += sdf_data.dist;// move forward if (ray.minDist>sdf_data.dist) { ray.minDist = sdf_data.dist; ray.distToMinDist = fRayLen; } #ifdef USE_DYNAMIC_QUALITY if (fRayLen > _MaxDist) {ray.bMissed = true; break;}//flag this as transparent/sky #else if (fRayLen > MAX_DIST) {ray.bMissed = true; break;}//flag this as transparent/sky #endif } ray.dist = fRayLen; ray.iSteps = i; ray.mat = sdf_data.mat; ray.vHit = vPos; #ifdef CALC_NORM ray.vNorm = getNorm(vPos, sdf_data.dist); #endif return ray; } ////////////////////////////////////////////////////////////////////// // // Lighting // ////////////////////////////////////////////////////////////////////// //generates a skybox, use when ray didn't hit anything (ray_data.bMissed) inline fixed4 sky(float3 vRayDir) { float4 cRenderedSun = max(0, pow(dot(vRayDir, normalize(float3(8,4,2))) + 0.4, 10)-28) * float4(.8,.4,0,1); return fixed4(0.7, 0.75, 0.8, 1) - abs(vRayDir.y) * 0.5 + cRenderedSun; } //calculate sun light based on normal fixed4 lightSun(float3 vNorm, float3 vSunDir = float3(8, 4, 2), fixed4 cSunCol = fixed4(7.0, 5.5, 3.0, 1)) { float fSunLight = max(dot(vNorm, vSunDir), 0); return fSunLight * cSunCol; } //calculate shadow from sun float lightShadow(float3 vPos, float3 vSunDir, float fSharpness = 8) { float fShadow = 1; #ifdef USE_DYNAMIC_QUALITY for (float fRayLen = 0.001; fRayLen < _MaxDist/2.0;) #else for (float fRayLen = 0.001; fRayLen < MAX_DIST/2.0;) #endif { float dist = scene(vPos + vSunDir * fRayLen).dist; #ifdef USE_DYNAMIC_QUALITY if (dist < _SurfDist) return 0; #else if (dist < SURF_DIST) return 0; #endif fShadow = min(fShadow, fSharpness * dist/fRayLen); fRayLen += dist; } return fShadow; } //calculate sky light inline fixed4 lightSky(float3 vNorm, fixed4 cSkyCol = fixed4(0.5, 0.8, 0.9, 1)) { return cSkyCol * (0.5 + 0.5 * vNorm.y); } //bad ambient occlusion (screen space) based on steps float lightSSAO(rayData ray_data, float fDarkenFactor = 2) { #ifdef USE_DYNAMIC_QUALITY return pow(1 - float(ray_data.iSteps) / _MaxSteps, fDarkenFactor); #else return pow(1 - float(ray_data.iSteps) / MAX_STEPS, fDarkenFactor); #endif } //ambient occlusion float lightAO(float3 vPos, float3 vNorm, float fEpsilon = 0.05) { float ao = 0; for (int i = 0; i < AO_STEPS; i++) { float fOffset = i * fEpsilon; float fDist = scene(vPos + vNorm * fOffset).dist; ao += 1/pow(2, i) * (fOffset - fDist); } ao = 1 - AO_STEPS * ao; return ao; } inline fixed4 lightFog(fixed4 col, fixed4 cFog, float fDist, float fStart=16, float fFull=32) { if (fDist < 0) return cFog; return lerp(col, cFog, smoothstep(fStart, fFull, fDist)); } //a light pass for debugging fixed4 lightOnly(float3 vPos, float3 vNorm, float3 vSunDir) { float fLight = lightSun(vNorm, vSunDir, 1); float fAO = lightAO(vPos, vNorm); float fShadow = lightShadow(vPos, vSunDir); return fLight * fAO * fShadow; } ////////////////////////////////////////////////////////////////////// // // Interpolation and Math // ////////////////////////////////////////////////////////////////////// //soft min of a and b with smoothing factor k inline float smin(float a, float b, float k) { float h = max(k - abs(a-b), 0) / k; return min(a, b) - h*h*h*k * 1/6.0; } //soft max of a and b with smoothing factor k inline float smax(float a, float b, float k) { float h = max(k - abs(a - b), 0) / k; return max(a, b) + h*h*h*k * 1/6.0; } //interpolate between the colours of 2 SDFs inline material mixMat(sdfData sdfA, sdfData sdfB) { material m; float fac = clamp(sdfA.dist/(sdfA.dist + sdfB.dist), 0, 1); m.col = lerp(sdfA.mat.col, sdfB.mat.col, fac); m.fRough = lerp(sdfA.mat.fRough, sdfB.mat.fRough, fac); return m; } //interpolate between the colours of 2 SDFs inline material mixMat(material a, material b, float fac) { material m; m.col = lerp(a.col, b.col, fac); m.fRough = lerp(a.fRough, b.fRough, fac); return m; } // from: https://github.com/michaldrobot/ShaderFastLibs/blob/master/ShaderFastMathLib.h // modified to be more "optimized" (WAY worse approximations) static const float fsl_PI = 3.1415926535897932384626433f; static const float fsl_PI_half = fsl_PI/2; inline float acosFast4(float inX) { return 1.57-inX; float x1 = abs(inX); //float x2 = x1 * x1; //float x3 = x2 * x1; float s; s = -0.2121144f * x1 + 1.5707288f; //s = 0.0742610f * x2 + s; //s = -0.0187293f * x3 + s; s = sqrt(1.0f - x1) * s; // acos function mirroring // check per platform if compiles to a selector - no branch neeeded return s; //return inX >= 0.0f ? s : fsl_PI - s; } // polynomial degree 2 // Tune for positive input [0, infinity] and provide output [0, PI/2] inline float ATanPos(float x) { const float C1 = 1.01991; const float C2 = -0.218891; float t0 = (x < 1.0f) ? x : 1.0f / x; float t1 = (C2 * t0 + C1) * t0; // p(x) return t1;//return (x < 1.0f) ? t1: fsl_PI_half - t1; // undo range reduction } // Common function, ATanPos is implemented below // input [-infinity, infinity] and output [-PI/2, PI/2] inline float ATan(float x) { float t0 = ATanPos(abs(x)); return t0;//(x < 0.0f) ? -t0: t0; // undo range reduction } inline float atanFast4(float inX) { //return atan(inX); return ATan(inX); float x = inX; return x*(-0.1784f * abs(x) - 0.0663f * x * x + 1.0301f); } // https://en.wikipedia.org/wiki/Atan2#Definition_and_computation inline float atanFast4_2(float y, float x) { //return sign(x)*sign(x)*atanFast4(y/x)+((1-sign(x))/2)*(1-sign(y)-sign(y)*sign(y))*fsl_PI; return atanFast4(y/x)+(1-sign(x))*(sign(y))*fsl_PI/2; } ////////////////////////////////////////////////////////////////////// // // SDF operations // ////////////////////////////////////////////////////////////////////// //union of SDF A and B sdfData sdfAdd(float3 p, sdfData sA, sdfData sB) { sdfData sC; sC.dist = min(sA.dist, sB.dist); sC.mat = mixMat(sA, sB); return sC; } //union of SDF A and B, with smoothing sdfData sdfAdd(float3 p, sdfData sA, sdfData sB, float fSmooth) { sdfData sC; sC.dist = smin(sA.dist, sB.dist, fSmooth); sC.mat = mixMat(sA, sB); return sC; } //remove the SDF B from A (colour is from A) sdfData sdfSub(float3 p, sdfData sA, sdfData sB) { sdfData sC; sC.dist = max(sA.dist, -sB.dist); sC.mat = sA.mat; return sC; } //remove the SDF B from A (colour is from A), with smoothing sdfData sdfSub(float3 p, sdfData sA, sdfData sB, float fSmooth) { sdfData sC; sC.dist = smax(sA.dist, -sB.dist, fSmooth); sC.mat = sA.mat; return sC; } //intersection of SDF A and B sdfData sdfInter(float3 p, sdfData sA, sdfData sB) { sdfData sC; sC.dist = max(sA.dist, sB.dist); sC.mat = mixMat(sA, sB); return sC; } //intersection of SDF A and B, with smoothing sdfData sdfInter(float3 p, sdfData sA, sdfData sB, float fSmooth) { sdfData sC; sC.dist = smax(sA.dist, sB.dist, fSmooth); sC.mat = mixMat(sA, sB); return sC; } //round edges of an SDF sdfData sdfRound(float3 p, sdfData sdfIn, float fRadius) { sdfData sdfOut = sdfIn; sdfOut.dist -= fRadius; return sdfOut; } ////////////////////////////////////////////////////////////////////// // // SDF shapes // ////////////////////////////////////////////////////////////////////// //create sphere sdfData sdfSphere(float3 p, float fRadius, material mat = DEFMAT) { sdfData sdf; sdf.dist = length(p) - fRadius; sdf.mat = mat; return sdf; } //create plane pointing to positive Y sdfData sdfPlane(float3 p, float fHeight, material mat = DEFMAT) { sdfData sdf; sdf.dist = p.y - fHeight; sdf.mat = mat; return sdf; } //create plane with normal sdfData sdfPlane(float3 p, float3 vNorm, float fHeight, material mat = DEFMAT) { sdfData sdf; sdf.dist = dot(p, normalize(vNorm)) - fHeight; sdf.mat = mat; return sdf; } //create cuboid sdfData sdfBox(float3 p, float3 vDim, material mat = DEFMAT) { sdfData sdf; float3 q = abs(p) - vDim/2.0; sdf.dist = length(max(q, 0)) + min(max(q.x, max(q.y, q.z)), 0); sdf.mat = mat; return sdf; } //create cuboid sdfData sdfBox(float3 p, float3 vDim, float fRound, material mat = DEFMAT) { sdfData sdf; float3 q = abs(p) - vDim/2.0; sdf.dist = length(max(q, 0)) + min(max(q.x, max(q.y, q.z)), 0) - fRound; sdf.mat = mat; return sdf; } //create line segment sdfData sdfLine(float3 p, float3 vStart, float3 vEnd, float fRadius, material mat = DEFMAT) { sdfData sdf; float h = min(1, max(0, dot(p-vStart, vEnd-vStart) / dot(vEnd-vStart, vEnd-vStart))); sdf.dist = length(p-vStart-(vEnd-vStart)*h)-fRadius; sdf.mat = mat; return sdf; } //create cylinder sdfData sdfCylinder(float3 p, float fRadius, float fHeight, material mat = DEFMAT) { sdfData sdf; sdf.dist = max(abs(p.y) - fHeight/2.0, length(p.xz) - fRadius); sdf.mat = mat; return sdf; } //create cylinder sdfData sdfCylinder(float3 p, float fRadius, float fHeight, float fRound, material mat = DEFMAT) { sdfData sdf; sdf.dist = max(abs(p.y) - fHeight/2.0, length(p.xz) - fRadius) - fRound; sdf.mat = mat; return sdf; } //create torus sdfData sdfTorus(float3 p, float fRadius, float fThickness, material mat = DEFMAT) { sdfData sdf; float2 q = float2(length(p.xz) - fRadius, p.y); sdf.dist = length(q) - fThickness; sdf.mat = mat; return sdf; } //triangular prism (BOUND) sdfData sdfTriPrism(float3 p, float fSide, float fDepth, material mat = DEFMAT) { float3 q = abs(p); sdfData sdf; sdf.dist = max(q.z - fDepth, max(q.x * 0.866025 + p.y * 0.5, -p.y) - fSide * 0.5); sdf.mat = mat; return sdf; } ////////////////////////////////////////////////////////////////////// // // Fractals, complex shapes and scenes (frac prefix) // ////////////////////////////////////////////////////////////////////// //TODO: // complex :julia, // simple sierpinsky, menger // Mandelbolb - OPTIMIZED AF, still a fractal but visually diffrent. sdfData fracMandelbolb(float3 p, material mat = DEFMAT) { // http://blog.hvidtfeldts.net/index.php/2011/09/distance-estimated-3d-fractals-v-the-mandelbulb-different-de-approximations/ float3 pos; pos.x = p.x; pos.y = p.y; pos.z = p.z; float dr = 1.0; float r = 0; const int iterations = 4; const float maxRThreshold = 2;//2; const float Power = 16; for (int i = 0; i < iterations; i++) { r = length(p); if (r>maxRThreshold) break; // xyz -> polar //float theta = acos( p.z / r ); float theta = acosFast4( p.z / r ); //float phi = atan2( p.y, p.x ); float phi = atanFast4_2( p.y, p.x ); dr = pow( r, Power-1.0)*Power*dr + 1.0; // transform point float zr = pow( r, Power ); theta = theta * Power; phi = phi * Power; // polar -> xyz p = zr*float3(sin(theta)*cos(phi), sin(phi)*sin(theta), cos(theta)); p += pos; } sdfData sdf; sdf.mat = mat; sdf.dist = 0.5*log(r)*r/dr; return sdf; } // Mandelbulb sdfData fracMandelbulb(float3 p, material mat = DEFMAT) { // http://blog.hvidtfeldts.net/index.php/2011/09/distance-estimated-3d-fractals-v-the-mandelbulb-different-de-approximations/ float3 pos; pos.x = p.x; pos.y = p.y; pos.z = p.z; float dr = 1.0; float r = 0; // Lowest number of iterations without loosing a significant amount of detail // Depends on maxRThreshold //int iterations = 1; //int iterations = 8; const int iterations = 5; //float maxRThreshold = 2; const float maxRThreshold = 2; // Z_(n+1) = Z(n)^? // float Power = 8 + 6 * sin(_Time.x); float Power = 8; for (int i = 0; i < iterations; i++) { r = length(p); if (r>maxRThreshold) break; // xyz -> polar float theta = acos( p.z / r ); float phi = atan2( p.y, p.x ); dr = pow( r, Power-1.0)*Power*dr + 1.0; // transform point float zr = pow( r, Power ); theta = theta * Power; phi = phi * Power; // polar -> xyz p = zr*float3(sin(theta)*cos(phi), sin(phi)*sin(theta), cos(theta)); p += pos; } sdfData sdf; sdf.mat = mat; //sdf.mat.col.y = sin(p.x); //sdf.dist = sdfSphere(pos, 10).dist; //sdf.mat = mat; sdf.dist = 0.5*log(r)*r/dr; //sdf.mat = mat; return sdf; } void sphereFold(inout float3 p, inout float dz, float minRadius, float fixedRadius); void boxFold(inout float3 p, float dz, float foldingLimit); // Mandelbox sdfData fracMandelbox(float3 p, float scaleFactor, material mat = DEFMAT) { // http://blog.hvidtfeldts.net/index.php/2011/11/distance-estimated-3d-fractals-vi-the-mandelbox/ float3 offset = p; float dr = 0; // Parameters int iterations = 8;//20;//14; //scaleFactor = -2 + (_SinTime.x*4+2); float fixedRadius = 1.0; float minRadius = 0.5; /*float foldingLimit = 0.2 + _SinTime.x/4 + 0.25; float minRadius = 0.07; float fixedRadius = 0.2;*/ //float scaleFactor = -0.8; /*float foldingLimit = _FoldingLimit; float minRadius = _MinRadius; float fixedRadius = _FixedRadius;*/ for(int i=0; i 1.0) p.x = 2.0 - p.x; else if (p.x < -1.0) p.x = -2.0 - p.x; if (p.y > 1.0) p.y = 2.0 - p.y; else if (p.y < -1.0) p.y = -2.0 - p.y; if (p.z > 1.0) p.z = 2.0 - p.z; else if (p.z < -1.0) p.z = -2.0 - p.z; // radius squared float r2 = dot(p,p); if (r2 < mR2) { p*=(fR2/mR2); DEfactor*=(fR2/mR2); } else if (r2 < fR2) { p*=(fR2/r2); DEfactor*=(fR2/r2); } p=p*scale+1; DEfactor*=scale; } sdfData sdf; sdf.mat = mat; sdf.dist = length(p)/abs(DEfactor); return sdf; } // Feather sdfData fracFeather(float3 p, float cx = 2.0, float cy = 2.7, float cz = 1.4, material mat=DEFMAT) { // https://fractalforums.org/index.php?action=gallery;sa=view;id=5732 int iterations = 5; //float cx = 2.0; //float cy = 2.7; //float cz = 1.4; float cw = 0.1; float dx = 1.5;// + _FoldingLimit-0.5; float lp,r2,s = 1; float icy = 1.0 / cy; float3 p2,cy3 = float3(cy,cy,cy); for (int i=0; i