rbgl 0.1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +21 -0
  4. data/README.md +123 -0
  5. data/Rakefile +12 -0
  6. data/examples/array_test.rb +99 -0
  7. data/examples/chemical_heartbeat.rb +166 -0
  8. data/examples/color_test.rb +61 -0
  9. data/examples/cube_spinning.rb +87 -0
  10. data/examples/dark_transit.rb +166 -0
  11. data/examples/fractured_orb.rb +428 -0
  12. data/examples/fractured_orb_rb.rb +598 -0
  13. data/examples/gradient.rb +84 -0
  14. data/examples/hexagonal_flow.rb +333 -0
  15. data/examples/multi_return_test.rb +98 -0
  16. data/examples/plasma.rb +78 -0
  17. data/examples/sphere_raymarch.rb +126 -0
  18. data/examples/teapot.rb +362 -0
  19. data/examples/teapot_mcu.rb +344 -0
  20. data/examples/triangle_basic.rb +36 -0
  21. data/examples/triangle_window.rb +62 -0
  22. data/examples/window_test.rb +36 -0
  23. data/lib/rbgl/engine/buffer.rb +160 -0
  24. data/lib/rbgl/engine/context.rb +157 -0
  25. data/lib/rbgl/engine/framebuffer.rb +115 -0
  26. data/lib/rbgl/engine/pipeline.rb +35 -0
  27. data/lib/rbgl/engine/rasterizer.rb +213 -0
  28. data/lib/rbgl/engine/shader.rb +324 -0
  29. data/lib/rbgl/engine/texture.rb +125 -0
  30. data/lib/rbgl/engine.rb +15 -0
  31. data/lib/rbgl/gui/backend.rb +76 -0
  32. data/lib/rbgl/gui/cocoa/backend.rb +121 -0
  33. data/lib/rbgl/gui/event.rb +34 -0
  34. data/lib/rbgl/gui/file_backend.rb +91 -0
  35. data/lib/rbgl/gui/wayland/backend.rb +126 -0
  36. data/lib/rbgl/gui/wayland/connection.rb +331 -0
  37. data/lib/rbgl/gui/window.rb +148 -0
  38. data/lib/rbgl/gui/x11/backend.rb +156 -0
  39. data/lib/rbgl/gui/x11/connection.rb +344 -0
  40. data/lib/rbgl/gui.rb +16 -0
  41. data/lib/rbgl/version.rb +5 -0
  42. data/lib/rbgl.rb +6 -0
  43. metadata +114 -0
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class Rasterizer
6
+ attr_accessor :viewport
7
+
8
+ def initialize(framebuffer)
9
+ @framebuffer = framebuffer
10
+ @viewport = { x: 0, y: 0, width: framebuffer.width, height: framebuffer.height }
11
+ end
12
+
13
+ def rasterize_triangle(v0, v1, v2, fragment_shader, uniforms, cull_mode: :none)
14
+ p0 = viewport_transform(v0[:position])
15
+ p1 = viewport_transform(v1[:position])
16
+ p2 = viewport_transform(v2[:position])
17
+
18
+ min_x = [p0.x, p1.x, p2.x].min.floor.clamp(0, @framebuffer.width - 1)
19
+ max_x = [p0.x, p1.x, p2.x].max.ceil.clamp(0, @framebuffer.width - 1)
20
+ min_y = [p0.y, p1.y, p2.y].min.floor.clamp(0, @framebuffer.height - 1)
21
+ max_y = [p0.y, p1.y, p2.y].max.ceil.clamp(0, @framebuffer.height - 1)
22
+
23
+ area = edge_function(p0, p1, p2)
24
+ return if area.abs < 1e-10
25
+
26
+ case cull_mode
27
+ when :back
28
+ return if area < 0
29
+ when :front
30
+ return if area > 0
31
+ end
32
+
33
+ (min_y..max_y).each do |y|
34
+ (min_x..max_x).each do |x|
35
+ px = x + 0.5
36
+ py = y + 0.5
37
+ p = Larb::Vec2.new(px, py)
38
+
39
+ w0 = edge_function(p1, p2, p)
40
+ w1 = edge_function(p2, p0, p)
41
+ w2 = edge_function(p0, p1, p)
42
+
43
+ if (w0 >= 0 && w1 >= 0 && w2 >= 0) || (w0 <= 0 && w1 <= 0 && w2 <= 0)
44
+ inv_area = 1.0 / area
45
+ w0 *= inv_area
46
+ w1 *= inv_area
47
+ w2 *= inv_area
48
+
49
+ depth = w0 * p0.z + w1 * p1.z + w2 * p2.z
50
+
51
+ interpolated = interpolate_attributes(v0, v1, v2, w0, w1, w2)
52
+
53
+ frag_output = fragment_shader.process(interpolated, uniforms)
54
+ color = frag_output[:color]
55
+
56
+ @framebuffer.write_pixel(x, y, color, depth)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def rasterize_line(v0, v1, fragment_shader, uniforms)
63
+ p0 = viewport_transform(v0[:position])
64
+ p1 = viewport_transform(v1[:position])
65
+
66
+ x0 = p0.x.round
67
+ y0 = p0.y.round
68
+ x1 = p1.x.round
69
+ y1 = p1.y.round
70
+
71
+ dx = (x1 - x0).abs
72
+ dy = -(y1 - y0).abs
73
+ sx = x0 < x1 ? 1 : -1
74
+ sy = y0 < y1 ? 1 : -1
75
+ err = dx + dy
76
+
77
+ total_dist = Math.sqrt((x1 - x0)**2 + (y1 - y0)**2)
78
+
79
+ loop do
80
+ current_dist = Math.sqrt((x0 - p0.x.round)**2 + (y0 - p0.y.round)**2)
81
+ t = total_dist > 0 ? current_dist / total_dist : 0
82
+
83
+ depth = p0.z + (p1.z - p0.z) * t
84
+
85
+ interpolated = interpolate_line_attributes(v0, v1, t)
86
+
87
+ frag_output = fragment_shader.process(interpolated, uniforms)
88
+ @framebuffer.write_pixel(x0, y0, frag_output[:color], depth)
89
+
90
+ break if x0 == x1 && y0 == y1
91
+
92
+ e2 = 2 * err
93
+ if e2 >= dy
94
+ err += dy
95
+ x0 += sx
96
+ end
97
+ if e2 <= dx
98
+ err += dx
99
+ y0 += sy
100
+ end
101
+ end
102
+ end
103
+
104
+ def rasterize_point(vertex, fragment_shader, uniforms, size: 1)
105
+ p = viewport_transform(vertex[:position])
106
+ x = p.x.round
107
+ y = p.y.round
108
+ depth = p.z
109
+
110
+ half = size / 2
111
+ (-half..half).each do |dy|
112
+ (-half..half).each do |dx|
113
+ frag_output = fragment_shader.process(vertex, uniforms)
114
+ @framebuffer.write_pixel(x + dx, y + dy, frag_output[:color], depth)
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def viewport_transform(position)
122
+ ndc = if position.is_a?(Larb::Vec4)
123
+ position.perspective_divide
124
+ else
125
+ position
126
+ end
127
+
128
+ Larb::Vec3.new(
129
+ (ndc.x + 1) * 0.5 * @viewport[:width] + @viewport[:x],
130
+ (1 - ndc.y) * 0.5 * @viewport[:height] + @viewport[:y],
131
+ (ndc.z + 1) * 0.5
132
+ )
133
+ end
134
+
135
+ def edge_function(a, b, c)
136
+ (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x)
137
+ end
138
+
139
+ def interpolate_attributes(v0, v1, v2, w0, w1, w2)
140
+ result = ShaderIO.new
141
+
142
+ all_keys = (v0.to_h.keys | v1.to_h.keys | v2.to_h.keys) - [:position]
143
+
144
+ all_keys.each do |key|
145
+ a0 = v0[key]
146
+ a1 = v1[key]
147
+ a2 = v2[key]
148
+ next unless a0 && a1 && a2
149
+
150
+ result[key] = interpolate_value(a0, a1, a2, w0, w1, w2)
151
+ end
152
+
153
+ result
154
+ end
155
+
156
+ def interpolate_value(a, b, c, w0, w1, w2)
157
+ case a
158
+ when Larb::Vec2
159
+ Larb::Vec2.new(
160
+ a.x * w0 + b.x * w1 + c.x * w2,
161
+ a.y * w0 + b.y * w1 + c.y * w2
162
+ )
163
+ when Larb::Vec3
164
+ Larb::Vec3.new(
165
+ a.x * w0 + b.x * w1 + c.x * w2,
166
+ a.y * w0 + b.y * w1 + c.y * w2,
167
+ a.z * w0 + b.z * w1 + c.z * w2
168
+ )
169
+ when Larb::Vec4
170
+ Larb::Vec4.new(
171
+ a.x * w0 + b.x * w1 + c.x * w2,
172
+ a.y * w0 + b.y * w1 + c.y * w2,
173
+ a.z * w0 + b.z * w1 + c.z * w2,
174
+ a.w * w0 + b.w * w1 + c.w * w2
175
+ )
176
+ when Larb::Color
177
+ Larb::Color.new(
178
+ a.r * w0 + b.r * w1 + c.r * w2,
179
+ a.g * w0 + b.g * w1 + c.g * w2,
180
+ a.b * w0 + b.b * w1 + c.b * w2,
181
+ a.a * w0 + b.a * w1 + c.a * w2
182
+ )
183
+ when Numeric
184
+ a * w0 + b * w1 + c * w2
185
+ else
186
+ a
187
+ end
188
+ end
189
+
190
+ def interpolate_line_attributes(v0, v1, t)
191
+ result = ShaderIO.new
192
+ all_keys = (v0.to_h.keys | v1.to_h.keys) - [:position]
193
+
194
+ all_keys.each do |key|
195
+ a0 = v0[key]
196
+ a1 = v1[key]
197
+ next unless a0 && a1
198
+
199
+ result[key] = case a0
200
+ when Larb::Vec2, Larb::Vec3, Larb::Vec4, Larb::Color
201
+ a0.lerp(a1, t)
202
+ when Numeric
203
+ a0 + (a1 - a0) * t
204
+ else
205
+ a0
206
+ end
207
+ end
208
+
209
+ result
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ 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
323
+ end
324
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module Engine
5
+ class Texture
6
+ attr_reader :width, :height, :data
7
+ attr_accessor :wrap_s, :wrap_t, :filter_min, :filter_mag
8
+
9
+ WRAP_REPEAT = :repeat
10
+ WRAP_CLAMP = :clamp
11
+ WRAP_MIRROR = :mirror
12
+
13
+ FILTER_NEAREST = :nearest
14
+ FILTER_LINEAR = :linear
15
+
16
+ def initialize(width, height, data = nil)
17
+ @width = width
18
+ @height = height
19
+ @data = data || Array.new(width * height) { Larb::Color.black }
20
+ @wrap_s = WRAP_REPEAT
21
+ @wrap_t = WRAP_REPEAT
22
+ @filter_min = FILTER_LINEAR
23
+ @filter_mag = FILTER_LINEAR
24
+ end
25
+
26
+ def sample(u, v, lod: 0)
27
+ u = wrap_coord(u, @wrap_s)
28
+ v = wrap_coord(v, @wrap_t)
29
+
30
+ x = u * (@width - 1)
31
+ y = v * (@height - 1)
32
+
33
+ if @filter_mag == FILTER_NEAREST
34
+ sample_nearest(x, y)
35
+ else
36
+ sample_bilinear(x, y)
37
+ end
38
+ end
39
+
40
+ 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]
44
+ end
45
+
46
+ def set_pixel(x, y, color)
47
+ return if x < 0 || x >= @width || y < 0 || y >= @height
48
+
49
+ @data[y.to_i * @width + x.to_i] = color
50
+ end
51
+
52
+ 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
69
+
70
+ new(width, height, data)
71
+ end
72
+
73
+ def self.checker(width, height, size, color1 = Larb::Color.white, color2 = Larb::Color.black)
74
+ data = Array.new(width * height)
75
+ height.times do |y|
76
+ width.times do |x|
77
+ checker = ((x / size) + (y / size)) % 2
78
+ data[y * width + x] = checker == 0 ? color1 : color2
79
+ end
80
+ end
81
+ new(width, height, data)
82
+ end
83
+
84
+ def self.solid(width, height, color)
85
+ new(width, height, Array.new(width * height) { color })
86
+ end
87
+
88
+ private
89
+
90
+ def wrap_coord(coord, mode)
91
+ case mode
92
+ when WRAP_REPEAT
93
+ coord - coord.floor
94
+ when WRAP_CLAMP
95
+ coord.clamp(0.0, 1.0)
96
+ when WRAP_MIRROR
97
+ t = coord - coord.floor
98
+ coord.floor.to_i.even? ? t : 1.0 - t
99
+ end
100
+ end
101
+
102
+ def sample_nearest(x, y)
103
+ get_pixel(x.round, y.round)
104
+ end
105
+
106
+ def sample_bilinear(x, y)
107
+ x0 = x.floor.to_i
108
+ y0 = y.floor.to_i
109
+ x1 = x0 + 1
110
+ y1 = y0 + 1
111
+ fx = x - x0
112
+ fy = y - y0
113
+
114
+ c00 = get_pixel(x0, y0)
115
+ c10 = get_pixel(x1, y0)
116
+ c01 = get_pixel(x0, y1)
117
+ c11 = get_pixel(x1, y1)
118
+
119
+ c0 = c00.lerp(c10, fx)
120
+ c1 = c01.lerp(c11, fx)
121
+ c0.lerp(c1, fy)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "engine/framebuffer"
4
+ require_relative "engine/buffer"
5
+ require_relative "engine/shader"
6
+ require_relative "engine/texture"
7
+ require_relative "engine/rasterizer"
8
+ require_relative "engine/pipeline"
9
+ require_relative "engine/context"
10
+
11
+ module RBGL
12
+ module Engine
13
+ VERSION = "0.1.0"
14
+ end
15
+ end