Writing Shaders

Revision as of 05:59, 14 May 2022 by MoKaLux (talk | contribs) (tryin' to decipher shaders documentation hell)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

As you may know, most of graphics hardware now allow processing of user code directly on the GPU. This means two things:

  • You can relieve the CPU from performing some graphics-related computation by letting the GPU perform those computations and thus gain overall performance in your app.
  • You can achieve beautiful graphic effects by modifying the per-pixel rendering process.

Those little pieces of code you can upload to the GPU are called shaders.

Gideros uses a shader system (a programmable pipeline), which uses five distinct shaders:

  • the Basic shader handle shapes with a constant color
  • the Color shader handle shapes with per-vertex colors (mostly used by Mesh sprite)
  • the Texture shader handle textured shapes (Bitmaps)
  • the TextureColor shader handle textured and per-vertex colored shapes
  • the Particle shader deals with Box2D particle systems

The shader API allows replacing the default shader used by Gideros with a custom one, on a sprite per sprite basis. As with most of Gideros API, this one is straightforward: create a Shader object and assign it to one or several sprites.

local myShader=Shader.new(shader parameters)

That said, since Gideros will use your shader as if it was the standard one, you will have to make sure that your custom shader is compatible with the standard one, which essentially means that it takes the same input parameters.

Gideros users can access the shaders system themselves, opening a broad range of possibilities. The rendering pipeline has several chained processing stages, which is why we call it a pipeline. A few stages are programmable, each requiring a particular type of program or shader. Gideros uses and lets user access two of them:

The Vertex Shader

The first processing stage that Gideros deals with is the Vertex shader. As its name implies, its purpose is to manipulate vertices for the geometry being rendered. No real big deal here, most of the work of a vertex shader is transforming geometry coordinates from sprite local space to actual window coordinates. Other input data is most of the time just passed along unmodified to the next stage.

The Vertex shaders typically uses the following attributes:

  • Vertex coordinates (always)
  • Texture coordinates (if required)
  • Vertex color (if required)

The Fragment Shader (also called pixel shader)

The fragment shader is the most interesting. It takes its inputs from the vertex shader outputs but is applied to every pixel of the shape being rendered. Input data from the vertex shader is interpolated and then fed to the fragment shader. The final goal of the fragment shader is to produce the color of the pixel to actually draw.

The Fragment shaders typically uses the following attributes:

  • Interpolated texture coordinates (if required)
  • Interpolated pixel color (if required)

Shader Programs

Plenty of literature is available to learn writing shaders, so we won’t go into language details here.

Shaders can be written in either Lua (recommended) or in the Shader language for the platform you are coding for (eg. GLSL, HLSL or MTL).

Shader Language for the Platform you are coding for

The API allows the creation of Shader objects from within Lua. The Shader.new() constructor takes five arguments:

Gideros will search the assets for a file with the supplied name, automatically adding the extension relevant for the target platform:

  • .glsl for OpenGL
  • .cso or .hlsl for DirectX

The Flag 'Shader.FLAG_FROM_CODE' can be used to let Gideros know that the string is actually Shader code, not contained within a file.

Lua Shaders (recommended)

If you write your shader in Lua then it will automatically be re-written in the appropriate shader language for the platform. This makes it ideal for games that are multi-platform as you only have to code the shader once.

If you decide to write directly for the platforms then start with the HLSL version which is in many ways more restrictive than the GLSL (it is far easier to port HLSL code to GLSL than the opposite way).

GLSL comes in two flavours: the regular language developed for desktop platforms, and the GLSL for an embedded system, aka OpenGL ES 3.0. The latter is often more strict and needs additional precision modifiers. Gideros helps you to deal with those differences by removing precision modifiers if run on a desktop platform and setting the appropriate language version. That way you only need to write an OpenGL ES 3.0 compliant shader.

If you are a beginner, it is better to start with a working shader, from an example or from this documentation, and then modify it to suit your needs.

You can avoid common compatibility pitfalls by reading this: https://www.opengl.org/wiki/GLSL_:_common_mistakes

Each shader processes one or more stream of data, which we will call attributes as per OpenGL semantics, and further produce one or more stream of (processed) data.

Each shader also has access to one or more constants called uniforms. Those constants are settable at will before invoking the shader program, so that they are in fact only constants from the shader program point of view, and because their values can’t be changed in the middle of the processing of a stream of data.

Creating a Shader

Typical Lua Vertex and Fragment shader code:

function vertex(vVertex,vColor,vTexCoord)
	local vertex = hF4(vVertex,0.0,1.0)
	return vMatrix*vertex

function fragment()
	local frag=lF4(fColor)*texture2D(fTexture, fTexCoord)
	local coef=lF3(0.2125, 0.7154, 0.0721)
	local gray=dot(frag.rgb,coef)
	if (frag.a==0.0) then discard() end
	return frag

Changing Uniforms/Constants

In order to change the value of a uniform from Lua, use the setConstant function, it takes three arguments:

  • the uniform name
  • the type of data to set (one of the Shader.Cxxx constants)
  • the actual data to set, either as a table or as multiple arguments

Associating a Shader to a Sprite

The Sprite API has a new call to deal with that: Sprite:setShader(shader) tells Gideros to use the specified shader for rendering the sprite. Setting back the shader to nil actually revert to the default shader.

Further reading: