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/shader.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/rbgl/engine/texture.rb
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
module RBGL
|
|
4
4
|
module Engine
|
|
5
5
|
class Texture
|
|
6
|
-
|
|
7
|
-
|
|
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, :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
|
|
22
|
+
@data = data ? normalize_data!(data) : Array.new(width * height, immutable_color(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
|
|
@@ -24,50 +29,60 @@ module RBGL
|
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def sample(u, v, lod: 0)
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
level = mip_level_for(lod)
|
|
33
|
+
level_width = width_for_level(level)
|
|
34
|
+
level_height = height_for_level(level)
|
|
35
|
+
filter = filter_for_lod(lod)
|
|
36
|
+
texel_offset = filter == FILTER_LINEAR ? -0.5 : 0.0
|
|
29
37
|
|
|
30
|
-
x = u *
|
|
31
|
-
y = v *
|
|
38
|
+
x = (u.to_f * level_width) + texel_offset
|
|
39
|
+
y = (v.to_f * level_height) + texel_offset
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
sample_with_filter(level, x, y, filter)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def data
|
|
45
|
+
@data.dup.freeze
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def get_pixel(x, y)
|
|
41
|
-
|
|
42
|
-
y = y.clamp(0, @height - 1).to_i
|
|
43
|
-
@data[y * @width + x]
|
|
49
|
+
pixel_for_level(0, x, y)
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
def set_pixel(x, y, color)
|
|
47
53
|
return if x < 0 || x >= @width || y < 0 || y >= @height
|
|
48
54
|
|
|
49
|
-
@data[y.to_i * @width + x.to_i] = color
|
|
55
|
+
@data[(y.to_i * @width) + x.to_i] = immutable_color(color)
|
|
56
|
+
@mipmaps_dirty = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def generate_mipmaps!
|
|
60
|
+
rebuild_mipmap_chain!
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def wrap_s=(mode)
|
|
65
|
+
@wrap_s = validate_wrap_mode!(mode)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def wrap_t=(mode)
|
|
69
|
+
@wrap_t = validate_wrap_mode!(mode)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def filter_min=(filter)
|
|
73
|
+
@filter_min = validate_filter_mode!(filter)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def filter_mag=(filter)
|
|
77
|
+
@filter_mag = validate_filter_mode!(filter)
|
|
50
78
|
end
|
|
51
79
|
|
|
52
80
|
def self.from_ppm(filename)
|
|
53
|
-
content = File.
|
|
54
|
-
|
|
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
|
|
81
|
+
content = File.binread(filename)
|
|
82
|
+
format, width, height, max_val, body_offset = parse_ppm_header(content)
|
|
83
|
+
samples = parse_ppm_samples(content, format, width * height, max_val, body_offset)
|
|
69
84
|
|
|
70
|
-
new(width, height,
|
|
85
|
+
new(width, height, colors_from_ppm_samples(samples, max_val))
|
|
71
86
|
end
|
|
72
87
|
|
|
73
88
|
def self.checker(width, height, size, color1 = Larb::Color.white, color2 = Larb::Color.black)
|
|
@@ -75,35 +90,137 @@ module RBGL
|
|
|
75
90
|
height.times do |y|
|
|
76
91
|
width.times do |x|
|
|
77
92
|
checker = ((x / size) + (y / size)) % 2
|
|
78
|
-
data[y * width + x] = checker == 0 ? color1 : color2
|
|
93
|
+
data[(y * width) + x] = checker == 0 ? color1 : color2
|
|
79
94
|
end
|
|
80
95
|
end
|
|
81
96
|
new(width, height, data)
|
|
82
97
|
end
|
|
83
98
|
|
|
84
99
|
def self.solid(width, height, color)
|
|
85
|
-
|
|
100
|
+
immutable = ImmutableColor.from(color)
|
|
101
|
+
new(width, height, Array.new(width * height, immutable))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.parse_ppm_header(content)
|
|
105
|
+
match = PPM_HEADER.match(content)
|
|
106
|
+
raise ArgumentError, "Invalid PPM header" unless match
|
|
107
|
+
|
|
108
|
+
format = match[1]
|
|
109
|
+
width = match[2].to_i
|
|
110
|
+
height = match[3].to_i
|
|
111
|
+
max_val = match[4].to_i
|
|
112
|
+
raise ArgumentError, "Unsupported PPM max value: #{max_val}" unless max_val.between?(1, 65_535)
|
|
113
|
+
|
|
114
|
+
[format, width, height, max_val, match.end(0)]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.parse_ppm_samples(content, format, pixel_count, max_val, body_offset)
|
|
118
|
+
case format
|
|
119
|
+
when "P3"
|
|
120
|
+
parse_p3_samples(content.byteslice(body_offset..), pixel_count, max_val)
|
|
121
|
+
when "P6"
|
|
122
|
+
parse_p6_samples(content, body_offset, pixel_count, max_val)
|
|
123
|
+
else
|
|
124
|
+
raise ArgumentError, "Unsupported PPM format: #{format}"
|
|
125
|
+
end
|
|
86
126
|
end
|
|
87
127
|
|
|
128
|
+
def self.parse_p3_samples(body, pixel_count, max_val)
|
|
129
|
+
tokens = body.to_s.gsub(/#[^\n]*/, " ").split
|
|
130
|
+
expected_count = pixel_count * 3
|
|
131
|
+
raise ArgumentError, "PPM pixel data is truncated" if tokens.length < expected_count
|
|
132
|
+
|
|
133
|
+
tokens.first(expected_count).map do |token|
|
|
134
|
+
raise ArgumentError, "Invalid PPM sample: #{token}" unless /\A\d+\z/.match?(token)
|
|
135
|
+
|
|
136
|
+
value = token.to_i
|
|
137
|
+
raise ArgumentError, "PPM sample exceeds max value: #{value}" if value > max_val
|
|
138
|
+
|
|
139
|
+
value
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.parse_p6_samples(content, body_offset, pixel_count, max_val)
|
|
144
|
+
sample_size = max_val < 256 ? 1 : 2
|
|
145
|
+
expected_bytes = pixel_count * 3 * sample_size
|
|
146
|
+
body = content.byteslice(body_offset, expected_bytes)
|
|
147
|
+
raise ArgumentError, "PPM pixel data is truncated" unless body && body.bytesize == expected_bytes
|
|
148
|
+
|
|
149
|
+
if sample_size == 1
|
|
150
|
+
body.bytes
|
|
151
|
+
else
|
|
152
|
+
body.unpack("n*")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.colors_from_ppm_samples(samples, max_val)
|
|
157
|
+
samples.each_slice(3).map do |r, g, b|
|
|
158
|
+
Larb::Color.rgb(r / max_val.to_f, g / max_val.to_f, b / max_val.to_f)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private_class_method :parse_ppm_header, :parse_ppm_samples, :parse_p3_samples,
|
|
163
|
+
:parse_p6_samples, :colors_from_ppm_samples
|
|
164
|
+
|
|
88
165
|
private
|
|
89
166
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
coord.floor.to_i.even? ? t : 1.0 - t
|
|
167
|
+
def normalize_data!(data)
|
|
168
|
+
raise ArgumentError, "Texture data must be convertible to an Array" unless data.respond_to?(:to_a)
|
|
169
|
+
|
|
170
|
+
normalized = data.to_a
|
|
171
|
+
expected_size = @width * @height
|
|
172
|
+
|
|
173
|
+
unless normalized.size == expected_size
|
|
174
|
+
raise ArgumentError, "Texture data size mismatch: expected #{expected_size}, got #{normalized.size}"
|
|
99
175
|
end
|
|
176
|
+
|
|
177
|
+
normalized.map { |pixel| immutable_color(pixel) }
|
|
100
178
|
end
|
|
101
179
|
|
|
102
|
-
def
|
|
103
|
-
|
|
180
|
+
def validate_color!(color)
|
|
181
|
+
return color if color.is_a?(Larb::Color)
|
|
182
|
+
|
|
183
|
+
raise ArgumentError, "Texture data must contain only Larb::Color values"
|
|
104
184
|
end
|
|
105
185
|
|
|
106
|
-
def
|
|
186
|
+
def immutable_color(color)
|
|
187
|
+
ImmutableColor.from(validate_color!(color))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def validate_wrap_mode!(mode)
|
|
191
|
+
normalized = mode.to_sym
|
|
192
|
+
return normalized if WRAP_MODES.include?(normalized)
|
|
193
|
+
|
|
194
|
+
raise ArgumentError, "Unsupported wrap mode: #{mode}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def validate_filter_mode!(mode)
|
|
198
|
+
normalized = mode.to_sym
|
|
199
|
+
return normalized if FILTER_MODES.include?(normalized)
|
|
200
|
+
|
|
201
|
+
raise ArgumentError, "Unsupported filter mode: #{mode}"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def sample_nearest(level, x, y)
|
|
205
|
+
pixel_for_level(level, x.floor, y.floor, wrap_s: @wrap_s, wrap_t: @wrap_t)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def filter_for_lod(lod)
|
|
209
|
+
lod.to_f.positive? ? @filter_min : @filter_mag
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def sample_with_filter(level, x, y, filter)
|
|
213
|
+
case filter
|
|
214
|
+
when FILTER_NEAREST
|
|
215
|
+
sample_nearest(level, x, y)
|
|
216
|
+
when FILTER_LINEAR
|
|
217
|
+
sample_bilinear(level, x, y)
|
|
218
|
+
else
|
|
219
|
+
raise ArgumentError, "Unsupported filter mode: #{filter}"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def sample_bilinear(level, x, y)
|
|
107
224
|
x0 = x.floor.to_i
|
|
108
225
|
y0 = y.floor.to_i
|
|
109
226
|
x1 = x0 + 1
|
|
@@ -111,15 +228,111 @@ module RBGL
|
|
|
111
228
|
fx = x - x0
|
|
112
229
|
fy = y - y0
|
|
113
230
|
|
|
114
|
-
c00 =
|
|
115
|
-
c10 =
|
|
116
|
-
c01 =
|
|
117
|
-
c11 =
|
|
231
|
+
c00 = pixel_for_level(level, x0, y0, wrap_s: @wrap_s, wrap_t: @wrap_t)
|
|
232
|
+
c10 = pixel_for_level(level, x1, y0, wrap_s: @wrap_s, wrap_t: @wrap_t)
|
|
233
|
+
c01 = pixel_for_level(level, x0, y1, wrap_s: @wrap_s, wrap_t: @wrap_t)
|
|
234
|
+
c11 = pixel_for_level(level, x1, y1, wrap_s: @wrap_s, wrap_t: @wrap_t)
|
|
118
235
|
|
|
119
236
|
c0 = c00.lerp(c10, fx)
|
|
120
237
|
c1 = c01.lerp(c11, fx)
|
|
121
238
|
c0.lerp(c1, fy)
|
|
122
239
|
end
|
|
240
|
+
|
|
241
|
+
def mip_level_for(lod)
|
|
242
|
+
return 0 unless lod.to_f.positive?
|
|
243
|
+
|
|
244
|
+
rebuild_mipmap_chain! if @mipmaps_dirty
|
|
245
|
+
[lod.floor, @levels.size - 1].min
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def pixel_for_level(level, x, y, wrap_s: WRAP_CLAMP, wrap_t: WRAP_CLAMP)
|
|
249
|
+
level_width = width_for_level(level)
|
|
250
|
+
level_height = height_for_level(level)
|
|
251
|
+
level_data = level.zero? ? @data : level_data(level)
|
|
252
|
+
sample_x = wrap_index(x.to_i, level_width, wrap_s)
|
|
253
|
+
sample_y = wrap_index(y.to_i, level_height, wrap_t)
|
|
254
|
+
level_data[(sample_y * level_width) + sample_x]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def wrap_index(index, size, mode)
|
|
258
|
+
case mode
|
|
259
|
+
when WRAP_REPEAT
|
|
260
|
+
index % size
|
|
261
|
+
when WRAP_CLAMP
|
|
262
|
+
index.clamp(0, size - 1)
|
|
263
|
+
when WRAP_MIRROR
|
|
264
|
+
mirrored = index % (size * 2)
|
|
265
|
+
mirrored < size ? mirrored : ((size * 2) - mirrored - 1)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def level_data(level)
|
|
270
|
+
rebuild_mipmap_chain! if @mipmaps_dirty
|
|
271
|
+
@levels[level] || @data
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def width_for_level(level)
|
|
275
|
+
[@width >> level, 1].max
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def height_for_level(level)
|
|
279
|
+
[@height >> level, 1].max
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def rebuild_mipmap_chain!
|
|
283
|
+
@levels = [@data]
|
|
284
|
+
|
|
285
|
+
current_data = @data
|
|
286
|
+
current_width = @width
|
|
287
|
+
current_height = @height
|
|
288
|
+
|
|
289
|
+
while current_width > 1 || current_height > 1
|
|
290
|
+
next_width = [current_width / 2, 1].max
|
|
291
|
+
next_height = [current_height / 2, 1].max
|
|
292
|
+
current_data = build_next_level(current_data, current_width, current_height, next_width, next_height)
|
|
293
|
+
@levels << current_data
|
|
294
|
+
current_width = next_width
|
|
295
|
+
current_height = next_height
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
@mipmaps_dirty = false
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def build_next_level(source_data, source_width, source_height, target_width, target_height)
|
|
302
|
+
Array.new(target_width * target_height) do |index|
|
|
303
|
+
x = index % target_width
|
|
304
|
+
y = index / target_width
|
|
305
|
+
start_x = x * source_width.fdiv(target_width)
|
|
306
|
+
end_x = x == target_width - 1 ? source_width.to_f : (x + 1) * source_width.fdiv(target_width)
|
|
307
|
+
start_y = y * source_height.fdiv(target_height)
|
|
308
|
+
end_y = y == target_height - 1 ? source_height.to_f : (y + 1) * source_height.fdiv(target_height)
|
|
309
|
+
average_texel_area(source_data, source_width, start_x, end_x, start_y, end_y)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def average_texel_area(source_data, source_width, start_x, end_x, start_y, end_y)
|
|
314
|
+
red = green = blue = alpha = total_weight = 0.0
|
|
315
|
+
|
|
316
|
+
(start_y.floor...end_y.ceil).each do |y|
|
|
317
|
+
y_weight = [end_y, y + 1].min - [start_y, y].max
|
|
318
|
+
(start_x.floor...end_x.ceil).each do |x|
|
|
319
|
+
weight = ([end_x, x + 1].min - [start_x, x].max) * y_weight
|
|
320
|
+
color = source_data[(y * source_width) + x]
|
|
321
|
+
red += color.r * weight
|
|
322
|
+
green += color.g * weight
|
|
323
|
+
blue += color.b * weight
|
|
324
|
+
alpha += color.a * weight
|
|
325
|
+
total_weight += weight
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
immutable_color(Larb::Color.new(
|
|
330
|
+
red / total_weight,
|
|
331
|
+
green / total_weight,
|
|
332
|
+
blue / total_weight,
|
|
333
|
+
alpha / total_weight
|
|
334
|
+
))
|
|
335
|
+
end
|
|
123
336
|
end
|
|
124
337
|
end
|
|
125
338
|
end
|