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
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Controller Input Example
4
+ # This example demonstrates how to handle VR controller input using the InputHelper.
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/controller_input.rb`).
8
+ #
9
+ # See hello_webxr.html for a working browser example.
10
+
11
+ require "webxr"
12
+
13
+ # Use the SessionManager for easier session handling
14
+ manager = WebXR::Helpers::SessionManager.new
15
+
16
+ unless manager.available?
17
+ puts "WebXR is not available"
18
+ exit
19
+ end
20
+
21
+ unless manager.supports?(WebXR::SessionMode::IMMERSIVE_VR)
22
+ puts "VR is not supported"
23
+ exit
24
+ end
25
+
26
+ # Get WebGL context
27
+ canvas = JS.global[:document].call(:getElementById, "xr-canvas")
28
+ gl = canvas.call(:getContext, "webgl2", JS.eval("{ xrCompatible: true }"))
29
+
30
+ # Start VR session with block for automatic cleanup
31
+ manager.start_vr(optional_features: ["hand-tracking"]) do |session, reference_space|
32
+ puts "VR Session started"
33
+
34
+ # Setup rendering
35
+ renderer = WebXR::Helpers::RenderingHelper.new(session, gl, reference_space)
36
+ renderer.setup_layer(antialias: true, framebuffer_scale_factor: 1.0)
37
+
38
+ # Setup input handling
39
+ input = WebXR::Helpers::InputHelper.new(session, reference_space)
40
+
41
+ # Handle select events (trigger press)
42
+ input.on_select do |source, event, pose|
43
+ if pose
44
+ position = pose.transform.position
45
+ puts "Select at position: (#{position[:x].round(2)}, #{position[:y].round(2)}, #{position[:z].round(2)})"
46
+ end
47
+
48
+ case source.handedness
49
+ when WebXR::Handedness::LEFT
50
+ puts "Left controller select"
51
+ when WebXR::Handedness::RIGHT
52
+ puts "Right controller select"
53
+ end
54
+ end
55
+
56
+ # Handle squeeze events (grip button)
57
+ input.on_squeeze do |source, event, pose|
58
+ puts "Squeeze from #{source.handedness} controller"
59
+ end
60
+
61
+ # Run render loop
62
+ renderer.run_frame_loop do |time, frame, view, viewport|
63
+ # Clear with dark blue color
64
+ renderer.clear(r: 0.0, g: 0.0, b: 0.2, a: 1.0)
65
+
66
+ # Get controller states
67
+ input.each_controller do |controller|
68
+ states = input.button_states(controller)
69
+
70
+ # Check thumbstick movement
71
+ thumbstick_x = states[:thumbstick][:x]
72
+ thumbstick_y = states[:thumbstick][:y]
73
+
74
+ if thumbstick_x.abs > 0.1 || thumbstick_y.abs > 0.1
75
+ puts "#{controller.handedness} thumbstick: (#{thumbstick_x.round(2)}, #{thumbstick_y.round(2)})"
76
+ end
77
+
78
+ # Get controller pose
79
+ pose = input.grip_pose_for(controller, frame)
80
+ if pose
81
+ # Use pose.transform.matrix for rendering controller model
82
+ end
83
+ end
84
+
85
+ # Your 3D rendering code here
86
+ projection = view.projection_matrix
87
+ view_matrix = view.view_matrix
88
+ end
89
+ end
90
+
91
+ puts "Session ended"
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hand Tracking Example
4
+ # This example demonstrates how to use hand tracking 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/hand_tracking.rb`).
8
+ #
9
+ # Prerequisites:
10
+ # - A WebXR-compatible browser with hand tracking support
11
+ # - A device with hand tracking capabilities (e.g., Meta Quest)
12
+ # - ruby.wasm runtime loaded in the browser
13
+
14
+ require "webxr"
15
+
16
+ # Check WebXR availability
17
+ unless WebXR.available?
18
+ puts "WebXR is not available"
19
+ exit
20
+ end
21
+
22
+ xr = WebXR.system
23
+
24
+ # Check hand tracking support
25
+ unless xr.session_supported?(WebXR::SessionMode::IMMERSIVE_VR)
26
+ puts "VR is not supported"
27
+ exit
28
+ end
29
+
30
+ # Get WebGL context
31
+ canvas = JS.global[:document].call(:getElementById, "xr-canvas")
32
+ gl = canvas.call(:getContext, "webgl2", JS.eval("{ xrCompatible: true }"))
33
+
34
+ # Request VR session with hand tracking
35
+ session = xr.request_session(
36
+ WebXR::SessionMode::IMMERSIVE_VR,
37
+ required_features: ["local-floor"],
38
+ optional_features: ["hand-tracking"]
39
+ )
40
+
41
+ puts "VR Session started with hand tracking"
42
+ puts "Enabled features: #{session.enabled_features.join(', ')}"
43
+
44
+ # Get reference space
45
+ reference_space = session.request_reference_space(WebXR::ReferenceSpaceType::LOCAL_FLOOR)
46
+
47
+ # Create WebGL layer
48
+ layer = WebXR::WebGLLayer.new(session, gl)
49
+ session.update_render_state(base_layer: layer)
50
+
51
+ # Rendering loop
52
+ render_frame = proc do |time, js_frame|
53
+ session.request_animation_frame(&render_frame)
54
+
55
+ frame = WebXR::Frame.new(js_frame)
56
+ pose = frame.viewer_pose(reference_space)
57
+
58
+ next unless pose
59
+
60
+ # Bind framebuffer
61
+ gl.call(:bindFramebuffer, gl[:FRAMEBUFFER], layer.framebuffer)
62
+ gl.call(:clearColor, 0.1, 0.1, 0.2, 1.0)
63
+ gl.call(:clear, gl[:COLOR_BUFFER_BIT] | gl[:DEPTH_BUFFER_BIT])
64
+
65
+ # Process input sources for hand tracking
66
+ session.input_sources.each do |source|
67
+ hand = source.hand
68
+ next unless hand
69
+
70
+ puts "Processing #{source.handedness} hand with #{hand.size} joints"
71
+
72
+ # Get joint poses
73
+ hand.each do |joint_name, joint_space|
74
+ joint_pose = frame.joint_pose(joint_space, reference_space)
75
+ next unless joint_pose
76
+
77
+ position = joint_pose.transform.position
78
+ radius = joint_pose.radius
79
+
80
+ # Draw a sphere at each joint position
81
+ # In a real app, you would use WebGL to render spheres
82
+ if joint_space.tip?
83
+ puts " #{joint_name}: pos=(#{position[:x].round(3)}, #{position[:y].round(3)}, #{position[:z].round(3)}), radius=#{radius.round(4)}"
84
+ end
85
+ end
86
+
87
+ # Detect pinch gesture (thumb tip close to index finger tip)
88
+ thumb_tip = hand.thumb_tip
89
+ index_tip = hand.index_finger_tip
90
+
91
+ if thumb_tip && index_tip
92
+ thumb_pose = frame.joint_pose(thumb_tip, reference_space)
93
+ index_pose = frame.joint_pose(index_tip, reference_space)
94
+
95
+ if thumb_pose && index_pose
96
+ thumb_pos = thumb_pose.transform.position
97
+ index_pos = index_pose.transform.position
98
+
99
+ # Calculate distance between thumb and index tips
100
+ dx = thumb_pos[:x] - index_pos[:x]
101
+ dy = thumb_pos[:y] - index_pos[:y]
102
+ dz = thumb_pos[:z] - index_pos[:z]
103
+ distance = Math.sqrt(dx * dx + dy * dy + dz * dz)
104
+
105
+ # Pinch detected if distance is less than 2cm
106
+ if distance < 0.02
107
+ puts " PINCH detected on #{source.handedness} hand! Distance: #{(distance * 100).round(1)}cm"
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # Render views
114
+ pose.each_view do |view|
115
+ viewport = layer.viewport(view)
116
+ gl.call(:viewport, viewport.x, viewport.y, viewport.width, viewport.height)
117
+ # Your 3D rendering code here
118
+ end
119
+ end
120
+
121
+ # Start render loop
122
+ session.request_animation_frame(&render_frame)
123
+
124
+ puts "Hand tracking demo started"
@@ -0,0 +1,288 @@
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>Hello WebXR Ruby</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
+ position: absolute;
22
+ top: 0;
23
+ left: 0;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+ .container {
28
+ text-align: center;
29
+ padding: 2rem;
30
+ z-index: 10;
31
+ position: relative;
32
+ }
33
+ button {
34
+ background: #e94560;
35
+ color: white;
36
+ border: none;
37
+ padding: 1rem 2rem;
38
+ font-size: 1.2rem;
39
+ border-radius: 8px;
40
+ cursor: pointer;
41
+ margin: 0.5rem;
42
+ transition: background 0.3s;
43
+ }
44
+ button:hover { background: #ff6b6b; }
45
+ button:disabled {
46
+ background: #444;
47
+ cursor: not-allowed;
48
+ }
49
+ #status {
50
+ margin-top: 1rem;
51
+ padding: 1rem;
52
+ background: rgba(255,255,255,0.1);
53
+ border-radius: 8px;
54
+ }
55
+ #log {
56
+ margin-top: 1rem;
57
+ padding: 1rem;
58
+ background: #0f0f23;
59
+ border-radius: 8px;
60
+ text-align: left;
61
+ max-height: 200px;
62
+ overflow-y: auto;
63
+ font-family: monospace;
64
+ font-size: 0.9rem;
65
+ width: 80%;
66
+ max-width: 500px;
67
+ }
68
+ .log-entry { margin: 0.25rem 0; }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <canvas id="xr-canvas"></canvas>
73
+
74
+ <div class="container" id="ui">
75
+ <h1>Hello WebXR Ruby</h1>
76
+ <p>WebXR Device API bindings for Ruby via ruby.wasm</p>
77
+
78
+ <div id="status">Checking WebXR support...</div>
79
+
80
+ <div id="buttons" style="display: none;">
81
+ <button id="vr-btn" disabled>Enter VR</button>
82
+ <button id="ar-btn" disabled>Enter AR</button>
83
+ </div>
84
+
85
+ <div id="log"></div>
86
+ </div>
87
+
88
+ <script type="module">
89
+ const statusDiv = document.getElementById("status");
90
+ const buttonsDiv = document.getElementById("buttons");
91
+ const vrBtn = document.getElementById("vr-btn");
92
+ const arBtn = document.getElementById("ar-btn");
93
+ const logDiv = document.getElementById("log");
94
+ const canvas = document.getElementById("xr-canvas");
95
+
96
+ // Setup WebGL context
97
+ const gl = canvas.getContext("webgl2", { xrCompatible: true });
98
+ if (!gl) {
99
+ console.error("WebGL2 not supported");
100
+ }
101
+
102
+ function log(message) {
103
+ const entry = document.createElement("div");
104
+ entry.className = "log-entry";
105
+ const time = new Date().toLocaleTimeString();
106
+ entry.textContent = `[${time}] ${message}`;
107
+ logDiv.appendChild(entry);
108
+ logDiv.scrollTop = logDiv.scrollHeight;
109
+ console.log(message);
110
+ }
111
+
112
+ async function startXRSession(mode) {
113
+ try {
114
+ const isAR = mode === "immersive-ar";
115
+ log(`Requesting ${isAR ? 'AR' : 'VR'} session...`);
116
+
117
+ const session = await navigator.xr.requestSession(mode, {
118
+ requiredFeatures: [isAR ? "local" : "local-floor"]
119
+ });
120
+
121
+ log(`Session started: ${mode}`);
122
+ statusDiv.textContent = `${isAR ? 'AR' : 'VR'} session active`;
123
+
124
+ // Hide UI during session
125
+ document.getElementById("ui").style.display = "none";
126
+
127
+ // Setup WebGL layer
128
+ const layer = new XRWebGLLayer(session, gl, { alpha: isAR });
129
+ session.updateRenderState({ baseLayer: layer });
130
+
131
+ log(`Layer created: ${layer.framebufferWidth}x${layer.framebufferHeight}`);
132
+ log(`Framebuffer: ${layer.framebuffer ? 'valid' : 'null (using canvas)'}`);
133
+
134
+ // Get reference space
135
+ const spaceType = isAR ? "local" : "local-floor";
136
+ const refSpace = await session.requestReferenceSpace(spaceType);
137
+ log("Reference space acquired");
138
+
139
+ // Handle session end
140
+ session.addEventListener("end", () => {
141
+ log("Session ended");
142
+ statusDiv.textContent = "Session ended. Click to restart.";
143
+ document.getElementById("ui").style.display = "block";
144
+ vrBtn.disabled = false;
145
+ arBtn.disabled = false;
146
+ });
147
+
148
+ // Handle select events
149
+ session.addEventListener("select", (event) => {
150
+ const hand = event.inputSource.handedness;
151
+ log(`Select from ${hand} controller`);
152
+ });
153
+
154
+ // Animation frame counter
155
+ let frameCount = 0;
156
+ let hue = 0;
157
+
158
+ // Render loop
159
+ function onFrame(time, frame) {
160
+ session.requestAnimationFrame(onFrame);
161
+ frameCount++;
162
+
163
+ const pose = frame.getViewerPose(refSpace);
164
+ if (!pose) return;
165
+
166
+ // Bind XR framebuffer
167
+ gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer);
168
+
169
+ // Animate background color (hue rotation)
170
+ hue = (hue + 0.5) % 360;
171
+ const rgb = hslToRgb(hue / 360, 0.5, 0.3);
172
+
173
+ // Clear with animated color (or transparent for AR)
174
+ if (isAR) {
175
+ gl.clearColor(0, 0, 0, 0);
176
+ } else {
177
+ gl.clearColor(rgb[0], rgb[1], rgb[2], 1.0);
178
+ }
179
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
180
+
181
+ // Render each view
182
+ for (const view of pose.views) {
183
+ const viewport = layer.getViewport(view);
184
+ gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
185
+
186
+ // Draw a simple colored rectangle for each eye
187
+ // In a real app, you would render 3D content here
188
+ }
189
+
190
+ // Log every 100 frames
191
+ if (frameCount % 100 === 0) {
192
+ log(`Frame ${frameCount}`);
193
+ }
194
+ }
195
+
196
+ session.requestAnimationFrame(onFrame);
197
+ log("Render loop started");
198
+
199
+ } catch (e) {
200
+ log("Error: " + e.message);
201
+ vrBtn.disabled = false;
202
+ arBtn.disabled = false;
203
+ }
204
+ }
205
+
206
+ // HSL to RGB conversion
207
+ function hslToRgb(h, s, l) {
208
+ let r, g, b;
209
+ if (s === 0) {
210
+ r = g = b = l;
211
+ } else {
212
+ const hue2rgb = (p, q, t) => {
213
+ if (t < 0) t += 1;
214
+ if (t > 1) t -= 1;
215
+ if (t < 1/6) return p + (q - p) * 6 * t;
216
+ if (t < 1/2) return q;
217
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
218
+ return p;
219
+ };
220
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
221
+ const p = 2 * l - q;
222
+ r = hue2rgb(p, q, h + 1/3);
223
+ g = hue2rgb(p, q, h);
224
+ b = hue2rgb(p, q, h - 1/3);
225
+ }
226
+ return [r, g, b];
227
+ }
228
+
229
+ // Initialize
230
+ async function init() {
231
+ log("Initializing WebXR demo...");
232
+
233
+ if (!navigator.xr) {
234
+ statusDiv.textContent = "WebXR is not supported in this browser";
235
+ log("WebXR not available");
236
+ return;
237
+ }
238
+
239
+ log("WebXR API detected");
240
+ buttonsDiv.style.display = "block";
241
+
242
+ // Check VR support
243
+ try {
244
+ const vrSupported = await navigator.xr.isSessionSupported("immersive-vr");
245
+ if (vrSupported) {
246
+ vrBtn.disabled = false;
247
+ log("Immersive VR supported");
248
+ } else {
249
+ log("Immersive VR not supported");
250
+ }
251
+ } catch (e) {
252
+ log("VR check failed: " + e.message);
253
+ }
254
+
255
+ // Check AR support
256
+ try {
257
+ const arSupported = await navigator.xr.isSessionSupported("immersive-ar");
258
+ if (arSupported) {
259
+ arBtn.disabled = false;
260
+ log("Immersive AR supported");
261
+ } else {
262
+ log("Immersive AR not supported");
263
+ }
264
+ } catch (e) {
265
+ log("AR check failed: " + e.message);
266
+ }
267
+
268
+ statusDiv.textContent = "Ready! Click a button to start XR session.";
269
+
270
+ // Button handlers
271
+ vrBtn.addEventListener("click", () => {
272
+ vrBtn.disabled = true;
273
+ arBtn.disabled = true;
274
+ startXRSession("immersive-vr");
275
+ });
276
+
277
+ arBtn.addEventListener("click", () => {
278
+ vrBtn.disabled = true;
279
+ arBtn.disabled = true;
280
+ startXRSession("immersive-ar");
281
+ });
282
+ }
283
+
284
+ init();
285
+ log("WebXR demo initialized");
286
+ </script>
287
+ </body>
288
+ </html>