RayMarching Shader pt.2 (Boolean Operators)

These examples show how boolean operators can be used to create complex shapes from simple ones. This 3d-modelling method is called CSG (Constructive solid geometry).

CSG is primarily built on 3 primitive operations named intersection ( ∩ ), union ( ∪ ), and difference ( − ).

The diagram below shows an example of what is possible with using this technique:

Trying the examples
If you quickly want to try out the examples I recommend the web based GLSL editor from the book of shaders over here:
http://editor.thebookofshaders.com

If you prefer working with an offline editor I recommend Visual Studio Code with the glsl-canvas extension: https://code.visualstudio.com
Or if you prefer Sublime Text then you can install this package : https://packagecontrol.io/packages/glslViewer

If you are reading this on an iOS device I recommend the app ‘Shaders’ which can be used to program and run GLSL shaders and also has a public timeline that you can use to share your work with others:
https://apps.apple.com/app/shaders-shader-editor/id1221602862?l=en

1. Intersection ()

Ray Marching Boolean Operator: Intersection
This example shows how the intersection operator is used on a cube and a sphere to create the shape as seen in the bottom left of the diagram:

precision highp float;

uniform vec2 u_resolution; // Width & height of the shader
uniform float u_time; // Time elapsed

// Constants
#define PI 3.1415925359
//#define TWO_PI 6.2831852
#define MAX_STEPS 100 // Mar Raymarching steps
#define MAX_DIST 100. // Max Raymarching distance
#define SURF_DIST .01 // Surface Distance

///////////////////////
// Boolean Operators
///////////////////////

float intersectSDF(float distA, float distB) {
    return max(distA, distB);
}

float unionSDF(float distA, float distB) {
    return min(distA, distB);
}

float differenceSDF(float distA, float distB) {
    return max(distA, -distB);
}
//////////////////////////

mat2 Rotate(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c,-s,s,c);
}

float sphereSDF( vec3 p, float s ) {
  return length(p)-s;
}

float boxSDF( vec3 p, vec3 b ) {
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

float GetDist(vec3 p) 
{   
    // Rotate the whole scene
    p.xz *=Rotate(u_time / 2.0);
  
    // Sphere. xyz is position w is radius
    vec4 s0 = vec4(0,1,0,1);
    float sphereDist_0 = sphereSDF(p-s0.xyz,s0.w);
    
    // Box
    vec3 b0s = vec3(.8,.8,.8); //box size
    vec3 b0p = vec3(0,1,0); // box position
    float boxDist_0 = boxSDF(p-b0p,b0s);
  
    // Plane
    float planeDist  = p.y;

    float d = 0.0;
    d = intersectSDF(boxDist_0,sphereDist_0);
    // Uncomment to view the box
    //d = min(boxDist_0,sphereDist_0);
    d = min(d,planeDist);

    return d;
}

float RayMarch(vec3 ro, vec3 rd) 
{
    float dO = 0.; //Distane Origin
    for(int i=0;i<MAX_STEPS;i++)
    {
    	vec3 p = ro + rd * dO;
    	float ds = GetDist(p); // ds is Distance Scene
    	dO += ds;
    	if(dO > MAX_DIST || ds < SURF_DIST) 
      		break;
    }
    return dO;
}

vec3 GetNormal(vec3 p)
{ 
    float d = GetDist(p); // Distance
    vec2 e = vec2(.01,0); // Epsilon

    vec3 n = d - vec3(
        GetDist(p-e.xyy),  // e.xyy is the same as vec3(.01,0,0). The x of e is .01. this is called a swizzle
        GetDist(p-e.yxy),
        GetDist(p-e.yyx));

    return normalize(n);
}

float GetLight(vec3 p)
{ 
    // Directional light
    vec3 lightPos = vec3(5.*sin(u_time),5.,5.0*cos(u_time)); // Light Position
  	vec3 l = normalize(lightPos-p); // Light Vector
    vec3 n = GetNormal(p); // Normal Vector
  
    float dif = dot(n,l); // Diffuse light
    dif = clamp(dif,0.,1.); // Clamp so it doesnt go below 0

    // Shadows
    float d = RayMarch(p+n*SURF_DIST*2., l);
    
    if(d<length(lightPos-p)) dif *= .1;
    return dif;
}

void main()
{
    vec2 uv = (gl_FragCoord.xy-.5*u_resolution.xy)/u_resolution.y;
    
    vec3 ro = vec3(0,1,-6); // Ray Origin/Camera
    vec3 rd = normalize(vec3(uv.x,uv.y,1)); // Ray Direction
  
    float d = RayMarch(ro,rd); // Distance
    
    vec3 p = ro + rd * d;
    float dif = GetLight(p); // Diffuse lighting
    d*= .2;
    vec3 color = vec3(dif);

    // Set the output color
    gl_FragColor = vec4(color,1.0);
}

2. Union ()

Ray Marching Boolean Operator: Union
This example shows how the Union operator is used to create a single shape from three capped cylinders.

precision highp float;

uniform vec2 u_resolution; // Width & height of the shader
uniform float u_time; // Time elapsed

// Constants
#define PI 3.1415925359
//#define TWO_PI 6.2831852
#define MAX_STEPS 100 // Mar Raymarching steps
#define MAX_DIST 100. // Max Raymarching distance
#define SURF_DIST .01 // Surface Distance

///////////////////////
// Boolean Operators
///////////////////////

float intersectSDF(float distA, float distB) {
    return max(distA, distB);
}

float unionSDF(float distA, float distB) {
    return min(distA, distB);
}

float differenceSDF(float distA, float distB) {
    return max(distA, -distB);
}
//////////////////////////

mat2 Rotate(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c,-s,s,c);
}

