Shaders in AppGameKit Studio

AppGameKit Studio uses shaders written in the OpenGL 2.0 Shading Language (GLSL version 110) on desktop platforms, and OpenGL ES 2.0 shaders on mobile platforms. Any shaders that you write yourself must be compatible with both desktop and mobile versions, thankfully OpenGL 2.0 and OpenGL ES 2.0 are almost identical in this regard. However you cannot use the "#version" definition sometimes used on desktop platforms, or the "precision highp float" definition sometimes used on mobile platforms, these will be added automatically by AppGameKit Studio.

Vulkan Shaders

AppGameKit Studio automatically converts your GLSL shaders to SPIRV format. This means you only need to create one set of shaders for your app and it will work fine when rendering in either OpenGL and Vulkan.

Types of Shader

Everything in AppGameKit Studio requires a shader to appear on the screen, this includes sprites, text, and 3D objects. For 2D items it uses the same shader for everything to improve performance, but for 3D objects AppGameKit Studio will look at the object properties, such as the number of textures it uses, and create a shader for it. These are refered to as Default Shaders and are the most common way of displaying things in AppGameKit Studio, if you want to see the code that AppGameKit Studio created for an object you can use GetObjectMeshVSSource and GetObjectMeshPSSource, and use this as the basis for a Custom Shader.

Custom shaders are shaders that you write yourself and load into AppGameKit Studio, this is to cover use cases that AppGameKit Studio can't handle with its Default Shaders. These are loaded with LoadShader. Note that AppGameKit Studio will not check the suitablility of the shader for the objects it is applied to, so if your shader uses 2 textures, but the object only has 1 texture, then AppGameKit Studio won't warn you and the object may not render correctly.

Sprite shaders are loaded with LoadSpriteShader, which is a convenience function that loads a custom pixel shader but uses a standard vertex shader that is suitable for most sprites. If you wanted to modify the vertex shader you can use LoadShader and apply it to a sprite, but be aware that vertex shaders used for 3D objects will not work on sprites. The default sprite vertex shader looks like this

attribute highp vec4 position;
attribute mediump vec4 color;
attribute mediump vec2 uv;
varying mediump vec2 uvVarying;
varying mediump vec4 colorVarying;
uniform highp mat4 agk_Ortho;
 
void main() 
{
    gl_Position = agk_Ortho * position;
    uvVarying = uv;
    colorVarying = color;
}

The default pixel shader for sprites is

uniform sampler2D texture0;
varying mediump vec2 uvVarying;
varying mediump vec4 colorVarying;
 
void main()
{
    gl_FragColor = texture2D(texture0, uvVarying) * colorVarying;
}

Fullscreen shaders are loaded with LoadFullScreenShader, which is another convenience function that loads a custom pixel shader but uses a standard vertex shader that is suitable for full screen quads. Again you could modify the vertex shader by using LoadShader and applying it to a quad. The default quad vertex shader looks like this

attribute highp vec3 position;
varying mediump vec2 uvVarying;
uniform mediump vec4 uvBounds0;
uniform mediump float agk_invert; // used to correct for render images being inverted
 
void main() 
{
    gl_Position = vec4(position.xy*vec2(1.0,agk_invert),0.5,1.0);
    uvVarying = (position.xy*vec2(0.5,-0.5) + 0.5) * uvBounds0.xy + uvBounds0.zw;
}

The default pixel shader for fullscreen quads does nothing to the image and looks like this

uniform sampler2D texture0;
varying mediump vec2 uvVarying;
 
void main()
{
    gl_FragColor = texture2D(texture0, uvVarying);
}

All the shaders so far form what is called a Base Shader, telling AppGameKit Studio about the known attributes, such as number of textures. However there are unknown factors like the number of lights affecting the object that need to be in the shader, but which you don't always know in advance. As such AppGameKit Studio takes the given Base Shader and creates what we call a Generated Shader, which fills in any missing details before using it to draw. Typically this only applies to 3D objects which have a variable numbers of lights shining on them at any one time. Generated Shaders are temporary and may be thrown away if the scene changes, always using the given Base Shader to create a suitable Generated Shader for the current scene. This does mean that if the Base Shader is a Default Shader, then AppGameKit Studio is actually doing two rounds of shader creation, firstly to create the Default Shader and then using that as a base to create the Generated Shader. Using GetObjectMeshVSSource and GetObjectMeshPSSource will only ever return the Base Shader, never the Generated Shader, as these can change frequently and are scene dependent.

When writing a Custom Shader it is useful to know how the shader creation process works so you can tell AppGameKit Studio what you want, or don't want, to be in the final Generated Shader. You can also exclude everything, in which case AppGameKit Studio will recognise there is nothing to add and will use your Custom Shader directly to draw the object, skipping the whole Generated Shader path. This is done by using none of the functions from the next section.

Generated Shaders

There are currently three additional elements that AppGameKit Studio adds dynamically to shaders, these are Vertex Lighting, Pixel Lighting, and Fog. These are added if a particular function is declared in the shader, if the function is not declared then that element is not added. For example the function for vertex lighting is

