Ray marching is a technique in computer graphics used to render 3D scenes. It works by sending rays from a camera into a scene and stepping along the ray to find the surfaces of objects, using signed distance functions (SDFs) to determine the closest point. I first explored this technique through Shadertoy, using GLSL shaders. It’s a powerful and efficient way to create detailed 3D visuals with relatively simple code. Here’s a breakdown of how it works
A signed distance function (SDF) returns the shortest distance from any point in space to the surface of a shape. Function returns a positive distance if the point is outside the shape, a negative distance if the point is inside the shape, and zero if the point is exactly on the surface. REF prrimitive shapes.
float sdSphere(vec3 p, float - radius) {
return length(p) -radius;
}
pis the point in space.radiusis the radius of the sphere.length(p)calculates the Euclidean distance from the pointpto the origin (center of the sphere).- Subtracting
radiusadjusts this distance to represent the signed distance. - If
pis outside the sphere,length(p)-radiusis positive.
Here is final primitive generation script, A transformation matrix that can be used to apply rotations or other transformations to the sphere. Surface struct returns signed distance from a point p & color of the object. These functions define various objects in the scene, each returning a Surface struct with the distance from a point to the object and its color:
Surface sdSphere(vec3 p, float radius , vec3 offset , vec3 col, mat3 transform ) {
p = (p - offset) * transform;
float d = length(p) - radius;
return Surface(d, col);
}
Union operation that combines two SDFs. it takes two vec2 inputs, each representing a distance and an associated ID, and returns the one with the smaller distance value. This is used to determine which of the two objects is closer to a given point.
vec2 opU(vec2 d1, vec2 d2) {
return (d1.x < d2.x) ? d1 : d2;
}
Calculate Normal
vec3 calcNormal(in vec3 p) {
vec2 e = vec2(1, -1) * EPSILON;
return normalize(
e.xyy * sdScene(p + e.xyy).sd +
e.yyx * sdScene(p + e.yyx).sd +
e.yxy * sdScene(p + e.yxy).sd +
e.xxx * sdScene(p + e.xxx).sd
);
}
- Define Epsilon Vector:
eis a small vector used for numerical differentiation. It has two components:1.0and-1.0, scaled by a small value (0.0005). This small value (0.0005) is chosen to provide a small offset for the differentiation process, ensuring a precise approximation. - Calculate Perturbed Positions: For each of the following terms, the function perturbs the position
pby a small offset and evaluates the signed distance functionsdSceneat these perturbed positions. - Evaluate Signed Distance Function:For each perturbed position, the signed distance function
sdSceneis evaluated, giving the distance from the perturbed point to the surface.
Surface Struct and Utility Functions
This struct represents a surface with a signed distance and a color, and the utility functions work with these surfaces:
struct Surface {
float sd; // Signed distance
vec3 col; // Color
};
Surface minSurface(Surface objA, Surface objB) {
if (objB.sd < objA.sd) return objB;
return objA;
}
Ray Marching
These functions perform ray marching at a point on a surface: This method ensures that the ray stops as soon as it detects an intersection
Surface rayMarch(vec3 ro, vec3 rd, float start, float end) {
float depth = start;
Surface co; // closest object
//The function starts from a given point (start) along a ray (ro + rd)
//and moves forward incrementally (depth += co.sd)
for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
vec3 p = ro + depth * rd;
//At each step, it evaluates the distance to the nearest surface.
co = sdScene(p);
depth += co.sd;
//If it gets very close to a surface or exceeds the maximum distance it stops.
if (co.sd < PRECISION || depth > end) break;
}
//The final Surface struct (co) contains the distance from the ray origin
//to the closest surface and the color of the surface at that intersection point.
co.sd = depth;
return co;
}
Main Image Function, This function renders the scene by setting up the camera, performing ray marching, and calculating the color at each pixel:
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
//Normalize Fragment Coordinates:
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
vec3 backgroundColor = vec3(0.835, 1, 1);
vec3 col = vec3(0);
//Calculate Camera Position& matrix
vec3 lookAtPos = vec3(0, 0.5, 0);
vec3 ro = cameraLookatPosition(.5, 5., lookAtPos);
mat3 cam = cameraLookat(ro, lookAtPos);
//Calculate Ray Direction:
vec3 rd = cam * normalize(vec3(uv, -1));
// Perform ray marching
Surface co = rayMarch(ro, rd, MIN_DIST, MAX_DIST);
if (co.sd > MAX_DIST) {
col = backgroundColor; // Ray didn't hit anything
} else {
vec3 p = ro + rd * co.sd; // Point on the object we discovered from ray marching
vec3 normal = calcNormal(p);//Calculate Surface Normal:
vec3 lightPosition = vec3(2, .5, 7);//Calculate Light Direction:
vec3 lightDirection = normalize(lightPosition - p);//Calculate Diffuse Lighting:
float dif = clamp(dot(normal, lightDirection), 0.02, 1.0); // Diffuse reflection
col = dif * co.col + backgroundColor * 0.2; // Add a bit of background color to the diffuse color
}
fragColor = vec4(col, 1.0);//Output the Color:
}
Final Shader : https://www.shadertoy.com/view/4cVczW
