three-rb 0.1.0 → 0.2.1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -1
  3. data/README.md +66 -3
  4. data/docs/browser-runtime.md +92 -24
  5. data/docs/loaded-assets-design.md +1 -1
  6. data/docs/next-work.md +9 -5
  7. data/docs/publishing.md +119 -23
  8. data/docs/release-readiness.md +5 -3
  9. data/docs/standalone-browser-app.md +106 -0
  10. data/examples/browser/README.md +8 -0
  11. data/examples/browser/composition/main.rb +44 -64
  12. data/examples/browser/cube/main.rb +4 -34
  13. data/examples/browser/cubemap/assets/checker.svg +11 -0
  14. data/examples/browser/cubemap/main.rb +17 -40
  15. data/examples/browser/gltf/main.rb +30 -50
  16. data/examples/browser/picking/main.rb +27 -53
  17. data/examples/browser/postprocessing/main.rb +23 -42
  18. data/examples/browser/primitives/assets/checker.svg +11 -0
  19. data/examples/browser/primitives/main.rb +19 -42
  20. data/examples/browser/ruby/README.md +24 -0
  21. data/examples/browser/ruby/boot.mjs +6 -0
  22. data/examples/browser/ruby/index.html +142 -0
  23. data/examples/browser/ruby/main.rb +313 -0
  24. data/examples/browser/ruby/smoke_test.mjs +126 -0
  25. data/examples/browser/serialization/assets/checker.svg +11 -0
  26. data/examples/browser/serialization/main.rb +20 -42
  27. data/examples/browser/shared/boot.mjs +37 -5
  28. data/examples/browser/textures/assets/checker.svg +11 -0
  29. data/examples/browser/textures/assets/studio.hdr +5 -0
  30. data/examples/browser/textures/main.rb +23 -41
  31. data/exe/three-rb +56 -0
  32. data/lib/three/backends/threejs/materialization.rb +6 -0
  33. data/lib/three/backends/threejs/parameters.rb +17 -0
  34. data/lib/three/backends/threejs/ruby_wasm_adapter.rb +166 -59
  35. data/lib/three/backends/threejs/synchronization.rb +38 -4
  36. data/lib/three/backends/threejs.rb +24 -0
  37. data/lib/three/browser.rb +389 -0
  38. data/lib/three/constants.rb +6 -0
  39. data/lib/three/core/buffer_attribute.rb +5 -1
  40. data/lib/three/core/buffer_geometry.rb +29 -1
  41. data/lib/three/core/object3d.rb +39 -1
  42. data/lib/three/exporters/three_json_exporter.rb +3 -0
  43. data/lib/three/generators/browser_example.rb +396 -0
  44. data/lib/three/geometries/text_geometry.rb +41 -0
  45. data/lib/three/loaders/font_loader.rb +29 -0
  46. data/lib/three/loaders/three_json_loader.rb +92 -46
  47. data/lib/three/materials/material.rb +2 -1
  48. data/lib/three/math/matrix4.rb +27 -0
  49. data/lib/three/renderers/threejs_renderer.rb +19 -0
  50. data/lib/three/scenes/fog.rb +86 -0
  51. data/lib/three/scenes/scene.rb +19 -1
  52. data/lib/three/textures/texture.rb +2 -1
  53. data/lib/three/version.rb +1 -1
  54. data/lib/three.rb +4 -0
  55. data/package.json +2 -1
  56. metadata +26 -8
  57. /data/examples/browser/{assets → composition/assets}/checker.svg +0 -0
  58. /data/examples/browser/{assets → gltf/assets}/animated_triangle.gltf +0 -0
  59. /data/examples/browser/{assets → gltf/assets}/compressed_triangle.gltf +0 -0
  60. /data/examples/browser/{assets → gltf/assets}/triangle.gltf +0 -0
  61. /data/examples/browser/{assets → ruby/assets}/studio.hdr +0 -0
@@ -1,24 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "js"
4
-
5
- begin
6
- JS.global[:__threeReady].await
7
-
8
- require_relative "../../../lib/three"
9
-
10
- document = JS.global[:document]
11
- window = JS.global[:window]
12
- viewport = document.call(:querySelector, "#viewport")
13
- status = document.call(:querySelector, "#status")
14
- status_dot = document.call(:querySelector, "#status-dot")
15
- status[:textContent] = "Starting Ruby scene"
3
+ require_relative "../../../lib/three"
16
4
 
