RayMarching OpenGl



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;
}
  • p is the point in space.
  • radius is the radius of the sphere.
  • length(p) calculates the Euclidean distance from the point p to the origin (center of the sphere).
  • Subtracting radius adjusts this distance to represent the signed distance.
  • If p is outside the sphere, length(p)-radius is 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: e is a small vector used for numerical differentiation. It has two components: 1.0 and -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 p by a small offset and evaluates the signed distance function sdScene at these perturbed positions.
  • Evaluate Signed Distance Function:For each perturbed position, the signed distance function sdScene is 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

REFERENCES

Leave a comment