rbgl 0.1.0 → 1.0.1

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +20 -2
  4. data/README.md +130 -74
  5. data/Rakefile +7 -0
  6. data/bench/renderer_bench.rb +283 -0
  7. data/docs/.nojekyll +1 -0
  8. data/docs/index.html +38 -0
  9. data/examples/array_test.rb +1 -1
  10. data/examples/color_test.rb +1 -1
  11. data/examples/cube_spinning.rb +1 -1
  12. data/examples/gradient.rb +1 -1
  13. data/examples/multi_return_test.rb +1 -1
  14. data/examples/plasma.rb +1 -1
  15. data/examples/sphere_raymarch.rb +1 -1
  16. data/examples/triangle_window.rb +1 -1
  17. data/exe/rbgl +42 -0
  18. data/lib/rbgl/cli/doctor.rb +155 -0
  19. data/lib/rbgl/engine/buffer.rb +150 -40
  20. data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
  21. data/lib/rbgl/engine/context.rb +133 -63
  22. data/lib/rbgl/engine/framebuffer.rb +153 -48
  23. data/lib/rbgl/engine/immutable_color.rb +32 -0
  24. data/lib/rbgl/engine/pipeline.rb +38 -7
  25. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
  26. data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
  27. data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
  28. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
  29. data/lib/rbgl/engine/rasterizer.rb +65 -171
  30. data/lib/rbgl/engine/shader/base_shader.rb +51 -0
  31. data/lib/rbgl/engine/shader/builtins.rb +257 -0
  32. data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
  33. data/lib/rbgl/engine/shader.rb +5 -318
  34. data/lib/rbgl/engine/texture.rb +264 -51
  35. data/lib/rbgl/engine.rb +4 -1
  36. data/lib/rbgl/gui/backend.rb +21 -34
  37. data/lib/rbgl/gui/backend_factory.rb +89 -0
  38. data/lib/rbgl/gui/cocoa/backend.rb +59 -46
  39. data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
  40. data/lib/rbgl/gui/event.rb +4 -2
  41. data/lib/rbgl/gui/file_backend/bmp_writer.rb +63 -0
  42. data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
  43. data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
  44. data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
  45. data/lib/rbgl/gui/file_backend.rb +50 -55
  46. data/lib/rbgl/gui/wayland/backend.rb +214 -60
  47. data/lib/rbgl/gui/wayland/codec.rb +50 -0
  48. data/lib/rbgl/gui/wayland/connection.rb +188 -231
  49. data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
  50. data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
  51. data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
  52. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
  53. data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
  54. data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
  55. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
  56. data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
  57. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
  58. data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
  59. data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
  60. data/lib/rbgl/gui/window/render_loop.rb +111 -0
  61. data/lib/rbgl/gui/window.rb +74 -84
  62. data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
  63. data/lib/rbgl/gui/x11/backend.rb +73 -49
  64. data/lib/rbgl/gui/x11/connection.rb +284 -219
  65. data/lib/rbgl/gui/x11/event_parser.rb +71 -0
  66. data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
  67. data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
  68. data/lib/rbgl/gui/x11/request_encoder.rb +155 -0
  69. data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
  70. data/lib/rbgl/gui/x11/transport.rb +81 -0
  71. data/lib/rbgl/gui/x11/x_authority.rb +107 -0
  72. data/lib/rbgl/gui.rb +3 -5
  73. data/lib/rbgl/version.rb +1 -1
  74. metadata +63 -24
  75. data/examples/chemical_heartbeat.rb +0 -166
  76. data/examples/dark_transit.rb +0 -166
  77. data/examples/fractured_orb.rb +0 -428
  78. data/examples/fractured_orb_rb.rb +0 -598
  79. data/examples/hexagonal_flow.rb +0 -333
  80. data/examples/teapot.rb +0 -362
  81. data/examples/teapot_mcu.rb +0 -344
@@ -8,6 +8,7 @@ module RBGL
8
8
  def initialize(width:, height:)
9
9
  @framebuffer = Framebuffer.new(width, height)
10
10
  @rasterizer = Rasterizer.new(@framebuffer)
11
+ @clipper = ClipSpaceClipper.new
11
12
  @pipeline = nil
