3rb 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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/3rb.gemspec +29 -0
  4. data/CHANGELOG.md +12 -0
  5. data/LICENSE +21 -0
  6. data/README.md +321 -0
  7. data/Rakefile +13 -0
  8. data/examples/01_hello_cube.rb +29 -0
  9. data/examples/02_basic_geometries.rb +56 -0
  10. data/examples/03_materials.rb +61 -0
  11. data/examples/04_lighting.rb +63 -0
  12. data/examples/05_animation.rb +79 -0
  13. data/examples/06_custom_shader.rb +92 -0
  14. data/examples/07_scene_graph.rb +74 -0
  15. data/examples/08_orbit_controls.rb +50 -0
  16. data/examples/09_3d_chart.rb +71 -0
  17. data/examples/10_procedural_terrain.rb +140 -0
  18. data/examples/11_particle_system.rb +68 -0
  19. data/examples/12_model_loader.rb +73 -0
  20. data/examples/13_game_prototype.rb +145 -0
  21. data/examples/14_utah_teapot.rb +291 -0
  22. data/examples/15_stanford_bunny.rb +200 -0
  23. data/examples/16_cornell_box.rb +373 -0
  24. data/examples/17_weird_fractal4.rb +130 -0
  25. data/examples/18_platonic_solids.rb +268 -0
  26. data/lib/3rb/animation/animation_clip.rb +287 -0
  27. data/lib/3rb/animation/animation_mixer.rb +366 -0
  28. data/lib/3rb/cameras/camera.rb +50 -0
  29. data/lib/3rb/cameras/orthographic_camera.rb +92 -0
  30. data/lib/3rb/cameras/perspective_camera.rb +103 -0
  31. data/lib/3rb/controls/orbit_controls.rb +341 -0
  32. data/lib/3rb/core/buffer_attribute.rb +172 -0
  33. data/lib/3rb/core/group.rb +9 -0
  34. data/lib/3rb/core/object3d.rb +298 -0
  35. data/lib/3rb/core/scene.rb +78 -0
  36. data/lib/3rb/dsl/helpers.rb +57 -0
  37. data/lib/3rb/dsl/scene_builder.rb +288 -0
  38. data/lib/3rb/ffi/glfw.rb +61 -0
  39. data/lib/3rb/ffi/opengl.rb +137 -0
  40. data/lib/3rb/ffi/platform.rb +65 -0
  41. data/lib/3rb/geometries/box_geometry.rb +101 -0
  42. data/lib/3rb/geometries/buffer_geometry.rb +345 -0
  43. data/lib/3rb/geometries/cone_geometry.rb +29 -0
  44. data/lib/3rb/geometries/cylinder_geometry.rb +149 -0
  45. data/lib/3rb/geometries/plane_geometry.rb +75 -0
  46. data/lib/3rb/geometries/sphere_geometry.rb +93 -0
  47. data/lib/3rb/geometries/torus_geometry.rb +77 -0
  48. data/lib/3rb/lights/ambient_light.rb +9 -0
  49. data/lib/3rb/lights/directional_light.rb +57 -0
  50. data/lib/3rb/lights/hemisphere_light.rb +26 -0
  51. data/lib/3rb/lights/light.rb +27 -0
  52. data/lib/3rb/lights/point_light.rb +68 -0
  53. data/lib/3rb/lights/rect_area_light.rb +35 -0
  54. data/lib/3rb/lights/spot_light.rb +88 -0
  55. data/lib/3rb/loaders/gltf_loader.rb +304 -0
  56. data/lib/3rb/loaders/loader.rb +94 -0
  57. data/lib/3rb/loaders/obj_loader.rb +186 -0
  58. data/lib/3rb/loaders/texture_loader.rb +55 -0
  59. data/lib/3rb/materials/basic_material.rb +70 -0
  60. data/lib/3rb/materials/lambert_material.rb +102 -0
  61. data/lib/3rb/materials/material.rb +114 -0
  62. data/lib/3rb/materials/phong_material.rb +106 -0
  63. data/lib/3rb/materials/shader_material.rb +104 -0
  64. data/lib/3rb/materials/standard_material.rb +106 -0
  65. data/lib/3rb/math/color.rb +246 -0
  66. data/lib/3rb/math/euler.rb +156 -0
  67. data/lib/3rb/math/math_utils.rb +132 -0
  68. data/lib/3rb/math/matrix3.rb +269 -0
  69. data/lib/3rb/math/matrix4.rb +501 -0
  70. data/lib/3rb/math/quaternion.rb +337 -0
  71. data/lib/3rb/math/vector2.rb +216 -0
  72. data/lib/3rb/math/vector3.rb +366 -0
  73. data/lib/3rb/math/vector4.rb +233 -0
  74. data/lib/3rb/native/gl.rb +382 -0
  75. data/lib/3rb/native/native.rb +55 -0
  76. data/lib/3rb/native/window.rb +111 -0
  77. data/lib/3rb/native.rb +9 -0
  78. data/lib/3rb/objects/line.rb +116 -0
  79. data/lib/3rb/objects/mesh.rb +40 -0
  80. data/lib/3rb/objects/points.rb +71 -0
  81. data/lib/3rb/renderers/opengl_renderer.rb +567 -0
  82. data/lib/3rb/renderers/renderer.rb +60 -0
  83. data/lib/3rb/renderers/shader_lib.rb +100 -0
  84. data/lib/3rb/textures/cube_texture.rb +26 -0
  85. data/lib/3rb/textures/data_texture.rb +35 -0
  86. data/lib/3rb/textures/render_target.rb +125 -0
  87. data/lib/3rb/textures/texture.rb +190 -0
  88. data/lib/3rb/version.rb +5 -0
  89. data/lib/3rb.rb +86 -0
  90. data/shaders/basic.frag +19 -0
  91. data/shaders/basic.vert +15 -0
  92. data/shaders/common/lights.glsl +53 -0
  93. data/shaders/common/uniforms.glsl +9 -0
  94. data/shaders/lambert.frag +37 -0
  95. data/shaders/lambert.vert +22 -0
  96. data/shaders/phong.frag +51 -0
  97. data/shaders/phong.vert +28 -0
  98. data/shaders/standard.frag +92 -0
  99. data/shaders/standard.vert +28 -0
  100. metadata +155 -0
