rbgl 0.1.0 → 1.0.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -1
  3. data/lib/rbgl/engine/buffer.rb +102 -33
  4. data/lib/rbgl/engine/clip_space_clipper.rb +143 -0
  5. data/lib/rbgl/engine/context.rb +117 -64
  6. data/lib/rbgl/engine/framebuffer.rb +41 -32
  7. data/lib/rbgl/engine/pipeline.rb +38 -7
  8. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +105 -0
  9. data/lib/rbgl/engine/rasterizer/line_renderer.rb +72 -0
  10. data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
  11. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +87 -0
  12. data/lib/rbgl/engine/rasterizer.rb +73 -162
  13. data/lib/rbgl/engine/shader/base_shader.rb +55 -0
  14. data/lib/rbgl/engine/shader/builtins.rb +254 -0
  15. data/lib/rbgl/engine/shader/dynamic_data.rb +65 -0
  16. data/lib/rbgl/engine/shader.rb +5 -318
  17. data/lib/rbgl/engine/texture.rb +227 -38
  18. data/lib/rbgl/engine.rb +1 -0
  19. data/lib/rbgl/gui/backend.rb +12 -30
  20. data/lib/rbgl/gui/backend_factory.rb +85 -0
  21. data/lib/rbgl/gui/cocoa/backend.rb +15 -35
  22. data/lib/rbgl/gui/file_backend/bmp_writer.rb +63 -0
  23. data/lib/rbgl/gui/file_backend/frame_writer.rb +28 -0
  24. data/lib/rbgl/gui/file_backend/ppm_writer.rb +25 -0
  25. data/lib/rbgl/gui/file_backend.rb +25 -48
  26. data/lib/rbgl/gui/wayland/backend.rb +108 -26
  27. data/lib/rbgl/gui/wayland/codec.rb +54 -0
  28. data/lib/rbgl/gui/wayland/connection.rb +80 -240
  29. data/lib/rbgl/gui/wayland/event_dispatcher.rb +74 -0
  30. data/lib/rbgl/gui/wayland/global_binder.rb +44 -0
  31. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +51 -0
  32. data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
  33. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +146 -0
  34. data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
  35. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +45 -0
  36. data/lib/rbgl/gui/wayland/protocol_objects.rb +7 -0
  37. data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
  38. data/lib/rbgl/gui/window/render_loop.rb +58 -0
  39. data/lib/rbgl/gui/window.rb +41 -82
  40. data/lib/rbgl/gui/x11/atom_cache.rb +26 -0
  41. data/lib/rbgl/gui/x11/backend.rb +14 -28
  42. data/lib/rbgl/gui/x11/connection.rb +104 -169
  43. data/lib/rbgl/gui/x11/event_parser.rb +60 -0
  44. data/lib/rbgl/gui/x11/request_encoder.rb +154 -0
  45. data/lib/rbgl/gui/x11/transport.rb +31 -0
  46. data/lib/rbgl/gui.rb +1 -0
  47. data/lib/rbgl/version.rb +1 -1
  48. metadata +31 -4
@@ -1,324 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "shader/dynamic_data"
4
+ require_relative "shader/builtins"
5
+ require_relative "shader/base_shader"
6
+
3
7
  module RBGL
4
8
  module Engine
