Skip to content

Commit 56b3352

Browse files
authored
Add WebXR Locomotion (player movement) to tesseract_viewer_python (#78)
* Add webxr locomotion to tesseract_viewer_python * Remove debug print statements * Bump version
1 parent c2a1a03 commit 56b3352

File tree

7 files changed

+322
-18
lines changed

7 files changed

+322
-18
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# SSL may be necessary for some systems that use WebXR. By default WebXR is not allowed to run on insecure origins.
2+
# The user will need to accept the security warning for a self signed certificate.
3+
# Also allow all incoming addresses to connect to the server by binding to 0.0.0.0 instead of localhost.
4+
# Firewalls may also need to be configured to allow incoming connections.
5+
# Point your VR headset to the https address of the computer running the server. For example
6+
# https://192.168.1.10:8000 if the server is running on port 8000 on the computer with IP address
7+
# 192.168.1.10.
8+
#
9+
# To generate a self-signed certificate, run the following command (openssl must be installed):
10+
#
11+
# openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem
12+
#
13+
# When viewed on a VR headset web browser, click the "Enter VR" button to enter VR mode.
14+
15+
from tesseract_robotics.tesseract_environment import Environment
16+
from tesseract_robotics.tesseract_common import ResourceLocator, SimpleLocatedResource
17+
import os
18+
import re
19+
import traceback
20+
from tesseract_robotics_viewer import TesseractViewer
21+
import numpy as np
22+
import time
23+
import sys
24+
import ssl
25+
26+
shapes_urdf="""
27+
<robot name="multipleshapes">
28+
29+
<link name="world"/>
30+
<link name="cylinder_link">
31+
<visual>
32+
<geometry>
33+
<cylinder length="0.6" radius="0.2"/>
34+
</geometry>
35+
<material name="red">
36+
<color rgba="0.8 0 0 1"/>
37+
</material>
38+
</visual>
39+
</link>
40+
41+
<joint name="clyinder_joint" type="revolute">
42+
<parent link="world"/>
43+
<child link="cylinder_link"/>
44+
<axis xyz="0 1 0"/>
45+
<limit effort="0" lower="-2.0944" upper="2.0944" velocity="6.2832"/>
46+
</joint>
47+
48+
<link name="box_link">
49+
<visual>
50+
<geometry>
51+
<box size="0.6 0.1 0.2"/>
52+
</geometry>
53+
<material name="green">
54+
<color rgba="0 0.8 0 1"/>
55+
</material>
56+
</visual>
57+
</link>
58+
59+
<joint name="box_joint" type="revolute">
60+
<parent link="world"/>
61+
<child link="box_link"/>
62+
<origin xyz="1 0 0"/>
63+
<axis xyz="0 1 0"/>
64+
<limit effort="0" lower="-2.0944" upper="2.0944" velocity="6.2832"/>
65+
</joint>
66+
67+
<link name="sphere_link">
68+
<visual>
69+
<geometry>
70+
<sphere radius="0.5"/>
71+
</geometry>
72+
<material name="blue">
73+
<color rgba="0 0 0.8 1"/>
74+
</material>
75+
</visual>
76+
</link>
77+
78+
<joint name="sphere_joint" type="revolute">
79+
<parent link="world"/>
80+
<child link="sphere_link"/>
81+
<origin xyz="2 0 0"/>
82+
<axis xyz="0 1 0"/>
83+
<limit effort="0" lower="-2.0944" upper="2.0944" velocity="6.2832"/>
84+
</joint>
85+
86+
</robot>
87+
"""
88+
89+
TESSERACT_SUPPORT_DIR = os.environ["TESSERACT_SUPPORT_DIR"]
90+
91+
class TesseractSupportResourceLocator(ResourceLocator):
92+
def __init__(self):
93+
super().__init__()
94+
95+
def locateResource(self, url):
96+
try:
97+
try:
98+
if os.path.exists(url):
99+
return SimpleLocatedResource(url, url, self)
100+
except:
101+
pass
102+
url_match = re.match(r"^package:\/\/tesseract_support\/(.*)$",url)
103+
if (url_match is None):
104+
print("url_match failed")
105+
return None
106+
if not "TESSERACT_SUPPORT_DIR" in os.environ:
107+
return None
108+
tesseract_support = os.environ["TESSERACT_SUPPORT_DIR"]
109+
filename = os.path.join(tesseract_support, os.path.normpath(url_match.group(1)))
110+
ret = SimpleLocatedResource(url, filename, self)
111+
return ret
112+
except:
113+
traceback.print_exc()
114+
115+
116+
t_env = Environment()
117+
118+
# locator must be kept alive by maintaining a reference
119+
locator=TesseractSupportResourceLocator()
120+
t_env.init(shapes_urdf, locator)
121+
122+
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
123+
ssl_context.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
124+
viewer = TesseractViewer(("0.0.0.0", 8000), ssl_context)
125+
126+
viewer.update_environment(t_env, [0,0,0])
127+
128+
viewer.start_serve_background()
129+
130+
if sys.version_info[0] < 3:
131+
raw_input("press enter")
132+
else:
133+
input("press enter")
134+

tesseract_viewer_python/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<package format="3">
33
<name>tesseract_viewer_python</name>
4-
<version>0.4.0</version>
4+
<version>0.5.0</version>
55
<description>The tesseract_viewer_python package</description>
66
<maintainer email="wason@wasontech.com">John Wason</maintainer>
77
<license>Apache 2.0</license>

tesseract_viewer_python/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
description='Tesseract Viewer Python Library',
1515
author='John Wason',
1616
author_email='wason@wasontech.com',
17-
url='http://robotraconteur.com/',
17+
url='https://github.com/tesseract-robotics/tesseract_python',
1818
packages=['tesseract_robotics_viewer','tesseract_robotics_viewer.resources'],
1919
package_data={'tesseract_robotics_viewer.resources':['static/index.html','static/app.js','geometries.json']},
2020
license='Apache-2.0',

tesseract_viewer_python/tesseract_robotics_viewer/resources/static/app.js

Lines changed: 175 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { VRButton } from 'https://unpkg.com/three@0.153.0/examples/jsm/webxr/VRB
66
import { LineMaterial } from 'https://unpkg.com/three@0.153.0/examples/jsm/lines/LineMaterial.js'
77
import { Line2 } from 'https://unpkg.com/three@0.153.0/examples/jsm/lines/Line2.js'
88
import { LineGeometry } from 'https://unpkg.com/three@0.153.0/examples/jsm/lines/LineGeometry.js'
9+
import { XRControllerModelFactory } from 'https://unpkg.com/three@0.153.0/examples/jsm/webxr/XRControllerModelFactory.js';
910
import 'https://cdn.jsdelivr.net/npm/robust-websocket@1.0.0/robust-websocket.min.js';
1011

1112
class TesseractViewer {
@@ -31,6 +32,17 @@ class TesseractViewer {
3132
this._update_trajectory_timer = null;
3233
this._update_markers_timer = null;
3334
this._update_scene_timer = null;
35+
this._xr_dolly = null;
36+
this._xr_controller1_grip = null;
37+
this._xr_controller2_grip = null;
38+
this._xr_controller1_model = null;
39+
this._xr_controller2_model = null;
40+
this._xr_gamepad1 = null;
41+
this._xr_gamepad2 = null;
42+
this._controls = null;
43+
this._xr_drag_controller_start = null;
44+
this._xr_drag_controller_orientation = null;
45+
this._xr_drag_dolly_start = null;
3446

3547
}
3648

@@ -39,9 +51,7 @@ class TesseractViewer {
3951
this._clock = new THREE.Clock();
4052

4153
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth/window.innerHeight, 0.1, 1000 );
42-
camera.position.x = 3;
43-
camera.position.y = 3;
44-
camera.position.z = -1.5;
54+
camera.position.set(3, 3, -1.5)
4555
this._camera = camera;
4656

4757
const renderer = new THREE.WebGLRenderer( { antialias: true } );
@@ -71,14 +81,20 @@ class TesseractViewer {
7181

7282
document.body.appendChild( renderer.domElement );
7383

74-
const controls = new OrbitControls( camera, renderer.domElement );
84+
this._controls = new OrbitControls( camera, renderer.domElement );
85+
86+
let _this = this;
7587

7688
// Only add VR button if it is supported
7789
if ( 'xr' in navigator )
7890
{
7991
if (await navigator.xr.isSessionSupported( 'immersive-vr' ))
8092
{
8193
document.body.appendChild( VRButton.createButton( renderer ) );
94+
95+
renderer.xr.addEventListener('sessionstart', function() {
96+
_this.enterXR();
97+
});
8298
}
8399
}
84100

@@ -101,7 +117,6 @@ class TesseractViewer {
101117

102118
await this.updateScene();
103119

104-
let _this = this;
105120
const queryString = window.location.search;
106121
const urlParams = new URLSearchParams(queryString);
107122
let do_update = true;
@@ -119,7 +134,19 @@ class TesseractViewer {
119134

120135
createWebSocket() {
121136
// Create a new WebSocket instance
122-
const socket = new RobustWebSocket('ws://localhost:8000/websocket', null, {
137+
138+
// Get host and port from current URL
139+
const host = window.location.hostname;
140+
const port = window.location.port;
141+
let ws_protocol = "ws";
142+
143+
if (window.location.protocol === "https:") {
144+
ws_protocol = "wss";
145+
}
146+
147+
const ws_url = ws_protocol + "://" + host + ":" + port + "/websocket";
148+
149+
const socket = new RobustWebSocket(ws_url, null, {
123150
shouldReconnect: (event,ws) => { return 1000; }
124151
});
125152

@@ -157,7 +184,9 @@ class TesseractViewer {
157184

158185
var delta = this._clock.getDelta();
159186
if ( this._animation_mixer ) this._animation_mixer.update( delta );
160-
};
187+
188+
this.xrLocomotion();
189+
}
161190

162191
async fetchIfModified(url, etag) {
163192
let fetch_res;
@@ -704,8 +733,147 @@ class TesseractViewer {
704733
tf.parent.remove(tf);
705734
});
706735
}
736+
737+
initXRControllers(dolly) {
738+
const controller1 = this._renderer.xr.getController(0);
739+
const controller2 = this._renderer.xr.getController(1);
740+
741+
dolly.add(controller1);
742+
dolly.add(controller2);
743+
744+
const controllerModelFactory = new XRControllerModelFactory();
745+
746+
const controllerGrip1 = this._renderer.xr.getControllerGrip(0);
747+
this._xr_controller1_grip = controllerGrip1;
748+
this._xr_controller1_model = controllerModelFactory.createControllerModel(controllerGrip1);
749+
controllerGrip1.add(this._xr_controller1_model);
750+
dolly.add(controllerGrip1);
751+
752+
const controllerGrip2 = this._renderer.xr.getControllerGrip(1);
753+
this._xr_controller2_grip = controllerGrip2;
754+
this._xr_controller2_model = controllerModelFactory.createControllerModel(controllerGrip2);
755+
controllerGrip2.add(this._xr_controller2_model);
756+
dolly.add(controllerGrip2);
757+
758+
let _this = this;
759+
760+
controllerGrip1.addEventListener("connected", (e)=> {
761+
_this._xr_gamepad1 = e.data.gamepad;
762+
})
763+
764+
controllerGrip2.addEventListener("connected", (e)=> {
765+
_this._xr_gamepad2 = e.data.gamepad;
766+
})
767+
}
768+
769+
enterXR() {
770+
this._controls.saveState();
771+
this._xr_dolly = new THREE.Object3D();
772+
this.initXRControllers(this._xr_dolly)
773+
this._xr_dolly.add(this._camera);
774+
this._camera.position.set(0, 0, 0);
775+
this._camera.rotation.set(0, 0, 0);
776+
this._scene.add(this._xr_dolly);
777+
this._xr_dolly.position.set(2.5,0,0);
778+
this._xr_dolly.rotateY(Math.PI / 2.0);
779+
780+
this._renderer.xr.getSession().addEventListener('end', () => {
781+
this._scene.add(this._camera);
782+
this._scene.remove(this._xr_dolly);
783+
this._xr_dolly.remove(this._camera);
784+
this._xr_dolly = null;
785+
this._xr_controller1_grip.remove(this._xr_controller1_model);
786+
this._xr_controller2_grip.remove(this._xr_controller2_model);
787+
this._xr_controller1_grip = null;
788+
this._xr_controller2_grip = null;
789+
this._xr_controller1_model = null;
790+
this._xr_controller2_model = null;
791+
this._controls.reset();
792+
this._controls.update();
793+
});
794+
795+
796+
}
797+
798+
xrLocomotion()
799+
{
800+
if (this._xr_dolly && this._xr_gamepad2) {
801+
try
802+
{
803+
804+
if (this._xr_gamepad2.buttons[5].pressed)
805+
{
806+
if (!this._xr_drag_controller_start)
807+
{
808+
this._xr_drag_controller_start = this._xr_controller2_grip.position.clone();
809+
let world_quat = new THREE.Quaternion();
810+
this._xr_drag_controller_orientation = this._camera.getWorldQuaternion(world_quat);
811+
812+
this._xr_drag_dolly_start = this._xr_dolly.position.clone();
813+
}
814+
815+
let controller_diff = this._xr_controller2_grip.position.clone().sub(this._xr_drag_controller_start);
816+
controller_diff.applyQuaternion(this._xr_drag_controller_orientation.clone());
817+
let y_diff = controller_diff.y;
818+
y_diff = Math.floor(y_diff / 0.1);
819+
if (y_diff < 0) {
820+
y_diff = y_diff + 1;
821+
}
822+
controller_diff.y = y_diff * 0.1;
823+
let dolly_pos = this._xr_drag_dolly_start.clone().sub(controller_diff);
824+
this._xr_dolly.position.copy(dolly_pos);
825+
}
826+
else
827+
{
828+
if (this._xr_drag_controller_start)
829+
{
830+
this._xr_drag_controller_start = null;
831+
this._xr_drag_dolly_start = null;
832+
}
833+
}
834+
835+
}
836+
catch (e)
837+
{
838+
console.log(e);
839+
}
840+
841+
if (this._xr_gamepad2.axes.length == 4) {
842+
let axis_2 = this._xr_gamepad2.axes[2];
843+
if (axis_2 > 0.2)
844+
{
845+
let scale = -(axis_2 - 0.2)/0.8;
846+
this._xr_dolly.rotateY(0.01 * scale);
847+
}
848+
if (axis_2 < -0.2)
849+
{
850+
let scale = -(axis_2 + 0.2)/0.8;
851+
this._xr_dolly.rotateY(0.01 * scale);
852+
}
853+
854+
let axis_3 = this._xr_gamepad2.axes[3];
855+
let dolly_world_quat = new THREE.Quaternion();
856+
this._xr_dolly.getWorldQuaternion(dolly_world_quat);
857+
let dolly_forward = new THREE.Vector3(0,0,-1);
858+
dolly_forward.applyQuaternion(dolly_world_quat);
859+
860+
if (axis_3 > 0.2)
861+
{
862+
let scale = -(axis_3 - 0.2)/0.8;
863+
this._xr_dolly.position.add(dolly_forward.clone().multiplyScalar(0.01 * scale));
864+
}
865+
if (axis_3 < -0.2)
866+
{
867+
let scale = -(axis_3 + 0.2)/0.8;
868+
this._xr_dolly.position.add(dolly_forward.clone().multiplyScalar(0.01 * scale));
869+
}
870+
}
871+
}
872+
};
707873
}
708874

875+
876+
709877
window.addEventListener("DOMContentLoaded", async function () {
710878
let viewer = new TesseractViewer();
711879
window.tesseract_viewer = viewer;

0 commit comments

Comments
 (0)