5
+ Three::Browser.run(starting: "Starting Ruby scene") do |app|
17
6
  scene = Three::Scene.new
18
7
  camera = Three::OrthographicCamera.new(-2.5, 2.5, 1.6, -1.6, near: 0.1, far: 100)
19
8
  camera.position.z = 5
20
9
 
21
- environment_texture = Three::Loaders::RGBELoader.new.load("/examples/browser/assets/studio.hdr")
10
+ environment_texture = Three::Loaders::RGBELoader.new.load("/examples/browser/textures/assets/studio.hdr")
22
11
  scene.environment = environment_texture
23
12
 
24
13
  scene.add(Three::AmbientLight.new(0xffffff, 0.45))
@@ -27,7 +16,7 @@ begin
27
16
  key_light.position.set(2.5, 3.0, 4.0)
28
17
  scene.add(key_light)
29
18
 
30
- texture = Three::Loaders::TextureLoader.new.load("/examples/browser/assets/checker.svg")
19
+ texture = Three::Loaders::TextureLoader.new.load("/examples/browser/textures/assets/checker.svg")
31
20
  texture.wrap_s = Three::RepeatWrapping
32
21
  texture.wrap_t = Three::RepeatWrapping
33
22
  texture.mag_filter = Three::NearestFilter
@@ -91,9 +80,7 @@ begin
91
80
  )
92
81
  renderer.set_clear_color(0x11161a, 1)
93
82
 
94
- resize = proc do
95
- width = [viewport[:clientWidth].to_i, 1].max
96
- height = [viewport[:clientHeight].to_i, 1].max
83
+ app.resize_renderer(renderer, camera) do |width, height, _aspect|
97
84
  view_height = 3.4
98
85
  view_width = view_height * width.to_f / height
99
86
 
@@ -102,25 +89,26 @@ begin
102
89
  camera.top = view_height / 2
103
90
  camera.bottom = -view_height / 2
104
91
  camera.update_projection_matrix
105
- renderer.set_size(width, height)
106
92
  end
107
-
108
- resize.call
109
- window.call(:addEventListener, "resize", resize)
110
93
  renderer.render(scene, camera)
111
94
 
112
- JS.global[:__threeRbRenderer] = renderer.handle
113
- JS.global[:__threeRbScene] = renderer.backend.materialize(scene)
114
- JS.global[:__threeRbCamera] = renderer.backend.materialize(camera)
115
- JS.global[:__threeRbTexturedMesh] = renderer.backend.materialize(mesh)
116
- JS.global[:__threeRbTextureMaterial] = renderer.backend.materialize(material)
117
- JS.global[:__threeRbMatcapMesh] = renderer.backend.materialize(matcap_mesh)
118
- JS.global[:__threeRbMatcapMaterial] = renderer.backend.materialize(matcap_material)
119
- JS.global[:__threeRbToonMesh] = renderer.backend.materialize(toon_mesh)
120
- JS.global[:__threeRbToonMaterial] = renderer.backend.materialize(toon_material)
121
- JS.global[:__threeRbTextureExampleTexture] = renderer.backend.materialize(texture)
122
- JS.global[:__threeRbTextureExampleEnvironment] = renderer.backend.materialize(environment_texture)
123
- JS.global[:__threeRbTextureExampleFrame] = 0
95
+ app.expose(
96
+ {
97
+ renderer: renderer,
98
+ scene: scene,
99
+ camera: camera,
100
+ textured_mesh: mesh,
101
+ texture_material: material,
102
+ matcap_mesh: matcap_mesh,
103
+ matcap_material: matcap_material,
104
+ toon_mesh: toon_mesh,
105
+ toon_material: toon_material,
106
+ texture_example_texture: texture,
107
+ texture_example_environment: environment_texture,
108
+ texture_example_frame: 0
109
+ },
110
+ renderer: renderer
111
+ )
124
112
 
125
113
  frame = 0
126
114
  renderer.animation_loop do
@@ -130,13 +118,7 @@ begin
130
118
  matcap_mesh.rotation.x += 0.005
131
119
  matcap_mesh.rotation.y -= 0.009
132
120
  toon_mesh.rotation.y += 0.012
