three-rb 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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE +21 -0
  4. data/README.md +153 -0
  5. data/docs/browser-runtime.md +153 -0
  6. data/docs/implementation-plan.md +874 -0
  7. data/docs/loaded-assets-design.md +400 -0
  8. data/docs/next-work.md +107 -0
  9. data/docs/publishing.md +64 -0
  10. data/docs/release-readiness.md +83 -0
  11. data/examples/browser/README.md +54 -0
  12. data/examples/browser/assets/animated_triangle.gltf +123 -0
  13. data/examples/browser/assets/checker.svg +11 -0
  14. data/examples/browser/assets/compressed_triangle.gltf +74 -0
  15. data/examples/browser/assets/studio.hdr +5 -0
  16. data/examples/browser/assets/triangle.gltf +67 -0
  17. data/examples/browser/composition/README.md +35 -0
  18. data/examples/browser/composition/boot.mjs +6 -0
  19. data/examples/browser/composition/index.html +136 -0
  20. data/examples/browser/composition/main.rb +216 -0
  21. data/examples/browser/composition/smoke_test.mjs +266 -0
  22. data/examples/browser/cube/README.md +41 -0
  23. data/examples/browser/cube/boot.mjs +6 -0
  24. data/examples/browser/cube/index.html +142 -0
  25. data/examples/browser/cube/main.rb +62 -0
  26. data/examples/browser/cube/smoke_test.mjs +99 -0
  27. data/examples/browser/cubemap/README.md +23 -0
  28. data/examples/browser/cubemap/boot.mjs +6 -0
  29. data/examples/browser/cubemap/index.html +142 -0
  30. data/examples/browser/cubemap/main.rb +84 -0
  31. data/examples/browser/cubemap/smoke_test.mjs +91 -0
  32. data/examples/browser/gltf/README.md +23 -0
  33. data/examples/browser/gltf/boot.mjs +6 -0
  34. data/examples/browser/gltf/index.html +142 -0
  35. data/examples/browser/gltf/main.rb +105 -0
  36. data/examples/browser/gltf/smoke_test.mjs +162 -0
  37. data/examples/browser/picking/README.md +33 -0
  38. data/examples/browser/picking/boot.mjs +6 -0
  39. data/examples/browser/picking/index.html +142 -0
  40. data/examples/browser/picking/main.rb +113 -0
  41. data/examples/browser/picking/smoke_test.mjs +78 -0
  42. data/examples/browser/postprocessing/README.md +26 -0
  43. data/examples/browser/postprocessing/boot.mjs +6 -0
  44. data/examples/browser/postprocessing/index.html +142 -0
  45. data/examples/browser/postprocessing/main.rb +117 -0
  46. data/examples/browser/postprocessing/smoke_test.mjs +121 -0
  47. data/examples/browser/primitives/README.md +33 -0
  48. data/examples/browser/primitives/boot.mjs +6 -0
  49. data/examples/browser/primitives/index.html +142 -0
  50. data/examples/browser/primitives/main.rb +116 -0
  51. data/examples/browser/primitives/smoke_test.mjs +113 -0
  52. data/examples/browser/serialization/README.md +33 -0
  53. data/examples/browser/serialization/boot.mjs +6 -0
  54. data/examples/browser/serialization/index.html +142 -0
  55. data/examples/browser/serialization/main.rb +97 -0
  56. data/examples/browser/serialization/smoke_test.mjs +67 -0
  57. data/examples/browser/shared/boot.mjs +79 -0
  58. data/examples/browser/shared/smoke_test_helpers.mjs +151 -0
  59. data/examples/browser/textures/README.md +35 -0
  60. data/examples/browser/textures/boot.mjs +6 -0
  61. data/examples/browser/textures/index.html +142 -0
  62. data/examples/browser/textures/main.rb +142 -0
  63. data/examples/browser/textures/smoke_test.mjs +189 -0
  64. data/lib/three/animation/animation_action.rb +57 -0
  65. data/lib/three/animation/animation_clip.rb +22 -0
  66. data/lib/three/animation/animation_mixer.rb +43 -0
  67. data/lib/three/backends/base.rb +87 -0
  68. data/lib/three/backends/threejs/materialization.rb +143 -0
  69. data/lib/three/backends/threejs/parameters.rb +97 -0
  70. data/lib/three/backends/threejs/resource_management.rb +69 -0
  71. data/lib/three/backends/threejs/ruby_wasm_adapter.rb +873 -0
  72. data/lib/three/backends/threejs/synchronization.rb +224 -0
  73. data/lib/three/backends/threejs.rb +365 -0
  74. data/lib/three/cameras/camera.rb +39 -0
  75. data/lib/three/cameras/orthographic_camera.rb +107 -0
  76. data/lib/three/cameras/perspective_camera.rb +137 -0
  77. data/lib/three/constants.rb +40 -0
  78. data/lib/three/controls/orbit_controls.rb +118 -0
  79. data/lib/three/core/buffer_attribute.rb +151 -0
  80. data/lib/three/core/buffer_geometry.rb +181 -0
  81. data/lib/three/core/clock.rb +58 -0
  82. data/lib/three/core/event_dispatcher.rb +57 -0
  83. data/lib/three/core/layers.rb +75 -0
  84. data/lib/three/core/object3d.rb +331 -0
  85. data/lib/three/core/raycaster.rb +73 -0
  86. data/lib/three/dirty.rb +58 -0
  87. data/lib/three/exporters/three_json_exporter.rb +187 -0
  88. data/lib/three/geometries/box_geometry.rb +97 -0
  89. data/lib/three/geometries/plane_geometry.rb +70 -0
  90. data/lib/three/geometries/sphere_geometry.rb +107 -0
  91. data/lib/three/lights/ambient_light.rb +12 -0
  92. data/lib/three/lights/directional_light.rb +38 -0
  93. data/lib/three/lights/hemisphere_light.rb +34 -0
  94. data/lib/three/lights/light.rb +85 -0
  95. data/lib/three/lights/point_light.rb +33 -0
  96. data/lib/three/loaders/cube_texture_loader.rb +13 -0
  97. data/lib/three/loaders/gltf_loader.rb +48 -0
  98. data/lib/three/loaders/rgbe_loader.rb +15 -0
  99. data/lib/three/loaders/texture_loader.rb +13 -0
  100. data/lib/three/loaders/three_json_loader.rb +409 -0
  101. data/lib/three/materials/line_basic_material.rb +65 -0
  102. data/lib/three/materials/material.rb +158 -0
  103. data/lib/three/materials/mesh_basic_material.rb +64 -0
  104. data/lib/three/materials/mesh_lambert_material.rb +71 -0
  105. data/lib/three/materials/mesh_matcap_material.rb +86 -0
  106. data/lib/three/materials/mesh_normal_material.rb +42 -0
  107. data/lib/three/materials/mesh_phong_material.rb +119 -0
  108. data/lib/three/materials/mesh_physical_material.rb +155 -0
  109. data/lib/three/materials/mesh_standard_material.rb +149 -0
  110. data/lib/three/materials/mesh_toon_material.rb +98 -0
  111. data/lib/three/materials/points_material.rb +74 -0
  112. data/lib/three/materials/shadow_material.rb +45 -0
  113. data/lib/three/materials/sprite_material.rb +75 -0
  114. data/lib/three/math/color.rb +133 -0
  115. data/lib/three/math/euler.rb +197 -0
  116. data/lib/three/math/math_utils.rb +36 -0
  117. data/lib/three/math/matrix3.rb +255 -0
  118. data/lib/three/math/matrix4.rb +448 -0
  119. data/lib/three/math/quaternion.rb +277 -0
  120. data/lib/three/math/vector2.rb +95 -0
  121. data/lib/three/math/vector3.rb +396 -0
  122. data/lib/three/objects/external_object3d.rb +28 -0
  123. data/lib/three/objects/group.rb +12 -0
  124. data/lib/three/objects/instanced_mesh.rb +110 -0
  125. data/lib/three/objects/line.rb +41 -0
  126. data/lib/three/objects/mesh.rb +45 -0
  127. data/lib/three/objects/points.rb +41 -0
  128. data/lib/three/objects/sprite.rb +57 -0
  129. data/lib/three/postprocessing/dot_screen_pass.rb +83 -0
  130. data/lib/three/postprocessing/effect_composer.rb +56 -0
  131. data/lib/three/postprocessing/output_pass.rb +40 -0
  132. data/lib/three/postprocessing/render_pass.rb +42 -0
  133. data/lib/three/postprocessing/unreal_bloom_pass.rb +56 -0
  134. data/lib/three/renderers/renderer.rb +11 -0
  135. data/lib/three/renderers/threejs_renderer.rb +85 -0
  136. data/lib/three/scenes/scene.rb +29 -0
  137. data/lib/three/textures/cube_texture.rb +72 -0
  138. data/lib/three/textures/rgbe_texture.rb +45 -0
  139. data/lib/three/textures/texture.rb +200 -0
  140. data/lib/three/version.rb +5 -0
  141. data/lib/three-rb.rb +3 -0
  142. data/lib/three.rb +77 -0
  143. data/package.json +30 -0
  144. data/pnpm-lock.yaml +86 -0
  145. metadata +216 -0
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/object3d"
4
+ require_relative "../math/color"
5
+
6
+ module Three
7
+ class Light < Object3D
8
+ attr_reader :color, :intensity
9
+ attr_reader :shadow_map_size, :shadow_bias, :shadow_normal_bias, :shadow_radius
10
+
11
+ def initialize(color = 0xffffff, intensity = 1)
12
+ super()
13
+ @type = "Light"
14
+ @color = Color.new(color)
15
+ @intensity = intensity
16
+ @shadow_map_size = [512, 512]
17
+ @shadow_bias = 0
18
+ @shadow_normal_bias = 0
19
+ @shadow_radius = 1
20
+ bind_color_changes
21
+ mark_dirty!(:light)
22
+ end
23
+
24
+ def color=(value)
25
+ @color = value.is_a?(Color) ? value : Color.new(value)
26
+ bind_color_changes
27
+ mark_dirty!(:light)
28
+ end
29
+
30
+ def intensity=(value)
31
+ @intensity = value
32
+ mark_dirty!(:light)
33
+ end
34
+
35
+ def shadow_map_size=(value)
36
+ array = coerce_shadow_map_size(value)
37
+ @shadow_map_size = array
38
+ mark_dirty!(:shadow)
39
+ end
40
+
41
+ def shadow_bias=(value)
42
+ @shadow_bias = value
43
+ mark_dirty!(:shadow)
44
+ end
45
+
46
+ def shadow_normal_bias=(value)
47
+ @shadow_normal_bias = value
48
+ mark_dirty!(:shadow)
49
+ end
50
+
51
+ def shadow_radius=(value)
52
+ @shadow_radius = value
53
+ mark_dirty!(:shadow)
54
+ end
55
+
56
+ def dispose
57
+ dispatch_event(:dispose)
58
+ end
59
+
60
+ def to_h
61
+ super.merge(
62
+ color: @color.hex,
63
+ intensity: @intensity,
64
+ shadow_map_size: @shadow_map_size.dup,
65
+ shadow_bias: @shadow_bias,
66
+ shadow_normal_bias: @shadow_normal_bias,
67
+ shadow_radius: @shadow_radius
68
+ )
69
+ end
70
+
71
+ private
72
+
73
+ def bind_color_changes
74
+ @color.on_change { mark_dirty!(:light) }
75
+ end
76
+
77
+ def coerce_shadow_map_size(value)
78
+ array = value.to_ary if value.respond_to?(:to_ary)
79
+ array ||= value.to_a if value.respond_to?(:to_a)
80
+ raise TypeError, "shadow_map_size must be array-like [width, height]" unless array && array.length >= 2
81
+
82
+ [array[0], array[1]]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "light"
4
+
5
+ module Three
6
+ class PointLight < Light
7
+ attr_reader :distance, :decay
8
+
9
+ def initialize(color = 0xffffff, intensity = 1, distance = 0, decay = 2)
10
+ super(color, intensity)
11
+ @type = "PointLight"
12
+ @distance = distance
13
+ @decay = decay
14
+ end
15
+
16
+ def distance=(value)
17
+ @distance = value
18
+ mark_dirty!(:light)
19
+ end
20
+
21
+ def decay=(value)
22
+ @decay = value
23
+ mark_dirty!(:light)
24
+ end
25
+
26
+ def to_h
27
+ super.merge(
28
+ distance: @distance,
29
+ decay: @decay
30
+ )
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../textures/cube_texture"
4
+
5
+ module Three
6
+ module Loaders
7
+ class CubeTextureLoader
8
+ def load(sources)
9
+ CubeTexture.new(sources)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../backends/threejs"
4
+ require_relative "../animation/animation_clip"
5
+ require_relative "../objects/external_object3d"
6
+
7
+ module Three
8
+ module Loaders
9
+ class GLTF
10
+ attr_reader :handle, :scene, :animations
11
+
12
+ def initialize(handle, adapter:)
13
+ @handle = handle
14
+ @adapter = adapter
15
+ @scene = ExternalObject3D.new(read_property(handle, :scene), type: "GLTFScene")
16
+ @animations = @adapter.gltf_animations(handle).map do |animation|
17
+ AnimationClip.new(animation, adapter: @adapter)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def read_property(object, name)
24
+ object[name]
25
+ end
26
+ end
27
+
28
+ class GLTFLoader
29
+ def initialize(adapter: nil, backend: nil, draco_decoder_path: nil, draco_decoder_config: nil)
30
+ @adapter = adapter || backend&.adapter || Backends::ThreeJS::RubyWasmAdapter.new
31
+ @draco_decoder_path = draco_decoder_path
32
+ @draco_decoder_config = draco_decoder_config
33
+ end
34
+
35
+ def load(source)
36
+ result = @adapter.load_gltf(
37
+ source,
38
+ draco_decoder_path: @draco_decoder_path,
39
+ draco_decoder_config: @draco_decoder_config
40
+ )
41
+ result = result.await if result.respond_to?(:await)
42
+ gltf = GLTF.new(result, adapter: @adapter)
43
+ yield gltf if block_given?
44
+ gltf
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../textures/rgbe_texture"
4
+
5
+ module Three
6
+ module Loaders
7
+ class RGBELoader
8
+ def load(source, **parameters)
9
+ texture = RGBETexture.new(source, **parameters)
10
+ yield texture if block_given?
11
+ texture
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../textures/texture"
4
+
5
+ module Three
6
+ module Loaders
7
+ class TextureLoader
8
+ def load(source)
9
+ Texture.new(source)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,409 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Three
6
+ module Loaders
7
+ class ThreeJSONLoader
8
+ def parse(input)
9
+ data = input.is_a?(String) ? JSON.parse(input) : input
10
+ @textures = build_resource_map(value(data, :textures)) { |entry| build_texture(entry) }
11
+ @geometries = build_resource_map(value(data, :geometries)) { |entry| build_geometry(entry) }
12
+ @materials = build_resource_map(value(data, :materials)) { |entry| build_material(entry) }
13
+ build_object(value(data, :object))
14
+ end
15
+
16
+ private
17
+
18
+ def build_resource_map(entries)
19
+ Array(entries).each_with_object({}) do |entry, result|
20
+ result[value(entry, :uuid)] = yield(entry)
21
+ end
22
+ end
23
+
24
+ def build_texture(entry)
25
+ case value(entry, :type)
26
+ when "CubeTexture"
27
+ CubeTexture.new(
28
+ value(entry, :sources) || value(entry, :source),
29
+ **texture_parameters(entry)
30
+ )
31
+ when "RGBETexture"
32
+ RGBETexture.new(value(entry, :source), **texture_parameters(entry))
33
+ else
34
+ Texture.new(value(entry, :source), **texture_parameters(entry))
35
+ end
36
+ end
37
+
38
+ def texture_parameters(entry)
39
+ {
40
+ mapping: value(entry, :mapping),
41
+ color_space: value(entry, :color_space),
42
+ flip_y: value(entry, :flip_y),
43
+ wrap_s: value(entry, :wrap_s),
44
+ wrap_t: value(entry, :wrap_t),
45
+ mag_filter: value(entry, :mag_filter),
46
+ min_filter: value(entry, :min_filter),
47
+ offset: value(entry, :offset),
48
+ repeat: value(entry, :repeat),
49
+ center: value(entry, :center),
50
+ rotation: value(entry, :rotation),
51
+ matrix_auto_update: value(entry, :matrix_auto_update),
52
+ matrix: value(entry, :matrix)
53
+ }.compact
54
+ end
55
+
56
+ def build_geometry(entry)
57
+ case value(entry, :type)
58
+ when "BoxGeometry"
59
+ parameters = value(entry, :parameters) || {}
60
+ BoxGeometry.new(
61
+ value(parameters, :width) || 1,
62
+ value(parameters, :height) || 1,
63
+ value(parameters, :depth) || 1,
64
+ width_segments: value(parameters, :width_segments) || 1,
65
+ height_segments: value(parameters, :height_segments) || 1,
66
+ depth_segments: value(parameters, :depth_segments) || 1
67
+ )
68
+ when "PlaneGeometry"
69
+ parameters = value(entry, :parameters) || {}
70
+ PlaneGeometry.new(
71
+ value(parameters, :width) || 1,
72
+ value(parameters, :height) || 1,
73
+ width_segments: value(parameters, :width_segments) || 1,
74
+ height_segments: value(parameters, :height_segments) || 1
75
+ )
76
+ when "SphereGeometry"
77
+ parameters = value(entry, :parameters) || {}
78
+ SphereGeometry.new(
79
+ value(parameters, :radius) || 1,
80
+ width_segments: value(parameters, :width_segments) || 32,
81
+ height_segments: value(parameters, :height_segments) || 16,
82
+ phi_start: value(parameters, :phi_start) || 0,
83
+ phi_length: value(parameters, :phi_length) || Math::PI * 2,
84
+ theta_start: value(parameters, :theta_start) || 0,
85
+ theta_length: value(parameters, :theta_length) || Math::PI
86
+ )
87
+ else
88
+ build_buffer_geometry(entry)
89
+ end
90
+ end
91
+
92
+ def build_buffer_geometry(entry)
93
+ geometry = BufferGeometry.new
94
+ geometry.name = value(entry, :name) if value(entry, :name)
95
+ geometry.set_index(build_buffer_attribute(value(entry, :index))) if value(entry, :index)
96
+
97
+ (value(entry, :attributes) || {}).each do |name, attribute_entry|
98
+ geometry.set_attribute(name.to_sym, build_buffer_attribute(attribute_entry))
99
+ end
100
+
101
+ Array(value(entry, :groups)).each do |group|
102
+ geometry.add_group(
103
+ value(group, :start),
104
+ value(group, :count),
105
+ value(group, :material_index) || 0
106
+ )
107
+ end
108
+
109
+ geometry
110
+ end
111
+
112
+ def build_buffer_attribute(entry)
113
+ component_type = (value(entry, :component_type) || :generic).to_sym
114
+ array = value(entry, :array) || []
115
+ item_size = value(entry, :item_size)
116
+ normalized = value(entry, :normalized) || false
117
+
118
+ case component_type
119
+ when :float32
120
+ Float32BufferAttribute.new(array, item_size, normalized)
121
+ when :uint16
122
+ Uint16BufferAttribute.new(array, item_size, normalized)
123
+ when :uint32
124
+ Uint32BufferAttribute.new(array, item_size, normalized)
125
+ else
126
+ BufferAttribute.new(array, item_size, normalized, component_type: component_type)
127
+ end
128
+ end
129
+
130
+ def build_material(entry)
131
+ parameters = material_parameters(entry)
132
+ case value(entry, :type)
133
+ when "MeshBasicMaterial"
134
+ MeshBasicMaterial.new(parameters)
135
+ when "LineBasicMaterial"
136
+ LineBasicMaterial.new(parameters)
137
+ when "MeshLambertMaterial"
138
+ MeshLambertMaterial.new(parameters)
139
+ when "MeshMatcapMaterial"
140
+ MeshMatcapMaterial.new(parameters)
141
+ when "MeshNormalMaterial"
142
+ MeshNormalMaterial.new(parameters)
143
+ when "MeshPhongMaterial"
144
+ MeshPhongMaterial.new(parameters)
145
+ when "MeshPhysicalMaterial"
146
+ MeshPhysicalMaterial.new(parameters)
147
+ when "MeshStandardMaterial"
148
+ MeshStandardMaterial.new(parameters)
149
+ when "MeshToonMaterial"
150
+ MeshToonMaterial.new(parameters)
151
+ when "PointsMaterial"
152
+ PointsMaterial.new(parameters)
153
+ when "ShadowMaterial"
154
+ ShadowMaterial.new(parameters)
155
+ when "SpriteMaterial"
156
+ SpriteMaterial.new(parameters)
157
+ else
158
+ Material.new(parameters)
159
+ end
160
+ end
161
+
162
+ def material_parameters(entry)
163
+ parameters = {}
164
+ %i[
165
+ name
166
+ side
167
+ opacity
168
+ transparent
169
+ visible
170
+ vertex_colors
171
+ color
172
+ emissive
173
+ specular
174
+ shininess
175
+ roughness
176
+ metalness
177
+ anisotropy
178
+ anisotropy_rotation
179
+ clearcoat
180
+ clearcoat_roughness
181
+ transmission
182
+ thickness
183
+ ior
184
+ reflectivity
185
+ iridescence
186
+ iridescence_ior
187
+ iridescence_thickness_range
188
+ sheen
189
+ sheen_color
190
+ sheen_roughness
191
+ dispersion
192
+ specular_intensity
193
+ specular_color
194
+ attenuation_distance
195
+ attenuation_color
196
+ wireframe
197
+ wireframe_linewidth
198
+ linewidth
199
+ linecap
200
+ linejoin
201
+ fog
202
+ flat_shading
203
+ rotation
204
+ size
205
+ size_attenuation
206
+ ].each do |key|
207
+ next unless has_value?(entry, key)
208
+
209
+ parameters[key] = value(entry, key)
210
+ end
211
+
212
+ texture_slots(entry).each do |slot|
213
+ uuid = value(entry, slot)
214
+ parameters[slot] = @textures[uuid] if uuid
215
+ end
216
+ parameters
217
+ end
218
+
219
+ def texture_slots(entry)
220
+ %i[
221
+ map
222
+ alpha_map
223
+ ao_map
224
+ bump_map
225
+ displacement_map
226
+ emissive_map
227
+ env_map
228
+ gradient_map
229
+ light_map
230
+ matcap
231
+ metalness_map
232
+ normal_map
233
+ roughness_map
234
+ specular_map
235
+ anisotropy_map
236
+ clearcoat_map
237
+ clearcoat_normal_map
238
+ clearcoat_roughness_map
239
+ transmission_map
240
+ thickness_map
241
+ iridescence_map
242
+ iridescence_thickness_map
243
+ sheen_color_map
244
+ sheen_roughness_map
245
+ specular_color_map
246
+ specular_intensity_map
247
+ ].select { |slot| has_value?(entry, slot) }
248
+ end
249
+
250
+ def build_object(entry)
251
+ object = instantiate_object(entry)
252
+ apply_object_properties(object, entry)
253
+ Array(value(entry, :children)).each { |child| object.add(build_object(child)) }
254
+ object
255
+ end
256
+
257
+ def instantiate_object(entry)
258
+ case value(entry, :type)
259
+ when "Scene"
260
+ build_scene(entry)
261
+ when "PerspectiveCamera"
262
+ PerspectiveCamera.new(
263
+ value(entry, :fov) || 50,
264
+ aspect: value(entry, :aspect) || 1,
265
+ near: value(entry, :near) || 0.1,
266
+ far: value(entry, :far) || 2000
267
+ )
268
+ when "OrthographicCamera"
269
+ OrthographicCamera.new(
270
+ value(entry, :left),
271
+ value(entry, :right),
272
+ value(entry, :top),
273
+ value(entry, :bottom),
274
+ near: value(entry, :near) || 0.1,
275
+ far: value(entry, :far) || 2000
276
+ )
277
+ when "AmbientLight"
278
+ AmbientLight.new(value(entry, :color) || 0xffffff, value(entry, :intensity) || 1)
279
+ when "DirectionalLight"
280
+ build_directional_light(entry)
281
+ when "PointLight"
282
+ PointLight.new(
283
+ value(entry, :color) || 0xffffff,
284
+ value(entry, :intensity) || 1,
285
+ value(entry, :distance) || 0,
286
+ value(entry, :decay) || 2
287
+ )
288
+ when "HemisphereLight"
289
+ HemisphereLight.new(
290
+ value(entry, :color) || 0xffffff,
291
+ value(entry, :ground_color) || 0xffffff,
292
+ value(entry, :intensity) || 1
293
+ )
294
+ when "InstancedMesh"
295
+ build_instanced_mesh(entry)
296
+ when "Mesh"
297
+ Mesh.new(@geometries[value(entry, :geometry)], material_reference(entry))
298
+ when "Line"
299
+ Line.new(@geometries[value(entry, :geometry)], material_reference(entry))
300
+ when "Points"
301
+ Points.new(@geometries[value(entry, :geometry)], material_reference(entry))
302
+ when "Sprite"
303
+ build_sprite(entry)
304
+ when "Group"
305
+ Group.new
306
+ else
307
+ Object3D.new
308
+ end
309
+ end
310
+
311
+ def build_scene(entry)
312
+ scene = Scene.new
313
+ scene.background = @textures[value(entry, :background)] if value(entry, :background)
314
+ scene.environment = @textures[value(entry, :environment)] if value(entry, :environment)
315
+ scene
316
+ end
317
+
318
+ def build_directional_light(entry)
319
+ light = DirectionalLight.new(value(entry, :color) || 0xffffff, value(entry, :intensity) || 1)
320
+ camera = value(entry, :shadow_camera)
321
+ light.set_shadow_camera(**symbolize_keys(camera)) if camera
322
+ light
323
+ end
324
+
325
+ def build_instanced_mesh(entry)
326
+ mesh = InstancedMesh.new(
327
+ @geometries[value(entry, :geometry)],
328
+ material_reference(entry),
329
+ value(entry, :capacity) || value(entry, :count) || 1
330
+ )
331
+ mesh.count = value(entry, :count) if has_value?(entry, :count)
332
+
333
+ Array(value(entry, :instance_matrices)).each_with_index do |matrix, index|
334
+ mesh.set_matrix_at(index, matrix)
335
+ end
336
+ Array(value(entry, :instance_colors)).each_with_index do |color, index|
337
+ mesh.set_color_at(index, color) if color
338
+ end
339
+ mesh
340
+ end
341
+
342
+ def build_sprite(entry)
343
+ sprite = Sprite.new(material_reference(entry))
344
+ sprite.center = value(entry, :center) if value(entry, :center)
345
+ sprite
346
+ end
347
+
348
+ def material_reference(entry)
349
+ material = value(entry, :material)
350
+ return material.map { |uuid| @materials[uuid] } if material.is_a?(Array)
351
+
352
+ @materials[material]
353
+ end
354
+
355
+ def apply_object_properties(object, entry)
356
+ object.name = value(entry, :name) if has_value?(entry, :name)
357
+ object.visible = value(entry, :visible) if has_value?(entry, :visible)
358
+ object.layers.mask = value(entry, :layers) if has_value?(entry, :layers)
359
+ object.cast_shadow = value(entry, :cast_shadow) if has_value?(entry, :cast_shadow)
360
+ object.receive_shadow = value(entry, :receive_shadow) if has_value?(entry, :receive_shadow)
361
+ object.matrix_auto_update = value(entry, :matrix_auto_update) if has_value?(entry, :matrix_auto_update)
362
+ object.user_data = value(entry, :user_data) || {}
363
+
364
+ object.position.set(*value(entry, :position)) if value(entry, :position)
365
+ object.quaternion.set(*value(entry, :quaternion)) if value(entry, :quaternion)
366
+ object.scale.set(*value(entry, :scale)) if value(entry, :scale)
367
+
368
+ apply_camera_properties(object, entry)
369
+ apply_light_properties(object, entry)
370
+ object
371
+ end
372
+
373
+ def apply_camera_properties(object, entry)
374
+ return unless object.is_a?(Camera)
375
+
376
+ object.zoom = value(entry, :zoom) if has_value?(entry, :zoom)
377
+ object.update_projection_matrix if object.respond_to?(:update_projection_matrix)
378
+ end
379
+
380
+ def apply_light_properties(object, entry)
381
+ return unless object.is_a?(Light)
382
+
383
+ object.shadow_map_size = value(entry, :shadow_map_size) if value(entry, :shadow_map_size)
384
+ object.shadow_bias = value(entry, :shadow_bias) if has_value?(entry, :shadow_bias)
385
+ object.shadow_normal_bias = value(entry, :shadow_normal_bias) if has_value?(entry, :shadow_normal_bias)
386
+ object.shadow_radius = value(entry, :shadow_radius) if has_value?(entry, :shadow_radius)
387
+ end
388
+
389
+ def has_value?(hash, key)
390
+ hash.key?(key) || hash.key?(key.to_s)
391
+ end
392
+
393
+ def value(hash, key)
394
+ return nil unless hash
395
+ return hash[key] if hash.key?(key)
396
+
397
+ hash[key.to_s]
398
+ end
399
+
400
+ def symbolize_keys(hash)
401
+ return {} unless hash
402
+
403
+ hash.each_with_object({}) do |(key, value), result|
404
+ result[key.to_sym] = value
405
+ end
406
+ end
407
+ end
408
+ end
409
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../math/color"
4
+ require_relative "material"
5
+
6
+ module Three
7
+ class LineBasicMaterial < Material
8
+ attr_reader :color, :linewidth, :linecap, :linejoin, :fog
9
+
10
+ def initialize(parameters = nil)
11
+ super(nil)
12
+ @type = "LineBasicMaterial"
13
+ @color = Color.new(0xffffff)
14
+ @linewidth = 1
15
+ @linecap = "round"
16
+ @linejoin = "round"
17
+ @fog = true
18
+ bind_color_changes
19
+ set_values(parameters) if parameters
20
+ mark_dirty!
21
+ end
22
+
23
+ def color=(value)
24
+ @color = value.is_a?(Color) ? value : Color.new(value)
25
+ bind_color_changes
26
+ mark_dirty!(:parameters)
27
+ end
28
+
29
+ def linewidth=(value)
30
+ @linewidth = value
31
+ mark_dirty!(:parameters)
32
+ end
33
+
34
+ def linecap=(value)
35
+ @linecap = value
36
+ mark_dirty!(:parameters)
37
+ end
38
+
39
+ def linejoin=(value)
40
+ @linejoin = value
41
+ mark_dirty!(:parameters)
42
+ end
43
+
44
+ def fog=(value)
45
+ @fog = value
46
+ mark_dirty!(:parameters)
47
+ end
48
+
49
+ def to_h
50
+ super.merge(
51
+ color: @color.hex,
52
+ linewidth: @linewidth,
53
+ linecap: @linecap,
54
+ linejoin: @linejoin,
55
+ fog: @fog
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ def bind_color_changes
62
+ @color.on_change { mark_dirty!(:parameters) }
63
+ end
64
+ end
65
+ end