12
13
  @uniforms = Uniforms.new
13
14
  @vertex_buffer = nil
@@ -38,119 +39,188 @@ module RBGL
38
39
  @framebuffer.clear(color: color, depth: depth)
39
40
  end
40
41
 
42
+ def resize(width:, height:)
43
+ @framebuffer.resize(width, height)
44
+ @rasterizer.resize(width, height)
45
+ end
46
+
41
47
  def draw_arrays(mode, first, count)
48
+ validate_draw_state!
49
+ validate_draw_range!(first, count, @vertex_buffer.vertex_count)
50
+ draw_vertices(mode, first...(first + count))
51
+ end
52
+
53
+ def draw_elements(mode, count, offset = 0)
54
+ validate_draw_state!(indexed: true)
55
+ all_indices = @index_buffer.indices
56
+ validate_draw_range!(offset, count, all_indices.size)
57
+ indices = all_indices[offset, count]
58
+ invalid_index = indices.find { |index| index >= @vertex_buffer.vertex_count }
59
+ raise ArgumentError, "Vertex index out of bounds: #{invalid_index}" if invalid_index
60
+
61
+ draw_vertices(mode, indices, vertex_cache: {})
62
+ end
63
+
64
+ def width
65
+ @framebuffer.width
66
+ end
67
+
68
+ def height
69
+ @framebuffer.height
70
+ end
71
+
72
+ def aspect_ratio
73
+ width.to_f / height
74
+ end
75
+
76
+ private
77
+
78
+ def validate_draw_state!(indexed: false)
42
79
  raise "No pipeline bound" unless @pipeline
43
80
  raise "No vertex buffer bound" unless @vertex_buffer
81
+ raise "No index buffer bound" if indexed && !@index_buffer
82
+ end
83
+
84
+ def validate_draw_range!(offset, count, limit)
85
+ unless offset.is_a?(Integer) && count.is_a?(Integer) && offset >= 0 && count >= 0
86
+ raise ArgumentError, "Draw offset and count must be non-negative Integers"
87
+ end
88
+ return if offset + count <= limit
89
+
90
+ raise ArgumentError, "Draw range exceeds available data"
91
+ end
44
92
 
45
- vertices = (first...first + count).map { |i| process_vertex(i) }
93
+ def draw_vertices(mode, indices, vertex_cache: nil)
94
+ each_primitive(mode, indices) do |primitive_indices|
95
+ primitive = primitive_indices.map { |index| fetch_vertex(index, vertex_cache) }
96
+ draw_primitive(mode, primitive)
97
+ end
98
+ end
99
+
100
+ def each_primitive(mode, vertices)
101
+ return enum_for(:each_primitive, mode, vertices) unless block_given?
102
+
103
+ vertices = vertices.to_a unless vertices.respond_to?(:[])
46
104
 
47
105
  case mode
48
106
  when :triangles
49
- (0...vertices.size).step(3) do |i|
50
- draw_triangle(vertices[i], vertices[i + 1], vertices[i + 2])
51
- end
107
+ each_slice(vertices, 3) { |triangle| yield triangle }
52
108
  when :lines
53
- (0...vertices.size).step(2) do |i|
54
- draw_line(vertices[i], vertices[i + 1])
55
- end
109
+ each_slice(vertices, 2) { |line| yield line }
56
110
  when :points
57
- vertices.each { |v| draw_point(v) }
111
+ vertices.each { |vertex| yield [vertex] }
58
112
  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])
113
+ (0...(vertices.size - 2)).each do |index|
114
+ if index.even?
115
+ yield [vertices[index], vertices[index + 1], vertices[index + 2]]
62
116
  else
63
- draw_triangle(vertices[i], vertices[i + 2], vertices[i + 1])
117
+ yield [vertices[index], vertices[index + 2], vertices[index + 1]]
64
118
  end
65
119
  end
66
120
  when :triangle_fan
67
- (1...vertices.size - 1).each do |i|
68
- draw_triangle(vertices[0], vertices[i], vertices[i + 1])
121
+ (1...(vertices.size - 1)).each do |index|
122
+ yield [vertices[0], vertices[index], vertices[index + 1]]
69
123
  end