133
- JS.global[:__threeRbTextureExampleFrame] = frame
121
+ app.set(:texture_example_frame, frame)
134
122
  renderer.render(scene, camera)
135
123
  end
136
-
137
- status[:textContent] = "Running"
138
- status_dot[:dataset][:state] = "running"
139
- rescue StandardError => error
140
- JS.global.call(:__threeRbBootFailed, error.message) if JS.global[:__threeRbBootFailed]
141
- raise
142
124
  end
data/exe/three-rb ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "three/generators/browser_example"
6
+
7
+ def usage
8
+ <<~TEXT
9
+ Usage:
10
+ three-rb browser PATH [--force]
11
+
12
+ Commands:
13
+ browser PATH Generate a Ruby-only browser example at PATH.
14
+
15
+ Example:
16
+ three-rb browser examples/browser/quickstart
17
+ three-rb browser examples/browser/ruby
18
+ TEXT
19
+ end
20
+
21
+ command = ARGV.shift
22
+
23
+ case command
24
+ when "browser", "browser-example"
25
+ options = { force: false }
26
+ parser = OptionParser.new do |opts|
27
+ opts.banner = "Usage: three-rb #{command} PATH [--force]"
28
+ opts.on("--force", "Overwrite generated example files") { options[:force] = true }
29
+ opts.on("-h", "--help", "Show help") do
30
+ puts opts
31
+ exit 0
32
+ end
33
+ end
34
+ begin
35
+ parser.parse!(ARGV)
36
+ rescue OptionParser::ParseError => error
37
+ warn error.message
38
+ warn parser
39
+ exit 1
40
+ end
41
+ target = ARGV.shift
42
+
43
+ unless target
44
+ warn parser
45
+ exit 1
46
+ end
47
+
48
+ Three::Generators::BrowserExample.new(target: target, force: options[:force]).call
49
+ when "-h", "--help", nil
50
+ puts usage
51
+ exit(command ? 0 : 1)
52
+ else
53
+ warn "Unknown command: #{command}"
54
+ warn usage
55
+ exit 1
56
+ end
@@ -30,6 +30,10 @@ module Three
30
30
  @adapter.load_rgbe_texture(object.source, texture_parameters(object))
31
31
  when Texture
32
32
  @adapter.load_texture(object.source, texture_parameters(object))
33
+ when FogExp2
34
+ @adapter.new_fog_exp2(object.color.hex, object.density)
35
+ when Fog
36
+ @adapter.new_fog(object.color.hex, object.near, object.far)
33
37
  when AmbientLight
34
38
  @adapter.new_ambient_light(object.color.hex, object.intensity)
35
39
  when DirectionalLight
@@ -67,6 +71,8 @@ module Three
67
71
  parameters[:theta_start],
68
72
  parameters[:theta_length]
69
73
  )
74
+ when TextGeometry
75
+ @adapter.new_text_geometry(object.text, text_geometry_parameters(object))
70
76
  when BufferGeometry
71
77
  build_buffer_geometry(object)
72
78
  when LineBasicMaterial
@@ -89,6 +89,23 @@ module Three
89
89
  matrix: texture.matrix.to_a
90
90
  }
91
91
  end
92
+
93
+ def text_geometry_parameters(geometry)
94
+ parameters = geometry.parameters
95
+ {
96
+ font: parameters[:font].handle,
97
+ size: parameters[:size],
98
+ depth: parameters[:depth],
99
+ curveSegments: parameters[:curve_segments],
100
+ steps: parameters[:steps],
101
+ bevelEnabled: parameters[:bevel_enabled],
102
+ bevelThickness: parameters[:bevel_thickness],
103
+ bevelSize: parameters[:bevel_size],
104
+ bevelOffset: parameters[:bevel_offset],
105
+ bevelSegments: parameters[:bevel_segments],
106
+ direction: parameters[:direction]
107
+ }
108
+ end
92
109
  end
93
110
 
94
111
  include Parameters
@@ -33,6 +33,75 @@ module Three
33
33
  specularIntensityMap
34
34
  ].freeze
35
35
 
