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...
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:
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:
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:
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) |
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.
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:
@paramvalues are uniforms: each becomes a typed setter method on the shader class, as seen above.@in/@outdeclare the vertex attributes and the varyings passed from vertex to fragment.- The math functions (
sin,vec4,texture,mix...) come fromshade.Functions: add animport.hxnext to your shader withimport 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:
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.
Related examples
- Custom shader: a plasma shader written in Haxe
- Gaussian blur: a built-in shader applied through a filter
- Pointer in filter: interactions inside a filtered hierarchy
Continue reading ➔ Saving Data