webxr 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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +7 -0
  4. data/README.md +138 -0
  5. data/Rakefile +21 -0
  6. data/examples/ar_demo.html +238 -0
  7. data/examples/ar_hit_test.rb +157 -0
  8. data/examples/basic_vr.rb +110 -0
  9. data/examples/controller_input.rb +91 -0
  10. data/examples/hand_tracking.rb +124 -0
  11. data/examples/hello_webxr.html +288 -0
  12. data/examples/inline_demo.html +261 -0
  13. data/examples/server.rb +34 -0
  14. data/examples/vr_scene_demo.html +330 -0
  15. data/lib/webxr/ar/anchor.rb +83 -0
  16. data/lib/webxr/ar/hit_test_result.rb +54 -0
  17. data/lib/webxr/ar/hit_test_source.rb +34 -0
  18. data/lib/webxr/ar/ray.rb +90 -0
  19. data/lib/webxr/constants.rb +61 -0
  20. data/lib/webxr/core/frame.rb +155 -0
  21. data/lib/webxr/core/render_state.rb +47 -0
  22. data/lib/webxr/core/session.rb +212 -0
  23. data/lib/webxr/core/system.rb +122 -0
  24. data/lib/webxr/errors.rb +18 -0
  25. data/lib/webxr/events/input_source_event.rb +53 -0
  26. data/lib/webxr/events/reference_space_event.rb +44 -0
  27. data/lib/webxr/events/session_event.rb +56 -0
  28. data/lib/webxr/geometry/pose.rb +49 -0
  29. data/lib/webxr/geometry/rigid_transform.rb +73 -0
  30. data/lib/webxr/geometry/view.rb +68 -0
  31. data/lib/webxr/geometry/viewer_pose.rb +40 -0
  32. data/lib/webxr/geometry/viewport.rb +55 -0
  33. data/lib/webxr/hand/hand.rb +197 -0
  34. data/lib/webxr/hand/joint_pose.rb +33 -0
  35. data/lib/webxr/hand/joint_space.rb +74 -0
  36. data/lib/webxr/helpers/input_helper.rb +142 -0
  37. data/lib/webxr/helpers/rendering_helper.rb +94 -0
  38. data/lib/webxr/helpers/session_manager.rb +105 -0
  39. data/lib/webxr/input/gamepad.rb +115 -0
  40. data/lib/webxr/input/gamepad_button.rb +36 -0
  41. data/lib/webxr/input/input_source.rb +101 -0
  42. data/lib/webxr/input/input_source_array.rb +86 -0
  43. data/lib/webxr/js_wrapper.rb +116 -0
  44. data/lib/webxr/layers/layer.rb +28 -0
  45. data/lib/webxr/layers/webgl_binding.rb +69 -0
  46. data/lib/webxr/layers/webgl_layer.rb +102 -0
  47. data/lib/webxr/layers/webgl_sub_image.rb +59 -0
  48. data/lib/webxr/spaces/bounded_reference_space.rb +43 -0
  49. data/lib/webxr/spaces/reference_space.rb +51 -0
  50. data/lib/webxr/spaces/space.rb +18 -0
  51. data/lib/webxr/version.rb +5 -0
  52. data/lib/webxr.rb +73 -0
  53. data/webxr.gemspec +33 -0
  54. metadata +111 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cd0e42685794b12081b40602d78cfb5edf0ff103f259972bcf2a61354e9ce239
