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,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../math/vector2"
4
+ require_relative "../math/vector3"
5
+ require_relative "../backends/threejs"
6
+
7
+ module Three
8
+ class Raycaster
9
+ class Intersection
10
+ attr_reader :distance, :point, :object, :object_handle, :uv, :face_index, :index, :instance_id, :raw
11
+
12
+ def initialize(distance:, point:, object:, object_handle:, uv: nil, face_index: nil, index: nil, instance_id: nil, raw: nil)
13
+ @distance = distance
14
+ @point = point
15
+ @object = object
16
+ @object_handle = object_handle
17
+ @uv = uv
18
+ @face_index = face_index
19
+ @index = index
20
+ @instance_id = instance_id
21
+ @raw = raw
22
+ end
23
+ end
24
+
25
+ attr_reader :backend, :handle
26
+
27
+ def initialize(backend: Backends::ThreeJS.new)
28
+ @backend = backend
29
+ @handle = @backend.create_raycaster
30
+ end
31
+
32
+ def set_from_camera(coords, camera)
33
+ @backend.set_raycaster_from_camera(@handle, coerce_vector2(coords), camera)
34
+ self
35
+ end
36
+
37
+ def intersect_object(object, recursive: false)
38
+ intersect_objects([object], recursive: recursive)
39
+ end
40
+
41
+ def intersect_objects(objects, recursive: false)
42
+ @backend.intersect_objects(@handle, objects, recursive: recursive).map do |entry|
43
+ build_intersection(entry)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def build_intersection(entry)
50
+ Intersection.new(
51
+ distance: entry[:distance],
52
+ point: entry[:point] ? Vector3.new(*entry[:point]) : nil,
53
+ object: entry[:object],
54
+ object_handle: entry[:object_handle],
55
+ uv: entry[:uv] ? Vector2.new(*entry[:uv]) : nil,
56
+ face_index: entry[:face_index],
57
+ index: entry[:index],
58
+ instance_id: entry[:instance_id],
59
+ raw: entry[:raw]
60
+ )
61
+ end
62
+
63
+ def coerce_vector2(value)
64
+ return value.to_a if value.is_a?(Vector2)
65
+
66
+ array = value.to_ary if value.respond_to?(:to_ary)
67
+ array ||= value.to_a if value.respond_to?(:to_a)
68
+ raise TypeError, "coords must be a Three::Vector2 or an array-like [x, y]" unless array&.length == 2
69
+
70
+ array
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Three
4
+ module Dirty
5
+ def dirty?
6
+ !dirty_fields.empty?
7
+ end
8
+
9
+ def dirty_fields
10
+ @dirty_fields ||= {}
11
+ end
12
+
13
+ def dirty_field?(field)
14
+ dirty_fields.key?(:all) || dirty_fields.key?(field)
15
+ end
16
+
17
+ def dirty_dependents
18
+ @dirty_dependents ||= []
19
+ end
20
+
21
+ def add_dirty_dependent(dependent)
22
+ dirty_dependents << dependent unless dirty_dependents.include?(dependent)
23
+ self
24
+ end
25
+
26
+ def remove_dirty_dependent(dependent)
27
+ dirty_dependents.delete(dependent)
28
+ self
29
+ end
30
+
31
+ def mark_dirty!(field = :all)
32
+ dirty_fields[field] = true
33
+ notify_dirty_dependents(field)
34
+ self
35
+ end
36
+
37
+ def mark_clean!(*fields)
38
+ if fields.empty? || fields.include?(:all)
39
+ dirty_fields.clear
40
+ else
41
+ fields.each { |field| dirty_fields.delete(field) }
42
+ end
43
+ self
44
+ end
45
+
46
+ private
47
+
48
+ def notify_dirty_dependents(field)
49
+ dirty_dependents.dup.each do |dependent|
50
+ if dependent.respond_to?(:dirty_dependency_changed)
51
+ dependent.dirty_dependency_changed(self, field)
52
+ elsif dependent.respond_to?(:mark_dirty!)
53
+ dependent.mark_dirty!(:dependency)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Three
6
+ module Exporters
7
+ class ThreeJSONExporter
8
+ FORMAT_VERSION = 1
9
+
10
+ def initialize(deterministic_ids: false)
11
+ @deterministic_ids = deterministic_ids
12
+ end
13
+
14
+ def export(object)
15
+ raise TypeError, "object must be a Three::Object3D" unless object.is_a?(Object3D)
16
+
17
+ reset!
18
+ {
19
+ metadata: {
20
+ version: FORMAT_VERSION,
21
+ generator: "three-rb",
22
+ type: "Object"
23
+ },
24
+ object: serialize_object(object),
25
+ geometries: @geometries.values,
26
+ materials: @materials.values,
27
+ textures: @textures.values
28
+ }
29
+ end
30
+
31
+ def to_json(object, *args)
32
+ export(object).to_json(*args)
33
+ end
34
+
35
+ private
36
+
37
+ def reset!
38
+ @geometries = {}
39
+ @materials = {}
40
+ @textures = {}
41
+ @stable_ids = {}
42
+ @stable_id_counts = Hash.new(0)
43
+ end
44
+
45
+ def serialize_object(object)
46
+ data = {
47
+ uuid: export_id(object, :object),
48
+ type: object.type,
49
+ name: object.name,
50
+ visible: object.visible,
51
+ layers: object.layers.mask,
52
+ cast_shadow: object.cast_shadow,
53
+ receive_shadow: object.receive_shadow,
54
+ matrix_auto_update: object.matrix_auto_update,
55
+ position: object.position.to_a,
56
+ quaternion: object.quaternion.to_a,
57
+ scale: object.scale.to_a,
58
+ user_data: object.user_data,
59
+ children: object.children.map { |child| serialize_object(child) }
60
+ }
61
+
62
+ serialize_scene(object, data) if object.is_a?(Scene)
63
+ serialize_camera(object, data) if object.is_a?(Camera)
64
+ serialize_light(object, data) if object.is_a?(Light)
65
+ serialize_geometry_material_object(object, data) if geometry_material_object?(object)
66
+ data[:external] = true if object.is_a?(ExternalObject3D)
67
+
68
+ data
69
+ end
70
+
71
+ def serialize_scene(scene, data)
72
+ data[:background] = serialize_texture_reference(scene.background)
73
+ data[:environment] = serialize_texture_reference(scene.environment)
74
+ end
75
+
76
+ def serialize_camera(camera, data)
77
+ data[:zoom] = camera.zoom if camera.respond_to?(:zoom)
78
+
79
+ case camera
80
+ when PerspectiveCamera
81
+ data[:fov] = camera.fov
82
+ data[:aspect] = camera.aspect
83
+ data[:near] = camera.near
84
+ data[:far] = camera.far
85
+ when OrthographicCamera
86
+ data[:left] = camera.left
87
+ data[:right] = camera.right
88
+ data[:top] = camera.top
89
+ data[:bottom] = camera.bottom
90
+ data[:near] = camera.near
91
+ data[:far] = camera.far
92
+ end
93
+ end
94
+
95
+ def serialize_light(light, data)
96
+ data[:color] = light.color.hex
97
+ data[:intensity] = light.intensity
98
+ data[:shadow_map_size] = light.shadow_map_size.dup
99
+ data[:shadow_bias] = light.shadow_bias
100
+ data[:shadow_normal_bias] = light.shadow_normal_bias
101
+ data[:shadow_radius] = light.shadow_radius
102
+ data[:shadow_camera] = light.shadow_camera.dup if light.respond_to?(:shadow_camera)
103
+ data[:distance] = light.distance if light.respond_to?(:distance)
104
+ data[:decay] = light.decay if light.respond_to?(:decay)
105
+ data[:ground_color] = light.ground_color.hex if light.respond_to?(:ground_color)
106
+ end
107
+
108
+ def geometry_material_object?(object)
109
+ object.is_a?(Mesh) || object.is_a?(Line) || object.is_a?(Points) || object.is_a?(Sprite)
110
+ end
111
+
112
+ def serialize_geometry_material_object(object, data)
113
+ data[:geometry] = register_geometry(object.geometry) if object.respond_to?(:geometry)
114
+ data[:material] = register_material(object.material)
115
+ data[:center] = object.center.to_a if object.is_a?(Sprite)
116
+
117
+ return unless object.is_a?(InstancedMesh)
118
+
119
+ data[:count] = object.count
120
+ data[:capacity] = object.capacity
121
+ data[:instance_matrices] = object.instance_matrices.map(&:to_a)
122
+ data[:instance_colors] = object.instance_colors&.map(&:to_a)
123
+ end
124
+
125
+ def register_geometry(geometry)
126
+ return nil unless geometry&.respond_to?(:uuid)
127
+
128
+ @geometries[geometry.uuid] ||= serialize_geometry(geometry)
129
+ export_id(geometry, :geometry)
130
+ end
131
+
132
+ def serialize_geometry(geometry)
133
+ data = geometry.to_h
134
+ data[:uuid] = export_id(geometry, :geometry)
135
+ data[:parameters] = geometry.parameters.dup if geometry.respond_to?(:parameters)
136
+ data
137
+ end
138
+
139
+ def register_material(material)
140
+ return material.map { |entry| register_material(entry) } if material.is_a?(Array)
141
+ return nil unless material&.respond_to?(:uuid)
142
+
143
+ @materials[material.uuid] ||= serialize_material(material)
144
+ export_id(material, :material)
145
+ end
146
+
147
+ def serialize_material(material)
148
+ data = material.to_h
149
+ data[:uuid] = export_id(material, :material)
150
+ material.textures.each { |texture| register_texture(texture) } if material.respond_to?(:textures)
151
+
152
+ return data unless material.respond_to?(:texture_slots)
153
+
154
+ material.texture_slots.each do |slot|
155
+ data[slot] = serialize_texture_reference(material.public_send(slot))
156
+ end
157
+ data
158
+ end
159
+
160
+ def serialize_texture_reference(texture)
161
+ return nil unless texture
162
+ return texture unless texture.respond_to?(:uuid)
163
+
164
+ register_texture(texture)
165
+ end
166
+
167
+ def register_texture(texture)
168
+ @textures[texture.uuid] ||= serialize_texture(texture)
169
+ export_id(texture, :texture)
170
+ end
171
+
172
+ def serialize_texture(texture)
173
+ texture.to_h.merge(uuid: export_id(texture, :texture))
174
+ end
175
+
176
+ def export_id(resource, prefix)
177
+ return resource.uuid unless @deterministic_ids
178
+
179
+ @stable_ids[resource.uuid] ||= begin
180
+ index = @stable_id_counts[prefix]
181
+ @stable_id_counts[prefix] += 1
182
+ "#{prefix}-#{index}"
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/buffer_attribute"
4
+ require_relative "../core/buffer_geometry"
5
+
6
+ module Three
7
+ class BoxGeometry < BufferGeometry
8
+ attr_accessor :parameters
9
+
10
+ def initialize(width = 1, height = 1, depth = 1, width_segments: 1, height_segments: 1, depth_segments: 1)
11
+ super()
12
+ @type = "BoxGeometry"
13
+ @parameters = {
14
+ width: width,
15
+ height: height,
16
+ depth: depth,
17
+ width_segments: width_segments,
18
+ height_segments: height_segments,
19
+ depth_segments: depth_segments
20
+ }
21
+
22
+ build(width, height, depth, width_segments.floor, height_segments.floor, depth_segments.floor)
23
+ end
24
+
25
+ private
26
+
27
+ def build(width, height, depth, width_segments, height_segments, depth_segments)
28
+ indices = []
29
+ vertices = []
30
+ normals = []
31
+ uvs = []
32
+ state = { vertex_count: 0, group_start: 0 }
33
+
34
+ build_plane(indices, vertices, normals, uvs, state, "z", "y", "x", -1, -1, depth, height, width, depth_segments, height_segments, 0)
35
+ build_plane(indices, vertices, normals, uvs, state, "z", "y", "x", 1, -1, depth, height, -width, depth_segments, height_segments, 1)
36
+ build_plane(indices, vertices, normals, uvs, state, "x", "z", "y", 1, 1, width, depth, height, width_segments, depth_segments, 2)
37
+ build_plane(indices, vertices, normals, uvs, state, "x", "z", "y", 1, -1, width, depth, -height, width_segments, depth_segments, 3)
38
+ build_plane(indices, vertices, normals, uvs, state, "x", "y", "z", 1, -1, width, height, depth, width_segments, height_segments, 4)
39
+ build_plane(indices, vertices, normals, uvs, state, "x", "y", "z", -1, -1, width, height, -depth, width_segments, height_segments, 5)
40
+
41
+ set_index(indices)
42
+ set_attribute(:position, Float32BufferAttribute.new(vertices, 3))
43
+ set_attribute(:normal, Float32BufferAttribute.new(normals, 3))
44
+ set_attribute(:uv, Float32BufferAttribute.new(uvs, 2))
45
+ end
46
+
47
+ # Plane construction follows three.js BoxGeometry's MIT-licensed algorithm.
48
+ def build_plane(indices, vertices, normals, uvs, state, u, v, w, udir, vdir, width, height, depth, grid_x, grid_y, material_index)
49
+ segment_width = width.to_f / grid_x
50
+ segment_height = height.to_f / grid_y
51
+ width_half = width / 2.0
52
+ height_half = height / 2.0
53
+ depth_half = depth / 2.0
54
+ grid_x1 = grid_x + 1
55
+ grid_y1 = grid_y + 1
56
+ vertex_counter = 0
57
+ group_count = 0
58
+
59
+ grid_y1.times do |iy|
60
+ y = iy * segment_height - height_half
61
+
62
+ grid_x1.times do |ix|
63
+ x = ix * segment_width - width_half
64
+ vector = { "x" => 0, "y" => 0, "z" => 0 }
65
+ vector[u] = x * udir
66
+ vector[v] = y * vdir
67
+ vector[w] = depth_half
68
+ vertices.push(vector["x"], vector["y"], vector["z"])
69
+
70
+ vector[u] = 0
71
+ vector[v] = 0
72
+ vector[w] = depth.positive? ? 1 : -1
73
+ normals.push(vector["x"], vector["y"], vector["z"])
74
+
75
+ uvs.push(ix.to_f / grid_x, 1 - (iy.to_f / grid_y))
76
+ vertex_counter += 1
77
+ end
78
+ end
79
+
80
+ grid_y.times do |iy|
81
+ grid_x.times do |ix|
82
+ a = state[:vertex_count] + ix + grid_x1 * iy
83
+ b = state[:vertex_count] + ix + grid_x1 * (iy + 1)
84
+ c = state[:vertex_count] + (ix + 1) + grid_x1 * (iy + 1)
85
+ d = state[:vertex_count] + (ix + 1) + grid_x1 * iy
86
+
87
+ indices.push(a, b, d, b, c, d)
88
+ group_count += 6
89
+ end
90
+ end
91
+
92
+ add_group(state[:group_start], group_count, material_index)
93
+ state[:group_start] += group_count
94
+ state[:vertex_count] += vertex_counter
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/buffer_attribute"
4
+ require_relative "../core/buffer_geometry"
5
+
6
+ module Three
7
+ class PlaneGeometry < BufferGeometry
8
+ attr_accessor :parameters
9
+
10
+ def initialize(width = 1, height = 1, width_segments: 1, height_segments: 1)
11
+ super()
12
+ @type = "PlaneGeometry"
13
+ @parameters = {
14
+ width: width,
15
+ height: height,
16
+ width_segments: width_segments,
17
+ height_segments: height_segments
18
+ }
19
+
20
+ build(width, height, width_segments.floor, height_segments.floor)
21
+ end
22
+
23
+ private
24
+
25
+ # Plane construction follows three.js PlaneGeometry's MIT-licensed algorithm.
26
+ def build(width, height, width_segments, height_segments)
27
+ width_half = width / 2.0
28
+ height_half = height / 2.0
29
+ grid_x = width_segments
30
+ grid_y = height_segments
31
+ grid_x1 = grid_x + 1
32
+ grid_y1 = grid_y + 1
33
+ segment_width = width.to_f / grid_x
34
+ segment_height = height.to_f / grid_y
35
+
36
+ indices = []
37
+ vertices = []
38
+ normals = []
39
+ uvs = []
40
+
41
+ grid_y1.times do |iy|
42
+ y = iy * segment_height - height_half
43
+
44
+ grid_x1.times do |ix|
45
+ x = ix * segment_width - width_half
46
+
47
+ vertices.push(x, -y, 0)
48
+ normals.push(0, 0, 1)
49
+ uvs.push(ix.to_f / grid_x, 1 - (iy.to_f / grid_y))
50
+ end
51
+ end
52
+
53
+ grid_y.times do |iy|
54
+ grid_x.times do |ix|
55
+ a = ix + grid_x1 * iy
56
+ b = ix + grid_x1 * (iy + 1)
57
+ c = (ix + 1) + grid_x1 * (iy + 1)
58
+ d = (ix + 1) + grid_x1 * iy
59
+
60
+ indices.push(a, b, d, b, c, d)
61
+ end
62
+ end
63
+
64
+ set_index(indices)
65
+ set_attribute(:position, Float32BufferAttribute.new(vertices, 3))
66
+ set_attribute(:normal, Float32BufferAttribute.new(normals, 3))
67
+ set_attribute(:uv, Float32BufferAttribute.new(uvs, 2))
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/buffer_attribute"
4
+ require_relative "../core/buffer_geometry"
5
+
6
+ module Three
7
+ class SphereGeometry < BufferGeometry
8
+ attr_accessor :parameters
9
+
10
+ def initialize(radius = 1, width_segments: 32, height_segments: 16, phi_start: 0, phi_length: Math::PI * 2, theta_start: 0, theta_length: Math::PI)
11
+ super()
12
+ @type = "SphereGeometry"
13
+ @parameters = {
14
+ radius: radius,
15
+ width_segments: width_segments,
16
+ height_segments: height_segments,
17
+ phi_start: phi_start,
18
+ phi_length: phi_length,
19
+ theta_start: theta_start,
20
+ theta_length: theta_length
21
+ }
22
+
23
+ build(
24
+ radius,
25
+ [3, width_segments.floor].max,
26
+ [2, height_segments.floor].max,
27
+ phi_start,
28
+ phi_length,
29
+ theta_start,
30
+ theta_length
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ # Sphere construction follows three.js SphereGeometry's MIT-licensed algorithm.
37
+ def build(radius, width_segments, height_segments, phi_start, phi_length, theta_start, theta_length)
38
+ theta_end = [theta_start + theta_length, Math::PI].min
39
+ index = 0
40
+ grid = []
41
+ indices = []
42
+ vertices = []
43
+ normals = []
44
+ uvs = []
45
+
46
+ (height_segments + 1).times do |iy|
47
+ vertices_row = []
48
+ v = iy.to_f / height_segments
49
+ u_offset = uv_offset(iy, height_segments, width_segments, theta_start, theta_end)
50
+
51
+ (width_segments + 1).times do |ix|
52
+ u = ix.to_f / width_segments
53
+ theta = theta_start + v * theta_length
54
+ phi = phi_start + u * phi_length
55
+
56
+ x = -radius * Math.cos(phi) * Math.sin(theta)
57
+ y = radius * Math.cos(theta)
58
+ z = radius * Math.sin(phi) * Math.sin(theta)
59
+
60
+ vertices.push(x, y, z)
61
+ push_normal(normals, x, y, z)
62
+ uvs.push(u + u_offset, 1 - v)
63
+ vertices_row << index
64
+ index += 1
65
+ end
66
+
67
+ grid << vertices_row
68
+ end
69
+
70
+ height_segments.times do |iy|
71
+ width_segments.times do |ix|
72
+ a = grid[iy][ix + 1]
73
+ b = grid[iy][ix]
74
+ c = grid[iy + 1][ix]
75
+ d = grid[iy + 1][ix + 1]
76
+
77
+ indices.push(a, b, d) if iy != 0 || theta_start.positive?
78
+ indices.push(b, c, d) if iy != height_segments - 1 || theta_end < Math::PI
79
+ end
80
+ end
81
+
82
+ set_index(indices)
83
+ set_attribute(:position, Float32BufferAttribute.new(vertices, 3))
84
+ set_attribute(:normal, Float32BufferAttribute.new(normals, 3))
85
+ set_attribute(:uv, Float32BufferAttribute.new(uvs, 2))
86
+ end
87
+
88
+ def uv_offset(iy, height_segments, width_segments, theta_start, theta_end)
89
+ if iy.zero? && theta_start.zero?
90
+ 0.5 / width_segments
91
+ elsif iy == height_segments && theta_end == Math::PI
92
+ -0.5 / width_segments
93
+ else
94
+ 0
95
+ end
96
+ end
97
+
98
+ def push_normal(normals, x, y, z)
99
+ length = Math.sqrt((x * x) + (y * y) + (z * z))
100
+ if length.zero?
101
+ normals.push(0, 0, 0)
102
+ else
103
+ normals.push(x / length, y / length, z / length)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "light"
4
+
5
+ module Three
6
+ class AmbientLight < Light
7
+ def initialize(color = 0xffffff, intensity = 1)
8
+ super
9
+ @type = "AmbientLight"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "light"
4
+
5
+ module Three
6
+ class DirectionalLight < Light
7
+ attr_reader :shadow_camera
8
+
9
+ def initialize(color = 0xffffff, intensity = 1)
10
+ super
11
+ @type = "DirectionalLight"
12
+ @position.copy(Object3D::DEFAULT_UP)
13
+ @shadow_camera = {
14
+ left: -5,
15
+ right: 5,
16
+ top: 5,
17
+ bottom: -5,
18
+ near: 0.5,
19
+ far: 500
20
+ }
21
+ end
22
+
23
+ def set_shadow_camera(left: nil, right: nil, top: nil, bottom: nil, near: nil, far: nil)
24
+ @shadow_camera[:left] = left unless left.nil?
25
+ @shadow_camera[:right] = right unless right.nil?
26
+ @shadow_camera[:top] = top unless top.nil?
27
+ @shadow_camera[:bottom] = bottom unless bottom.nil?
28
+ @shadow_camera[:near] = near unless near.nil?
29
+ @shadow_camera[:far] = far unless far.nil?
30
+ mark_dirty!(:shadow)
31
+ self
32
+ end
33
+
34
+ def to_h
35
+ super.merge(shadow_camera: @shadow_camera.dup)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "light"
4
+
5
+ module Three
6
+ class HemisphereLight < Light
7
+ attr_reader :ground_color
8
+
9
+ def initialize(sky_color = 0xffffff, ground_color = 0xffffff, intensity = 1)
10
+ super(sky_color, intensity)
11
+ @type = "HemisphereLight"
12
+ @ground_color = Color.new(ground_color)
13
+ bind_ground_color_changes
14
+ end
15
+
16
+ def ground_color=(value)
17
+ @ground_color = value.is_a?(Color) ? value : Color.new(value)
18
+ bind_ground_color_changes
19
+ mark_dirty!(:light)
20
+ end
21
+
22
+ def to_h
23
+ super.merge(
24
+ ground_color: @ground_color.hex
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def bind_ground_color_changes
31
+ @ground_color.on_change { mark_dirty!(:light) }
32
+ end
33
+ end
34
+ end