inconvergent

Depth of Field with Colour Shift

The last time I wrote, I described a relatively easy way to get a nice depth of field effect. Let's see how we can add a colour shift effect as well. I've tried to repeat most of the relevant information here, but you might want to read the previous post before you continue.

colour shift animation

We can modify the depth of field algorithm so that you offset the rendered point in two dimensions, not in 3D. That is, we offset it inside a circle, not a sphere. This has the added benefit of requiring slightly fewer calculations. And it will look quite similar.

We assume that we have the same definitions as before:

  • A line in 3D, l=[a, b];
  • a camera at position, c;
  • a focus distance from the camera, f
  • a distance function, dst(a, b) that yields the distance between a and b;
  • a function, lerp(a, b, s) = a + s*(b-a), which interpolates between a and b;
  • a function, rnd(), that yields random numbers between 0 and 1.

In addition, we need the following new functions:

  • rndNorm(), returns a normally distributed random number with a zero mean and unit variance. ;
  • rndNorm2D(r) = , returns r * [rndNorm(), rndNorm()]
  • clamp(v, l, h), returns min(h, max(l, v));
  • pow(a, b), returns a to the power of b;
  • cross(a, b), returns a.x*b.y - a.y*b.x;
  • hsvToRgb(h, s, v) converts a HSV value to an RGB value;
  • nrm(v), scales v to unit length; and
  • proj(v), projects v from 3D into 2D.

The modified algorithm is as follows:

; a point on l
v = lerp(a, b, rnd())
; projection of v on the canvas
q = proj(v)
; sample radius based on distance to camera
; larger m yields more blur
; start with e=1 and adjust
r = m * pow(abs(f - dst(v, c)), e)
; random vector
o = rndNorm2D(r)
; position to draw on canvas
w = q + o
; mid is the middle of your canvas
x = cross(nrm(q-mid), o)

Finally we are ready to obtain the colour value. We assume HSV where where hue, saturation and value range between 0 and 1. This also assumes a dark background. To obtain different results, you will have to experiment.

; start with he=1, and adjust
; lower hs yields more colour variation
rr = pow(abs(r) / hs, he)
; select your colours
if r < 0:
  ; cyan-ish
  hueStart = 0.5
else:
  ; magenta-ish
  hueStart = 0.833

hue = clamp(hueStart + rr, 0, 1)

; start with se=1, and adjust
; higher ss yields lower saturation
sat = clamp(pow(abs(r) / ss, se), 0, 1)

; value = 1
rgb = hsvToRgb(hue, sat, 1)

Now that we have an RGB colour we can draw a point at w using rgb. As in the previous post, this method relies on sampling a large number of points from l, and drawing those points with a low alpha value.

colour shift rendering
Colour shift rendering

There are a number of factors in here that will depend on the 3D shapes you are drawing, the desired output resolution as well as your programming environment. The trickiest part might be to scale the values of hs, he, ss and se. Keep in mind that you want hue and sat to fall largely between 0 and 1. The clamp functions are there for safety.

Hopefully this is enough to give you some ideas.


  1. Note that this is a description of how my code works, it is not an "out of the box" solution.
  2. This is a little simplified. My projection function, proj(v), uses orthographic projection. Because of this, the distance function does not calculate the distance from p to c. Instead it calculates the distance from p to the point where p hits the camera plane. Using the euclidean distance will also work. Depending on how you project, you might notice that the focal area is curved.
  3. One way to get this is to use the Box-Muller transform. Many random number libraries will also provide this readily.
  4. This depends a lot on what tools you use. My images use an orthographic projection. Mostly because it is not too complicated to implement.