36
+ ADDON_CONSTRUCTORS = {
37
+ orbit_controls: {
38
+ owner: "Three::Controls::OrbitControls",
39
+ global: :THREE_ORBIT_CONTROLS,
40
+ import: 'import { OrbitControls } from "three/addons/controls/OrbitControls.js";',
41
+ assignment: "globalThis.THREE_ORBIT_CONTROLS = OrbitControls;"
42
+ },
43
+ effect_composer: {
44
+ owner: "Three::Postprocessing::EffectComposer",
45
+ global: :THREE_EFFECT_COMPOSER,
46
+ import: 'import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";',
47
+ assignment: "globalThis.THREE_EFFECT_COMPOSER = EffectComposer;"
48
+ },
49
+ render_pass: {
50
+ owner: "Three::Postprocessing::RenderPass",
51
+ global: :THREE_RENDER_PASS,
52
+ import: 'import { RenderPass } from "three/addons/postprocessing/RenderPass.js";',
53
+ assignment: "globalThis.THREE_RENDER_PASS = RenderPass;"
54
+ },
55
+ unreal_bloom_pass: {
56
+ owner: "Three::Postprocessing::UnrealBloomPass",
57
+ global: :THREE_UNREAL_BLOOM_PASS,
58
+ import: 'import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";',
59
+ assignment: "globalThis.THREE_UNREAL_BLOOM_PASS = UnrealBloomPass;"
60
+ },
61
+ dot_screen_pass: {
62
+ owner: "Three::Postprocessing::DotScreenPass",
63
+ global: :THREE_DOT_SCREEN_PASS,
64
+ import: 'import { DotScreenPass } from "three/addons/postprocessing/DotScreenPass.js";',
65
+ assignment: "globalThis.THREE_DOT_SCREEN_PASS = DotScreenPass;"
66
+ },
67
+ output_pass: {
68
+ owner: "Three::Postprocessing::OutputPass",
69
+ global: :THREE_OUTPUT_PASS,
70
+ import: 'import { OutputPass } from "three/addons/postprocessing/OutputPass.js";',
71
+ assignment: "globalThis.THREE_OUTPUT_PASS = OutputPass;"
72
+ },
73
+ gltf_loader: {
74
+ owner: "Three::Loaders::GLTFLoader",
75
+ global: :THREE_GLTF_LOADER,
76
+ import: 'import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";',
77
+ assignment: "globalThis.THREE_GLTF_LOADER = GLTFLoader;"
78
+ },
79
+ draco_loader: {
80
+ owner: "Three::Loaders::GLTFLoader with DRACO",
81
+ global: :THREE_DRACO_LOADER,
82
+ import: 'import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";',
83
+ assignment: "globalThis.THREE_DRACO_LOADER = DRACOLoader;"
84
+ },
85
+ rgbe_loader: {
86
+ owner: "Three::Loaders::RGBELoader",
87
+ global: :THREE_RGBE_LOADER,
88
+ import: 'import { HDRLoader } from "three/addons/loaders/HDRLoader.js";',
89
+ assignment: "globalThis.THREE_RGBE_LOADER = HDRLoader;"
90
+ },
91
+ font_loader: {
92
+ owner: "Three::Loaders::FontLoader",
93
+ global: :THREE_FONT_LOADER,
94
+ import: 'import { FontLoader } from "three/addons/loaders/FontLoader.js";',
95
+ assignment: "globalThis.THREE_FONT_LOADER = FontLoader;"
96
+ },
97
+ text_geometry: {
98
+ owner: "Three::TextGeometry",
99
+ global: :THREE_TEXT_GEOMETRY,
100
+ import: 'import { TextGeometry } from "three/addons/geometries/TextGeometry.js";',
101
+ assignment: "globalThis.THREE_TEXT_GEOMETRY = TextGeometry;"
102
+ }
103
+ }.freeze
104
+
36
105
  def initialize(three: nil)
37
106
  @three = three || default_three
38
107
  end
@@ -55,6 +124,14 @@ module Three
55
124
  renderer.call(:setClearColor, color, alpha)
56
125
  end
57
126
 
127
+ def set_renderer_tone_mapping(renderer, value)
128
+ renderer[:toneMapping] = value
129
+ end
130
+
131
+ def set_renderer_tone_mapping_exposure(renderer, value)
132
+ renderer[:toneMappingExposure] = value
133
+ end
134
+
58
135
  def set_renderer_shadow_map(renderer, enabled: nil, type: nil, auto_update: nil)
59
136
  shadow_map = renderer[:shadowMap]
60
137
  shadow_map[:enabled] = enabled unless enabled.nil?
@@ -78,6 +155,10 @@ module Three
78
155
  end
79
156
  end
80
157
 
158
+ def add_event_listener(handle, type, callback)
159
+ handle.call(:addEventListener, type.to_s, callback)
160
+ end
161
+
81
162
  def new_effect_composer(renderer)
82
163
  effect_composer_constructor.new(renderer)
83
164
  end
@@ -172,6 +253,18 @@ module Three
172
253
  texture
173
254
  end
174
255
 
256
+ def new_fog(color, near, far)
257
+ @three[:Fog].new(color, near, far)
258
+ end
259
+
260
+ def new_fog_exp2(color, density)
261
+ @three[:FogExp2].new(color, density)
262
+ end
263
+
264
+ def load_font(source)
265
+ font_loader_constructor.new.call(:loadAsync, source)
266
+ end
267
+
175
268
  def load_gltf(source, draco_decoder_path: nil, draco_decoder_config: nil)
176
269
  loader = gltf_loader_constructor.new
177
270
  configure_draco_loader(loader, draco_decoder_path, draco_decoder_config) if draco_decoder_path
@@ -395,6 +488,10 @@ module Three
395
488
  @three[:SphereGeometry].new(radius, width_segments, height_segments, phi_start, phi_length, theta_start, theta_length)
396
489
  end
397
490
 
491
+ def new_text_geometry(text, parameters)
492
+ text_geometry_constructor.new(text, stringify_keys(parameters))
493
+ end
494
+
398
495
  def new_buffer_geometry
399
496
  @three[:BufferGeometry].new
400
497
  end
@@ -428,6 +525,10 @@ module Three
428
525
  geometry.call(:setDrawRange, start, count)
429
526
  end
430
527
 
528
+ def center_geometry(geometry)
529
+ geometry.call(:center)
530
+ end
531
+
431
532
  def new_mesh_basic_material(parameters)
432
533
  @three[:MeshBasicMaterial].new(stringify_keys(parameters))
433
534
  end
@@ -497,6 +598,15 @@ module Three
497
598
  object[:scale].call(:set, *scale)
498
599
  end
499
600
 
601
+ def set_object_matrix_auto_update(object, value)
602
+ object[:matrixAutoUpdate] = value
603
+ end
604
+
605
+ def set_object_matrix(object, elements)
606
+ object[:matrix].call(:fromArray, js_array(elements))
607
+ object[:matrixWorldNeedsUpdate] = true
608
+ end
609
+
500
610
  def update_perspective_camera(camera, fov, aspect, near, far, zoom)
501
611
  camera[:fov] = fov
502
612
  camera[:aspect] = aspect
@@ -580,6 +690,25 @@ module Three
580
690
  scene[:environment] = environment
581
691
  end
582
692
 
693
+ def set_scene_fog(scene, fog)
694
+ scene[:fog] = fog
695
+ end
696
+
697
+ def set_scene_override_material(scene, material)
698
+ scene[:overrideMaterial] = material
699
+ end
700
+
701
+ def update_fog(fog, color, near, far)
702
+ fog[:color].call(:setHex, color)
703
+ fog[:near] = near
704
+ fog[:far] = far
705
+ end
706
+
707
+ def update_fog_exp2(fog, color, density)
708
+ fog[:color].call(:setHex, color)
709
+ fog[:density] = density
710
+ end
711
+
583
712
  def dispose(handle)
584
713
  handle.call(:dispose) if handle.respond_to?(:call)
585
714
  end
@@ -675,83 +804,35 @@ module Three
675
804
  end
676
805
 
677
806
  def orbit_controls_constructor
678
- require "js"
679
- constructor = JS.global[:THREE_ORBIT_CONTROLS]
680
- raise RuntimeError, "Three::Controls::OrbitControls requires globalThis.THREE_ORBIT_CONTROLS" if constructor.typeof == "undefined"
681
-
682
- constructor
683
- rescue LoadError
684
- raise RuntimeError, "Three::Controls::OrbitControls requires ruby.wasm's js gem or an injected adapter"
807
+ addon_constructor(:orbit_controls)
685
808
  end
686
809
 
687
810
  def effect_composer_constructor