mediump vec3 GetVSLighting( mediump vec3 normal, highp vec3 pos );

If this function is declared anywhere in the vertex shader then AppGameKit Studio will add a definition for the function before using it to draw an object. Its contents will vary depending on how many vertex lights affect that object. The function declaration must appear exactly as above, with the same spacing and case, for it to be detected. A custom vertex shader that uses this function might look like this

attribute highp vec3 position;
attribute mediump vec3 normal;
attribute mediump vec2 uv;
 
varying highp vec3 posVarying;
varying mediump vec3 normalVarying;
varying mediump vec2 uvVarying;
varying mediump vec3 lightVarying;
 
uniform highp mat3 agk_WorldNormal;
uniform highp mat4 agk_World;
uniform highp mat4 agk_ViewProj;
uniform mediump vec4 uvBounds0;
 
mediump vec3 GetVSLighting( mediump vec3 normal, highp vec3 pos );
 
void main()
{ 
    uvVarying = uv * uvBounds0.xy + uvBounds0.zw;
    highp vec4 pos = agk_World * vec4(position,1.0);
    gl_Position = agk_ViewProj * pos;
    mediump vec3 norm = normalize(agk_WorldNormal * normal);
    posVarying = pos.xyz;
    normalVarying = norm;
    lightVarying = GetVSLighting( norm, posVarying );
}

Note the GetVSLighting function is declared but not defined, AppGameKit Studio will define it at runtime. In this case the lighting information is passed directly to the pixel shader, but you can do anything you like to the value. The GetVSLighting function makes no changes to any global values, it only returns a value. Also note that the call to GetVSLighting must occur after setting the gl_Position value, the lighting function makes use of this value.

The function for pixel lighting looks like this

mediump vec3 GetPSLighting( mediump vec3 normal, highp vec3 pos );

and the function for fog is

mediump vec3 ApplyFog( mediump vec3 color, highp vec3 pointPos );

Again they must appear exactly as above, with the same spacing and case. A pixel shader that uses both pixel lighting and fog might look like this

uniform sampler2D texture0;
 
varying highp vec3 posVarying;
varying mediump vec3 normalVarying;
varying mediump vec2 uvVarying;
varying mediump vec3 lightVarying;
 
mediump vec3 GetPSLighting( mediump vec3 normal, highp vec3 pos );
mediump vec3 ApplyFog( mediump vec3 color, highp vec3 pointPos );
 
void main()
{ 
    mediump vec3 norm = normalize(normalVarying);
    mediump vec3 light = lightVarying + GetPSLighting( norm, posVarying ); 
    
    mediump vec3 color = texture2D(texture0, uvVarying).rgb * light;
    color = ApplyFog( color, posVarying );
    
    gl_FragColor = vec4(color,1.0);
}

In this case we receive the lighting from the vertex shader in the lightVarying variable then add the pixel shader lighting to make up the final light value. You can use these values for any purpose, as mentioned earlier the lighting functions do not modify any global state, they only return a value. The same applies to fog.

If you wish your object to use lighting then it is recommended that you use both the vertex and pixel lighting functions unless you can be sure that all the lights will be of a particular type (vertex or pixel). Note that the global directional light (sun) is handled in the vertex shader function.

The fog function takes the desired pixel color and the pixel position in the world and returns a new color for this pixel based on the amount of fog it received. The fog function is only valid in the pixel shader, and will be ignored if placed in the vertex shader. If you turn off fog in AppGameKit Studio then the fog function will be removed from the shader to improve performance. It will be re-added if you turn fog back on.

AppGameKit Studio Shader Variables

AppGameKit Studio will recognise certain variable names in shaders and fill them with useful values that you can use in your shaders. Here is a list of the variables and a description of what they represent

Variables must be declared with the same case as shown above, and can be used in either the vertex or pixel shader, or both. They must be preceeded by the uniform keyword. These will be updated every time the shader is used to drawn an item.

Shader Constants

You can declare your own shader variables using the uniform keyword and set them using the AppGameKit Studio commands SetShaderConstantByName and SetShaderConstantArrayByName, see the documentation for these commands to see the parameters they use. Setting a shader value using these commands will change it permanently for all draw calls until you change it to something else.

You can also set a shader value on a per object basis by using SetObjectShaderConstantByName and SetObjectShaderConstantArrayByName. In which case the shader will use this value when drawing the specified object, but return to the default value when drawing other objects. To return an object to using the the default use SetObjectShaderConstantDefault.

Shader Precision

You can set the precision of each variable in a shader using the keywords highp, mediump, lowp, and it is recommended you use them to get the right trade off between performance and accuracy. In general position values should use highp, whilst normals and UVs (or anything that will always use small float values) should use mediump. Color values can use lowp if they will always be in the range 0 to 1, if there is a chance they will go beyond this range then they should use mediump. By default AppGameKit Studio sets all vertex shader values to use highp and all pixel shader values to use mediump.