Shading

Shaders (sometimes also called materials) are required to draw objects to the screen (or into a texture). kool comes with a set of general purpose shaders as well as its own shading language to write custom shaders.

Table of contents

  1. Builtin shaders
    1. Common options
    2. Lit shader options
    3. KslPbrShader
    4. KslBlinnPhongShader
    5. KslUnlitShader
  2. KSL - kool shading language
    1. Why KSL
    2. Example KSL shader
    3. Compute shaders
  3. Deferred shading

Builtin shaders

kool’s builtin shaders provide easy-to-use materials suitable for most standard use-cases.

graph TD
    A[KslBuiltinShader]
    A --> B[KslLitShader]
    A --> C[KslUnlitShader]

    B --> D[KslPbrShader]
    B --> E[KslBlinnPhongShader]
  
    style A stroke-dasharray: 5 5
    style B stroke-dasharray: 5 5

Currently, there are three different builtin shaders available (described in detail further below):

All builtin shaders are written in KSL, the same DSL you can use to write your own custom shaders.

All builtin shaders have a variety of configuration options, which you can use to customize their behavior. Shaders are typically created by a factory function, which provides the configuration API:

val myShader = KslPbrShader {
    color { uniformColor(MdColor.BLUE.toLinear()) }
}

After creation, all shaders are compiled and executed directly on the GPU. This means that, after creation, basic configuration settings cannot be changed anymore. It is however possible to change assigned values later on (textures, colors, etc.).

Depending on their type, shaders share several configuration options.

Common options

Apply to all builtin shaders.

The list of configuration options presented here is not exhaustive. There are a few more rather advanced options used for animations, morphing, etc. Try digging into the code in case you want to find out more.

color { ... }

Configures the color source. Color sources can be:

  • textureColor() - Use a texture. Requires the mesh to include texture coordinates.
  • vertexColor() - Use a vertex attribute. Requires the mesh to include per-vertex color values.
  • instanceColor() - Use an instance attribute. Only applicable to instanced meshes.
  • uniformColor() - Use a single uniform color for the entire mesh.
  • constColor() - Similar to uniformColor() but hardcodes the color value in the shader code making it immutable.

Most shaders work with linear color space, while common color constants usually assume sRGB color space. Mismatching color spaces can result in awkward looking colors (usually either very dull or dark and oversaturated). Use Color.toLinear() / Color.toSrgb() in such a case.

It is also possible to combine multiple color sources using various blend modes:

color {
    // Use a texture as base color
    textureColor(someTexture)
    // Tint the texture by multiplying another color to it
    uniformColor(MdColor.PINK.toLinear(), blendMode = ColorBlockConfig.BlendMode.Multiply)
}

pipeline { ... }

Optional block

Configures various pipeline settings, which affect how the object is drawn into the scene. In most cases the default values should be fine and the pipeline block can be omitted.

  • blendMode - Enables / disables alpha-blending. Defaults to BlendMode.BLEND_MULTIPLY_ALPHA
  • cullMethod - Determines which triangle sides should be culled. Defaults to BlendMode.CULL_BACK_FACES
  • depthTest - Determines the depth testing function. Defaults to DepthCompareOp.LESS_EQUAL
  • isWriteDepth - Enables / disables updating the depth buffer when the object is drawn.

A common case is to disable depth-testing for a certain object. This can be achieved by setting depthTest = DepthCompareOp.ALWAYS and isWriteDepth = false.

vertices { ... }

Optional block

Configures various vertex-transform related settings.

  • isInstanced - Enables mesh instancing for this shader.
  • displacement() - Can be used to provide a displacement texture to add geometry detail.

Lit shader options

The lit shader options are all related to lighting and, hence, don’t apply to KslUnlitShader.

normalMapping { ... }

Optional block

Can be used to enable and set a material specific normal map (sometimes also referred to as bump map).

shadow { ... }

Optional block

Can be used to enable and set one or more shadow maps.

ao { ... }

Optional block

Configures ambient occlusion (AO). AO comes in two flavors: First, materials can have a static ao map (similar to other material properties like color or a normal map). Second, in case screen-space ambient occlusion (SSAO) is enabled, the SSAO map contains the dynamic scene-specific AO component.

Both AO types can be configured in this block via materialAo { ... } and enableSsao() respectively.

emission { ... }

Optional block

Can be used to enable and set a material specific emission map.

KslPbrShader

KslPbrShader provides a general purpose shader for lit materials following the physical based rendering (PBR) approach. PBR shaders try to approximate the physical properties of real materials more closely than older shading techniques like, e.g., Blinn-Phong shading. This typically results in a more realistic look of rendered objects. On the downside PBR shading is quite a bit more expensive than simpler shading models. So, in case you are targeting low-end and / or mobile devices, you should consider using a Blinn-Phong shader instead.

kool’s PBR shader follows the metallic workflow, meaning it has two basic material properties:

  • Roughness: A value ranging from 0 (very smooth) to 1 (very rough).
  • Metallic: A value ranging from 0 (dielectric, i.e. non-metallic, e.g. plastic) to 1 (pure metal).

To assign a PBR shader to a mesh you can use the corresponding factory function:

