Rugl
Functional OpenGL for Ruby.
Rugl is a small command-oriented rendering layer on top of OpenGL bindings. You describe draw calls as immutable Ruby hashes, resolve dynamic values at execution time, and let Rugl handle shader compilation, VAO setup, resource tracking, and render-state diffing.
Current Feature Set
Rugl.createbuilds a rendering context and can bootstrap a default GLFW window/contextrugl.command(...)compiles reusable draw commands from shader source plus draw/state options- dynamic values can come from runtime props, frame context, scoped props, or procs
- commands support single draws, batched draws, indexed draws, instancing, scoped execution, and framebuffer targets
- GPU resources include buffers, element buffers, textures, renderbuffers, and framebuffers
- render state diffing covers
depth,blend,stencil,scissor,cull,polygon_offset,color_mask,front_face,line_width,dither,sample, andviewport - context helpers include
clear,read,limits, andhas_extension?
Installation
Runtime requirements
- Ruby 3.1+
opengl-bindingsfor OpenGL symbolsglfwonly if you wantRugl.createto open a default window/context for you
System dependencies
If you want Rugl to create its own window/context via GLFW, install GLFW and OpenGL development libraries first.
# Ubuntu / Debian
sudo apt-get install libglfw3-dev libgl1-mesa-dev
# macOS
brew install glfw
Gem install
gem install rugl
gem install glfw # optional, only for the default window/context path
Or in your Gemfile:
gem "rugl", git: "https://github.com/ydah/rugl"
gem "glfw" # optional
Quick Start
require "rugl"
rugl = Rugl.create(width: 800, height: 600, title: "Rugl Triangle")
draw_triangle = rugl.command(
vert: <<~GLSL,
frag: <<~GLSL,
#version 410 core
uniform vec4 color;
out vec4 fragColor;
void main() {
fragColor = color;
}
GLSL
attributes: {
position: rugl.buffer([[-2, -2], [4, -2], [4, 4]])
},
uniforms: {
color: Rugl.prop(:color)
},
count: 3
)
rugl.frame do |ctx|
rugl.clear(color: [0.0, 0.0, 0.0, 1.0], depth: 1.0)
t = ctx[:time]
draw_triangle.call(
color: [Math.cos(t), Math.sin(t * 0.8), Math.cos(t * 0.3), 1.0]
)
end
Core Concepts
Context
Rugl.create(**opts) returns a Rugl::Context.
Important context options:
width:,height:,title:control the default GLFW windowversion:defaults to[4, 1]profile:defaults to:coresamples:enables multisample hints when GLFW is availabledebug:enables shader/program/GL error checksgl:,glfw:, andwindow:let you inject your own backendsauto_init: falsedisables default backend/window setup
Useful context methods:
rugl.command(opts)compiles a commandrugl.frame(max_frames: nil) { |ctx| ... }runs the frame looprugl.clear(color: nil, depth: nil, stencil: nil)clears active buffersrugl.read(x: 0, y: 0, width: nil, height: nil)reads RGBA pixels into a binary stringrugl.limitsqueries a few common GL limitsrugl.has_extension?("GL_EXT_name")checks extension supportrugl.destroydestroys the window/context and warns about leaked resources
If no window is present, frame runs a single iteration by default. That makes auto_init: false useful for tests and offscreen/headless flows.
Commands
Commands are compiled once and executed many times:
draw = rugl.command(
vert: "...",
frag: "...",
attributes: { position: rugl.buffer([...]) },
uniforms: { color: Rugl.prop(:color) },
count: 3,
primitive: :triangles,
depth: { enable: true },
blend: { enable: true }
)
draw.call(color: [1, 0, 0, 1])
draw.call([{ color: [1, 0, 0, 1] }, { color: [0, 1, 0, 1] }])
Supported top-level command keys:
- shaders:
:vert,:frag - draw inputs:
:attributes,:uniforms,:elements - draw params:
:count,:primitive,:offset,:instances - targets:
:framebuffer - state:
:depth,:blend,:stencil,:scissor,:cull,:polygon_offset,:color_mask,:front_face,:line_width,:dither,:sample,:viewport
Programs are cached per context by identical vertex/fragment shader source pairs.
Dynamic Values
Rugl resolves dynamic values at draw time:
Rugl.prop(:color) # runtime props passed to command.call
Rugl.context(:time) # per-frame context from rugl.frame
Rugl.this(:model) # current scoped/merged props
-> { 1.0 } # arity 0
->(ctx) { ctx[:tick] } # arity 1
->(ctx, props) { ... } # arity 2+
Dynamic markers can be used in uniforms, attributes, draw params, framebuffer targets, and state blocks.
Scoped Execution
Passing a block to command.call turns the command into a scope. The command applies its state and props, then child commands inherit those merged props through Rugl.this(...) and normal prop lookup.
scope.call(color: [1, 0.5, 0.2, 1]) do
child.call
end
Resources
Buffers
positions = rugl.buffer([[-1, -1], [1, -1], [0, 1]])
dynamic = rugl.buffer(data: [0, 1, 2, 3], usage: :dynamic, type: :float)
empty = rugl.buffer(byte_length: 4096, usage: :stream)
buffer accepts either raw array data, an integer byte size, or a hash with data:, byte_length:, usage:, and type:.
Element Buffers
indices = rugl.elements(data: [0, 1, 2], type: :uint16, primitive: :triangles)
elements accepts raw index data or a hash with data:, usage:, type:, and primitive:.
Textures
texture = rugl.texture(
width: 256,
height: 256,
data: Array.new(256 * 256 * 4, 255),
min_filter: :linear,
mag_filter: :linear,
wrap_s: :clamp_to_edge,
wrap_t: :clamp_to_edge,
mipmap: true
)
Current texture support is intentionally small:
Textureexpects a hash source withwidth:andheight:- pixel data can be raw binary or array-like numeric data
format:currently supports:rgba- loading image files by path is not built in
Renderbuffers and Framebuffers
depth = rugl.renderbuffer(width: 512, height: 512, format: :depth_component24)
fbo = rugl.framebuffer(
width: 512,
height: 512,
color: true,
depth: depth
)
Framebuffer defaults:
color: truecreates and owns an RGBA texture attachmentdepth: truecreates and owns a depth renderbufferstencil:defaults tofalsedepth_stencil:can be used instead of separate depth/stencil attachments
Framebuffer instances expose color_attachment, depth_attachment, stencil_attachment, depth_stencil_attachment, use, bind, unbind, resize, and destroy.
Headless And Injected Backends
You can skip GLFW/window creation and inject your own GL backend:
require "rugl"
gl = MyOpenGLBackend
rugl = Rugl.create(gl: gl, auto_init: false, width: 256, height: 256)
draw = rugl.command(
vert: "...",
frag: "...",
count: 0
)
rugl.frame(max_frames: 1) do
draw.call
end
pixels = rugl.read(width: 256, height: 256)
This is also how most of the test suite exercises the library: with a fake GL backend rather than a real window.
Examples
Examples assume glfw is installed and available.
bundle exec ruby examples/triangle.rb
bundle exec ruby examples/batch.rb
bundle exec ruby examples/framebuffer.rb
bundle exec ruby examples/scope.rb
examples/triangle.rb: minimal animated triangleexamples/batch.rb: batched draws with per-instance propsexamples/framebuffer.rb: offscreen render target and texture feedbackexamples/scope.rb: scoped props/state inheritance
Development
bundle install
bundle exec rake # default task: spec_unit (`--tag ~gl`)
bundle exec rake spec # full suite
The specs are mostly built around fake GL backends, so they are fast to run without a real OpenGL window.
License
MIT License.