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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +153 -0
- data/docs/browser-runtime.md +153 -0
- data/docs/implementation-plan.md +874 -0
- data/docs/loaded-assets-design.md +400 -0
- data/docs/next-work.md +107 -0
- data/docs/publishing.md +64 -0
- data/docs/release-readiness.md +83 -0
- data/examples/browser/README.md +54 -0
- data/examples/browser/assets/animated_triangle.gltf +123 -0
- data/examples/browser/assets/checker.svg +11 -0
- data/examples/browser/assets/compressed_triangle.gltf +74 -0
- data/examples/browser/assets/studio.hdr +5 -0
- data/examples/browser/assets/triangle.gltf +67 -0
- data/examples/browser/composition/README.md +35 -0
- data/examples/browser/composition/boot.mjs +6 -0
- data/examples/browser/composition/index.html +136 -0
- data/examples/browser/composition/main.rb +216 -0
- data/examples/browser/composition/smoke_test.mjs +266 -0
- data/examples/browser/cube/README.md +41 -0
- data/examples/browser/cube/boot.mjs +6 -0
- data/examples/browser/cube/index.html +142 -0
- data/examples/browser/cube/main.rb +62 -0
- data/examples/browser/cube/smoke_test.mjs +99 -0
- data/examples/browser/cubemap/README.md +23 -0
- data/examples/browser/cubemap/boot.mjs +6 -0
- data/examples/browser/cubemap/index.html +142 -0
- data/examples/browser/cubemap/main.rb +84 -0
- data/examples/browser/cubemap/smoke_test.mjs +91 -0
- data/examples/browser/gltf/README.md +23 -0
- data/examples/browser/gltf/boot.mjs +6 -0
- data/examples/browser/gltf/index.html +142 -0
- data/examples/browser/gltf/main.rb +105 -0
- data/examples/browser/gltf/smoke_test.mjs +162 -0
- data/examples/browser/picking/README.md +33 -0
- data/examples/browser/picking/boot.mjs +6 -0
- data/examples/browser/picking/index.html +142 -0
- data/examples/browser/picking/main.rb +113 -0
- data/examples/browser/picking/smoke_test.mjs +78 -0
- data/examples/browser/postprocessing/README.md +26 -0
- data/examples/browser/postprocessing/boot.mjs +6 -0
- data/examples/browser/postprocessing/index.html +142 -0
- data/examples/browser/postprocessing/main.rb +117 -0
- data/examples/browser/postprocessing/smoke_test.mjs +121 -0
- data/examples/browser/primitives/README.md +33 -0
- data/examples/browser/primitives/boot.mjs +6 -0
- data/examples/browser/primitives/index.html +142 -0
- data/examples/browser/primitives/main.rb +116 -0
- data/examples/browser/primitives/smoke_test.mjs +113 -0
- data/examples/browser/serialization/README.md +33 -0
- data/examples/browser/serialization/boot.mjs +6 -0
- data/examples/browser/serialization/index.html +142 -0
- data/examples/browser/serialization/main.rb +97 -0
- data/examples/browser/serialization/smoke_test.mjs +67 -0
- data/examples/browser/shared/boot.mjs +79 -0
- data/examples/browser/shared/smoke_test_helpers.mjs +151 -0
- data/examples/browser/textures/README.md +35 -0
- data/examples/browser/textures/boot.mjs +6 -0
- data/examples/browser/textures/index.html +142 -0
- data/examples/browser/textures/main.rb +142 -0
- data/examples/browser/textures/smoke_test.mjs +189 -0
- data/lib/three/animation/animation_action.rb +57 -0
- data/lib/three/animation/animation_clip.rb +22 -0
- data/lib/three/animation/animation_mixer.rb +43 -0
- data/lib/three/backends/base.rb +87 -0
- data/lib/three/backends/threejs/materialization.rb +143 -0
- data/lib/three/backends/threejs/parameters.rb +97 -0
- data/lib/three/backends/threejs/resource_management.rb +69 -0
- data/lib/three/backends/threejs/ruby_wasm_adapter.rb +873 -0
- data/lib/three/backends/threejs/synchronization.rb +224 -0
- data/lib/three/backends/threejs.rb +365 -0
- data/lib/three/cameras/camera.rb +39 -0
- data/lib/three/cameras/orthographic_camera.rb +107 -0
- data/lib/three/cameras/perspective_camera.rb +137 -0
- data/lib/three/constants.rb +40 -0
- data/lib/three/controls/orbit_controls.rb +118 -0
- data/lib/three/core/buffer_attribute.rb +151 -0
- data/lib/three/core/buffer_geometry.rb +181 -0
- data/lib/three/core/clock.rb +58 -0
- data/lib/three/core/event_dispatcher.rb +57 -0
- data/lib/three/core/layers.rb +75 -0
- data/lib/three/core/object3d.rb +331 -0
- data/lib/three/core/raycaster.rb +73 -0
- data/lib/three/dirty.rb +58 -0
- data/lib/three/exporters/three_json_exporter.rb +187 -0
- data/lib/three/geometries/box_geometry.rb +97 -0
- data/lib/three/geometries/plane_geometry.rb +70 -0
- data/lib/three/geometries/sphere_geometry.rb +107 -0
- data/lib/three/lights/ambient_light.rb +12 -0
- data/lib/three/lights/directional_light.rb +38 -0
- data/lib/three/lights/hemisphere_light.rb +34 -0
- data/lib/three/lights/light.rb +85 -0
- data/lib/three/lights/point_light.rb +33 -0
- data/lib/three/loaders/cube_texture_loader.rb +13 -0
- data/lib/three/loaders/gltf_loader.rb +48 -0
- data/lib/three/loaders/rgbe_loader.rb +15 -0
- data/lib/three/loaders/texture_loader.rb +13 -0
- data/lib/three/loaders/three_json_loader.rb +409 -0
- data/lib/three/materials/line_basic_material.rb +65 -0
- data/lib/three/materials/material.rb +158 -0
- data/lib/three/materials/mesh_basic_material.rb +64 -0
- data/lib/three/materials/mesh_lambert_material.rb +71 -0
- data/lib/three/materials/mesh_matcap_material.rb +86 -0
- data/lib/three/materials/mesh_normal_material.rb +42 -0
- data/lib/three/materials/mesh_phong_material.rb +119 -0
- data/lib/three/materials/mesh_physical_material.rb +155 -0
- data/lib/three/materials/mesh_standard_material.rb +149 -0
- data/lib/three/materials/mesh_toon_material.rb +98 -0
- data/lib/three/materials/points_material.rb +74 -0
- data/lib/three/materials/shadow_material.rb +45 -0
- data/lib/three/materials/sprite_material.rb +75 -0
- data/lib/three/math/color.rb +133 -0
- data/lib/three/math/euler.rb +197 -0
- data/lib/three/math/math_utils.rb +36 -0
- data/lib/three/math/matrix3.rb +255 -0
- data/lib/three/math/matrix4.rb +448 -0
- data/lib/three/math/quaternion.rb +277 -0
- data/lib/three/math/vector2.rb +95 -0
- data/lib/three/math/vector3.rb +396 -0
- data/lib/three/objects/external_object3d.rb +28 -0
- data/lib/three/objects/group.rb +12 -0
- data/lib/three/objects/instanced_mesh.rb +110 -0
- data/lib/three/objects/line.rb +41 -0
- data/lib/three/objects/mesh.rb +45 -0
- data/lib/three/objects/points.rb +41 -0
- data/lib/three/objects/sprite.rb +57 -0
- data/lib/three/postprocessing/dot_screen_pass.rb +83 -0
- data/lib/three/postprocessing/effect_composer.rb +56 -0
- data/lib/three/postprocessing/output_pass.rb +40 -0
- data/lib/three/postprocessing/render_pass.rb +42 -0
- data/lib/three/postprocessing/unreal_bloom_pass.rb +56 -0
- data/lib/three/renderers/renderer.rb +11 -0
- data/lib/three/renderers/threejs_renderer.rb +85 -0
- data/lib/three/scenes/scene.rb +29 -0
- data/lib/three/textures/cube_texture.rb +72 -0
- data/lib/three/textures/rgbe_texture.rb +45 -0
- data/lib/three/textures/texture.rb +200 -0
- data/lib/three/version.rb +5 -0
- data/lib/three-rb.rb +3 -0
- data/lib/three.rb +77 -0
- data/package.json +30 -0
- data/pnpm-lock.yaml +86 -0
- metadata +216 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dirty"
|
|
4
|
+
require_relative "../math/math_utils"
|
|
5
|
+
require_relative "../math/vector3"
|
|
6
|
+
require_relative "buffer_attribute"
|
|
7
|
+
require_relative "event_dispatcher"
|
|
8
|
+
|
|
9
|
+
module Three
|
|
10
|
+
class BufferGeometry < EventDispatcher
|
|
11
|
+
include Dirty
|
|
12
|
+
|
|
13
|
+
@next_id = 0
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
attr_accessor :next_id
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :id, :uuid
|
|
20
|
+
attr_accessor :name, :type, :index, :attributes, :groups, :draw_range
|
|
21
|
+
attr_accessor :bounding_box, :bounding_sphere, :user_data
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
super()
|
|
25
|
+
@id = self.class.allocate_id
|
|
26
|
+
@uuid = MathUtils.generate_uuid
|
|
27
|
+
@name = ""
|
|
28
|
+
@type = "BufferGeometry"
|
|
29
|
+
@index = nil
|
|
30
|
+
@attributes = {}
|
|
31
|
+
@groups = []
|
|
32
|
+
@bounding_box = nil
|
|
33
|
+
@bounding_sphere = nil
|
|
34
|
+
@draw_range = { start: 0, count: Float::INFINITY }
|
|
35
|
+
@user_data = {}
|
|
36
|
+
mark_dirty!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.allocate_id
|
|
40
|
+
id = BufferGeometry.next_id
|
|
41
|
+
BufferGeometry.next_id += 1
|
|
42
|
+
id
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_index
|
|
46
|
+
@index
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_index(index)
|
|
50
|
+
@index =
|
|
51
|
+
if index.is_a?(Array)
|
|
52
|
+
attribute_class = index.any? { |value| value > 65_535 } ? Uint32BufferAttribute : Uint16BufferAttribute
|
|
53
|
+
attribute_class.new(index, 1)
|
|
54
|
+
else
|
|
55
|
+
index
|
|
56
|
+
end
|
|
57
|
+
bind_attribute_change(@index)
|
|
58
|
+
mark_dirty!(:index)
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def get_attribute(name)
|
|
63
|
+
@attributes[name.to_sym]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def set_attribute(name, attribute)
|
|
67
|
+
@attributes[name.to_sym] = attribute
|
|
68
|
+
bind_attribute_change(attribute)
|
|
69
|
+
mark_dirty!(:attributes)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def delete_attribute(name)
|
|
74
|
+
@attributes.delete(name.to_sym)
|
|
75
|
+
mark_dirty!(:attributes)
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def has_attribute?(name)
|
|
80
|
+
@attributes.key?(name.to_sym)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def add_group(start, count, material_index = 0)
|
|
84
|
+
@groups << { start: start, count: count, material_index: material_index }
|
|
85
|
+
mark_dirty!(:groups)
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def clear_groups
|
|
90
|
+
@groups.clear
|
|
91
|
+
mark_dirty!(:groups)
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def set_draw_range(start, count)
|
|
96
|
+
@draw_range[:start] = start
|
|
97
|
+
@draw_range[:count] = count
|
|
98
|
+
mark_dirty!(:draw_range)
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def compute_bounding_box
|
|
103
|
+
position = get_attribute(:position)
|
|
104
|
+
@bounding_box = nil
|
|
105
|
+
return self unless position && position.count.positive?
|
|
106
|
+
|
|
107
|
+
min = Vector3.new(Float::INFINITY, Float::INFINITY, Float::INFINITY)
|
|
108
|
+
max = Vector3.new(-Float::INFINITY, -Float::INFINITY, -Float::INFINITY)
|
|
109
|
+
|
|
110
|
+
position.count.times do |index|
|
|
111
|
+
x = position.get_x(index)
|
|
112
|
+
y = position.get_y(index)
|
|
113
|
+
z = position.get_z(index)
|
|
114
|
+
min.x = [min.x, x].min
|
|
115
|
+
min.y = [min.y, y].min
|
|
116
|
+
min.z = [min.z, z].min
|
|
117
|
+
max.x = [max.x, x].max
|
|
118
|
+
max.y = [max.y, y].max
|
|
119
|
+
max.z = [max.z, z].max
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@bounding_box = { min: min, max: max }
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def compute_bounding_sphere
|
|
127
|
+
compute_bounding_box unless @bounding_box
|
|
128
|
+
position = get_attribute(:position)
|
|
129
|
+
@bounding_sphere = nil
|
|
130
|
+
return self unless position && @bounding_box
|
|
131
|
+
|
|
132
|
+
center = @bounding_box[:min].clone.add(@bounding_box[:max]).multiply_scalar(0.5)
|
|
133
|
+
max_radius_sq = 0
|
|
134
|
+
|
|
135
|
+
position.count.times do |index|
|
|
136
|
+
point = Vector3.new(position.get_x(index), position.get_y(index), position.get_z(index))
|
|
137
|
+
max_radius_sq = [max_radius_sq, center.distance_to_squared(point)].max
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
@bounding_sphere = { center: center, radius: Math.sqrt(max_radius_sq) }
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def dispose
|
|
145
|
+
dispatch_event(:dispose)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def to_h
|
|
149
|
+
{
|
|
150
|
+
uuid: @uuid,
|
|
151
|
+
type: @type,
|
|
152
|
+
name: @name,
|
|
153
|
+
index: @index&.to_h,
|
|
154
|
+
attributes: @attributes.transform_values(&:to_h),
|
|
155
|
+
groups: @groups.map(&:dup),
|
|
156
|
+
bounding_box: serialize_bounds(@bounding_box),
|
|
157
|
+
bounding_sphere: serialize_sphere(@bounding_sphere)
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def serialize_bounds(bounds)
|
|
164
|
+
return nil unless bounds
|
|
165
|
+
|
|
166
|
+
{ min: bounds[:min].to_a, max: bounds[:max].to_a }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def serialize_sphere(sphere)
|
|
170
|
+
return nil unless sphere
|
|
171
|
+
|
|
172
|
+
{ center: sphere[:center].to_a, radius: sphere[:radius] }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def bind_attribute_change(attribute)
|
|
176
|
+
return unless attribute.respond_to?(:on)
|
|
177
|
+
|
|
178
|
+
attribute.on(:change) { mark_dirty!(:attributes) }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Three
|
|
4
|
+
class Clock
|
|
5
|
+
attr_accessor :auto_start
|
|
6
|
+
attr_reader :start_time, :old_time, :elapsed_time, :running
|
|
7
|
+
|
|
8
|
+
def initialize(auto_start: true, time_source: nil)
|
|
9
|
+
@auto_start = auto_start
|
|
10
|
+
@time_source = time_source || proc { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
11
|
+
@start_time = 0
|
|
12
|
+
@old_time = 0
|
|
13
|
+
@elapsed_time = 0
|
|
14
|
+
@running = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start
|
|
18
|
+
@start_time = now
|
|
19
|
+
@old_time = @start_time
|
|
20
|
+
@elapsed_time = 0
|
|
21
|
+
@running = true
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stop
|
|
26
|
+
get_elapsed_time
|
|
27
|
+
@running = false
|
|
28
|
+
@auto_start = false
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get_elapsed_time
|
|
33
|
+
get_delta
|
|
34
|
+
@elapsed_time
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_delta
|
|
38
|
+
if @auto_start && !@running
|
|
39
|
+
start
|
|
40
|
+
return 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return 0 unless @running
|
|
44
|
+
|
|
45
|
+
current_time = now
|
|
46
|
+
diff = current_time - @old_time
|
|
47
|
+
@old_time = current_time
|
|
48
|
+
@elapsed_time += diff
|
|
49
|
+
diff
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def now
|
|
55
|
+
@time_source.call.to_f
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Three
|
|
4
|
+
class EventDispatcher
|
|
5
|
+
Event = Struct.new(:type, :target, :data, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@listeners = Hash.new { |hash, key| hash[key] = [] }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_event_listener(type, listener = nil, &block)
|
|
12
|
+
callback = listener || block
|
|
13
|
+
raise ArgumentError, "listener or block is required" unless callback
|
|
14
|
+
|
|
15
|
+
listeners = @listeners[type.to_sym]
|
|
16
|
+
listeners << callback unless listeners.include?(callback)
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
alias on add_event_listener
|
|
21
|
+
|
|
22
|
+
def has_event_listener?(type, listener)
|
|
23
|
+
@listeners[type.to_sym].include?(listener)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def remove_event_listener(type, listener)
|
|
27
|
+
@listeners[type.to_sym].delete(listener)
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias off remove_event_listener
|
|
32
|
+
|
|
33
|
+
def dispatch_event(event, data = nil)
|
|
34
|
+
event = normalize_event(event, data)
|
|
35
|
+
listeners = @listeners[event.type.to_sym]
|
|
36
|
+
return self if listeners.empty?
|
|
37
|
+
|
|
38
|
+
event.target = self
|
|
39
|
+
listeners.dup.each { |listener| listener.call(event) }
|
|
40
|
+
event.target = nil
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def normalize_event(event, data)
|
|
47
|
+
case event
|
|
48
|
+
when Event
|
|
49
|
+
event
|
|
50
|
+
when Hash
|
|
51
|
+
Event.new(type: event.fetch(:type), data: event)
|
|
52
|
+
else
|
|
53
|
+
Event.new(type: event, data: data)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Three
|
|
4
|
+
class Layers
|
|
5
|
+
ALL_MASK = 0xffffffff
|
|
6
|
+
|
|
7
|
+
attr_reader :mask
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@mask = 1
|
|
11
|
+
@on_change_callback = proc {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mask=(value)
|
|
15
|
+
@mask = value.to_i
|
|
16
|
+
changed!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set(channel)
|
|
20
|
+
self.mask = bit(channel)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enable(channel)
|
|
25
|
+
self.mask = @mask | bit(channel)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def enable_all
|
|
30
|
+
self.mask = ALL_MASK
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def toggle(channel)
|
|
35
|
+
self.mask = @mask ^ bit(channel)
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def disable(channel)
|
|
40
|
+
self.mask = @mask & ~bit(channel)
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def disable_all
|
|
45
|
+
self.mask = 0
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test(layers)
|
|
50
|
+
(@mask & layers.mask) != 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def is_enabled(channel)
|
|
54
|
+
(@mask & bit(channel)) != 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def on_change(&callback)
|
|
58
|
+
@on_change_callback = callback || proc {}
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def bit(channel)
|
|
65
|
+
channel = Integer(channel)
|
|
66
|
+
raise RangeError, "layer channel must be between 0 and 31" unless channel.between?(0, 31)
|
|
67
|
+
|
|
68
|
+
1 << channel
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def changed!
|
|
72
|
+
@on_change_callback.call
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../math/euler"
|
|
4
|
+
require_relative "../math/math_utils"
|
|
5
|
+
require_relative "../math/matrix4"
|
|
6
|
+
require_relative "../math/quaternion"
|
|
7
|
+
require_relative "../math/vector3"
|
|
8
|
+
require_relative "../dirty"
|
|
9
|
+
require_relative "event_dispatcher"
|
|
10
|
+
require_relative "layers"
|
|
11
|
+
|
|
12
|
+
module Three
|
|
13
|
+
class Object3D < EventDispatcher
|
|
14
|
+
include Dirty
|
|
15
|
+
|
|
16
|
+
DEFAULT_UP = Vector3.new(0, 1, 0)
|
|
17
|
+
DEFAULT_MATRIX_AUTO_UPDATE = true
|
|
18
|
+
DEFAULT_MATRIX_WORLD_AUTO_UPDATE = true
|
|
19
|
+
|
|
20
|
+
@next_id = 0
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
attr_accessor :next_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :id, :uuid, :parent, :children
|
|
27
|
+
attr_reader :layers
|
|
28
|
+
attr_reader :position, :rotation, :quaternion, :scale
|
|
29
|
+
attr_reader :matrix, :matrix_world
|
|
30
|
+
attr_reader :name, :type, :up, :visible, :cast_shadow, :receive_shadow
|
|
31
|
+
attr_accessor :matrix_auto_update, :matrix_world_auto_update, :matrix_world_needs_update
|
|
32
|
+
attr_accessor :user_data
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
super
|
|
36
|
+
|
|
37
|
+
@id = self.class.allocate_id
|
|
38
|
+
@uuid = MathUtils.generate_uuid
|
|
39
|
+
@name = ""
|
|
40
|
+
@type = "Object3D"
|
|
41
|
+
@parent = nil
|
|
42
|
+
@children = []
|
|
43
|
+
@layers = Layers.new
|
|
44
|
+
@up = DEFAULT_UP.clone
|
|
45
|
+
|
|
46
|
+
@position = Vector3.new
|
|
47
|
+
@rotation = Euler.new
|
|
48
|
+
@quaternion = Quaternion.new
|
|
49
|
+
@scale = Vector3.new(1, 1, 1)
|
|
50
|
+
@matrix = Matrix4.new
|
|
51
|
+
@matrix_world = Matrix4.new
|
|
52
|
+
@matrix_auto_update = DEFAULT_MATRIX_AUTO_UPDATE
|
|
53
|
+
@matrix_world_auto_update = DEFAULT_MATRIX_WORLD_AUTO_UPDATE
|
|
54
|
+
@matrix_world_needs_update = false
|
|
55
|
+
@visible = true
|
|
56
|
+
@cast_shadow = false
|
|
57
|
+
@receive_shadow = false
|
|
58
|
+
@user_data = {}
|
|
59
|
+
|
|
60
|
+
bind_rotation_and_quaternion
|
|
61
|
+
bind_transform_changes
|
|
62
|
+
bind_layer_changes
|
|
63
|
+
mark_dirty!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def name=(value)
|
|
67
|
+
@name = value
|
|
68
|
+
mark_dirty!(:properties)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def type=(value)
|
|
72
|
+
@type = value
|
|
73
|
+
mark_dirty!(:properties)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def up=(value)
|
|
77
|
+
@up = value
|
|
78
|
+
mark_dirty!(:transform)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visible=(value)
|
|
82
|
+
@visible = value
|
|
83
|
+
mark_dirty!(:properties)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def cast_shadow=(value)
|
|
87
|
+
@cast_shadow = value
|
|
88
|
+
mark_dirty!(:properties)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def receive_shadow=(value)
|
|
92
|
+
@receive_shadow = value
|
|
93
|
+
mark_dirty!(:properties)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def mark_dirty!(field = :all)
|
|
97
|
+
super
|
|
98
|
+
@parent&.mark_descendant_dirty!
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def mark_descendant_dirty!
|
|
103
|
+
return self if dirty_fields.key?(:descendants)
|
|
104
|
+
|
|
105
|
+
dirty_fields[:descendants] = true
|
|
106
|
+
@parent&.mark_descendant_dirty!
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def dirty_dependency_changed(_resource, _field)
|
|
111
|
+
mark_dirty!(:resources)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.allocate_id
|
|
115
|
+
id = Object3D.next_id
|
|
116
|
+
Object3D.next_id += 1
|
|
117
|
+
id
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def add(*objects)
|
|
121
|
+
objects.each { |object| add_one(object) }
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def remove(*objects)
|
|
126
|
+
objects.each { |object| remove_one(object) }
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def remove_from_parent
|
|
131
|
+
@parent&.remove(self)
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def clear
|
|
136
|
+
remove(*@children.dup)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def traverse(&block)
|
|
140
|
+
return enum_for(:traverse) unless block
|
|
141
|
+
|
|
142
|
+
block.call(self)
|
|
143
|
+
@children.each { |child| child.traverse(&block) }
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def traverse_visible(&block)
|
|
148
|
+
return enum_for(:traverse_visible) unless block
|
|
149
|
+
return self unless @visible
|
|
150
|
+
|
|
151
|
+
block.call(self)
|
|
152
|
+
@children.each { |child| child.traverse_visible(&block) }
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def traverse_ancestors(&block)
|
|
157
|
+
return enum_for(:traverse_ancestors) unless block
|
|
158
|
+
return self unless @parent
|
|
159
|
+
|
|
160
|
+
block.call(@parent)
|
|
161
|
+
@parent.traverse_ancestors(&block)
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def get_object_by_id(id)
|
|
166
|
+
get_object_by_property(:id, id)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def get_object_by_name(name)
|
|
170
|
+
get_object_by_property(:name, name)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def get_object_by_property(property, value)
|
|
174
|
+
return self if public_send(property) == value
|
|
175
|
+
|
|
176
|
+
@children.each do |child|
|
|
177
|
+
object = child.get_object_by_property(property, value)
|
|
178
|
+
return object if object
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def get_objects_by_property(property, value, result = [])
|
|
185
|
+
result << self if public_send(property) == value
|
|
186
|
+
@children.each { |child| child.get_objects_by_property(property, value, result) }
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def update_matrix
|
|
191
|
+
@matrix.compose(@position, @quaternion, @scale)
|
|
192
|
+
@matrix_world_needs_update = true
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def update_matrix_world(force = false)
|
|
197
|
+
update_matrix if @matrix_auto_update
|
|
198
|
+
|
|
199
|
+
if @matrix_world_needs_update || force
|
|
200
|
+
if @matrix_world_auto_update
|
|
201
|
+
if @parent
|
|
202
|
+
@matrix_world.multiply_matrices(@parent.matrix_world, @matrix)
|
|
203
|
+
else
|
|
204
|
+
@matrix_world.copy(@matrix)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@matrix_world_needs_update = false
|
|
209
|
+
force = true
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
@children.each { |child| child.update_matrix_world(force) }
|
|
213
|
+
self
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def update_world_matrix(update_parents = false, update_children = false)
|
|
217
|
+
@parent&.update_world_matrix(true, false) if update_parents
|
|
218
|
+
update_matrix if @matrix_auto_update
|
|
219
|
+
|
|
220
|
+
if @matrix_world_auto_update
|
|
221
|
+
if @parent
|
|
222
|
+
@matrix_world.multiply_matrices(@parent.matrix_world, @matrix)
|
|
223
|
+
else
|
|
224
|
+
@matrix_world.copy(@matrix)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@children.each { |child| child.update_world_matrix(false, true) } if update_children
|
|
229
|
+
self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def get_world_position(target = Vector3.new)
|
|
233
|
+
update_world_matrix(true, false)
|
|
234
|
+
target.set_from_matrix_position(@matrix_world)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def get_world_quaternion(target = Quaternion.new)
|
|
238
|
+
update_world_matrix(true, false)
|
|
239
|
+
@matrix_world.decompose(Vector3.new, target, Vector3.new)
|
|
240
|
+
target
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def get_world_scale(target = Vector3.new)
|
|
244
|
+
update_world_matrix(true, false)
|
|
245
|
+
@matrix_world.decompose(Vector3.new, Quaternion.new, target)
|
|
246
|
+
target
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def get_world_direction(target = Vector3.new)
|
|
250
|
+
update_world_matrix(true, false)
|
|
251
|
+
elements = @matrix_world.elements
|
|
252
|
+
target.set(elements[8], elements[9], elements[10]).normalize
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def to_h
|
|
256
|
+
{
|
|
257
|
+
uuid: @uuid,
|
|
258
|
+
type: @type,
|
|
259
|
+
name: @name,
|
|
260
|
+
matrix: @matrix.to_a,
|
|
261
|
+
layers: @layers.mask,
|
|
262
|
+
visible: @visible,
|
|
263
|
+
cast_shadow: @cast_shadow,
|
|
264
|
+
receive_shadow: @receive_shadow,
|
|
265
|
+
user_data: @user_data,
|
|
266
|
+
children: @children.map(&:to_h)
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def to_json(*args)
|
|
271
|
+
Exporters::ThreeJSONExporter.new.to_json(self, *args)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
protected
|
|
275
|
+
|
|
276
|
+
attr_writer :parent
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
def add_one(object)
|
|
281
|
+
raise ArgumentError, "object cannot be added as a child of itself" if object.equal?(self)
|
|
282
|
+
raise TypeError, "object must be a Three::Object3D" unless object.is_a?(Object3D)
|
|
283
|
+
|
|
284
|
+
object.remove_from_parent
|
|
285
|
+
object.parent = self
|
|
286
|
+
@children << object
|
|
287
|
+
mark_dirty!(:children)
|
|
288
|
+
object.mark_dirty!(:transform)
|
|
289
|
+
object.dispatch_event(:added)
|
|
290
|
+
dispatch_event(:childadded, object)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def remove_one(object)
|
|
294
|
+
return unless @children.include?(object)
|
|
295
|
+
|
|
296
|
+
@children.delete(object)
|
|
297
|
+
object.parent = nil
|
|
298
|
+
mark_dirty!(:children)
|
|
299
|
+
object.mark_dirty!(:transform)
|
|
300
|
+
object.dispatch_event(:removed)
|
|
301
|
+
dispatch_event(:childremoved, object)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def bind_rotation_and_quaternion
|
|
305
|
+
@rotation.on_change do
|
|
306
|
+
@quaternion.set_from_euler(@rotation, update: false)
|
|
307
|
+
@matrix_world_needs_update = true
|
|
308
|
+
mark_dirty!(:transform)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
@quaternion.on_change do
|
|
312
|
+
@rotation.set_from_quaternion(@quaternion, @rotation.order, update: false)
|
|
313
|
+
@matrix_world_needs_update = true
|
|
314
|
+
mark_dirty!(:transform)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def bind_transform_changes
|
|
319
|
+
[@position, @scale].each do |vector|
|
|
320
|
+
vector.on_change do
|
|
321
|
+
@matrix_world_needs_update = true
|
|
322
|
+
mark_dirty!(:transform)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def bind_layer_changes
|
|
328
|
+
@layers.on_change { mark_dirty!(:properties) }
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|