688
- require "js"
689
- constructor = JS.global[:THREE_EFFECT_COMPOSER]
690
- raise RuntimeError, "Three::Postprocessing::EffectComposer requires globalThis.THREE_EFFECT_COMPOSER" if constructor.typeof == "undefined"
691
-
692
- constructor
693
- rescue LoadError
694
- raise RuntimeError, "Three::Postprocessing::EffectComposer requires ruby.wasm's js gem or an injected adapter"
811
+ addon_constructor(:effect_composer)
695
812
  end
696
813
 
697
814
  def render_pass_constructor
698
- require "js"
699
- constructor = JS.global[:THREE_RENDER_PASS]
700
- raise RuntimeError, "Three::Postprocessing::RenderPass requires globalThis.THREE_RENDER_PASS" if constructor.typeof == "undefined"
701
-
702
- constructor
703
- rescue LoadError
704
- raise RuntimeError, "Three::Postprocessing::RenderPass requires ruby.wasm's js gem or an injected adapter"
815
+ addon_constructor(:render_pass)
705
816
  end
706
817
 
707
818
  def unreal_bloom_pass_constructor
708
- require "js"
709
- constructor = JS.global[:THREE_UNREAL_BLOOM_PASS]
710
- raise RuntimeError, "Three::Postprocessing::UnrealBloomPass requires globalThis.THREE_UNREAL_BLOOM_PASS" if constructor.typeof == "undefined"
711
-
712
- constructor
713
- rescue LoadError
714
- raise RuntimeError, "Three::Postprocessing::UnrealBloomPass requires ruby.wasm's js gem or an injected adapter"
819
+ addon_constructor(:unreal_bloom_pass)
715
820
  end
716
821
 
717
822
  def dot_screen_pass_constructor
718
- require "js"
719
- constructor = JS.global[:THREE_DOT_SCREEN_PASS]
720
- raise RuntimeError, "Three::Postprocessing::DotScreenPass requires globalThis.THREE_DOT_SCREEN_PASS" if constructor.typeof == "undefined"
721
-
722
- constructor
723
- rescue LoadError
724
- raise RuntimeError, "Three::Postprocessing::DotScreenPass requires ruby.wasm's js gem or an injected adapter"
823
+ addon_constructor(:dot_screen_pass)
725
824
  end
726
825
 
727
826
  def output_pass_constructor
728
- require "js"
729
- constructor = JS.global[:THREE_OUTPUT_PASS]
730
- raise RuntimeError, "Three::Postprocessing::OutputPass requires globalThis.THREE_OUTPUT_PASS" if constructor.typeof == "undefined"
731
-
732
- constructor
733
- rescue LoadError
734
- raise RuntimeError, "Three::Postprocessing::OutputPass requires ruby.wasm's js gem or an injected adapter"
827
+ addon_constructor(:output_pass)
735
828
  end
736
829
 
737
830
  def gltf_loader_constructor
738
- require "js"
739
- constructor = JS.global[:THREE_GLTF_LOADER]
740
- raise RuntimeError, "Three::Loaders::GLTFLoader requires globalThis.THREE_GLTF_LOADER" if constructor.typeof == "undefined"
741
-
742
- constructor
743
- rescue LoadError
744
- raise RuntimeError, "Three::Loaders::GLTFLoader requires ruby.wasm's js gem or an injected adapter"
831
+ addon_constructor(:gltf_loader)
745
832
  end
746
833
 
747
834
  def draco_loader_constructor
748
- require "js"
749
- constructor = JS.global[:THREE_DRACO_LOADER]
750
- raise RuntimeError, "Three::Loaders::GLTFLoader with DRACO requires globalThis.THREE_DRACO_LOADER" if constructor.typeof == "undefined"
751
-
752
- constructor
753
- rescue LoadError
754
- raise RuntimeError, "Three::Loaders::GLTFLoader with DRACO requires ruby.wasm's js gem or an injected adapter"
835
+ addon_constructor(:draco_loader)
755
836
  end
756
837
 
757
838
  def configure_draco_loader(loader, decoder_path, decoder_config)
@@ -762,13 +843,39 @@ module Three
762
843
  end
763
844
 
764
845
  def rgbe_loader_constructor
