Skip to content

Instantly share code, notes, and snippets.

@fearofcode
Created September 11, 2025 03:58
Show Gist options
  • Select an option

  • Save fearofcode/4e7ef5d9c72fb4590186646a93183c0a to your computer and use it in GitHub Desktop.

Select an option

Save fearofcode/4e7ef5d9c72fb4590186646a93183c0a to your computer and use it in GitHub Desktop.
Simple microui Odin OpenGL basic example
package main
import "core:fmt"
import glm "core:math/linalg/glsl"
import gl "vendor:OpenGL"
import mu "vendor:microui"
Microui_Vertex :: struct {
pos : glm.vec2,
tex : glm.vec2,
col : [4]u8,
}
Debug_UI_Context :: struct {
mu_ctx : mu.Context,
mu_shader : Shader,
mu_vao : u32,
mu_vbo : u32,
mu_ebo : u32,
mu_atlas_tex : u32,
// Buffers for batching draw commands
vert_buf : [dynamic]Microui_Vertex,
index_buf : [dynamic]u32,
}
debug_ui_context : Debug_UI_Context
DEBUG_UI_VERTEX_SOURCE := `#version 330 core
layout(location=0) in vec2 a_position;
layout(location=1) in vec2 a_tex_coord;
layout(location=2) in vec4 a_color;
out vec2 v_tex_coord;
out vec4 v_color;
uniform mat4 u_projection;
void main() {
gl_Position = u_projection * vec4(a_position, 0.0, 1.0);
v_tex_coord = a_tex_coord;
v_color = a_color;
}
`
DEBUG_UI_FRAGMENT_SOURCE := `#version 330 core
in vec2 v_tex_coord;
in vec4 v_color;
out vec4 o_color;
uniform sampler2D u_texture;
void main() {
// Sample the alpha channel from the atlas and multiply by vertex color
float alpha = texture(u_texture, v_tex_coord).r;
o_color = vec4(v_color.rgb, v_color.a * alpha);
}
`
debug_ui_init :: proc() -> bool {
shader, shader_ok := shader_from_source_strings(DEBUG_UI_VERTEX_SOURCE, DEBUG_UI_FRAGMENT_SOURCE)
if !shader_ok {
fmt.eprintln("Error compiling debug UI shader")
return false
}
debug_ui_context.mu_shader = shader
gl.GenVertexArrays(1, &debug_ui_context.mu_vao)
gl.GenBuffers(1, &debug_ui_context.mu_vbo)
gl.GenBuffers(1, &debug_ui_context.mu_ebo)
gl.BindVertexArray(debug_ui_context.mu_vao)
gl.BindBuffer(gl.ARRAY_BUFFER, debug_ui_context.mu_vbo)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, debug_ui_context.mu_ebo)
gl.EnableVertexAttribArray(0) // pos
gl.EnableVertexAttribArray(1) // tex
gl.EnableVertexAttribArray(2) // col
gl.VertexAttribPointer(0, 2, gl.FLOAT, false, size_of(Microui_Vertex), offset_of(Microui_Vertex, pos))
gl.VertexAttribPointer(1, 2, gl.FLOAT, false, size_of(Microui_Vertex), offset_of(Microui_Vertex, tex))
gl.VertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, size_of(Microui_Vertex), offset_of(Microui_Vertex, col))
// Create atlas texture
gl.GenTextures(1, &debug_ui_context.mu_atlas_tex)
gl.BindTexture(gl.TEXTURE_2D, debug_ui_context.mu_atlas_tex)
gl.TexImage2D(
gl.TEXTURE_2D,
0,
gl.R8, // Internal format: Store as 8-bit Red channel
mu.DEFAULT_ATLAS_WIDTH,
mu.DEFAULT_ATLAS_HEIGHT,
0,
gl.RED, // Source format: The data we provide is in the Red channel
gl.UNSIGNED_BYTE,
&mu.default_atlas_alpha,
)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
// Initialize microui context
mu.init(&debug_ui_context.mu_ctx)
debug_ui_context.mu_ctx.text_width = mu.default_atlas_text_width
debug_ui_context.mu_ctx.text_height = mu.default_atlas_text_height
return true
}
// Flushes the vertex/index buffers to the GPU and issues a draw call
microui_flush :: proc() {
if len(debug_ui_context.index_buf) == 0 {
return
}
gl.BindVertexArray(debug_ui_context.mu_vao)
// Upload vertex and index data to GPU
gl.BindBuffer(gl.ARRAY_BUFFER, debug_ui_context.mu_vbo)
gl.BufferData(
gl.ARRAY_BUFFER,
len(debug_ui_context.vert_buf) * size_of(Microui_Vertex),
raw_data(debug_ui_context.vert_buf),
gl.STREAM_DRAW,
)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, debug_ui_context.mu_ebo)
gl.BufferData(
gl.ELEMENT_ARRAY_BUFFER,
len(debug_ui_context.index_buf) * size_of(u32),
raw_data(debug_ui_context.index_buf),
gl.STREAM_DRAW,
)
// Draw the batch
gl.DrawElements(gl.TRIANGLES, i32(len(debug_ui_context.index_buf)), gl.UNSIGNED_INT, nil)
// Reset buffers for the next batch
clear(&debug_ui_context.vert_buf)
clear(&debug_ui_context.index_buf)
}
// Pushes a quad to the vertex/index buffers
microui_push_quad :: proc(dst, src : mu.Rect, color : mu.Color) {
idx := u32(len(debug_ui_context.vert_buf))
atlas_w, atlas_h := f32(mu.DEFAULT_ATLAS_WIDTH), f32(mu.DEFAULT_ATLAS_HEIGHT)
u0, v0 := f32(src.x) / atlas_w, f32(src.y) / atlas_h
u1, v1 := f32(src.x + src.w) / atlas_w, f32(src.y + src.h) / atlas_h
x0, y0 := f32(dst.x), f32(dst.y)
x1, y1 := f32(dst.x + dst.w), f32(dst.y + dst.h)
col := [4]u8{color.r, color.g, color.b, color.a}
append(
&debug_ui_context.vert_buf,
Microui_Vertex{{x0, y0}, {u0, v0}, col},
Microui_Vertex{{x1, y0}, {u1, v0}, col},
Microui_Vertex{{x1, y1}, {u1, v1}, col},
Microui_Vertex{{x0, y1}, {u0, v1}, col},
)
append(&debug_ui_context.index_buf, idx, idx + 1, idx + 2, idx, idx + 2, idx + 3)
}
// Process the Microui command list and render it
microui_render :: proc() {
// Prepare OpenGL state for 2D UI rendering
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.Disable(gl.CULL_FACE)
gl.Disable(gl.DEPTH_TEST)
gl.Enable(gl.SCISSOR_TEST)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, debug_ui_context.mu_atlas_tex)
use_shader(debug_ui_context.mu_shader)
// Set up orthographic projection
projection_matrix := glm.mat4Ortho3d(0, f32(WINDOW_WIDTH) / UI_SCALE, f32(WINDOW_HEIGHT) / UI_SCALE, 0, -1, 1)
set_shader_uniform_mat4(debug_ui_context.mu_shader, "u_projection", projection_matrix)
set_shader_uniform_int(debug_ui_context.mu_shader, "u_texture", 0)
// Process commands
command_backing : ^mu.Command
for variant in mu.next_command_iterator(&debug_ui_context.mu_ctx, &command_backing) {
#partial switch cmd in variant {
case ^mu.Command_Text:
dst := mu.Rect{cmd.pos.x, cmd.pos.y, 0, 0}
for ch in cmd.str {
if ch & 0xc0 == 0x80 {continue}
r := min(int(ch), 127)
src := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + r]
dst.w, dst.h = src.w, src.h
microui_push_quad(dst, src, cmd.color)
dst.x += dst.w
}
case ^mu.Command_Rect:
// Use the special white pixel in the atlas for solid colors
white_pixel_rect := mu.default_atlas[mu.DEFAULT_ATLAS_WHITE]
microui_push_quad(cmd.rect, white_pixel_rect, cmd.color)
case ^mu.Command_Icon:
src := mu.default_atlas[cmd.id]
x := cmd.rect.x + (cmd.rect.w - src.w) / 2
y := cmd.rect.y + (cmd.rect.h - src.h) / 2
microui_push_quad(mu.Rect{x, y, src.w, src.h}, src, cmd.color)
case ^mu.Command_Clip:
microui_flush() // Flush before changing scissor rect
gl.Scissor(cmd.rect.x, WINDOW_HEIGHT - (cmd.rect.y + cmd.rect.h), cmd.rect.w, cmd.rect.h)
}
}
microui_flush() // Flush remaining commands at the end of the frame
// Reset scissor to full screen
gl.Scissor(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT)
}
// Defines and processes the UI for our application
run_debug_ui :: proc() {
if !app_state.show_debug_ui {
return
}
mu.begin(&debug_ui_context.mu_ctx)
// open and close UI with F1, don't let UI be closed by clicking X
if mu.window(&debug_ui_context.mu_ctx, "Controls", {10, 10, 200, 150}, {.NO_CLOSE}) {
mu.layout_row(&debug_ui_context.mu_ctx, {-1}, 0)
mu.label(&debug_ui_context.mu_ctx, "Simple UI Controls")
mu.layout_row(&debug_ui_context.mu_ctx, {-1}, 0)
if .CHANGE in mu.checkbox(&debug_ui_context.mu_ctx, "Show 3D Quad", &app_state.show_quad) {
fmt.println("show_quad changed to", app_state.show_quad)
}
mu.layout_row(&debug_ui_context.mu_ctx, {20, -1})
mu.label(&debug_ui_context.mu_ctx, "BG:")
// A helper to create a u8 slider
u8_slider :: proc(val : ^u8, lo, hi : int) {
ctx := &debug_ui_context.mu_ctx
// Push the memory address of `val` as a unique ID for this slider.
mu.push_id(ctx, uintptr(val))
// This static var is now safe because Microui uses the ID stack
// to distinguish between the controls that use it.
@(static) tmp : mu.Real
tmp = mu.Real(val^)
mu.slider(ctx, &tmp, f32(lo), f32(hi), 0, "%.0f", {})
val^ = u8(tmp)
// Pop the ID to restore the stack for the next control.
mu.pop_id(ctx)
}
mu.layout_begin_column(&debug_ui_context.mu_ctx)
u8_slider(&app_state.bg_color.r, 0, 255)
u8_slider(&app_state.bg_color.g, 0, 255)
u8_slider(&app_state.bg_color.b, 0, 255)
mu.layout_end_column(&debug_ui_context.mu_ctx)
}
mu.end(&debug_ui_context.mu_ctx)
}
package main
import "core:fmt"
import "core:math"
import glm "core:math/linalg/glsl"
import "core:time"
import gl "vendor:OpenGL"
import mu "vendor:microui"
import SDL "vendor:sdl2"
App_State :: struct {
bg_color : mu.Color,
show_quad : bool,
show_debug_ui : bool,
}
app_state : App_State
Vertex :: struct {
pos : glm.vec3,
col : glm.vec4,
}
main :: proc() {
window_context, window_ok := init_sdl_opengl_context()
if !window_ok {
return
}
defer sdl_opengl_cleanup(window_context)
main_shader, shader_ok := shader_from_source_strings(vertex_source, fragment_source)
if !shader_ok {
fmt.eprintln("Failed to create GLSL program")
return
}
defer cleanup_shader(main_shader)
use_shader(main_shader)
vao : u32
gl.GenVertexArrays(1, &vao); defer gl.DeleteVertexArrays(1, &vao)
gl.BindVertexArray(vao)
vbo, ebo : u32
gl.GenBuffers(1, &vbo)
defer gl.DeleteBuffers(1, &vbo)
gl.GenBuffers(1, &ebo)
defer gl.DeleteBuffers(1, &ebo)
vertices := []Vertex {
// position color
{{-0.5, +0.5, 0}, {1.0, 0.0, 0.0, 0.75}},
{{-0.5, -0.5, 0}, {1.0, 1.0, 0.0, 0.75}},
{{+0.5, -0.5, 0}, {0.0, 1.0, 0.0, 0.75}},
{{+0.5, +0.5, 0}, {0.0, 0.0, 1.0, 0.75}},
}
indices := []u16{0, 1, 2, 2, 3, 0}
index_count := i32(len(indices))
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.BufferData(gl.ARRAY_BUFFER, len(vertices) * size_of(vertices[0]), raw_data(vertices), gl.STATIC_DRAW)
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, size_of(Vertex), offset_of(Vertex, pos))
gl.EnableVertexAttribArray(0)
gl.VertexAttribPointer(1, 4, gl.FLOAT, false, size_of(Vertex), offset_of(Vertex, col))
gl.EnableVertexAttribArray(1)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo)
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(indices) * size_of(indices[0]), raw_data(indices), gl.STATIC_DRAW)
if (!debug_ui_init()) {
fmt.eprintln("Error initializing debug UI")
return
}
start_tick := time.tick_now()
camera_position : glm.vec3 = {0, 0, -2}
world_center : glm.vec3 = {0, 0, 0}
y_up : glm.vec3 = {0, 1, 0}
camera_fov := math.to_radians_f32(45)
app_state.bg_color = {100, 100, 100, 255}
app_state.show_quad = true
app_state.show_debug_ui = true
loop: for {
duration := time.tick_since(start_tick)
t := f32(time.duration_seconds(duration))
event : SDL.Event
for SDL.PollEvent(&event) {
#partial switch event.type {
case .QUIT:
break loop
case .MOUSEMOTION:
scaled_x := i32(f32(event.motion.x) / UI_SCALE)
scaled_y := i32(f32(event.motion.y) / UI_SCALE)
mu.input_mouse_move(&debug_ui_context.mu_ctx, scaled_x, scaled_y)
case .MOUSEWHEEL:
mu.input_scroll(&debug_ui_context.mu_ctx, event.wheel.x * 30, event.wheel.y * -30)
case .TEXTINPUT:
mu.input_text(&debug_ui_context.mu_ctx, string(cstring(&event.text.text[0])))
case .MOUSEBUTTONDOWN, .MOUSEBUTTONUP:
fn := mu.input_mouse_down if event.type == .MOUSEBUTTONDOWN else mu.input_mouse_up
// Also scale mouse coordinates here!
scaled_x := i32(f32(event.button.x) / UI_SCALE)
scaled_y := i32(f32(event.button.y) / UI_SCALE)
switch event.button.button {
case SDL.BUTTON_LEFT:
fn(&debug_ui_context.mu_ctx, scaled_x, scaled_y, .LEFT)
case SDL.BUTTON_MIDDLE:
fn(&debug_ui_context.mu_ctx, scaled_x, scaled_y, .MIDDLE)
case SDL.BUTTON_RIGHT:
fn(&debug_ui_context.mu_ctx, scaled_x, scaled_y, .RIGHT)
}
case .KEYDOWN, .KEYUP:
if event.key.keysym.sym == .ESCAPE {break loop}
if event.type == .KEYDOWN {
#partial switch event.key.keysym.sym {
case .F1:
app_state.show_debug_ui = !app_state.show_debug_ui
}
}
fn := mu.input_key_down if event.type == .KEYDOWN else mu.input_key_up
#partial switch event.key.keysym.sym {
case .LSHIFT, .RSHIFT:
fn(&debug_ui_context.mu_ctx, .SHIFT)
case .LCTRL, .RCTRL:
fn(&debug_ui_context.mu_ctx, .CTRL)
case .LALT, .RALT:
fn(&debug_ui_context.mu_ctx, .ALT)
case .RETURN, .KP_ENTER:
fn(&debug_ui_context.mu_ctx, .RETURN)
case .BACKSPACE:
fn(&debug_ui_context.mu_ctx, .BACKSPACE)
}
}
}
run_debug_ui()
bg := app_state.bg_color
gl.Viewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT)
gl.ClearColor(f32(bg.r) / 255, f32(bg.g) / 255, f32(bg.b) / 255, 1.0)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
if app_state.show_quad {
gl.Enable(gl.DEPTH_TEST)
gl.Disable(gl.SCISSOR_TEST)
gl.Disable(gl.BLEND)
gl.BindVertexArray(vao)
// rotate about Z axis
model := mat4_identity() * glm.mat4Rotate({0, 1, 0}, t)
view := glm.mat4LookAt(eye = camera_position, centre = world_center, up = y_up)
projection := glm.mat4Perspective(camera_fov, ASPECT, 0.1, 100.0)
use_shader(main_shader)
transform := projection * view * model
set_shader_uniform_mat4(main_shader, "u_transform", transform)
gl.DrawElements(gl.TRIANGLES, index_count, gl.UNSIGNED_SHORT, nil)
}
if app_state.show_debug_ui {
microui_render()
}
SDL.GL_SwapWindow(window_context.window)
free_all(context.temp_allocator)
}
}
vertex_source := `#version 460 core
layout(location=0) in vec3 a_position;
layout(location=1) in vec4 a_color;
out vec4 v_color;
uniform mat4 u_transform;
void main() {
gl_Position = u_transform * vec4(a_position, 1.0);
v_color = a_color;
}
`
fragment_source := `#version 460 core
in vec4 v_color;
out vec4 o_color;
void main() {
o_color = v_color;
}
`
package main
import "core:fmt"
import glm "core:math/linalg/glsl"
import gl "vendor:OpenGL"
Shader :: struct {
program_id : u32,
uniforms : gl.Uniforms,
}
shader_from_source_strings :: proc(vertex_source, fragment_source : string) -> (Shader, bool) {
shader : Shader
program_id, program_ok := gl.load_shaders_source(vertex_source, fragment_source)
if !program_ok {
fmt.eprintln("Failed to create GLSL program!")
}
shader.program_id = program_id
shader.uniforms = gl.get_uniforms_from_program(program_id)
return shader, program_ok
}
cleanup_shader :: proc(shader : Shader) {
gl.DeleteProgram(shader.program_id)
delete(shader.uniforms)
}
use_shader :: proc(shader : Shader) {
gl.UseProgram(shader.program_id)
}
set_shader_uniform_mat4 :: proc(shader : Shader, name : string, m : glm.mat4) {
m := m // so we can pass a pointer
gl.UniformMatrix4fv(shader.uniforms[name].location, 1, false, &m[0, 0])
}
set_shader_uniform_int :: proc(shader : Shader, name : string, i : i32) {
gl.Uniform1i(shader.uniforms[name].location, i)
}
package main
import glm "core:math/linalg/glsl"
mat4_identity :: proc() -> glm.mat4 {
return glm.mat4{1.0, 0, 0, 0, 0, 1.0, 0, 0, 0, 0, 1.0, 0, 0, 0, 0, 1.0}
}
package main
import "core:fmt"
import gl "vendor:OpenGL"
import SDL "vendor:sdl2"
GL_VERSION_MAJOR :: 4
GL_VERSION_MINOR :: 6
WINDOW_WIDTH :: 1920
WINDOW_HEIGHT :: 1080
ASPECT :: WINDOW_WIDTH / WINDOW_HEIGHT
UI_SCALE :: 1.0 // todo compute this dynamically based on window and display dimensions
SDL_Window_Context :: struct {
window : ^SDL.Window,
gl_context : SDL.GLContext,
}
init_sdl_opengl_context :: proc() -> (SDL_Window_Context, bool) {
window_context : SDL_Window_Context
SDL.Init({.VIDEO})
window := SDL.CreateWindow(
"Odin SDL2 Demo",
SDL.WINDOWPOS_UNDEFINED,
SDL.WINDOWPOS_UNDEFINED,
WINDOW_WIDTH,
WINDOW_HEIGHT,
{.OPENGL},
)
if window == nil {
fmt.eprintln("Failed to create window")
return window_context, false
}
SDL.GL_SetAttribute(.CONTEXT_PROFILE_MASK, i32(SDL.GLprofile.CORE))
SDL.GL_SetAttribute(.CONTEXT_MAJOR_VERSION, GL_VERSION_MAJOR)
SDL.GL_SetAttribute(.CONTEXT_MINOR_VERSION, GL_VERSION_MINOR)
gl_context := SDL.GL_CreateContext(window)
// vsync
SDL.GL_SetSwapInterval(1)
gl.load_up_to(GL_VERSION_MAJOR, GL_VERSION_MINOR, SDL.gl_set_proc_address)
window_context.window = window
window_context.gl_context = gl_context
return window_context, true
}
sdl_opengl_cleanup :: proc(window_context : SDL_Window_Context) {
SDL.Quit()
SDL.DestroyWindow(window_context.window)
SDL.GL_DeleteContext(window_context.gl_context)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment