Skip to content

Instantly share code, notes, and snippets.

@charlieroberts
Last active May 16, 2024 17:00
Show Gist options
  • Select an option

  • Save charlieroberts/2cb12ad8332e17acf60a6ef2754877e8 to your computer and use it in GitHub Desktop.

Select an option

Save charlieroberts/2cb12ad8332e17acf60a6ef2754877e8 to your computer and use it in GitHub Desktop.

Ray marching, glslify, and signed-distance functions

Show examples

  • ray marched bug
  • First basic red sphere
  • Final repeated, warped sphere

How does ray marching work?

  • for every pixel, start a ray from an origin, through the camera view plane, until it hits something
    • draw?
  • difference between marching / tracing

Signed distance functions

Get a sphere up

  • npm install glslify gl-toy glsl-raytrace glsl-camera-ray glsl-square-frame

app.js

const glslify = require('glslify')
const toy     = require('gl-toy')

const shaderText = glslify(`precision mediump float;

vec2 doModel( vec3 p );
uniform vec2  uScreenSize;
uniform float uTime;

#pragma glslify: raytrace = require('glsl-raytrace', map=doModel, steps=50)
#pragma glslify: camera   = require('glsl-camera-ray')
#pragma glslify: square   = require('glsl-square-frame')
#pragma glslify: getNormal= require('glsl-sdf-normal', map=doModel)

vec2 doModel( vec3 p ) {
  float radius = 1.;
  float dist = length(p) - radius;

  return vec2( dist, 0. );
}

void main() {
  vec3 cameraPos = vec3( 0., 0., 5. );
  vec3 cameraDir = vec3( 0., 0., 0. );

  vec3 ray = camera( cameraPos, cameraDir, square( uScreenSize ), 2.0 );
  vec2 t   = raytrace( cameraPos, ray );

  float color = t.x > -.5 ? 1. : 0.;

  gl_FragColor = vec4( color, 0., 0., 1. );
}`)

const start = Date.now()

toy( shaderText, ( gl, shader ) => {
  shader.uniforms.uScreenSize = [ gl.drawingBufferWidth, gl.drawingBufferHeight ]
  shader.uniforms.uTime = ( Date.now() - start ) / 1000
})

Light the sphere with normals

  • npm install glsl-sdf-normal
  • all of our javascript remains the same, only the main function of our shader will change. Only the shader code is provided here, just paste it into the glslify command.
precision mediump float;

vec2 doModel( vec3 p );
uniform vec2  uScreenSize;
uniform float uTime;

#pragma glslify: raytrace = require('glsl-raytrace', map=doModel, steps=50)
#pragma glslify: camera   = require('glsl-camera-ray')
#pragma glslify: square   = require('glsl-square-frame')
#pragma glslify: getNormal= require('glsl-sdf-normal', map=doModel)

vec2 doModel( vec3 p ) {
  float radius = 1.;
  float dist = length(p) - radius;

  return vec2( dist, 0. );
}

void main() {
  vec3 cameraPos = vec3( 0., 0., 5. );
  vec3 cameraDir = vec3( 0., 0., 0. );

  vec3 ray = camera( cameraPos, cameraDir, square( uScreenSize ), 2.0 );
  vec2 t   = raytrace( cameraPos, ray );

  vec3 color = vec3(0.);
  if( t.x > -.5 ) {
    vec3 pos = cameraPos + t.x * ray;
    vec3 normal = getNormal( pos );
    color = normal;
  }

  gl_FragColor = vec4( color, 1. );
}

Simple lighting

  • we'll add ambient and diffuse lighting for our sphere using the normal that we generate last time. We'll create a function, lighting, that takes in our normal and calculates diffuse lighting based on its values.
precision mediump float;

vec2 doModel( vec3 p );
uniform vec2  uScreenSize;
uniform float uTime;

#pragma glslify: raytrace = require('glsl-raytrace', map=doModel, steps=50)
#pragma glslify: camera   = require('glsl-camera-ray')
#pragma glslify: square   = require('glsl-square-frame')
#pragma glslify: getNormal= require('glsl-sdf-normal', map=doModel)

vec2 doModel( vec3 p ) {
  float radius = 1.;
  float dist = length(p) - radius;

  return vec2( dist, 0. );
}

vec3 lighting( vec3 normal ) {
  vec3 lightDir   = normalize(vec3(0, 1, 0));
  vec3 lightColor = vec3(0.9, 0.5, 0.3);
  vec3 diffuseAmt = lightColor * max(0.0, dot( lightDir, normal ));

  vec3 ambientAmt = vec3(0.05);

  return diffuseAmt + ambientAmt;
}

void main() {
  vec3 cameraPos = vec3( 0., 0., 5. );
  vec3 cameraDir = vec3( 0., 0., 0. );

  vec3 ray = camera( cameraPos, cameraDir, square( uScreenSize ), 2.0 );
  vec2 t   = raytrace( cameraPos, ray );

  vec3 color = vec3(0.);

  // only perform lighting if we have a "hit"
  if( t.x > -.5 ) {
    vec3 pos = cameraPos + t.x * ray;
    vec3 normal = getNormal( pos );
    color = lighting( normal );
  }

  gl_FragColor = vec4( color, 1. );
}

Add some noise

  • 4D perlin noise enables us to include a time component (obtained as the uniform uTime)
  • We just need to include one more glslify module (glsl-noise) and change the first line of our doModel function to alter our sphere radius based on perlin noise.
  • The revised doModel function, and the call to import glsl-noise, is given below:
#pragma glslify: noise = require('glsl-noise/simplex/4d')

vec2 doModel(vec3 p) {
  float radius  = 1.0 + noise( vec4( p, uTime )) * 0.25;
  float dist = length(p) - radius;

  return vec2( dist, 0. );
}

Fancier lighting

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment