rbgl 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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +21 -0
  4. data/README.md +123 -0
  5. data/Rakefile +12 -0
  6. data/examples/array_test.rb +99 -0
  7. data/examples/chemical_heartbeat.rb +166 -0
  8. data/examples/color_test.rb +61 -0
  9. data/examples/cube_spinning.rb +87 -0
  10. data/examples/dark_transit.rb +166 -0
  11. data/examples/fractured_orb.rb +428 -0
  12. data/examples/fractured_orb_rb.rb +598 -0
  13. data/examples/gradient.rb +84 -0
  14. data/examples/hexagonal_flow.rb +333 -0
  15. data/examples/multi_return_test.rb +98 -0
  16. data/examples/plasma.rb +78 -0
  17. data/examples/sphere_raymarch.rb +126 -0
  18. data/examples/teapot.rb +362 -0
  19. data/examples/teapot_mcu.rb +344 -0
  20. data/examples/triangle_basic.rb +36 -0
  21. data/examples/triangle_window.rb +62 -0
  22. data/examples/window_test.rb +36 -0
  23. data/lib/rbgl/engine/buffer.rb +160 -0
  24. data/lib/rbgl/engine/context.rb +157 -0
  25. data/lib/rbgl/engine/framebuffer.rb +115 -0
  26. data/lib/rbgl/engine/pipeline.rb +35 -0
  27. data/lib/rbgl/engine/rasterizer.rb +213 -0
  28. data/lib/rbgl/engine/shader.rb +324 -0
  29. data/lib/rbgl/engine/texture.rb +125 -0
  30. data/lib/rbgl/engine.rb +15 -0
  31. data/lib/rbgl/gui/backend.rb +76 -0
  32. data/lib/rbgl/gui/cocoa/backend.rb +121 -0
  33. data/lib/rbgl/gui/event.rb +34 -0
  34. data/lib/rbgl/gui/file_backend.rb +91 -0
  35. data/lib/rbgl/gui/wayland/backend.rb +126 -0
  36. data/lib/rbgl/gui/wayland/connection.rb +331 -0
  37. data/lib/rbgl/gui/window.rb +148 -0
  38. data/lib/rbgl/gui/x11/backend.rb +156 -0
  39. data/lib/rbgl/gui/x11/connection.rb +344 -0
  40. data/lib/rbgl/gui.rb +16 -0
  41. data/lib/rbgl/version.rb +5 -0
  42. data/lib/rbgl.rb +6 -0
  43. metadata +114 -0
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "rbgl"
6
+
7
+ include Larb
8
+ include RBGL::Engine
9
+
10
+ ctx = Context.new(width: 320, height: 240)
11
+
12
+ pipeline = Pipeline.create do
13
+ vertex do |input, _uniforms, output|
14
+ output.position = input.position.to_vec4
15
+ output.color = input.color
16
+ end
17
+
18
+ fragment do |input, _uniforms, output|
19
+ output.color = input.color
20
+ end
21
+ end
22
+
23
+ layout = VertexLayout.position_color
24
+ vertices = VertexBuffer.from_array(layout, [
25
+ { position: Vec3[0.0, 0.5, 0.0], color: Color.red },
26
+ { position: Vec3[-0.5, -0.5, 0.0], color: Color.green },
27
+ { position: Vec3[0.5, -0.5, 0.0], color: Color.blue }
28
+ ])
29
+
30
+ ctx.clear(color: Color.from_hex("#1a1a2e"))
31
+ ctx.bind_pipeline(pipeline)
32
+ ctx.bind_vertex_buffer(vertices)
33
+ ctx.draw_arrays(:triangles, 0, 3)
34
+
35
+ File.write("hello_triangle.ppm", ctx.framebuffer.to_ppm)
36
+ puts "Rendered to hello_triangle.ppm"
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "rbgl"
6
+
7
+ include Larb
8
+ include RBGL::Engine
9
+
10
+ window = RBGL::GUI::Window.new(
11
+ width: 320,
12
+ height: 240,
13
+ title: "Hello Triangle - Window",
14
+ backend: :auto
15
+ )
16
+
17
+ pipeline = Pipeline.create do
18
+ vertex do |input, uniforms, output|
19
+ angle = uniforms.time || 0
20
+ pos = input.position
21
+
22
+ rotated_x = pos.x * Math.cos(angle) - pos.y * Math.sin(angle)
23
+ rotated_y = pos.x * Math.sin(angle) + pos.y * Math.cos(angle)
24
+
25
+ output.position = Vec4[rotated_x, rotated_y, pos.z, 1.0]
26
+ output.color = input.color
27
+ end
28
+
29
+ fragment do |input, _uniforms, output|
30
+ output.color = input.color
31
+ end
32
+ end
33
+
34
+ layout = VertexLayout.position_color
35
+ vertices = VertexBuffer.from_array(layout, [
36
+ { position: Vec3[0.0, 0.5, 0.0], color: Color.red },
37
+ { position: Vec3[-0.5, -0.5, 0.0], color: Color.green },
38
+ { position: Vec3[0.5, -0.5, 0.0], color: Color.blue }
39
+ ])
40
+
41
+ time = 0.0
42
+
43
+ window.on(:key_press) do |event|
44
+ puts "Key: #{event.key}"
45
+ window.stop if event.key == 12 || event.key == "q" || event.key == "Escape"
46
+ end
47
+
48
+ window.on(:mouse_move) do |event|
49
+ puts "Mouse: #{event.x}, #{event.y}" if event.x && event.y
50
+ end
51
+
52
+ puts "Spinning triangle - Press 'q' or Escape to quit"
53
+
54
+ window.run do |ctx, dt|
55
+ time += dt
56
+
57
+ ctx.clear(color: Color.from_hex("#1a1a2e"))
58
+ ctx.bind_pipeline(pipeline)
59
+ ctx.bind_vertex_buffer(vertices)
60
+ ctx.set_uniform(:time, time)
61
+ ctx.draw_arrays(:triangles, 0, 3)
62
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "rbgl"
6
+
7
+ puts "Creating window..."
8
+
9
+ window = RBGL::GUI::Window.new(
10
+ width: 320,
11
+ height: 240,
12
+ title: "Simple Test",
13
+ backend: :cocoa
14
+ )
15
+
16
+ puts "Window created, starting render loop..."
17
+
18
+ frame = 0
19
+ window.run do |ctx, dt|
20
+ frame += 1
21
+
22
+ # Simple red background
23
+ ctx.clear(color: Larb::Color.red)
24
+
25
+ if frame % 60 == 0
26
+ puts "Frame: #{frame}, FPS: #{window.fps.round(1)}"
27
+ end
28
+
29
+ # Exit after 5 seconds
30
+ if frame > 300
31
+ puts "Stopping..."
32
+ window.stop
33
+ end
34
+ end
35
+
36
+ puts "Done"
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class VertexAttribute
6
+ attr_reader :name, :size, :offset
7
+
8
+ def initialize(name, size, offset = 0)
9
+ @name = name.to_sym
10
+ @size = size
11
+ @offset = offset
12
+ end
13
+ end
14
+
15
+ class VertexLayout
16
+ attr_reader :attributes, :stride
17
+
18
+ def initialize(&block)
19
+ @attributes = {}
20
+ @offset = 0
21
+ instance_eval(&block) if block_given?
22
+ @stride = @offset
23
+ end
24
+
25
+ def attribute(name, size)
26
+ @attributes[name.to_sym] = VertexAttribute.new(name, size, @offset)
27
+ @offset += size
28
+ end
29
+
30
+ def self.position_only
31
+ new { attribute :position, 3 }
32
+ end
33
+
34
+ def self.position_color
35
+ new do
36
+ attribute :position, 3
37
+ attribute :color, 4
38
+ end
39
+ end
40
+
41
+ def self.position_normal_uv
42
+ new do
43
+ attribute :position, 3
44
+ attribute :normal, 3
45
+ attribute :uv, 2
46
+ end
47
+ end
48
+
49
+ def self.position_normal_uv_color
50
+ new do
51
+ attribute :position, 3
52
+ attribute :normal, 3
53
+ attribute :uv, 2
54
+ attribute :color, 4
55
+ end
56
+ end
57
+ end
58
+
59
+ class VertexBuffer
60
+ attr_reader :data, :layout, :vertex_count
61
+
62
+ def initialize(layout)
63
+ @layout = layout
64
+ @data = []
65
+ @vertex_count = 0
66
+ end
67
+
68
+ def add_vertex(**attributes)
69
+ vertex_data = []
70
+ @layout.attributes.each do |name, attr|
71
+ value = attributes[name]
72
+ raise "Missing attribute: #{name}" unless value
73
+
74
+ case value
75
+ when Larb::Vec2
76
+ vertex_data.concat([value.x, value.y])
77
+ when Larb::Vec3
78
+ vertex_data.concat([value.x, value.y, value.z])
79
+ when Larb::Vec4
80
+ vertex_data.concat([value.x, value.y, value.z, value.w])
81
+ when Larb::Color
82
+ vertex_data.concat(value.to_a)
83
+ when Array
84
+ vertex_data.concat(value)
85
+ when Numeric
86
+ vertex_data << value.to_f
87
+ end
88
+ end
89
+ @data.concat(vertex_data)
90
+ @vertex_count += 1
91
+ self
92
+ end
93
+
94
+ def add_vertices(*vertices)
95
+ vertices.each { |v| add_vertex(**v) }
96
+ self
97
+ end
98
+
99
+ def get_vertex(index)
100
+ return nil if index < 0 || index >= @vertex_count
101
+
102
+ start = index * @layout.stride
103
+ vertex = {}
104
+
105
+ @layout.attributes.each do |name, attr|
106
+ offset = start + attr.offset
107
+ values = @data[offset, attr.size]
108
+
109
+ vertex[name] = case attr.size
110
+ when 1 then values[0]
111
+ when 2 then Larb::Vec2.new(*values)
112
+ when 3 then Larb::Vec3.new(*values)
113
+ when 4
114
+ if name == :color
115
+ Larb::Color.new(*values)
116
+ else
117
+ Larb::Vec4.new(*values)
118
+ end
119
+ end
120
+ end
121
+
122
+ vertex
123
+ end
124
+
125
+ def self.from_array(layout, data)
126
+ buffer = new(layout)
127
+ data.each { |vertex| buffer.add_vertex(**vertex) }
128
+ buffer
129
+ end
130
+ end
131
+
132
+ class IndexBuffer
133
+ attr_reader :indices
134
+
135
+ def initialize(indices = [])
136
+ @indices = indices.map(&:to_i)
137
+ end
138
+
139
+ def add(*idx)
140
+ @indices.concat(idx.flatten.map(&:to_i))
141
+ self
142
+ end
143
+
144
+ def triangle_count
145
+ @indices.size / 3
146
+ end
147
+
148
+ def get_triangle(index)
149
+ start = index * 3
150
+ @indices[start, 3]
151
+ end
152
+
153
+ def each_triangle
154
+ return enum_for(:each_triangle) unless block_given?
155
+
156
+ (0...triangle_count).each { |i| yield get_triangle(i) }
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class Context
6
+ attr_reader :framebuffer, :rasterizer
7
+
8
+ def initialize(width:, height:)
9
+ @framebuffer = Framebuffer.new(width, height)
10
+ @rasterizer = Rasterizer.new(@framebuffer)
11
+ @pipeline = nil
12
+ @uniforms = Uniforms.new
13
+ @vertex_buffer = nil
14
+ @index_buffer = nil
15
+ end
16
+
17
+ def bind_pipeline(pipeline)
18
+ @pipeline = pipeline
19
+ end
20
+
21
+ def bind_vertex_buffer(buffer)
22
+ @vertex_buffer = buffer
23
+ end
24
+
25
+ def bind_index_buffer(buffer)
26
+ @index_buffer = buffer
27
+ end
28
+
29
+ def set_uniform(name, value)
30
+ @uniforms[name] = value
31
+ end
32
+
33
+ def set_uniforms(hash)
34
+ hash.each { |k, v| @uniforms[k] = v }
35
+ end
36
+
37
+ def clear(color: Larb::Color.black, depth: Float::INFINITY)
38
+ @framebuffer.clear(color: color, depth: depth)
39
+ end
40
+
41
+ def draw_arrays(mode, first, count)
42
+ raise "No pipeline bound" unless @pipeline
43
+ raise "No vertex buffer bound" unless @vertex_buffer
44
+
45
+ vertices = (first...first + count).map { |i| process_vertex(i) }
46
+
47
+ case mode
48
+ when :triangles
49
+ (0...vertices.size).step(3) do |i|
50
+ draw_triangle(vertices[i], vertices[i + 1], vertices[i + 2])
51
+ end
52
+ when :lines
53
+ (0...vertices.size).step(2) do |i|
54
+ draw_line(vertices[i], vertices[i + 1])
55
+ end
56
+ when :points
57
+ vertices.each { |v| draw_point(v) }
58
+ when :triangle_strip
59
+ (0...vertices.size - 2).each do |i|
60
+ if i.even?
61
+ draw_triangle(vertices[i], vertices[i + 1], vertices[i + 2])
62
+ else
63
+ draw_triangle(vertices[i], vertices[i + 2], vertices[i + 1])
64
+ end
65
+ end
66
+ when :triangle_fan
67
+ (1...vertices.size - 1).each do |i|
68
+ draw_triangle(vertices[0], vertices[i], vertices[i + 1])
69
+ end
70
+ end
71
+ end
72
+
73
+ def draw_elements(mode, count, offset = 0)
74
+ raise "No pipeline bound" unless @pipeline
75
+ raise "No vertex buffer bound" unless @vertex_buffer
76
+ raise "No index buffer bound" unless @index_buffer
77
+
78
+ indices = @index_buffer.indices[offset, count]
79
+ vertices = indices.map { |i| process_vertex(i) }
80
+
81
+ case mode
82
+ when :triangles
83
+ (0...vertices.size).step(3) do |i|
84
+ draw_triangle(vertices[i], vertices[i + 1], vertices[i + 2])
85
+ end
86
+ when :lines
87
+ (0...vertices.size).step(2) do |i|
88
+ draw_line(vertices[i], vertices[i + 1])
89
+ end
90
+ end
91
+ end
92
+
93
+ def width
94
+ @framebuffer.width
95
+ end
96
+
97
+ def height
98
+ @framebuffer.height
99
+ end
100
+
101
+ def aspect_ratio
102
+ width.to_f / height
103
+ end
104
+
105
+ private
106
+
107
+ def process_vertex(index)
108
+ input = @vertex_buffer.get_vertex(index)
109
+ input_io = ShaderIO.new
110
+ input.each { |k, v| input_io[k] = v }
111
+
112
+ @pipeline.vertex_shader.process(input_io, @uniforms)
113
+ end
114
+
115
+ def draw_triangle(v0, v1, v2)
116
+ return unless v0 && v1 && v2
117
+ return if clip_triangle?(v0, v1, v2)
118
+
119
+ @rasterizer.rasterize_triangle(
120
+ v0, v1, v2,
121
+ @pipeline.fragment_shader,
122
+ @uniforms,
123
+ cull_mode: @pipeline.cull_mode
124
+ )
125
+ end
126
+
127
+ def draw_line(v0, v1)
128
+ return unless v0 && v1
129
+
130
+ @rasterizer.rasterize_line(v0, v1, @pipeline.fragment_shader, @uniforms)
131
+ end
132
+
133
+ def draw_point(v)
134
+ return unless v
135
+
136
+ @rasterizer.rasterize_point(v, @pipeline.fragment_shader, @uniforms)
137
+ end
138
+
139
+ def clip_triangle?(v0, v1, v2)
140
+ positions = [v0[:position], v1[:position], v2[:position]].map do |p|
141
+ if p.is_a?(Larb::Vec4)
142
+ p.perspective_divide
143
+ else
144
+ p
145
+ end
146
+ end
147
+
148
+ positions.all? { |p| p.x < -1 } ||
149
+ positions.all? { |p| p.x > 1 } ||
150
+ positions.all? { |p| p.y < -1 } ||
151
+ positions.all? { |p| p.y > 1 } ||
152
+ positions.all? { |p| p.z < -1 } ||
153
+ positions.all? { |p| p.z > 1 }
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class Framebuffer
6
+ attr_reader :width, :height, :color_buffer, :depth_buffer
7
+
8
+ def initialize(width, height)
9
+ @width = width
10
+ @height = height
11
+ @color_buffer = Array.new(width * height) { Larb::Color.black }
12
+ @depth_buffer = Array.new(width * height) { Float::INFINITY }
13
+ end
14
+
15
+ def get_pixel(x, y)
16
+ return nil if x < 0 || x >= @width || y < 0 || y >= @height
17
+
18
+ @color_buffer[y * @width + x]
19
+ end
20
+
21
+ def set_pixel(x, y, color)
22
+ return if x < 0 || x >= @width || y < 0 || y >= @height
23
+
24
+ @color_buffer[y * @width + x] = color
25
+ end
26
+
27
+ def get_depth(x, y)
28
+ return Float::INFINITY if x < 0 || x >= @width || y < 0 || y >= @height
29
+
30
+ @depth_buffer[y * @width + x]
31
+ end
32
+
33
+ def set_depth(x, y, depth)
34
+ return if x < 0 || x >= @width || y < 0 || y >= @height
35
+
36
+ @depth_buffer[y * @width + x] = depth
37
+ end
38
+
39
+ def write_pixel(x, y, color, depth, depth_test: true)
40
+ return false if x < 0 || x >= @width || y < 0 || y >= @height
41
+
42
+ idx = y * @width + x
43
+ if !depth_test || depth < @depth_buffer[idx]
44
+ @color_buffer[idx] = color
45
+ @depth_buffer[idx] = depth
46
+ true
47
+ else
48
+ false
49
+ end
50
+ end
51
+
52
+ def clear(color: Larb::Color.black, depth: Float::INFINITY)
53
+ @color_buffer.fill(color)
54
+ @depth_buffer.fill(depth)
55
+ end
56
+
57
+ def clear_color(color)
58
+ @color_buffer.fill(color)
59
+ end
60
+
61
+ def clear_depth(depth = Float::INFINITY)
62
+ @depth_buffer.fill(depth)
63
+ end
64
+
65
+ def to_ppm
66
+ ppm = "P3\n#{@width} #{@height}\n255\n"
67
+ @height.times do |y|
68
+ row = @width.times.map do |x|
69
+ c = @color_buffer[y * @width + x]
70
+ bytes = c.to_bytes
71
+ "#{bytes[0]} #{bytes[1]} #{bytes[2]}"
72
+ end
73
+ ppm += row.join(" ") + "\n"
74
+ end
75
+ ppm
76
+ end
77
+
78
+ def to_ppm_binary
79
+ header = "P6\n#{@width} #{@height}\n255\n"
80
+ pixels = @color_buffer.flat_map { |c| c.to_bytes[0..2] }.pack("C*")
81
+ header + pixels
82
+ end
83
+
84
+ def to_rgba_bytes
85
+ size = @color_buffer.size
86
+ bytes = Array.new(size * 4)
87
+ i = 0
88
+ size.times do |idx|
89
+ c = @color_buffer[idx]
90
+ bytes[i] = (c.r * 255).round.clamp(0, 255)
91
+ bytes[i + 1] = (c.g * 255).round.clamp(0, 255)
92
+ bytes[i + 2] = (c.b * 255).round.clamp(0, 255)
93
+ bytes[i + 3] = (c.a * 255).round.clamp(0, 255)
94
+ i += 4
95
+ end
96
+ bytes.pack("C*")
97
+ end
98
+
99
+ def to_bgra_bytes
100
+ size = @color_buffer.size
101
+ bytes = Array.new(size * 4)
102
+ i = 0
103
+ size.times do |idx|
104
+ c = @color_buffer[idx]
105
+ bytes[i] = (c.b * 255).round.clamp(0, 255)
106
+ bytes[i + 1] = (c.g * 255).round.clamp(0, 255)
107
+ bytes[i + 2] = (c.r * 255).round.clamp(0, 255)
108
+ bytes[i + 3] = (c.a * 255).round.clamp(0, 255)
109
+ i += 4
110
+ end
111
+ bytes.pack("C*")
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class Pipeline
6
+ attr_accessor :vertex_shader, :fragment_shader
7
+ attr_accessor :depth_test, :depth_write
8
+ attr_accessor :cull_mode
9
+ attr_accessor :blend_mode
10
+
11
+ def initialize
12
+ @vertex_shader = nil
13
+ @fragment_shader = nil
14
+ @depth_test = true
15
+ @depth_write = true
16
+ @cull_mode = :back
17
+ @blend_mode = :none
18
+ end
19
+
20
+ def self.create(&block)
21
+ pipeline = new
22
+ pipeline.instance_eval(&block) if block_given?
23
+ pipeline
24
+ end
25
+
26
+ def vertex(&block)
27
+ @vertex_shader = VertexShader.new(&block)
28
+ end
29
+
30
+ def fragment(&block)
31
+ @fragment_shader = FragmentShader.new(&block)
32
+ end
33
+ end
34
+ end
35
+ end