Skip to content

Screen Effects

Full-screen post-processing effects (color grading, distortion, blur, anything a fragment shader can do to the finished frame) whose uniforms can change every frame. Package: net.ixdarklord.coolcatcanvas.api.client.effect (client) and api.effect (server).

Info

Canvas ships no effects of its own. Your mod brings its post_effect JSON and shaders, or registers a vanilla one such as minecraft:invert.

1. Write the effect

Put a vanilla-format post effect at assets/<modid>/post_effect/<name>.json and its shader at assets/<modid>/shaders/post/<name>.fsh:

{
  "targets": { "swap": {} },
  "passes": [
    {
      "vertex_shader": "minecraft:core/screenquad",
      "fragment_shader": "mymod:post/vignette",
      "inputs": [ { "sampler_name": "In", "target": "minecraft:main", "bilinear": false } ],
      "output": "swap",
      "uniforms": {
        "VignetteConfig": [
          { "name": "Color", "type": "vec4", "value": [0.0, 0.0, 0.0, 1.0] },
          { "name": "Radius", "type": "float", "value": 0.75 },
          { "name": "Softness", "type": "float", "value": 0.45 }
        ]
      }
    },
    {
      "vertex_shader": "minecraft:core/screenquad",
      "fragment_shader": "minecraft:core/blit_screen",
      "inputs": [ { "sampler_name": "In", "target": "swap" } ],
      "output": "minecraft:main"
    }
  ]
}

Differences from vanilla's format: - Every uniform needs a "name". - Every pass also gets the EffectInfo and Globals uniform blocks. - The only external target is minecraft:main; read its depth with "use_depth_buffer": true. - A target declared as {"persistent": true} keeps its contents across frames, for feedback effects such as trails.

The shader imports Canvas's include to get the effect's strength and time:

#version 330
#moj_import <coolcatcanvas:screen_effect.glsl>

uniform sampler2D InSampler;
layout(std140) uniform SamplerInfo { vec2 OutSize; vec2 InSize; };
layout(std140) uniform VignetteConfig { vec4 Color; float Radius; float Softness; };

in vec2 texCoord;
out vec4 fragColor;

void main() {
    vec3 color = texture(InSampler, texCoord).rgb;
    float edge = smoothstep(Radius - Softness, Radius, ce_edge(texCoord, OutSize));
    fragColor = vec4(mix(color, Color.rgb, clamp(edge * Color.a * Strength, 0.0, 1.0)), 1.0);
}

coolcatcanvas:screen_effect.glsl provides:

Name What it is
float Strength 0 to 1: the effect's fade × manual strength × strength function. Scale your work by it so the effect fades.
float Time Real seconds since the game started (wraps every hour).
float Age Seconds since the effect last became visible.
float Seed A fresh random number in [0, 1) every frame.
ce_luma(vec3) Rec. 709 luminance.
ce_hash(vec2), ce_noise(vec2) Hash and value noise.
ce_edge(vec2 uv, vec2 size) 0 at the center, 1 at the corners, aspect-corrected.

2. Register and control it

Register once from client code (a ClientModConstructor is a good place):

ScreenEffect insanity = ScreenEffects.register(MyMod.id("insanity"), MyMod.id("desaturate"))
        .priority(10)
        .fade(40)
        .activeWhen(context -> context.inWorld() && Sanity.of(context.player()) > 0.4F)
        .strength(context -> Mth.inverseLerp(Sanity.of(context.player()), 0.4F, 0.8F))
        .displayName(Component.literal("Insanity"))
        .setUniform("Contrast", 1.4F);

ScreenEffect tint = ScreenEffects.register(MyMod.id("tint"), MyMod.id("tint")).fade(15);
tint.uniform("Color").setColor(0x5933CC66);
tint.enableFor(40);                                   // a 2-second pulse
tint.animateStrength(0.5F, 20, Easing.SINE_IN_OUT);

// A vanilla effect that ignores Strength: autoBlend fades it anyway.
ScreenEffects.register(MyMod.id("invert"), Identifier.withDefaultNamespace("invert")).autoBlend(true).enable();

Or build the definition in code (its shaders still come from resources):

