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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +20 -2
- data/README.md +130 -74
- data/Rakefile +7 -0
- data/bench/renderer_bench.rb +283 -0
- data/docs/.nojekyll +1 -0
- data/docs/index.html +38 -0
- data/examples/array_test.rb +1 -1
- data/examples/color_test.rb +1 -1
- data/examples/cube_spinning.rb +1 -1
- data/examples/gradient.rb +1 -1
- data/examples/multi_return_test.rb +1 -1
- data/examples/plasma.rb +1 -1
- data/examples/sphere_raymarch.rb +1 -1
- data/examples/triangle_window.rb +1 -1
- data/exe/rbgl +42 -0
- data/lib/rbgl/cli/doctor.rb +155 -0
- data/lib/rbgl/engine/buffer.rb +150 -40
- data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
- data/lib/rbgl/engine/context.rb +133 -63
- data/lib/rbgl/engine/framebuffer.rb +153 -48
- data/lib/rbgl/engine/immutable_color.rb +32 -0
- data/lib/rbgl/engine/pipeline.rb +38 -7
- data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
- data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
- data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
- data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
- data/lib/rbgl/engine/rasterizer.rb +65 -171
- data/lib/rbgl/engine/shader/base_shader.rb +51 -0
- data/lib/rbgl/engine/shader/builtins.rb +257 -0
- data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
- data/lib/rbgl/engine/shader.rb +5 -318
- data/lib/rbgl/engine/texture.rb +264 -51
- data/lib/rbgl/engine.rb +4 -1
- data/lib/rbgl/gui/backend.rb +21 -34
- data/lib/rbgl/gui/backend_factory.rb +89 -0
- data/lib/rbgl/gui/cocoa/backend.rb +59 -46
- data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
- data/lib/rbgl/gui/event.rb +4 -2
- data/lib/rbgl/gui/file_backend/bmp_writer.rb +63 -0
- data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
- data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
- data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
- data/lib/rbgl/gui/file_backend.rb +50 -55
- data/lib/rbgl/gui/wayland/backend.rb +214 -60
- data/lib/rbgl/gui/wayland/codec.rb +50 -0
- data/lib/rbgl/gui/wayland/connection.rb +188 -231
- data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
- data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
- data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
- data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
- data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
- data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
- data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
- data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
- data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
- data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
- data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
- data/lib/rbgl/gui/window/render_loop.rb +111 -0
- data/lib/rbgl/gui/window.rb +74 -84
- data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
- data/lib/rbgl/gui/x11/backend.rb +73 -49
- data/lib/rbgl/gui/x11/connection.rb +284 -219
- data/lib/rbgl/gui/x11/event_parser.rb +71 -0
- data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
- data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
- data/lib/rbgl/gui/x11/request_encoder.rb +155 -0
- data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
- data/lib/rbgl/gui/x11/transport.rb +81 -0
- data/lib/rbgl/gui/x11/x_authority.rb +107 -0
- data/lib/rbgl/gui.rb +3 -5
- data/lib/rbgl/version.rb +1 -1
- metadata +63 -24
- data/examples/chemical_heartbeat.rb +0 -166
- data/examples/dark_transit.rb +0 -166
- data/examples/fractured_orb.rb +0 -428
- data/examples/fractured_orb_rb.rb +0 -598
- data/examples/hexagonal_flow.rb +0 -333
- data/examples/teapot.rb +0 -362
- data/examples/teapot_mcu.rb +0 -344
data/lib/rbgl/engine/context.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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 { |
|
|
111
|
+
vertices.each { |vertex| yield [vertex] }
|
|
58
112
|
when :triangle_strip
|
|
59
|
-
(0...vertices.size - 2).each do |
|
|
60
|
-
if
|
|
61
|
-
|
|
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
|
-
|
|
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 |
|
|
68
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
(
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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, :
|
|
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
|
-
@
|
|
12
|
-
@depth_buffer =
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
@
|
|
74
|
+
@pixels.fill(pack_color(color))
|
|
54
75
|
@depth_buffer.fill(depth)
|
|
55
76
|
end
|
|
56
77
|
|
|
57
78
|
def clear_color(color)
|
|
58
|
-
@
|
|
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 =
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
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
|
|
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 =
|
|
102
|
+
pixels = packed_color_bytes(:rgb)
|
|
81
103
|
header + pixels
|
|
82
104
|
end
|
|
83
105
|
|
|
84
106
|
def to_rgba_bytes
|
|
85
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
data/lib/rbgl/engine/pipeline.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|