All Lessons
Lesson 9

Ray Marching and SDFs

Code

const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');

const vsSource = `
  attribute vec2 aPosition;
  varying highp vec2 vUV;
  
  void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
    vUV = aPosition;
  }
`;

const fsSource = `
  varying highp vec2 vUV;
  uniform highp float uTime;
  
  highp float sdSphere(vec3 p, float r) {
    return length(p) - r;
  }
  
  highp float sdBox(vec3 p, vec3 b) {
    vec3 d = abs(p) - b;
    return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
  }
  
  highp float smin(float a, float b, float k) {
    float h = max(k - abs(a - b), 0.0) / k;
    return min(a, b) - h * h * k * 0.25;
  }
  
  highp float map(vec3 p) {
    float t = uTime * 0.5;
    vec3 p1 = p + vec3(sin(t) * 0.3, 0.0, 0.0);
    vec3 p2 = p - vec3(sin(t) * 0.3, 0.0, 0.0);
    
    float sphere = sdSphere(p1, 0.5);
    float box = sdBox(p2, vec3(0.4));
    
    return smin(sphere, box, 0.5);
  }
  
  highp vec3 calcNormal(vec3 p) {
    const float h = 0.001;
    const vec2 k = vec2(1, -1);
    return normalize(
      k.xyy * map(p + k.xyy * h) +
      k.yyx * map(p + k.yyx * h) +
      k.yxy * map(p + k.yxy * h) +
      k.xxx * map(p + k.xxx * h)
    );
  }
  
  void main() {
    vec3 ro = vec3(0.0, 0.0, 2.5);
    vec3 rd = normalize(vec3(vUV, -1.5));
    
    float t = 0.0;
    for (int i = 0; i < 64; i++) {
      vec3 p = ro + rd * t;
      float d = map(p);
      if (d < 0.001 || t > 10.0) break;
      t += d;
    }
    
    vec3 color = vec3(0.0);
    if (t < 10.0) {
      vec3 p = ro + rd * t;
      vec3 normal = calcNormal(p);
      vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
      
      float diff = max(dot(normal, lightDir), 0.0);
      float spec = pow(max(dot(reflect(-lightDir, normal), -rd), 0.0), 32.0);
      
      vec3 baseColor = vec3(0.3, 0.6, 1.0);
      color = baseColor * diff + vec3(1.0) * spec + vec3(0.1);
    }
    
    gl_FragColor = vec4(color, 1.0);
  }
`;

function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  return shaderProgram;
}

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  return shader;
}

const positions = [-1, -1, 1, -1, -1, 1, 1, 1];
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const posAttrib = gl.getAttribLocation(shaderProgram, 'aPosition');
const timeUniform = gl.getUniformLocation(shaderProgram, 'uTime');

let time = 0;

function render() {
  time += 0.016;
  
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
  gl.vertexAttribPointer(posAttrib, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(posAttrib);
  
  gl.useProgram(shaderProgram);
  gl.uniform1f(timeUniform, time);
  
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  
  requestAnimationFrame(render);
}

render();

Preview