124
+ else
125
+ raise ArgumentError, "Unsupported primitive mode: #{mode}"
70
126
  end
71
127
  end
72
128
 
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
129
+ def each_slice(vertices, size)
130
+ vertices.each_slice(size) do |primitive|
131
+ next unless primitive.size == size
77
132
 
78
- indices = @index_buffer.indices[offset, count]
79
- vertices = indices.map { |i| process_vertex(i) }
133
+ yield primitive
134
+ end
135
+ end
80
136
 
137
+ def draw_primitive(mode, primitive)
81
138
  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
139
  when :lines
87
- (0...vertices.size).step(2) do |i|
88
- draw_line(vertices[i], vertices[i + 1])
89
- end
140
+ draw_line(*primitive)
141
+ when :points
142
+ draw_point(primitive.first)
143
+ else
144
+ draw_triangle(*primitive)
90
145
  end
91
146
  end
92
147
 
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
148
  def process_vertex(index)
108
149
  input = @vertex_buffer.get_vertex(index)
150
+ return nil unless input
151
+
109
152
  input_io = ShaderIO.new
110
153
  input.each { |k, v| input_io[k] = v }
111
154
 
112
155
  @pipeline.vertex_shader.process(input_io, @uniforms)
113
156
  end
114
157
 
158
+ def fetch_vertex(index, vertex_cache)
159
+ return process_vertex(index) unless vertex_cache
160
+ return vertex_cache[index] if vertex_cache.key?(index)
161
+
162
+ vertex_cache[index] = process_vertex(index)
163
+ end
164
+
115
165
  def draw_triangle(v0, v1, v2)
116
166
  return unless v0 && v1 && v2
117
- return if clip_triangle?(v0, v1, v2)
118
167
 
119
- @rasterizer.rasterize_triangle(
120
- v0, v1, v2,
121
- @pipeline.fragment_shader,
122
- @uniforms,
123
- cull_mode: @pipeline.cull_mode
124
- )
168
+ clip_triangle(v0, v1, v2).each do |triangle|
169
+ @rasterizer.rasterize_triangle(
170
+ *triangle,
171
+ @pipeline.fragment_shader,
172
+ @uniforms,
173
+ **rasterizer_state
174
+ )
175
+ end
125
176
  end
126
177
 
127
178
  def draw_line(v0, v1)
128
179
  return unless v0 && v1
180
+ clipped = clip_line(v0, v1)
181
+ return unless clipped
129
182
 
130
- @rasterizer.rasterize_line(v0, v1, @pipeline.fragment_shader, @uniforms)
183
+ @rasterizer.rasterize_line(*clipped, @pipeline.fragment_shader, @uniforms, **rasterizer_state)
131
184
  end
132
185
 
133
186
  def draw_point(v)
134
187
  return unless v
188
+ clipped = @clipper.clip_point(v)
189
+ return unless clipped
135
190
 
136
- @rasterizer.rasterize_point(v, @pipeline.fragment_shader, @uniforms)
191
+ @rasterizer.rasterize_point(
192
+ clipped,
193
+ @pipeline.fragment_shader,
194
+ @uniforms,
195
+ depth_test: @pipeline.depth_test,
196
+ depth_write: @pipeline.depth_write,
197
+ blend_mode: @pipeline.blend_mode
198
+ )
199
+ end
200
+
201
+ def clip_triangle(v0, v1, v2)
202
+ @clipper.clip(v0, v1, v2)
203
+ end
204
+
205
+ def clip_line(v0, v1)
206
+ @clipper.clip_line(v0, v1)
137
207
  end
138
208
 
139
209
  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
210
+ clip_triangle(v0, v1, v2).empty?
211
+ end
212
+
213
+ def point_visible?(vertex)
214
+ @clipper.visible_point?(vertex)
215
+ end
147
216
 
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 }
217
+ def rasterizer_state
218
+ {
219
+ cull_mode: @pipeline.cull_mode,
220
+ depth_test: @pipeline.depth_test,
221
+ depth_write: @pipeline.depth_write,
222
+ blend_mode: @pipeline.blend_mode
223
+ }
154
224
  end
155
225
  end
156
226
  end
@@ -3,59 +3,80 @@
3
3
  module RBGL
4
4
  module Engine
5
5
  class Framebuffer
6
- attr_reader :width, :height, :color_buffer, :depth_buffer
6
+ attr_reader :width, :height, :depth_buffer
7
+
8
+ def self.from_rgba_bytes(width, height, buffer)
9
+ new(width, height).tap { |framebuffer| framebuffer.send(:load_rgba_bytes, buffer) }
10
+ end
7
11
 
8
12
  def initialize(width, height)
13
+ resize(width, height)
14
+ end
15
+
16
+ def resize(width, height)
17
+ validate_dimensions!(width, height)
18
+ pixels = Array.new(width * height, pack_color(Larb::Color.black))
19
+ depth_buffer = Array.new(width * height) { Float::INFINITY }
20
+
9
21
  @width = width
10
22
  @height = height
11
- @color_buffer = Array.new(width * height) { Larb::Color.black }
12
- @depth_buffer = Array.new(width * height) { Float::INFINITY }
23
+ @pixels = pixels
24
+ @depth_buffer = depth_buffer
25
+ end
26
+
27
+ def color_buffer
28
+ @pixels.map { |pixel| unpack_color(pixel) }
29
+ end
30
+
31
+ def each_packed_pixel(&block)
32
+ return @pixels.each unless block
33
+
34
+ @pixels.each(&block)
13
35
  end
14
36
 
15
37
  def get_pixel(x, y)
16
38
  return nil if x < 0 || x >= @width || y < 0 || y >= @height
17
39
 
18
- @color_buffer[y * @width + x]
40
+ unpack_color(@pixels[(y * @width) + x])
19
41
  end
20
42
 
21
43
  def set_pixel(x, y, color)
22
44
  return if x < 0 || x >= @width || y < 0 || y >= @height
23
45
 
24
- @color_buffer[y * @width + x] = color
46
+ @pixels[(y * @width) + x] = pack_color(color)
25
47
  end
26
48
 
27
49
  def get_depth(x, y)
28
50
  return Float::INFINITY if x < 0 || x >= @width || y < 0 || y >= @height
29
51
 
30
- @depth_buffer[y * @width + x]
52
+ @depth_buffer[(y * @width) + x]
31
53
  end
32
54
 
33
55
  def set_depth(x, y, depth)
34
56
  return if x < 0 || x >= @width || y < 0 || y >= @height
35
57
 
36
- @depth_buffer[y * @width + x] = depth
58
+ @depth_buffer[(y * @width) + x] = depth
37
59
  end
38
60
 
39
- def write_pixel(x, y, color, depth, depth_test: true)
61
+ def write_pixel(x, y, color, depth, depth_test: true, depth_write: true, blend_mode: :none)
40
62
  return false if x < 0 || x >= @width || y < 0 || y >= @height
63
+ return false unless depth.is_a?(Numeric) && depth.real? && depth.finite?
41
64
 
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
65
+ idx = (y * @width) + x
66
+ return false if depth_test && depth >= @depth_buffer[idx]
67
+
68
+ @pixels[idx] = blend_mode == :alpha ? blend_pixel(@pixels[idx], color) : pack_color(color)
69
+ @depth_buffer[idx] = depth if depth_write
70
+ true
50
71
  end
51
72
 
52
73
  def clear(color: Larb::Color.black, depth: Float::INFINITY)
53
- @color_buffer.fill(color)
74
+ @pixels.fill(pack_color(color))
54
75
  @depth_buffer.fill(depth)
55
76
  end
56
77
 
57
78
  def clear_color(color)
58
- @color_buffer.fill(color)
79
+ @pixels.fill(pack_color(color))
59
80
  end
60
81
 
61
82
  def clear_depth(depth = Float::INFINITY)
@@ -63,52 +84,136 @@ module RBGL
63
84
  end
64
85
 
65
86
  def to_ppm
66
- ppm = "P3\n#{@width} #{@height}\n255\n"
87
+ ppm = String.new(capacity: @width * @height * 12, encoding: Encoding::BINARY)
88
+ ppm << "P3\n#{@width} #{@height}\n255\n"
67
89
  @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]}"
