Skip to content

Instantly share code, notes, and snippets.

@erichlof
Created February 20, 2025 21:22
Show Gist options
  • Save erichlof/f309a059d79d8e58ad6d6e430948bfe1 to your computer and use it in GitHub Desktop.
Save erichlof/f309a059d79d8e58ad6d6e430948bfe1 to your computer and use it in GitHub Desktop.
shader (glsl) file for experimental sphere BVH (as opposed to typical AABB BVH)
precision highp float;
precision highp int;
precision highp sampler2D;
#include <pathtracing_uniforms_and_defines>
uniform sampler2D tTriangleTexture;
uniform sampler2D tAABBTexture;
uniform sampler2D tAlbedoTextures[8]; // 8 = max number of diffuse albedo textures per model
//float InvTextureWidth = 0.000244140625; // (1 / 4096 texture width)
//float InvTextureWidth = 0.00048828125; // (1 / 2048 texture width)
//float InvTextureWidth = 0.0009765625; // (1 / 1024 texture width)
#define INV_TEXTURE_WIDTH 0.000244140625
#define N_SPHERES 3
#define N_BOXES 2
//-----------------------------------------------------------------------
vec3 rayOrigin, rayDirection;
// recorded intersection data:
vec3 hitNormal, hitEmission, hitColor;
vec2 hitUV;
float hitObjectID = -INFINITY;
int hitType = -100;
struct Sphere { float radius; vec3 position; vec3 emission; vec3 color; int type; };
struct Box { vec3 minCorner; vec3 maxCorner; vec3 emission; vec3 color; int type; };
Sphere spheres[N_SPHERES];
Box boxes[N_BOXES];
#include <pathtracing_random_functions>
#include <pathtracing_calc_fresnel_reflectance>
#include <pathtracing_sphere_intersect>
#include <pathtracing_box_intersect>
#include <pathtracing_bvhTriangle_intersect>
#include <pathtracing_sample_sphere_light>
vec2 stackLevels[32];
void GetSphereNodeData(const in float i, inout vec4 sphereNodeData)
{
// each bounding sphere node's data is encoded in only 1 rgba(or xyzw) texture slot
ivec2 uv = ivec2( mod(i, 4096.0), i * INV_TEXTURE_WIDTH );
sphereNodeData = texelFetch(tAABBTexture, uv, 0);
}
//--------------------------------------------------------------------------------------------------------------
float SceneIntersect( out int isRayExiting )
//--------------------------------------------------------------------------------------------------------------
{
vec4 currentSphereNodeData, nodeAData, nodeBData, tmpNodeData;
vec4 vd0, vd1, vd2, vd3, vd4, vd5, vd6, vd7;
vec3 normal;
vec3 hitPos;
vec2 currentStackData, stackDataA, stackDataB, tmpStackData;
ivec2 uv0, uv1, uv2, uv3, uv4, uv5, uv6, uv7;
float d;
float t = INFINITY;
float q;
float stackptr = 0.0;
float id = 0.0;
float tu, tv;
float triangleID = 0.0;
float triangleU = 0.0;
float triangleV = 0.0;
float triangleW = 0.0;
int objectCount = 0;
hitObjectID = -INFINITY;
int skip = FALSE;
int triangleLookupNeeded = FALSE;
GetSphereNodeData(stackptr, currentSphereNodeData);
currentStackData = vec2(stackptr, SphereIntersect(fract(abs(currentSphereNodeData.x)) * 1000.0, currentSphereNodeData.yzw, rayOrigin, rayDirection));
stackLevels[0] = currentStackData;
skip = (currentStackData.y < t) ? TRUE : FALSE;
while (true)
{
if (skip == FALSE)
{
// decrease pointer by 1 (0.0 is root level, 27.0 is maximum depth)
if (--stackptr < 0.0) // went past the root level, terminate loop
break;
currentStackData = stackLevels[int(stackptr)];
if (currentStackData.y >= t)
continue;
GetSphereNodeData(floor(abs(currentStackData.x)), currentSphereNodeData);
}
skip = FALSE; // reset skip
if (currentSphereNodeData.x < 0.0) // leafOrChild_ID < 0.0 signifies an inner node
{
GetSphereNodeData(floor(abs(currentSphereNodeData.x)), nodeAData);
GetSphereNodeData(floor(abs(currentSphereNodeData.x)) + 1.0, nodeBData);
stackDataA = vec2(currentSphereNodeData.x, SphereIntersect( fract(abs(nodeAData.x)) * 1000.0, nodeAData.yzw, rayOrigin, rayDirection ));
stackDataB = vec2(currentSphereNodeData.x - 1.0, SphereIntersect( fract(abs(nodeBData.x)) * 1000.0, nodeBData.yzw, rayOrigin, rayDirection ));
/* d = SphereIntersect(fract(abs(currentSphereNodeData.x)) * 1000.0, currentSphereNodeData.yzw, rayOrigin, rayDirection);
if (stackptr == 0.0 && d < t)
{
t = d;
hitNormal = (rayOrigin + rayDirection * t) - currentSphereNodeData.yzw;
hitEmission = vec3(0);
hitColor = vec3(1, 0, 1);
hitType = REFR;
hitObjectID = 2.0;
//break;
} */
/* d = SphereIntersect(fract(abs(nodeAData.x)) * 1000.0, nodeAData.yzw, rayOrigin, rayDirection);
if (stackptr == 1.0 && d < t)
{
t = d;
hitNormal = (rayOrigin + rayDirection * t) - nodeAData.yzw;
hitEmission = vec3(0);
hitColor = vec3(1, 0, 1);
hitType = DIFF;
hitObjectID = 2.0;
//break;
}
d = SphereIntersect(fract(abs(nodeBData.x)) * 1000.0, nodeBData.yzw, rayOrigin, rayDirection);
if (stackptr == 1.0 && d < t)
{
t = d;
hitNormal = (rayOrigin + rayDirection * t) - nodeBData.yzw;
hitEmission = vec3(0);
hitColor = vec3(1, 1, 0);
hitType = DIFF;
hitObjectID = 2.0;
//break;
} */
// first sort the branch node data so that 'a' is the smallest
if (stackDataB.y < stackDataA.y)
{
tmpStackData = stackDataB;
stackDataB = stackDataA;
stackDataA = tmpStackData;
tmpNodeData = nodeBData;
nodeBData = nodeAData;
nodeAData = tmpNodeData;
} // branch 'b' now has the larger rayT value of 'a' and 'b'
if (stackDataB.y < INFINITY) // see if branch 'b' (the larger rayT) needs to be processed
{
currentStackData = stackDataB;
currentSphereNodeData = nodeBData;
skip = TRUE; // this will prevent the stackptr from decreasing by 1
}
if (stackDataA.y < INFINITY) // see if branch 'a' (the smaller rayT) needs to be processed
{
if (skip == TRUE) // if larger branch 'b' needed to be processed also,
stackLevels[int(stackptr++)] = stackDataB; // cue larger branch 'b' for future round
// also, increase pointer by 1
currentStackData = stackDataA;
currentSphereNodeData = nodeAData;
skip = TRUE; // this will prevent the stackptr from decreasing by 1
}
continue;
} // end if (currentSphereNodeData.x < 0.0) // inner node
// else this is a leaf
// each triangle's data is encoded in 8 rgba(or xyzw) texture slots
id = 8.0 * floor(currentSphereNodeData.x);
uv0 = ivec2( mod(id + 0.0, 4096.0), (id + 0.0) * INV_TEXTURE_WIDTH );
uv1 = ivec2( mod(id + 1.0, 4096.0), (id + 1.0) * INV_TEXTURE_WIDTH );
uv2 = ivec2( mod(id + 2.0, 4096.0), (id + 2.0) * INV_TEXTURE_WIDTH );
vd0 = texelFetch(tTriangleTexture, uv0, 0);
vd1 = texelFetch(tTriangleTexture, uv1, 0);
vd2 = texelFetch(tTriangleTexture, uv2, 0);
d = BVH_TriangleIntersect( vec3(vd0.xyz), vec3(vd0.w, vd1.xy), vec3(vd1.zw, vd2.x), rayOrigin, rayDirection, tu, tv );
if (d < t)
{
t = d;
triangleID = id;
triangleU = tu;
triangleV = tv;
triangleLookupNeeded = TRUE;
}
} // end while (TRUE)
if (triangleLookupNeeded == TRUE)
{
uv0 = ivec2( mod(triangleID + 0.0, 4096.0), (triangleID + 0.0) * INV_TEXTURE_WIDTH );
uv1 = ivec2( mod(triangleID + 1.0, 4096.0), (triangleID + 1.0) * INV_TEXTURE_WIDTH );
uv2 = ivec2( mod(triangleID + 2.0, 4096.0), (triangleID + 2.0) * INV_TEXTURE_WIDTH );
uv3 = ivec2( mod(triangleID + 3.0, 4096.0), (triangleID + 3.0) * INV_TEXTURE_WIDTH );
uv4 = ivec2( mod(triangleID + 4.0, 4096.0), (triangleID + 4.0) * INV_TEXTURE_WIDTH );
uv5 = ivec2( mod(triangleID + 5.0, 4096.0), (triangleID + 5.0) * INV_TEXTURE_WIDTH );
uv6 = ivec2( mod(triangleID + 6.0, 4096.0), (triangleID + 6.0) * INV_TEXTURE_WIDTH );
uv7 = ivec2( mod(triangleID + 7.0, 4096.0), (triangleID + 7.0) * INV_TEXTURE_WIDTH );
vd0 = texelFetch(tTriangleTexture, uv0, 0);
vd1 = texelFetch(tTriangleTexture, uv1, 0);
vd2 = texelFetch(tTriangleTexture, uv2, 0);
vd3 = texelFetch(tTriangleTexture, uv3, 0);
vd4 = texelFetch(tTriangleTexture, uv4, 0);
vd5 = texelFetch(tTriangleTexture, uv5, 0);
vd6 = texelFetch(tTriangleTexture, uv6, 0);
vd7 = texelFetch(tTriangleTexture, uv7, 0);
// face normal for flat-shaded polygon look
//hitNormal = cross(vec3(vd0.w, vd1.xy) - vec3(vd0.xyz), vec3(vd1.zw, vd2.x) - vec3(vd0.xyz));
// interpolated normal using triangle intersection's uv's
triangleW = 1.0 - triangleU - triangleV;
hitNormal = (triangleW * vec3(vd2.yzw)) + (triangleU * vec3(vd3.xyz)) + (triangleV * vec3(vd3.w, vd4.xy));
hitEmission = vec3(1, 0, 1); // use this if hitType will be LIGHT
hitColor = vd6.yzw;
hitUV = triangleW * vec2(vd4.zw) + triangleU * vec2(vd5.xy) + triangleV * vec2(vd5.zw);
//hitType = int(vd6.x);
//hitAlbedoTextureID = int(vd7.x);
hitType = COAT;
hitObjectID = float(objectCount);
}
objectCount++;
d = SphereIntersect( spheres[0].radius, spheres[0].position, rayOrigin, rayDirection );
if (d < t)
{
t = d;
hitNormal = (rayOrigin + rayDirection * t) - spheres[0].position;
hitEmission = spheres[0].emission;
hitColor = spheres[0].color;
hitType = spheres[0].type;
hitObjectID = float(objectCount);
}
objectCount++;
d = SphereIntersect( spheres[1].radius, spheres[1].position, rayOrigin, rayDirection );
if (d < t)
{
t = d;
hitNormal = (rayOrigin + rayDirection * t) - spheres[1].position;
hitEmission = spheres[1].emission;
hitColor = spheres[1].color;
hitType = spheres[1].type;
hitObjectID = float(objectCount);
}
objectCount++;
d = SphereIntersect( spheres[2].radius, spheres[2].position, rayOrigin, rayDirection );
if (d < t)
{
t = d;
hitPos = rayOrigin + (rayDirection * t);
hitNormal = hitPos - spheres[2].position;
hitEmission = spheres[2].emission;
q = clamp( mod( dot( floor(hitPos.xz * 0.04), vec2(1.0) ), 2.0 ) , 0.0, 1.0 );
hitColor = mix(vec3(0.5), spheres[2].color, q);
hitType = spheres[2].type;
hitObjectID = float(objectCount);
}
objectCount++;
d = BoxIntersect( boxes[0].minCorner, boxes[0].maxCorner, rayOrigin, rayDirection, normal, isRayExiting );
if (d < t)
{
t = d;
hitNormal = normal;
hitEmission = boxes[0].emission;
hitColor = boxes[0].color;
hitType = boxes[0].type;
hitObjectID = float(objectCount);
}
objectCount++;
d = BoxIntersect( boxes[1].minCorner, boxes[1].maxCorner, rayOrigin, rayDirection, normal, isRayExiting );
if (d < t)
{
t = d;
hitNormal = normal;
hitEmission = boxes[1].emission;
hitColor = boxes[1].color;
hitType = boxes[1].type;
hitObjectID = float(objectCount);
}
return t;
} // end float SceneIntersect( out int isRayExiting )
//----------------------------------------------------------------------------------------------------------------------------------------------------
vec3 CalculateRadiance( out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness )
//----------------------------------------------------------------------------------------------------------------------------------------------------
{
Sphere light = spheres[1];
vec3 accumCol = vec3(0);
vec3 mask = vec3(1);
vec3 reflectionMask = vec3(1);
vec3 reflectionRayOrigin = vec3(0);
vec3 reflectionRayDirection = vec3(0);
vec3 dirToLight;
vec3 x, n, nl;
float t;
float nc, nt, ratioIoR, Re, Tr;
float weight;
float thickness = 0.1;
float previousObjectID;
int reflectionBounces = -1;
int diffuseCount = 0;
int previousIntersecType = -100;
hitType = -100;
int bounceIsSpecular = TRUE;
int sampleLight = FALSE;
int isRayExiting = FALSE;
int willNeedReflectionRay = FALSE;
int isReflectionTime = FALSE;
int reflectionNeedsToBeSharp = FALSE;
for (int bounces = 0; bounces < 7; bounces++)
{
if (isReflectionTime == TRUE)
reflectionBounces++;
previousIntersecType = hitType;
previousObjectID = hitObjectID;
t = SceneIntersect(isRayExiting);
// shouldn't happen because we are inside a huge sphere, but just in case
if (t == INFINITY)
{
break;
}
// large sphere light surrounding the scene
if (hitType == LIGHT)
{
// this makes the object edges sharp against the background
if (bounces == 0 || (bounces == 1 && previousIntersecType == SPEC))
pixelSharpness = 1.01;
accumCol += mask * hitEmission;
if (willNeedReflectionRay == TRUE)
{
mask = reflectionMask;
rayOrigin = reflectionRayOrigin;
rayDirection = reflectionRayDirection;
diffuseCount = 0;
willNeedReflectionRay = FALSE;
bounceIsSpecular = TRUE;
sampleLight = FALSE;
isReflectionTime = TRUE;
continue;
}
// reached a light, so we can exit
break;
}
// useful data
n = normalize(hitNormal);
nl = dot(n, rayDirection) < 0.0 ? n : -n;
x = rayOrigin + rayDirection * t;
if (bounces == 0)
{
objectID = hitObjectID;
}
if (isReflectionTime == FALSE && diffuseCount == 0 && hitObjectID != previousObjectID)
{
objectNormal = nl;
objectColor = hitColor;
}
if (reflectionNeedsToBeSharp == TRUE && reflectionBounces == 0)
{
objectNormal = nl;
objectColor = hitColor;
}
if (hitType == POINT_LIGHT)
{
if (diffuseCount == 0 && isReflectionTime == FALSE)
pixelSharpness = 1.0;
if (isReflectionTime == TRUE && bounceIsSpecular == TRUE)
{
objectNormal = nl;
//objectColor = hitColor;
objectID = hitObjectID;
}
if (bounceIsSpecular == TRUE)
{
if (bounces == 0) // looking directly at light
accumCol += mask * clamp(hitEmission, 0.0, 5.0);
else //if (bounces < 3) // bounce reflection or refraction
accumCol += mask * clamp(hitEmission, 0.0, 100.0);
}
if (sampleLight == TRUE)
accumCol += mask * hitEmission;
if (willNeedReflectionRay == TRUE)
{
mask = reflectionMask;
rayOrigin = reflectionRayOrigin;
rayDirection = reflectionRayDirection;
diffuseCount = 0;
willNeedReflectionRay = FALSE;
bounceIsSpecular = TRUE;
sampleLight = FALSE;
isReflectionTime = TRUE;
continue;
}
// reached a light, so we can exit
break;
}
// if we get here and sampleLight is still TRUE, shadow ray failed to find the light source
// the ray hit an occluding object along its way to the light
if (sampleLight == TRUE)
{
if (willNeedReflectionRay == TRUE)
{
mask = reflectionMask;
rayOrigin = reflectionRayOrigin;
rayDirection = reflectionRayDirection;
diffuseCount = 0;
willNeedReflectionRay = FALSE;
bounceIsSpecular = TRUE;
sampleLight = FALSE;
isReflectionTime = TRUE;
continue;
}
break;
}
if (hitType == DIFF) // Ideal DIFFUSE reflection
{
diffuseCount++;
mask *= hitColor;
bounceIsSpecular = FALSE;
if (diffuseCount == 1 && rand() < 0.5)
{
mask *= 2.0;
// choose random Diffuse sample vector
rayDirection = randomCosWeightedDirectionInHemisphere(nl);
rayOrigin = x + nl * uEPS_intersect;
continue;
}
dirToLight = sampleSphereLight(x, nl, light, weight);
mask *= diffuseCount == 1 ? 2.0 : 1.0;
mask *= weight;
rayDirection = dirToLight;
rayOrigin = x + nl * uEPS_intersect;
sampleLight = TRUE;
continue;
} // end if (hitType == DIFF)
if (hitType == SPEC) // Ideal SPECULAR reflection
{
mask *= hitColor;
rayDirection = reflect(rayDirection, nl);
rayOrigin = x + nl * uEPS_intersect;
//bounceIsSpecular = TRUE; // turn on mirror caustics
continue;
}
if (hitType == REFR) // Ideal dielectric REFRACTION
{
nc = 1.0; // IOR of Air
nt = 1.5; // IOR of common Glass
Re = calcFresnelReflectance(rayDirection, n, nc, nt, ratioIoR);
Tr = 1.0 - Re;
if (Re == 1.0)
{
rayDirection = reflect(rayDirection, nl);
rayOrigin = x + nl * uEPS_intersect;
continue;
}
if (diffuseCount == 0 && hitObjectID != previousObjectID && n == nl)
{
reflectionMask = mask * Re;
reflectionRayDirection = reflect(rayDirection, nl); // reflect ray from surface
reflectionRayOrigin = x + nl * uEPS_intersect;
willNeedReflectionRay = TRUE;
if (bounces == 0 && hitColor == vec3(0.2,0.9,0.7) && isRayExiting == FALSE)
reflectionNeedsToBeSharp = TRUE;
}
// transmit ray through surface
// is ray leaving a solid object from the inside?
// If so, attenuate ray color with object color by how far ray has travelled through the medium
if (isRayExiting == TRUE)
{
mask *= exp(log(hitColor) * thickness * t);
}
mask *= Tr;
rayDirection = refract(rayDirection, nl, ratioIoR);
rayOrigin = x - nl * uEPS_intersect;
if (bounces == 1)
bounceIsSpecular = TRUE; // turn on refracting caustics
continue;
} // end if (hitType == REFR)
if (hitType == COAT) // Diffuse object underneath with ClearCoat on top
{
nc = 1.0; // IOR of Air
nt = 1.5; // IOR of Clear Coat
Re = calcFresnelReflectance(rayDirection, nl, nc, nt, ratioIoR);
Tr = 1.0 - Re;
if (diffuseCount == 0 && hitObjectID != previousObjectID)
{
reflectionMask = mask * Re;
reflectionRayDirection = reflect(rayDirection, nl); // reflect ray from surface
reflectionRayOrigin = x + nl * uEPS_intersect;
willNeedReflectionRay = TRUE;
}
diffuseCount++;
mask *= Tr;
mask *= hitColor;
bounceIsSpecular = FALSE;
if (diffuseCount == 1 && rand() < 0.5)
{
mask *= 2.0;
// choose random Diffuse sample vector
rayDirection = randomCosWeightedDirectionInHemisphere(nl);
rayOrigin = x + nl * uEPS_intersect;
continue;
}
dirToLight = sampleSphereLight(x, nl, light, weight);
mask *= diffuseCount == 1 ? 2.0 : 1.0;
mask *= weight;
rayDirection = dirToLight;
rayOrigin = x + nl * uEPS_intersect;
sampleLight = TRUE;
continue;
} //end if (hitType == COAT)
} // end for (int bounces = 0; bounces < 7; bounces++)
return max(vec3(0), accumCol);
} // end vec3 CalculateRadiance( out vec3 objectNormal, out vec3 objectColor, out float objectID, out float pixelSharpness )
//-----------------------------------------------------------------------
void SetupScene(void)
//-----------------------------------------------------------------------
{
vec3 z = vec3(0);
vec3 L1 = vec3(0.5, 0.7, 1.0) * 1.0;//0.01;// Blueish sky light
vec3 L2 = vec3(1.0, 0.9, 0.8) * 1000.0;// Bright white light bulb
spheres[0] = Sphere( 10000.0, vec3(0, 0, 0), L1, z, LIGHT);//large spherical sky light
spheres[1] = Sphere( 0.5, vec3(-10, 35, -10), L2, z, POINT_LIGHT);//small spherical point light
spheres[2] = Sphere( 4000.0, vec3(0, -4000, 0), z, vec3(1.0, 1.0, 1.0), DIFF);//Checkered Floor
boxes[0] = Box( vec3(-20.0, 11.0, -110.0), vec3(70.0, 18.0, -20.0), z, vec3(0.2, 0.9, 0.7), REFR);//Glass Box
boxes[1] = Box( vec3(-14.0, 13.0, -104.0), vec3(64.0, 16.0, -26.0), z, vec3(0, 0, 0), DIFF);//Inner Box
}
#include <pathtracing_main>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment