This demo delves into a WEBGL p5 sketch that builds upon the blur effect with added features. It introduces uniformsUI for interactive shader uniform variables setting (here just the blur intensity), incorporates a focal target defined by the scene’s sole sphere (amidst random toruses and boxes) for enhanced visual depth, and employs first-person directional light to improve immersion. It also showcases the applyShader function, demonstrating its role in applying and managing custom shader effects within the sketch.

code
const blur_shader = `#version 300 es
precision highp float;

in vec2 texcoords2;
uniform sampler2D blender; // <- shared layer should be named 'blender'
uniform sampler2D depth;
uniform float focus;
uniform float blurIntensity; // 0, 4, 2, 0.1 controls blurriness
out vec4 fragColor;
const float TWO_PI = 6.28318530718;

float getBlurriness(float d) {
  // Blur more the farther away we go from the focal point at depth=focus
  // The blurIntensity uniform scales the blurriness
  return abs(d - focus) * 40. * blurIntensity;
}

float maxBlurDistance(float blurriness) {
  // The maximum distance for blurring, based on blurriness
  return blurriness * 0.01;
}

void main() {
  vec4 color = texture(blender, texcoords2);
  float samples = 1.;
  float centerDepth = texture(depth, texcoords2).r;
  float blurriness = getBlurriness(centerDepth);
  for (int sampleIndex = 0; sampleIndex < 20; sampleIndex++) {
    // Sample nearby pixels in a spiral going out from the current pixel
    // Using TWO_PI to convert loop index to radians
    float angle = float(sampleIndex) * TWO_PI / 20.;
    float distance = float(sampleIndex) / 20. * maxBlurDistance(blurriness);
    vec2 offset = vec2(cos(angle), sin(angle)) * distance;
    // How close is the object at the nearby pixel?
    float sampleDepth = texture(depth, texcoords2 + offset).r;
    // How far should its blur reach?
    float sampleBlurDistance = maxBlurDistance(getBlurriness(sampleDepth));
    // If it's in front of the current pixel, or its blur overlaps
    // with the current pixel, add its color to the average
    if (sampleDepth >= centerDepth || sampleBlurDistance >= distance) {
      color += texture(blender, texcoords2 + offset);
      samples++;
    }
  }
  color /= samples;
  fragColor = color;
}`;

let layer, blur, models

function setup() {
  createCanvas(600, 400, WEBGL)
  blur = makeShader(blur_shader)
  showUniformsUI(blur)
  layer = createFramebuffer()
  //noStroke();
  const trange = 200
  models = [];
  for (let i = 0; i < 50; i++) {
    models.push(
      {
        position: createVector((random() * 2 - 1) * trange,
                               (random() * 2 - 1) * trange,
                               (random() * 2 - 1) * trange),
        size: random() * 25 + 8,
        color: color(int(random(256)), int(random(256)), int(random(256))),
        type: i === 0 ? 'ball' : i < 25 ? 'torus' : 'box'
      }
    )
  }
}

function draw() {
  layer.begin()
  background(0)
  axes()
  orbitControl()
  noStroke()
  ambientLight(100)
  // direction aligned towards the viewing direction.
  const direction = treeDisplacement(Tree._k, { from: Tree.EYE, to: Tree.WORLD })
  directionalLight(255, 255, 255, direction.x, direction.y, direction.z)
  ambientMaterial(255, 0, 0)
  fill(255, 255, 100)
  specularMaterial(255)
  shininess(150)
  models.forEach(model => {
    push()
    noStroke()
    fill(model.color);
    translate(model.position)
    model.type === 'box' ? box(model.size) : 
          model.type === 'torus' ? torus(model.size) : sphere(model.size)
    pop()
  })
  const focus = treeLocation(models[0].position,
                             { from: Tree.WORLD, to: Tree.SCREEN }).z
  layer.end()
  applyShader(blur, {
    target: this, // called by default anyways
    scene: () => overlay(), // <- not really needed since it's called by default
    uniforms: {
      focus,
      blender: layer.color,
      depth: layer.depth,
    }
  })
}

uniformsUI

The uniformsUI feature in a WebGL p5 sketch enhances interactivity by enabling dynamic adjustments of shader uniform variables. This functionality is particularly useful for artists and developers who wish to experiment with real-time visual effects in their sketches. By integrating uniformsUI, users can interactively modify shader properties, thereby offering a more engaging and customizable visual experience.

// blur fragment shader excerpt
// code comment creates slider element to controlling blurriness
uniform float blurIntensity; // 0, 4, 2, 0.1 controls blurriness
const blur_shader = //...
let blur

function setup() {
  blur = makeShader(blur_shader)
  // displaying the ui requires this call
  showUniformsUI(blur)
  // ...
}

Focal target

The focal target is set using the treeLocation function, which converts a model’s position from world coordinates to screen coordinates, dynamically adjusting the scene’s focus depth based on the sphere model location’s z-coordinate. This mechanism is key for depth-based visual effects, allowing for precise control over which parts of the scene appear in focus or blurred, thereby enhancing the depth perception and realism of the rendering.

function draw() {
  layer.begin()
  // render scene onto layer...
  const focus = treeLocation(models[0].position,
                             { from: Tree.WORLD, to: Tree.SCREEN }).z
  layer.end()
}

First person directional light

First person directional light is achieved by transforming the light’s direction from eye space to world space, utilizing the treeDisplacement function. This transformation is essential because directionalLight requires the direction to be defined in world space. The snippet below demonstrates how to align the light direction with the viewer’s perspective to simulating first-person light interactions.

function draw() {
  layer.begin()
  // render scene onto layer...
  // Convert direction from eye space to world space for directional light
  const direction = treeDisplacement(Tree._k, { from: Tree.EYE, to: Tree.WORLD })
  directionalLight(255, 255, 255, direction.x, direction.y, direction.z)
  layer.end()
}

Observe that due to the value of default parameters, each of the following calls is equivalent to treeDisplacement(Tree._k, { from: Tree.EYE, to: Tree.WORLD }):

treeDisplacement([0, 0, -1], { from: Tree.EYE, to: Tree.WORLD })
treeDisplacement({ from: Tree.EYE, to: Tree.WORLD })
treeDisplacement(createVector(0, 0 -1))
treeDisplacement()

applyShader

The applyShader function renders a given scene using custom shaders, as shown in the snippet. It allows for specifying the shader to use (blur), the target canvas (this), and a set of uniforms (focus, blender, and depth) that the shader requires.

function draw() {
  // render scene onto layer...
  applyShader(blur, {
    target: this, // called by default anyways
    scene: () => overlay(), // <- not really needed since it's called by default
    uniforms: { // besides uniformsUI
      focus,
      blender: layer.color,
      depth: layer.depth,
    }
  })
}