90
+ @width.times do |x|
91
+ pixel = @pixels[(y * @width) + x]
92
+ ppm << " " unless x.zero?
93
+ ppm << red_byte(pixel).to_s << " " << green_byte(pixel).to_s << " " << blue_byte(pixel).to_s
72
94
  end
73
- ppm += row.join(" ") + "\n"
95
+ ppm << "\n"
74
96
  end
75
97
  ppm
76
98
  end
77
99
 
78
100
  def to_ppm_binary
79
101
  header = "P6\n#{@width} #{@height}\n255\n"
80
- pixels = @color_buffer.flat_map { |c| c.to_bytes[0..2] }.pack("C*")
102
+ pixels = packed_color_bytes(:rgb)
81
103
  header + pixels
82
104
  end
83
105
 
84
106
  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*")
107
+ packed_color_bytes(:rgba)
97
108
  end
98
109
 
99
110
  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
111
+ packed_color_bytes(:bgra)
112
+ end
113
+
114
+ private
115
+
116
+ def validate_dimensions!(width, height)
117
+ return if width.is_a?(Integer) && width.positive? && height.is_a?(Integer) && height.positive?
118
+
119
+ raise ArgumentError, "Framebuffer dimensions must be positive integers"
120
+ end
121
+
122
+ def load_rgba_bytes(buffer)
123
+ bytes = String.try_convert(buffer)
124
+ raise ArgumentError, "Pixel buffer must be a String" unless bytes
125
+
126
+ expected_size = @width * @height * 4
127
+ unless bytes.bytesize == expected_size
128
+ raise ArgumentError, "Pixel buffer size mismatch: expected #{expected_size}, got #{bytes.bytesize}"
129
+ end
130
+
131
+ @pixels = Array.new(@width * @height)
132
+ pixel_index = 0
133
+ byte_index = 0
134
+ while pixel_index < @pixels.length
135
+ @pixels[pixel_index] = pack_bytes(
136
+ bytes.getbyte(byte_index),
137
+ bytes.getbyte(byte_index + 1),
138
+ bytes.getbyte(byte_index + 2),
139
+ bytes.getbyte(byte_index + 3)
140
+ )
141
+ pixel_index += 1
142
+ byte_index += 4
110
143
  end
111
- bytes.pack("C*")
144
+ end
145
+
146
+ def blend_pixel(destination, source)
147
+ validate_color!(source)
148
+ alpha = source.a
149
+ inv_alpha = 1.0 - alpha
150
+ pack_bytes(
151
+ color_byte((source.r * alpha) + ((red_byte(destination) / 255.0) * inv_alpha)),
152
+ color_byte((source.g * alpha) + ((green_byte(destination) / 255.0) * inv_alpha)),
153
+ color_byte((source.b * alpha) + ((blue_byte(destination) / 255.0) * inv_alpha)),
154
+ color_byte(alpha + ((alpha_byte(destination) / 255.0) * inv_alpha))
155
+ )
156
+ end
157
+
158
+ def pack_color(color)
159
+ validate_color!(color)
160
+ pack_bytes(color_byte(color.r), color_byte(color.g), color_byte(color.b), color_byte(color.a))
161
+ end
162
+
163
+ def validate_color!(color)
164
+ return if color.is_a?(Larb::Color)
165
+
166
+ raise ArgumentError, "Colors must be Larb::Color values"
167
+ end
168
+
169
+ def packed_color_bytes(format)
170
+ channel_count = format == :rgb ? 3 : 4
171
+ bytes = String.new(capacity: @pixels.length * channel_count, encoding: Encoding::BINARY)
172
+
173
+ @pixels.each do |pixel|
174
+ case format
175
+ when :rgb
176
+ bytes << red_byte(pixel) << green_byte(pixel) << blue_byte(pixel)
177
+ when :rgba
178
+ bytes << red_byte(pixel) << green_byte(pixel) << blue_byte(pixel) << alpha_byte(pixel)
179
+ when :bgra
180
+ bytes << blue_byte(pixel) << green_byte(pixel) << red_byte(pixel) << alpha_byte(pixel)
181
+ end
182
+ end
183
+ bytes
184
+ end
185
+
186
+ def unpack_color(pixel)
187
+ ImmutableColor.new(
188
+ red_byte(pixel) / 255.0,
189
+ green_byte(pixel) / 255.0,
190
+ blue_byte(pixel) / 255.0,
191
+ alpha_byte(pixel) / 255.0
192
+ ).freeze
193
+ end
194
+
195
+ def pack_bytes(red, green, blue, alpha)
196
+ (alpha << 24) | (red << 16) | (green << 8) | blue
197
+ end
198
+
199
+ def alpha_byte(pixel)
200
+ (pixel >> 24) & 0xFF
201
+ end
202
+
203
+ def red_byte(pixel)
204
+ (pixel >> 16) & 0xFF
205
+ end
206
+
207
+ def green_byte(pixel)
208
+ (pixel >> 8) & 0xFF
209
+ end
210
+
211
+ def blue_byte(pixel)
212
+ pixel & 0xFF
213
+ end
214
+
215
+ def color_byte(value)
216
+ (value * 255).round.clamp(0, 255)
112
217
  end
