Shaders

Edit

Shaders are small GPU programs that can be used to customize rendering or perform arbitrary computation. This guide explains how shaders work in LÖVR.

GLSL

Shaders are written in GLSL. GLSL is a lower-level language than Lua, so shaders are a bit harder to learn at first. Explaining GLSL is outside the scope of these docs, but some good ways to learn it are:

LÖVR uses GLSL version 4.60.

Basics

There are 2 types of shaders, given by ShaderType:

Shaders have one or more "stages", which are basically functions, given by ShaderStage:

Each Pass has an active shader it uses to process draws. Pass:setShader changes the active shader. The active shader will affect all draws until the shader is changed again. When the active shader is nil, LÖVR will use a built-in shader based on the type of draw (unlit for meshes, font for text, etc.).

The set of shaders built in to LÖVR are given by DefaultShader.

Writing Shaders

LÖVR uses the lovrmain function as the GLSL entrypoint. For vertex stages, lovrmain should return the final transformed vertex position. Here's the default vertex shader:

vec4 lovrmain() {
  return Projection * View * Transform * VertexPosition;
}

It can also be written using the DefaultPosition shorthand:

vec4 lovrmain() {
  return DefaultPosition;
}

The vertex position is multiplied by several matrices to get it into "normalized device coordinates", which is the coordinate space the GPU uses internally to process vertices.

For fragment shaders, lovrmain should return the final color of the pixel. Here's the default fragment shader:

vec4 lovrmain() {
  return Color * getPixel(ColorTexture, UV);
}

It can also be written using the DefaultColor shorthand:

vec4 lovrmain() {
  return DefaultColor;
}

The default pixel color is calculated by multiplying the Color from the vertex stage (which includes the vertex color, material color, and pass color) with a pixel sampled from the color texture.

Compute shaders implement the void lovrmain() function, and don't return anything.

Shaders are created with lovr.graphics.newShader, with the code for each stage:

shader = lovr.graphics.newShader([[
  vec4 lovrmain() {
    return DefaultPosition;
  }
]], [[
  vec4 lovrmain() {
    return DefaultColor;
  }
]])

The code can also be loaded from a filename or a Blob. Also, a DefaultShader can be used for the code of one or both of the stages:

shader = lovr.graphics.newShader('vertex.glsl', 'unlit')

Finally, for advanced use, lovr.graphics.newShader takes a raw option that will use raw GLSL code without any LÖVR helpers:

shader = lovr.graphics.newShader([[
  #version 460
  void main() {
    //
  }
]], { raw = true })

Shader Builtins

The following built-in variables and macros are available in vertex and fragment shaders:

Name Type Notes
PI float
TAU float (PI * 2)
PI_2 float (PI / 2)
Resolution vec2 The size of the render pass texture, in pixels.
Time float Current time in seconds (lovr.headset.getTime).
CameraPositionWorld vec3 The position of the current view (Pass:setViewPose).
Sampler sampler The default sampler (Pass:setSampler).

The following built-in variables are available only in vertex shaders:

Name Type Notes
VertexPosition vec4 The local position of the current vertex.
VertexNormal vec3 The normal vector of the current vertex.
VertexUV vec2 The texture coordinate of the current vertex.
VertexColor vec4 The color of the current vertex.
VertexTangent vec4 The tangent vector of the current vertex (w is sign of tangent basis).
Projection mat4 The projection matrix of the current view (Pass:setProjection).
View mat4 The view matrix of the current view (Pass:setViewPose).
ViewProjection mat4 The projection matrix multiplied with the view matrix.
InverseProjection mat4 The inverse of the projection matrix.
Transform mat4 The model matrix (includes transform stack and draw transform).
NormalMatrix mat3 Transforms normal vectors from local space to world space.
ClipFromLocal mat4 Transforms from local space to clip space (Projection * View * Transform).
ClipFromWorld mat4 Transforms from world space to clip space (Projection * View).
ClipFromView mat4 Transforms from view space to clip space (Projection).
ViewFromLocal mat4 Transforms from local space to view space (View * Transform).
ViewFromWorld mat4 Transforms from world space to view space (View).
ViewFromClip mat4 Transforms from clip space to view space (InverseProjection).
WorldFromLocal mat4 Transforms from local space to world space (Transform).
WorldFromView mat4 Transforms from view space to world space (inverse(View)).
WorldFromClip mat4 Transforms from clip space to world space (inverse(ViewProjection)).
PassColor vec4 The color set with Pass:setColor.

The following built-in variables and macros are available only in fragment shaders:

Name Type Notes
PositionWorld vec3 The position of the pixel, in world space.
Normal vec3 The normal vector of the pixel, in world space.
Color vec4 The vertex, material, and pass colors multiplied together.
UV vec2 The texture coordinate of the current pixel.
Tangent vec4 The tangent vector of the current pixel, in world space.
TangentMatrix mat3 The tangent matrix, used for normal mapping.