4
+ data.tar.gz: 28c6fe4cca40ea53a9ad9441e8b14ac197aa82f570882cfd728940ebddc9b6cc
5
+ SHA512:
6
+ metadata.gz: b0e714b7fdcccfa04bbded4421483ca981cbe59b23f784adb3c03ec1f3eee92e18b829f984b4e9076d8b66c7797d6aeb7cfbacd63d6576f89e4047a9a381791b
7
+ data.tar.gz: 27dc069631dbe704579dd6b664388bf8154d588cc136eca63a127a389572e9c72225f4dfaceb504e4868ab885b48322acd7ed7f8f8208e58f982f5a41cc58dec
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0 - 2026-01-03
6
+
7
+ - Initial release.
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # WebXR Ruby
2
+
3
+ [![CI](https://github.com/ydah/webxr-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/ydah/webxr-ruby/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/webxr.svg)](https://badge.fury.io/rb/webxr)
5
+
6
+ WebXR Device API bindings for Ruby. Build VR/AR applications in Ruby using [ruby.wasm](https://github.com/ruby/ruby.wasm).
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem "webxr"
14
+ ```
15
+
16
+ Or install it directly:
17
+
18
+ ```bash
19
+ gem install webxr
20
+ ```
21
+
22
+ ## Requirements
23
+
24
+ - Ruby 3.2+
25
+ - [ruby.wasm](https://github.com/ruby/ruby.wasm) runtime (for browser execution)
26
+ - WebXR-compatible browser (Chrome, Edge, Firefox, etc.)
27
+
28
+ ## Usage
29
+
30
+ ### Basic VR Session
31
+
32
+ ```ruby
33
+ require "webxr"
34
+
35
+ # Check WebXR availability
36
+ if WebXR::System.available?
37
+ system = WebXR::System.instance
38
+
39
+ # Check VR support
40
+ if system.session_supported?("immersive-vr")
41
+ # Request VR session
42
+ session = system.request_session("immersive-vr")
43
+
44
+ # Get reference space
45
+ space = session.request_reference_space("local-floor")
46
+
47
+ # Animation loop
48
+ session.request_animation_frame do |time, frame|
49
+ pose = frame.viewer_pose(space)
50
+ # Render your scene using pose.views
51
+ end
52
+ end
53
+ end
54
+ ```
55
+
56
+ ### Using Helpers
57
+
58
+ ```ruby
59
+ require "webxr"
60
+
61
+ # SessionManager for simplified session handling
62
+ manager = WebXR::Helpers::SessionManager.new
63
+
64
+ manager.start_vr do |session, space|
65
+ # Automatically handles setup and cleanup
66
+ session.request_animation_frame do |time, frame|
67
+ # Your render loop
68
+ end
69
+ end
70
+ ```
71
+
72
+ ### Input Handling
73
+
74
+ ```ruby
75
+ # Access controllers
76
+ session.input_sources.each do |source|
77
+ puts "Controller: #{source.handedness}" # "left", "right", "none"
78
+
79
+ if source.gamepad
80
+ source.gamepad.buttons.each_with_index do |button, i|
81
+ puts "Button #{i} pressed" if button.pressed?
82
+ end
83
+ end
84
+ end
85
+
86
+ # Input events
87
+ session.on(:select) do |event|
88
+ source = event.input_source
89
+ puts "Select from #{source.handedness} controller"
90
+ end
91
+ ```
92
+
93
+ ### AR with Hit Testing
94
+
95
+ ```ruby
96
+ session = system.request_session("immersive-ar", required_features: ["hit-test"])
97
+ space = session.request_reference_space("local")
98
+ viewer_space = session.request_reference_space("viewer")
99
+
100
+ hit_test_source = session.request_hit_test_source(space: viewer_space)
101
+
102
+ session.request_animation_frame do |time, frame|
103
+ results = frame.get_hit_test_results(hit_test_source)
104
+
105
+ results.each do |result|
106
+ pose = result.get_pose(space)
107
+ # Place objects at hit position
108
+ end
109
+ end
110
+ ```
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ # Install dependencies
116
+ bundle install
117
+
118
+ # Run tests
119
+ bundle exec rake spec
120
+
121
+ # Generate documentation
122
+ bundle exec rake doc
123
+ ```
124
+
125
+ ## Contributing
126
+
127
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/webxr-ruby.
128
+
129
+ ## License
130
+
131
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
132
+
133
+ ## Resources
134
+
135
+ - [WebXR Device API Specification](https://www.w3.org/TR/webxr/)
136
+ - [MDN WebXR Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API)
137
+ - [ruby.wasm](https://github.com/ruby/ruby.wasm)
138
+ - [Immersive Web Emulator](https://github.com/nickolinko/webxr-emulator-extension) - Browser extension for testing
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ desc "Generate YARD documentation"
11
+ task :doc do
12
+ sh "bundle exec yard doc"
13
+ end
14
+
15
+ desc "Open YARD documentation server"
16
+ task :doc_server do
17
+ sh "bundle exec yard server --reload"
18
+ end
19
+
20
+ desc "Run all checks"
21
+ task check: :spec
@@ -0,0 +1,238 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WebXR Ruby - AR Hit Test Demo</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ background: #1a1a2e;
11
+ color: white;
12
+ font-family: system-ui, sans-serif;
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ justify-content: center;
17
+ min-height: 100vh;
18
+ }
19
+ h1 { color: #e94560; }
20
+ #xr-canvas {
21
+ width: 100%;
22
+ height: 100%;
23
+ position: absolute;
24
+ top: 0;
25
+ left: 0;
26
+ display: none;
27
+ }
28
+ .container {
29
+ text-align: center;
30
+ padding: 2rem;
31
+ }
32
+ button {
33
+ background: #e94560;
34
+ color: white;
35
+ border: none;
36
+ padding: 1rem 2rem;
37
+ font-size: 1.2rem;
38
+ border-radius: 8px;
39
+ cursor: pointer;
40
+ margin: 0.5rem;
41
+ transition: background 0.3s;
42
+ }
43
+ button:hover { background: #ff6b6b; }
44
+ button:disabled {
45
+ background: #444;
46
+ cursor: not-allowed;
47
+ }
48
+ #status {
49
+ margin-top: 1rem;
50
+ padding: 1rem;
51
+ background: rgba(255,255,255,0.1);
52
+ border-radius: 8px;
53
+ }
54
+ #log {
55
+ margin-top: 1rem;
56
+ padding: 1rem;
57
+ background: #0f0f23;
58
+ border-radius: 8px;
59
+ text-align: left;
60
+ max-height: 300px;
61
+ overflow-y: auto;
62
+ font-family: monospace;
63
+ font-size: 0.85rem;
64
+ width: 80%;
65
+ max-width: 600px;
66
+ }
67
+ .log-entry { margin: 0.25rem 0; }
68
+ .log-info { color: #4ecdc4; }
69
+ .log-warn { color: #ffe66d; }
70
+ .log-error { color: #ff6b6b; }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <canvas id="xr-canvas"></canvas>
75
+
76
+ <div class="container" id="ui">
77
+ <h1>WebXR Ruby - AR Demo</h1>
78
+ <p>AR Hit Test and Anchor Placement</p>
79
+
80
+ <div id="status">Checking WebXR support...</div>
81
+
82
+ <div id="buttons" style="display: none;">
83
+ <button id="ar-btn" disabled>Start AR Session</button>
84
+ </div>
85
+
86
+ <div id="log"></div>
87
+ </div>
88
+
89
+ <script type="module">
90
+ const statusDiv = document.getElementById("status");
91
+ const buttonsDiv = document.getElementById("buttons");
92
+ const arBtn = document.getElementById("ar-btn");
93
+ const logDiv = document.getElementById("log");
94
+
95
+ function log(message, type = "info") {
96
+ const entry = document.createElement("div");
97
+ entry.className = `log-entry log-${type}`;
98
+ const time = new Date().toLocaleTimeString();
99
+ entry.textContent = `[${time}] ${message}`;
100
+ logDiv.appendChild(entry);
101
+ logDiv.scrollTop = logDiv.scrollHeight;
102
+ console.log(message);
103
+ }
104
+
105
+ async function main() {
106
+ try {
107
+ log("Initializing AR demo...");
108
+
109
+ buttonsDiv.style.display = "block";
110
+
111
+ // Check WebXR availability
112
+ if (!navigator.xr) {
113
+ statusDiv.textContent = "WebXR is not supported in this browser";
114
+ log("WebXR not available", "error");
115
+ return;
116
+ }
117
+
118
+ log("WebXR API detected");
119
+
120
+ // Check AR support
121
+ const arSupported = await navigator.xr.isSessionSupported("immersive-ar");
122
+ if (arSupported) {
123
+ arBtn.disabled = false;
124
+ log("Immersive AR supported");
125
+ statusDiv.textContent = "AR is supported! Click the button to start.";
126
+ } else {
127
+ log("Immersive AR not supported on this device", "warn");
128
+ statusDiv.textContent = "AR is not supported on this device";
129
+ }
130
+
131
+ // AR button handler
132
+ arBtn.addEventListener("click", async () => {
133
+ try {
134
+ log("Requesting AR session...");
135
+ arBtn.disabled = true;
136
+
137
+ const session = await navigator.xr.requestSession("immersive-ar", {
138
+ requiredFeatures: ["local", "hit-test"]
139
+ });
140
+
141
+ log(`AR session started: ${session.mode}`);
142
+ statusDiv.textContent = "AR session active - tap surfaces to place objects";
143
+
144
+ // Get reference spaces
145
+ const localSpace = await session.requestReferenceSpace("local");
146
+ const viewerSpace = await session.requestReferenceSpace("viewer");
147
+
148
+ log("Reference spaces acquired");
149
+
150
+ // Request hit test source
151
+ const hitTestSource = await session.requestHitTestSource({ space: viewerSpace });
152
+ log("Hit test source created");
153
+
154
+ // Setup WebGL
155
+ const canvas = document.getElementById("xr-canvas");
156
+ canvas.style.display = "block";
157
+ const gl = canvas.getContext("webgl2", { xrCompatible: true });
158
+
159
+ const layer = new XRWebGLLayer(session, gl, { alpha: true });
160
+ session.updateRenderState({ baseLayer: layer });
161
+
162
+ log(`Framebuffer: ${layer.framebufferWidth}x${layer.framebufferHeight}`);
163
+
164
+ // Track anchors
165
+ const anchors = [];
166
+
167
+ // Handle select events
168
+ session.addEventListener("select", async (event) => {
169
+ const frame = event.frame;
170
+ const results = frame.getHitTestResults(hitTestSource);
171
+
172
+ if (results.length > 0) {
173
+ const hit = results[0];
174
+ const pose = hit.getPose(localSpace);
175
+
176
+ if (pose) {
177
+ const pos = pose.transform.position;
178
+ log(`Tap at: (${pos.x.toFixed(3)}, ${pos.y.toFixed(3)}, ${pos.z.toFixed(3)})`);
179
+
180
+ try {
181
+ const anchor = await hit.createAnchor();
182
+ anchors.push(anchor);
183
+ log(`Created anchor #${anchors.length}`, "info");
184
+ } catch (e) {
185
+ log(`Failed to create anchor: ${e.message}`, "error");
186
+ }
187
+ }
188
+ }
189
+ });
190
+
191
+ // Render loop
192
+ let frameCount = 0;
193
+ session.requestAnimationFrame(function onFrame(time, frame) {
194
+ session.requestAnimationFrame(onFrame);
195
+ frameCount++;
196
+
197
+ const pose = frame.getViewerPose(localSpace);
198
+ if (!pose) return;
199
+
200
+ gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer);
201
+ gl.clearColor(0, 0, 0, 0);
202
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
203
+
204
+ // Render views
205
+ for (const view of pose.views) {
206
+ const viewport = layer.getViewport(view);
207
+ gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
208
+ }
209
+ });
210
+
211
+ // Handle session end
212
+ session.addEventListener("end", () => {
213
+ log(`Session ended. Total anchors: ${anchors.length}`);
214
+ statusDiv.textContent = "AR session ended";
215
+ canvas.style.display = "none";
216
+ arBtn.disabled = false;
217
+
218
+ // Clean up
219
+ hitTestSource.cancel();
220
+ anchors.forEach(a => a.delete());
221
+ });
222
+
223
+ } catch (e) {
224
+ log(`Error: ${e.message}`, "error");
225
+ arBtn.disabled = false;
226
+ }
227
+ });
228
+
229
+ } catch (e) {
230
+ statusDiv.textContent = `Error: ${e.message}`;
231
+ log(`Initialization error: ${e.message}`, "error");
232
+ }
233
+ }
234
+
235
+ main();
236
+ </script>
237
+ </body>
238
+ </html>
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ # AR Hit Test Example
4
+ # This example demonstrates how to use AR hit testing in WebXR Ruby.
5
+ #
6
+ # IMPORTANT: This code runs ONLY in a browser via ruby.wasm.
7
+ # It cannot be executed with standard Ruby (e.g., `ruby examples/ar_hit_test.rb`).
8
+ #
9
+ # To run this example:
10
+ # 1. Use the ar_demo.html file which loads ruby.wasm
11
+ # 2. Or embed this code in an HTML page with ruby.wasm runtime
12
+ #
13
+ # Prerequisites:
14
+ # - A WebXR-compatible browser with AR support
15
+ # - A device with AR capabilities (e.g., smartphone with ARCore/ARKit)
16
+ # - ruby.wasm runtime loaded in the browser
17
+
18
+ require "webxr"
19
+
20
+ # Check WebXR availability
21
+ unless WebXR.available?
22
+ puts "WebXR is not available"
23
+ exit
24
+ end
25
+
26
+ xr = WebXR.system
27
+
28
+ # Check AR support
29
+ unless xr.session_supported?(WebXR::SessionMode::IMMERSIVE_AR)
30
+ puts "AR is not supported"
31
+ exit
32
+ end
33
+
34
+ # Get WebGL context
35
+ canvas = JS.global[:document].call(:getElementById, "xr-canvas")
36
+ gl = canvas.call(:getContext, "webgl2", JS.eval("{ xrCompatible: true }"))
37
+
38
+ # Request AR session with hit testing
39
+ session = xr.request_session(
40
+ WebXR::SessionMode::IMMERSIVE_AR,
41
+ required_features: ["local", "hit-test"],
42
+ optional_features: ["dom-overlay", "light-estimation"]
43
+ )
44
+
45
+ puts "AR Session started"
46
+ puts "Enabled features: #{session.enabled_features.join(', ')}"
47
+ puts "Environment blend mode: #{session.environment_blend_mode}"
48
+
49
+ # Get reference space
50
+ reference_space = session.request_reference_space(WebXR::ReferenceSpaceType::LOCAL)
51
+ viewer_space = session.request_reference_space(WebXR::ReferenceSpaceType::VIEWER)
52
+
53
+ # Create WebGL layer
54
+ layer = WebXR::WebGLLayer.new(session, gl, alpha: true)
55
+ session.update_render_state(base_layer: layer)
56
+
57
+ # Request hit test source
58
+ # This will test rays cast from the viewer (screen center)
59
+ hit_test_source = nil
60
+
61
+ # In a real implementation, you would use the session's requestHitTestSource method
62
+ # hit_test_promise = session.js.call(:requestHitTestSource, JS.eval("{ space: viewerSpace.js }"))
63
+ # hit_test_source = WebXR::AR::HitTestSource.new(JS.await(hit_test_promise))
64
+
65
+ # Store placed anchors
66
+ anchors = []
67
+
68
+ # Handle select events to place anchors
69
+ session.on(:select) do |event|
70
+ frame = event.frame
71
+ source = event.input_source
72
+
73
+ # Get the pose where the user selected
74
+ pose = event.pose(reference_space)
75
+ next unless pose
76
+
77
+ position = pose.transform.position
78
+ puts "Select at position: (#{position[:x].round(3)}, #{position[:y].round(3)}, #{position[:z].round(3)})"
79
+
80
+ # Create an anchor at this position
81
+ begin
82
+ anchor = frame.create_anchor(pose.transform, reference_space)
83
+ anchors << anchor
84
+ puts "Created anchor ##{anchors.length}"
85
+ rescue => e
86
+ puts "Failed to create anchor: #{e.message}"
87
+ end
88
+ end
89
+
90
+ # Rendering loop
91
+ render_frame = proc do |time, js_frame|
92
+ session.request_animation_frame(&render_frame)
93
+
94
+ frame = WebXR::Frame.new(js_frame)
95
+ pose = frame.viewer_pose(reference_space)
96
+
97
+ next unless pose
98
+
99
+ # Clear with transparent background for AR
100
+ gl.call(:bindFramebuffer, gl[:FRAMEBUFFER], layer.framebuffer)
101
+ gl.call(:clearColor, 0.0, 0.0, 0.0, 0.0)
102
+ gl.call(:clear, gl[:COLOR_BUFFER_BIT] | gl[:DEPTH_BUFFER_BIT])
103
+
104
+ # Process hit test results (if hit test source is available)
105
+ if hit_test_source
106
+ results = frame.hit_test_results(hit_test_source)
107
+
108
+ if results.any?
109
+ # Get the first (closest) hit
110
+ hit = results.first
111
+ hit_pose = hit.pose(reference_space)
112
+
113
+ if hit_pose
114
+ position = hit_pose.transform.position
115
+ # Draw a reticle at the hit position
116
+ # puts "Hit at: (#{position[:x].round(3)}, #{position[:y].round(3)}, #{position[:z].round(3)})"
117
+ end
118
+ end
119
+ end
120
+
121
+ # Get tracked anchors and render objects at their positions
122
+ frame.tracked_anchors.each do |anchor|
123
+ anchor_pose = frame.pose(anchor.anchor_space, reference_space)
124
+ next unless anchor_pose
125
+
126
+ position = anchor_pose.transform.position
127
+ # Render your 3D object at this position
128
+ # In a real app, you would draw a cube, model, etc.
129
+ end
130
+
131
+ # Render each view
132
+ pose.each_view do |view|
133
+ viewport = layer.viewport(view)
134
+ gl.call(:viewport, viewport.x, viewport.y, viewport.width, viewport.height)
135
+
136
+ projection = view.projection_matrix
137
+ view_matrix = view.view_matrix
138
+
139
+ # Your 3D rendering code here
140
+ # Render anchored objects, reticle, etc.
141
+ end
142
+ end
143
+
144
+ # Start render loop
145
+ session.request_animation_frame(&render_frame)
146
+
147
+ # Handle session end
148
+ session.on(:end) do
149
+ puts "AR Session ended"
150
+ puts "Total anchors created: #{anchors.length}"
151
+
152
+ # Clean up anchors
153
+ anchors.each(&:delete)
154
+ end
155
+
156
+ puts "AR hit test demo started"
157
+ puts "Tap on surfaces to place anchors"
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Basic VR Session Example
4
+ # This example demonstrates how to create a simple VR session using WebXR Ruby bindings.
5
+ #
6
+ # IMPORTANT: This code runs ONLY in a browser via ruby.wasm.
7
+ # It cannot be executed with standard Ruby (e.g., `ruby examples/basic_vr.rb`).
8
+ #
9
+ # To run this example:
10
+ # 1. Include the ruby.wasm runtime in an HTML page
11
+ # 2. Load this Ruby script using <script type="text/ruby">
12
+ # 3. Use a WebXR-compatible browser and VR headset
13
+ #
14
+ # See hello_webxr.html for a working browser example.
15
+
16
+ require "webxr"
17
+
18
+ # Check if WebXR is available
19
+ unless WebXR.available?
20
+ puts "WebXR is not available in this browser"
21
+ exit
22
+ end
23
+
24
+ xr = WebXR.system
25
+
26
+ # Check VR support
27
+ unless xr.session_supported?(WebXR::SessionMode::IMMERSIVE_VR)
28
+ puts "Immersive VR is not supported"
29
+ exit
30
+ end
31
+
32
+ # Get the WebGL canvas and context
33
+ canvas = JS.global[:document].call(:getElementById, "xr-canvas")
34
+ gl = canvas.call(:getContext, "webgl2", JS.eval("{ xrCompatible: true }"))
35
+
36
+ # Request VR session
37
+ session = xr.request_session(
38
+ WebXR::SessionMode::IMMERSIVE_VR,
39
+ required_features: ["local-floor"],
40
+ optional_features: ["bounded-floor", "hand-tracking"]
41
+ )
42
+
43
+ puts "VR Session started: #{session.mode}"
44
+ puts "Enabled features: #{session.enabled_features.join(', ')}"
45
+
46
+ # Get reference space
47
+ reference_space = session.request_reference_space(WebXR::ReferenceSpaceType::LOCAL_FLOOR)
48
+
49
+ # Create WebGL layer
50
+ layer = WebXR::WebGLLayer.new(session, gl, antialias: true)
51
+ session.update_render_state(base_layer: layer)
52
+
53
+ puts "Framebuffer size: #{layer.framebuffer_width}x#{layer.framebuffer_height}"
54
+
55
+ # Setup event handlers
56
+ session.on(:end) do
57
+ puts "VR Session ended"
58
+ end
59
+
60
+ session.on(:select) do |event|
61
+ puts "Select event from #{event.handedness} controller"
62
+ end
63
+
64
+ session.on(:inputsourceschange) do |event|
65
+ puts "Input sources changed"
66
+ puts " Added: #{event.added.length}"
67
+ puts " Removed: #{event.removed.length}"
68
+ end
69
+
70
+ # Rendering loop
71
+ render_frame = proc do |time, js_frame|
72
+ session.request_animation_frame(&render_frame)
73
+
74
+ frame = WebXR::Frame.new(js_frame)
75
+ pose = frame.viewer_pose(reference_space)
76
+
77
+ return unless pose
78
+
79
+ # Bind XR framebuffer
80
+ gl.call(:bindFramebuffer, gl[:FRAMEBUFFER], layer.framebuffer)
81
+ gl.call(:clearColor, 0.1, 0.1, 0.2, 1.0)
82
+ gl.call(:clear, gl[:COLOR_BUFFER_BIT] | gl[:DEPTH_BUFFER_BIT])
83
+
84
+ # Render each view (left and right eye)
85
+ pose.each_view do |view|
86
+ viewport = layer.viewport(view)
87
+ gl.call(:viewport, viewport.x, viewport.y, viewport.width, viewport.height)
88
+
89
+ projection_matrix = view.projection_matrix
90
+ view_matrix = view.view_matrix
91
+
92
+ # Your 3D rendering code here
93
+ # Use projection_matrix and view_matrix for rendering
94
+ end
95
+
96
+ # Process input sources
97
+ session.input_sources.each do |source|
98
+ next unless source.gamepad
99
+
100
+ # Check trigger state
101
+ if source.gamepad.trigger&.pressed?
102
+ puts "#{source.handedness} trigger pressed"
103
+ end
104
+ end
105
+ end
106
+
107
+ # Start the render loop
108
+ session.request_animation_frame(&render_frame)
109
+
110
+ puts "VR rendering started"