Postprocessing

-- This demo renders a scene to a canvas, then renders the canvas to screen filtered through a shader.
-- Set this to false to see the scene with no postprocessing.
local useCanvas = true

-- A shader program consists of a vertex shader (which describes how to transform polygons)
-- and a fragment shader (which describes how to color pixels).
-- For a full-screen shader, the vertex shader should just pass the polygon through unaltered.
-- This is the same as the "default" full-screen shader used by lovr.graphics.plane:
local screenShaderVertex = [[
  vec4 position(mat4 projection, mat4 transform, vec4 vertex) {
    return vertex;
  }
]]

-- For the fragment shader: We are going to create a separable gaussian blur.
-- A "separable" blur means we first blur horizontally, then blur vertically to get a 2D blur.
local screenShaderFragment = [[
  // This one-dimensional blur filter samples five points and averages them by different amounts.
  // Weights and offsets taken from http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/

  // The weights for the center, one-point-out, and two-point-out samples
  #define WEIGHT0 0.2270270270
  #define WEIGHT1 0.3162162162
  #define WEIGHT2 0.0702702703

  // The distances-from-center for the samples
  #define OFFSET1 1.3846153846
  #define OFFSET2 3.2307692308

  // UVs are sampled from a texture over the range 0..1.
  // This uniform is set outside the shader so we know what UV distance "one pixel" is.
  uniform vec2 resolution;

  // This uniform will be set every draw to determine whether we are sampling horizontally or vertically.
  uniform vec2 direction;

  // lovr's shader architecture will automatically supply a main(), which will call this color() function
  vec4 color(vec4 graphicsColor, sampler2D image, vec2 uv) {
    vec2 pixelOff = direction / resolution;
    vec4 color = vec4(0.0);
    color += texture(image, uv) * WEIGHT0;
    color += texture(image, uv + pixelOff * OFFSET1) * WEIGHT1;
    color += texture(image, uv - pixelOff * OFFSET1) * WEIGHT1;
    color += texture(image, uv + pixelOff * OFFSET2) * WEIGHT2;
    color += texture(image, uv - pixelOff * OFFSET2) * WEIGHT2;
    return color;
  }
]]

-- The vertex and fragment shaders will be combined together into a shader program
local screenShader

-- Image of an eyechart
local eyechart

-- This table will contain two canvases we will use as scratch space
local tempCanvas

function lovr.load()
  -- Load the eyechart image
  -- Source: https://www.publicdomainpictures.net/view-image.php?image=244244&picture=eye-chart-test-vintage
  -- Creative Commons 0 / Public Domain license
  local texture = lovr.graphics.newTexture('eye-chart-test-vintage-cc0.jpg')
  local textureWidth, textureHeight = texture:getDimensions()
  eyechart = {
    scale = .75,
    aspect = textureHeight / textureWidth,
    material = lovr.graphics.newMaterial( texture )
  }

  -- Configure the shader
  if useCanvas then
    local width, height = lovr.headset.getDisplayDimensions()

    -- Compile the shader
    screenShader = lovr.graphics.newShader(screenShaderVertex, screenShaderFragment)

    -- Set the resolution uniform
    screenShader:send("resolution", {width, height})

    -- Create two temporary canvases
    tempCanvas = {
      lovr.graphics.newCanvas(width, height),
      lovr.graphics.newCanvas(width, height)
    }

    tempCanvas[1]:getTexture():setWrap('clamp')
    tempCanvas[2]:getTexture():setWrap('clamp')
  end
end

-- The scene is drawn in this callback
local function sceneDraw()
  lovr.graphics.clear() -- Because we are drawing to a canvas, we must manually clear
  lovr.graphics.setShader(nil)

  -- Draw text on the left and right
  for _, sign in ipairs {-1, 1} do
    lovr.graphics.push()
    lovr.graphics.rotate(sign * math.pi/2, 0, 1, 0)
    lovr.graphics.print("MOVE CLOSER", 0, 0, -10, 5)
    lovr.graphics.pop()
  end

  lovr.graphics.plane(eyechart.material, 0, 1.7, -1, eyechart.scale, eyechart.scale * eyechart.aspect)
end

-- This simple callback is used to draw one canvas onto another
local function fullScreenDraw(source)
  lovr.graphics.fill(source)
end

function lovr.draw()
  if not useCanvas then

    -- No-postprocessing path: Call scene-draw callback without doing anything fancy
    sceneDraw()

  else

    -- Start by drawing the scene to one of our temp canvases.
    tempCanvas[1]:renderTo(sceneDraw)

    -- We now have the scene in a texture (a canvas), which means we can apply a full-screen effect
    -- by rendering the texture with a shader material. However, because our blur is separable,
    -- we will need to do this twice, once for horizontal blur and once for vertical.
    -- We would also like to do multiple blur passes at larger and larger scales, to get a blurrier blur.
    -- To achieve these many passes we will render from canvas A into B, and then B back into A, and repeat.
    lovr.graphics.setShader(screenShader)

    screenShader:send("direction", {1, 0})
    tempCanvas[2]:renderTo(fullScreenDraw, tempCanvas[1])

    screenShader:send("direction", {0, 1})
    tempCanvas[1]:renderTo(fullScreenDraw, tempCanvas[2])

    screenShader:send("direction", {2, 0})
    tempCanvas[2]:renderTo(fullScreenDraw, tempCanvas[1])

    screenShader:send("direction", {0, 2})
    tempCanvas[1]:renderTo(fullScreenDraw, tempCanvas[2])

    screenShader:send("direction", {4, 0})
    tempCanvas[2]:renderTo(fullScreenDraw, tempCanvas[1])

    screenShader:send("direction", {0, 4})
    lovr.graphics.fill(tempCanvas[2]) -- On the final pass, render directly to the screen.
  end
end