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/context -
rugl.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 symbols -
glfwonly 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 glfwGem install
gem install rugl
gem install glfw # optional, only for the default window/context pathOr in your Gemfile:
gem "rugl", git: "https://github.com/ydah/rugl"
gem "glfw" # optionalQuick Start
require "rugl"
rugl = Rugl.create(width: 800, height: 600, title: "Rugl Triangle")
draw_triangle = rugl.command(
vert: <<~GLSL,
#version 410 core
layout(location = 0) in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
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]
)
endCore Concepts
Context
Rugl.create(**opts) returns a Rugl::Context.
Important context options:
-
width:,height:,title:control the default GLFW window -
version:defaults to[4, 1] -
profile:defaults to:core -
samples:enables multisample hints when GLFW is available -
debug:enables shader/program/GL error checks -
gl:,glfw:, andwindow:let you inject your own backends -
auto_init: falsedisables default backend/window setup
Useful context methods:
-
rugl.command(opts)compiles a command -
rugl.frame(max_frames: nil) { |ctx| ... }runs the frame loop -
rugl.clear(color: nil, depth: nil, stencil: nil)clears active buffers -
rugl.read(x: 0, y: 0, width: nil, height: nil)reads RGBA pixels into a binary string -
rugl.limitsqueries a few common GL limits -
rugl.has_extension?("GL_EXT_name")checks extension support -
rugl.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
endResources
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 attachment -
depth: truecreates and owns a depth renderbuffer -
stencil:defaults tofalse -
depth_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 triangle -
examples/batch.rb: batched draws with per-instance props -
examples/framebuffer.rb: offscreen render target and texture feedback -
examples/scope.rb: scoped props/state inheritance
Development
bundle install
bundle exec rake # default task: spec_unit (`--tag ~gl`)
bundle exec rake spec # full suiteThe specs are mostly built around fake GL backends, so they are fast to run without a real OpenGL window.
License
MIT License.