Skip to content

Screen effects

Full-screen effects are pixel shaders drawn over each player's finished picture of the world: built in, or ones you write. The HUD, chat and menus stay on top, untouched. Use them for:

  • colour grading per area, weather or time of day;
  • night vision or thermal goggles;
  • being drunk, drugged or hurt;
  • black and white flashbacks.
const grey = player.screenEffects.add("greyscale", { fadeMs: 1000 });
grey.set({ strength: 0.5 });
grey.remove(1000);   // fade out over a second

player.screenEffects.add("shaders/underwater.hlsl", { params: { depth: 0.5 } });  // your own

player.screenEffects puts effects on one player's screen. server.screenEffects puts them on everyone's, including players who join later. Client scripts have the same, sagl.screenEffects.


Built in

Add these by name. Each starts from its defaults; give only the parameters you want to change, and set any of them later.

player.screenEffects.add("nightvision");
player.screenEffects.add("hurt", { params: { color: [0, 0, 0.5] } });  // blue edges instead of red
Name Parameters (defaults) What it does
greyscale strength (1) Black and white: 0 is full colour, 1 grey.
sepia strength (1) An old photo's brown.
nightvision gain (3), grain (1) Brightened gain times and green, with grain (0 for none), scanlines and dark edges.
thermal grain (1), blur (1.5), cooling (60), coldest, cold, warm, hot, hottest (colours) A thermal camera: people glow hot and everything else is cold, fading into the cold with distance. Dead bodies cool over cooling seconds. grain is the sensor's noise (0 for none), blur its softness in pixels. See below.
drunk amount (1) The picture sways and doubles.
hurt amount (1), color ([0.55, 0, 0]) color creeping in from the edges, pulsing.
grade saturation (1), contrast (1), brightness (1), tint ([1, 1, 1]) Colour grading: 1 is as it is for each; tint multiplies the picture.
blur amount (4) Out of focus: the blur's radius in pixels at 1080p (it scales with the screen).
vignette amount (0.5), color ([0, 0, 0]) The edges darkened (or tinted towards color).
fade amount (1), color ([0, 0, 0]) The world faded to color, with the HUD and chat still showing.
dof focus (0), range (1), amount (8) Depth of field: sharp at focus metres (0: the nearest thing in the middle of the screen), and range metres either side; blurring up to amount pixels (at 1080p) away from it. The camera's focus does the same for a shot.
tiltshift center (0.55), width (0.08), angle (0), amount (12), saturation (1.4) Tilt-shift, the miniature "toy town" look: a band across the picture stays sharp and the rest blurs more the further it is from it, with richer colours. center is where the band is (0 the top, 1 the bottom), width how far it reaches either side (as a share of the screen's height), angle tilts it (degrees), amount is the most blur in pixels (at 1080p), and saturation the colour boost (1 leaves it). It looks best from high up looking down, with a narrow field of view.

Thermal changes how the game draws the world, the way its own thermal goggles do. It comes on at once (fadeMs is ignored when adding it), but fades out as usual. Only people are heat sources: vehicles and objects show cold.

Its colours run from coldest to hottest. The world shows between coldest and cold (bright surfaces reaching towards warm), and people between hot and hottest, with a cooling body sliding back down. The defaults go from near-black blue through purple, red and orange to a pale yellow. A black and white "white hot" camera:

player.screenEffects.add("thermal", { params: {
  coldest: [0, 0, 0], cold: [0.25, 0.25, 0.25], warm: [0.5, 0.5, 0.5], hot: [0.85, 0.85, 0.85], hottest: [1, 1, 1],
} });

Colours are red, green and blue, each 0 to 1. The SDK exports this list, with the defaults, as BUILT_IN_SCREEN_EFFECTS.

Naming your own

define names one of your shaders, with its parameters' defaults, so it's added by name like a built-in:

server.screenEffects.define("underwater", "shaders/underwater.hlsl", { depth: 0.5, tint: [0.2, 0.5, 0.6] });
player.screenEffects.add("underwater");                              // the defaults
player.screenEffects.add("underwater", { params: { depth: 1 } });   // deeper

A name is for the whole server: server.screenEffects and every player.screenEffects. Freeroam names its pixelate shader this way.


Writing a shader

A shader is an HLSL file among your client files, with a main that gives a pixel's colour. Players' games compile it with Direct3D's shader compiler (pixel shader 3.0).

// client-files/shaders/greyscale.hlsl
float4 main(float2 uv : TEXCOORD0) : COLOR {
  float4 c = tex2D(scene, uv);                        // the picture so far
  float g = dot(c.rgb, float3(0.299, 0.587, 0.114));  // its brightness
  return float4(lerp(c.rgb, g.xxx, strength), 1);     // strength: a parameter
}
Name Type Description
uv float2 The pixel's place on screen: 0,0 top left to 1,1 bottom right.
scene sampler2D The picture so far: the world, and any effects drawn before this one. Read it with tex2D(scene, uv).
time float Seconds, counting up (it wraps each hour), for animation.
screenSize float2 The picture's size in pixels.
pixelSize float2 One pixel, as a uv step (1 / screenSize): to read the pixels around this one.
depthAt(uv) float How far away the 3D picture is at uv, in metres (along the camera's view). The sky and anything past the draw distance give the far clip.
hasDepth bool False on the rare graphics card that can't give depth; depthAt then always gives the far clip.
your parameters float to float4 By the names you add the effect with (below).

The result's alpha is ignored. Fading is done for you: while an effect fades in or out, it's blended with the picture under it.

When a shader doesn't compile, or the file isn't there, the player's game reports it to the server as a clientScriptError, with where set to "shader <file>" and the compiler's message, line numbers included.

The built-ins' shaders are in client/src/screen_effects.cpp (kBuiltIns), written the same way, to start from. Freeroam has one of its own, client-files/shaders/pixelate.hlsl. Freeroam's Debug page in the pause menu has a button for each effect.


ScreenEffects

add(effect, options?)

add(effect: string, options?: ScreenEffectOptions): ScreenEffect

Adds an effect: a built-in's name, a name you gave with define, or a client file with the shader. A named effect starts from its defaults and takes only its own parameters.

Option Type Default Description
params Record<string, number \| number[]> none (a named effect: its defaults) Its parameters, by the names the shader uses: a number, or 2 to 4 numbers (a vector, a colour). At most 8. Their names and sizes are fixed once added.
order number 0 Effects draw lowest first, each on the picture the ones before it left.
fadeMs number 0 Fades in over this long.

Throws ReferenceError if it's no effect's name or client file; RangeError for a bad parameter.

define(name, file, defaults?)

define(name: string, file: string, defaults?: ScreenEffectParams): void

Names a shader of yours (a client file) with its parameters' defaults, to add by name. Throws RangeError if the name is a built-in's or already defined, ReferenceError if there's no such client file.

names

readonly names: string[]

The effects you can add by name: the built-ins, then those defined.

list, clear(fadeMs?)

readonly list: ScreenEffect[]
clear(fadeMs?: number): void

The effects on now, and taking them all off.

save(), restore(fadeMs?), snapshot()

save(): ScreenEffectsSnapshot
restore(fadeMs?: number): void
snapshot(): ScreenEffectsSnapshot   // { restore(fadeMs?) }

For taking over someone's screen for a while and giving it back: save() remembers the effects on now, with their parameters. Then clear them and add your own. restore() goes back to what was last saved, whatever happened since (cleared, reset, others added): effects on now that weren't saved go, and the saved ones come back as they were. They're the same ScreenEffect objects, so code holding them carries on. The save stays, so you can restore again.

player.screenEffects.save();
player.screenEffects.clear(300);
player.screenEffects.add("tiltshift");   // the scene's own
// ...
player.screenEffects.restore(500);       // theirs back, tilt-shift gone

snapshot() is the same without touching what save() remembers: it returns its own restore(). Mission cinematics use one to clear each player's effects for a scene and put them back after.

A camera shot with its own focus takes over from any dof effect: theirs doesn't draw until the shot's focus is off.

ScreenEffect

Member Description
file What it was added as: a name or a client file.
params Its parameters now.
active Still on screen (not removed).
set(params) Changes parameters: only those given, of those it was added with.
remove(fadeMs?) Takes it off, fading out over fadeMs (default 0).
// Drunker with every drink, sober again over a minute.
const drunk = player.screenEffects.add("drunk", { params: { amount: 0.3 }, fadeMs: 2000 });
drunk.set({ amount: 0.8 });
setTimeout(() => drunk.remove(60_000), 120_000);