846
+ addon_constructor(:rgbe_loader)
847
+ end
848
+
849
+ def font_loader_constructor
850
+ addon_constructor(:font_loader)
851
+ end
852
+
853
+ def text_geometry_constructor
854
+ addon_constructor(:text_geometry)
855
+ end
856
+
857
+ def addon_constructor(name)
858
+ addon = ADDON_CONSTRUCTORS.fetch(name)
765
859
  require "js"
766
- constructor = JS.global[:THREE_RGBE_LOADER]
767
- raise RuntimeError, "Three::Loaders::RGBELoader requires globalThis.THREE_RGBE_LOADER" if constructor.typeof == "undefined"
860
+ constructor = JS.global[addon.fetch(:global)]
861
+ raise RuntimeError, missing_addon_constructor_message(addon) if js_undefined?(constructor)
768
862
 
769
863
  constructor
770
864
  rescue LoadError
771
- raise RuntimeError, "Three::Loaders::RGBELoader requires ruby.wasm's js gem or an injected adapter"
865
+ raise RuntimeError, "#{addon.fetch(:owner)} requires ruby.wasm's js gem or an injected adapter"
866
+ end
867
+
868
+ def js_undefined?(value)
869
+ value.respond_to?(:typeof) && value.typeof == "undefined"
870
+ end
871
+
872
+ def missing_addon_constructor_message(addon)
873
+ <<~MESSAGE.chomp
874
+ #{addon.fetch(:owner)} requires globalThis.#{addon.fetch(:global)}.
875
+ Add this to boot.mjs before loading Ruby:
876
+ #{addon.fetch(:import)}
877
+ #{addon.fetch(:assignment)}
878
+ MESSAGE
772
879
  end
773
880
 
774
881
  def resolve_canvas(canvas)
@@ -17,7 +17,12 @@ module Three
17
17
  end
18
18
 
19
19
  if object.dirty_field?(:transform)
20
- @adapter.set_object_transform(handle, object.position.to_a, object.quaternion.to_a, object.scale.to_a)
20
+ @adapter.set_object_matrix_auto_update(handle, object.matrix_auto_update)
21
+ if object.matrix_auto_update
22
+ @adapter.set_object_transform(handle, object.position.to_a, object.quaternion.to_a, object.scale.to_a)
23
+ else
24
+ @adapter.set_object_matrix(handle, object.matrix.to_a)
25
+ end
21
26
  end
22
27
 
23
28
  if object.dirty_field?(:camera)
@@ -117,14 +122,28 @@ module Three
117
122
  def sync_scene(scene, handle)
118
123
  @adapter.set_scene_background(handle, scene.background ? sync(scene.background) : nil)
119
124
  @adapter.set_scene_environment(handle, scene.environment ? sync(scene.environment) : nil)
125
+ @adapter.set_scene_fog(handle, scene.fog ? sync(scene.fog) : nil)
126
+ @adapter.set_scene_override_material(handle, scene.override_material ? sync(scene.override_material) : nil)
120
127
  end
121
128
 
122
129
  def scene_resource_dirty?(scene)
123
- [scene.background, scene.environment].compact.any? do |resource|
130
+ [scene.background, scene.environment, scene.fog, scene.override_material].compact.any? do |resource|
124
131
  resource.respond_to?(:dirty?) && resource.dirty?
125
132
  end
126
133
  end
127
134
 
135
+ def sync_fog(fog, handle)
136
+ return handle unless fog.dirty?
137
+
138
+ if fog.is_a?(FogExp2)
139
+ @adapter.update_fog_exp2(handle, fog.color.hex, fog.density)
140
+ else
141
+ @adapter.update_fog(handle, fog.color.hex, fog.near, fog.far)
142
+ end
143
+ fog.mark_clean!
144
+ handle
145
+ end
146
+
128
147
  def sync_camera(object, handle)
129
148
  case object
130
149
  when OrthographicCamera
@@ -172,7 +191,10 @@ module Three
172
191
  end
173
192
 
174
193
  def sync_geometry(geometry, handle)
175
- return handle if built_in_geometry?(geometry)
194
+ if built_in_geometry?(geometry)
195
+ sync_geometry_operations(geometry, handle)
196
+ return handle
197
+ end
176
198
  return handle unless geometry_dirty?(geometry)
177
199
 
178
200
  if geometry.dirty_field?(:all) || geometry.dirty_field?(:index) || geometry.index&.dirty?
