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,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ class Object3D
5
+ attr_accessor :position, :rotation, :scale
6
+ attr_accessor :matrix, :matrix_world
7
+ attr_accessor :parent, :visible, :name
8
+ attr_accessor :cast_shadow, :receive_shadow
9
+ attr_accessor :frustum_culled, :render_order
10
+ attr_accessor :user_data
11
+ attr_reader :uuid, :children, :up, :quaternion
12
+
13
+ def initialize
14
+ @uuid = MathUtils.generate_uuid
15
+ @name = ""
16
+
17
+ @position = Vector3.new(0, 0, 0)
18
+ @rotation = Euler.new(0, 0, 0)
19
+ @quaternion = Quaternion.new
20
+ @scale = Vector3.new(1, 1, 1)
21
+
22
+ @up = Vector3.new(0, 1, 0)
23
+
24
+ @matrix = Matrix4.new
25
+ @matrix_world = Matrix4.new
26
+ @matrix_auto_update = true
27
+ @matrix_world_needs_update = false
28
+
29
+ @parent = nil
30
+ @children = []
31
+
32
+ @visible = true
33
+ @cast_shadow = false
34
+ @receive_shadow = false
35
+ @frustum_culled = true
36
+ @render_order = 0
37
+
38
+ @user_data = {}
39
+
40
+ sync_rotation_and_quaternion
41
+ end
42
+
43
+ def rotation=(value)
44
+ @rotation = value
45
+ @quaternion.set_from_euler(@rotation)
46
+ end
47
+
48
+ def quaternion=(value)
49
+ @quaternion = value
50
+ @rotation.set_from_quaternion(@quaternion)
51
+ end
52
+
53
+ def add(*objects)
54
+ objects.each do |object|
55
+ next if object == self
56
+
57
+ object.remove_from_parent if object.parent
58
+ object.parent = self
59
+ @children << object
60
+ end
61
+ self
62
+ end
63
+
64
+ def remove(*objects)
65
+ objects.each do |object|
66
+ index = @children.index(object)
67
+ next unless index
68
+
69
+ object.parent = nil
70
+ @children.delete_at(index)
71
+ end
72
+ self
73
+ end
74
+
75
+ def remove_from_parent
76
+ @parent&.remove(self)
77
+ self
78
+ end
79
+
80
+ def clear
81
+ @children.each do |child|
82
+ child.parent = nil
83
+ end
84
+ @children.clear
85
+ self
86
+ end
87
+
88
+ def get_object_by_id(id)
89
+ return self if @uuid == id
90
+
91
+ @children.each do |child|
92
+ found = child.get_object_by_id(id)
93
+ return found if found
94
+ end
95
+
96
+ nil
97
+ end
98
+
99
+ def get_object_by_name(name)
100
+ return self if @name == name
101
+
102
+ @children.each do |child|
103
+ found = child.get_object_by_name(name)
104
+ return found if found
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ def get_object_by_property(name, value)
111
+ return self if respond_to?(name) && send(name) == value
112
+
113
+ @children.each do |child|
114
+ found = child.get_object_by_property(name, value)
115
+ return found if found
116
+ end
117
+
118
+ nil
119
+ end
120
+
121
+ def get_world_position(target = Vector3.new)
122
+ update_world_matrix(true, false)
123
+ target.set(@matrix_world.elements[12], @matrix_world.elements[13], @matrix_world.elements[14])
124
+ end
125
+
126
+ def get_world_quaternion(target = Quaternion.new)
127
+ update_world_matrix(true, false)
128
+ @matrix_world.decompose[1].tap { |q| target.copy(q) }
129
+ target
130
+ end
131
+
132
+ def get_world_scale(target = Vector3.new)
133
+ update_world_matrix(true, false)
134
+ @matrix_world.decompose[2].tap { |s| target.copy(s) }
135
+ target
136
+ end
137
+
138
+ def get_world_direction(target = Vector3.new)
139
+ update_world_matrix(true, false)
140
+ e = @matrix_world.elements
141
+ target.set(e[8], e[9], e[10]).normalize
142
+ end
143
+
144
+ def look_at(x, y = nil, z = nil)
145
+ target = if x.is_a?(Vector3)
146
+ x
147
+ else
148
+ Vector3.new(x, y, z)
149
+ end
150
+
151
+ update_world_matrix(true, false)
152
+ position = get_world_position
153
+
154
+ m1 = Matrix4.new
155
+ m1.make_look_at(position, target, @up)
156
+
157
+ @quaternion.set_from_rotation_matrix(m1)
158
+
159
+ if @parent
160
+ m1.set_from_rotation_matrix(@parent.matrix_world)
161
+ parent_quaternion = Quaternion.new.set_from_rotation_matrix(m1)
162
+ @quaternion.premultiply!(parent_quaternion.invert!)
163
+ end
164
+
165
+ sync_rotation_from_quaternion
166
+ end
167
+
168
+ def traverse(&block)
169
+ block.call(self)
170
+ @children.each { |child| child.traverse(&block) }
171
+ end
172
+
173
+ def traverse_visible(&block)
174
+ return unless @visible
175
+
176
+ block.call(self)
177
+ @children.each { |child| child.traverse_visible(&block) }
178
+ end
179
+
180
+ def traverse_ancestors(&block)
181
+ @parent&.tap do |parent|
182
+ block.call(parent)
183
+ parent.traverse_ancestors(&block)
184
+ end
185
+ end
186
+
187
+ def update_matrix
188
+ @quaternion.set_from_euler(@rotation)
189
+ @matrix.compose(@position, @quaternion, @scale)
190
+ @matrix_world_needs_update = true
191
+ end
192
+
193
+ def update_matrix_world(force = false)
194
+ update_matrix if @matrix_auto_update
195
+
196
+ if @matrix_world_needs_update || force
197
+ if @parent.nil?
198
+ @matrix_world.copy(@matrix)
199
+ else
200
+ @matrix_world.copy(@parent.matrix_world).multiply!(@matrix)
201
+ end
202
+ @matrix_world_needs_update = false
203
+ force = true
204
+ end
205
+
206
+ @children.each { |child| child.update_matrix_world(force) }
207
+ end
208
+
209
+ def update_world_matrix(update_parents, update_children)
210
+ if update_parents && @parent
211
+ @parent.update_world_matrix(true, false)
212
+ end
213
+
214
+ update_matrix if @matrix_auto_update
215
+
216
+ if @parent.nil?
217
+ @matrix_world.copy(@matrix)
218
+ else
219
+ @matrix_world.copy(@parent.matrix_world).multiply!(@matrix)
220
+ end
221
+
222
+ if update_children
223
+ @children.each { |child| child.update_world_matrix(false, true) }
224
+ end
225
+ end
226
+
227
+ def clone(recursive = true)
228
+ new_object = self.class.new
229
+ new_object.name = @name
230
+ new_object.up.copy(@up)
231
+ new_object.position.copy(@position)
232
+ new_object.rotation.copy(@rotation)
233
+ new_object.scale.copy(@scale)
234
+ new_object.matrix.copy(@matrix)
235
+ new_object.matrix_world.copy(@matrix_world)
236
+ new_object.visible = @visible
237
+ new_object.cast_shadow = @cast_shadow
238
+ new_object.receive_shadow = @receive_shadow
239
+ new_object.frustum_culled = @frustum_culled
240
+ new_object.render_order = @render_order
241
+
242
+ if recursive
243
+ @children.each do |child|
244
+ new_object.add(child.clone)
245
+ end
246
+ end
247
+
248
+ new_object
249
+ end
250
+
251
+ def copy(source, recursive = true)
252
+ @name = source.name
253
+ @up.copy(source.up)
254
+ @position.copy(source.position)
255
+ @rotation.copy(source.rotation)
256
+ @quaternion.copy(source.quaternion)
257
+ @scale.copy(source.scale)
258
+ @matrix.copy(source.matrix)
259
+ @matrix_world.copy(source.matrix_world)
260
+ @visible = source.visible
261
+ @cast_shadow = source.cast_shadow
262
+ @receive_shadow = source.receive_shadow
263
+ @frustum_culled = source.frustum_culled
264
+ @render_order = source.render_order
265
+
266
+ if recursive
267
+ source.children.each do |child|
268
+ add(child.clone)
269
+ end
270
+ end
271
+
272
+ self
273
+ end
274
+
275
+ def to_h
276
+ {
277
+ uuid: @uuid,
278
+ type: self.class.name,
279
+ name: @name,
280
+ position: @position.to_a,
281
+ rotation: @rotation.to_a,
282
+ scale: @scale.to_a,
283
+ visible: @visible,
284
+ children: @children.map(&:to_h)
285
+ }
286
+ end
287
+
288
+ private
289
+
290
+ def sync_rotation_and_quaternion
291
+ @quaternion.set_from_euler(@rotation)
292
+ end
293
+
294
+ def sync_rotation_from_quaternion
295
+ @rotation.set_from_quaternion(@quaternion)
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ class Scene < Object3D
5
+ attr_accessor :background, :environment, :fog
6
+ attr_accessor :override_material
7
+
8
+ def initialize
9
+ super
10
+ @background = nil
11
+ @environment = nil
12
+ @fog = nil
13
+ @override_material = nil
14
+ end
15
+
16
+ def clone(recursive = true)
17
+ new_scene = super(recursive)
18
+ new_scene.background = @background&.respond_to?(:clone) ? @background.clone : @background
19
+ new_scene.environment = @environment
20
+ new_scene.fog = @fog&.clone
21
+ new_scene.override_material = @override_material
22
+ new_scene
23
+ end
24
+
25
+ def copy(source, recursive = true)
26
+ super(source, recursive)
27
+ @background = source.background&.respond_to?(:clone) ? source.background.clone : source.background
28
+ @environment = source.environment
29
+ @fog = source.fog&.clone
30
+ @override_material = source.override_material
31
+ self
32
+ end
33
+
34
+ def to_h
35
+ super.merge(
36
+ background: @background.respond_to?(:to_h) ? @background.to_h : @background,
37
+ fog: @fog&.to_h
38
+ )
39
+ end
40
+ end
41
+
42
+ class Fog
43
+ attr_accessor :name, :color, :near, :far
44
+
45
+ def initialize(color, near = 1, far = 1000)
46
+ @name = ""
47
+ @color = color.is_a?(Color) ? color : Color.new(color)
48
+ @near = near.to_f
49
+ @far = far.to_f
50
+ end
51
+
52
+ def clone
53
+ Fog.new(@color.clone, @near, @far)
54
+ end
55
+
56
+ def to_h
57
+ { color: @color.get_hex, near: @near, far: @far }
58
+ end
59
+ end
60
+
61
+ class FogExp2
62
+ attr_accessor :name, :color, :density
63
+
64
+ def initialize(color, density = 0.00025)
65
+ @name = ""
66
+ @color = color.is_a?(Color) ? color : Color.new(color)
67
+ @density = density.to_f
68
+ end
69
+
70
+ def clone
71
+ FogExp2.new(@color.clone, @density)
72
+ end
73
+
74
+ def to_h
75
+ { color: @color.get_hex, density: @density }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ module DSL
5
+ module Helpers
6
+ def self.color(value)
7
+ Color.new(value)
8
+ end
9
+
10
+ def self.vector2(x, y)
11
+ Vector2.new(x, y)
12
+ end
13
+
14
+ def self.vector3(x, y, z)
15
+ Vector3.new(x, y, z)
16
+ end
17
+
18
+ def self.euler(x, y, z, order = "XYZ")
19
+ Euler.new(x, y, z, order)
20
+ end
21
+
22
+ def self.degrees_to_radians(degrees)
23
+ degrees * Math::PI / 180
24
+ end
25
+
26
+ def self.radians_to_degrees(radians)
27
+ radians * 180 / Math::PI
28
+ end
29
+ end
30
+
31
+ module TopLevelHelpers
32
+ def color(value)
33
+ Helpers.color(value)
34
+ end
35
+
36
+ def vec2(x, y)
37
+ Helpers.vector2(x, y)
38
+ end
39
+
40
+ def vec3(x, y, z)
41
+ Helpers.vector3(x, y, z)
42
+ end
43
+
44
+ def euler(x, y, z, order = "XYZ")
45
+ Helpers.euler(x, y, z, order)
46
+ end
47
+
48
+ def deg2rad(degrees)
49
+ Helpers.degrees_to_radians(degrees)
50
+ end
51
+
52
+ def rad2deg(radians)
53
+ Helpers.radians_to_degrees(radians)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ module DSL
5
+ class SceneBuilder
6
+ attr_reader :scene, :camera, :renderer
7
+ attr_reader :objects, :lights
8
+
9
+ def initialize(width: 800, height: 600, title: "Sunrb")
10
+ @scene = Scene.new
11
+ @camera = nil
12
+ @renderer = OpenGLRenderer.new(width: width, height: height, title: title)
13
+ @objects = {}
14
+ @lights = {}
15
+ @frame_callbacks = []
16
+ @key_callbacks = []
17
+ @mouse_callbacks = []
18
+ end
19
+
20
+ def background(color)
21
+ @scene.background = color.is_a?(Color) ? color : Color.new(color)
22
+ end
23
+
24
+ def fog(color, near, far)
25
+ @scene.fog = Fog.new(color, near, far)
26
+ end
27
+
28
+ def perspective_camera(fov: 50, aspect: nil, near: 0.1, far: 2000, &block)
29
+ aspect ||= @renderer.width.to_f / @renderer.height
30
+ @camera = PerspectiveCamera.new(fov: fov, aspect: aspect, near: near, far: far)
31
+ ObjectConfigurator.new(@camera).instance_eval(&block) if block
32
+ @camera
33
+ end
34
+
35
+ def orthographic_camera(left: -1, right: 1, top: 1, bottom: -1, near: 0.1, far: 2000, &block)
36
+ @camera = OrthographicCamera.new(left: left, right: right, top: top, bottom: bottom, near: near, far: far)
37
+ ObjectConfigurator.new(@camera).instance_eval(&block) if block
38
+ @camera
39
+ end
40
+
41
+ def ambient_light(color: 0xffffff, intensity: 1.0, &block)
42
+ light = AmbientLight.new(color: color, intensity: intensity)
43
+ ObjectConfigurator.new(light).instance_eval(&block) if block
44
+ @scene.add(light)
45
+ light
46
+ end
47
+
48
+ def directional_light(color: 0xffffff, intensity: 1.0, &block)
49
+ light = DirectionalLight.new(color: color, intensity: intensity)
50
+ ObjectConfigurator.new(light).instance_eval(&block) if block
51
+ @scene.add(light)
52
+ light
53
+ end
54
+
55
+ def point_light(color: 0xffffff, intensity: 1.0, distance: 0, decay: 2, &block)
56
+ light = PointLight.new(color: color, intensity: intensity, distance: distance, decay: decay)
57
+ ObjectConfigurator.new(light).instance_eval(&block) if block
58
+ @scene.add(light)
59
+ light
60
+ end
61
+
62
+ def spot_light(color: 0xffffff, intensity: 1.0, distance: 0, angle: Math::PI / 3, penumbra: 0, decay: 2, &block)
63
+ light = SpotLight.new(color: color, intensity: intensity, distance: distance, angle: angle, penumbra: penumbra, decay: decay)
64
+ ObjectConfigurator.new(light).instance_eval(&block) if block
65
+ @scene.add(light)
66
+ light
67
+ end
68
+
69
+ def hemisphere_light(sky_color: 0xffffff, ground_color: 0xffffff, intensity: 1.0, &block)
70
+ light = HemisphereLight.new(sky_color: sky_color, ground_color: ground_color, intensity: intensity)
71
+ ObjectConfigurator.new(light).instance_eval(&block) if block
72
+ @scene.add(light)
73
+ light
74
+ end
75
+
76
+ def mesh(name = nil, &block)
77
+ builder = MeshBuilder.new
78
+ builder.instance_eval(&block) if block
79
+ mesh = builder.build
80
+ mesh.name = name.to_s if name
81
+ @objects[name] = mesh if name
82
+ @scene.add(mesh)
83
+ mesh
84
+ end
85
+
86
+ def group(name = nil, &block)
87
+ grp = Group.new
88
+ grp.name = name.to_s if name
89
+ GroupConfigurator.new(grp, self).instance_eval(&block) if block
90
+ @objects[name] = grp if name
91
+ @scene.add(grp)
92
+ grp
93
+ end
94
+
95
+ def on_frame(&block)
96
+ @frame_callbacks << block
97
+ end
98
+
99
+ def on_key(&block)
100
+ @key_callbacks << block
101
+ end
102
+
103
+ def on_mouse(&block)
104
+ @mouse_callbacks << block
105
+ end
106
+
107
+ def close
108
+ @renderer.stop
109
+ end
110
+
111
+ def run
112
+ @renderer.on_frame do |delta|
113
+ @frame_callbacks.each { |cb| cb.call(delta) }
114
+ end
115
+
116
+ @renderer.on_key do |key, action|
117
+ @key_callbacks.each { |cb| cb.call(key, action) }
118
+ end
119
+
120
+ @renderer.on_mouse do |button, action, x, y|
121
+ @mouse_callbacks.each { |cb| cb.call(button, action, x, y) }
122
+ end
123
+
124
+ @renderer.run do |_delta|
125
+ @renderer.render(@scene, @camera)
126
+ end
127
+ end
128
+
129
+ def method_missing(name, *args, &block)
130
+ return @objects[name] if @objects.key?(name)
131
+
132
+ super
133
+ end
134
+
135
+ def respond_to_missing?(name, include_private = false)
136
+ @objects.key?(name) || super
137
+ end
138
+ end
139
+
140
+ class ObjectConfigurator
141
+ def initialize(object)
142
+ @object = object
143
+ end
144
+
145
+ def position(x, y, z)
146
+ @object.position.set(x, y, z)
147
+ end
148
+
149
+ def rotation(x, y, z)
150
+ @object.rotation.set(x, y, z)
151
+ end
152
+
153
+ def scale(x, y = nil, z = nil)
154
+ y ||= x
155
+ z ||= x
156
+ @object.scale.set(x, y, z)
157
+ end
158
+
159
+ def look_at(x, y = nil, z = nil)
160
+ if x.is_a?(Vector3)
161
+ @object.look_at(x)
162
+ else
163
+ @object.look_at(x, y, z)
164
+ end
165
+ end
166
+
167
+ def visible(value)
168
+ @object.visible = value
169
+ end
170
+
171
+ def cast_shadow(value)
172
+ @object.cast_shadow = value
173
+ end
174
+
175
+ def receive_shadow(value)
176
+ @object.receive_shadow = value
177
+ end
178
+ end
179
+
180
+ class MeshBuilder
181
+ GEOMETRY_TYPES = {
182
+ box: BoxGeometry,
183
+ sphere: SphereGeometry,
184
+ plane: PlaneGeometry,
185
+ cylinder: CylinderGeometry,
186
+ cone: ConeGeometry,
187
+ torus: TorusGeometry
188
+ }.freeze
189
+
190
+ MATERIAL_TYPES = {
191
+ basic: MeshBasicMaterial,
192
+ lambert: MeshLambertMaterial,
193
+ phong: MeshPhongMaterial,
194
+ standard: MeshStandardMaterial
195
+ }.freeze
196
+
197
+ def initialize
198
+ @geometry = nil
199
+ @material = nil
200
+ @position = nil
201
+ @rotation = nil
202
+ @scale = nil
203
+ @name = nil
204
+ end
205
+
206
+ def geometry(type, **options)
207
+ klass = GEOMETRY_TYPES[type]
208
+ raise ArgumentError, "Unknown geometry type: #{type}" unless klass
209
+
210
+ @geometry = klass.new(**options)
211
+ end
212
+
213
+ def material(type, **options)
214
+ klass = MATERIAL_TYPES[type]
215
+ raise ArgumentError, "Unknown material type: #{type}" unless klass
216
+
217
+ @material = klass.new(**options)
218
+ end
219
+
220
+ def position(x, y, z)
221
+ @position = [x, y, z]
222
+ end
223
+
224
+ def rotation(x, y, z)
225
+ @rotation = [x, y, z]
226
+ end
227
+
228
+ def scale(x, y = nil, z = nil)
229
+ y ||= x
230
+ z ||= x
231
+ @scale = [x, y, z]
232
+ end
233
+
234
+ def build
235
+ @geometry ||= BoxGeometry.new
236
+ @material ||= MeshBasicMaterial.new
237
+
238
+ mesh = Mesh.new(@geometry, @material)
239
+ mesh.position.set(*@position) if @position
240
+ mesh.rotation.set(*@rotation) if @rotation
241
+ mesh.scale.set(*@scale) if @scale
242
+ mesh.name = @name if @name
243
+ mesh
244
+ end
245
+ end
246
+
247
+ class GroupConfigurator < ObjectConfigurator
248
+ def initialize(group, scene_builder)
249
+ super(group)
250
+ @group = group
251
+ @scene_builder = scene_builder
252
+ end
253
+
254
+ def mesh(name = nil, &block)
255
+ builder = MeshBuilder.new
256
+ builder.instance_eval(&block) if block
257
+ mesh = builder.build
258
+ mesh.name = name.to_s if name
259
+ @scene_builder.objects[name] = mesh if name
260
+ @group.add(mesh)
261
+ mesh
262
+ end
263
+
264
+ def group(name = nil, &block)
265
+ grp = Group.new
266
+ grp.name = name.to_s if name
267
+ GroupConfigurator.new(grp, @scene_builder).instance_eval(&block) if block
268
+ @scene_builder.objects[name] = grp if name
269
+ @group.add(grp)
270
+ grp
271
+ end
272
+ end
273
+ end
274
+
275
+ class << self
276
+ def app(width: 800, height: 600, title: "Sunrb", &block)
277
+ builder = DSL::SceneBuilder.new(width: width, height: height, title: title)
278
+ builder.instance_eval(&block)
279
+ builder.run
280
+ end
281
+
282
+ def scene(&block)
283
+ builder = DSL::SceneBuilder.new
284
+ builder.instance_eval(&block)
285
+ builder
286
+ end
287
+ end
288
+ end