Project

rugl

0.0
No release in over 3 years
Declarative OpenGL 4.x commands, explicit GPU resources, and state-diffed rendering for Ruby.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.12
~> 0.9

Runtime

 Project Readme

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.create builds 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, and viewport
  • context helpers include clear, read, limits, and has_extension?

Installation

Runtime requirements

  • Ruby 3.1+
  • opengl-bindings for OpenGL symbols
  • glfw only if you want Rugl.create to 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,
    #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]
  )
end

Core 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:, and window: let you inject your own backends
  • auto_init: false disables 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.limits queries a few common GL limits
  • rugl.has_extension?("GL_EXT_name") checks extension support
  • rugl.destroy destroys 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:

  • Texture expects a hash source with width: and height:
  • 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: true creates and owns an RGBA texture attachment
  • depth: true creates and owns a depth renderbuffer
  • stencil: defaults to false
  • 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 suite

The specs are mostly built around fake GL backends, so they are fast to run without a real OpenGL window.

License

MIT License.