Shaders
Shader is a set of programs that run directly on graphics adapter. Each program from the set is called sub-shader. Sub-shaders linked with render pass, each render pass defines "where" to draw an object. "where" means that you can set up your own render pass and the renderer will use the sub-shader with your render pass. For the ease of use, there are a number of predefined render passes.
Shaders have properties of various types that can be used together with materials to draw an object.
Shaders language
The engine uses GLSL shading language for every sub-shader. There are many GLSL guides over the internet, so there is no need to "re-post" the well-documented info again.
There are very few differences:
- No need to define a version of the shader. Every shader source will be pre-processed, and it will get the correct version automatically. Preprocessing is needed because the same shader could run on OpenGL and WebGL (OpenGL ES) which have some differences.
- There is a "standard" library of useful methods that is automatically included in every shader source at preprocessing stage. The library source could be found here. It is well documented, and you may find some functions useful for your job.
- There's automatic generation of uniform buffers for property groups (see this section for more info).
Structure
Shader has a rigid structure that could be described in this code snippet:
(
name: "MyShader",
// A set of resources, the maximum amount of resources is limited by your GPU. The engine
// guarantees, that there could at least 16 textures and 16 resource groups per shader.
resources: [
(
// Each resource binding must have a name.
name: "diffuseTexture",
// Value has limited set of possible variants.
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "properties",
kind: PropertyGroup([
(
name: "diffuseColor",
kind: Color(r: 255, g: 255, b: 255, a: 255),
),
]),
binding: 0
),
// The following property groups are built-in and provides useful data for each shader.
(
name: "fyrox_instanceData",
kind: PropertyGroup([
// Autogenerated
]),
binding: 1
),
(
name: "fyrox_boneMatrices",
kind: PropertyGroup([
// Autogenerated
]),
binding: 2
),
(
name: "fyrox_graphicsSettings",
kind: PropertyGroup([
// Autogenerated
]),
binding: 3
),
(
name: "fyrox_cameraData",
kind: PropertyGroup([
// Autogenerated
]),
binding: 4
),
(
name: "fyrox_lightData",
kind: PropertyGroup([
// Autogenerated
]),
binding: 5
),
],
// A set of render passes (see a section `Render pass` for more info)
passes: [
(
// Name must match with the name of either standard render pass (see below) or
// one of your passes.
name: "Forward",
// A set of parameters that regulate renderer pipeline state.
// This is mandatory field of each render pass.
draw_parameters: DrawParameters(
// A face to cull. Either Front or Back.
cull_face: Some(Back),
// Color mask. Defines which colors should be written to render target.
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
// Whether to modify depth buffer or not.
depth_write: true,
// Whether to use stencil test or not.
stencil_test: None,
// Whether to perform depth test when drawing.
depth_test: Some(Less),
// Blending options.
blend: Some(BlendParameters(
func: BlendFunc(
sfactor: SrcAlpha,
dfactor: OneMinusSrcAlpha,
alpha_sfactor: SrcAlpha,
alpha_dfactor: OneMinusSrcAlpha,
),
equation: BlendEquation(
rgb: Add,
alpha: Add
)
)),
// Stencil options.
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
// Scissor box. Could be something like this:
//
// scissor_box: Some(ScissorBox(
// x: 10,
// y: 20,
// width: 100,
// height: 30
// ))
scissor_box: None
),
// Vertex shader code.
vertex_shader:
r#"
layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = fyrox_instanceData.worldViewProjection * vec4(vertexPosition, 1.0);
}
"#,
// Fragment shader code.
fragment_shader:
r#"
out vec4 FragColor;
in vec2 texCoord;
void main()
{
FragColor = properties.diffuseColor * texture(diffuseTexture, texCoord);
}
"#,
)
],
)
Shader should contain at least one render pass to actually do some job. A shader could not have properties at all. Currently only vertex and fragment programs are supported. Each program mush be written in GLSL. Comprehensive GLSL documentation can be found here
The engine can load such shaders if you save it in a file with .shader
extension. After that, you can assign
the shader to your material in the Material Editor:
Alternatively, you can load the shader from code. To do this, you can use this code:
#![allow(unused)] fn main() { fn load_shader(resource_manager: &ResourceManager) -> ShaderResource { resource_manager.request::<Shader>("path/to/my/cool.shader") } }
After that, you can use the shader to build a material from it:
#![allow(unused)] fn main() { fn create_material(resource_manager: &ResourceManager) -> MaterialResource { let shader = resource_manager.request::<Shader>("path/to/my/cool.shader"); MaterialResource::new(Material::from_shader(shader)) } }
This material instance can be used for rendering. For example, you can assign it a surface of some mesh.
Render pass
Modern rendering is a very complex thing that requires drawing an object multiple times with different "scripts". For example, to draw an object with shadows, you need to draw an object twice: one directly in a render target, and one in a shadow map. Such stages called render passes.
Binding of shaders to render passes is done via names, each render pass has a unique name.
Predefined render passes
Predefined render passes helps you to create your own shader without a need to create your own render pass and to quickly start writing your shaders.
- GBuffer - A pass that fills a set with render target sized textures with various data about each rendered object. These textures then are used for physically based lighting. Use this pass when you want the standard lighting to work with your objects.
- Forward - A pass that draws an object directly in a render target. It could be used to render translucent objects.
- SpotShadow - A pass that emits depth values for an object, later this depth map will be used to render shadows.
- PointShadow - A pass that emits distance from a fragment to a point light, later this depth map will be used to render shadows.
- DirectionalShadow - A pass that emits depth values for an object, later this depth map will be used to render shadows for directional light sources using cascaded shadow mapping.
Resources
Each shader requires a specific set of external resources that will be used during the rendering.
This set is defined in resources
section of the shader and could contain the following resources:
Texture
- a texture of arbitrary typePropertyGroup
- a group of numeric properties.
Binding points
Shader resource must define a unique (over its type) binding index. The engine will use these points to prepare appropriate resource descriptor sets for GPU. Keep in mind, that binding point indices are unique per each type of resource. This means that a set of texture resources could use the same indices as property groups. The binding points must be unique in its group. If there is more than one resource of a certain type, that shares the same binding point, the engine will refuse to use such a shader.
Built-in resources
There are a number of built-in resources, that Fyrox will try to assign automatically if they're defined in your shader, something like this:
(
name: "fyrox_instanceData",
kind: PropertyGroup([
// Autogenerated
]),
binding: 1
),
The full list of built-in resources is defined below.
fyrox_instanceData
Property group. Provided for each rendered surface instance.
Name | Type | Description |
---|---|---|
worldMatrix | mat4 | Local-to-world transformation. |
worldViewProjection | mat4 | Local-to-clip-space transform. |
blendShapesCount | int | Total amount of blend shapes. |
useSkeletalAnimation | bool | Whether skinned meshes is rendering or not. |
blendShapesWeights | vec4[32] | Blend shape weights. |
fyrox_boneMatrices
Property group. Provided for each rendered surface, that has skeletal animation.
Name | Type | Description |
---|---|---|
matrices | mat4[256] | Bone matrices |
fyrox_cameraData
Property group. Contains camera properties. It contains info not only about scene camera, but also observer info when rendering shadow maps. In other words - it is generic observer properties.
Name | Type | Description |
---|---|---|
viewProjectionMatrix | mat4 | World-to-clip-space transformation. |
position | vec3 | World-space position of the camera. |
upVector | vec3 | World-space up-vector of the camera. |
sideVector | vec3 | World-space side-vector of the camera. |
zNear | float | Near clipping plane location. |
zFar | float | Far clipping plane location. |
zRange | float | zFar - zNear |
fyrox_lightData
Property group. Available only in shadow passes.
Name | Type | Description |
---|---|---|
lightPosition | vec3 | World-space light source position. Only for shadow passes. |
ambientLightColor | vec4 | Ambient lighting color of the scene. |
fyrox_lightsBlock
Property group. Information about visible light sources
Name | Type | Description |
---|---|---|
lightCount | int | Total amount of light sources visible on screen. |
lightsColorRadius | vec4[16] | Color (xyz) and radius (w) of light source |
lightsParameters | vec2[16] | Hot-spot cone angle cos (x) and half cone angle cos (y) |
lightsPosition | vec3[16] | World-space light position. |
lightsDirection | vec3[16] | World-space light direction |
fyrox_graphicsSettings
Property group. Contains graphics options of the renderer.
Name | Type | Description |
---|---|---|
usePom | bool | Whether to use parallax occlusion mapping or not. |
fyrox_sceneDepth
Texture. Contains depth values of a scene. Available only after opaque geometry is rendered (read - G-Buffer is filled). Typical usage is something like this:
(
name: "fyrox_sceneDepth",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 1
),
Code generation
Fyrox automatically generates code for resource bindings. This is made specifically to prevent subtle mistakes. For example, when you define this set of resources:
(
name: "MyShader",
resources: [
(
name: "diffuseTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
(
name: "normalTexture",
kind: Texture(kind: Sampler2D, fallback: Normal),
binding: 1
),
(
name: "properties",
kind: PropertyGroup([
(
name: "texCoordScale",
kind: Vector2((1.0, 1.0)),
),
(
name: "diffuseColor",
kind: Color(r: 255, g: 255, b: 255, a: 255),
),
]),
binding: 0
),
]
)
The engine generates the following code and adds it to source code of every shader in every render pass automatically:
uniform sampler2D diffuseTexture;
uniform sampler2D normalTexture;
struct Tproperties {
vec2 texCoordScale;
vec4 diffuseColor;
};
layout(std140) uniform Uproperties { Tproperties properties; }
The most important thing is that the engine keeps properties in the struct Tproperties
in
correct order and forces std140
layout on the generated uniform block. Since the engine knows
the layout of the properties from their definition section, it could easily form a memory block
with all required alignments and paddings that could be uploaded to GPU. The next important thing
is that the engine batches all the data needed into large chunks of data and uploads them
all at once, which is much faster.
Drawing parameters
Drawing parameters defines which GPU functions to use and at which state. For example, to render transparent objects, you need to enable blending with specific blending rules. Or you need to disable culling to draw objects from both sides. This is when draw parameters come in handy.
There is a relatively large list of drawing parameters, and it could confuse a person who didn't get used to work with graphics. The following list should help you to use drawing parameters correctly.
cull_face
:- Defines which side of polygon should be culled.
- Possible values:
None
,Some(CullFace::Back)
,Some(CullFace::Front)
color_write
:- Defines which components of color should be written to a render target
- Possible values:
ColorMask { .. }
depth_write
:- Whether to modify depth buffer or not.
- Possible values:
true/false
stencil_test
:- Whether to use stencil test or not.
- Possible values:
None
Some(StencilFunc { .. })
depth_test
:- Whether to perform depth test when drawing.
- Possible values:
true/false
blend
:- Blending options.
- Possible values:
None
Some(BlendFunc { .. } )
stencil_op
:- Stencil options.
- Possible values:
StencilOp { .. }
Vertex shader
Vertex shader operates on single vertices, it must provide at least the position of the vertex in clipping space. In other words, it has to do at least this:
layout(location = 0) in vec3 vertexPosition;
layout(location = 1) in vec2 vertexTexCoord;
out vec2 texCoord;
void main()
{
texCoord = vertexTexCoord;
gl_Position = fyrox_instanceData.worldViewProjection * vertexPosition;
}
This is the simplest vertex shader, using vertex shaders you can create various graphical effects that affect vertices.
Pixel Shader
Pixel shader (or more precisely - fragment shader), operates on a small fragment of your render target. In general, pixel shaders write some color to a render target (or multiple targets) using some program.
out vec4 FragColor;
void main()
{
FragColor = vec4(1, 0, 0, 1);
}
This is the simplest pixel shader, it just fills the render target with red color.