rugl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4dd4f813b58ccae38822027f48263c23a4e2e89a19a4f45a04c10542eee35784
4
+ data.tar.gz: 0bc5750099f8ce7d8f087bda51996ae9480efea9972065d0f403d2f07bfc090e
5
+ SHA512:
6
+ metadata.gz: 6047ff624719eb0a15179a639ff12765d041e36ce30823ca7ad43e01c4dcd91498eceab9c5ddfbf1fa1e317893f5e229e3358570a8f4dd1fc4d2d1352d486b02
7
+ data.tar.gz: efd1efe350e4233e2f6327ba0c9befc2b0c15b9f27b2b80214c5378bd87336ba4b113bdbd54c375d2cc0fa0d92a4d23f0d3fbe29949b94f254aab64668a969a1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # Rugl
2
+
3
+ Functional OpenGL for Ruby.
4
+
5
+ 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.
6
+
7
+ ## Current Feature Set
8
+
9
+ - `Rugl.create` builds a rendering context and can bootstrap a default GLFW window/context
10
+ - `rugl.command(...)` compiles reusable draw commands from shader source plus draw/state options
11
+ - dynamic values can come from runtime props, frame context, scoped props, or procs
12
+ - commands support single draws, batched draws, indexed draws, instancing, scoped execution, and framebuffer targets
13
+ - GPU resources include buffers, element buffers, textures, renderbuffers, and framebuffers
14
+ - render state diffing covers `depth`, `blend`, `stencil`, `scissor`, `cull`, `polygon_offset`, `color_mask`, `front_face`, `line_width`, `dither`, `sample`, and `viewport`
15
+ - context helpers include `clear`, `read`, `limits`, and `has_extension?`
16
+
17
+ ## Installation
18
+
19
+ ### Runtime requirements
20
+
21
+ - Ruby 3.1+
22
+ - [`opengl-bindings`](https://rubygems.org/gems/opengl-bindings) for OpenGL symbols
23
+ - `glfw` only if you want `Rugl.create` to open a default window/context for you
24
+
25
+ ### System dependencies
26
+
27
+ If you want Rugl to create its own window/context via GLFW, install GLFW and OpenGL development libraries first.
28
+
29
+ ```bash
30
+ # Ubuntu / Debian
31
+ sudo apt-get install libglfw3-dev libgl1-mesa-dev
32
+
33
+ # macOS
34
+ brew install glfw
35
+ ```
36
+
37
+ ### Gem install
38
+
39
+ ```bash
40
+ gem install rugl
41
+ gem install glfw # optional, only for the default window/context path
42
+ ```
43
+
44
+ Or in your `Gemfile`:
45
+
46
+ ```ruby
47
+ gem "rugl", git: "https://github.com/ydah/rugl"
48
+ gem "glfw" # optional
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```ruby
54
+ require "rugl"
55
+
56
+ rugl = Rugl.create(width: 800, height: 600, title: "Rugl Triangle")
57
+
58
+ draw_triangle = rugl.command(
59
+ vert: <<~GLSL,
60
+ #version 410 core
61
+ layout(location = 0) in vec2 position;
62
+ void main() {
63
+ gl_Position = vec4(position, 0.0, 1.0);
64
+ }
65
+ GLSL
66
+ frag: <<~GLSL,
67
+ #version 410 core
68
+ uniform vec4 color;
69
+ out vec4 fragColor;
70
+ void main() {
71
+ fragColor = color;
72
+ }
73
+ GLSL
74
+ attributes: {
75
+ position: rugl.buffer([[-2, -2], [4, -2], [4, 4]])
76
+ },
77
+ uniforms: {
78
+ color: Rugl.prop(:color)
79
+ },
80
+ count: 3
81
+ )
82
+
83
+ rugl.frame do |ctx|
84
+ rugl.clear(color: [0.0, 0.0, 0.0, 1.0], depth: 1.0)
85
+
86
+ t = ctx[:time]
87
+ draw_triangle.call(
88
+ color: [Math.cos(t), Math.sin(t * 0.8), Math.cos(t * 0.3), 1.0]
89
+ )
90
+ end
91
+ ```
92
+
93
+ ## Core Concepts
94
+
95
+ ### Context
96
+
97
+ `Rugl.create(**opts)` returns a `Rugl::Context`.
98
+
99
+ Important context options:
100
+
101
+ - `width:`, `height:`, `title:` control the default GLFW window
102
+ - `version:` defaults to `[4, 1]`
103
+ - `profile:` defaults to `:core`
104
+ - `samples:` enables multisample hints when GLFW is available
105
+ - `debug:` enables shader/program/GL error checks
106
+ - `gl:`, `glfw:`, and `window:` let you inject your own backends
107
+ - `auto_init: false` disables default backend/window setup
108
+
109
+ Useful context methods:
110
+
111
+ - `rugl.command(opts)` compiles a command
112
+ - `rugl.frame(max_frames: nil) { |ctx| ... }` runs the frame loop
113
+ - `rugl.clear(color: nil, depth: nil, stencil: nil)` clears active buffers
114
+ - `rugl.read(x: 0, y: 0, width: nil, height: nil)` reads RGBA pixels into a binary string
115
+ - `rugl.limits` queries a few common GL limits
116
+ - `rugl.has_extension?("GL_EXT_name")` checks extension support
117
+ - `rugl.destroy` destroys the window/context and warns about leaked resources
118
+
119
+ If no window is present, `frame` runs a single iteration by default. That makes `auto_init: false` useful for tests and offscreen/headless flows.
120
+
121
+ ### Commands
122
+
123
+ Commands are compiled once and executed many times:
124
+
125
+ ```ruby
126
+ draw = rugl.command(
127
+ vert: "...",
128
+ frag: "...",
129
+ attributes: { position: rugl.buffer([...]) },
130
+ uniforms: { color: Rugl.prop(:color) },
131
+ count: 3,
132
+ primitive: :triangles,
133
+ depth: { enable: true },
134
+ blend: { enable: true }
135
+ )
136
+
137
+ draw.call(color: [1, 0, 0, 1])
138
+ draw.call([{ color: [1, 0, 0, 1] }, { color: [0, 1, 0, 1] }])
139
+ ```
140
+
141
+ Supported top-level command keys:
142
+
143
+ - shaders: `:vert`, `:frag`
144
+ - draw inputs: `:attributes`, `:uniforms`, `:elements`
145
+ - draw params: `:count`, `:primitive`, `:offset`, `:instances`
146
+ - targets: `:framebuffer`
147
+ - state: `:depth`, `:blend`, `:stencil`, `:scissor`, `:cull`, `:polygon_offset`, `:color_mask`, `:front_face`, `:line_width`, `:dither`, `:sample`, `:viewport`
148
+
149
+ Programs are cached per context by identical vertex/fragment shader source pairs.
150
+
151
+ ### Dynamic Values
152
+
153
+ Rugl resolves dynamic values at draw time:
154
+
155
+ ```ruby
156
+ Rugl.prop(:color) # runtime props passed to command.call
157
+ Rugl.context(:time) # per-frame context from rugl.frame
158
+ Rugl.this(:model) # current scoped/merged props
159
+ -> { 1.0 } # arity 0
160
+ ->(ctx) { ctx[:tick] } # arity 1
161
+ ->(ctx, props) { ... } # arity 2+
162
+ ```
163
+
164
+ Dynamic markers can be used in uniforms, attributes, draw params, framebuffer targets, and state blocks.
165
+
166
+ ### Scoped Execution
167
+
168
+ 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.
169
+
170
+ ```ruby
171
+ scope.call(color: [1, 0.5, 0.2, 1]) do
172
+ child.call
173
+ end
174
+ ```
175
+
176
+ ## Resources
177
+
178
+ ### Buffers
179
+
180
+ ```ruby
181
+ positions = rugl.buffer([[-1, -1], [1, -1], [0, 1]])
182
+ dynamic = rugl.buffer(data: [0, 1, 2, 3], usage: :dynamic, type: :float)
183
+ empty = rugl.buffer(byte_length: 4096, usage: :stream)
184
+ ```
185
+
186
+ `buffer` accepts either raw array data, an integer byte size, or a hash with `data:`, `byte_length:`, `usage:`, and `type:`.
187
+
188
+ ### Element Buffers
189
+
190
+ ```ruby
191
+ indices = rugl.elements(data: [0, 1, 2], type: :uint16, primitive: :triangles)
192
+ ```
193
+
194
+ `elements` accepts raw index data or a hash with `data:`, `usage:`, `type:`, and `primitive:`.
195
+
196
+ ### Textures
197
+
198
+ ```ruby
199
+ texture = rugl.texture(
200
+ width: 256,
201
+ height: 256,
202
+ data: Array.new(256 * 256 * 4, 255),
203
+ min_filter: :linear,
204
+ mag_filter: :linear,
205
+ wrap_s: :clamp_to_edge,
206
+ wrap_t: :clamp_to_edge,
207
+ mipmap: true
208
+ )
209
+ ```
210
+
211
+ Current texture support is intentionally small:
212
+
213
+ - `Texture` expects a hash source with `width:` and `height:`
214
+ - pixel data can be raw binary or array-like numeric data
215
+ - `format:` currently supports `:rgba`
216
+ - loading image files by path is not built in
217
+
218
+ ### Renderbuffers and Framebuffers
219
+
220
+ ```ruby
221
+ depth = rugl.renderbuffer(width: 512, height: 512, format: :depth_component24)
222
+
223
+ fbo = rugl.framebuffer(
224
+ width: 512,
225
+ height: 512,
226
+ color: true,
227
+ depth: depth
228
+ )
229
+ ```
230
+
231
+ Framebuffer defaults:
232
+
233
+ - `color: true` creates and owns an RGBA texture attachment
234
+ - `depth: true` creates and owns a depth renderbuffer
235
+ - `stencil:` defaults to `false`
236
+ - `depth_stencil:` can be used instead of separate depth/stencil attachments
237
+
238
+ Framebuffer instances expose `color_attachment`, `depth_attachment`, `stencil_attachment`, `depth_stencil_attachment`, `use`, `bind`, `unbind`, `resize`, and `destroy`.
239
+
240
+ ## Headless And Injected Backends
241
+
242
+ You can skip GLFW/window creation and inject your own GL backend:
243
+
244
+ ```ruby
245
+ require "rugl"
246
+
247
+ gl = MyOpenGLBackend
248
+ rugl = Rugl.create(gl: gl, auto_init: false, width: 256, height: 256)
249
+
250
+ draw = rugl.command(
251
+ vert: "...",
252
+ frag: "...",
253
+ count: 0
254
+ )
255
+
256
+ rugl.frame(max_frames: 1) do
257
+ draw.call
258
+ end
259
+
260
+ pixels = rugl.read(width: 256, height: 256)
261
+ ```
262
+
263
+ This is also how most of the test suite exercises the library: with a fake GL backend rather than a real window.
264
+
265
+ ## Examples
266
+
267
+ Examples assume `glfw` is installed and available.
268
+
269
+ ```bash
270
+ bundle exec ruby examples/triangle.rb
271
+ bundle exec ruby examples/batch.rb
272
+ bundle exec ruby examples/framebuffer.rb
273
+ bundle exec ruby examples/scope.rb
274
+ ```
275
+
276
+ - `examples/triangle.rb`: minimal animated triangle
277
+ - `examples/batch.rb`: batched draws with per-instance props
278
+ - `examples/framebuffer.rb`: offscreen render target and texture feedback
279
+ - `examples/scope.rb`: scoped props/state inheritance
280
+
281
+ ## Development
282
+
283
+ ```bash
284
+ bundle install
285
+ bundle exec rake # default task: spec_unit (`--tag ~gl`)
286
+ bundle exec rake spec # full suite
287
+ ```
288
+
289
+ The specs are mostly built around fake GL backends, so they are fast to run without a real OpenGL window.
290
+
291
+ ## License
292
+
293
+ MIT License.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ RSpec::Core::RakeTask.new(:spec_unit) do |task|
9
+ task.rspec_opts = "--tag ~gl"
10
+ end
11
+
12
+ task default: :spec_unit
data/examples/batch.rb ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "example_helper"
4
+
5
+ rugl = Rugl.create(width: 960, height: 640, title: "Rugl Batch")
6
+
7
+ draw_triangle = rugl.command(
8
+ vert: <<~GLSL,
9
+ #version 410 core
10
+ layout(location = 0) in vec2 position;
11
+ uniform vec2 offset;
12
+ void main() {
13
+ gl_Position = vec4(position * 0.08 + offset, 0.0, 1.0);
14
+ }
15
+ GLSL
16
+ frag: <<~GLSL,
17
+ #version 410 core
18
+ uniform vec4 color;
19
+ out vec4 fragColor;
20
+ void main() {
21
+ fragColor = color;
22
+ }
23
+ GLSL
24
+ attributes: {
25
+ position: rugl.buffer([[-1, -1], [1, -1], [0, 1]])
26
+ },
27
+ uniforms: {
28
+ color: Rugl.prop(:color),
29
+ offset: Rugl.prop(:offset)
30
+ },
31
+ count: 3,
32
+ blend: {
33
+ enable: true,
34
+ func: { src: :src_alpha, dst: :one_minus_src_alpha }
35
+ }
36
+ )
37
+
38
+ grid_size = 10
39
+ cells = grid_size * grid_size
40
+
41
+ rugl.frame do |ctx|
42
+ rugl.clear(color: [0.06, 0.08, 0.12, 1.0], depth: 1.0)
43
+ t = ctx[:time]
44
+
45
+ props = Array.new(cells) do |i|
46
+ x = i % grid_size
47
+ y = i / grid_size
48
+ nx = (x.to_f / (grid_size - 1)) * 2.0 - 1.0
49
+ ny = (y.to_f / (grid_size - 1)) * 2.0 - 1.0
50
+
51
+ {
52
+ offset: [nx, ny],
53
+ color: [
54
+ 0.5 + 0.5 * Math.cos(t + x * 0.4),
55
+ 0.5 + 0.5 * Math.sin(t * 0.8 + y * 0.5),
56
+ 0.5 + 0.5 * Math.cos(t * 0.6 + i * 0.1),
57
+ 0.85
58
+ ]
59
+ }
60
+ end
61
+
62
+ draw_triangle.call(props)
63
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib_path = File.expand_path("../lib", __dir__)
4
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
5
+
6
+ require "rugl"
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "example_helper"
4
+
5
+ rugl = Rugl.create(width: 800, height: 600, title: "Rugl Framebuffer")
6
+ offscreen = rugl.framebuffer(width: 512, height: 512)
7
+
8
+ draw_scene = rugl.command(
9
+ vert: <<~GLSL,
10
+ #version 410 core
11
+ layout(location = 0) in vec2 position;
12
+ uniform float time;
13
+ void main() {
14
+ float a = time;
15
+ mat2 r = mat2(cos(a), -sin(a), sin(a), cos(a));
16
+ vec2 p = r * position;
17
+ gl_Position = vec4(p, 0.0, 1.0);
18
+ }
19
+ GLSL
20
+ frag: <<~GLSL,
21
+ #version 410 core
22
+ out vec4 fragColor;
23
+ void main() {
24
+ fragColor = vec4(0.2, 0.8, 0.5, 1.0);
25
+ }
26
+ GLSL
27
+ attributes: {
28
+ position: rugl.buffer([[-0.7, -0.6], [0.7, -0.6], [0.0, 0.8]])
29
+ },
30
+ uniforms: {
31
+ time: Rugl.context(:time)
32
+ },
33
+ count: 3,
34
+ framebuffer: offscreen
35
+ )
36
+
37
+ draw_quad = rugl.command(
38
+ vert: <<~GLSL,
39
+ #version 410 core
40
+ layout(location = 0) in vec2 position;
41
+ layout(location = 1) in vec2 uv;
42
+ out vec2 vUv;
43
+ void main() {
44
+ vUv = uv;
45
+ gl_Position = vec4(position, 0.0, 1.0);
46
+ }
47
+ GLSL
48
+ frag: <<~GLSL,
49
+ #version 410 core
50
+ in vec2 vUv;
51
+ uniform sampler2D tex;
52
+ out vec4 fragColor;
53
+ void main() {
54
+ fragColor = texture(tex, vUv);
55
+ }
56
+ GLSL
57
+ attributes: {
58
+ position: rugl.buffer([[-1, -1], [1, -1], [1, 1], [-1, 1]]),
59
+ uv: rugl.buffer([[0, 0], [1, 0], [1, 1], [0, 1]])
60
+ },
61
+ uniforms: {
62
+ tex: offscreen.color_attachment
63
+ },
64
+ primitive: :triangle_fan,
65
+ count: 4
66
+ )
67
+
68
+ rugl.frame do
69
+ offscreen.use do
70
+ rugl.clear(color: [0.0, 0.0, 0.0, 1.0], depth: 1.0)
71
+ draw_scene.call
72
+ end
73
+
74
+ rugl.clear(color: [0.03, 0.03, 0.05, 1.0], depth: 1.0)
75
+ draw_quad.call
76
+ end
data/examples/scope.rb ADDED
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "example_helper"
4
+
5
+ rugl = Rugl.create(width: 800, height: 600, title: "Rugl Scope")
6
+
7
+ draw_triangle = rugl.command(
8
+ vert: <<~GLSL,
9
+ #version 410 core
10
+ layout(location = 0) in vec2 position;
11
+ uniform vec2 offset;
12
+ void main() {
13
+ gl_Position = vec4(position + offset, 0.0, 1.0);
14
+ }
15
+ GLSL
16
+ frag: <<~GLSL,
17
+ #version 410 core
18
+ uniform vec4 color;
19
+ out vec4 fragColor;
20
+ void main() {
21
+ fragColor = color;
22
+ }
23
+ GLSL
24
+ attributes: {
25
+ position: rugl.buffer([[-0.2, -0.2], [0.2, -0.2], [0.0, 0.2]])
26
+ },
27
+ uniforms: {
28
+ offset: Rugl.prop(:offset),
29
+ color: Rugl.prop(:color)
30
+ },
31
+ count: 3
32
+ )
33
+
34
+ scope = rugl.command(
35
+ vert: <<~GLSL,
36
+ #version 410 core
37
+ void main() {
38
+ gl_Position = vec4(0.0);
39
+ }
40
+ GLSL
41
+ frag: <<~GLSL,
42
+ #version 410 core
43
+ uniform vec4 color;
44
+ out vec4 fragColor;
45
+ void main() {
46
+ fragColor = color;
47
+ }
48
+ GLSL
49
+ uniforms: {
50
+ color: Rugl.prop(:color)
51
+ },
52
+ count: 0,
53
+ viewport: { x: 0, y: 0, width: Rugl.context(:viewport_width), height: Rugl.context(:viewport_height) },
54
+ depth: { enable: false }
55
+ )
56
+
57
+ rugl.frame do |ctx|
58
+ rugl.clear(color: [0.08, 0.08, 0.1, 1.0], depth: 1.0)
59
+ t = ctx[:time]
60
+
61
+ scope.call(color: [0.95, 0.6, 0.2, 1.0]) do
62
+ draw_triangle.call(offset: [Math.cos(t) * 0.35, 0.0], color: [0.9, 0.2, 0.3, 1.0])
63
+ draw_triangle.call(offset: [0.0, Math.sin(t * 1.3) * 0.35], color: [0.2, 0.8, 0.9, 1.0])
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "example_helper"
4
+
5
+ rugl = Rugl.create(width: 800, height: 600, title: "Rugl Triangle")
6
+
7
+ draw_triangle = rugl.command(
8
+ vert: <<~GLSL,
9
+ #version 410 core
10
+ layout(location = 0) in vec2 position;
11
+ void main() {
12
+ gl_Position = vec4(position, 0.0, 1.0);
13
+ }
14
+ GLSL
15
+ frag: <<~GLSL,
16
+ #version 410 core
17
+ uniform vec4 color;
18
+ out vec4 fragColor;
19
+ void main() {
20
+ fragColor = color;
21
+ }
22
+ GLSL
23
+ attributes: {
24
+ position: rugl.buffer([[-2, -2], [4, -2], [4, 4]])
25
+ },
26
+ uniforms: {
27
+ color: Rugl.prop(:color)
28
+ },
29
+ count: 3
30
+ )
31
+
32
+ rugl.frame do |ctx|
33
+ rugl.clear(color: [0.0, 0.0, 0.0, 1.0], depth: 1.0)
34
+ t = ctx[:time]
35
+ draw_triangle.call(color: [Math.cos(t), Math.sin(t * 0.8), Math.cos(t * 0.3), 1.0])
36
+ end