v0.7.1 (build 925)

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

By task #

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 (or yum install glslang on 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.spv

Use 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=0 named uTexture.
  • 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 blend or blendMode: 'premultiplied-alpha' (default), 'opaque', 'additive', or 'alpha'.
  • Vertex inputs: location=0 vec3 inPosition, location=1 vec4 inColor, location=2 vec2 inTexCoord.

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 glslangValidator is missing, install it (see Prerequisites).
  • Ensure you compiled with the correct stage: -S vert for vertex, -S frag for fragment.
  • Paths in JS must point to .spv files (SPIR-V), not the .glsl sources.
  • 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/user1 are 16-byte aligned per Vulkan push-constant layout; keep them as vec4 for compatibility.
  • The MVP + time + user params fit under the 128-byte push constant minimum on all Vulkan-conformant devices.