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
|
@@ -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>
|