This is a short introduction about how to make a basic Vertex+Fragment shader working in LÖVE, using Lua language + LÖVE specific shaders language.
I managed to mix some vertex and fragment shaders for the first time, and using #
LÖVE, #
Lua multimedia framework.
Using fragments in LÖVE for some time (also called pixel shaders in LÖVE), for some time, more unusual in my experience, it was hard for me to found any receipe to use vertex shaders (even Khronos example use old libs incompatible with today version), and fragment at the same time. I would like to try this for years. So I share a simple example and some explaination, hoping it will help others. I mixed several uncomplete examples I found around until I managed to make it work.
Sources are available on Framagit, showing some exemple of shaders, will grow with time. And the
specific file discussed here, I tried to comment it extensively.
The syntax of
shaders in LÖVE is near from, but doesn't totally follow standard GLSL one. In recent version, the community changed to be more compatible, for example by using the keyword
uniform
to accept data send by application to
Khronos GLSL.
I try to give here only the most essentials elements to achieve this, and to avoid to describe too deeply how work 3D or shaders in general, to keep the tutorial short. The usage of vertex shader for what is done in this example is not the best choice, but is fast to put in place and to proove this work, that's the only goal :).
Simple shapeSome variables are defined at the begin of the code, as explained here. We will use regular polygon as simpler example with N vertices. So we set it in a constant, and define an array for the vertices and a shape object for the whole object.
local NUMBER_OF_VERTICES = 6
local vertices={} -- vertices of the shape
local shape -- shape object itself
The shape itself is build here by the function
function build_shape(nbrV)
, In short, it places n vectors around a closed circle (2×π), by dividing this lenght by the number of segment, to determine the step between vertices.
for i=0,nbrV do
a=pi2*i/nbrV -- compute angle depending on vertices/(2×pi)
By default format used in shape for itch vertex is 2d geometric vector (x,y), followed by uvmapping vector (u,v), and then it's color (c):
vertices[i] = { cos(a),sin(a), -- vertex coordinates (projection of angle a x,y
cos(a),sin(a), -- UVmapping texture coordinates, needed for fragment shading
1,1,1} -- white R,G,B (lua don't need
They can be redefined to only have to define geometrics coordinates and colors, using
vertexformat
definition (attributes table) that will be passed to
newMesh() function, but we need UV coordinates too for fragment shader, pixel colors computation in our example.
Here is an exemple for 2d coords + RGBA colors VertexFormat:
local attributes = {
{"VertexPosition", "float", 2},
{"VertexColor", "byte", 4},
}
The shape ils then finally created in the
function load()
, we use there the version without vertexFormat attributes:
shape = lg.newMesh(vertices, "fan","dynamic")
Shaders themselvesThis variable will be used as a pointer to the both vertex and fragment compiled shaders.
local shader -- will be used for compiled shaders
We have 2 shaders, the vertex shader, we define with
local vertshader=[[…]]
and the fragment shader
local fragshader = [[…]]
, and the 2 shaders is compiled in one pass, in
load()
function (
don't compile them at each frame!!!!) via the function:
shader = lg.newShader(fragshader, vertshader)
The syntax of the shaders is not Lua, but C, as used in GLSL, even if variables names are a bit differents in the case of LÖVE.
Vertex shaderIn the Vertex Shader,
uniform
are values sent by application to shaders.
varying
are used to transmit values between shaders.
local vertshader = [[
uniform number time; // Given by Lua script
varying vec4 vColor; // Need a variable to transmit vertex colors to fragment shader
In the Lua code, the value time is given by the following function call in
function love()
:
shader:send("time",love.timer.getTime()) -- sent time parameter to shaders
Still in the Vertex shader, in the position function we get projection matrix (transfor_projection) and vertex positions. We must put the assignation of VertexColor to varying variable vColor in this function. This function is also used to transform vertex of the object on which the shader apply to.
vec4 position(mat4 transform_projection, vec4 vertex_position)
{
// VerteColor can be transformed depending on shape before transforming depending on coords
vColor = VertexColor; // put back vertex colors in variable for reusage in fragment shader
As a simple exemple, so we make a simple rotation around the
Z axis (so on the plan of the screen).
If you are not familiar with linear algebra, you need to learn at least vector and matrix multiplication, they are not so complex and can't be avoided to understand basic maths needed for 3D. In short, transformation in space are made by 3 rotation matrix, that are multiplied one after each other. to simplify the computation we generaly put them premultiplied in one only matrix that contain also scale, scale and shear factors.
As we only do a 2D rotation here it will be simplier. we rotate around the Z axis, so only x and y coordinates will b changed:
// The operation to make a rotation around z axis of angle a:
// |x'| = | cos(a) sin(a)| × |x|
// |y'| |-sin(a) cos(a)| |y|
transform_projection[0][0]=cos(time); // the angle change with time here
transform_projection[0][1]=sin(time); // to have a rotation animation
transform_projection[1][0]=-sin(time);
transform_projection[1][1]=cos(time);
return transform_projection * vertex_position; // applying transformation
Warning note for people with at least basic linear algebra knowledge: In OpenGL Matrix columns and lines are inverted in regard to mathematics usage, but at the same time, the matrix is at left and the vector is vertical ar right instead of horizontal at left.
Fragment shaderWe still get the time uniform from Lua, and the vec4 vColor from the vertex shader:
uniform number time; // given by Lua script
varying vec4 vColor; // given by vertex shader
Then we made various transform in the effect function. here we get color of the sufrace, 2d coordinates of the textures (we need UV map for this to work) and coordinates of the whole GL viewport (window or screen). Using 2d coordinates of the texture is important here, as we want the computation of the texture to stuck on the object and follow it's moves, not to be stuck in front of screen, so we reuse vertex color information given by vertex shader, on which we made some transformation:
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
return vec4(
cos(texture_coords.x*20) * sin(texture_coords.y*20), // Red (only vary on x,y space)
cos(texture_coords.x*20) * sin(texture_coords.y*20) + 0.5*sin(time),// Green (vary with time)
vColor.b/2.0 + 0.25*cos(time*2.0) * cos(texture_coords.x*200), // Blue (vertical bars)
1.0); // Alpha (fully opaque)
}
Finally, we need to say on which shape we want to apply the shader, this can be changed at each frame. So in
love.draw()
:
lg.setShader(shader) -- set current shader to apply it to next drawing(s)
lg.draw(shape, -- draw the shape (with shaders)
lg.getWidth()/2, lg.getHeight()/2) -- screen/window centered
lg.setShader() -- unset current shader (to draw other things that will not use it.
The
lg.setShade()
here is used to stop to apply any shader effect. So we can draw other shapes with or without textures, without having to be transformad by the effect of the shader.
That's all.