How to Read the Keyboard in a GLSL Shader



Interesting question, especially if you try to port some Shadertoy demos that can read the keyboard from the pixel shader.

I’m going to describe a way to read the keyboard inside a shader that is directly compatible with Shadertoy demos.

The principle is actually quite easy: the keyboard buffer is mapped on a texture and this texture is read in the shader.

The keyboard buffer has 256 entries, one entry / key. The keyboard buffer is copied in a texture with 256×2=512 texels.

The first line of 256 texels is used to keep track of the toggled state: once a key, a texel of the second line will stay to 1 until the same key is pressed again. The second line of 256 texels is used to store the state of each key (pressed or released).

The first line can be sampled in the shader if uv.y < 0.5. The second line is sampled if uv.y >= 0.5.

Shadertoy uses Javascript key codes: the [backspace] key = 8, the [A] key = 65, and so on. Here is a web app that show the Javascript key codes: JavaScript Event KeyCodes.

GeeXLab virtual key codes are not the same than Javascript ones. So here is a code snipped in Lua that maps all GeeXLab key codes to Javascript ones:

local lib_dir = gh_utils.get_scripting_libs_dir() 		
dofile(lib_dir .. "lua/keyboard_codes.lua")

g_keyboard_map = {}
for i=1, 256 do
    g_keyboard_map[i] = 0
end

local js_backspace = 8
local js_tab = 9
local js_enter = 13
local js_shift = 16
local js_ctrl = 17
local js_alt = 18
local js_pause_break = 19
local js_caps_lock = 20
local js_escape = 27
local js_space = 8
local js_page_up = 33
local js_page_down = 34
local js_end = 35
local js_home = 36
local js_left_arrow =	37
local js_up_arrow =	38
local js_right_arrow = 39
local js_down_arrow = 40
local js_insert = 45
local js_delete = 46
local js_0 = 48
local js_1 = 49
local js_2 = 50
local js_3 = 51
local js_4 = 52
local js_5 = 53
local js_6 = 54
local js_7 = 55
local js_8 = 56
local js_9 = 57
local js_a = 65
local js_b = 66
local js_c = 67
local js_d = 68
local js_e = 69
local js_f = 70
local js_g = 71
local js_h = 72
local js_i = 73
local js_j = 74
local js_k = 75
local js_l = 76
local js_m = 77
local js_n = 78
local js_o = 79
local js_p = 80
local js_q = 81
local js_r = 82
local js_s = 83
local js_t = 84
local js_u = 85
local js_v = 86
local js_w = 87
local js_x = 88
local js_y = 89
local js_z = 90
local js_left_window_key = 91
local js_right_window_key = 92
local js_select_key = 93
local js_numpad_0 = 96
local js_numpad_1 = 97
local js_numpad_2 = 98
local js_numpad_3 = 99
local js_numpad_4 = 100
local js_numpad_5 = 101
local js_numpad_6 = 102
local js_numpad_7 = 103
local js_numpad_8 = 104
local js_numpad_9 = 105
local js_multiply = 106
local js_add = 107
local js_subtract = 109
local js_decimal_point = 110
local js_divide = 111
local js_f1 = 112
local js_f2 = 113
local js_f3 = 114
local js_f4 = 115
local js_f5 = 116
local js_f6 = 117
local js_f7 = 118
local js_f8 = 119
local js_f9 = 120
local js_f10 = 121
local js_f11 = 122
local js_f12 = 123
local js_num_lock = 144
local js_scroll_lock = 145
local js_semi_colon = 186
local js_equal_sign = 187
local js_comma = 188
local js_dash = 189
local js_period = 190
local js_forward_slash = 191
local js_grave_accent = 192
local js_open_bracket = 219
local js_back_slash = 220
local js_close_braket = 221
local js_single_quote = 222