The properties of the active material, set using Pass:setMaterial, can be accessed in vertex and fragment shaders. Textures can be sampled using the getPixel helper function. The Material and lovr.graphics.newMaterial pages have more info on these properties.

Name Type Notes
Material.color vec4 The material color.
Material.glow vec4 The material glow color (alpha is glow strength).
Material.uvShift vec2 The material UV shift.
Material.uvScale vec2 The material UV scale.
Material.metalness float The material metalness.
Material.roughness float The material roughness.
Material.clearcoat float The material clearcoat factor.
Material.clearcoatRoughness float The roughness of the clearcoat layer.
Material.occlusionStrength float The strength of the occlusion texture.
Material.normalScale float The strength of the normal map texture.
Material.alphaCutoff float The alpha cutoff.
ColorTexture texture2D The color texture.
GlowTexture texture2D The glow texture.
OcclusionTexture texture2D The ambient occlusion texture.
MetalnessTexture texture2D The metalness texture.
RoughnessTexture texture2D The roughness texture.
ClearcoatTexture texture2D The clearcoat texture.
NormalTexture texture2D The normal map.

Shader Inputs

It's also possible to send values or objects from Lua to a Shader. There are a few different ways to do this, each with their own tradeoffs (speed, size, ease of use, etc.).

Uniforms

Shaders can declare uniforms, which can be booleans, numbers, vectors, or matrices. These have a constant or "uniform" value for all vertices/pixels that are drawn. They are easy to use and inexpensive to update, but they must be resent every frame and whenever the shader changes.

Uniforms are declared in shader code with the uniform keyword, and can be set with Pass:send:

function lovr.load()
  shader = lovr.graphics.newShader('unlit', [[
    uniform vec4 color1;
    uniform vec4 color2;

    vec4 lovrmain() {
      // Apply a vertical gradient using the 2 colors from the uniforms:
      return mix(color1, color2, dot(Normal, vec3(0, 1, 0)) * .5 + .5);
    }
  ]])
end

function lovr.draw(pass)
  pass:setShader(shader)
  pass:send('color1', { 1, 0, 1, 1 })
  pass:send('color2', { 0, 1, 1, 1 })
  pass:sphere(0, 1.7, -2)
end

When the active shader is changed, uniforms with the same name and type will be preserved.

Vertex Attributes

Vertex attributes are the data for each vertex of a mesh. They should be used for data that varies on a per-vertex basis. Attributes have a name, a type, and a location (an integer ID). LÖVR uses the following default vertex attributes for shapes and meshes:

Name Type Location
VertexPosition vec4 10
VertexNormal vec3 11
VertexUV vec2 12
VertexColor vec4 13
VertexTangent vec4 14

Custom vertex attributes can be declared like this:

in vec3 customAttribute;

The data in a buffer can then be associated with the attribute, by name:

vertices = lovr.graphics.newBuffer(vertexCount, {
  { type = 'vec3', name = 'customAttribute' }
})

Buffers

Shaders can access data in Buffer objects. Buffers can store large arrays of data from Lua tables or Blobs. The GPU can also write to buffers using compute shaders.

Data in buffers can be accessed in 2 ways:

First, the buffer should be declared in the shader. Here's an example declaring a uniform buffer:

uniform Colors {
  vec4 colors[100];
};

And an example storage buffer:

buffer Colors {
  vec4 colors[100];
};

First the uniform or buffer keyword is used to declare which type of buffer it is, followed by the name of the variable. Finally, there is a block declaring the format of the data in the buffer, which should match the format used to create the Buffer in Lua (structs can be used if the buffer has multiple fields per element).

A Buffer can be sent to one of the above variables like this:

-- palette is a table with 100 colors in it
buffer = lovr.graphics.newBuffer('vec4', palette)

-- bind it to the shader later
pass:send('Colors', buffer)

The shader can then use the colors array to access the data from the palette table.

There is a very handy Shader:getBufferFormat method that will return a Buffer format from a variable in a shader, so you don't have to duplicate it in the Lua code.

It's possible to bind a subset of a buffer to the shader by passing the range as extra arguments to Pass:send.

Textures

Shaders can also access data from Texture objects. Similar to buffers, textures can be accessed in 2 ways:

Sampled textures are declared like this:

uniform texture2D myTexture;

The texture type can be texture2D, textureCube, texture2DArray, or texture3D (see TextureType).

Storage textures are declared like this:

uniform image2D myImage;

A texture can be sent to the shader variable using Pass:send.

The getPixel helper function can be used to sample from a texture:

getPixel(myTexture, UV)

This will sample from the texture using the UV coordinates and the default sampler set using Pass:setSampler. It's the same as writing this for 2D textures:

texture(sampler2D(myTexture, Sampler), UV)

It's also possible to declare a custom sampler variable and use it to sample textures:

uniform sampler mySampler;

// texture(sampler2D(myTexture, mySampler), UV)

A Sampler object can be sent to the shader using Pass:send, similar to buffers and textures.