5
- class ShaderIO
6
- def initialize
7
- @data = {}
8
- end
9
-
10
- def method_missing(name, *args)
11
- if name.to_s.end_with?("=")
12
- @data[name.to_s.chomp("=").to_sym] = args.first
13
- else
14
- @data[name]
15
- end
16
- end
17
-
18
- def respond_to_missing?(_name, _include_private = false)
19
- true
20
- end
21
-
22
- def [](key)
23
- @data[key]
24
- end
25
-
26
- def []=(key, value)
27
- @data[key] = value
28
- end
29
-
30
- def to_h
31
- @data.dup
32
- end
33
-
34
- def keys
35
- @data.keys
36
- end
37
- end
38
-
39
- class Uniforms
40
- def initialize(data = {})
41
- @data = data.transform_keys(&:to_sym)
42
- end
43
-
44
- def method_missing(name, *args)
45
- if name.to_s.end_with?("=")
46
- @data[name.to_s.chomp("=").to_sym] = args.first
47
- else
48
- @data[name]
49
- end
50
- end
51
-
52
- def respond_to_missing?(_name, _include_private = false)
53
- true
54
- end
55
-
56
- def [](key)
57
- @data[key.to_sym]
58
- end
59
-
60
- def []=(key, value)
61
- @data[key.to_sym] = value
62
- end
63
-
64
- def merge(other)
65
- Uniforms.new(@data.merge(other.to_h))
66
- end
67
-
68
- def to_h
69
- @data.dup
70
- end
71
- end
72
-
73
- module ShaderBuiltins
74
- def vec2(x, y = nil)
75
- y ||= x
76
- Larb::Vec2.new(x, y)
77
- end
78
-
79
- def vec3(x, y = nil, z = nil)
80
- if y.nil? && z.nil?
81
- Larb::Vec3.new(x, x, x)
82
- elsif z.nil? && y.is_a?(Larb::Vec2)
83
- Larb::Vec3.new(x, y.x, y.y)
84
- else
85
- Larb::Vec3.new(x, y, z)
86
- end
87
- end
88
-
89
- def vec4(x, y = nil, z = nil, w = nil)
90
- case
91
- when y.nil? && z.nil? && w.nil?
92
- Larb::Vec4.new(x, x, x, x)
93
- when x.is_a?(Larb::Vec3) && !y.nil?
94
- Larb::Vec4.new(x.x, x.y, x.z, y)
95
- when x.is_a?(Larb::Vec2) && y.is_a?(Larb::Vec2)
96
- Larb::Vec4.new(x.x, x.y, y.x, y.y)
97
- else
98
- Larb::Vec4.new(x, y, z, w)
99
- end
100
- end
101
-
102
- def dot(a, b)
103
- a.dot(b)
104
- end
105
-
106
- def cross(a, b)
107
- a.cross(b)
108
- end
109
-
110
- def normalize(v)
111
- v.normalize
112
- end
113
-
114
- def length(v)
115
- v.length
116
- end
117
-
118
- def reflect(v, n)
119
- v.reflect(n)
120
- end
121
-
122
- def refract(v, n, eta)
123
- cos_i = -dot(n, v)
124
- sin_t2 = eta * eta * (1.0 - cos_i * cos_i)
125
- return vec3(0) if sin_t2 > 1.0
126
-
127
- cos_t = Math.sqrt(1.0 - sin_t2)
128
- v * eta + n * (eta * cos_i - cos_t)
129
- end
130
-
131
- def mix(a, b, t)
132
- case a
133
- when Numeric then a + (b - a) * t
134
- when Larb::Vec2, Larb::Vec3, Larb::Vec4 then a.lerp(b, t)
135
- when Larb::Color then a.lerp(b, t)
136
- end
137
- end
138
- alias lerp mix
139
-
140
- def clamp(v, min_val, max_val)
141
- case v
142
- when Numeric then v.clamp(min_val, max_val)
143
- when Larb::Vec3
144
- Larb::Vec3.new(
145
- v.x.clamp(min_val, max_val),
146
- v.y.clamp(min_val, max_val),
147
- v.z.clamp(min_val, max_val)
148
- )
149
- when Larb::Color then v.clamp
150
- end
151
- end
152
-
153
- def saturate(v)
154
- clamp(v, 0.0, 1.0)
155
- end
156
-
157
- def smoothstep(edge0, edge1, x)
158
- t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0)
159
- t * t * (3.0 - 2.0 * t)
160
- end
161
-
162
- def step(edge, x)
163
- x < edge ? 0.0 : 1.0
164
- end
165
-
166
- def fract(x)
167
- x - x.floor
168
- end
169
-
170
- def mod(x, y)
171
- x - y * (x / y).floor
172
- end
173
-
174
- def abs(x)
175
- case x
176
- when Numeric then x.abs
177
- when Larb::Vec3 then Larb::Vec3.new(x.x.abs, x.y.abs, x.z.abs)
178
- end
179
- end
180
-
181
- def sign(x)
182
- x <=> 0
183
- end
184
-
185
- def floor(x)
186
- case x
187
- when Numeric then x.floor
188
- when Larb::Vec3 then Larb::Vec3.new(x.x.floor, x.y.floor, x.z.floor)
189
- end
190
- end
191
-
192
- def ceil(x)
193
- case x
194
- when Numeric then x.ceil
195
- when Larb::Vec3 then Larb::Vec3.new(x.x.ceil, x.y.ceil, x.z.ceil)
196
- end
197
- end
198
-
199
- def pow(x, y)
200
- case x
201
- when Numeric then x**y
202
- when Larb::Vec3 then Larb::Vec3.new(x.x**y, x.y**y, x.z**y)
203
- end
204
- end
205
-
206
- def sqrt(x)
207
- case x
208
- when Numeric then Math.sqrt(x)
209
- when Larb::Vec3 then Larb::Vec3.new(Math.sqrt(x.x), Math.sqrt(x.y), Math.sqrt(x.z))
210
- end
211
- end
212
-
213
- def sin(x)
214
- Math.sin(x)
215
- end
216
-
217
- def cos(x)
218
- Math.cos(x)
219
- end
220
-
221
- def tan(x)
222
- Math.tan(x)
223
- end
224
-
225
- def asin(x)
226
- Math.asin(x)
227
- end
228
-
229
- def acos(x)
230
- Math.acos(x)
231
- end
232
-
233
- def atan(y, x = nil)
234
- x ? Math.atan2(y, x) : Math.atan(y)
235
- end
236
-
237
- def min(*args)
238
- args.flatten.min
239
- end
240
-
241
- def max(*args)
242
- args.flatten.max
243
- end
244
-
245
- def texture(tex, uv)
246
- tex.sample(uv.x, uv.y)
247
- end
248
-
249
- def texture_lod(tex, uv, lod)
250
- tex.sample(uv.x, uv.y, lod: lod)
251
- end
252
-
253
- def rgb(r, g, b)
254
- Larb::Color.rgb(r, g, b)
255
- end
256
-
257
- def rgba(r, g, b, a)
258
- Larb::Color.rgba(r, g, b, a)
259
- end
260
-
261
- def color_from_vec3(v)
262
- Larb::Color.from_vec3(v)
263
- end
264
-
265
- def color_from_vec4(v)
266
- Larb::Color.from_vec4(v)
267
- end
268
- end
269
-
270
- class VertexShader
271
- include ShaderBuiltins
272
-
273
- def initialize(&block)
274
- @process_block = block
275
- end
276
-
277
- def process(input, uniforms)
278
- output = ShaderIO.new
279
- @input = input
280
- @uniforms = uniforms
281
- @output = output
282
-
283
- instance_exec(input, uniforms, output, &@process_block)
284
-
285
- raise "VertexShader must set output.position" unless output[:position]
286
-
287
- output
288
- end
289
-
290
- attr_reader :input, :uniforms, :output
291
-
292
- def self.create(&block)
293
- new(&block)
294
- end
295
- end
296
-
297
- class FragmentShader
298
- include ShaderBuiltins
299
-
300
- def initialize(&block)
301
- @process_block = block
302
- end
303
-
304
- def process(input, uniforms)
305
- output = ShaderIO.new
306
- @input = input
307
- @uniforms = uniforms
308
- @output = output
309
-
310
- instance_exec(input, uniforms, output, &@process_block)
311
-
312
- output[:color] ||= Larb::Color.white
313
-
314
- output
315
- end
316
-
317
- attr_reader :input, :uniforms, :output
318
-
319
- def self.create(&block)
320
- new(&block)
321
- end
322
- end
9
+ private_constant :DynamicData
323
10
  end
