Shaders
On this page
core.render can draw any shape, text or quad through a pixel shader you write in HLSL. Compile it once with core.render.create_shader, then either pass the handle in the shader option of any drawing call, or draw a bare quad with core.render.shader_rect and let the shader paint every pixel of it. Shaders run per pixel on the graphics card, so effects that would take thousands of shapes (soft glows, scanlines, animated outlines, distorting the game frame) cost a single call.
local pulse = assert(core.render.create_shader([[
float4 main(PS_INPUT input) : SV_Target
{
float2 uv = shape_uv(input.pos); // 0..1 across the shape
float d = distance(uv, float2(0.5, 0.5)) * 2.0; // 0 at the centre, 1 at the edge
float radius = 0.6 + 0.4 * sin(time.x * 4.0);
float ring = smoothstep(0.08, 0.0, abs(d - radius));
return float4(input.col.rgb, input.col.a * ring);
}
]]))
function on_render()
local w, h = core.render.screen_size()
core.render.shader_rect(pulse, w / 2 - 100, h / 2 - 100, 200, 200, { color = {255, 80, 200} })
endA shader runs on the graphics card. An infinite loop or an extremely expensive shader can stall the driver; Windows resets it after a few seconds, which takes the game down with it. Keep loops bounded, avoid dependent texture reads in loops, and try a new shader on a small quad first.
Writing a shader
A shader is an HLSL pixel shader whose entry point is float4 main(PS_INPUT input) : SV_Target, returning the pixel's colour with straight (non-premultiplied) alpha. The following prelude is prepended to your source, so these names are always available:
struct PS_INPUT
{
float4 pos : SV_POSITION;
float4 col : COLOR0;
float2 uv : TEXCOORD0;
};
cbuffer OverlayParams : register(b0)
{
float4 bounds;
float4 resolution;
float4 time;
float4 params[16];
};
Texture2D tex0 : register(t0);
SamplerState samp0 : register(s0);
Texture2D screen : register(t1);
SamplerState screen_sampler : register(s1);
float param(int i) { return params[i >> 2][i & 3]; }
float2 shape_uv(float4 pos) { return (pos.xy - bounds.xy) / max(bounds.zw, 1.0); }
float2 screen_uv(float4 pos) { return pos.xy * resolution.zw; }
Compiler messages use your own line numbers; the prelude does not shift them. Pass raw = true to create_shader to skip the prelude and declare everything yourself (your PS_INPUT must then match the layout above).
Inputs
| Field | Meaning |
|---|---|
input.pos | The pixel's position in screen pixels (SV_POSITION) |
input.col | The color option as 0..1 RGBA |
input.uv | Texture coordinates: 0..1 across a shader_rect quad, glyph coordinates for text, a constant for plain shapes. Use shape_uv(input.pos) for a 0..1 coordinate inside any shape. |
Constants
| Name | Type | Meaning |
|---|---|---|
bounds | float4 | x, y, width, height of the shape being drawn, in screen pixels |
resolution | float4 | width, height, 1/width, 1/height of the window |
time | float4 | seconds since the overlay started, seconds since the previous frame, frame index, 0 |
params | float4[16] | the params option, packed four per element; param(i) reads the i-th number (0-based) |
Textures
tex0/samp0is what the shape is drawn with: the font atlas for text and plain shapes, a white pixel forshader_rect. Multiply by it to keep text readable:tex0.Sample(samp0, input.uv) * input.col.screen/screen_sampleris a copy of the game frame beneath the overlay, available only when the drawing call setsscreen = true. Sample it atscreen_uv(input.pos). The copy is taken before the overlay draws, so other overlay shapes are not in it.
Blending
By default the shader's output is alpha-blended over the frame. blend = "add" adds it instead, which suits glows and light effects.
Functions
core.render.create_shader(source: string, opts?: table) -> shader | nil, string
Compiles an HLSL pixel shader and returns a handle. On failure returns nil and the compiler's messages, formatted like shader(12,5-20): error X3004: undeclared identifier 'foo'. On success a second return value carries any compiler warnings, or nil.
Options:
| Key | Default | Notes |
|---|---|---|
entry | "main" | entry point function name |
profile | "ps_4_0" | "ps_4_0", "ps_4_1" or "ps_5_0" |
raw | false | skip the prelude |
Compiling happens on the spot and can be done from any callback or at the top of the script, which is the recommended place. Limits: 64 KB of source, 32 shaders per script, 256 in total. Shaders are released when the script unloads.
local shader, err = core.render.create_shader(source)
if not shader then
core.log_error("shader failed to compile:\n" .. err)
return
endshader:is_valid() -> boolean
true until the shader is released or its script is unloaded.
shader:release() -> nil
Frees the shader early. Passing the handle to a drawing call afterwards raises an error, and shader_rect returns nil, reason.
core.render.shader_rect(shader: shader, x: number, y: number, w: number, h: number, opts?: table) -> boolean | nil, string
Draws a quad through the shader, with input.uv running from 0, 0 at the top-left to 1, 1 at the bottom-right. Takes the same options as the other drawing calls (color, params, screen, blend, world, anchor). Only inside on_render().
A color with alpha 0 draws nothing: the quad is discarded before the shader runs. Keep the alpha above zero and compute any transparency in the shader. A quad whose shader cannot run (released handle, rejected by the graphics device) is skipped rather than drawn as a plain box.
core.render.shader_rect(scanlines, 0, 0, w, h, { screen = true, params = { 3.0, 0.15 } })Shading shapes and text
Pass shader = handle to any drawing call and its primitives, glow passes included, are drawn through it. bounds covers the whole shape, so shape_uv works the same way for a circle, a line or a string of text.
local rainbow = assert(core.render.create_shader([[
float3 hue(float h)
{
float3 k = float3(0.0, 2.0, 4.0);
return saturate(abs(fmod(h * 6.0 + k, 6.0) - 3.0) - 1.0);
}
float4 main(PS_INPUT input) : SV_Target
{
float4 glyph = tex0.Sample(samp0, input.uv) * input.col;
float phase = shape_uv(input.pos).x + time.x * 0.5;
return float4(hue(frac(phase)), glyph.a);
}
]]))
function on_render()
core.render.text(40, 40, "Rainbow", { shader = rainbow, size = 32, shadow = true })
endExamples
Scanlines over the game frame
local scanlines = assert(core.render.create_shader([[
float4 main(PS_INPUT input) : SV_Target
{
float3 frame = screen.Sample(screen_sampler, screen_uv(input.pos)).rgb;
float spacing = max(param(0), 1.0);
float strength = param(1);
float band = 0.5 + 0.5 * sin(input.pos.y * 3.14159 / spacing);
return float4(frame * (1.0 - strength * band), 1.0);
}
]]))
function on_render()
local w, h = core.render.screen_size()
core.render.shader_rect(scanlines, 0, 0, w, h, { screen = true, params = { 3.0, 0.25 } })
endPulsing outline around each mob
local outline = assert(core.render.create_shader([[
float4 main(PS_INPUT input) : SV_Target
{
float2 uv = shape_uv(input.pos);
float2 edge = min(uv, 1.0 - uv);
float inset = min(edge.x, edge.y) * min(bounds.z, bounds.w);
float width = 2.0 + 2.0 * sin(time.x * 6.0 + param(0));
float alpha = smoothstep(width + 1.0, width, inset);
return float4(input.col.rgb, input.col.a * alpha);
}
]]))
function on_render()
local map = core.object_manager.get_current_map()
if not (map and map:is_valid()) then return end
for _, mob in ipairs(map:get_mobs()) do
local pos = mob:is_valid() and mob:get_position()
if pos then
core.render.shader_rect(outline, pos.x - 40, pos.y - 80, 80, 80, {
anchor = mob:anchor(), color = {255, 120, 40}, blend = "add",
params = { (mob:get_unique_id() or 0) % 7 },
})
end
end
endTroubleshooting
| Message | Meaning |
|---|---|
shader(3,12-20): error X3004: … | HLSL error at line 3 of your source; the compiler lists every error |
shader compiler is unavailable | this system has no HLSL compiler, so shaders cannot be used on it |
shader limit reached | 32 shaders for this script or 256 in total already exist; release some |
expected a valid shader | shader_rect got a handle that was released, or belongs to a script that was unloaded |
render: shader is invalid or released | error raised when the shader option of a drawing call holds such a handle |
shader:is_valid() turns false on its own | the graphics device rejected the compiled shader (for example ps_5_0 on older graphics hardware); recompile with ps_4_0 |
| a shape draws plainly, a quad not at all | the same device rejection, seen from the drawing call that used the handle |