Shaders

Shaders are small programs that run on the GPU for every vertex and every pixel drawn. They power visual effects that would be far too expensive on the CPU: blurs, glows, distortions, procedural animations...

The custom shader example: a plasma computed entirely on the GPU.

Using a shader on a visual

Every visual has a shader property. Assign a Shader instance to change how it is drawn. assets.shader() returns the typed shader class, so you keep access to its typed API:

Assigning a shader to a visual
var myShader = assets.shader(shaders.MyShader);
visual.shader = myShader;

Shaders expose uniforms: values that your code sets, and that the GPU program reads. In ceramic, shaders are written in Haxe (see Writing your own shader below), and declare their uniforms as @param variables. Say MyShader's fragment shader declares these:

The @param uniforms declared by MyShader (fragment)
class MyShader_Frag extends Frag {

    // The visual's texture (set automatically by ceramic)
     var mainTex:Sampler2D;

    // Custom uniforms, set from your code
     var time:Float;
     var resolution:Vec2;
     var tint:Vec4;
     var secondaryTexture:Sampler2D;

    // ...

}

Each of these @param becomes a typed method of the same name on the shaders.MyShader class, so setting uniforms is checked at compile time:

Setting these uniforms from your code (typed)
myShader.time(elapsed);              // uniform 'time' (Float)
myShader.resolution(width, height);  // uniform 'resolution' (Vec2)
myShader.tint(Color.RED);            // a Vec3/Vec4 uniform also accepts a Color
myShader.secondaryTexture(texture);  // uniform 'secondaryTexture' (Sampler2D)

Under the hood these call the generic setters setFloat('time', ...), setVec2('resolution', ...), etc. Those string-based setters are still available on any ceramic.Shader if you hold a shader without its concrete type.

A Shader instance carries its uniform values with it. If two visuals need the same shader with different values, clone it: var other:shaders.MyShader = cast myShader.clone(); (clone() returns a base Shader, so cast it back to keep the typed API).

Built-in shaders

Ceramic ships with ready-to-use shaders, exposed as classes in the shaders package. Add the ones you use in preload() like any other asset:

Shader Effect
shaders.GaussianBlur Smooth blur (see the gaussian blur example)
shaders.PixelArt Crisp pixel art scaling with optional CRT-style effects
shaders.Msdf Multi-channel signed distance field fonts (used internally for MSDF text)
shaders.Bloom, shaders.Glow, shaders.Outline, shaders.Fxaa, shaders.InnerLight Various effects
shaders.TintBlack "Tint black" coloring (as popularized by Spine)
Using the built-in gaussian blur
override function preload() {
    assets.add(shaders.GaussianBlur);
}

override function create() {
    // clone() so we don't mutate the shared cached shader; cast back to the
    // typed class to keep the typed uniform setters
    var blur:shaders.GaussianBlur = cast assets.shader(shaders.GaussianBlur).clone();
    blur.resolution(filter.width, filter.height);
    blur.blurSize(6.0, 6.0);
    filter.shader = blur;
}

Shaders are often combined with filters: the filter renders its content to a texture, and the shader processes that texture as a whole (post-processing), instead of processing each visual separately.

Writing your own shader

Custom shaders are written in Haxe, with ceramic's cross-platform shader system (the shade plugin). You write a vertex and a fragment class, and they get transpiled at build time to the native shader language of each backend (GLSL for web & native, ShaderLab for Unity...): the same shader runs everywhere.

src/shaders/Plasma.hx
package shaders;

class Plasma extends Shader<Plasma_Vert, Plasma_Frag> {}

class Plasma_Vert extends Vert {

     var projectionMatrix:Mat4;
     var modelViewMatrix:Mat4;

     var vertexPosition:Vec3;
     var vertexTCoord:Vec2;
     var vertexColor:Vec4;

     var tcoord:Vec2;
     var color:Vec4;

    function main():Vec4 {
        tcoord = vertexTCoord;
        color = vertexColor;
        return projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);
    }

}

class Plasma_Frag extends Frag {

     var mainTex:Sampler2D;

    // Custom uniform, set from Haxe code with the typed time() method
     var time:Float;

     var tcoord:Vec2;
     var color:Vec4;

    function main():Vec4 {
        var wave:Float = sin(tcoord.x * 10.0 + time) * 0.5 + 0.5;
        return color * texture(mainTex, tcoord) * vec4(wave, wave, 1.0, 1.0);
    }

}

A few things to note:

  • @param values are uniforms: each becomes a typed setter method on the shader class, as seen above. @in/@out declare the vertex attributes and the varyings passed from vertex to fragment.
  • The math functions (sin, vec4, texture, mix...) come from shade.Functions: add an import.hx next to your shader with import shade.*; import shade.Functions.*;.
  • The vertex shader receives the standard attributes (vertexPosition, vertexTCoord, vertexColor) and matrices (projectionMatrix, modelViewMatrix).

Look at the custom shader example for a complete shader, including how to forward extra data from the vertex to the fragment shader with an additional @out varying.

Then load and use it like any built-in shader. Because assets.shader() returns the typed shaders.Plasma, its @param uniforms are typed methods:

Loading a custom shader
var plasma:shaders.Plasma;

override function preload() {
    // The Plasma class is transpiled and loaded like any shader asset
    assets.add(shaders.Plasma);
}

override function create() {
    plasma = assets.shader(shaders.Plasma);
    quad.shader = plasma;
}

override function update(delta:Float) {
    elapsed += delta;
    plasma.time(elapsed); // typed setter for the 'time' @param
}

Because shaders are plain Haxe classes, they are type-checked at compile time, and the multi-texture batching variants are generated automatically for you. No need to maintain separate shader files per platform.


Continue reading ➔ Saving Data