@@ -203,10 +225,19 @@ module Three
203
225
  @adapter.set_geometry_draw_range(handle, geometry.draw_range[:start], geometry.draw_range[:count])
204
226
  end
205
227
 
228
+ sync_geometry_operations(geometry, handle)
206
229
  geometry.mark_clean!
207
230
  handle
208
231
  end
209
232
 
233
+ def sync_geometry_operations(geometry, handle)
234
+ return unless geometry.respond_to?(:centered?) && geometry.centered?
235
+ return unless geometry.dirty_field?(:geometry_operations)
236
+
237
+ @adapter.center_geometry(handle)
238
+ geometry.mark_clean!(:geometry_operations)
239
+ end
240
+
210
241
  def geometry_dirty?(geometry)
211
242
  geometry.dirty? ||
212
243
  geometry.index&.dirty? ||
@@ -214,7 +245,10 @@ module Three
214
245
  end
215
246
 
216
247
  def built_in_geometry?(geometry)
217
- geometry.is_a?(BoxGeometry) || geometry.is_a?(PlaneGeometry) || geometry.is_a?(SphereGeometry)
248
+ geometry.is_a?(BoxGeometry) ||
249
+ geometry.is_a?(PlaneGeometry) ||
250
+ geometry.is_a?(SphereGeometry) ||
251
+ geometry.is_a?(TextGeometry)
218
252
  end
219
253
  end
220
254
 
@@ -6,6 +6,7 @@ require_relative "../core/buffer_geometry"
6
6
  require_relative "../geometries/box_geometry"
7
7
  require_relative "../geometries/plane_geometry"
8
8
  require_relative "../geometries/sphere_geometry"
9
+ require_relative "../geometries/text_geometry"
9
10
  require_relative "../lights/ambient_light"
10
11
  require_relative "../lights/directional_light"
11
12
  require_relative "../lights/hemisphere_light"
@@ -28,6 +29,7 @@ require_relative "../objects/line"
28
29
  require_relative "../objects/mesh"
29
30
  require_relative "../objects/points"
30
31
  require_relative "../objects/sprite"
32
+ require_relative "../scenes/fog"
31
33
  require_relative "../scenes/scene"
32
34
  require_relative "../textures/cube_texture"
33
35
  require_relative "../textures/rgbe_texture"
@@ -93,6 +95,14 @@ module Three
93
95
  @adapter.set_clear_color(renderer_handle, color, alpha)
94
96
  end
95
97
 
98
+ def set_renderer_tone_mapping(renderer_handle, value)
99
+ @adapter.set_renderer_tone_mapping(renderer_handle, value)
100
+ end
101
+
102
+ def set_renderer_tone_mapping_exposure(renderer_handle, value)
103
+ @adapter.set_renderer_tone_mapping_exposure(renderer_handle, value)
104
+ end
105
+
96
106
  def set_renderer_shadow_map(renderer_handle, enabled: nil, type: nil, auto_update: nil)
97
107
  @adapter.set_renderer_shadow_map(renderer_handle, enabled: enabled, type: type, auto_update: auto_update)
98
108
  end
@@ -247,6 +257,7 @@ module Three
247
257
  return @handles[key] if key && @handles.key?(key)
248
258
 
249
259
  handle = build_handle(object)
260
+ sync_geometry_operations(object, handle) if object.is_a?(BufferGeometry)
250
261
  @handles[key] = handle if key
251
262
  register_object_handle(object, handle)
252
263
  mark_clean_after_materialize(object)
@@ -259,6 +270,8 @@ module Three
259
270
  case object
260
271
  when Object3D
261
272
  sync_object3d(object, handle)
273
+ when Fog
274
+ sync_fog(object, handle)
262
275
  when BufferGeometry
263
276
  sync_geometry(object, handle)
264
277
  when Texture
@@ -281,6 +294,17 @@ module Three
281
294
  handle
282
295
  end
283
296
 
297
+ def add_event_listener(object, type, callback)
298
+ raise ArgumentError, "callback is required" unless callback
299
+
300
+ @adapter.add_event_listener(materialize(object), type, callback)
301
+ end
302
+
303
+ def cached?(object)
304
+ key = cache_key(object)
305
+ key ? @handles.key?(key) : false
306
+ end
307
+
284
308
  def traverse_handles(object, &block)
285
309
  return enum_for(:traverse_handles, object) unless block
286
310