Al Jazari – ambient occlusion

A big part of the look of Minecraft comes from it’s Smooth lighting, which includes an illumination model called ambient occlusion. Ambient occlusion darkens areas of an object based on how obscured they are from a wide area light source, for example an entire sky area, as opposed to a point light source. This is often complicated to calculate in realtime, but with simplified geometric voxels it’s fairly fast to do, building on the code from the hollowing out optimisation I mentioned previously. For each point on a cube’s corners you can add up it’s surrounding empty cubes – the more empty space in the 8 voxels, the brighter the corner needs to be. In this way, we are assuming light is coming from all directions (hence ambient) and don’t need to take into account positions of lights etc.

occ

Here is the raw ambient occlusion lighting in Al Jazari 2, rendered as unlit vertex colours on each visible cube:

aj2-2

Then we add standard direct lighting to the ambient occlusion:

aj2-3

And here is the final image with textures added:

aj2-4

There are a few artefacts in the lighting due to the cubes not all being the same size (in order to minimise the complexity of what is drawn), here is the world coloured based on the size of the octree leaf node – the brighter blue areas are bigger cubes:

aj2-1

Once the ambient occlusion is calculated we only need to recalculate it when surrounding voxels are changed. In practice the splitting/joining of the octree levels when blocks are created and destroyed seems to take care of this in most cases.

;; returns a list of samples to test
(define (occ-samples pos) 
    (list 
     (vadd pos (vector  -1 -1 -1))
     (vadd pos (vector  -1 -1  0))
     (vadd pos (vector  -1  0 -1))
     (vadd pos (vector  -1  0  0))
     (vadd pos (vector   0 -1 -1))
     (vadd pos (vector   0 -1  0))
     (vadd pos (vector   0  0 -1))
     (vadd pos (vector   0  0  0))))

;; count up the empty voxels and calculate occlusion value
(define (calc-occlusion o pos)
  (let* ((samples (occ-samples pos))
         (coverage (/ (foldl
                       (lambda (p r)
                         (+ (if (octree-leaf-empty? (octree-ref o p)) 1 0) r))
                       0
                       samples)
                     (length samples))))
    (min 1 (* 2 coverage))))

;; helper to set a bunch of verts in one go
(define (pdata-list-set! k l v)
  (for-each (lambda (i) (pdata-set! k i v)) l))

...

;; uses the cube's position and size to set the vertex colour on each corner
(with-primitive my-cube
  (translate pos)
  (pdata-list-set! "c" '(3 7 19) (calc-occlusion o (vadd pos (vector 0 0 0))))
  (pdata-list-set! "c" '(10 14 21) (calc-occlusion o (vadd pos (vector size size size))))
  (pdata-list-set! "c" '(2 4 23) (calc-occlusion o (vadd pos (vector size 0 0))))
  (pdata-list-set! "c" '(9 15 17) (calc-occlusion o (vadd pos (vector 0 size size))))
  (pdata-list-set! "c" '(0 8 18) (calc-occlusion o (vadd pos (vector 0 size 0))))
  (pdata-list-set! "c" '(5 13 22) (calc-occlusion o (vadd pos (vector size 0 size))))
  (pdata-list-set! "c" '(6 12 16) (calc-occlusion o (vadd pos (vector 0 0 size))))
  (pdata-list-set! "c" '(1 11 20) (calc-occlusion o (vadd pos (vector size size 0)))))