Ray Marching: Surface (SDF) vs. Volumetric — with Sample GLSL

Ray Marching: Surface (SDF) vs. Volumetric — with Sample GLSL

Ray Marching: Surface (SDF) vs. Volumetric

1) Surface (SDF) Ray Marching

  • Uses a Signed Distance Function (SDF) d(p) giving distance to the nearest surface.
  • Sphere tracing: advance the ray by the current distance; when close enough, compute the normal and shade.
  • Notable: works by Iñigo Quílez (iq); common shapes include boxes, spheres, CSG, fractals.
// --- Minimal SDF sphere tracing (Shadertoy-style fragment) ---
// iTime, iResolution are assumed uniforms; write result to fragColor.

// Signed distance to a sphere of radius r at origin
float sdSphere(vec3 p, float r){ return length(p) - r; }

// Distance field (add more primitives/ops as you like)
float mapSDF(vec3 p){
  return sdSphere(p, 1.0);
}

// Estimate normal via central differences
vec3 calcNormal(vec3 p){
  const vec2 e = vec2(1e-3, 0.0);
  return normalize(vec3(
    mapSDF(p + e.xyy) - mapSDF(p - e.xyy),
    mapSDF(p + e.yxy) - mapSDF(p - e.yxy),
    mapSDF(p + e.yyx) - mapSDF(p - e.yyx)
  ));
}

// Sphere tracing: returns hit distance or -1.0; also outputs hit position
float raymarchSDF(vec3 ro, vec3 rd, out vec3 hitPos){
  float t = 0.0;
  for(int i=0;i<120;i++){
    hitPos = ro + rd * t;
    float d = mapSDF(hitPos);
    if(d < 1e-3) return t;     // hit
    t += d;                    // distance-based jump
    if(t > 50.0) break;        // miss
  }
  return -1.0;
}

// Simple Blinn-Phong shading
vec3 shadeSDF(vec3 p, vec3 rd){
  vec3 n = calcNormal(p);
  vec3 lp = vec3(2.5, 2.0, 2.0);      // light position
  vec3 l  = normalize(lp - p);
  float diff = max(dot(n,l), 0.0);
  float spec = pow(max(dot(reflect(-l,n), -rd), 0.0), 32.0);
  return vec3(0.05) + diff*vec3(1.0) + 0.25*spec;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord){
  vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

  // Camera setup
  vec3 ro = vec3(0.0, 0.0, -3.0);
  vec3 rd = normalize(vec3(uv, 1.5));

  vec3 col = vec3(0.02, 0.03, 0.05);  // background
  vec3 p;
  float t = raymarchSDF(ro, rd, p);
  if(t > 0.0){
    col = shadeSDF(p, rd);
  }

  fragColor = vec4(col, 1.0);
}

Tip: If you see artifacts, clamp step size near surfaces (e.g., t += clamp(d, 1e-4, 1.0)) and cap max steps.

2) Volumetric Ray Marching

  • Integrates a density field across space (fog, clouds, emissive gas, light shafts) instead of hitting a surface.
  • Accumulates emission and updates transmittance along the ray (Beer–Lambert law).
  • Typical: emissive glows (nebulae, “magic”), fog/smoke/flames; add scattering for god-rays.
// --- Minimal Volumetric Emission + Absorption (Shadertoy-style) ---
// iTime, iResolution assumed; write result to fragColor.

// Smooth density field (nebula-like)
float density(vec3 p){
  // center the glow a bit in front of the camera
  float r = length(p - vec3(0.0, 0.0, 3.0));
  float base = exp(-0.6 * r);  // radial falloff
  float warp = 0.5 + 0.5 * sin(5.0*p.x + iTime) * cos(5.0*p.y - iTime);
  return base * warp;          // unitless
}

// March the volume using front-to-back compositing
vec3 raymarchVolume(vec3 ro, vec3 rd){
  const float stepLen = 0.05;    // fixed step length
  const float tMax    = 20.0;
  const float sigma_t = 2.0;     // extinction coefficient
  const float emitK   = 3.0;     // emission gain

  float t = 0.0;
  float T = 1.0;                 // accumulated transmittance
  vec3  L = vec3(0.0);           // accumulated radiance

  // jitter to reduce banding (per-pixel)
  float seed = dot(floor(gl_FragCoord.xy), vec2(12.9898,78.233));
  t += fract(sin(seed)*43758.5453) * stepLen;

  for(int i=0;i<600;i++){
    if(t > tMax || T < 1e-3) break;
    vec3  p    = ro + rd * t;
    float rho  = density(p);
    float ext  = sigma_t * rho;
    float a    = 1.0 - exp(-ext * stepLen);      // alpha from Beer–Lambert
    vec3  emiss = emitK * rho * vec3(1.0, 0.9, 1.2); // tinted glow

    L += T * emiss * a;                           // accumulate light
    T *= exp(-ext * stepLen);                     // update transmittance
    t += stepLen;
  }
  return L;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord){
  vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

  // Camera
  vec3 ro = vec3(0.0, 0.0, -3.0);
  vec3 rd = normalize(vec3(uv, 1.5));

  vec3 col = raymarchVolume(ro, rd);

  // Optional tone mapping / gamma
  col = col / (1.0 + col);
  col = pow(col, vec3(1.0/2.2));

  fragColor = vec4(col, 1.0);
}

To add light shafts (single scattering), at each step sample a short shadow ray toward a light to estimate T_light, then add L += T * (sigma_s * rho * phase * Li * T_light) * stepLen.

How to use

  • These are Shadertoy-style fragments: paste each into a fragment tab there, or adapt to your engine (WebGL/GLSL).
  • Uniforms expected: iTime (seconds), iResolution (viewport size).
  • Mixing solids + volumes: march to the first surface distance and also composite volume in front using a smaller fixed step.

Comments

Popular posts from this blog

Japan Jazz Anthology Select: Jazz of the SP Era

In practice, the most workable approach is to measure a composite “civility score” built from multiple indicators.