ScreenEffects.register(MyMod.id("vignette"),
        ScreenEffectDefinition.simple(MyMod.id("post/vignette"), "VignetteConfig",
                ScreenEffectDefinition.UniformSpec.ofVec4("Color", 0F, 0F, 0F, 1F),
                ScreenEffectDefinition.UniformSpec.ofFloat("Radius", 0.75F),
                ScreenEffectDefinition.UniformSpec.ofFloat("Softness", 0.45F)))
        .stage(ScreenEffectStage.SCREEN)
        .bindUniform("Radius", context -> 0.6F + 0.1F * Mth.sin(context.time()));

From the server, drive a player's registered effects with ScreenEffectControl (clients without the effect ignore it):

ScreenEffectControl.enableFor(serverPlayer, MyMod.id("heartbeat"), 60);
ScreenEffectControl.setUniform(serverPlayer, MyMod.id("tint"), "Color", 20, 1F, 0F, 0F, 0.35F);

Reference

ScreenEffects (static entry point)

Method Description
register(Identifier id, Identifier definition) Registers an effect from assets/<ns>/post_effect/<path>.json (vanilla ones included); reloaded with resources. Throws if the id is taken.
register(Identifier id, ScreenEffectDefinition definition) Registers a definition built in code.
get(id), all(), unregister(id) Look up, list, or remove an effect (freeing its GPU resources).
disableAllInstantly() Hides every effect at once.
layers() The draw order (ScreenEffectLayers).
createScreen(parent), openScreen() The effects screen: every selectable effect with a switch and a strength slider, reorderable layers, live preview. Also on a key (unbound by default) and /coolcatcanvas_client effects in development.

ScreenEffect (the handle; render thread only)

The drawn strength is fade × manual strength × strength function, and a strength of 0 skips the effect. Durations are ticks of real time.

Group Methods
Presentation displayName(Component), description(Component), selectable(boolean)
Configuration stage(ScreenEffectStage) (default WORLD), priority(int) (lower draws first), fade(int ticks) / fade(in, out, Easing), autoBlend(boolean), activeWhen(Predicate<EffectContext>), strength(StrengthFunction), onFrame(Consumer<EffectContext>)
State enable(), disable(), toggle(), setEnabled(boolean), enableFor(ticks), enableInstantly(), disableInstantly(), isEnabled(), isVisible(), setStrength(float), animateStrength(target, ticks, Easing), strength()
Uniforms uniform(name), setUniform(name, float...), setUniformInt(name, int...), bindUniform(name, FloatBinding / VectorBinding), resetUniforms()
Loading isLoaded(), error()

Other types

Type Description
ScreenEffectDefinition The post-effect definition (targets and passes). builder() with target, persistentTarget, pass(fragmentShader, pass -> ...), blit; simple(fragmentShader, uniformBlock, UniformSpec...) for one-pass effects; UniformSpec.ofFloat/ofInt/ofVec2/ofVec3/ofVec4. PassBuilder: vertexShader, input, depthInput, textureInput (reads textures/effect/<path>.png), output, uniforms.
EffectContext The frame an effect is drawn in: effect, player, level, partialTick, time, deltaTime, age, strength, width, height, minecraft(), inWorld().
EffectUniform One named uniform: set(...) (floats, ints, JOML vectors and matrices), setColor(argb), animateTo(Easing, ticks, target...), bind(...), unbind(), reset(), get(), getAll(). A binding beats an animation, which beats the last set value, which beats the definition's default.
ScreenEffectStage WORLD (over the world and held item, under the HUD; depth available) or SCREEN (over everything, menus included).
ScreenEffectLayers The draw order: order(), visible(stage), moveTo, moveUp, moveDown, bringToTop, sendToBottom, isCustomized(), resetOrder(). Every WORLD effect draws before any SCREEN effect.
api.effect.ScreenEffectControl Server side: enable, disable, enableFor, enableInstantly, disableInstantly, setStrength, setUniform, resetUniforms for a ServerPlayer. It keeps no state, so resend after a player joins.
api.event.v2.client.ScreenEffectEvents BEFORE_TOGGLE (return EventResult.INTERRUPT to veto), TOGGLED, STRENGTH_CHANGED, UNIFORM_CHANGED, LAYERS_CHANGED, LOADED. The toggle cause is CODE, CONDITION, TIMEOUT, PLAYER or SERVER.

Players' choices on the effects screen (on/off, strengths, order) are saved to config/coolcatcanvas-screen-effects.json.