three-rb 0.1.0 → 0.2.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +64 -3
  4. data/docs/browser-runtime.md +90 -24
  5. data/docs/next-work.md +9 -5
  6. data/docs/publishing.md +116 -23
  7. data/docs/release-readiness.md +5 -3
  8. data/docs/standalone-browser-app.md +102 -0
  9. data/examples/browser/README.md +6 -0
  10. data/examples/browser/composition/main.rb +41 -61
  11. data/examples/browser/cube/main.rb +4 -34
  12. data/examples/browser/cubemap/main.rb +16 -39
  13. data/examples/browser/gltf/main.rb +28 -48
  14. data/examples/browser/picking/main.rb +27 -53
  15. data/examples/browser/postprocessing/main.rb +23 -42
  16. data/examples/browser/primitives/main.rb +18 -41
  17. data/examples/browser/ruby/README.md +22 -0
  18. data/examples/browser/ruby/boot.mjs +6 -0
  19. data/examples/browser/ruby/index.html +142 -0
  20. data/examples/browser/ruby/main.rb +313 -0
  21. data/examples/browser/ruby/smoke_test.mjs +126 -0
  22. data/examples/browser/serialization/main.rb +19 -41
  23. data/examples/browser/shared/boot.mjs +37 -5
  24. data/examples/browser/textures/main.rb +21 -39
  25. data/exe/three-rb +55 -0
  26. data/lib/three/backends/threejs/materialization.rb +6 -0
  27. data/lib/three/backends/threejs/parameters.rb +17 -0
  28. data/lib/three/backends/threejs/ruby_wasm_adapter.rb +166 -59
  29. data/lib/three/backends/threejs/synchronization.rb +38 -4
  30. data/lib/three/backends/threejs.rb +24 -0
  31. data/lib/three/browser.rb +389 -0
  32. data/lib/three/constants.rb +6 -0
  33. data/lib/three/core/buffer_attribute.rb +5 -1
  34. data/lib/three/core/buffer_geometry.rb +29 -1
  35. data/lib/three/core/object3d.rb +39 -1
  36. data/lib/three/exporters/three_json_exporter.rb +3 -0
  37. data/lib/three/generators/browser_example.rb +315 -0
  38. data/lib/three/geometries/text_geometry.rb +41 -0
  39. data/lib/three/loaders/font_loader.rb +29 -0
  40. data/lib/three/loaders/three_json_loader.rb +92 -46
  41. data/lib/three/materials/material.rb +2 -1
  42. data/lib/three/math/matrix4.rb +27 -0
  43. data/lib/three/renderers/threejs_renderer.rb +19 -0
  44. data/lib/three/scenes/fog.rb +86 -0
  45. data/lib/three/scenes/scene.rb +19 -1
  46. data/lib/three/textures/texture.rb +2 -1
  47. data/lib/three/version.rb +1 -1
  48. data/lib/three.rb +4 -0
  49. data/package.json +2 -1
  50. metadata +16 -3
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "cgi"
5
+ require "json"
6
+ require "pathname"
7
+
8
+ module Three
9
+ module Generators
10
+ class BrowserExample
11
+ attr_reader :created, :skipped
12
+
13
+ def initialize(target:, root: Dir.pwd, force: false, stream: $stdout)
14
+ @root = File.expand_path(root)
15
+ @target = File.expand_path(target, @root)
16
+ @force = force
17
+ @stream = stream
18
+ @created = []
19
+ @skipped = []
20
+ end
21
+
22
+ def call
23
+ validate_target!
24
+ validate_example_files!
25
+ copy_runtime
26
+ write_example_files
27
+ report
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def validate_target!
34
+ target_path = Pathname.new(@target)
35
+ root_path = Pathname.new(@root)
36
+ relative = begin
37
+ target_path.relative_path_from(root_path).to_s
38
+ rescue ArgumentError
39
+ raise ArgumentError, "target must be inside #{root_path}"
40
+ end
41
+
42
+ raise ArgumentError, "target must be a directory inside #{root_path}, not the project root" if relative == "."
43
+ return unless relative == ".." || relative.start_with?("../")
44
+
45
+ raise ArgumentError, "target must be inside #{root_path}"
46
+ end
47
+
48
+ def validate_example_files!
49
+ return if @force
50
+
51
+ example_files.each do |path|
52
+ next unless File.exist?(path)
53
+
54
+ raise ArgumentError, "#{relative_path(path)} already exists; pass --force to overwrite generated example files"
55
+ end
56
+ end
57
+
58
+ def copy_runtime
59
+ copy_file(runtime_path("package.json"), File.join(@root, "package.json"), overwrite: false)
60
+ copy_file(runtime_path("pnpm-lock.yaml"), File.join(@root, "pnpm-lock.yaml"), overwrite: false)
61
+ copy_tree(runtime_path("lib"), File.join(@root, "lib"), overwrite: false)
62
+ copy_file(
63
+ runtime_path("examples/browser/shared/boot.mjs"),
64
+ File.join(File.dirname(@target), "shared", "boot.mjs"),
65
+ overwrite: @force
66
+ )
67
+ end
68
+
69
+ def write_example_files
70
+ write_file(example_path("index.html"), index_html, overwrite: @force)
71
+ write_file(example_path("boot.mjs"), boot_js, overwrite: @force)
72
+ write_file(example_path("main.rb"), main_rb, overwrite: @force)
73
+ write_file(example_path("README.md"), readme, overwrite: @force)
74
+ end
75
+
76
+ def copy_tree(source, destination, overwrite:)
77
+ Dir.glob(File.join(source, "**/*"), File::FNM_DOTMATCH).each do |path|
78
+ next if File.directory?(path)
79
+
80
+ relative = Pathname.new(path).relative_path_from(Pathname.new(source)).to_s
81
+ copy_file(path, File.join(destination, relative), overwrite: overwrite)
82
+ end
83
+ end
84
+
85
+ def copy_file(source, destination, overwrite:)
86
+ if File.exist?(destination) && !overwrite
87
+ @skipped << relative_path(destination)
88
+ return
89
+ end
90
+
91
+ FileUtils.mkdir_p(File.dirname(destination))
92
+ FileUtils.cp(source, destination)
93
+ @created << relative_path(destination)
94
+ end
95
+
96
+ def write_file(path, content, overwrite:)
97
+ if File.exist?(path) && !overwrite
98
+ raise ArgumentError, "#{relative_path(path)} already exists; pass --force to overwrite generated example files"
99
+ end
100
+
101
+ FileUtils.mkdir_p(File.dirname(path))
102
+ File.write(path, content)
103
+ @created << relative_path(path)
104
+ end
105
+
106
+ def report
107
+ @stream.puts "Created browser example at #{relative_path(@target)}"
108
+ @stream.puts "Created #{created.length} files" unless created.empty?
109
+ @stream.puts "Skipped #{skipped.length} existing runtime files" unless skipped.empty?
110
+ end
111
+
112
+ def runtime_path(relative)
113
+ File.expand_path(File.join("../../../", relative), __dir__)
114
+ end
115
+
116
+ def example_files
117
+ %w[index.html boot.mjs main.rb README.md].map { |name| example_path(name) }
118
+ end
119
+
120
+ def example_path(name)
121
+ File.join(@target, name)
122
+ end
123
+
124
+ def relative_path(path)
125
+ Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s
126
+ end
127
+
128
+ def main_module_path
129
+ File.join(relative_path(@target), "main").delete_prefix("./")
130
+ end
131
+
132
+ def require_three_path
133
+ Pathname
134
+ .new(File.join(@root, "lib", "three"))
135
+ .relative_path_from(Pathname.new(@target))
136
+ .to_s
137
+ end
138
+
139
+ def shared_boot_path
140
+ Pathname
141
+ .new(File.join(File.dirname(@target), "shared", "boot.mjs"))
142
+ .relative_path_from(Pathname.new(@target))
143
+ .to_s
144
+ end
145
+
146
+ def title
147
+ "three-rb #{File.basename(@target)}"
148
+ end
149
+
150
+ def html_text(text)
151
+ CGI.escapeHTML(text)
152
+ end
153
+
154
+ def js_string(text)
155
+ JSON.generate(text)
156
+ end
157
+
158
+ def index_html
159
+ <<~HTML
160
+ <!doctype html>
161
+ <html lang="en">
162
+ <head>
163
+ <meta charset="utf-8">
164
+ <meta name="viewport" content="width=device-width, initial-scale=1">
165
+ <title>#{html_text(title)}</title>
166
+ <style>
167
+ :root {
168
+ color-scheme: dark;
169
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
170
+ background: #101418;
171
+ color: #eef3f7;
172
+ }
173
+
174
+ * { box-sizing: border-box; }
175
+ html, body, #viewport { width: 100%; height: 100%; margin: 0; }
176
+ body { overflow: hidden; }
177
+ #viewport { position: relative; min-width: 320px; min-height: 320px; }
178
+ canvas { display: block; width: 100%; height: 100%; }
179
+ .hud {
180
+ position: absolute;
181
+ top: 16px;
182
+ left: 16px;
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 8px;
186
+ padding: 8px 10px;
187
+ border: 1px solid rgba(255, 255, 255, 0.14);
188
+ border-radius: 6px;
189
+ background: rgba(10, 14, 18, 0.72);
190
+ color: #dce7ef;
191
+ font-size: 13px;
192
+ backdrop-filter: blur(10px);
193
+ }
194
+ .status-dot {
195
+ width: 8px;
196
+ height: 8px;
197
+ border-radius: 999px;
198
+ background: #f2b84b;
199
+ }
200
+ .status-dot[data-state="running"] { background: #4ed08f; }
201
+ .status-dot[data-state="error"] { background: #f15b5b; }
202
+ </style>
203
+ <script type="importmap">
204
+ {
205
+ "imports": {
206
+ "@bjorn3/browser_wasi_shim": "/node_modules/@bjorn3/browser_wasi_shim/dist/index.js",
207
+ "@ruby/wasm-wasi/browser": "/node_modules/@ruby/wasm-wasi/dist/esm/browser.js",
208
+ "three": "/node_modules/three/build/three.module.js",
209
+ "three/addons/": "/node_modules/three/examples/jsm/"
210
+ }
211
+ }
212
+ </script>
213
+ <script type="module">
214
+ const status = document.querySelector("#status");
215
+ const statusDot = document.querySelector("#status-dot");
216
+
217
+ globalThis.__threeRbSetStatus = (message, state) => {
218
+ if (status) status.textContent = message;
219
+ if (statusDot) statusDot.dataset.state = state;
220
+ };
221
+ globalThis.__threeRbBootFailed = (message) => globalThis.__threeRbSetStatus(message, "error");
222
+ globalThis.addEventListener("error", (event) => globalThis.__threeRbBootFailed(event.message || "Browser error"));
223
+ globalThis.addEventListener("unhandledrejection", (event) => {
224
+ const reason = event.reason;
225
+ globalThis.__threeRbBootFailed(reason && reason.message ? reason.message : "Ruby boot failed");
226
+ });
227
+ globalThis.__threeRbSetStatus("Loading ruby.wasm", "loading");
228
+ </script>
229
+ <script type="module" src="./boot.mjs"></script>
230
+ </head>
231
+ <body>
232
+ <main id="viewport">
233
+ <canvas id="scene" data-testid="scene-canvas"></canvas>
234
+ <div class="hud" aria-live="polite">
235
+ <span class="status-dot" id="status-dot"></span>
236
+ <span id="status" data-testid="status">Loading ruby.wasm</span>
237
+ </div>
238
+ </main>
239
+ </body>
240
+ </html>
241
+ HTML
242
+ end
243
+
244
+ def boot_js
245
+ <<~JS
246
+ import { bootRubyExample } from #{js_string(shared_boot_path)};
247
+
248
+ await bootRubyExample({
249
+ main: #{js_string(main_module_path)},
250
+ clearColor: 0x101418
251
+ });
252
+ JS
253
+ end
254
+
255
+ def main_rb
256
+ <<~RUBY
257
+ # frozen_string_literal: true
258
+
259
+ require_relative "#{require_three_path}"
260
+
261
+ Three::Browser.run(starting: "Starting Ruby scene") do |app|
262
+ scene = Three::Scene.new
263
+ camera = Three::PerspectiveCamera.new(70, aspect: 1.0, near: 0.1, far: 100)
264
+ camera.position.z = 3
265
+
266
+ cube = Three::Mesh.new(
267
+ Three::BoxGeometry.new(1, 1, 1),
268
+ Three::MeshBasicMaterial.new(color: 0x4ed08f)
269
+ )
270
+ scene.add(cube)
271
+
272
+ renderer = Three::Renderers::ThreeJSRenderer.new(
273
+ canvas: "#scene",
274
+ antialias: true,
275
+ alpha: false
276
+ )
277
+ renderer.set_clear_color(0x101418, 1)
278
+
279
+ app.resize_renderer(renderer, camera)
280
+ renderer.render(scene, camera)
281
+
282
+ app.animation_loop(renderer) do
283
+ cube.rotation.x += 0.01
284
+ cube.rotation.y += 0.015
285
+ renderer.render(scene, camera)
286
+ end
287
+ end
288
+ RUBY
289
+ end
290
+
291
+ def readme
292
+ <<~MARKDOWN
293
+ # #{title}
294
+
295
+ This generated browser example runs Ruby through ruby.wasm and renders with three.js.
296
+
297
+ From the project root:
298
+
299
+ ```sh
300
+ pnpm install
301
+ ruby -run -e httpd . -p 8000
302
+ ```
303
+
304
+ Open:
305
+
306
+ ```text
307
+ http://localhost:8000/#{relative_path(@target)}/
308
+ ```
309
+
310
+ Keep serving the project root. The browser runtime loads `node_modules/`, `lib/`, and this example over HTTP.
311
+ MARKDOWN
312
+ end
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/buffer_geometry"
4
+
5
+ module Three
6
+ class TextGeometry < BufferGeometry
7
+ attr_reader :text, :parameters
8
+
9
+ def initialize(
10
+ text,
11
+ font:,
12
+ size: 1,
13
+ depth: 0.2,
14
+ curve_segments: 12,
15
+ steps: 1,
16
+ bevel_enabled: false,
17
+ bevel_thickness: 0.01,
18
+ bevel_size: 0.01,
19
+ bevel_offset: 0,
20
+ bevel_segments: 3,
21
+ direction: "ltr"
22
+ )
23
+ super()
24
+ @type = "TextGeometry"
25
+ @text = text.to_s
26
+ @parameters = {
27
+ font: font,
28
+ size: size,
29
+ depth: depth,
30
+ curve_segments: curve_segments,
31
+ steps: steps,
32
+ bevel_enabled: bevel_enabled,
33
+ bevel_thickness: bevel_thickness,
34
+ bevel_size: bevel_size,
35
+ bevel_offset: bevel_offset,
36
+ bevel_segments: bevel_segments,
37
+ direction: direction
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../backends/threejs"
4
+
5
+ module Three
6
+ class Font
7
+ attr_reader :handle
8
+
9
+ def initialize(handle)
10
+ @handle = handle
11
+ end
12
+ end
13
+
14
+ module Loaders
15
+ class FontLoader
16
+ def initialize(adapter: nil, backend: nil)
17
+ @adapter = adapter || backend&.adapter || Backends::ThreeJS::RubyWasmAdapter.new
18
+ end
19
+
20
+ def load(source)
21
+ handle = @adapter.load_font(source)
22
+ handle = handle.await if handle.respond_to?(:await)
23
+ font = Font.new(handle)
24
+ yield font if block_given?
25
+ font
26
+ end
27
+ end
28
+ end
29
+ end
@@ -22,17 +22,20 @@ module Three
22
22
  end
23
23
 
24
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
25
+ texture =
26
+ case value(entry, :type)
27
+ when "CubeTexture"
28
+ CubeTexture.new(
29
+ value(entry, :sources) || value(entry, :source),
30
+ **texture_parameters(entry)
31
+ )
32
+ when "RGBETexture"
33
+ RGBETexture.new(value(entry, :source), **texture_parameters(entry))
34
+ else
35
+ Texture.new(value(entry, :source), **texture_parameters(entry))
36
+ end
37
+ texture.user_data = value(entry, :user_data) || {}
38
+ texture
36
39
  end
37
40
 
38
41
  def texture_parameters(entry)
@@ -57,25 +60,25 @@ module Three
57
60
  case value(entry, :type)
58
61
  when "BoxGeometry"
59
62
  parameters = value(entry, :parameters) || {}
60
- BoxGeometry.new(
63
+ apply_geometry_properties(BoxGeometry.new(
61
64
  value(parameters, :width) || 1,
62
65
  value(parameters, :height) || 1,
63
66
  value(parameters, :depth) || 1,
64
67
  width_segments: value(parameters, :width_segments) || 1,
65
68
  height_segments: value(parameters, :height_segments) || 1,
66
69
  depth_segments: value(parameters, :depth_segments) || 1
67
- )
70
+ ), entry)
68
71
  when "PlaneGeometry"
69
72
  parameters = value(entry, :parameters) || {}
70
- PlaneGeometry.new(
73
+ apply_geometry_properties(PlaneGeometry.new(
71
74
  value(parameters, :width) || 1,
72
75
  value(parameters, :height) || 1,
73
76
  width_segments: value(parameters, :width_segments) || 1,
74
77
  height_segments: value(parameters, :height_segments) || 1
75
- )
78
+ ), entry)
76
79
  when "SphereGeometry"
77
80
  parameters = value(entry, :parameters) || {}
78
- SphereGeometry.new(
81
+ apply_geometry_properties(SphereGeometry.new(
79
82
  value(parameters, :radius) || 1,
80
83
  width_segments: value(parameters, :width_segments) || 32,
81
84
  height_segments: value(parameters, :height_segments) || 16,
@@ -83,7 +86,7 @@ module Three
83
86
  phi_length: value(parameters, :phi_length) || Math::PI * 2,
84
87
  theta_start: value(parameters, :theta_start) || 0,
85
88
  theta_length: value(parameters, :theta_length) || Math::PI
86
- )
89
+ ), entry)
87
90
  else
88
91
  build_buffer_geometry(entry)
89
92
  end
@@ -91,7 +94,6 @@ module Three
91
94
 
92
95
  def build_buffer_geometry(entry)
93
96
  geometry = BufferGeometry.new
94
- geometry.name = value(entry, :name) if value(entry, :name)
95
97
  geometry.set_index(build_buffer_attribute(value(entry, :index))) if value(entry, :index)
96
98
 
97
99
  (value(entry, :attributes) || {}).each do |name, attribute_entry|
@@ -106,9 +108,29 @@ module Three
106
108
  )
107
109
  end
108
110
 
111
+ apply_geometry_properties(geometry, entry)
112
+ end
113
+
114
+ def apply_geometry_properties(geometry, entry)
115
+ geometry.name = value(entry, :name) if value(entry, :name)
116
+ geometry.user_data = value(entry, :user_data) || {}
117
+ draw_range = value(entry, :draw_range)
118
+ if draw_range
119
+ geometry.set_draw_range(
120
+ deserialize_number(value(draw_range, :start)),
121
+ deserialize_number(value(draw_range, :count))
122
+ )
123
+ end
109
124
  geometry
110
125
  end
111
126
 
127
+ def deserialize_number(value)
128
+ return Float::INFINITY if value == "Infinity"
129
+ return -Float::INFINITY if value == "-Infinity"
130
+
131
+ value
132
+ end
133
+
112
134
  def build_buffer_attribute(entry)
113
135
  component_type = (value(entry, :component_type) || :generic).to_sym
114
136
  array = value(entry, :array) || []
@@ -129,34 +151,37 @@ module Three
129
151
 
130
152
  def build_material(entry)
131
153
  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
154
+ material =
155
+ case value(entry, :type)
156
+ when "MeshBasicMaterial"
157
+ MeshBasicMaterial.new(parameters)
158
+ when "LineBasicMaterial"
159
+ LineBasicMaterial.new(parameters)
160
+ when "MeshLambertMaterial"
161
+ MeshLambertMaterial.new(parameters)
162
+ when "MeshMatcapMaterial"
163
+ MeshMatcapMaterial.new(parameters)
164
+ when "MeshNormalMaterial"
165
+ MeshNormalMaterial.new(parameters)
166
+ when "MeshPhongMaterial"
167
+ MeshPhongMaterial.new(parameters)
168
+ when "MeshPhysicalMaterial"
169
+ MeshPhysicalMaterial.new(parameters)
170
+ when "MeshStandardMaterial"
171
+ MeshStandardMaterial.new(parameters)
172
+ when "MeshToonMaterial"
173
+ MeshToonMaterial.new(parameters)
174
+ when "PointsMaterial"
175
+ PointsMaterial.new(parameters)
176
+ when "ShadowMaterial"
177
+ ShadowMaterial.new(parameters)
178
+ when "SpriteMaterial"
179
+ SpriteMaterial.new(parameters)
180
+ else
181
+ Material.new(parameters)
182
+ end
183
+ material.user_data = value(entry, :user_data) || {}
184
+ material
160
185
  end
161
186
 
162
187
  def material_parameters(entry)
@@ -312,9 +337,29 @@ module Three
312
337
  scene = Scene.new
313
338
  scene.background = @textures[value(entry, :background)] if value(entry, :background)
314
339
  scene.environment = @textures[value(entry, :environment)] if value(entry, :environment)
340
+ scene.fog = build_fog(value(entry, :fog)) if value(entry, :fog)
341
+ scene.override_material = @materials[value(entry, :override_material)] if value(entry, :override_material)
315
342
  scene
316
343
  end
317
344
 
345
+ def build_fog(entry)
346
+ case value(entry, :type)
347
+ when "FogExp2"
348
+ FogExp2.new(
349
+ value(entry, :color) || 0xffffff,
350
+ density: has_value?(entry, :density) ? value(entry, :density) : 0.00025,
351
+ name: value(entry, :name) || ""
352
+ )
353
+ else
354
+ Fog.new(
355
+ value(entry, :color) || 0xffffff,
356
+ near: has_value?(entry, :near) ? value(entry, :near) : 1,
357
+ far: has_value?(entry, :far) ? value(entry, :far) : 1000,
358
+ name: value(entry, :name) || ""
359
+ )
360
+ end
361
+ end
362
+
318
363
  def build_directional_light(entry)
319
364
  light = DirectionalLight.new(value(entry, :color) || 0xffffff, value(entry, :intensity) || 1)
320
365
  camera = value(entry, :shadow_camera)
@@ -361,6 +406,7 @@ module Three
361
406
  object.matrix_auto_update = value(entry, :matrix_auto_update) if has_value?(entry, :matrix_auto_update)
362
407
  object.user_data = value(entry, :user_data) || {}
363
408
 
409
+ object.matrix.from_array(value(entry, :matrix)) if value(entry, :matrix)
364
410
  object.position.set(*value(entry, :position)) if value(entry, :position)
365
411
  object.quaternion.set(*value(entry, :quaternion)) if value(entry, :quaternion)
366
412
  object.scale.set(*value(entry, :scale)) if value(entry, :scale)
@@ -140,7 +140,8 @@ module Three
140
140
  opacity: @opacity,
141
141
  transparent: @transparent,
142
142
  visible: @visible,
143
- vertex_colors: @vertex_colors
143
+ vertex_colors: @vertex_colors,
144
+ user_data: @user_data
144
145
  }
145
146
  end
146
147