g_keyboard_map[KC_A] = js_a
g_keyboard_map[KC_B] = js_b
g_keyboard_map[KC_C] = js_c
g_keyboard_map[KC_D] = js_d
g_keyboard_map[KC_E] = js_e
g_keyboard_map[KC_F] = js_f
g_keyboard_map[KC_G] = js_g
g_keyboard_map[KC_H] = js_h
g_keyboard_map[KC_I] = js_i
g_keyboard_map[KC_J] = js_j
g_keyboard_map[KC_K] = js_k
g_keyboard_map[KC_L] = js_l
g_keyboard_map[KC_M] = js_m
g_keyboard_map[KC_N] = js_n
g_keyboard_map[KC_O] = js_o
g_keyboard_map[KC_P] = js_p
g_keyboard_map[KC_Q] = js_q
g_keyboard_map[KC_R] = js_r
g_keyboard_map[KC_S] = js_s
g_keyboard_map[KC_T] = js_t
g_keyboard_map[KC_U] = js_u
g_keyboard_map[KC_V] = js_v
g_keyboard_map[KC_W] = js_w
g_keyboard_map[KC_X] = js_x
g_keyboard_map[KC_Y] = js_y
g_keyboard_map[KC_Z] = js_z

g_keyboard_map[KC_0] = js_0
g_keyboard_map[KC_1] = js_1
g_keyboard_map[KC_2] = js_2
g_keyboard_map[KC_3] = js_3
g_keyboard_map[KC_4] = js_4
g_keyboard_map[KC_5] = js_5
g_keyboard_map[KC_6] = js_6
g_keyboard_map[KC_7] = js_7
g_keyboard_map[KC_8] = js_8
g_keyboard_map[KC_9] = js_9

g_keyboard_map[KC_LEFT] = js_left_arrow
g_keyboard_map[KC_UP] = js_up_arrow
g_keyboard_map[KC_RIGHT] = js_right_arrow
g_keyboard_map[KC_DOWN] = js_down_arrow

g_keyboard_map[KC_F1] = js_f1
g_keyboard_map[KC_F2] = js_f2
g_keyboard_map[KC_F3] = js_f3
g_keyboard_map[KC_F4] = js_f4
g_keyboard_map[KC_F5] = js_f5
g_keyboard_map[KC_F6] = js_f6
g_keyboard_map[KC_F7] = js_f7
g_keyboard_map[KC_F8] = js_f8
g_keyboard_map[KC_F9] = js_f9
g_keyboard_map[KC_F10] = js_f10
g_keyboard_map[KC_F11] = js_f11
g_keyboard_map[KC_F12] = js_f12

g_keyboard_map[KC_SPACE] = js_space
g_keyboard_map[KC_LEFT_SHIFT] = js_shift
g_keyboard_map[KC_RIGHT_SHIFT] = js_shift
g_keyboard_map[KC_ESCAPE] = js_escape
g_keyboard_map[KC_BACK] = js_backspace
g_keyboard_map[KC_TAB] = js_tab
g_keyboard_map[KC_RETURN] = js_enter
g_keyboard_map[KC_PGDOWN] = js_page_down
g_keyboard_map[KC_PGUP] = js_page_up
g_keyboard_map[KC_HOME] = js_home
g_keyboard_map[KC_END] = js_end
g_keyboard_map[KC_INSERT] = js_insert
g_keyboard_map[KC_DELETE] = js_delete


g_keyboard_map[KC_ADD] = js_add
g_keyboard_map[KC_SUBTRACT] = js_subtract
g_keyboard_map[KC_MULTIPLY] = js_multiply
g_keyboard_map[KC_DIVIDE] = js_divide

g_keyboard_map[KC_NUMLOCK] = js_num_lock

g_keyboard_map[KC_NUMPAD0] = js_numpad_0
g_keyboard_map[KC_NUMPAD1] = js_numpad_1
g_keyboard_map[KC_NUMPAD2] = js_numpad_2
g_keyboard_map[KC_NUMPAD3] = js_numpad_3
g_keyboard_map[KC_NUMPAD4] = js_numpad_4
g_keyboard_map[KC_NUMPAD5] = js_numpad_5
g_keyboard_map[KC_NUMPAD6] = js_numpad_6
g_keyboard_map[KC_NUMPAD7] = js_numpad_7
g_keyboard_map[KC_NUMPAD8] = js_numpad_8
g_keyboard_map[KC_NUMPAD9] = js_numpad_9