324
11
  end
@@ -3,8 +3,11 @@
3
3
  module RBGL
4
4
  module Engine
5
5
  class Texture
6
- attr_reader :width, :height, :data
7
- attr_accessor :wrap_s, :wrap_t, :filter_min, :filter_mag
6
+ PPM_HEADER = /\A(P[36])(?:\s+|#[^\n]*\n)+(\d+)(?:\s+|#[^\n]*\n)+(\d+)(?:\s+|#[^\n]*\n)+(\d+)\s/m
7
+ WRAP_MODES = %i[repeat clamp mirror].freeze
8
+ FILTER_MODES = %i[nearest linear].freeze
9
+
10
+ attr_reader :width, :height, :data, :wrap_s, :wrap_t, :filter_min, :filter_mag
8
11
 
9
12
  WRAP_REPEAT = :repeat
10
13
  WRAP_CLAMP = :clamp
@@ -16,7 +19,9 @@ module RBGL
16
19
  def initialize(width, height, data = nil)
17
20
  @width = width
18
21
  @height = height
19
- @data = data || Array.new(width * height) { Larb::Color.black }
22
+ @data = data ? normalize_data!(data) : Array.new(width * height) { Larb::Color.black }
23
+ @levels = [@data]
24
+ @mipmaps_dirty = true
20
25
  @wrap_s = WRAP_REPEAT
21
26
  @wrap_t = WRAP_REPEAT
22
27
  @filter_min = FILTER_LINEAR
@@ -26,48 +31,54 @@ module RBGL
26
31
  def sample(u, v, lod: 0)
27
32
  u = wrap_coord(u, @wrap_s)
28
33
  v = wrap_coord(v, @wrap_t)
34
+ level = mip_level_for(lod)
35
+ level_width = width_for_level(level)
36
+ level_height = height_for_level(level)
29
37
 
30
- x = u * (@width - 1)
31
- y = v * (@height - 1)
38
+ x = u * (level_width - 1)
39
+ y = v * (level_height - 1)
32
40
 
33
- if @filter_mag == FILTER_NEAREST
34
- sample_nearest(x, y)
35
- else
36
- sample_bilinear(x, y)
37
- end
41
+ sample_with_filter(level, x, y, filter_for_lod(lod))
38
42
  end
39
43
 
40
44
  def get_pixel(x, y)
41
- x = x.clamp(0, @width - 1).to_i
42
- y = y.clamp(0, @height - 1).to_i
43
- @data[y * @width + x]
45
+ pixel_for_level(0, x, y)
44
46
  end
45
47
 
46
48
  def set_pixel(x, y, color)
47
49
  return if x < 0 || x >= @width || y < 0 || y >= @height
48
50
 
49
- @data[y.to_i * @width + x.to_i] = color
51
+ @data[y.to_i * @width + x.to_i] = validate_color!(color)
52
+ @mipmaps_dirty = true
53
+ end
54
+
55
+ def generate_mipmaps!
56
+ rebuild_mipmap_chain!
57
+ self
58
+ end
59
+
60
+ def wrap_s=(mode)
61
+ @wrap_s = validate_wrap_mode!(mode)
62
+ end
63
+
64
+ def wrap_t=(mode)
65
+ @wrap_t = validate_wrap_mode!(mode)
66
+ end
67
+
68
+ def filter_min=(filter)
69
+ @filter_min = validate_filter_mode!(filter)
70
+ end
71
+
72
+ def filter_mag=(filter)
73
+ @filter_mag = validate_filter_mode!(filter)
50
74
  end
51
75
 
52
76
  def self.from_ppm(filename)
53
- content = File.read(filename, mode: "rb")
54
- lines = content.lines.reject { |l| l.start_with?("#") }
55
-
56
- _format = lines.shift.strip
57
- dimensions = lines.shift.strip.split.map(&:to_i)
58
- width, height = dimensions
59
- max_val = lines.shift.strip.to_i
60
-
61
- data = []
62
- pixels = lines.join.split.map(&:to_i)
63
- (pixels.size / 3).times do |i|
64
- r = pixels[i * 3] / max_val.to_f
65
- g = pixels[i * 3 + 1] / max_val.to_f
66
- b = pixels[i * 3 + 2] / max_val.to_f
67
- data << Larb::Color.rgb(r, g, b)
68
- end
77
+ content = File.binread(filename)
78
+ format, width, height, max_val, body_offset = parse_ppm_header(content)
79
+ samples = parse_ppm_samples(content, format, width * height, max_val, body_offset)
69
80
 
70
- new(width, height, data)
81
+ new(width, height, colors_from_ppm_samples(samples, max_val))
71
82
  end
72
83
 
73
84
  def self.checker(width, height, size, color1 = Larb::Color.white, color2 = Larb::Color.black)
@@ -85,8 +96,81 @@ module RBGL
85
96
  new(width, height, Array.new(width * height) { color })
86
97
  end
87
98
 
99
+ def self.parse_ppm_header(content)
100
+ match = PPM_HEADER.match(content)
101
+ raise ArgumentError, "Invalid PPM header" unless match
102
+
103
+ format = match[1]
104
+ width = match[2].to_i
105
+ height = match[3].to_i
106
+ max_val = match[4].to_i
107
+ raise ArgumentError, "Unsupported PPM max value: #{max_val}" unless max_val.between?(1, 65_535)
108
+
109
+ [format, width, height, max_val, match.end(0)]
110
+ end
111
+
112
+ def self.parse_ppm_samples(content, format, pixel_count, max_val, body_offset)
113
+ case format
114
+ when "P3"
115
+ parse_p3_samples(content.byteslice(body_offset..), pixel_count)
116
+ when "P6"
117
+ parse_p6_samples(content, body_offset, pixel_count, max_val)
118
+ else
119
+ raise ArgumentError, "Unsupported PPM format: #{format}"
120
+ end
121
+ end
122
+
123
+ def self.parse_p3_samples(body, pixel_count)
124
+ samples = body.to_s.gsub(/#[^\n]*/, " ").scan(/\d+/).map(&:to_i)
125
+ expected_count = pixel_count * 3
126
+ raise ArgumentError, "PPM pixel data is truncated" if samples.length < expected_count
127
+
128
+ samples.first(expected_count)
129
+ end
130
+
131
+ def self.parse_p6_samples(content, body_offset, pixel_count, max_val)
132
+ sample_size = max_val < 256 ? 1 : 2
133
+ expected_bytes = pixel_count * 3 * sample_size
134
+ body = content.byteslice(body_offset, expected_bytes)
135
+ raise ArgumentError, "PPM pixel data is truncated" unless body && body.bytesize == expected_bytes
136
+
137
+ if sample_size == 1
138
+ body.bytes
139
+ else
140
+ body.unpack("n*")
141
+ end
142
+ end
143
+
144
+ def self.colors_from_ppm_samples(samples, max_val)
145
+ samples.each_slice(3).map do |r, g, b|
146
+ Larb::Color.rgb(r / max_val.to_f, g / max_val.to_f, b / max_val.to_f)
147
+ end
148
+ end
149
+
150
+ private_class_method :parse_ppm_header, :parse_ppm_samples, :parse_p3_samples,
151
+ :parse_p6_samples, :colors_from_ppm_samples
152
+
88
153
  private
89
154
 
155
+ def normalize_data!(data)
156
+ raise ArgumentError, "Texture data must be convertible to an Array" unless data.respond_to?(:to_a)
157
+
158
+ normalized = data.to_a
159
+ expected_size = @width * @height
160
+
161
+ unless normalized.size == expected_size
162
+ raise ArgumentError, "Texture data size mismatch: expected #{expected_size}, got #{normalized.size}"
163
+ end
164
+
165
+ normalized.map { |pixel| validate_color!(pixel) }
166
+ end
167
+
168
+ def validate_color!(color)
169
+ return color if color.is_a?(Larb::Color)
170
+
171
+ raise ArgumentError, "Texture data must contain only Larb::Color values"
172
+ end
173
+
90
174
  def wrap_coord(coord, mode)
91
175
  case mode
92
176
  when WRAP_REPEAT
@@ -99,11 +183,40 @@ module RBGL
99
183
  end
100
184
  end
101
185
 
102
- def sample_nearest(x, y)
103
- get_pixel(x.round, y.round)
186
+ def validate_wrap_mode!(mode)
187
+ normalized = mode.to_sym
188
+ return normalized if WRAP_MODES.include?(normalized)
189
+
190
+ raise ArgumentError, "Unsupported wrap mode: #{mode}"
191
+ end
192
+
193
+ def validate_filter_mode!(mode)
194
+ normalized = mode.to_sym
195
+ return normalized if FILTER_MODES.include?(normalized)
196
+
197
+ raise ArgumentError, "Unsupported filter mode: #{mode}"
198
+ end
199
+
200
+ def sample_nearest(level, x, y)
201
+ pixel_for_level(level, x.round, y.round)
202
+ end
203
+
204
+ def filter_for_lod(lod)
205
+ lod.to_f.positive? ? @filter_min : @filter_mag
104
206
  end
105
207
 
106
- def sample_bilinear(x, y)
208
+ def sample_with_filter(level, x, y, filter)
209
+ case filter
210
+ when FILTER_NEAREST
211
+ sample_nearest(level, x, y)
212
+ when FILTER_LINEAR
213
+ sample_bilinear(level, x, y)
214
+ else
215
+ raise ArgumentError, "Unsupported filter mode: #{filter}"
216
+ end
217
+ end
218
+
219
+ def sample_bilinear(level, x, y)
107
220
  x0 = x.floor.to_i
108
221
  y0 = y.floor.to_i
109
222
  x1 = x0 + 1
@@ -111,15 +224,91 @@ module RBGL
111
224
  fx = x - x0
112
225
  fy = y - y0
113
226
 
114
- c00 = get_pixel(x0, y0)
115
- c10 = get_pixel(x1, y0)
116
- c01 = get_pixel(x0, y1)
117
- c11 = get_pixel(x1, y1)
227
+ c00 = pixel_for_level(level, x0, y0)
228
+ c10 = pixel_for_level(level, x1, y0)
229
+ c01 = pixel_for_level(level, x0, y1)
230
+ c11 = pixel_for_level(level, x1, y1)
118
231
 
119
232
  c0 = c00.lerp(c10, fx)
120
233
  c1 = c01.lerp(c11, fx)
121
234
  c0.lerp(c1, fy)
122
235
  end
236
+
237
+ def mip_level_for(lod)
238
+ return 0 unless lod.to_f.positive?
239
+
240
+ rebuild_mipmap_chain! if @mipmaps_dirty
241
+ [lod.floor, @levels.size - 1].min
242
+ end
243
+
244
+ def pixel_for_level(level, x, y)
245
+ level_width = width_for_level(level)
246
+ level_height = height_for_level(level)
247
+ level_data = level.zero? ? @data : level_data(level)
248
+ clamped_x = x.clamp(0, level_width - 1).to_i
249
+ clamped_y = y.clamp(0, level_height - 1).to_i
250
+ level_data[clamped_y * level_width + clamped_x]
251
+ end
252
+
253
+ def level_data(level)
254
+ rebuild_mipmap_chain! if @mipmaps_dirty
255
+ @levels[level] || @data
256
+ end
257
+
258
+ def width_for_level(level)
259
+ [@width >> level, 1].max
260
+ end
261
+
262
+ def height_for_level(level)
263
+ [@height >> level, 1].max
264
+ end
265
+
266
+ def rebuild_mipmap_chain!
267
+ @levels = [@data]
268
+
269
+ current_data = @data
270
+ current_width = @width
271
+ current_height = @height
272
+
273
+ while current_width > 1 || current_height > 1
274
+ next_width = [current_width / 2, 1].max
275
+ next_height = [current_height / 2, 1].max
276
+ current_data = build_next_level(current_data, current_width, current_height, next_width, next_height)
277
+ @levels << current_data
278
+ current_width = next_width
279
+ current_height = next_height
280
+ end
281
+
282
+ @mipmaps_dirty = false
283
+ end
284
+
285
+ def build_next_level(source_data, source_width, source_height, target_width, target_height)
286
+ Array.new(target_width * target_height) do |index|
287
+ x = index % target_width
288
+ y = index / target_width
289
+ average_texel_block(source_data, source_width, source_height, x * 2, y * 2)
290
+ end
291
+ end
292
+
293
+ def average_texel_block(source_data, source_width, source_height, start_x, start_y)
294
+ samples = []
295
+
296
+ 2.times do |dy|
297
+ 2.times do |dx|
298
+ x = [start_x + dx, source_width - 1].min
299
+ y = [start_y + dy, source_height - 1].min
300
+ samples << source_data[y * source_width + x]
301
+ end
302
+ end
303
+
304
+ sample_count = samples.length.to_f
305
+ Larb::Color.new(
306
+ samples.sum(&:r) / sample_count,
307
+ samples.sum(&:g) / sample_count,
308
+ samples.sum(&:b) / sample_count,
309
+ samples.sum(&:a) / sample_count
310
+ )
311
+ end
123
312
  end
124
313
  end
125
314
  end
data/lib/rbgl/engine.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "engine/shader"
6
6
  require_relative "engine/texture"
7
7
  require_relative "engine/rasterizer"
8
8
  require_relative "engine/pipeline"
9
+ require_relative "engine/clip_space_clipper"
9
10
  require_relative "engine/context"
10
11
 
11
12
  module RBGL