113
218
  end
114
219
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class ImmutableColor < Larb::Color
6
+ def self.from(color)
7
+ return color if color.is_a?(self)
8
+ unless color.is_a?(Larb::Color)
9
+ raise ArgumentError, "Colors must be Larb::Color values"
10
+ end
11
+
12
+ new(color.r, color.g, color.b, color.a).freeze
13
+ end
14
+
15
+ def r=(_value)
16
+ raise FrozenError, "stored colors are immutable"
17
+ end
18
+
19
+ def g=(_value)
20
+ raise FrozenError, "stored colors are immutable"
21
+ end
22
+
23
+ def b=(_value)
24
+ raise FrozenError, "stored colors are immutable"
25
+ end
26
+
27
+ def a=(_value)
28
+ raise FrozenError, "stored colors are immutable"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -3,18 +3,19 @@
3
3
  module RBGL
4
4
  module Engine
5
5
  class Pipeline
6
+ VALID_CULL_MODES = %i[none front back].freeze
7
+ VALID_BLEND_MODES = %i[none alpha].freeze
8
+
6
9
  attr_accessor :vertex_shader, :fragment_shader
7
- attr_accessor :depth_test, :depth_write
8
- attr_accessor :cull_mode
9
- attr_accessor :blend_mode
10
+ attr_reader :depth_test, :depth_write, :cull_mode, :blend_mode
10
11
 
11
12
  def initialize
12
13
  @vertex_shader = nil
13
14
  @fragment_shader = nil
14
- @depth_test = true
15
- @depth_write = true
16
- @cull_mode = :back
17
- @blend_mode = :none
15
+ self.depth_test = true
16
+ self.depth_write = true
17
+ self.cull_mode = :back
18
+ self.blend_mode = :none
18
19
  end
19
20
 
20
21
  def self.create(&block)
@@ -30,6 +31,36 @@ module RBGL
30
31
  def fragment(&block)
31
32
  @fragment_shader = FragmentShader.new(&block)
32
33
  end
34
+
35
+ def depth_test=(value)
36
+ @depth_test = validate_boolean!(:depth_test, value)
37
+ end
38
+
39
+ def depth_write=(value)
40
+ @depth_write = validate_boolean!(:depth_write, value)
41
+ end
42
+
43
+ def cull_mode=(value)
44
+ @cull_mode = validate_enum!(:cull_mode, value, VALID_CULL_MODES)
45
+ end
46
+
47
+ def blend_mode=(value)
48
+ @blend_mode = validate_enum!(:blend_mode, value, VALID_BLEND_MODES)
49
+ end
50
+
51
+ private
52
+
53
+ def validate_boolean!(name, value)
54
+ return value if value == true || value == false
55
+
56
+ raise ArgumentError, "#{name} must be true or false"
57
+ end
58
+
59
+ def validate_enum!(name, value, allowed_values)
60
+ return value if allowed_values.include?(value)
61
+
62
+ raise ArgumentError, "#{name} must be one of: #{allowed_values.join(', ')}"
63
+ end
33
64
  end
34
65
  end
35
66
  end