g_keyboard_map[KC_NUMPAD_ENTER] = js_enter

 
The keyboard mapping is the first part of the problem and it’s solved now. The second part is to update the keyboard texture.

First thing, the keyboard texture creation:

g_toggle_state = {}
g_keyboard_prev_state = {}
local PF_U8_RGBA = 3
local PF_U8_R = 11
local x_size = 256
local y_size = 2
local k = 0
keyboard_tex = gh_texture.create_2d(x_size, y_size, PF_U8_RGBA)
for i=0, 255 do
  g_toggle_state[k+1] = 0
  g_keyboard_prev_state[k+1] = 0
  k = k + 1
  for j=0, 255 do
    gh_texture.set_texel_2d(keyboard_tex, i, j, 0, 0, 0, 255)
  end
end

local SAMPLER_FILTERING_NEAREST = 1
local SAMPLER_FILTERING_LINEAR = 2
local SAMPLER_ADDRESSING_WRAP = 1
local SAMPLER_ADDRESSING_CLAMP_TO_EDGE = 2
local SAMPLER_ADDRESSING_MIRROR = 3
gh_texture.bind(keyboard_tex, 0)
gh_texture.set_sampler_params(keyboard_tex, SAMPLER_FILTERING_NEAREST, SAMPLER_ADDRESSING_CLAMP_TO_EDGE, 1.0)
gh_texture.bind(0, 0)

 
The g_keyboard_prev_state array allows to keep track of the previous state (pressed or released) of each key. The g_toggle_state keeps track of the toggled state of each key. The g_toggle_state array allows to simulate pusk-like buttons (the button keeps its pressed state until next time it’s pressed).

Now here is the function that is called every frame to update the keyboard texture:

function set_keyboard_texture_v2(gxl_kb_code, state)
  local k = g_keyboard_map[gxl_kb_code]
  if (k == nil) then
    return
  end

  local v = 0    
  if (state == 1) then
   -- Pressed state
    v = 255
  end
  
  -- Store the pressed or released state (second line of the texture). 
  local row = 1
  gh_texture.set_texel_2d(keyboard_tex, k, row, v,  0, 0, 255)

  
  -- Store the toggled state (first line of the texture). 
  row = 0

  local prev_state = g_keyboard_prev_state[k+1]

  if (state == 1 and prev_state == 0) then
    g_keyboard_prev_state[k+1] = 1
    local toggled = g_toggle_state[k+1]
    if (toggled == 0) then
      g_toggle_state[k+1] = 1
      gh_texture.set_texel_2d(keyboard_tex, k, row, 255, 255, 0, 255)
    else 
      g_toggle_state[k+1] = 0
      gh_texture.set_texel_2d(keyboard_tex, k, row, 0, 0, 0, 255)
    end
  end

  if (state == 0) then
    g_keyboard_prev_state[k+1] = 0
  end
 
end

 
In a FRAME script, this function can be called like that:

local platform_windows = 1 
local platform_osx = 2 
local platform_linux = 3 

if (gh_utils.get_platform() == platform_windows) then
  gh_window.keyboard_update_buffer(0)
end

for i=0, 255 do
  local kbcode = i
  local state = gh_input.keyboard_is_key_down(kbcode)
  set_keyboard_texture(kbcode, state)
end

 
The keyboard texture is bound like any texture and can be sampled in the shader.

I prepared a simple code sample that displays the keyboard texture:


GeeXLab demo - Keyboard to texture

 
The yellow lines show the toggle states while red lines are the pressed/released states. The demo (keyboard_to_texture_gl2.xml) is available in the host_api/keyboard/ folder of the code sample pack.

I also prepared another demo (that should help Stefan) this time based on this Shadertoy demo:


GeeXLab demo - Shadertoy Key Quest

 
This demo (key_quest_gl2.xml) is available in the host_api/GLSL_ShaderToy/ folder of the code sample pack.

 
References