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,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module Hand
|
|
5
|
+
# Joint names as defined in WebXR Hand Input specification
|
|
6
|
+
JOINTS = %w[
|
|
7
|
+
wrist
|
|
8
|
+
thumb-metacarpal
|
|
9
|
+
thumb-phalanx-proximal
|
|
10
|
+
thumb-phalanx-distal
|
|
11
|
+
thumb-tip
|
|
12
|
+
index-finger-metacarpal
|
|
13
|
+
index-finger-phalanx-proximal
|
|
14
|
+
index-finger-phalanx-intermediate
|
|
15
|
+
index-finger-phalanx-distal
|
|
16
|
+
index-finger-tip
|
|
17
|
+
middle-finger-metacarpal
|
|
18
|
+
middle-finger-phalanx-proximal
|
|
19
|
+
middle-finger-phalanx-intermediate
|
|
20
|
+
middle-finger-phalanx-distal
|
|
21
|
+
middle-finger-tip
|
|
22
|
+
ring-finger-metacarpal
|
|
23
|
+
ring-finger-phalanx-proximal
|
|
24
|
+
ring-finger-phalanx-intermediate
|
|
25
|
+
ring-finger-phalanx-distal
|
|
26
|
+
ring-finger-tip
|
|
27
|
+
pinky-finger-metacarpal
|
|
28
|
+
pinky-finger-phalanx-proximal
|
|
29
|
+
pinky-finger-phalanx-intermediate
|
|
30
|
+
pinky-finger-phalanx-distal
|
|
31
|
+
pinky-finger-tip
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# Number of joints in a hand
|
|
35
|
+
JOINT_COUNT = 25
|
|
36
|
+
|
|
37
|
+
# XRHand - Represents a tracked hand with 25 joints
|
|
38
|
+
class Hand < JSWrapper
|
|
39
|
+
include Enumerable
|
|
40
|
+
|
|
41
|
+
# @param js_hand [JS::Object] The XRHand JavaScript object
|
|
42
|
+
def initialize(js_hand)
|
|
43
|
+
super(js_hand)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get the number of joints
|
|
47
|
+
# @return [Integer] Always 25 for a complete hand
|
|
48
|
+
def size
|
|
49
|
+
js_int(:size) || JOINT_COUNT
|
|
50
|
+
end
|
|
51
|
+
alias length size
|
|
52
|
+
|
|
53
|
+
# Get a joint space by name or index
|
|
54
|
+
# @param key [String, Integer] Joint name or index
|
|
55
|
+
# @return [JointSpace, nil]
|
|
56
|
+
def [](key)
|
|
57
|
+
js_joint = case key
|
|
58
|
+
when Integer
|
|
59
|
+
joint_name = JOINTS[key]
|
|
60
|
+
return nil unless joint_name
|
|
61
|
+
|
|
62
|
+
js_call(:get, joint_name)
|
|
63
|
+
when String, Symbol
|
|
64
|
+
js_call(:get, key.to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return nil if js_joint.nil?
|
|
68
|
+
|
|
69
|
+
JointSpace.new(js_joint)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get a joint space by name
|
|
73
|
+
# @param name [String] The joint name
|
|
74
|
+
# @return [JointSpace, nil]
|
|
75
|
+
def get(name)
|
|
76
|
+
js_joint = js_call(:get, name)
|
|
77
|
+
return nil if js_joint.nil?
|
|
78
|
+
|
|
79
|
+
JointSpace.new(js_joint)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Iterate over all joint spaces
|
|
83
|
+
# @yield [String, JointSpace] Joint name and joint space
|
|
84
|
+
# @return [Enumerator, void]
|
|
85
|
+
def each(&block)
|
|
86
|
+
return enum_for(:each) unless block_given?
|
|
87
|
+
|
|
88
|
+
JOINTS.each do |name|
|
|
89
|
+
joint = get(name)
|
|
90
|
+
yield name, joint if joint
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get all joint spaces as an array
|
|
95
|
+
# @return [Array<JointSpace>]
|
|
96
|
+
def to_a
|
|
97
|
+
JOINTS.map { |name| get(name) }.compact
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get the wrist joint
|
|
101
|
+
# @return [JointSpace, nil]
|
|
102
|
+
def wrist
|
|
103
|
+
get("wrist")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get thumb joints
|
|
107
|
+
# @return [Array<JointSpace>]
|
|
108
|
+
def thumb
|
|
109
|
+
%w[thumb-metacarpal thumb-phalanx-proximal thumb-phalanx-distal thumb-tip].map { |n| get(n) }.compact
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get the thumb tip
|
|
113
|
+
# @return [JointSpace, nil]
|
|
114
|
+
def thumb_tip
|
|
115
|
+
get("thumb-tip")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get index finger joints
|
|
119
|
+
# @return [Array<JointSpace>]
|
|
120
|
+
def index_finger
|
|
121
|
+
%w[
|
|
122
|
+
index-finger-metacarpal
|
|
123
|
+
index-finger-phalanx-proximal
|
|
124
|
+
index-finger-phalanx-intermediate
|
|
125
|
+
index-finger-phalanx-distal
|
|
126
|
+
index-finger-tip
|
|
127
|
+
].map { |n| get(n) }.compact
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get the index finger tip
|
|
131
|
+
# @return [JointSpace, nil]
|
|
132
|
+
def index_finger_tip
|
|
133
|
+
get("index-finger-tip")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get middle finger joints
|
|
137
|
+
# @return [Array<JointSpace>]
|
|
138
|
+
def middle_finger
|
|
139
|
+
%w[
|
|
140
|
+
middle-finger-metacarpal
|
|
141
|
+
middle-finger-phalanx-proximal
|
|
142
|
+
middle-finger-phalanx-intermediate
|
|
143
|
+
middle-finger-phalanx-distal
|
|
144
|
+
middle-finger-tip
|
|
145
|
+
].map { |n| get(n) }.compact
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Get the middle finger tip
|
|
149
|
+
# @return [JointSpace, nil]
|
|
150
|
+
def middle_finger_tip
|
|
151
|
+
get("middle-finger-tip")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get ring finger joints
|
|
155
|
+
# @return [Array<JointSpace>]
|
|
156
|
+
def ring_finger
|
|
157
|
+
%w[
|
|
158
|
+
ring-finger-metacarpal
|
|
159
|
+
ring-finger-phalanx-proximal
|
|
160
|
+
ring-finger-phalanx-intermediate
|
|
161
|
+
ring-finger-phalanx-distal
|
|
162
|
+
ring-finger-tip
|
|
163
|
+
].map { |n| get(n) }.compact
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Get the ring finger tip
|
|
167
|
+
# @return [JointSpace, nil]
|
|
168
|
+
def ring_finger_tip
|
|
169
|
+
get("ring-finger-tip")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get pinky finger joints
|
|
173
|
+
# @return [Array<JointSpace>]
|
|
174
|
+
def pinky_finger
|
|
175
|
+
%w[
|
|
176
|
+
pinky-finger-metacarpal
|
|
177
|
+
pinky-finger-phalanx-proximal
|
|
178
|
+
pinky-finger-phalanx-intermediate
|
|
179
|
+
pinky-finger-phalanx-distal
|
|
180
|
+
pinky-finger-tip
|
|
181
|
+
].map { |n| get(n) }.compact
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Get the pinky finger tip
|
|
185
|
+
# @return [JointSpace, nil]
|
|
186
|
+
def pinky_finger_tip
|
|
187
|
+
get("pinky-finger-tip")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get all finger tips
|
|
191
|
+
# @return [Array<JointSpace>]
|
|
192
|
+
def finger_tips
|
|
193
|
+
[thumb_tip, index_finger_tip, middle_finger_tip, ring_finger_tip, pinky_finger_tip].compact
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module Hand
|
|
5
|
+
# XRJointPose - Represents the pose of a hand joint
|
|
6
|
+
# Includes radius information for collision detection
|
|
7
|
+
class JointPose < Pose
|
|
8
|
+
# @param js_joint_pose [JS::Object] The XRJointPose JavaScript object
|
|
9
|
+
def initialize(js_joint_pose)
|
|
10
|
+
super(js_joint_pose)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Get the radius of the joint in meters
|
|
14
|
+
# This can be used for collision detection or visualization
|
|
15
|
+
# @return [Float]
|
|
16
|
+
def radius
|
|
17
|
+
js_float(:radius) || 0.0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get the diameter of the joint in meters
|
|
21
|
+
# @return [Float]
|
|
22
|
+
def diameter
|
|
23
|
+
radius * 2
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if the joint pose is valid (has meaningful data)
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def valid?
|
|
29
|
+
radius > 0
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module Hand
|
|
5
|
+
# XRJointSpace - Represents a joint's coordinate space
|
|
6
|
+
# Each joint has its own space that can be used for pose calculations
|
|
7
|
+
class JointSpace < Space
|
|
8
|
+
# @param js_joint_space [JS::Object] The XRJointSpace JavaScript object
|
|
9
|
+
def initialize(js_joint_space)
|
|
10
|
+
super(js_joint_space)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Get the joint name
|
|
14
|
+
# @return [String]
|
|
15
|
+
def joint_name
|
|
16
|
+
js_string(:jointName) || ""
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get the space type
|
|
20
|
+
# @return [String]
|
|
21
|
+
def type
|
|
22
|
+
"joint-space"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if this is a wrist joint
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def wrist?
|
|
28
|
+
joint_name == "wrist"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if this is a thumb joint
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def thumb?
|
|
34
|
+
joint_name.start_with?("thumb")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check if this is an index finger joint
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def index_finger?
|
|
40
|
+
joint_name.start_with?("index-finger")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if this is a middle finger joint
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def middle_finger?
|
|
46
|
+
joint_name.start_with?("middle-finger")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if this is a ring finger joint
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def ring_finger?
|
|
52
|
+
joint_name.start_with?("ring-finger")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if this is a pinky finger joint
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def pinky_finger?
|
|
58
|
+
joint_name.start_with?("pinky-finger")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if this is a finger tip
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def tip?
|
|
64
|
+
joint_name.end_with?("-tip")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if this is a metacarpal joint
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def metacarpal?
|
|
70
|
+
joint_name.end_with?("-metacarpal")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module Helpers
|
|
5
|
+
# InputHelper - Simplifies XR input handling
|
|
6
|
+
class InputHelper
|
|
7
|
+
# @return [Session] The XR session
|
|
8
|
+
attr_reader :session
|
|
9
|
+
|
|
10
|
+
# @return [ReferenceSpace] The reference space
|
|
11
|
+
attr_reader :reference_space
|
|
12
|
+
|
|
13
|
+
# @param session [Session] The XR session
|
|
14
|
+
# @param reference_space [ReferenceSpace] The reference space for pose calculations
|
|
15
|
+
def initialize(session, reference_space)
|
|
16
|
+
@session = session
|
|
17
|
+
@reference_space = reference_space
|
|
18
|
+
@select_callbacks = []
|
|
19
|
+
@squeeze_callbacks = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Register a callback for select events
|
|
23
|
+
# @yield [InputSource, InputSourceEvent, Pose] Called on select
|
|
24
|
+
# @return [void]
|
|
25
|
+
def on_select(&block)
|
|
26
|
+
return unless block_given?
|
|
27
|
+
|
|
28
|
+
@select_callbacks << block
|
|
29
|
+
setup_select_listener if @select_callbacks.size == 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Register a callback for squeeze events
|
|
33
|
+
# @yield [InputSource, InputSourceEvent, Pose] Called on squeeze
|
|
34
|
+
# @return [void]
|
|
35
|
+
def on_squeeze(&block)
|
|
36
|
+
return unless block_given?
|
|
37
|
+
|
|
38
|
+
@squeeze_callbacks << block
|
|
39
|
+
setup_squeeze_listener if @squeeze_callbacks.size == 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Iterate over each controller (tracked pointers)
|
|
43
|
+
# @yield [InputSource] Each controller input source
|
|
44
|
+
# @return [void]
|
|
45
|
+
def each_controller(&block)
|
|
46
|
+
@session.input_sources.each do |source|
|
|
47
|
+
next unless source.tracked_pointer?
|
|
48
|
+
|
|
49
|
+
block.call(source)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get the left controller
|
|
54
|
+
# @return [InputSource, nil]
|
|
55
|
+
def left_controller
|
|
56
|
+
@session.input_sources.left
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the right controller
|
|
60
|
+
# @return [InputSource, nil]
|
|
61
|
+
def right_controller
|
|
62
|
+
@session.input_sources.right
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get the primary controller (first available)
|
|
66
|
+
# @return [InputSource, nil]
|
|
67
|
+
def primary_controller
|
|
68
|
+
left_controller || right_controller || @session.input_sources.first
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the pose of an input source
|
|
72
|
+
# @param source [InputSource] The input source
|
|
73
|
+
# @param frame [Frame] The current frame
|
|
74
|
+
# @return [Pose, nil]
|
|
75
|
+
def pose_for(source, frame)
|
|
76
|
+
frame.pose(source.target_ray_space, @reference_space)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get the grip pose of an input source
|
|
80
|
+
# @param source [InputSource] The input source
|
|
81
|
+
# @param frame [Frame] The current frame
|
|
82
|
+
# @return [Pose, nil]
|
|
83
|
+
def grip_pose_for(source, frame)
|
|
84
|
+
grip = source.grip_space
|
|
85
|
+
return nil if grip.nil?
|
|
86
|
+
|
|
87
|
+
frame.pose(grip, @reference_space)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get button state for a controller
|
|
91
|
+
# @param source [InputSource] The input source
|
|
92
|
+
# @return [Hash] Hash with button states
|
|
93
|
+
def button_states(source)
|
|
94
|
+
gamepad = source.gamepad
|
|
95
|
+
return {} if gamepad.nil?
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
trigger: {
|
|
99
|
+
pressed: gamepad.trigger&.pressed?,
|
|
100
|
+
touched: gamepad.trigger&.touched?,
|
|
101
|
+
value: gamepad.trigger&.value || 0.0
|
|
102
|
+
},
|
|
103
|
+
squeeze: {
|
|
104
|
+
pressed: gamepad.squeeze&.pressed?,
|
|
105
|
+
touched: gamepad.squeeze&.touched?,
|
|
106
|
+
value: gamepad.squeeze&.value || 0.0
|
|
107
|
+
},
|
|
108
|
+
thumbstick: {
|
|
109
|
+
x: gamepad.thumbstick_x,
|
|
110
|
+
y: gamepad.thumbstick_y,
|
|
111
|
+
pressed: gamepad.thumbstick&.pressed?,
|
|
112
|
+
touched: gamepad.thumbstick&.touched?
|
|
113
|
+
},
|
|
114
|
+
touchpad: {
|
|
115
|
+
x: gamepad.touchpad_x,
|
|
116
|
+
y: gamepad.touchpad_y,
|
|
117
|
+
pressed: gamepad.touchpad&.pressed?,
|
|
118
|
+
touched: gamepad.touchpad&.touched?
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def setup_select_listener
|
|
126
|
+
@session.on(:select) do |event|
|
|
127
|
+
source = event.input_source
|
|
128
|
+
pose = event.pose(@reference_space)
|
|
129
|
+
@select_callbacks.each { |cb| cb.call(source, event, pose) }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def setup_squeeze_listener
|
|
134
|
+
@session.on(:squeeze) do |event|
|
|
135
|
+
source = event.input_source
|
|
136
|
+
pose = event.pose(@reference_space)
|
|
137
|
+
@squeeze_callbacks.each { |cb| cb.call(source, event, pose) }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module Helpers
|
|
5
|
+
# RenderingHelper - Simplifies WebXR rendering setup and loop
|
|
6
|
+
class RenderingHelper
|
|
7
|
+
# @return [WebGLLayer, nil] The WebGL layer
|
|
8
|
+
attr_reader :layer
|
|
9
|
+
|
|
10
|
+
# @return [Session] The XR session
|
|
11
|
+
attr_reader :session
|
|
12
|
+
|
|
13
|
+
# @return [ReferenceSpace] The reference space
|
|
14
|
+
attr_reader :reference_space
|
|
15
|
+
|
|
16
|
+
# @param session [Session] The XR session
|
|
17
|
+
# @param gl_context [JS::Object] The WebGL rendering context
|
|
18
|
+
# @param reference_space [ReferenceSpace] The reference space
|
|
19
|
+
def initialize(session, gl_context, reference_space)
|
|
20
|
+
@session = session
|
|
21
|
+
@gl = gl_context
|
|
22
|
+
@reference_space = reference_space
|
|
23
|
+
@layer = nil
|
|
24
|
+
@running = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Setup the WebGL layer
|
|
28
|
+
# @param options [Hash] Layer options
|
|
29
|
+
# @return [WebGLLayer]
|
|
30
|
+
def setup_layer(**options)
|
|
31
|
+
@layer = WebGLLayer.new(@session, @gl, **options)
|
|
32
|
+
@session.update_render_state(base_layer: @layer)
|
|
33
|
+
@layer
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Run the rendering frame loop
|
|
37
|
+
# @yield [Float, Frame, View, Viewport] Called for each view each frame
|
|
38
|
+
# @return [void]
|
|
39
|
+
def run_frame_loop(&render_callback)
|
|
40
|
+
@running = true
|
|
41
|
+
|
|
42
|
+
frame_callback = proc do |time, js_frame|
|
|
43
|
+
next unless @running && !@session.ended?
|
|
44
|
+
|
|
45
|
+
# Request next frame
|
|
46
|
+
@session.request_animation_frame(&frame_callback)
|
|
47
|
+
|
|
48
|
+
# Wrap the frame
|
|
49
|
+
frame = Frame.new(js_frame)
|
|
50
|
+
|
|
51
|
+
# Get viewer pose
|
|
52
|
+
pose = frame.viewer_pose(@reference_space)
|
|
53
|
+
next unless pose
|
|
54
|
+
|
|
55
|
+
# Bind framebuffer
|
|
56
|
+
@gl.call(:bindFramebuffer, @gl[:FRAMEBUFFER], @layer.framebuffer)
|
|
57
|
+
|
|
58
|
+
# Render each view
|
|
59
|
+
pose.views.each do |view|
|
|
60
|
+
viewport = @layer.viewport(view)
|
|
61
|
+
@gl.call(:viewport, viewport.x, viewport.y, viewport.width, viewport.height)
|
|
62
|
+
|
|
63
|
+
render_callback.call(time.to_f, frame, view, viewport)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@session.request_animation_frame(&frame_callback)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Stop the rendering loop
|
|
71
|
+
# @return [void]
|
|
72
|
+
def stop
|
|
73
|
+
@running = false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if the rendering loop is running
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def running?
|
|
79
|
+
@running
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Clear the framebuffer
|
|
83
|
+
# @param r [Float] Red (0.0 to 1.0)
|
|
84
|
+
# @param g [Float] Green (0.0 to 1.0)
|
|
85
|
+
# @param b [Float] Blue (0.0 to 1.0)
|
|
86
|
+
# @param a [Float] Alpha (0.0 to 1.0)
|
|
87
|
+
# @return [void]
|
|
88
|
+
def clear(r: 0.0, g: 0.0, b: 0.0, a: 1.0)
|
|
89
|
+
@gl.call(:clearColor, r, g, b, a)
|
|
90
|
+
@gl.call(:clear, @gl[:COLOR_BUFFER_BIT] | @gl[:DEPTH_BUFFER_BIT])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebXR
|
|
4
|
+
module Helpers
|
|
5
|
+
# SessionManager - Simplifies XR session lifecycle management
|
|
6
|
+
class SessionManager
|
|
7
|
+
# @return [Session, nil] The current session
|
|
8
|
+
attr_reader :session
|
|
9
|
+
|
|
10
|
+
# @return [ReferenceSpace, nil] The current reference space
|
|
11
|
+
attr_reader :reference_space
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@session = nil
|
|
15
|
+
@reference_space = nil
|
|
16
|
+
@xr_system = System.instance
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if WebXR is available
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def available?
|
|
22
|
+
!@xr_system.nil?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if a session mode is supported
|
|
26
|
+
# @param mode [String] The session mode to check
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def supports?(mode)
|
|
29
|
+
return false unless available?
|
|
30
|
+
|
|
31
|
+
@xr_system.session_supported?(mode)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Start an XR session
|
|
35
|
+
# @param mode [String] The session mode ("inline", "immersive-vr", "immersive-ar")
|
|
36
|
+
# @param reference_space_type [String] The reference space type
|
|
37
|
+
# @param required_features [Array<String>] Required features
|
|
38
|
+
# @param optional_features [Array<String>] Optional features
|
|
39
|
+
# @yield [Session, ReferenceSpace] Optional block for auto-cleanup
|
|
40
|
+
# @return [Session]
|
|
41
|
+
def start_session(mode, reference_space_type: ReferenceSpaceType::LOCAL_FLOOR,
|
|
42
|
+
required_features: [], optional_features: [])
|
|
43
|
+
raise NotSupportedError, "WebXR not available" unless available?
|
|
44
|
+
|
|
45
|
+
@session = @xr_system.request_session(
|
|
46
|
+
mode,
|
|
47
|
+
required_features: required_features,
|
|
48
|
+
optional_features: optional_features
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@reference_space = @session.request_reference_space(reference_space_type)
|
|
52
|
+
|
|
53
|
+
if block_given?
|
|
54
|
+
begin
|
|
55
|
+
yield @session, @reference_space
|
|
56
|
+
ensure
|
|
57
|
+
end_session
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@session
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# End the current session
|
|
65
|
+
# @return [void]
|
|
66
|
+
def end_session
|
|
67
|
+
@session&.end_session
|
|
68
|
+
@session = nil
|
|
69
|
+
@reference_space = nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if a session is currently active
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def active?
|
|
75
|
+
@session && !@session.ended?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Start an immersive VR session
|
|
79
|
+
# @param options [Hash] Session options
|
|
80
|
+
# @yield [Session, ReferenceSpace] Optional block
|
|
81
|
+
# @return [Session]
|
|
82
|
+
def start_vr(**options, &block)
|
|
83
|
+
start_session(SessionMode::IMMERSIVE_VR, **options, &block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Start an immersive AR session
|
|
87
|
+
# @param options [Hash] Session options
|
|
88
|
+
# @yield [Session, ReferenceSpace] Optional block
|
|
89
|
+
# @return [Session]
|
|
90
|
+
def start_ar(**options, &block)
|
|
91
|
+
options[:reference_space_type] ||= ReferenceSpaceType::LOCAL
|
|
92
|
+
start_session(SessionMode::IMMERSIVE_AR, **options, &block)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Start an inline session
|
|
96
|
+
# @param options [Hash] Session options
|
|
97
|
+
# @yield [Session, ReferenceSpace] Optional block
|
|
98
|
+
# @return [Session]
|
|
99
|
+
def start_inline(**options, &block)
|
|
100
|
+
options[:reference_space_type] ||= ReferenceSpaceType::VIEWER
|
|
101
|
+
start_session(SessionMode::INLINE, **options, &block)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|