bjornbytes / shh

Spherical Harmonics Helpers
MIT License
3 stars 0 forks source link
spherical-harmonics

SHH 🤫

SHH (Spherical Harmonics Helpers) is a Lua/LÖVR library for computing spherical harmonics and using them for lighting. It uses compute shaders to generate spherical harmonics on the GPU at realtime speeds.

Spherical What Now?

Spherical harmonics basically store a "ball of light" using some fancy math and a very small amount of data. They're similar to a skybox, but more blurry and only take up about 100 bytes! They're a quick, cheap way to add soft diffuse lighting to objects so they look like they belong in a scene.

Here is a blog post that goes into more of the math behind spherical harmonics, with great diagrams and sample code!

Example

local shh = require 'shh'

function lovr.load()
  skybox = lovr.graphics.newTexture('sky.hdr', {
    usage = { 'storage', 'sample' }
  })

  SH = shh.new(skybox)

  model = lovr.graphics.newModel('armordillo.glb')
end

function lovr.draw(pass)
  pass:skybox(skybox)
  shh.setShader(pass, SH)
  pass:draw(model)
end

Ball Lit By Skybox

Usage

First, copy the shh.lua file to your project and require it.

To create a new spherical harmonics object, use SH = shh.new(arg). The argument can be:

A SH object is simply a table of 9 basis vectors, so you can access SH[x][y] to get the raw coefficient values (where x is 1-9 and y is 1-3).

Spherical harmonics objects have the following methods:

Lighting

There are a few ways to use spherical harmonics for lighting.

SHH provides a convenience function shh.setShader(pass, SH) which will set a shader on pass that will light objects using the SH object.

However, spherical harmonics lighting can be integrated into your own shaders and combined with direct lighting, PBR materials, etc. To make this easier, SHH has a shader.glsl file that can be included in custom shaders. It defines an evaluateSH function that takes a spherical harmonics basis and a direction vector, and returns a color:

#include "shh/shader.glsl"

layout(set = 2, binding = 0) uniform SH { vec3 sh[9]; };

vec4 lovrmain() {
  vec3 color = evaluateSH(sh, normalize(Normal)) / PI;
  return vec4(color, 1.);
}

Note that when doing lighting, you probably want to divide the result from evaluateSH by PI. This applies the Lambertian BRDF which gets the values in a more appropriate range.

Advanced Compute Shader API

To generate spherical harmonics from a Texture using a compute shader, use shh.compute:

shh.compute(pass, texture, buffer, bufferOffset)

Example:

local pass = lovr.graphics.newPass()
local skybox = lovr.graphics.newTexture('skybox.hdr', { usage = 'storage' })
local buffer = lovr.graphics.newBuffer('vec4', 9)
shh.compute(pass, skybox, buffer)
lovr.graphics.submit(pass)
SH = shh.new(buffer:getData())

-- or, compute SH dynamically every frame:
function lovr.draw(pass)
  shh.compute(pass, skybox, buffer)
  shh.setShader(pass, buffer)
  pass:draw(model)
end

This is a lower-level method that will run a compute shader on pass that computes spherical harmonics coefficients from texture and writes them to buffer (at offset bufferOffset, which defaults to zero). It can be used to compute spherical harmonics for many textures on the GPU in parallel.

Like shh.new, the texture should be a cubemap or an equirectangular texture. It must have the storage usage. Currently its format must be rgba8, rgba16f, rgba32f, or rg11b10f.

Tip: You can pass a texture view to this function to compute spherical harmonics from a smaller mipmap level of a cubemap. This usually gives roughly the same results but is much faster.

The buffer should use the vec4 format or the vec3 format with the std140 layout. 144 bytes will be written to the buffer.

The buffer can be used directly in a shader, or, after submitting the pass, the coefficients can be read back to the CPU using Buffer:getData. Sending the buffer directly to a shader will avoid any costly readbacks and keep all the data on the GPU.

License

MIT, see the LICENSE file for details.