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
@@ -67,7 +67,7 @@ view = Mat4.look_at(
67
67
 
68
68
  window.on(:key_press) do |event|
69
69
  puts "Key pressed: #{event.key}"
70
- window.stop if event.key == 12 || event.key == "q" || event.key == "Escape"
70
+ window.stop if %i[escape q].include?(event.key)
71
71
  end
72
72
 
73
73
  puts "Press 'q' or Escape to quit"
data/examples/gradient.rb CHANGED
@@ -67,7 +67,7 @@ while running && !window.should_close?
67
67
  events.each do |e|
68
68
  case e[:type]
69
69
  when :key_press
70
- running = false if e[:key] == 12 || e[:key] == "q"
70
+ running = false if %i[escape q].include?(e[:key])
71
71
  end
72
72
  end
73
73
 
@@ -81,7 +81,7 @@ while running && !window.should_close?
81
81
  events.each do |e|
82
82
  case e[:type]
83
83
  when :key_press
84
- running = false if e[:key] == 12 || e[:key] == "q"
84
+ running = false if %i[escape q].include?(e[:key])
85
85
  end
86
86
  end
87
87
 
data/examples/plasma.rb CHANGED
@@ -60,7 +60,7 @@ while running && !window.should_close?
60
60
 
61
61
  events = window.poll_events_raw
62
62
  events.each do |e|
63
- if e[:type] == :key_press && (e[:key] == 12 || e[:key] == "q")
63
+ if e[:type] == :key_press && %i[escape q].include?(e[:key])
64
64
  running = false
65
65
  end
66
66
  end
@@ -108,7 +108,7 @@ while running && !window.should_close?
108
108
 
109
109
  events = window.poll_events_raw
110
110
  events.each do |e|
111
- if e[:type] == :key_press && (e[:key] == 12 || e[:key] == "q")
111
+ if e[:type] == :key_press && %i[escape q].include?(e[:key])
112
112
  running = false
113
113
  end
114
114
  end
@@ -42,7 +42,7 @@ time = 0.0
42
42
 
43
43
  window.on(:key_press) do |event|
44
44
  puts "Key: #{event.key}"
45
- window.stop if event.key == 12 || event.key == "q" || event.key == "Escape"
45
+ window.stop if %i[escape q].include?(event.key)
46
46
  end
47
47
 
48
48
  window.on(:mouse_move) do |event|
data/exe/rbgl ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "optparse"
6
+
7
+ lib_path = File.expand_path("../lib", __dir__)
8
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
9
+
10
+ require "rbgl"
11
+ require "rbgl/cli/doctor"
12
+
13
+ module RBGLCLI
14
+ def self.run(argv = ARGV, stdout: $stdout, stderr: $stderr)
15
+ options = { json: false }
16
+
17
+ parser = OptionParser.new do |parser|
18
+ parser.banner = "Usage: rbgl <command>\n\nCommands:\n doctor [--json]\n"
19
+ parser.on("--json", "Output JSON") { options[:json] = true }
20
+ parser.on("-h", "--help", "Show this help") do
21
+ stdout.puts parser
22
+ return 0
23
+ end
24
+ end
25
+
26
+ parser.parse!(argv)
27
+
28
+ command = argv.shift
29
+ unless command == "doctor"
30
+ stderr.puts parser
31
+ return 1
32
+ end
33
+
34
+ RBGL::CLI::Doctor.new(json: options[:json], io: stdout).run
35
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => error
36
+ stderr.puts error.message
37
+ stderr.puts parser
38
+ 1
39
+ end
40
+ end
41
+
42
+ exit RBGLCLI.run
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "tmpdir"
6
+
7
+ module RBGL
8
+ module CLI
9
+ class Doctor
10
+ class << self
11
+ def default_checks
12
+ {
13
+ file_backend: ->(doctor, env, platform) { doctor.__send__(:check_file_backend, env, platform) },
14
+ wayland: ->(doctor, env, platform) { doctor.__send__(:check_wayland, env, platform) },
15
+ x11: ->(doctor, env, platform) { doctor.__send__(:check_x11, env, platform) },
16
+ cocoa: ->(doctor, env, platform) { doctor.__send__(:check_cocoa, env, platform) },
17
+ rlsl: ->(doctor, env, platform) { doctor.__send__(:check_rlsl, env, platform) }
18
+ }
19
+ end
20
+ end
21
+
22
+ def initialize(json: false, io: $stdout, env: ENV, platform: RUBY_PLATFORM, ruby_version: RUBY_VERSION, checks: {})
23
+ @json = json
24
+ @io = io
25
+ @env = env
26
+ @platform = platform
27
+ @ruby_version = ruby_version
28
+ @checks = self.class.default_checks.merge(checks)
29
+ end
30
+
31
+ def run
32
+ report = collect
33
+
34
+ if @json
35
+ output_json(report)
36
+ else
37
+ output_text(report)
38
+ end
39
+
40
+ 0
41
+ end
42
+
43
+ def collect
44
+ {
45
+ rbgl_version: RBGL::VERSION,
46
+ ruby_version: @ruby_version,
47
+ platform: @platform,
48
+ file_backend: collect_component(:file_backend),
49
+ wayland: collect_component(:wayland),
50
+ x11: collect_component(:x11),
51
+ cocoa: collect_component(:cocoa),
52
+ rlsl: collect_component(:rlsl)
53
+ }
54
+ end
55
+
56
+ private
57
+
58
+ def collect_component(key)
59
+ @checks.fetch(key).call(self, @env, @platform)
60
+ rescue StandardError => e
61
+ { available: false, error: "#{e.class}: #{e.message}" }
62
+ end
63
+
64
+ def output_text(report)
65
+ @io.puts("RBGL version: #{report[:rbgl_version]}")
66
+ @io.puts("Ruby version: #{report[:ruby_version]}")
67
+ @io.puts("RUBY_PLATFORM: #{report[:platform]}")
68
+ @io.puts
69
+ dump_section("File backend", report[:file_backend])
70
+ dump_section("Wayland", report[:wayland])
71
+ dump_section("X11", report[:x11])
72
+ dump_section("Cocoa", report[:cocoa])
73
+ dump_section("RLSL", report[:rlsl])
74
+ end
75
+
76
+ def output_json(report)
77
+ @io.puts(JSON.pretty_generate(report))
78
+ end
79
+
80
+ def dump_section(name, values)
81
+ @io.puts("#{name}:")
82
+ values.each do |key, value|
83
+ @io.puts(" #{key}: #{format_value(value)}")
84
+ end
85
+ @io.puts
86
+ end
87
+
88
+ def format_value(value)
89
+ return value.is_a?(TrueClass) || value.is_a?(FalseClass) ? (value ? "yes" : "no") : value.inspect
90
+ end
91
+
92
+ def check_file_backend(_env, _platform)
93
+ output_dir = Dir.mktmpdir("rbgl-doctor")
94
+ backend = RBGL::GUI::FileBackend.new(1, 1, "rbgl-doctor", output_dir: output_dir)
95
+ backend.present(RBGL::Engine::Framebuffer.new(1, 1))
96
+
97
+ { available: true, output_dir: output_dir }
98
+ ensure
99
+ backend&.close
100
+ FileUtils.rm_rf(output_dir) if output_dir
101
+ end
102
+
103
+ def check_wayland(env, _platform)
104
+ require_ok, require_error = safe_require("rbgl/gui/wayland/connection")
105
+ {
106
+ display_env: env["WAYLAND_DISPLAY"],
107
+ display_available: !env["WAYLAND_DISPLAY"].nil?,
108
+ require_ok: require_ok,
109
+ require_error: require_error
110
+ }
111
+ end
112
+
113
+ def check_x11(env, _platform)
114
+ require_ok, require_error = safe_require("rbgl/gui/x11/connection")
115
+ {
116
+ display_env: env["DISPLAY"],
117
+ display_available: !env["DISPLAY"].nil?,
118
+ require_ok: require_ok,
119
+ require_error: require_error
120
+ }
121
+ end
122
+
123
+ def check_cocoa(_env, platform)
124
+ darwin = platform.include?("darwin")
125
+ return { platform: platform, supported: false } unless darwin
126
+
127
+ require_ok, require_error = safe_require("metaco")
128
+ {
129
+ platform: platform,
130
+ supported: true,
131
+ require_ok: require_ok,
132
+ require_error: require_error
133
+ }
134
+ end
135
+
136
+ def check_rlsl(_env, _platform)
137
+ begin
138
+ require "rlsl"
139
+ version = Gem.loaded_specs["rlsl"]&.version&.to_s
140
+
141
+ { require_ok: true, version: version }
142
+ rescue LoadError => e
143
+ { require_ok: false, error: "#{e.class}: #{e.message}" }
144
+ end
145
+ end
146
+
147
+ def safe_require(file)
148
+ require file
149
+ [true, nil]
150
+ rescue LoadError => e
151
+ [false, "#{e.class}: #{e.message}"]
152
+ end
153
+ end
154
+ end
155
+ end
@@ -3,28 +3,132 @@
3
3
  module RBGL
4
4
  module Engine
5
5
  class VertexAttribute
6
- attr_reader :name, :size, :offset
6
+ attr_reader :name, :size, :offset, :kind
7
+
8
+ def initialize(name, size, offset = 0, kind: nil)
9
+ unless size.is_a?(Integer) && size.positive?
10
+ raise ArgumentError, "Vertex attribute size must be a positive Integer"
11
+ end
7
12
 
8
- def initialize(name, size, offset = 0)
9
13
  @name = name.to_sym
10
14
  @size = size
11
15
  @offset = offset
16
+ @kind = (kind || infer_kind).to_sym
17
+ validate_kind!
18
+ end
19
+
20
+ def serialize(value)
21
+ values = extract_values(value)
22
+ validate_value_size!(values)
23
+ values.first(@size).map { |component| Float(component) }
24
+ rescue ArgumentError, TypeError => e
25
+ raise ArgumentError, "Invalid value for attribute #{name}: #{e.message}"
26
+ end
27
+
28
+ def deserialize(values)
29
+ case @kind
30
+ when :scalar
31
+ values[0]
32
+ when :vec2
33
+ Larb::Vec2.new(*values)
34
+ when :vec3
35
+ Larb::Vec3.new(*values)
36
+ when :vec4
37
+ Larb::Vec4.new(*values)
38
+ when :color
39
+ Larb::Color.new(*values)
40
+ when :array
41
+ values.dup
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def infer_kind
48
+ return :color if @name == :color && @size == 4
49
+
50
+ case @size
51
+ when 1 then :scalar
52
+ when 2 then :vec2
53
+ when 3 then :vec3
54
+ when 4 then :vec4
55
+ else :array
56
+ end
57
+ end
58
+
59
+ def validate_kind!
60
+ expected_sizes = {
61
+ scalar: [1],
62
+ vec2: [2],
63
+ vec3: [3],
64
+ vec4: [4],
65
+ color: [4],
66
+ array: nil
67
+ }.fetch(@kind) do
68
+ raise ArgumentError, "Unsupported attribute kind: #{@kind}"
69
+ end
70
+
71
+ return if expected_sizes.nil? || expected_sizes.include?(@size)
72
+
73
+ raise ArgumentError, "Attribute #{name} kind #{@kind} requires size #{expected_sizes.join(' or ')}"
74
+ end
75
+
76
+ def extract_values(value)
77
+ case value
78
+ when Larb::Vec2
79
+ [value.x, value.y]
80
+ when Larb::Vec3
81
+ [value.x, value.y, value.z]
82
+ when Larb::Vec4
83
+ [value.x, value.y, value.z, value.w]
84
+ when Larb::Color
85
+ value.to_a
86
+ when Array
87
+ value
88
+ when Numeric
89
+ [value]
90
+ else
91
+ raise ArgumentError, "Unsupported attribute type #{value.class}"
92
+ end
93
+ end
94
+
95
+ def validate_value_size!(values)
96
+ return if values.size >= @size
97
+
98
+ raise ArgumentError, "Expected at least #{@size} components, got #{values.size}"
12
99
  end
13
100
  end
14
101
 
15
102
  class VertexLayout
16
- attr_reader :attributes, :stride
103
+ attr_reader :stride
17
104
 
18
105
  def initialize(&block)
19
106
  @attributes = {}
20
107
  @offset = 0
108
+ @stride = 0
109
+ @finalized = false
21
110
  instance_eval(&block) if block_given?
22
- @stride = @offset
23
111
  end
24
112
 
25
- def attribute(name, size)
26
- @attributes[name.to_sym] = VertexAttribute.new(name, size, @offset)
113
+ def attribute(name, size, kind: nil)
114
+ raise ArgumentError, "Vertex layout is finalized" if @finalized
115
+
116
+ key = name.to_sym
117
+ raise ArgumentError, "Duplicate vertex attribute: #{key}" if @attributes.key?(key)
118
+
119
+ @attributes[key] = VertexAttribute.new(key, size, @offset, kind: kind)
27
120
  @offset += size
121
+ @stride = @offset
122
+ end
123
+
124
+ def attributes
125
+ @finalized ? @attributes : @attributes.dup.freeze
126
+ end
127
+
128
+ def finalize!
129
+ @finalized = true
130
+ @attributes.freeze
131
+ self
28
132
  end
29
133
 
30
134
  def self.position_only
@@ -60,31 +164,23 @@ module RBGL
60
164
  attr_reader :data, :layout, :vertex_count
61
165
 
62
166
  def initialize(layout)
63
- @layout = layout
167
+ unless layout.is_a?(VertexLayout)
168
+ raise ArgumentError, "VertexBuffer layout must be an RBGL::Engine::VertexLayout"
169
+ end
170
+
171
+ @layout = layout.finalize!
64
172
  @data = []
65
173
  @vertex_count = 0
66
174
  end
67
175
 
68
176
  def add_vertex(**attributes)
177
+ validate_attribute_keys!(attributes)
178
+
69
179
  vertex_data = []
70
180
  @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
181
+ raise ArgumentError, "Missing attribute: #{name}" unless attributes.key?(name)
182
+
183
+ vertex_data.concat(attr.serialize(attributes[name]))
88
184
  end
89
185
  @data.concat(vertex_data)
90
186
  @vertex_count += 1
@@ -105,18 +201,7 @@ module RBGL
105
201
  @layout.attributes.each do |name, attr|
106
202
  offset = start + attr.offset
107
203
  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
204
+ vertex[name] = attr.deserialize(values)
120
205
  end
121
206
 
122
207
  vertex
@@ -127,17 +212,28 @@ module RBGL
127
212
  data.each { |vertex| buffer.add_vertex(**vertex) }
128
213
  buffer
129
214
  end
215
+
216
+ private
217
+
218
+ def validate_attribute_keys!(attributes)
219
+ unknown_attributes = attributes.keys.map(&:to_sym) - @layout.attributes.keys
220
+ return if unknown_attributes.empty?
221
+
222
+ raise ArgumentError, "Unknown attributes: #{unknown_attributes.join(', ')}"
223
+ end
130
224
  end
131
225
 
132
226
  class IndexBuffer
133
- attr_reader :indices
134
-
135
227
  def initialize(indices = [])
136
- @indices = indices.map(&:to_i)
228
+ @indices = validate_indices!(indices)
229
+ end
230
+
231
+ def indices
232
+ @indices.dup.freeze
137
233
  end
138
234
 
139
235
  def add(*idx)
140
- @indices.concat(idx.flatten.map(&:to_i))
236
+ @indices.concat(validate_indices!(idx.flatten))
141
237
  self
142
238
  end
143
239
 
@@ -146,6 +242,8 @@ module RBGL
146
242
  end
147
243
 
148
244
  def get_triangle(index)
245
+ return nil unless index.is_a?(Integer) && index >= 0
246
+
149
247
  start = index * 3
150
248
  @indices[start, 3]
151
249
  end
@@ -155,6 +253,18 @@ module RBGL
155
253
 
156
254
  (0...triangle_count).each { |i| yield get_triangle(i) }
157
255
  end
256
+
257
+ private
258
+
259
+ def validate_indices!(indices)
260
+ indices.map do |index|
261
+ unless index.is_a?(Integer) && index >= 0
262
+ raise ArgumentError, "Index must be a non-negative Integer: #{index.inspect}"
263
+ end
264
+
265
+ index
266
+ end
267
+ end
158
268
  end
159
269
  end
160
270
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class ClipSpaceClipper
6
+ CLIP_PLANES = [
7
+ ->(position) { position.x + position.w },
8
+ ->(position) { position.w - position.x },
9
+ ->(position) { position.y + position.w },
10
+ ->(position) { position.w - position.y },
11
+ ->(position) { position.z + position.w },
12
+ ->(position) { position.w - position.z }
13
+ ].freeze
14
+
15
+ def clip(v0, v1, v2)
16
+ polygon = [v0, v1, v2].map { |vertex| normalize_vertex(vertex) }
17
+ outcodes = polygon.map { |vertex| outcode(vertex) }
18
+ return [polygon] if outcodes.reduce(:|).zero?
19
+ return [] unless outcodes.reduce(:&).zero?
20
+
21
+ CLIP_PLANES.each do |plane|
22
+ polygon = clip_against_plane(polygon, plane)
23
+ return [] if polygon.empty?
24
+ end
25
+
26
+ triangulate(polygon)
27
+ end
28
+
29
+ def clip_line(v0, v1)
30
+ segment = [duplicate_vertex(v0), duplicate_vertex(v1)]
31
+
32
+ CLIP_PLANES.each do |plane|
33
+ segment = clip_segment_against_plane(segment, plane)
34
+ return nil unless segment
35
+ end
36
+
37
+ segment
38
+ end
39
+
40
+ def visible_point?(vertex)
41
+ CLIP_PLANES.all? { |plane| signed_distance(vertex, plane) >= 0 }
42
+ end
43
+
44
+ def clip_point(vertex)
45
+ point = normalize_vertex(vertex)
46
+ visible_point?(point) ? point : nil
47
+ end
48
+
49
+ private
50
+
51
+ def clip_against_plane(vertices, plane)
52
+ clipped = []
53
+ return clipped if vertices.empty?
54
+
55
+ previous_vertex = vertices.last
56
+ previous_distance = signed_distance(previous_vertex, plane)
57
+
58
+ vertices.each do |current_vertex|
59
+ current_distance = signed_distance(current_vertex, plane)
60
+ current_inside = current_distance >= 0
61
+ previous_inside = previous_distance >= 0
62
+
63
+ if current_inside != previous_inside
64
+ clipped << interpolate_vertex(previous_vertex, current_vertex, previous_distance, current_distance)
65
+ end
66
+
67
+ clipped << duplicate_vertex(current_vertex) if current_inside
68
+
69
+ previous_vertex = current_vertex
70
+ previous_distance = current_distance
71
+ end
72
+
73
+ clipped
74
+ end
75
+
76
+ def triangulate(polygon)
77
+ return [] if polygon.size < 3
78
+
79
+ polygon[1..].each_cons(2).map do |v1, v2|
80
+ [duplicate_vertex(polygon.first), duplicate_vertex(v1), duplicate_vertex(v2)]
81
+ end
82
+ end
83
+
84
+ def clip_segment_against_plane(segment, plane)
85
+ start_vertex, finish_vertex = segment
86
+ start_distance = signed_distance(start_vertex, plane)
87
+ finish_distance = signed_distance(finish_vertex, plane)
88
+ start_inside = start_distance >= 0
89
+ finish_inside = finish_distance >= 0
90
+
91
+ return [start_vertex, finish_vertex] if start_inside && finish_inside
92
+ return nil unless start_inside || finish_inside
93
+
94
+ intersection = interpolate_vertex(start_vertex, finish_vertex, start_distance, finish_distance)
95
+ start_inside ? [start_vertex, intersection] : [intersection, finish_vertex]
96
+ end
97
+
98
+ def interpolate_vertex(start_vertex, finish_vertex, start_distance, finish_distance)
99
+ denominator = start_distance - finish_distance
100
+ t = denominator.zero? ? 0.0 : start_distance / denominator
101
+ result = ShaderIO.new
102
+
103
+ interpolated_keys(start_vertex, finish_vertex).each do |key|
104
+ start_value = start_vertex[key]
105
+ finish_value = finish_vertex[key]
106
+ result[key] = interpolate_value(start_value, finish_value, t)
107
+ end
108
+
109
+ result
110
+ end
111
+
112
+ def duplicate_vertex(vertex)
113
+ ShaderIO.new(vertex.to_h).tap do |copy|
114
+ copy[:position] = clip_position(vertex) if vertex[:position].is_a?(Larb::Vec2)
115
+ end
116
+ end
117
+
118
+ def normalize_vertex(vertex)
119
+ vertex[:position].is_a?(Larb::Vec2) ? duplicate_vertex(vertex) : vertex
120
+ end
121
+
122
+ def interpolated_keys(start_vertex, finish_vertex)
123
+ (start_vertex.to_h.keys + finish_vertex.to_h.keys).uniq
124
+ end
125
+
126
+ def interpolate_value(start_value, finish_value, t)
127
+ return finish_value if start_value.nil?
128
+ return start_value if finish_value.nil?
129
+
130
+ case start_value
131
+ when Numeric
132
+ start_value + ((finish_value - start_value) * t)
133
+ when Larb::Vec2, Larb::Vec3, Larb::Vec4, Larb::Color
134
+ start_value.lerp(finish_value, t)
135
+ else
136
+ t < 0.5 ? start_value : finish_value
137
+ end
138
+ end
139
+
140
+ def signed_distance(vertex, plane)
141
+ plane.call(clip_position(vertex))
142
+ end
143
+
144
+ def outcode(vertex)
145
+ CLIP_PLANES.each_with_index.reduce(0) do |code, (plane, index)|
146
+ signed_distance(vertex, plane).negative? ? code | (1 << index) : code
147
+ end
148
+ end
149
+
150
+ def clip_position(vertex)
151
+ position = vertex[:position]
152
+
153
+ case position
154
+ when Larb::Vec4 then position
155
+ when Larb::Vec3 then Larb::Vec4.new(position.x, position.y, position.z, 1.0)
156
+ when Larb::Vec2 then Larb::Vec4.new(position.x, position.y, 0.0, 1.0)
157
+ else
158
+ raise ArgumentError, "Unsupported clip position type: #{position.class}"
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end