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,874 @@
1
+ # three-rb Implementation Plan
2
+
3
+ This document describes an implementation plan for building a Ruby 3D library inspired by three.js. The local reference repositories are:
4
+
5
+ - `~/ghq/github.com/mrdoob/three.js` at `96f057533a`
6
+ - `~/ghq/github.com/ruby/ruby.wasm` at `ef0300a`
7
+
8
+ ## Recommendation
9
+
10
+ The first version should not be "three.js fully reimplemented in Ruby with a native WebGL renderer." It should be a Ruby library that lets users build a scene graph, math objects, geometry, and materials in Ruby, while delegating browser rendering to three.js through ruby.wasm.
11
+
12
+ The reason is practical. The value of three.js is not only classes like `Vector3` and `Mesh`; it also includes a large rendering runtime for WebGL/WebGPU, shader generation, texture management, loaders, XR, postprocessing, and many renderer-side systems. Rebuilding that renderer in Ruby from day one would delay any useful product milestone. A Ruby object model backed by the existing JavaScript renderer can reach "write a 3D scene in Ruby and see it in the browser" much earlier.
13
+
14
+ The recommended initial direction is:
15
+
16
+ - Build `three-rb` as a Ruby gem.
17
+ - Implement core Ruby APIs such as `Three::Vector3`, `Three::Object3D`, `Three::Scene`, `Three::Mesh`, `Three::BufferGeometry`, and `Three::Material`.
18
+ - In browser environments, use ruby.wasm's `js` bridge and delegate actual rendering to JavaScript three.js.
19
+ - Keep the renderer abstract so future backends such as `Three::Renderers::NativeOpenGLRenderer` or `Three::Renderers::ExportRenderer` can be added.
20
+ - Define the MVP as "a rotating cube written in Ruby and rendered in the browser."
21
+
22
+ ## Goals
23
+
24
+ The initial goal is to let Ruby developers build and render a 3D scene with code like this:
25
+
26
+ ```ruby
27
+ require "three"
28
+
29
+ scene = Three::Scene.new
30
+ camera = Three::PerspectiveCamera.new(75, aspect: 16.0 / 9.0, near: 0.1, far: 1000)
31
+ camera.position.z = 5
32
+
33
+ geometry = Three::BoxGeometry.new(1, 1, 1)
34
+ material = Three::MeshBasicMaterial.new(color: 0x00ff00)
35
+ cube = Three::Mesh.new(geometry, material)
36
+ scene.add(cube)
37
+
38
+ renderer = Three::Renderers::ThreeJSRenderer.new(canvas: "#canvas")
39
+ renderer.set_size(800, 600)
40
+
41
+ renderer.animation_loop do
42
+ cube.rotation.x += 0.01
43
+ cube.rotation.y += 0.01
44
+ renderer.render(scene, camera)
45
+ end
46
+ ```
47
+
48
+ Class names and concepts should remain familiar to three.js users, while method names and control flow should feel natural in Ruby. For example, `set_size` should be the primary Ruby API rather than `setSize`; three.js-style camelCase aliases can be added later as compatibility helpers where useful.
49
+
50
+ ## Non-Goals
51
+
52
+ The initial version should not attempt to deliver:
53
+
54
+ - A complete Ruby implementation of a WebGL/WebGPU renderer.
55
+ - A mechanical port of every three.js class.
56
+ - Shader node systems, WebXR, physics, editor support, and postprocessing all at once.
57
+ - Native Ruby implementations of advanced loaders such as glTF, DRACO, or KTX2.
58
+ - A production-ready native desktop renderer.
59
+
60
+ These can be future extensions. Adding them too early would expand the scope before the core API has stabilized.
61
+
62
+ ## Notes From Reference Repositories
63
+
64
+ ### three.js
65
+
66
+ The local three.js reference has package version `0.184.0`. Its public API is assembled through `src/Three.Core.js` and `src/Three.js`, which re-export many modules.
67
+
68
+ The main module boundaries are:
69
+
70
+ - `src/math`: `Vector2`, `Vector3`, `Matrix4`, `Quaternion`, `Euler`, `Color`, and related math types.
71
+ - `src/core`: `Object3D`, `BufferGeometry`, `BufferAttribute`, `EventDispatcher`, `Raycaster`, and related core types.
72
+ - `src/objects`: `Mesh`, `Line`, `Points`, `Group`, `Sprite`, and other renderable scene objects.
73
+ - `src/scenes`: `Scene`, `Fog`, and scene-level state.
74
+ - `src/cameras`: `PerspectiveCamera`, `OrthographicCamera`, `Camera`, and related camera types.
75
+ - `src/materials`: `Material`, `MeshBasicMaterial`, `MeshStandardMaterial`, `ShaderMaterial`, and related materials.
76
+ - `src/geometries`: generated geometry classes such as `BoxGeometry`.
77
+ - `src/renderers`: `WebGLRenderer`, WebGPU support, WebXR, shaders, textures, and render state.
78
+ - `examples/jsm`: addons such as `OrbitControls`, loaders, exporters, and postprocessing.
79
+
80
+ The key architectural observation is that the renderer is much more complex than the math, object, and material layers. `WebGLRenderer` owns canvas/context handling, render lists, material programs, textures, render state, shadow maps, XR integration, and the animation loop. In three-rb, the renderer should initially be treated as a backend rather than as a direct porting target.
81
+
82
+ ### ruby.wasm
83
+
84
+ ruby.wasm provides CRuby builds for WebAssembly/WASI. It includes npm packages such as `@ruby/wasm-wasi` and Ruby-version-specific packages. In the local repository, `@ruby/3.4-wasm-wasi` and `@ruby/wasm-wasi` are version `2.9.4`.
85
+
86
+ Ruby can access JavaScript through `require "js"`. This enables Ruby code to use the DOM, call JavaScript constructors, pass callbacks, and await JavaScript promises.
87
+
88
+ For three-rb, ruby.wasm is useful for:
89
+
90
+ - Running the Ruby API in the browser.
91
+ - Bridging Ruby `Three::Scene` objects to JavaScript `THREE.Scene` objects.
92
+ - Connecting to browser APIs such as `requestAnimationFrame`, canvas, DOM events, and dynamic imports.
93
+
94
+ Important constraints:
95
+
96
+ - The WASI target has limitations around threads and networking.
97
+ - Shipping a Ruby VM to the browser means initial load size and startup time must be measured.
98
+ - Frequent fine-grained Ruby-to-JavaScript calls can become overhead.
99
+ - Per-frame transfer of large vertex data from Ruby to JavaScript should be avoided.
100
+
101
+ ## Architecture
102
+
103
+ three-rb should be split into layers:
104
+
105
+ ```text
106
+ Ruby user code
107
+ |
108
+ v
109
+ Three public API
110
+ |
111
+ +-- Math layer
112
+ | Vector2 / Vector3 / Vector4 / Matrix3 / Matrix4 / Quaternion / Euler / Color
113
+ |
114
+ +-- Scene graph layer
115
+ | EventDispatcher / Object3D / Scene / Camera / Mesh / Group / Light
116
+ |
117
+ +-- Resource layer
118
+ | BufferAttribute / BufferGeometry / Texture / Material / Geometry builders
119
+ |
120
+ +-- Serialization and sync layer
121
+ | to_h / to_json / dirty tracking / backend handles
122
+ |
123
+ +-- Renderer abstraction
124
+ ThreeJSRenderer / ExportRenderer / future NativeRenderer
125
+ ```
126
+
127
+ ### Ruby Core
128
+
129
+ The Ruby core should avoid environment-specific dependencies where possible. It should run on MRI Ruby, ruby.wasm, and ideally future Ruby runtimes.
130
+
131
+ The initial core should include:
132
+
133
+ - Math types and matrix calculations.
134
+ - `Object3D` parent/child relationships, transforms, and world matrix updates.
135
+ - Data models for geometry, materials, cameras, and lights.
136
+ - Stable IDs for JSON output and backend synchronization.
137
+ - Dispose events and dirty flags.
138
+
139
+ ### Backend Abstraction
140
+
141
+ Renderers should not depend directly on `JS.global`. They should go through a backend interface.
142
+
143
+ ```ruby
144
+ module Three
145
+ module Backends
146
+ class Base
147
+ def materialize(object)
148
+ raise NotImplementedError
149
+ end
150
+
151
+ def sync(object)
152
+ raise NotImplementedError
153
+ end
154
+
155
+ def dispose(object)
156
+ raise NotImplementedError
157
+ end
158
+ end
159
+ end
160
+ end
161
+ ```
162
+
163
+ The initial backend should be `Three::Backends::ThreeJS`. It creates JavaScript objects corresponding to Ruby objects and synchronizes Ruby-side changes into JavaScript.
164
+
165
+ ### Synchronization Model
166
+
167
+ For the MVP, Ruby objects should be the source of truth. Each object can hold backend-specific handles.
168
+
169
+ ```ruby
170
+ class Object3D
171
+ attr_reader :backend_handles
172
+
173
+ def mark_dirty!(field = :all)
174
+ @dirty_fields << field
175
+ end
176
+ end
177
+ ```
178
+
179
+ Before `renderer.render(scene, camera)`, the renderer traverses the scene graph and syncs dirty objects to JavaScript. A simple full sync is acceptable in the earliest implementation, but the API should be designed around dirty tracking.
180
+
181
+ Sync granularity should include:
182
+
183
+ - Transform fields: `position`, `rotation`, `quaternion`, `scale`, `matrix`, `visible`
184
+ - Graph fields: `parent`, `children`
185
+ - Geometry fields: attributes, index, draw range, groups
186
+ - Material fields: color, opacity, transparent, wireframe, side
187
+ - Camera fields: fov, aspect, near, far, projection matrix
188
+
189
+ Geometry attributes are large. They should not be recreated on every render. On materialization, the backend should create `Float32Array`, `Uint16Array`, or `Uint32Array` objects and only resend them when the attribute changes.
190
+
191
+ ## Recommended Directory Structure
192
+
193
+ ```text
194
+ three-rb/
195
+ three-rb.gemspec
196
+ Gemfile
197
+ Rakefile
198
+ README.md
199
+ lib/
200
+ three-rb.rb
201
+ three.rb
202
+ three/
203
+ version.rb
204
+ constants.rb
205
+ math/
206
+ vector2.rb
207
+ vector3.rb
208
+ vector4.rb
209
+ matrix3.rb
210
+ matrix4.rb
211
+ quaternion.rb
212
+ euler.rb
213
+ color.rb
214
+ core/
215
+ event_dispatcher.rb
216
+ object3d.rb
217
+ buffer_attribute.rb
218
+ buffer_geometry.rb
219
+ clock.rb
220
+ layers.rb
221
+ scenes/
222
+ scene.rb
223
+ cameras/
224
+ camera.rb
225
+ perspective_camera.rb
226
+ orthographic_camera.rb
227
+ objects/
228
+ group.rb
229
+ mesh.rb
230
+ line.rb
231
+ points.rb
232
+ materials/
233
+ material.rb
234
+ mesh_basic_material.rb
235
+ mesh_normal_material.rb
236
+ geometries/
237
+ box_geometry.rb
238
+ plane_geometry.rb
239
+ sphere_geometry.rb
240
+ lights/
241
+ light.rb
242
+ ambient_light.rb
243
+ directional_light.rb
244
+ renderers/
245
+ renderer.rb
246
+ threejs_renderer.rb
247
+ backends/
248
+ base.rb
249
+ threejs.rb
250
+ wasm/
251
+ browser_boot.rb
252
+ test/
253
+ math/
254
+ core/
255
+ geometries/
256
+ backends/
257
+ examples/
258
+ browser/
259
+ cube/
260
+ index.html
261
+ main.rb
262
+ package.json
263
+ docs/
264
+ implementation-plan.md
265
+ api-design.md
266
+ wasm-notes.md
267
+ ```
268
+
269
+ ## API Design Rules
270
+
271
+ ### Naming
272
+
273
+ - The top-level module is `Three`.
274
+ - Class names should map to three.js concepts, for example `Three::Vector3`, `Three::Mesh`, and `Three::Scene`.
275
+ - Public Ruby methods should use `snake_case`.
276
+ - camelCase aliases such as `setSize` can be added later for three.js users.
277
+ - Mutating operations should follow three.js behavior: `vector.add(other)` mutates `self` and returns `self`.
278
+ - Ruby-friendly non-mutating operators such as `vector + other` may also be provided.
279
+
280
+ ### Initial Public API
281
+
282
+ The first stable API surface should include:
283
+
284
+ ```ruby
285
+ Three::Vector3.new(x = 0, y = 0, z = 0)
286
+ Three::Vector3#set(x, y, z)
287
+ Three::Vector3#copy(other)
288
+ Three::Vector3#clone
289
+ Three::Vector3#add(other)
290
+ Three::Vector3#sub(other)
291
+ Three::Vector3#multiply_scalar(value)
292
+ Three::Vector3#normalize
293
+ Three::Vector3#length
294
+
295
+ Three::Object3D#add(child)
296
+ Three::Object3D#remove(child)
297
+ Three::Object3D#traverse { |object| ... }
298
+ Three::Object3D#update_matrix
299
+ Three::Object3D#update_matrix_world(force = false)
300
+
301
+ Three::Scene.new
302
+ Three::PerspectiveCamera.new(fov = 50, aspect: 1, near: 0.1, far: 2000)
303
+ Three::Mesh.new(geometry = Three::BufferGeometry.new, material = Three::MeshBasicMaterial.new)
304
+ Three::BoxGeometry.new(width = 1, height = 1, depth = 1)
305
+ Three::MeshBasicMaterial.new(color: 0xffffff, wireframe: false)
306
+ Three::Renderers::ThreeJSRenderer.new(canvas:)
307
+ Three::Renderers::ThreeJSRenderer#render(scene, camera)
308
+ Three::Renderers::ThreeJSRenderer#animation_loop { |time| ... }
309
+ ```
310
+
311
+ ### Options Hashes
312
+
313
+ three.js constructor parameter objects should become Ruby keyword arguments.
314
+
315
+ ```ruby
316
+ Three::MeshBasicMaterial.new(color: 0xffcc00, transparent: true, opacity: 0.5)
317
+ ```
318
+
319
+ Internally, classes may still keep a `parameters` hash to support later JSON reconstruction.
320
+
321
+ ### Events
322
+
323
+ The equivalent of three.js `EventDispatcher` should use Ruby blocks.
324
+
325
+ ```ruby
326
+ mesh.on(:dispose) { |event| puts event.type }
327
+ mesh.dispatch_event(:dispose)
328
+ ```
329
+
330
+ ## Implementation Phases
331
+
332
+ ### Phase 0: Gem Foundation
333
+
334
+ The goal is to create a Ruby project structure that can support continued development.
335
+
336
+ Tasks:
337
+
338
+ - Add `three-rb.gemspec`.
339
+ - Add `lib/three-rb.rb`, `lib/three.rb`, and `lib/three/version.rb`.
340
+ - Choose a test runner. `minitest` is sufficient initially.
341
+ - Make `bundle exec rake test` work.
342
+ - Avoid making `rubocop` too strict at the start; use minimal linting if needed.
343
+ - Add quick start and development commands to the README.
344
+
345
+ Completion criteria:
346
+
347
+ - Tests can run after `bundle install`.
348
+ - `require "three"` works as a gem entrypoint.
349
+ - The project layout is ready for CI.
350
+
351
+ ### Phase 1: Math Layer
352
+
353
+ The math layer is valuable even without a renderer and is the foundation for the rest of the library.
354
+
355
+ Implementation targets:
356
+
357
+ - `Vector2`
358
+ - `Vector3`
359
+ - `Vector4`
360
+ - `Euler`
361
+ - `Quaternion`
362
+ - `Matrix3`
363
+ - `Matrix4`
364
+ - `Color`
365
+ - `MathUtils`
366
+
367
+ Priority:
368
+
369
+ 1. `Vector3`
370
+ 2. `Matrix4`
371
+ 3. `Quaternion`
372
+ 4. `Euler`
373
+ 5. `Color`
374
+
375
+ Important details:
376
+
377
+ - Use the same column-major matrix representation as three.js.
378
+ - Use epsilon comparisons for floating-point tests.
379
+ - Design `Euler` and `Quaternion` with change callbacks so `Object3D#rotation` and `Object3D#quaternion` can stay synchronized.
380
+ - `Color` should accept values like `0xff00aa`, `"#ff00aa"`, and RGB floats.
381
+
382
+ Tests:
383
+
384
+ - Unit tests for math operations.
385
+ - `clone` and `copy` return or update distinct objects correctly.
386
+ - Matrix compose/decompose round trips.
387
+ - Quaternion/Euler round trips.
388
+ - A small set of fixtures matching known three.js results.
389
+
390
+ ### Phase 2: Core Scene Graph
391
+
392
+ The goal is to implement object hierarchy and transform updates.
393
+
394
+ Implementation targets:
395
+
396
+ - `EventDispatcher`
397
+ - `Object3D`
398
+ - `Group`
399
+ - `Scene`
400
+ - `Camera`
401
+ - `PerspectiveCamera`
402
+ - `OrthographicCamera`
403
+ - `Clock`
404
+ - `Layers`
405
+
406
+ Major `Object3D` properties:
407
+
408
+ - `id`
409
+ - `uuid`
410
+ - `name`
411
+ - `type`
412
+ - `parent`
413
+ - `children`
414
+ - `position`
415
+ - `rotation`
416
+ - `quaternion`
417
+ - `scale`
418
+ - `matrix`
419
+ - `matrix_world`
420
+ - `matrix_auto_update`
421
+ - `matrix_world_auto_update`
422
+ - `matrix_world_needs_update`
423
+ - `visible`
424
+ - `user_data`
425
+
426
+ Major methods:
427
+
428
+ - `add`
429
+ - `remove`
430
+ - `remove_from_parent`
431
+ - `clear`
432
+ - `traverse`
433
+ - `traverse_visible`
434
+ - `get_object_by_name`
435
+ - `update_matrix`
436
+ - `update_matrix_world`
437
+ - `look_at`
438
+ - `to_h`
439
+
440
+ Completion criteria:
441
+
442
+ - A scene can contain meshes, groups, and cameras.
443
+ - Parent/child transforms are reflected in world matrices.
444
+ - Updating `rotation` also updates `quaternion`.
445
+ - Updating `quaternion` also updates `rotation`.
446
+
447
+ ### Phase 3: Geometry and Materials
448
+
449
+ The goal is to create renderable objects that can be passed to a backend.
450
+
451
+ Implementation targets:
452
+
453
+ - `BufferAttribute`
454
+ - `Float32BufferAttribute`
455
+ - `Uint16BufferAttribute`
456
+ - `Uint32BufferAttribute`
457
+ - `BufferGeometry`
458
+ - `BoxGeometry`
459
+ - `PlaneGeometry`
460
+ - `SphereGeometry`
461
+ - `Material`
462
+ - `MeshBasicMaterial`
463
+ - `MeshNormalMaterial`
464
+ - `Mesh`
465
+ - `Line`
466
+ - `Points`
467
+
468
+ Initial `BufferGeometry` methods:
469
+
470
+ - `set_index`
471
+ - `get_index`
472
+ - `set_attribute`
473
+ - `get_attribute`
474
+ - `delete_attribute`
475
+ - `add_group`
476
+ - `clear_groups`
477
+ - `set_draw_range`
478
+ - `compute_bounding_box`
479
+ - `compute_bounding_sphere`
480
+ - `to_h`
481
+
482
+ For the first version, geometry data can be stored as Ruby `Array<Numeric>`. However, attributes should carry a `component_type` so the bridge can convert them to JavaScript TypedArrays.
483
+
484
+ Completion criteria:
485
+
486
+ - `BoxGeometry` generates position, normal, uv, and index data.
487
+ - `MeshBasicMaterial` supports color, wireframe, and opacity.
488
+ - A `Mesh` with geometry and material can be added to a scene.
489
+
490
+ ### Phase 4: Browser Renderer MVP
491
+
492
+ The goal is to render a Ruby-authored scene in the browser.
493
+
494
+ Current implementation status:
495
+
496
+ - `Three::Backends::ThreeJS` exists with an injectable adapter boundary.
497
+ - `Three::Renderers::ThreeJSRenderer` exists and delegates renderer creation, sizing, animation loops, scene syncing, and render calls to the backend.
498
+ - The bridge can materialize `Scene`, `Group`, `Object3D`, external loaded `Object3D` handles, `PerspectiveCamera`, `OrthographicCamera`, `Mesh`, `Line`, `Points`, `Sprite`, `AmbientLight`, `DirectionalLight`, `PointLight`, `HemisphereLight`, `Texture`, `CubeTexture`, `RGBETexture`, `BoxGeometry`, `PlaneGeometry`, `SphereGeometry`, generic `BufferGeometry`, `BufferAttribute`, `MeshBasicMaterial`, `LineBasicMaterial`, `PointsMaterial`, `SpriteMaterial`, `MeshLambertMaterial`, `MeshMatcapMaterial`, `MeshToonMaterial`, `MeshStandardMaterial`, `MeshPhysicalMaterial`, `MeshNormalMaterial`, and `ShadowMaterial`.
499
+ - Unit tests cover materialization, handle caching, transform syncing, rendering delegation, and disposal through a fake three.js adapter.
500
+ - `examples/browser/cube` loads pnpm-managed ruby.wasm and three.js browser packages, loads this library from `lib/`, and renders a rotating cube through `Three::Renderers::ThreeJSRenderer`.
501
+ - `examples/browser/cube/smoke_test.mjs` provides an opt-in Playwright smoke test that serves the repository root, waits for the example to reach `Running`, and samples the WebGL canvas for nonblank pixels.
502
+ - `examples/browser/composition` renders an `OrthographicCamera` view with ambient/directional/point/hemisphere lights, directional shadow mapping, `PlaneGeometry`, `SphereGeometry`, grouped meshes, `TextureLoader` repeat/wrap/filter settings, `MeshLambertMaterial`, `MeshPhongMaterial`, `MeshStandardMaterial`, `MeshNormalMaterial`, `ShadowMaterial`, backend material/texture disposal, and a material color update through the same renderer path.
503
+ - `examples/browser/textures` focuses on `TextureLoader`, `RGBELoader`, repeat/wrap/filter/UV-transform settings, `MeshPhysicalMaterial` standard/physical texture maps, `MeshMatcapMaterial` matcap texture assignment, `MeshToonMaterial` gradient-map texture assignment, and an HDR environment texture on textured meshes.
504
+ - `examples/browser/cubemap` focuses on `CubeTextureLoader`, `CubeTexture`, and scene `background`/`environment` synchronization.
505
+ - `examples/browser/gltf` focuses on `GLTFLoader`, optional `DRACOLoader` decoder configuration for compressed geometry, adding loaded external scenes to the Ruby-authored scene graph, playing loaded animation clips through `AnimationMixer`, and disposing loaded subtrees through the renderer API.
506
+ - `examples/browser/serialization` focuses on exporting a Ruby-authored scene to JSON, parsing it back into Ruby objects, preserving shared resources, and rendering the loaded scene.
507
+ - `test/fixtures/scene_export_v1.json` and `test/three/exporters/three_json_fixture_test.rb` provide saved fixture regression coverage for the exporter/loader format, including physical material texture slots, shared resources, instancing, line/points/sprite primitives, and RGBE environment textures.
508
+ - `examples/browser/picking` focuses on `Three::Raycaster`, mapping three.js intersections back to Ruby objects, and updating selected mesh materials from browser click coordinates.
509
+ - `examples/browser/primitives` focuses on `Line`, `Points`, `Sprite`, `LineBasicMaterial`, `PointsMaterial`, `SpriteMaterial`, and generic `BufferGeometry` attributes outside the `Mesh` path.
510
+ - `examples/browser/postprocessing` focuses on an explicit render pipeline using `Three::Postprocessing::EffectComposer`, `RenderPass`, `UnrealBloomPass`, `DotScreenPass`, `OutputPass`, composer sizing, and pass property/uniform updates.
511
+ - The browser bridge exposes the three.js `OrbitControls` addon through `Three::Controls::OrbitControls`.
512
+ - Browser examples share common ruby.wasm boot and Playwright smoke-test helpers under `examples/browser/shared`.
513
+ - CI runs the Ruby unit tests and Playwright browser smoke tests with pnpm-managed browser dependencies.
514
+ - Core scene, material, and geometry objects expose dirty state, and the Three.js backend skips clean transform, material, geometry, and child-list sync work.
515
+ - `Three::Matrix3` is implemented with inverse/transpose, normal-matrix, and UV-transform helpers; `Vector3` can apply `Matrix3` values.
516
+ - `Texture` exposes `offset`, `repeat`, `center`, `rotation`, `matrix_auto_update`, and `matrix`, and the Three.js backend synchronizes these UV-transform settings.
517
+ - `Texture` exposes `mapping` and `color_space`; `RGBETexture` defaults to equirectangular reflection mapping and linear-sRGB color space for HDR environment maps.
518
+ - `Three::Clock` and `Three::Layers` are implemented; `Object3D#layers` is serialized and synchronized to Three.js `layers.mask`.
519
+ - glTF animation is prioritized ahead of broader postprocessing or additional loader expansion because it builds directly on the existing `GLTFLoader`, `ExternalObject3D`, and browser smoke infrastructure while providing a visible user-facing capability with bounded backend API surface.
520
+ - `Three::Renderers::ThreeJSRenderer#dispose` exposes backend disposal and can explicitly dispose a material's mapped textures with `dispose_textures: true`.
521
+ - `Three::Renderers::ThreeJSRenderer#traverse_handles` and `#dispose_subtree` expose loaded-asset traversal and cleanup without changing Ruby `Object3D#traverse`.
522
+ - Loaded asset traversal/disposal design and implementation status are documented in `docs/loaded-assets-design.md`.
523
+ - `Three::Loaders::GLTFLoader` can configure a JavaScript `DRACOLoader` through `draco_decoder_path:` and optional `draco_decoder_config:`. This keeps compressed glTF support in the existing delegated-loader boundary instead of adding a Ruby decoder.
524
+ - `MeshStandardMaterial` supports common Ruby-side PBR texture slots such as `normal_map`, `roughness_map`, and `metalness_map`, and backend resource ownership helpers track all modeled texture slots.
525
+ - `MeshPhysicalMaterial` extends `MeshStandardMaterial` with anisotropy, clearcoat, transmission, iridescence, sheen, dispersion, specular, attenuation parameters, physical texture slots, backend sync, JSON export/load, and browser smoke coverage.
526
+ - `MeshPhongMaterial` supports specular color, emissive color, shininess, and common Phong texture slots including `specular_map`.
527
+ - `MeshMatcapMaterial` supports matcap, color map, alpha, bump, displacement, and normal texture slots with backend sync, JSON export/load, resource disposal, and browser smoke coverage.
528
+ - `MeshToonMaterial` supports color/emissive parameters plus map, gradient, light, AO, emissive, bump, normal, displacement, and alpha texture slots with backend sync, JSON export/load, resource disposal, and texture browser smoke coverage.
529
+ - `ShadowMaterial` supports transparent shadow-catching surfaces with color/fog parameters, backend sync, JSON export/load, and browser smoke coverage.
530
+ - `Sprite` and `SpriteMaterial` support textured billboard markers with center, rotation, size attenuation, backend sync, JSON export/load, resource disposal, and primitives browser smoke coverage.
531
+ - `Object3D#cast_shadow`, `Object3D#receive_shadow`, renderer shadow map configuration, and directional light shadow camera settings are supported through the Three.js backend.
532
+ - The Three.js backend internals are split into materialization, synchronization, parameter conversion, resource management, and ruby.wasm adapter files so renderer additions do not keep growing one monolithic backend file.
533
+ - `MeshPhysicalMaterial` was prioritized before additional addon loaders because it extends the existing material, texture-slot, JSON, disposal, and browser-smoke boundaries without adding new decoder or renderer-pipeline constraints.
534
+ - `RGBELoader`/`RGBETexture` was prioritized after `MeshPhysicalMaterial` because HDR environment maps directly improve PBR and physical-material scenes while reusing the existing texture, scene environment, JSON, and browser-smoke boundaries.
535
+ - `DRACOLoader` was prioritized before postprocessing because a compressed glTF fixture can verify it through the existing GLTFLoader, ExternalObject3D, loaded-asset disposal, and browser-smoke paths with a small API addition.
536
+ - Postprocessing was prioritized after `DRACOLoader` because the core render, material, texture, glTF, and interaction paths now have enough coverage to justify a dedicated render-pipeline example. The first wrapper set intentionally stayed small: `EffectComposer`, `RenderPass`, `UnrealBloomPass`, and `OutputPass`.
537
+ - `DotScreenPass` was added as the first follow-up postprocessing pass because it strengthens the existing composer example with deterministic shader-pass uniform updates without requiring render targets or external assets.
538
+ - The next implementation step is adding more material classes, postprocessing passes, render targets, or addon loaders only when an example or API target needs them; KTX2 should wait until texture-compression fixture coverage is needed.
539
+ - Public release readiness is tracked in `docs/release-readiness.md`; before adding broad new feature scope, prioritize install smoke coverage, CI gates, and public-scope documentation.
540
+ - The current resume point for the next implementation session is tracked in `docs/next-work.md`.
541
+
542
+ Recommended structure:
543
+
544
+ - `examples/browser/cube/index.html`
545
+ - `examples/browser/cube/main.rb`
546
+ - A JavaScript boot script starts the ruby.wasm VM.
547
+ - JavaScript imports three.js and exposes either `globalThis.THREE` or an explicit module handle to Ruby.
548
+ - `Three::Renderers::ThreeJSRenderer` uses `JS.global[:THREE]` to create JavaScript objects.
549
+
550
+ Initial bridge responsibilities:
551
+
552
+ - Ruby `Scene` -> JS `THREE.Scene`
553
+ - Ruby `PerspectiveCamera` -> JS `THREE.PerspectiveCamera`
554
+ - Ruby `OrthographicCamera` -> JS `THREE.OrthographicCamera`
555
+ - Ruby `AmbientLight` -> JS `THREE.AmbientLight`
556
+ - Ruby `DirectionalLight` -> JS `THREE.DirectionalLight`
557
+ - Ruby `BoxGeometry` -> JS `THREE.BufferGeometry` or JS `THREE.BoxGeometry`
558
+ - Ruby `MeshBasicMaterial` -> JS `THREE.MeshBasicMaterial`
559
+ - Ruby `MeshLambertMaterial` -> JS `THREE.MeshLambertMaterial`
560
+ - Ruby `Texture` -> JS `THREE.TextureLoader.load(...)`
561
+ - Ruby `Mesh` -> JS `THREE.Mesh`
562
+ - Ruby `OrbitControls` -> JS addon `OrbitControls`
563
+ - Ruby `Object3D` transform -> JS object transform
564
+ - `animation_loop` -> `requestAnimationFrame`
565
+
566
+ For early built-in geometries such as `BoxGeometry`, it is acceptable to directly create JS `THREE.BoxGeometry`. Still, design a `BufferGeometry` materialization path so Ruby-generated geometry can later be sent to JS as buffers.
567
+
568
+ Completion criteria:
569
+
570
+ - A cube appears in the browser.
571
+ - Ruby code can update the cube's rotation.
572
+ - Canvas resizing works.
573
+ - `dispose` releases JS geometry/material resources.
574
+
575
+ ### Phase 5: Serialization and Export
576
+
577
+ The goal is to make three-rb scenes useful beyond immediate rendering.
578
+
579
+ Current implementation status:
580
+
581
+ - `Three::Exporters::ThreeJSONExporter` exports a Ruby-authored object tree with separate deduplicated `geometries`, `materials`, and `textures` arrays.
582
+ - `Object3D#to_json` delegates to the exporter format when the full `three` entrypoint is loaded.
583
+ - The exporter serializes transform arrays directly instead of relying on a potentially stale local matrix, and stores resource references by UUID.
584
+ - `Three::Exporters::ThreeJSONExporter.new(deterministic_ids: true)` assigns traversal-order stable IDs for regression fixtures while preserving real UUIDs by default.
585
+ - `Three::Loaders::ThreeJSONLoader#parse` reconstructs the exporter format into Ruby `Scene`, camera, light, mesh, material, texture, geometry, and `InstancedMesh` objects while preserving shared resource identity.
586
+
587
+ Implementation targets:
588
+
589
+ - `Object3D#to_h`
590
+ - `Object3D#to_json`
591
+ - `Geometry#to_h`
592
+ - `Material#to_h`
593
+ - `Three::Exporters::ThreeJSONExporter`
594
+
595
+ Use cases:
596
+
597
+ - Build a scene on a Ruby server and send it to a browser as JSON.
598
+ - Save scene artifacts even when no native renderer is available.
599
+ - Use deterministic JSON fixtures for regression tests.
600
+
601
+ Completion criteria:
602
+
603
+ - A simple scene can be exported to JSON.
604
+ - A minimal loader can reconstruct a scene from JSON.
605
+ - Equivalent scenes produce deterministic JSON.
606
+
607
+ ### Phase 6: Interaction and Controls
608
+
609
+ The goal is to support interactive browser experiences.
610
+
611
+ Initially, wrap JavaScript `OrbitControls` instead of rewriting it in Ruby.
612
+
613
+ Implementation targets:
614
+
615
+ - `Three::Controls::OrbitControls`
616
+ - Pointer, wheel, and key event bridges.
617
+ - Camera update integration with the render loop.
618
+
619
+ Completion criteria:
620
+
621
+ - Ruby code can call `OrbitControls.new(camera, renderer.dom_element)`.
622
+ - Mouse drag and wheel zoom work.
623
+
624
+ ### Phase 7: Asset Loading
625
+
626
+ The goal is to handle external assets needed for practical scenes.
627
+
628
+ Initial loaders should delegate to JavaScript loaders.
629
+
630
+ Implemented delegate loaders so far:
631
+
632
+ 1. `TextureLoader`
633
+ 2. `CubeTextureLoader`
634
+ 3. `RGBELoader`
635
+ 4. `GLTFLoader`
636
+ 5. `DRACOLoader` through `GLTFLoader#draco_decoder_path`
637
+
638
+ Loader priority:
639
+
640
+ 1. Additional three.js addon loaders as examples require them.
641
+
642
+ Ruby API:
643
+
644
+ ```ruby
645
+ loader = Three::Loaders::GLTFLoader.new
646
+ loader.load("model.glb") do |gltf|
647
+ scene.add(gltf.scene)
648
+ end
649
+ ```
650
+
651
+ Important details:
652
+
653
+ - Do not depend on ruby.wasm networking for asset loading; let JavaScript `fetch` and three.js loaders handle it.
654
+ - Avoid loading binary assets into Ruby when a JavaScript loader result can be wrapped.
655
+ - Keep loaded three.js assets opaque by default; see `docs/loaded-assets-design.md` for the `ExternalObject3D` traversal and disposal design.
656
+ - Use `renderer.dispose_subtree(gltf.scene, remove: true)` for high-level loaded-asset cleanup. The renderer defaults to disposing textures; the lower backend API keeps texture disposal opt-in.
657
+
658
+ Completion criteria:
659
+
660
+ - Textures can be assigned to materials, including common `MeshStandardMaterial` PBR texture slots.
661
+ - glTF models can be added to a scene.
662
+ - Loaded glTF model resources can be explicitly disposed.
663
+
664
+ ### Phase 8: Renderer Maturity
665
+
666
+ The goal is to move from a wrapper into a practical 3D library.
667
+
668
+ Candidates:
669
+
670
+ - Lights: `AmbientLight`, `DirectionalLight`, `PointLight`
671
+ - Materials: `MeshLambertMaterial`, `MeshPhongMaterial`, `MeshStandardMaterial`
672
+ - Textures: wrapping, filtering, color space
673
+ - Render targets
674
+ - Shadows
675
+ - Instancing
676
+ - Raycaster
677
+ - Additional postprocessing wrappers
678
+ - WebGPU renderer wrapper
679
+
680
+ Current instancing direction:
681
+
682
+ - Prefer `Three::InstancedMesh` for large repeated geometry before optimizing thousands of individual `Mesh` objects.
683
+ - Keep Ruby as the source of truth for instance matrices and batch them into three.js with `setMatrixAt` during dirty sync.
684
+ - Treat the initial `InstancedMesh` API as matrix-and-color focused: `capacity`, `count`, `set_matrix_at`, `get_matrix_at`, `set_color_at`, `get_color_at`, `instance_matrix_needs_update!`, and `instance_color_needs_update!`. `capacity` is fixed at construction because three.js allocates the instance buffers then; `count` may be lowered within that capacity to render fewer active instances.
685
+ - Browser verification should include a 1000-count instanced scene so Phase 8 measures a realistic high-volume path, not only small object graphs.
686
+ - `pnpm benchmark:browser:instanced-mesh-sync` measures a 1000-count `InstancedMesh` path separately from `pnpm benchmark:browser:mesh-sync`, including whole-object transform updates and per-instance matrix updates.
687
+
688
+ Current sync performance direction:
689
+
690
+ - Track dirty descendants on `Object3D` ancestors so clean subtrees can be skipped during backend sync.
691
+ - Propagate dirty state from shared resources upward: `Texture` -> `Material` -> `Mesh` -> ancestor `Object3D`.
692
+ - Keep render-time world matrix recomputation separate from backend dirty tracking. A clean render should not make transforms dirty merely because matrices were recomputed.
693
+ - Use `pnpm benchmark:browser:mesh-sync` to measure 1000 individual `Mesh` sync before and after sync-layer changes.
694
+
695
+ Completion criteria:
696
+
697
+ - A basic lighting scene works.
698
+ - Directional shadow mapping can be enabled and verified in a browser smoke test.
699
+ - Material and texture disposal does not leak resources.
700
+ - Synchronizing 1000 repeated meshes through `InstancedMesh` remains interactive.
701
+ - A benchmark separately measures 1000 individual `Mesh` transform sync to decide whether backend batching is needed there too.
702
+ - Pointer picking can identify Ruby-authored meshes through `Three::Raycaster`.
703
+ - A Ruby-authored scene can render through an explicit `EffectComposer` pipeline with a render pass, bloom pass, and output pass.
704
+
705
+ ### Phase 9: Native Renderer Evaluation
706
+
707
+ Only after the earlier phases should a Ruby-native renderer be evaluated.
708
+
709
+ Options:
710
+
711
+ - OpenGL + GLFW backend
712
+ - Vulkan wrapper
713
+ - SDL + OpenGL
714
+ - Server-side software renderer
715
+ - Choosing glTF export as the non-browser path
716
+
717
+ If a native renderer is added, the public API should stay aligned with the existing core. Only the renderer backend should change.
718
+
719
+ ## ruby.wasm Adoption Decision
720
+
721
+ ruby.wasm fits this use case, but its role should be limited.
722
+
723
+ Good uses:
724
+
725
+ - Writing browser scenes in Ruby.
726
+ - Connecting a Ruby object model to the JavaScript three.js renderer.
727
+ - Writing event handlers and animation loops as Ruby blocks.
728
+ - Shipping interactive browser examples.
729
+
730
+ Poor uses:
731
+
732
+ - Calling low-level WebGL APIs from Ruby at high frequency.
733
+ - Updating large vertex buffers across the Ruby/JavaScript boundary every frame.
734
+ - Implementing network- or thread-heavy loaders entirely in Ruby.
735
+ - Pages where the startup size of a Ruby VM is unacceptable.
736
+
737
+ Therefore, three-rb should adopt ruby.wasm as the browser runtime while keeping hot paths in JavaScript three.js.
738
+
739
+ ## Performance Strategy
740
+
741
+ Do not over-optimize before the API exists, but avoid structural mistakes.
742
+
743
+ - Do not recreate all JS objects every render.
744
+ - Do not convert geometry attributes into TypedArrays every render.
745
+ - Avoid large numbers of tiny Ruby-to-JavaScript property calls.
746
+ - Use dirty flags and sync only changed objects.
747
+ - Do not immediately reflect `position.x = ...` to JS; batch transform updates before rendering.
748
+ - Keep `update_matrix_world` separate from backend dirty tracking. Rendering may recompute world matrices every frame, but clean transforms should stay clean unless user-facing transform state changes.
749
+ - Require explicit `needs_update!` calls for large geometry changes.
750
+
751
+ Initial benchmarks:
752
+
753
+ - 1 cube animation
754
+ - 100 cubes with transform sync
755
+ - 1000 cubes through `InstancedMesh`
756
+ - 1000 individual cubes with transform sync (`pnpm benchmark:browser:mesh-sync`)
757
+ - 1000 cubes through `InstancedMesh` sync (`pnpm benchmark:browser:instanced-mesh-sync`)
758
+ - 1 mesh with 100k vertices
759
+ - 10 textures
760
+ - 1 glTF model
761
+
762
+ ## Test Plan
763
+
764
+ ### Ruby Unit Tests
765
+
766
+ Targets:
767
+
768
+ - Math operations
769
+ - Matrix compose/decompose
770
+ - Scene graph add/remove
771
+ - `Object3D` traversal
772
+ - Camera projection
773
+ - Geometry generation
774
+ - Material options
775
+ - JSON serialization
776
+
777
+ ### Golden Tests
778
+
779
+ Create a small set of fixtures that match known three.js outputs.
780
+
781
+ Examples:
782
+
783
+ - Vertex and index counts for `BoxGeometry.new(1, 1, 1)`.
784
+ - Projection matrix for `PerspectiveCamera.new(75, aspect: 2)`.
785
+ - Result of `Vector3.new(1, 2, 3).normalize`.
786
+
787
+ Use epsilon comparisons instead of exact equality for floating-point values.
788
+
789
+ ### Browser Smoke Tests
790
+
791
+ Use Playwright or a similar tool to open the browser example and verify:
792
+
793
+ - A canvas exists.
794
+ - A WebGL context is created.
795
+ - The first frame draws non-empty pixels.
796
+ - Pixels change after the animation loop advances.
797
+
798
+ ### ruby.wasm Smoke Tests
799
+
800
+ Targets:
801
+
802
+ - The Ruby VM starts.
803
+ - `require "three"` works.
804
+ - `require "js"` works.
805
+ - Ruby can create a JS `THREE.Scene`.
806
+ - A Ruby callback can be called from `requestAnimationFrame`.
807
+
808
+ ## License Strategy
809
+
810
+ This project is MIT licensed. three.js is also MIT licensed, but the project should follow these rules:
811
+
812
+ - If specific three.js code is ported, record the source file.
813
+ - Preserve required copyright and MIT license notices.
814
+ - Prefer using three.js API behavior as a reference while writing Ruby implementations, instead of copying code verbatim.
815
+ - If large assets such as shader chunks are imported, document them in `NOTICE` or project docs.
816
+
817
+ ## Risks and Mitigations
818
+
819
+ | Risk | Impact | Mitigation |
820
+ | --- | --- | --- |
821
+ | A native renderer delays the MVP | High | Limit Phase 4 to the JS three.js backend |
822
+ | Ruby/JavaScript boundary overhead is too high | Medium | Design around dirty sync and batched conversion |
823
+ | ruby.wasm startup size is large | Medium | Measure in examples; evaluate CDN, cache, and lazy loading |
824
+ | The project chases full three.js compatibility too early | Medium | Limit the initial API to the cube demo subset |
825
+ | Ruby style conflicts with three.js compatibility | Medium | Make snake_case primary and keep camelCase in a compatibility layer |
826
+ | Geometry storage becomes hard to optimize later | Medium | Give `BufferAttribute` component types and update ranges early |
827
+ | Loaders become too complex | Medium | Wrap JS loaders initially |
828
+ | License attribution is missed | High | Track source files in docs and file headers when porting code |
829
+
830
+ ## MVP Definition
831
+
832
+ The MVP is complete when:
833
+
834
+ - `require "three"` works.
835
+ - Ruby can create scenes, cameras, meshes, materials, and geometry.
836
+ - A browser example starts ruby.wasm.
837
+ - A Ruby-authored cube scene renders through the JS three.js backend.
838
+ - Cube rotation animation is written as a Ruby block.
839
+ - Ruby unit tests and a browser smoke test exist.
840
+ - The README includes usage instructions.
841
+
842
+ ## First 10 Tasks
843
+
844
+ - [x] Add `three-rb.gemspec`, `Gemfile`, `Rakefile`, `lib/three-rb.rb`, and `lib/three.rb`.
845
+ - [x] Add `Three::Version` and the module skeleton.
846
+ - [x] Implement `Vector3`, `Matrix4`, `Quaternion`, `Euler`, and `Color`.
847
+ - [x] Add math unit tests.
848
+ - [x] Implement `EventDispatcher` and `Object3D`.
849
+ - [x] Implement `Scene`, `Camera`, `PerspectiveCamera`, and `Group`.
850
+ - [x] Implement `BufferAttribute`, `BufferGeometry`, and `BoxGeometry`.
851
+ - [x] Implement `Material`, `MeshBasicMaterial`, and `Mesh`.
852
+ - [x] Add skeletons for `Three::Backends::ThreeJS` and `ThreeJSRenderer`.
853
+ - [x] Build `examples/browser/cube` and render a cube with ruby.wasm + three.js.
854
+
855
+ ## Next Tasks
856
+
857
+ 1. Prefer feature work that has visible user value, reuses the current Three.js backend boundary, and can be verified by deterministic browser smoke tests.
858
+ 2. Keep the public release gate passing: Ruby tests, gem install smoke, browser smoke tests, and gem build.
859
+ 3. Expand postprocessing beyond `RenderPass`/`UnrealBloomPass` only when a dedicated example requires a new pass or render-target API; add KTX2 or other decoder loaders only with fixtures that require them.
860
+ 4. Keep `ThreeJSONExporter` and `ThreeJSONLoader` saved fixtures current whenever the format changes.
861
+ 5. Keep Ruby-side resource ownership helpers in sync whenever new material texture slots are introduced.
862
+ 6. Keep reviewing low-risk dependency updates after checking their CI results.
863
+
864
+ ## Resolved Early Decisions
865
+
866
+ The original MVP decisions are now resolved:
867
+
868
+ - Use `minitest` for the first public release test suite.
869
+ - Use pnpm-managed local browser dependencies for ruby.wasm and three.js, avoiding CDN runtime drift and browser ORB failures.
870
+ - Defer broad camelCase three.js compatibility aliases until after the first public release; snake-case Ruby methods are the documented API style.
871
+ - Start geometry and attribute data with standard Ruby `Array` values instead of requiring `numo-narray`.
872
+ - Publish the gem as `three-rb`.
873
+
874
+ The first public target is `0.1.0` browser-first alpha. Its completion definition and release gate are tracked in `docs/release-readiness.md`.