float sphereSDF( vec3 p, float s ) {
  return length(p)-s;
}

float boxSDF( vec3 p, vec3 b ) {
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

float cappedCylinderSDF( vec3 p, float h, float r )
{
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(r,h);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

float GetDist(vec3 p) 
{   
    // Rotate the whole scene
    p.xz *=Rotate(u_time / 3.0);
  
//     // Sphere. xyz is position w is radius
//     vec4 s0 = vec4(0,1,0,1);
//     float sphereDist_0 = sphereSDF(p-s0.xyz,s0.w);
    
//     // Box
//     vec3 b0s = vec3(.8,.8,.8); //box size
//     vec3 b0p = vec3(0,1,0); // box position
//     float boxDist_0 = boxSDF(p-b0p,b0s);
    
    // Cylinders
    float c0h = 1.,c0r = .8; // Cylinder height, radius.
    vec3 c0p = p - vec3 (0,1,0); // Position
    float cylDist_0 = cappedCylinderSDF(c0p,c0h,c0r); 
    
    float c1h = 1.,c1r = .8; 
    vec3 c1p = p - vec3 (0,1,0); 
    c1p.xy *= Rotate(PI*.5);  // Rotate
    float cylDist_1 = cappedCylinderSDF(c1p,c1h,c1r); 
    
    float c2h = 1.,c2r = .8; 
    vec3 c2p = p - vec3 (0,1,0);
    c2p.xy *= Rotate(PI*.5);  
    c2p.yz *= Rotate(PI*.5);  
    float cylDist_2 = cappedCylinderSDF(c2p,c2h,c2r); 
  
    // Plane
    float planeDist  = p.y;

    float d = 0.0;
    
    //d = intersectSDF(boxDist_0,sphereDist_0);
    // Uncomment to view the box
    //d = min(boxDist_0,sphereDist_0);
    
    d = unionSDF(cylDist_0,cylDist_1);
    d = unionSDF(d,cylDist_2);
    
    d = min(d,planeDist);

    return d;
}

float RayMarch(vec3 ro, vec3 rd) 
{
    float dO = 0.; //Distane Origin
    for(int i=0;i<MAX_STEPS;i++)
    {
    	vec3 p = ro + rd * dO;
    	float ds = GetDist(p); // ds is Distance Scene
    	dO += ds;
    	if(dO > MAX_DIST || ds < SURF_DIST) 
      		break;
    }
    return dO;
}

vec3 GetNormal(vec3 p)
{ 
    float d = GetDist(p); // Distance
    vec2 e = vec2(.01,0); // Epsilon

    vec3 n = d - vec3(
        GetDist(p-e.xyy),  // e.xyy is the same as vec3(.01,0,0). The x of e is .01. this is called a swizzle
        GetDist(p-e.yxy),
        GetDist(p-e.yyx));

    return normalize(n);
}

float GetLight(vec3 p)
{ 
    // Directional light
    vec3 lightPos = vec3(5.*sin(u_time),5.,5.0*cos(u_time)); // Light Position
  	vec3 l = normalize(lightPos-p); // Light Vector
    vec3 n = GetNormal(p); // Normal Vector
  
    float dif = dot(n,l); // Diffuse light
    dif = clamp(dif,0.,1.); // Clamp so it doesnt go below 0

    // Shadows
    float d = RayMarch(p+n*SURF_DIST*2., l);
    
    if(d<length(lightPos-p)) dif *= .1;
    return dif;
}

void main()
{
    vec2 uv = (gl_FragCoord.xy-.5*u_resolution.xy)/u_resolution.y;
    
    vec3 ro = vec3(0,1,-6); // Ray Origin/Camera
    vec3 rd = normalize(vec3(uv.x,uv.y,1)); // Ray Direction
  
    float d = RayMarch(ro,rd); // Distance
    
    vec3 p = ro + rd * d;
    float dif = GetLight(p); // Diffuse lighting
    d*= .2;
    vec3 color = vec3(dif);

	// Set the output color
    gl_FragColor = vec4(color,1.0);
}

3. Difference (−)

Ray Marching Boolean Operator: Difference

This example shows how the difference operator is used on the previous two shapes to create the final combined object as seen in the SDF diagram:

precision highp float;

uniform vec2 u_resolution; // Width & height of the shader
uniform float u_time; // Time elapsed

// Constants
#define PI 3.1415925359
//#define TWO_PI 6.2831852
#define MAX_STEPS 100 // Mar Raymarching steps
#define MAX_DIST 100. // Max Raymarching distance
#define SURF_DIST .01 // Surface Distance

///////////////////////
// Boolean Operators
///////////////////////

float intersectSDF(float distA, float distB) {
    return max(distA, distB);
}

float unionSDF(float distA, float distB) {
    return min(distA, distB);
}

float differenceSDF(float distA, float distB) {
    return max(distA, -distB);
}
//////////////////////////

mat2 Rotate(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c,-s,s,c);
}

float sphereSDF( vec3 p, float s ) {
  return length(p)-s;
}

float boxSDF( vec3 p, vec3 b ) {
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

float cappedCylinderSDF( vec3 p, float h, float r )
{
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(r,h);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}

float GetDist(vec3 p) 
{   
    // Rotate the whole scene
    p.xz *=Rotate(u_time / 3.0);
  
    // Sphere. xyz is position w is radius
    vec4 s0 = vec4(0,1,0,1.05);
    float sphereDist_0 = sphereSDF(p-s0.xyz,s0.w);
    
    // Box
    vec3 b0s = vec3(.75,.75,.75); //box size
    vec3 b0p = vec3(0,1,0); // box position
    float boxDist_0 = boxSDF(p-b0p,b0s);
    
    // Cylinders
    float c0h = 1.,c0r = .55; // Cylinder height, radius.
    vec3 c0p = p - vec3 (0,1,0); // Position
    float cylDist_0 = cappedCylinderSDF(c0p,c0h,c0r); 
    
    float c1h = 1.,c1r = .55; 
    vec3 c1p = p - vec3 (0,1,0); 
    c1p.xy *= Rotate(PI*.5);  // Rotate
    float cylDist_1 = cappedCylinderSDF(c1p,c1h,c1r); 
    
    float c2h = 1.,c2r = .55; 
    vec3 c2p = p - vec3 (0,1,0);
    c2p.xy *= Rotate(PI*.5);  
    c2p.yz *= Rotate(PI*.5);  
    float cylDist_2 = cappedCylinderSDF(c2p,c2h,c2r); 
  
    // Plane
    float planeDist  = p.y;

    float d0 = 0., d1 = 0., d2 = 0.;
    
    d1 = intersectSDF(boxDist_0,sphereDist_0);
    // Uncomment to view the box
    //d = min(boxDist_0,sphereDist_0);
    
    d2 = unionSDF(cylDist_0,cylDist_1);
    d2 = unionSDF(d2,cylDist_2);
    
    d0 = differenceSDF(d1,d2);
    
    d0 = min(d0,planeDist);

    return d0;
}

float RayMarch(vec3 ro, vec3 rd) 
{
    float dO = 0.; //Distane Origin
    for(int i=0;i<MAX_STEPS;i++)
    {
    	vec3 p = ro + rd * dO;
    	float ds = GetDist(p); // ds is Distance Scene
    	dO += ds;
    	if(dO > MAX_DIST || ds < SURF_DIST) 
      		break;
    }
    return dO;
}

vec3 GetNormal(vec3 p)
{ 
    float d = GetDist(p); // Distance
    vec2 e = vec2(.01,0); // Epsilon

    vec3 n = d - vec3(
        GetDist(p-e.xyy),  // e.xyy is the same as vec3(.01,0,0). The x of e is .01. this is called a swizzle
        GetDist(p-e.yxy),
        GetDist(p-e.yyx));

    return normalize(n);
}

float GetLight(vec3 p)
{ 
    // Directional light
    vec3 lightPos = vec3(2,5,0); // Light Position
    lightPos.xz *= Rotate(u_time); // Rotation
    
  	vec3 l = normalize(lightPos-p); // Light Vector
    vec3 n = GetNormal(p); // Normal Vector
  
    float dif = dot(n,l); // Diffuse light
    dif = clamp(dif,0.,1.); // Clamp so it doesnt go below 0

    // Shadows
    float d = RayMarch(p+n*SURF_DIST*2., l);
    
    if(d<length(lightPos-p)) dif *= .1;
    return dif;
}

void main()
{
    vec2 uv = (gl_FragCoord.xy-.5*u_resolution.xy)/u_resolution.y;
    
    vec3 ro = vec3(0,1,-6); // Ray Origin/Camera
    vec3 rd = normalize(vec3(uv.x,uv.y,1)); // Ray Direction
  
    float d = RayMarch(ro,rd); // Distance
    
    vec3 p = ro + rd * d;
    float dif = GetLight(p); // Diffuse lighting
    d*= .2;
    vec3 color = vec3(dif);

    // Set the output color
    gl_FragColor = vec4(color,1.0);
}

This example shows how the Difference operator can be used to create a shape from spheres only:

precision highp float;

uniform vec2 u_resolution; // Width & height of the shader
uniform float u_time; // Time elapsed

// Constants
#define PI 3.1415925359
//#define TWO_PI 6.2831852
#define MAX_STEPS 100 // Mar Raymarching steps
#define MAX_DIST 100. // Max Raymarching distance
#define SURF_DIST .01 // Surface Distance

///////////////////////
// Boolean Operators
///////////////////////

float intersectSDF(float distA, float distB) {
    return max(distA, distB);
}

float unionSDF(float distA, float distB) {
    return min(distA, distB);
}

float differenceSDF(float distA, float distB) {
    return max(distA, -distB);
}
//////////////////////////

mat2 Rotate(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c,-s,s,c);
}

float GetDist(vec3 p) 
{   
    // Rotate the whole scene
    p.xz *=Rotate(u_time / 2.0);
  
    // Spheres. xyz is position w is radius
  vec4 s0 = vec4(0,1,0,1), //center big
       s1 = vec4(0,1,-.65,.65), //front
       s2 = vec4(0,1,0.65,.65), //back
       s3 = vec4(-.65,1,0,.65), //left
       s4 = vec4(.65,1,0,.65), //right
       s5 = vec4(0,2,0,.65), //top
       s6 = vec4(0,0,0,.65), //bottom
       s7 = vec4(0,1,0,.8); //center small
  
    float sphereDist_0 = length(p-s0.xyz) - s0.w,
        sphereDist_1 = length(p-s1.xyz) - s1.w,
        sphereDist_2 = length(p-s2.xyz) - s2.w,
        sphereDist_3 = length(p-s3.xyz) - s3.w,
        sphereDist_4 = length(p-s4.xyz) - s4.w,
        sphereDist_5 = length(p-s5.xyz) - s5.w,
        sphereDist_6 = length(p-s6.xyz) - s6.w,
        sphereDist_7 = length(p-s7.xyz) - s7.w;

  float planeDist  = p.y;

  float d = 0.0;
    
    // d = min(sphereDist_0,sphereDist_1);
    // d = min(d,sphereDist_2);
    // d = min(d,sphereDist_3);
    // d = min(d,sphereDist_4);
    // d = min(d,sphereDist_5);
    // d = min(d,sphereDist_6);
    // d = min(d,sphereDist_7);
    // d = min(d,planeDist);

    d = differenceSDF(sphereDist_0,sphereDist_1);
    d = differenceSDF(d,sphereDist_2);
    d = differenceSDF(d,sphereDist_3);
    d = differenceSDF(d,sphereDist_4);
    d = differenceSDF(d,sphereDist_5);
    d = differenceSDF(d,sphereDist_6);
    d = differenceSDF(d,sphereDist_7);
    d = min(d,planeDist);

  return d;
}

float RayMarch(vec3 ro, vec3 rd) 
{
  float dO = 0.; //Distane Origin
  for(int i=0;i<MAX_STEPS;i++)
  {
    vec3 p = ro + rd * dO;
    float ds = GetDist(p); // ds is Distance Scene
    dO += ds;
    if(dO > MAX_DIST || ds < SURF_DIST) 
      break;
  }
  return dO;
}

vec3 GetNormal(vec3 p)
{ 
  float d = GetDist(p); // Distance
  vec2 e = vec2(.01,0); // Epsilon

  vec3 n = d - vec3(
    GetDist(p-e.xyy),  // e.xyy is the same as vec3(.01,0,0). The x of e is .01. this is called a swizzle
    GetDist(p-e.yxy),
    GetDist(p-e.yyx));

  return normalize(n);
}

float GetLight(vec3 p)
{ 
  // Directional light
  vec3 lightPos = vec3(5.*sin(u_time),5.,5.0*cos(u_time)); // Light Position
  vec3 l = normalize(lightPos-p); // Light Vector
  vec3 n = GetNormal(p); // Normal Vector
  
  float dif = dot(n,l); // Diffuse light
  dif = clamp(dif,0.,1.); // Clamp so it doesnt go below 0

  // Shadows
  float d = RayMarch(p+n*SURF_DIST*2., l); 

  if(d<length(lightPos-p)) dif *= .1;
  return dif;
}

void main()
{
  vec2 uv = (gl_FragCoord.xy-.5*u_resolution.xy)/u_resolution.y;

  vec3 ro = vec3(0,1,-6); // Ray Origin/Camera
  vec3 rd = normalize(vec3(uv.x,uv.y,1)); // Ray Direction
  
  float d = RayMarch(ro,rd); // Distance
  
  vec3 p = ro + rd * d;
  float dif = GetLight(p); // Diffuse lighting
  d*= .2;
  vec3 color = vec3(dif);
  //color += GetNormal(p);
  //float color = GetLight(p);

  // Set the output color
  gl_FragColor = vec4(color,1.0);
}

Sources
The Art of Code (Youtube): The Art Of Code
Inigo Quilez: http://www.iquilezles.org
Wikipedia: https://en.wikipedia.org/wiki/Constructive_solid_geometry

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Powered by WordPress.com.

Up ↑

%d bloggers like this: