How to render a Christmas Tree with two triangles

Ruslan Shestopalyuk


There are a few ways...

Obvious pipeline

  • Two screen space triangles (a quad)
  • Pass-through vertex shader
  • A texture with pre-rendered image
  • Pass-through pixel shader
  • The image

Less boring pipeline

  • Two screen space triangles (a quad)
  • Pass-through vertex shader
  • A few pixel shader constans
  • ???
  • PROFIT

Everything is in the pixel shader code


  • Procedural scene description
  • Ray tracing
  • Shading
  • Camera

Everything is in the pixel shader code

Signed distance function (SDF)

  • \(f_{dist}(\boldsymbol{p}): \mathbb{R}^{3} \Rightarrow \mathbb{R}\)
  • Distance to the closest point of some solid surface
  • Negative if point is inside
  • Does not tell where the closest point is, just the distance

Naive raycasting

  • Need to find where ray intersects the surface
  • In simple cases can be done analytically
  • Otherwise, need to step along the ray

Raymarching

  • Don't need to step less than the DF value at current point
  • Can potentially make big jumps
  • But still wasteful in "grazing" scenarios

SDF primitives: plane

\(D_{plane}(\boldsymbol{p}, \boldsymbol{n}, s)= \boldsymbol{p} \cdot \boldsymbol{n} - s\)

//  distance to a plane is specified with normal "n" and offset "offs"
float plane(vec3 p, vec3 n, float offs) {
  return dot(p, n) - offs;
}

SDF primitives: sphere

\(D_{sphere}(\boldsymbol{p}, r)= \| \boldsymbol{p} \| - r\)

//  distance to a sphere of radius "r" (in the centre of coordinates)
float sphere(vec3 p, float r) {
  return length(p) - r;
}

SDF primitives: torus

\(D_{torus}(\boldsymbol{p}, r_{o}, r_{i})= \| (\|(p_x,p_z)\|) - r_{i}, p_y \| - r_{o}\)

//  distance to torus of inner radius "ri" and outher radius "ro",
//  located in the center of coordinates, laying in plane xOz
float torus(vec3 p, float ri, float ro) {
  vec2 q = vec2(length(p.xz) - ri, p.y);
  return length(q) - ro;
}

SDF primitives: cylinder (infinite)

\(D_{cylinder}(\boldsymbol{p}, r)= \|(p_x,p_z)\| - r\)

//  distance to cylinder of radius "r", infinite along oY
float cylinder(in vec3 p, float r) {
  return length(p.xz) - r;
}

SDF primitives: cone (infinite)

\(D_{cone}(\boldsymbol{p}, \boldsymbol{n})= (\|(p_x,p_z)\|, p_y) \cdot \boldsymbol{n}\)

//  distance to cone, infinite along oY, going downwards,
//  specified by normal "n" in vertical cross-section 
float cone(vec3 p, vec2 n) {
  return dot(vec2(length(p.xz), p.y), n);
}

SDF operations: union

\(D_{A \cup B}(\boldsymbol{p})= \min{(D_A(\boldsymbol{p}),D_B(\boldsymbol{p}))}\)

//  union of two distance fields
float add(float d1, float d2) {
  return min(d2, d1);
}

//  example of distance field union  
float distf(vec3 p) {
  float d1 = sphere(p, 0.5);
  float d2 = torus(p, 0.5, 0.3);
  return add(d1, d2);
}

SDF operations: intersection

\(D_{A \cap B}(\boldsymbol{p})= \max{(D_A(\boldsymbol{p}),D_B(\boldsymbol{p}))}\)

//  intresection of two distance fields
float intersect(float d1, float d2) {
  return max(d2, d1);
}

//  example of distance field intersection
float distf(vec3 p) {
   float d1 = sphere(p, 0.5);
   float d2 = torus(p, 0.5, 0.3);
   return intersect(d1, d2);
}

SDF operations: difference

\(D_{A \setminus B}(\boldsymbol{p})= \max{(D_A(\boldsymbol{p}),-D_B(\boldsymbol{p}))}\)

//  difference of two distance fields
float diff(float d1, float d2) { 
  return max(-d2, d1);
}

//  example of two distance fields difference
float distf(vec3 p) {
  float d1 = sphere(p, 0.5);
  float d2 = torus(p, 0.5, 0.3);
  return diff(d1, d2);
}

SDF operations: combinations

float distf(vec3 p) {
  float d1 = sphere(p, 0.5);
  float d2 = torus(p, 0.5, 0.3);
  float d = add(d1, d2);   

  //  cut with a plane
  float d3 = plane(p, vec3(0.0, 0.0, -1.0), -0.1);
  d = diff(d, d3);

  return d;
}

SDF affine transforms: translation

\(D_{trans}(\boldsymbol{p}, \boldsymbol{d})= D(\boldsymbol{p}-\boldsymbol{d})\)

vec3 translate(vec3 p, vec3 d) {
  return p - d;
}

float scene(vec3 p) { /* ... */ }

float distf(vec3 p) {
  p = translate(p, vec3(1.0, 0.5, 0.0));
  return scene(p);
}

SDF affine transforms: rotation


\(D^{y}_{rot}(\boldsymbol{p}, \alpha)= D(\begin{pmatrix} \cos{\alpha} & 0 & \sin{\alpha} \\ 0 & 1 & 0 \\ -\sin{\alpha} & 0 & \cos{\alpha} \end{pmatrix} \boldsymbol{p})\)


vec2 rotate(vec2 p, float ang) {
  float c = cos(ang), s = sin(ang);
  return vec2(p.x*c - p.y*s, p.x*s + p.y*c);
}

SDF affine transforms: rotation (example)

vec2 rotate(vec2 p, float ang) {
  float c = cos(ang), s = sin(ang);
  return vec2(p.x*c - p.y*s, p.x*s + p.y*c);
}

float distf(vec3 p) {
  p.xy = rotate(p.xy, PI*0.25);
  return scene(p);
}

SDF affine transforms: rotation (another axis)

vec2 rotate(vec2 p, float ang) {
  float c = cos(ang), s = sin(ang);
  return vec2(p.x*c - p.y*s, p.x*s + p.y*c);
}

float distf(vec3 p) {
  p.yz = rotate(p.yz, PI*0.25);
  return scene(p);
}

SDF affine transforms: rotation (swapping coordinates)

float distf(in vec3 p) {
  return torus(p.xzy, 0.5, 0.1);
}

SDF affine transforms: scale

\(D_{scale}(\boldsymbol{p}, s)= D(\boldsymbol{p} / s) * s\)

float distf(vec3 p) {
  float scale = 3.0;
  return scene(p/scale)*scale;
}

SDF morphing: symmetry

\(D_{sym}^{Y+}(\boldsymbol{p})= D((p_x, \lvert p_y \rvert, p_z)) \\ D_{sym}^{Y-}(\boldsymbol{p})= D((p_x, -\lvert p_y \rvert, p_z))\)

float distf(in vec3 p) {
  //  mirror upper part symmetrically about XoZ plane 
  //  (p.y = -abs(p.y) for the lower part)
  p.y = abs(p.y); 
  return torus(translate(p.xzy, vec3(0.0, 0.0, 0.25)), 0.5, 0.1);
}

Example: capping infinite primitives

float capped_cylinder(in vec3 p, float r, float h) {
  p.y = abs(p.y);  //  mirror upper part about XoZ plane
  float cyl = cylinder(p, r);
  float capPlane = plane(p, vec3(0.0, 1.0, 0.0), h*0.5);
  float d = intersect(cyl, capPlane); // cap cylinder from the top
  return d;
}

simplified to:

float cylinder(in vec3 p, float r, float h) {
  float d = cylinder(p, r);
  return max(d, abs(p.y) - h*0.5);
}

SDF morphing: distortions

\(D_{distort}(\boldsymbol{p}, f_d)= D(\boldsymbol{p}) + f_d(\boldsymbol{p})\)

//  example distortion function
float wigglies(vec3 p) {
  return cos(p.y*40.0)*0.02;
}

float distf(vec3 p) {
  float d = torus(p.xzy, vec2(0.5, 0.1)); // the torus
  d += wigglies(p); // add wiggly horizontal distortions
  return d;
}

SDF morphing: bumped sphere

float bumps(vec3 p) {
  return cos(atan(p.x, p.z)*30.0)*0.01*(0.5 - p.y) + 
    sin(p.y*60.0)*0.01;
}

float distf(in vec3 p) {
  float d = sphere(p, 0.5);
  d += bumps(p);
  return d;
}

SDF morphing: "terrain"

float terrain(in vec3 p) {
  return p.y + (sin(sin(p.z*0.1253) - p.x*0.311)*0.31 + 
    cos(p.z*0.53 + sin(p.x*0.127))*0.12)*1.7 + 0.2;
}

SDF cloning: positional

\(D_{repeat}^{x}(\boldsymbol{p}, s)= D((p_x \bmod s - s/2,p_y,p_z))\)

float repeat(float coord, float spacing) {
  return mod(coord, spacing) - spacing*0.5;
}

float distf(in vec3 p) { 
  p.x = repeat(p.x, 0.7);
  return cylinder(p, 0.2, 1.0);
}

SDF cloning: positional, multiple axes

float distf(in vec3 p) { 
  p.x = repeat(p.x, 0.7);
  p.z = repeat(p.z, 0.2);
  return cylinder(p, 0.2, 1.0);
}

SDF cloning: positional, constrained

float distf(in vec3 p) { 
  p.x = repeat(clamp(p.x, -3.0, 0.0), 1.0);
  p.z = repeat(clamp(p.z, -4.0, 0.0), 1.0);
  return cylinder(p, 0.2, 1.0);
}

SDF cloning: rotational

\(D_{repeatAng}^{y}(\boldsymbol{p}, k)= D_{rot}^y(\boldsymbol{p}, \frac{2 \pi}{k} \lfloor \frac{\arctan{\frac{p_x}{p_z}}}{\frac{2 \pi}{k}} + \frac{1}{2} \rfloor)\)

//  also returns the number of the sector that p is in
vec3 repeatAngS(vec2 p, float k) {
    float ang = 2.0*PI/k;
    float sector = floor(atan(p.x, p.y)/ang + 0.5);
    p = rotate(p, sector*ang);
    return vec3(p.x, p.y, mod(sector, k));
}

Complex shapes: a star

float star(vec3 p) {
  p.xy = repeatAng(p.xy, 5.0);          // 3. Clone five corners radially 
  p.xz = abs(p.xz);                     // 2. Symmetrical about XoY and ZoY
  vec3 n = vec3(0.5, 0.25, 0.8);  
  float d = plane(p, normalize(n), 0.1);// 1. A plane cutting the corner 
  return d;
}

Complex shapes: a star

#define TOPPER_SCALE 2.0
#define TOPPER_OFFSET 0.3

float topper(vec3 pos) {
  pos /= TOPPER_SCALE;
  pos.y -= TOPPER_OFFSET;
  float base = cylinder(pos - vec3(0.0, -0.2, 0.0), vec2(0.04, 0.1));
  float d = add(star(pos), base)*TOPPER_SCALE;
  return d;
}

Complex shapes: tree decoration

Complex shapes: tree decoration

#define BAUBLE_SIZE 0.5
vec2 bauble(vec3 pos) {
    float d = sphere(pos, BAUBLE_SIZE);
    // bump
    d += cos(atan(pos.x, pos.z)*30.0)*0.01*(0.5 - pos.y) + sin(pos.y*60.0)*0.01;
    vec2 res = vec2(d, MTL_OBJ1);

    // dent the bumped sphere
    vec2 dent = vec2(sphere(pos + vec3(0.0, 0.0, -0.9), 0.7), MTL_OBJ2);
    diff(res, dent);
    
    //  the cap 
    pos = translate(pos, vec3(0.0, BAUBLE_SIZE + 0.03, 0.0));
    float cap = cylinder(pos, BAUBLE_SIZE*0.15, 0.1);
    //  the hook
    cap = add(cap, torus(pos.xzy - vec3(0.0, 0.0, 0.08), BAUBLE_SIZE*0.1, 0.015));
    vec2 res1 = vec2(cap, MTL_OBJ2);
    add(res, res1);
    return res;
}

vec2 distf(in vec3 p) {
    return bauble(p);
}

Complex shapes: a tree branch

Complex shapes: a tree branch

#define NEEDLE_LENGTH           0.5
#define NEEDLE_SPACING          0.2
#define NEEDLE_THICKNESS        0.05
#define NEEDLES_RADIAL_NUM      17.0
#define NEEDLE_BEND             0.99
#define NEEDLE_TWIST            1.6
#define NEEDLE_GAIN             1.5
float needles(in vec3 p) {   
    p.xy = rotate(p.xy, -length(p.xz)*NEEDLE_TWIST);// 7. twist the needles
    p.xy = repeatAng(p.xy, NEEDLES_RADIAL_NUM);     // 6. replicate the needles radially
    p.yz = rotate(p.yz, -NEEDLE_BEND);              // 5. rotate the row of needles to align back with Z axis
    p.y -= p.z*NEEDLE_GAIN;                         // 4. skew the row of needles down along Y
    p.z = min(p.z, 0.0);                            // 3. remove the Z+ part
    p.z = repeat(p.z, NEEDLE_SPACING);              // 2. clone the needle along Z
    return cone(p, NEEDLE_THICKNESS, NEEDLE_LENGTH);// 1. A single needle (cone)
}

Complex shapes: a tree branch

Complex shapes: a tree

Complex shapes: a tree

Shading: fog

vec3 applyFog(vec3 col, float dist) {
    return mix(col, BACKGROUND_COLOR, 1.0 - exp(-FOG_DENSITY*dist*dist));
}

vec3 render(in vec3 rayOrig, in vec3 rayDir) {
    vec2 d = rayMarch(rayOrig, rayDir);
    float t = d.x;
    col = vec3(0.8, 0.8, 1.8); // a constant color for now
    col = applyFog(col, t);
    return col;
}

Shading: materials

vec3 getMaterialColor(float matID) {
    vec3 col = BACKGROUND_COLOR;
         if (matID <= MTL_GROUND) col = vec3(3.3, 3.3, 4.5);
    else if (matID <= MTL_NEEDLE) col = vec3(0.152,0.36,0.18);
    else if (matID <= MTL_STEM)   col = vec3(0.79,0.51,0.066);
    else if (matID <= MTL_TOPPER) col = vec3(1.6,1.0,0.6);
    else if (matID <= MTL_CAP)    col = vec3(1.2,1.0,0.8);
    else                          col = jollyColor(matID);
    return col;
}

vec3 render(in vec3 rayOrig, in vec3 rayDir) {
    vec2 d = rayMarch(rayOrig, rayDir);
    float t = d.x, matID = d.y;
    col = getMaterialColor(matID); // get material color at ray the hit position
    col = applyFog(col, t);
    return col;
}

Shading: normal computation


#define NORMAL_EPS              0.001
vec3 normal(in vec3 p)
{
    vec2 d = vec2(NORMAL_EPS, 0.0);
    return normalize(vec3(
        distf(p + d.xyy).x - distf(p - d.xyy).x,
        distf(p + d.yxy).x - distf(p - d.yxy).x,
        distf(p + d.yyx).x - distf(p - d.yyx).x));
}}

Shading: Lambertian (diffuse) component

#define GLOBAL_LIGHT_COLOR      vec3(0.8,1.0,0.9)
#define GLOBAL_LIGHT_DIR        normalize(vec3(-1.2, 0.3, -1.1))
#define AMBIENT_COLOR           vec3(0.03, 0.03, 0.03)

vec3 render(in vec3 rayOrig, in vec3 rayDir) {
    vec2 d = rayMarch(rayOrig, rayDir);
    float t = d.x, matID = d.y;
    vec3 mtlDiffuse = getMaterialColor(mtlID);
    vec3 hitPos = rayOrig + t*rayDir;
    vec3 n = normal(hitPos);
    float diffuse = clamp(dot(n, GLOBAL_LIGHT_DIR), 0.0, 1.0); // diffuse term
    vec3 col = mtlDiffuse*(AMBIENT_COLOR + LIGHT_COLOR*diffuse);
    col = applyFog(col, t);
    return col;
}

Shading: Phong (specular) component

#define SPEC_POWER              16.0
#define SPEC_COLOR              vec3(0.8, 0.90, 0.60)
vec3 render(in vec3 rayOrig, in vec3 rayDir) {
    vec2 d = rayMarch(rayOrig, rayDir);
    float t = d.x, matID = d.y;
    vec3 mtlDiffuse = getMaterialColor(mtlID);
    vec3 hitPos = rayOrig + t*rayDir;
    vec3 n = normal(hitPos);
    
    // diffuse term
    float diffuse = clamp(dot(n, GLOBAL_LIGHT_DIR), 0.0, 1.0); 
    // specular term
    vec3 ref = reflect(rayDir, n);
    float specular = pow(clamp(dot(ref, GLOBAL_LIGHT_DIR), 0.0, 1.0), SPEC_POWER);
        
    vec3 col = mtlDiffuse*(AMBIENT_COLOR + LIGHT_COLOR*(diffuse + specular*SPEC_COLOR));
        
    col = applyFog(col, t);
    return col;
}}

Shading: Phong (specular) component

Shading: Reflections

#define MAX_RAY_BOUNCES         3.0
#define BAUBLE_REFLECTIVITY     0.7
vec3 render(in vec3 rayOrig, in vec3 rayDir) {
    vec3 resCol = vec3(0.0);
    float alpha = 1.0;
    for (float i = 0.0; i < MAX_RAY_BOUNCES; i++) {
        vec2 d = rayMarch(rayOrig, rayDir);
        float t = d.x, mtlID = d.y;
        // ... SNIP ...
        col = applyFog(col, t);
        resCol += col*alpha; //  blend in (a possibly reflected) new color 
        if (mtlID <= MTL_BAUBLE || 
            abs(dot(nrm, rayDir)) < 0.1) { //  poor man Fresnel
            break;
        }
        rayOrig = pos + ref*DIST_EPSILON;
        alpha *= BAUBLE_REFLECTIVITY;
        rayDir = ref;
    }
    return vec3(clamp(resCol, 0.0, 1.0));
}

Shading: Reflections

Shading: Shadows

Main function

void main(void) {
    vec2 q = gl_FragCoord.xy/iResolution.xy;
    vec2 p = -1.0 + 2.0*q;
    p.x *= iResolution.x/iResolution.y;
    
    float ang = 0.1*(40.0 + iGlobalTime);
    vec3 camPos = vec3(CAM_DIST*cos(ang), CAM_H, CAM_DIST*sin(ang));
    vec3 rayDir = getRayDir(camPos,normalize(LOOK_AT - camPos), p);
    vec3 color = render(camPos, rayDir);
    
    gl_FragColor = vec4(color, 1.0);
}

References

  • Shader Toy: https://www.shadertoy.com
  • Íñigo Quílez: http://www.iquilezles.org
  • Blog: http://blog.ruslans.com/2015/01/raymarching-christmas-tree.html