Skip to content

Shaders in Axmol

RH edited this page Dec 27, 2024 · 8 revisions

Shaders in Axmol 2.0+

The workflow for shaders has changed in version 2 of Axmol.

The language of the shader file being written needs to be in ESSL v310 or GLSL v450.

Axmol now automatically processors shaders at build time using axslcc fork of glslcc, which outputs new shaders that are suitable for the target platforms, such as Desktop GL(GLSL330), GLES3(ESSL300), GLES2(GLSL100), Apple Metal(MSL). Supported file extensions for shaders are (.vert, .vsh) for vertex and (.frag, .fsh) for fragment.

The generated shader files will be located in ${CMAKE_BINARY_DIR}/runtime/axslc. The axslc contents will then be copied to the target app_res_root/axslc. Do not change the content of this generated folder, since it is included into the final package (APK etc.). Axmol will automatically add the axslc path to the engine search path via FileUtils.

The generated shaders will have adjusted filenames, removing the original file extension, and replacing it with either _fs or _vs, for fragment and vertex shaders respectively.

For example, if the shaders are named MyEffect.fsh and MyEffect.vsh, then the processed shaders will be saved as MyEffect_fs and MyEffect_vs. In code, you would reference these new names, not the original shader filenames.

Example vertex shader:

#version 310 es

layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;

layout(location = 0) out vec2 v_texCoord;

layout(std140) uniform vs_ub {
    mat4 u_MVPMatrix;
};

void main()
{
    gl_Position = u_MVPMatrix * a_position;
    v_texCoord = a_texCoord;
}

Example fragment shader:

#version 310 es
precision highp float;
precision highp int;

layout(location = 0) in vec2 v_texCoord;
layout(binding = 0) uniform sampler2D u_tex0;
layout(location = 0) out vec4 FragColor;

void main()
{
    FragColor =  texture(u_tex0, v_texCoord);
}

Notes:

  • All non-sampler uniforms must in uniform block, because glslcc(spirv) limits
  • Recommended use of macros for layout locations to ensure they are same between vertex and fragment shaders, otherwise the Metal renderer will crash due to layout location mismatch.

Examples of macro usage:

Vertex shader:

layout(location = POSITION) in vec4 a_position;
layout(location = TEXCOORD0) in vec2 a_texCoord;

layout(location = TEXCOORD0) out vec2 v_texCoord;

layout(std140) uniform vs_ub {
    mat4 u_MVPMatrix;
};

void main()
{
    gl_Position = u_MVPMatrix * a_position;
    v_texCoord = a_texCoord;
}

Fragment shader:

#version 310 es
precision highp float;
precision highp int;

layout(location = TEXCOORD0) in vec2 v_texCoord;
uniform sampler2D u_tex0;
layout(location = SV_Target0) out vec4 FragColor;

void main()
{
    FragColor =  texture(u_tex0, v_texCoord);
}

Migration Tips from Axmol 1.0 or Cocos2d-x Shaders

  • both vertex & fragment shader should insert version declaration #version 310 es in header
  • fragment shader needs precision declarations for float & int
  • the qualifier attribute in vertex shader change to in
  • the qualifier varying in vertex shader change to out
  • the qualifier varying in fragment shader change to in
  • the gl_FragColor needs to be replaced by a defined out vec4 variable in fragment shader

For working examples of the new shader format, review the Axmol built-in shaders here.

Limitations

  • Please only write 1 uniform block per shader stage. Multi-uniform blocks are not implemented in the Metal backend.

Custom shaders

To use your own custom shaders in your project, you simply need to place the shaders into {project_root}/Source/shaders/.

Once the project is built, the shaders are compiled into the correct output format and placed into the build folder. In your code, you reference the custom shaders by prefixing the filename with custom/.

For example, if your shaders are {project_root}/Source/shaders/MyEffect.fsh and {project_root}/Source/shaders/MyEffect.vsh, then the shaders will be processed and saved into ${CMAKE_BINARY_DIR}/runtime/axslc/custom as MyEffect_fs and MyEffect_vs. Note that you should not manually modify the contents of the ${CMAKE_BINARY_DIR}/runtime/axslc/ folder.

To load them from code, you would use the following:

auto* program = ProgramManager::getInstance()->loadProgram("custom/MyEffect_vs", "custom/MyEffect_fs");
auto* programState = new ProgramState(program);
return programState;

Remember, the custom/ prefix only applies to custom shaders. For shaders included in the Axmol engine source, you would simply reference them by their name, such as:

auto* program = ProgramManager::getInstance()->loadProgram("positionColor_vs", "positionColor_fs");

The filenames of shaders included with Axmol are listed in the file Shaders.cpp/Shaders.h, for easier referencing in your code. Using the above example with the built-in positionColor vert and frag shaders, you could also load them via the following:

auto* program = ProgramManager::getInstance()->loadProgram(ax::positionColor_vert, ax::positionColor_frag);

Sprite batching when using custom shaders

By default, sprites with custom shaders applied are not automatically batched, meaning that the sprite will take up an extra draw call. It is possible to enable batching, and it is completely under the control of the developer to enable it for the sprites using the same custom shader and uniform data.

The reason batching is disabled by default for custom shaders is because there is no way to know if the uniform data passed to the shader is exactly the same between instances of a ProgramState. The data must be exactly the same to allow for batching, and from this data a unique hash is calculated, which is stored in ProgramState::_batchId. If the _batchId value is equal between multiple instances of the same Program and ProgramState, then it is possible to batch these instances into a single draw call.

Calculating the hash that represents the batch ID of the uniform data is an expensive process, so it is left up to the developer to decide when and where this occurs in their code.

To calculate and update the batch ID of a ProgramState instance, simply call ProgramState::updateBatchId(). This will calculate and store the value in ProgramState::_batchId. This would need to be done on each and every unique instance of ProgramState, with one exception, if and only if the ProgramState instances are all cloned from the same ProgramState instance after the batch ID is updated, and the uniform data of all the clones does not change.

An example of this is in cpp-tests > Renderer > RendererUniformBatch, where 3 different shaders are used for the drawn sprites. Two of the shaders are custom, and the third is a built-in shader. The UI takes up 6 draw calls, so any draw calls over that value would be as a result of the sprites being drawn. As you can see from the following screenshot, the total number of draw calls is 9, so 3 extra draw calls, one for each shader program being used:

image

Refer to the code in the cpp-tests project, RendererUniformBatch::RendererUniformBatch(), for how it is implemented.

Batching is affected by the order of the sprites being rendered, so if at any point a sprite or other object is drawn that uses a different shader program, then that will break the current batch, and result in an additional draw call. This isn't specific to custom shaders, but rather how the rendering works. An example of this would be test cpp-tests > Renderer > RendererUniformBatch 2, which draws the sprites while applying a random shader program to each, and because of this, the number of draw calls varies due to the order they are drawn, as you can see in the following screenshot (84 draw calls):

image