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/examples/cube_spinning.rb
CHANGED
|
@@ -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
|
|
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
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 &&
|
|
63
|
+
if e[:type] == :key_press && %i[escape q].include?(e[:key])
|
|
64
64
|
running = false
|
|
65
65
|
end
|
|
66
66
|
end
|
data/examples/sphere_raymarch.rb
CHANGED
|
@@ -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 &&
|
|
111
|
+
if e[:type] == :key_press && %i[escape q].include?(e[:key])
|
|
112
112
|
running = false
|
|
113
113
|
end
|
|
114
114
|
end
|
data/examples/triangle_window.rb
CHANGED
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
|
data/lib/rbgl/engine/buffer.rb
CHANGED
|
@@ -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 :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
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
|