mesh.shader = KslPbrShader {
    color { /* color config */ }

    // Use constant roughness / metallic values
    metallic(0f)
    roughness(0.5f)

    // or use textures providing the roughness / metallic values
    roughness { textureProperty(materialRoughnessMap) }
    // You can also use a uniform property, which allows changing the property
    // after shader construction
    metallic { uniformProperty(1f) }

    // Optionally, you can set additional settings
    normalMapping { setNormalMap(materialNormalMap) }
    shadow { addShadowMap(someShadowMap) }
    ao {
        materialAo { textureProperty(materialAoMap) }
        enableSsao(sceneSpaceAoMap)
    }
}

KslBlinnPhongShader

KslBlinnPhongShader provides a general purpose shader for lit materials following the traditional Blinn-Phong method. It is less sophisticated than PBR shading but quite a bit faster making it more suitable for low-end and / or mobile devices.

KslBlinnPhongShader has three specific material properties:

  • Shininess: A value ranging from 0 to infinity, describing how shiny the surface is.
  • Specular strength: A multiplier (typically ranging from 0 to 1) affecting the strength of the specular lighting term.
  • Specular color: A color value, which is multiplied to the specular lighting term (white by default).

Assigning a Blinn-Phong shader works similar to PBR shaders:

mesh.shader = KslBlinnPhongShader {
    color { /* color config */ }

    // Use constant roughness / metallic values
    shininess(50f)
    specularStrength(1f)

    // or use textures providing the roughness / metallic values
    shininess { textureProperty(materialShininessMap) }
    // You can also use a uniform property, which allows changing the property
    // after shader construction
    specularStrength { uniformProperty(1f) }

    // Optionally, you can set additional settings
    normalMapping { setNormalMap(materialNormalMap) }
    shadow { addShadowMap(someShadowMap) }
    ao {
        materialAo { textureProperty(materialAoMap) }
        enableSsao(sceneSpaceAoMap)
    }
}

KslUnlitShader

KslUnlitShader is a general purpose shader, that does not incorporate any lighting model. Instead, the material source color is forwarded more or less unmodified by the fragment shader (apart from optional color-space conversion).

Unlit shaders are typically used for UI overlays, navigation grids, wireframes etc.

Assigning an unlit shader works similar to lit shaders. Since there are no light-related properties, you often only set the color source:

mesh.shader = KslUnlitShader {
    color { /* color config */ }
}

KSL - kool shading language

KSL is quite powerful and offers pretty much the same feature set as GLSL, although the syntax is sometimes a bit more complicated. However, documentation is still very much incomplete. In case you want to dive deep you should take a look at the source code of the builtin shaders, to get an idea about how things work.

Why KSL

Maintaining a dedicated DSL to write arbitrarily complex shaders might seem a bit overkill at first. However, the benefit with this is that shader code also is multi-platform:

Besides OpenGL, kool also has a WebGPU backend, which requires shaders to be written in WGSL. Although WGSL follows the same concepts as GLSL (and all the other shader languages as well), the syntax is very different. By using a DSL, the same shader logic can be transformed into GLSL and WGSL. No need to maintain multiple shader sources for different backends! This approach should even work for more shading languages, like, e.g., metal.

Example KSL shader

Here’s a minimal example for a custom KSL shader:

val customShader = KslShader("Hello world shader") {
    val interStageColor = interStageFloat4()
    vertexStage {
        main {
            val mvp = mvpMatrix()
            val localPosition = float3Var(vertexAttribFloat3(Attribute.POSITIONS))
            outPosition set mvp.matrix * float4Value(localPosition, 1f.const)
            interStageColor.input set vertexAttribFloat4(Attribute.COLORS)
        }
    }
    fragmentStage {
        main {
            colorOutput(interStageColor.output)
        }
    }
}

If you ever wrote a shader before the structure should be familiar: The shader consists of a vertex stage (responsible for projecting the individual mesh vertices onto the screen) and a fragment stage (responsible for computing the output-color for each pixel covered by the mesh). This example shader is almost as simple as a valid shader can be: It uses a pre-multiplied MVP matrix to project the vertex position attribute to the screen. Moreover, the color attribute is taken from the vertex input and forwarded to the fragment shader via interStageColor. The fragment stage then simply takes the color from interStageColor and writes it to the screen.

A little more complex example is available in the HelloKsl demo.

Compute shaders

So far, this chapter discussed only regular shaders used for drawing geometry. A different kind of shaders are compute shaders, which can be used to offload compute workload to the GPU. kool and KSL also support compute shaders. Examples are available in the HelloCompute and Bee demos.

Deferred shading

The deferred rendering pipeline is somewhat deprecated at the moment and will probably change significantly in the future. Documentation is therefore very limited.

So far, all discussed shaders use traditional forward rendering. Another option to do the rendering is deferred shading which can be cheaper and allows for more advanced lighting effects.

To use deferred shading, you need to use an appropriate shader. Currently, the only builtin option is DeferredKslPbrShader, which has mostly the same configuration options like the forward-rendering version described above.

Examples using deferred shading are DeferredDemo, ReflectionDemo and the VehicleDemo