@@ -0,0 +1,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ class AnimationMixer
5
+ attr_reader :root, :time, :time_scale
6
+ attr_accessor :actions
7
+
8
+ def initialize(root)
9
+ @root = root
10
+ @actions = []
11
+ @time = 0
12
+ @time_scale = 1
13
+
14
+ @bindings = {}
15
+ @active_actions = []
16
+ end
17
+
18
+ def clip_action(clip, root: nil, blend_mode: nil)
19
+ target_root = root || @root
20
+
21
+ action = @actions.find { |a| a.clip == clip && a.root == target_root }
22
+ return action if action
23
+
24
+ action = AnimationAction.new(self, clip, target_root, blend_mode)
25
+ @actions << action
26
+ action
27
+ end
28
+
29
+ def existing_action(clip, root: nil)
30
+ target_root = root || @root
31
+ @actions.find { |a| a.clip == clip && a.root == target_root }
32
+ end
33
+
34
+ def stop_all_action
35
+ @actions.each(&:stop)
36
+ @active_actions.clear
37
+ end
38
+
39
+ def update(delta_time)
40
+ scaled_delta = delta_time * @time_scale
41
+ @time += scaled_delta
42
+
43
+ @active_actions.each do |action|
44
+ action.update(scaled_delta)
45
+ end
46
+ end
47
+
48
+ def set_time(time)
49
+ @time = time
50
+
51
+ @active_actions.each do |action|
52
+ action.time = time
53
+ action.apply
54
+ end
55
+ end
56
+
57
+ def get_root
58
+ @root
59
+ end
60
+
61
+ def uncache_clip(clip)
62
+ @actions.reject! { |a| a.clip == clip }
63
+ end
64
+
65
+ def uncache_root(root)
66
+ @actions.reject! { |a| a.root == root }
67
+ end
68
+
69
+ def uncache_action(clip, root: nil)
70
+ target_root = root || @root
71
+ @actions.reject! { |a| a.clip == clip && a.root == target_root }
72
+ end
73
+
74
+ def activate_action(action)
75
+ @active_actions << action unless @active_actions.include?(action)
76
+ end
77
+
78
+ def deactivate_action(action)
79
+ @active_actions.delete(action)
80
+ end
81
+
82
+ def get_binding(track_name, root)
83
+ key = "#{root.object_id}_#{track_name}"
84
+ @bindings[key] ||= PropertyBinding.new(root, track_name)
85
+ end
86
+ end
87
+
88
+ class AnimationAction
89
+ attr_accessor :loop, :repetitions, :clamp_when_finished
90
+ attr_accessor :paused, :enabled
91
+ attr_accessor :time, :time_scale, :weight
92
+ attr_accessor :zero_slope_at_start, :zero_slope_at_end
93
+
94
+ attr_reader :mixer, :clip, :root
95
+
96
+ LOOP_MODE = {
97
+ once: 2200,
98
+ repeat: 2201,
99
+ ping_pong: 2202
100
+ }.freeze
101
+
102
+ def initialize(mixer, clip, root = nil, blend_mode = nil)
103
+ @mixer = mixer
104
+ @clip = clip
105
+ @root = root || mixer.root
106
+ @blend_mode = blend_mode || clip.blend_mode
107
+
108
+ @loop = :repeat
109
+ @repetitions = Float::INFINITY
110
+
111
+ @paused = false
112
+ @enabled = true
113
+
114
+ @time = 0
115
+ @time_scale = 1
116
+ @weight = 1
117
+
118
+ @clamp_when_finished = false
119
+ @zero_slope_at_start = true
120
+ @zero_slope_at_end = true
121
+
122
+ @loop_count = -1
123
+ @start_time = nil
124
+
125
+ @bindings = clip.tracks.map do |track|
126
+ mixer.get_binding(track.name, @root)
127
+ end
128
+ end
129
+
130
+ def play
131
+ @mixer.activate_action(self)
132
+ self
133
+ end
134
+
135
+ def stop
136
+ @mixer.deactivate_action(self)
137
+ reset
138
+ self
139
+ end
140
+
141
+ def reset
142
+ @paused = false
143
+ @time = 0
144
+ @loop_count = -1
145
+ @start_time = nil
146
+ self
147
+ end
148
+
149
+ def is_running
150
+ @enabled && !@paused && @time_scale != 0 && @start_time.nil? &&
151
+ @mixer.instance_variable_get(:@active_actions).include?(self)
152
+ end
153
+
154
+ def is_scheduled
155
+ @mixer.instance_variable_get(:@active_actions).include?(self)
156
+ end
157
+
158
+ def start_at(time)
159
+ @start_time = time
160
+ self
161
+ end
162
+
163
+ def set_loop(mode, repetitions = Float::INFINITY)
164
+ @loop = mode
165
+ @repetitions = repetitions
166
+ self
167
+ end
168
+
169
+ def set_effective_time_scale(time_scale)
170
+ @time_scale = time_scale
171
+ self
172
+ end
173
+
174
+ def set_effective_weight(weight)
175
+ @weight = weight
176
+ self
177
+ end
178
+
179
+ def set_duration(duration)
180
+ @time_scale = @clip.duration / duration
181
+ self
182
+ end
183
+
184
+ def sync_with(action)
185
+ @time = action.time
186
+ @time_scale = action.time_scale
187
+ self
188
+ end
189
+
190
+ def halt(duration)
191
+ set_effective_weight(0) if duration == 0
192
+ self
193
+ end
194
+
195
+ def warp(start_time_scale, end_time_scale, duration)
196
+ @time_scale = start_time_scale
197
+ self
198
+ end
199
+
200
+ def crossfade_from(fade_out_action, duration, warp)
201
+ fade_out_action.fade_out(duration)
202
+ fade_in(duration)
203
+
204
+ if warp
205
+ fade_out_scale = fade_out_action.clip.duration / @clip.duration
206
+ fade_out_action.warp(1.0, fade_out_scale, duration)
207
+ warp(fade_out_scale, 1.0, duration)
208
+ end
209
+
210
+ self
211
+ end
212
+
213
+ def crossfade_to(fade_in_action, duration, warp)
214
+ fade_in_action.crossfade_from(self, duration, warp)
215
+ end
216
+
217
+ def fade_in(duration)
218
+ @weight = 0 if duration > 0
219
+ self
220
+ end
221
+
222
+ def fade_out(duration)
223
+ self
224
+ end
225
+
226
+ def update(delta)
227
+ return if @paused || !@enabled
228
+
229
+ if @start_time
230
+ return if @mixer.time < @start_time
231
+
232
+ @start_time = nil
233
+ end
234
+
235
+ @time += delta * @time_scale
236
+
237
+ apply
238
+ end
239
+
240
+ def apply
241
+ duration = @clip.duration
242
+ local_time = @time % duration
243
+ local_time = duration + local_time if local_time < 0
244
+
245
+ effective_weight = @weight
246
+
247
+ @clip.tracks.each_with_index do |track, i|
248
+ value = track.get_value_at_time(local_time)
249
+ binding = @bindings[i]
250
+ binding&.set_value(value, effective_weight, @blend_mode)
251
+ end
252
+ end
253
+
254
+ def get_effective_time_scale
255
+ @time_scale
256
+ end
257
+
258
+ def get_effective_weight
259
+ @weight
260
+ end
261
+
262
+ def get_clip
263
+ @clip
264
+ end
265
+
266
+ def get_mixer
267
+ @mixer
268
+ end
269
+
270
+ def get_root
271
+ @root
272
+ end
273
+ end
274
+
275
+ class PropertyBinding
276
+ attr_reader :root, :path
277
+
278
+ def initialize(root, path)
279
+ @root = root
280
+ @path = path
281
+
282
+ parse_path
283
+ end
284
+
285
+ def get_value
286
+ target = resolve_target
287
+ return nil unless target
288
+
289
+ if @property_index
290
+ target.send(@property_name)[@property_index]
291
+ else
292
+ target.send(@property_name)
293
+ end
294
+ end
295
+
296
+ def set_value(value, weight = 1.0, blend_mode = :normal)
297
+ target = resolve_target
298
+ return unless target
299
+
300
+ if value.is_a?(Array)
301
+ obj = target.send(@property_name)
302
+
303
+ case obj
304
+ when Vector2
305
+ obj.set(value[0], value[1])
306
+ when Vector3
307
+ obj.set(value[0], value[1], value[2])
308
+ when Vector4, Quaternion
309
+ obj.set(value[0], value[1], value[2], value[3])
310
+ when Color
311
+ obj.set_rgb(value[0], value[1], value[2])
312
+ when Euler
313
+ obj.set(value[0], value[1], value[2])
314
+ end
315
+ else
316
+ if @property_index
317
+ obj = target.send(@property_name)
318
+ obj[@property_index] = value
319
+ elsif target.respond_to?("#{@property_name}=")
320
+ target.send("#{@property_name}=", value)
321
+ end
322
+ end
323
+ end
324
+
325
+ private
326
+
327
+ def parse_path
328
+ parts = @path.split(".")
329
+
330
+ @node_name = nil
331
+ @property_name = nil
332
+ @property_index = nil
333
+
334
+ if parts.first&.match?(/^\[.+\]$/)
335
+ @node_name = parts.shift[1..-2]
336
+ end
337
+
338
+ if parts.last&.match?(/\[(\d+)\]$/)
339
+ match = parts.last.match(/(.+)\[(\d+)\]$/)
340
+ parts[-1] = match[1]
341
+ @property_index = match[2].to_i
342
+ end
343
+
344
+ @object_names = parts[0..-2]
345
+ @property_name = parts.last
346
+ end
347
+
348
+ def resolve_target
349
+ target = @root
350
+
351
+ if @node_name && target.respond_to?(:get_object_by_name)
352
+ target = target.get_object_by_name(@node_name) || target
353
+ end
354
+
355
+ @object_names&.each do |name|
356
+ if target.respond_to?(name)
357
+ target = target.send(name)
358
+ else
359
+ return nil
360
+ end
361
+ end
362
+
363
+ target
364
+ end
365
+ end
366
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ class Camera < Object3D
5
+ attr_reader :matrix_world_inverse, :projection_matrix, :projection_matrix_inverse
6
+
7
+ def initialize
8
+ super
9
+ @matrix_world_inverse = Matrix4.new
10
+ @projection_matrix = Matrix4.new
11
+ @projection_matrix_inverse = Matrix4.new
12
+ end
13
+
14
+ def update_matrix_world(force = false)
15
+ super
16
+ @matrix_world_inverse.copy(@matrix_world).invert!
17
+ end
18
+
19
+ def update_world_matrix(update_parents, update_children)
20
+ super
21
+ @matrix_world_inverse.copy(@matrix_world).invert!
22
+ end
23
+
24
+ def update_projection_matrix
25
+ raise NotImplementedError, "Subclasses must implement update_projection_matrix"
26
+ end
27
+
28
+ def clone(recursive = true)
29
+ new_camera = super
30
+ new_camera.matrix_world_inverse.copy(@matrix_world_inverse)
31
+ new_camera.projection_matrix.copy(@projection_matrix)
32
+ new_camera.projection_matrix_inverse.copy(@projection_matrix_inverse)
33
+ new_camera
34
+ end
35
+
36
+ def copy(source, recursive = true)
37
+ super
38
+ @matrix_world_inverse.copy(source.matrix_world_inverse)
39
+ @projection_matrix.copy(source.projection_matrix)
40
+ @projection_matrix_inverse.copy(source.projection_matrix_inverse)
41
+ self
42
+ end
43
+
44
+ def to_h
45
+ super.merge(
46
+ projection_matrix: @projection_matrix.elements
47
+ )
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ class OrthographicCamera < Camera
5
+ attr_accessor :left, :right, :top, :bottom, :near, :far, :zoom
6
+
7
+ def initialize(left: -1, right: 1, top: 1, bottom: -1, near: 0.1, far: 2000)
8
+ super()
9
+ @left = left.to_f
10
+ @right = right.to_f
11
+ @top = top.to_f
12
+ @bottom = bottom.to_f
13
+ @near = near.to_f
14
+ @far = far.to_f
15
+ @zoom = 1.0
16
+ update_projection_matrix
17
+ end
18
+
19
+ def update_projection_matrix
20
+ dx = (@right - @left) / (2 * @zoom)
21
+ dy = (@top - @bottom) / (2 * @zoom)
22
+ cx = (@right + @left) / 2
23
+ cy = (@top + @bottom) / 2
24
+
25
+ @projection_matrix.make_orthographic(
26
+ cx - dx,
27
+ cx + dx,
28
+ cy + dy,
29
+ cy - dy,
30
+ @near,
31
+ @far
32
+ )
33
+
34
+ @projection_matrix_inverse.copy(@projection_matrix).invert!
35
+ end
36
+
37
+ def set_view_offset(full_width, full_height, x, y, width, height)
38
+ @view = {
39
+ full_width: full_width,
40
+ full_height: full_height,
41
+ offset_x: x,
42
+ offset_y: y,
43
+ width: width,
44
+ height: height
45
+ }
46
+ update_projection_matrix
47
+ end
48
+
49
+ def clear_view_offset
50
+ @view = nil
51
+ update_projection_matrix
52
+ end
53
+
54
+ def clone(recursive = true)
55
+ new_camera = super
56
+ new_camera.left = @left
57
+ new_camera.right = @right
58
+ new_camera.top = @top
59
+ new_camera.bottom = @bottom
60
+ new_camera.near = @near
61
+ new_camera.far = @far
62
+ new_camera.zoom = @zoom
63
+ new_camera.update_projection_matrix
64
+ new_camera
65
+ end
66
+
67
+ def copy(source, recursive = true)
68
+ super
69
+ @left = source.left
70
+ @right = source.right
71
+ @top = source.top
72
+ @bottom = source.bottom
73
+ @near = source.near
74
+ @far = source.far
75
+ @zoom = source.zoom
76
+ update_projection_matrix
77
+ self
78
+ end
79
+
80
+ def to_h
81
+ super.merge(
82
+ left: @left,
83
+ right: @right,
84
+ top: @top,
85
+ bottom: @bottom,
86
+ near: @near,
87
+ far: @far,
88
+ zoom: @zoom
89
+ )
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ class PerspectiveCamera < Camera
5
+ attr_accessor :fov, :aspect, :near, :far, :focus, :zoom
6
+ attr_accessor :film_gauge, :film_offset
7
+
8
+ def initialize(fov: 50, aspect: 1, near: 0.1, far: 2000)
9
+ super()
10
+ @fov = fov.to_f
11
+ @aspect = aspect.to_f
12
+ @near = near.to_f
13
+ @far = far.to_f
14
+ @zoom = 1.0
15
+ @focus = 10.0
16
+ @film_gauge = 35.0
17
+ @film_offset = 0.0
18
+ update_projection_matrix
19
+ end
20
+
21
+ def set_focal_length(focal_length)
22
+ fov = 2 * MathUtils::RAD2DEG * Math.atan(@film_gauge / (focal_length * 2))
23
+ @fov = fov
24
+ update_projection_matrix
25
+ end
26
+
27
+ def get_focal_length
28
+ v_extent_slope = Math.tan(MathUtils::DEG2RAD * 0.5 * @fov) / @zoom
29
+ @film_gauge / (2 * v_extent_slope)
30
+ end
31
+
32
+ def get_effective_fov
33
+ MathUtils::RAD2DEG * 2 * Math.atan(Math.tan(MathUtils::DEG2RAD * 0.5 * @fov) / @zoom)
34
+ end
35
+
36
+ def get_film_width
37
+ @film_gauge * [@aspect, 1].min
38
+ end
39
+
40
+ def get_film_height
41
+ @film_gauge / [@aspect, 1].max
42
+ end
43
+
44
+ def update_projection_matrix
45
+ top = @near * Math.tan(MathUtils::DEG2RAD * 0.5 * @fov) / @zoom
46
+ height = 2 * top
47
+ width = @aspect * height
48
+ left = -0.5 * width
49
+
50
+ skew = @film_offset
51
+ left += @near * skew / get_film_width unless skew.zero?
52
+
53
+ @projection_matrix.make_perspective_bounds(
54
+ left,
55
+ left + width,
56
+ top,
57
+ top - height,
58
+ @near,
59
+ @far
60
+ )
61
+
62
+ @projection_matrix_inverse.copy(@projection_matrix).invert!
63
+ end
64
+
65
+ def clone(recursive = true)
66
+ new_camera = super
67
+ new_camera.fov = @fov
68
+ new_camera.aspect = @aspect
69
+ new_camera.near = @near
70
+ new_camera.far = @far
71
+ new_camera.zoom = @zoom
72
+ new_camera.focus = @focus
73
+ new_camera.film_gauge = @film_gauge
74
+ new_camera.film_offset = @film_offset
75
+ new_camera.update_projection_matrix
76
+ new_camera
77
+ end
78
+
79
+ def copy(source, recursive = true)
80
+ super
81
+ @fov = source.fov
82
+ @aspect = source.aspect
83
+ @near = source.near
84
+ @far = source.far
85
+ @zoom = source.zoom
86
+ @focus = source.focus
87
+ @film_gauge = source.film_gauge
88
+ @film_offset = source.film_offset
89
+ update_projection_matrix
90
+ self
91
+ end
92
+
93
+ def to_h
94
+ super.merge(
95
+ fov: @fov,
96
+ aspect: @aspect,
97
+ near: @near,
98
+ far: @far,
99
+ zoom: @zoom
100
+ )
101
+ end
102
+ end
103
+ end