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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/README.md +138 -0
- data/Rakefile +21 -0
- data/examples/ar_demo.html +238 -0
- data/examples/ar_hit_test.rb +157 -0
- data/examples/basic_vr.rb +110 -0
- data/examples/controller_input.rb +91 -0
- data/examples/hand_tracking.rb +124 -0
- data/examples/hello_webxr.html +288 -0
- data/examples/inline_demo.html +261 -0
- data/examples/server.rb +34 -0
- data/examples/vr_scene_demo.html +330 -0
- data/lib/webxr/ar/anchor.rb +83 -0
- data/lib/webxr/ar/hit_test_result.rb +54 -0
- data/lib/webxr/ar/hit_test_source.rb +34 -0
- data/lib/webxr/ar/ray.rb +90 -0
- data/lib/webxr/constants.rb +61 -0
- data/lib/webxr/core/frame.rb +155 -0
- data/lib/webxr/core/render_state.rb +47 -0
- data/lib/webxr/core/session.rb +212 -0
- data/lib/webxr/core/system.rb +122 -0
- data/lib/webxr/errors.rb +18 -0
- data/lib/webxr/events/input_source_event.rb +53 -0
- data/lib/webxr/events/reference_space_event.rb +44 -0
- data/lib/webxr/events/session_event.rb +56 -0
- data/lib/webxr/geometry/pose.rb +49 -0
- data/lib/webxr/geometry/rigid_transform.rb +73 -0
- data/lib/webxr/geometry/view.rb +68 -0
- data/lib/webxr/geometry/viewer_pose.rb +40 -0
- data/lib/webxr/geometry/viewport.rb +55 -0
- data/lib/webxr/hand/hand.rb +197 -0
- data/lib/webxr/hand/joint_pose.rb +33 -0
- data/lib/webxr/hand/joint_space.rb +74 -0
- data/lib/webxr/helpers/input_helper.rb +142 -0
- data/lib/webxr/helpers/rendering_helper.rb +94 -0
- data/lib/webxr/helpers/session_manager.rb +105 -0
- data/lib/webxr/input/gamepad.rb +115 -0
- data/lib/webxr/input/gamepad_button.rb +36 -0
- data/lib/webxr/input/input_source.rb +101 -0
- data/lib/webxr/input/input_source_array.rb +86 -0
- data/lib/webxr/js_wrapper.rb +116 -0
- data/lib/webxr/layers/layer.rb +28 -0
- data/lib/webxr/layers/webgl_binding.rb +69 -0
- data/lib/webxr/layers/webgl_layer.rb +102 -0
- data/lib/webxr/layers/webgl_sub_image.rb +59 -0
- data/lib/webxr/spaces/bounded_reference_space.rb +43 -0
- data/lib/webxr/spaces/reference_space.rb +51 -0
- data/lib/webxr/spaces/space.rb +18 -0
- data/lib/webxr/version.rb +5 -0
- data/lib/webxr.rb +73 -0
- data/webxr.gemspec +33 -0
- 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
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# WebXR Ruby
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ydah/webxr-ruby/actions/workflows/ci.yml)
|
|
4
|
+
[](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"
|