Shaders and Shader Control
Using custom shaders, time, and user parameters. Includes copies of default shaders.
Koya exposes a small, portable interface for driving shader animations:
- A per-frame time counter in seconds since renderer start - Two user-controlled vec4 parameters (user0, user1) - A 4×4 MVP matrix for transforming vertices
These are provided via Vulkan push constants and are visible in both vertex and fragment stages.
What this page covers #
- Push-constant layout used by Koya pipelines (
mvp,time,user0,user1) - Default shader copies for quick reference
- How to compile custom GLSL to SPIR-V and wire shaders in scripts
- Runtime controls for global, per-element, and animated shader params
Related concepts #
By task #
- Need shader inputs and structure? Start at Push constant layout
- Need working defaults? Jump to Default shaders (copies)
- Need to compile your own shaders? See Build your own shaders
- Need runtime control from JS? See Scripting control
Push constant layout #
layout(push_constant) uniform PushConstants {
mat4 mvp; // object->clip transform
vec4 time; // time.x = seconds since renderer init
vec4 user0; // user-defined parameters (set globally or per-element)
vec4 user1; // user-defined parameters (set globally or per-element)
} pushConstants;Default shaders (copies) #
Vertex (textured):
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec4 inColor;
layout(location = 2) in vec2 inTexCoord;
layout(location = 0) out vec4 fragColor;
layout(location = 1) out vec2 fragTexCoord;
layout(push_constant) uniform PushConstants {
mat4 mvp;
vec4 time;
vec4 user0;
vec4 user1;
} pushConstants;
void main() {
gl_Position = pushConstants.mvp * vec4(inPosition, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
}Fragment (textured):
#version 450
layout(location = 0) in vec4 fragColor;
layout(location = 1) in vec2 fragTexCoord;
layout(set = 0, binding = 0) uniform sampler2D uTexture;
layout(location = 0) out vec4 outColor;
layout(push_constant) uniform PushConstants {
mat4 mvp;
vec4 time;
vec4 user0;
vec4 user1;
} pushConstants;
void main() {
vec4 tex = texture(uTexture, fragTexCoord);
float ta = fragColor.a;
vec3 tint = fragColor.rgb;
outColor = vec4(tex.rgb * tint * ta, tex.a * ta);
}Vertex (vertex-colour only):
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec4 inColor;
layout(location = 2) in vec2 inTexCoord;
layout(location = 0) out vec4 fragColor;
layout(location = 1) out vec2 fragTexCoord;
layout(push_constant) uniform PushConstants { mat4 mvp; vec4 time; vec4 user0; vec4 user1; } pushConstants;
void main() {
gl_Position = pushConstants.mvp * vec4(inPosition, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
}Fragment (vertex-colour only):
#version 450
layout(location = 0) in vec4 fragColor;
layout(location = 1) in vec2 fragTexCoord;
layout(location = 0) out vec4 outColor;
layout(push_constant) uniform PushConstants { mat4 mvp; vec4 time; vec4 user0; vec4 user1; } pushConstants;
void main() { outColor = fragColor; }Build your own shaders #
Prerequisites #
glslangValidator installed (comes with the Vulkan SDK, or install package glslang on most distros):
- Arch:
pacman -S glslang - Debian/Ubuntu:
apt install glslang-tools - Fedora/RHEL/CentOS:
dnf install glslang(oryum install glslangon older releases)
Compile your shaders #
Put your GLSL files anywhere on disk you can read from scripts (e.g., ~/.config/koya/shaders/). Run glslangValidator to produce .spv files:
# Vertex
glslangValidator -S vert -V my_shader.vert.glsl -o ~/.config/koya/shaders/my_shader.vert.spv
# Fragment
glslangValidator -S frag -V my_shader.frag.glsl -o ~/.config/koya/shaders/my_shader.frag.spvUse in JS #
import * as UI from 'Helix/UserInterface';
const el = await UI.createElement(win, {
renderable: {
type: 'panel',
texture: '/rom/arch.png',
shader: {
vert: '/rom/shaders/my_shader.vert.spv',
frag: '/rom/shaders/my_shader.frag.spv',
blend: 'premultiplied-alpha'
},
shaderParams: { v0: [1,0,0,0] }
}
});
await UI.attachRoot(win, el);Notes and expectations #
- GLSL version: use
#version 450. - Push constants must match the layout on this page (MVP + time + user0 + user1) if you read them.
- Textured pipelines expect a combined image sampler at
set=0, binding=0nameduTexture. - The renderer uses premultiplied alpha blending by default: srcColor=ONE, dstColor=ONE_MINUS_SRC_ALPHA; srcAlpha=ONE, dstAlpha=ONE_MINUS_SRC_ALPHA. Textures should be premultiplied (RGB = RGB × A). Koya's built-in loaders premultiply for UI assets. If you write custom fragment shaders, output premultiplied colours.
- Shader definitions accept
blendorblendMode:'premultiplied-alpha'(default),'opaque','additive', or'alpha'. - Vertex inputs:
location=0vec3inPosition,location=1vec4inColor,location=2vec2inTexCoord.
Example: Animated tint in fragment using time #
Use pc.time.x (elapsed seconds) to drive a per-frame colour wave across the RGB channels. This creates a pulsing tint effect on any textured surface without changing any JS-side state.
#version 450
layout(location = 0) in vec4 fragColor;
layout(location = 1) in vec2 fragTexCoord;
layout(set = 0, binding = 0) uniform sampler2D uTexture;
layout(location = 0) out vec4 outColor;
layout(push_constant) uniform PushConstants {
mat4 mvp; vec4 time; vec4 user0; vec4 user1;
} pc;
void main() {
float t = pc.time.x;
vec3 wave = 0.5 + 0.5 * vec3(sin(t), sin(t*1.3), sin(t*1.7));
vec4 tex = texture(uTexture, fragTexCoord);
outColor = tex * vec4(wave, 1.0) * fragColor;
}Then in JS, you can also modulate with params:
await UI.setShaderParams(windowId, { v0: [0.5, 0.5, 0.5, 1.0] });Troubleshooting #
- If
glslangValidatoris missing, install it (see Prerequisites). - Ensure you compiled with the correct stage:
-S vertfor vertex,-S fragfor fragment. - Paths in JS must point to
.spvfiles (SPIR-V), not the.glslsources. - If your shader reads
uTexture, make sure the renderable actually has a texture set. - Validation errors usually mention a location/binding mismatch — double-check push constants and inputs.
Scripting control #
Global per-window params:
await UI.setShaderParams(windowId, { v0: [x,y,z,w], v1: [x,y,z,w] });Per-element overrides (take precedence over global):
await UI.setElementShaderParams(windowId, elementId, { v0: [...], v1: [...] });At creation time:
await UI.createElement(win, { renderable: { type: 'box', shaderParams: { v0: [1,0,0,0] } } });Animated via keyframes:
UI.addAnimation(win, id, [
{ time: 0.0, shaderParams: { v0: [0,0,0,0] } },
{ time: 0.5, shaderParams: { v0: [1,0,0,0] } },
{ time: 1.0, shaderParams: { v0: [0,0,0,0] }, looping: true }
]);Notes #
- Time is CPU-provided elapsed seconds since renderer init (
pushConstants.time.x). It's portable and stable across invocations. user0/user1are 16-byte aligned per Vulkan push-constant layout; keep them asvec4for compatibility.- The MVP + time + user params fit under the 128-byte push constant minimum on all Vulkan-conformant devices.