Blur

SourceEdit
-- This demo renders a scene to a canvas, then renders the canvas to screen filtered through a shader.
--
-- Sample contributed by andi mcc

local applyBlur = true -- Set this to false to see the scene with no postprocessing.

-- 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 blurShader = [[
  // 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

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

  // The texture to sample from.
  uniform texture2DArray sourceTexture;

  // lovr's shader architecture will automatically supply a main(), which will call this lovrmain() function
  vec4 lovrmain() {
    vec2 uvOffset = direction / Resolution.xy; // Convert the offset from pixels to UVs
    vec4 color = vec4(0.0);
    color += getPixel(sourceTexture, UV, ViewIndex) * WEIGHT0;
    color += getPixel(sourceTexture, UV + uvOffset * OFFSET1, ViewIndex) * WEIGHT1;
    color += getPixel(sourceTexture, UV - uvOffset * OFFSET1, ViewIndex) * WEIGHT1;
    color += getPixel(sourceTexture, UV + uvOffset * OFFSET2, ViewIndex) * WEIGHT2;
    color += getPixel(sourceTexture, UV - uvOffset * OFFSET2, ViewIndex) * 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 textures we will use as scratch space
local tempTexture

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,
    texture = texture
  }

  -- Configure the objects needed for the blur
  if applyBlur then
    local width, height = lovr.headset.getDisplayDimensions()
    local layers = lovr.headset.getViewCount()

    -- Compile the shader
    screenShader = lovr.graphics.newShader('fill', blurShader)

    -- Create two temporary textures
    tempTexture = {
      lovr.graphics.newTexture(width, height, layers, { mipmaps = false }),
      lovr.graphics.newTexture(width, height, layers, { mipmaps = false })
    }

    -- Make a clamping sampler (clampler, get it?) to prevent blurred
    -- pixels from wrapping around the edges
    clampler = lovr.graphics.newSampler({ wrap = 'clamp' })
  end
end

-- The scene is drawn in this callback
local function sceneDraw(pass)
  -- Draw text on the left and right
  for _, sign in ipairs { -1, 1 } do
    pass:push()
    pass:rotate(sign * math.pi / 2, 0, 1, 0)
    pass:text('MOVE CLOSER', 0, 0, -10, 5)
    pass:pop()
  end

  -- Draw the eye chart
  pass:setMaterial(eyechart.texture)
  pass:plane(0, 1.7, -1, eyechart.scale, eyechart.scale * eyechart.aspect)
end

-- This simple function is used to render a render pass that
-- draws one texture onto another with the blur shader
local function fullScreenDraw(source, destination, blurSize)
  local pass = lovr.graphics.newPass({ destination, depth = false, samples = 1 })
  pass:setSampler(clampler)
  pass:setShader(screenShader)
  pass:send('sourceTexture', source)
  pass:send('direction', blurSize)
  pass:fill()
  return pass
end

function lovr.draw(pass)
  if not applyBlur then
    -- No-postprocessing path: Call scene-draw callback without doing anything fancy
    sceneDraw(pass)
  else
    local passes = {}

    -- Start by drawing the scene to one of our temp textures.
    local scene = lovr.graphics.newPass(tempTexture[1])

    -- Make the scene pass use the same cameras as the headset
    for i = 1, pass:getViewCount() do
      scene:setViewPose(i, pass:getViewPose(i))
      scene:setProjection(i, pass:getProjection(i, mat4()))
    end

    sceneDraw(scene)

    table.insert(passes, scene)

    -- We now have the scene in a texture, which means we can apply a full-screen effect by
    -- rendering the texture with a shader. 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 texture 1 into 2, and then 2 back into 1, and repeat.

    table.insert(passes, fullScreenDraw(tempTexture[1], tempTexture[2], { 1, 0 }))
    table.insert(passes, fullScreenDraw(tempTexture[2], tempTexture[1], { 0, 1 }))

    table.insert(passes, fullScreenDraw(tempTexture[1], tempTexture[2], { 2, 0 }))
    table.insert(passes, fullScreenDraw(tempTexture[2], tempTexture[1], { 0, 2 }))

    table.insert(passes, fullScreenDraw(tempTexture[1], tempTexture[2], { 4, 0 }))
    table.insert(passes, fullScreenDraw(tempTexture[2], tempTexture[1], { 0, 4 }))

    -- Finally, draw the blurred texture to the main display
    pass:fill(tempTexture[1])

    table.insert(passes, pass)

    return lovr.graphics.submit(passes)
  end
end