diff --git a/tesseract_viewer_python/examples/shapes_viewer_ssl_webxr_headset.py b/tesseract_viewer_python/examples/shapes_viewer_ssl_webxr_headset.py
new file mode 100644
index 00000000..8a2c7225
--- /dev/null
+++ b/tesseract_viewer_python/examples/shapes_viewer_ssl_webxr_headset.py
@@ -0,0 +1,134 @@
+# SSL may be necessary for some systems that use WebXR. By default WebXR is not allowed to run on insecure origins.
+# The user will need to accept the security warning for a self signed certificate.
+# Also allow all incoming addresses to connect to the server by binding to 0.0.0.0 instead of localhost.
+# Firewalls may also need to be configured to allow incoming connections.
+# Point your VR headset to the https address of the computer running the server. For example
+# https://192.168.1.10:8000 if the server is running on port 8000 on the computer with IP address
+# 192.168.1.10.
+#
+# To generate a self-signed certificate, run the following command (openssl must be installed):
+#
+# openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem
+#
+# When viewed on a VR headset web browser, click the "Enter VR" button to enter VR mode.
+
+from tesseract_robotics.tesseract_environment import Environment
+from tesseract_robotics.tesseract_common import ResourceLocator, SimpleLocatedResource
+import os
+import re
+import traceback
+from tesseract_robotics_viewer import TesseractViewer
+import numpy as np
+import time
+import sys
+import ssl
+
+shapes_urdf="""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+TESSERACT_SUPPORT_DIR = os.environ["TESSERACT_SUPPORT_DIR"]
+
+class TesseractSupportResourceLocator(ResourceLocator):
+ def __init__(self):
+ super().__init__()
+
+ def locateResource(self, url):
+ try:
+ try:
+ if os.path.exists(url):
+ return SimpleLocatedResource(url, url, self)
+ except:
+ pass
+ url_match = re.match(r"^package:\/\/tesseract_support\/(.*)$",url)
+ if (url_match is None):
+ print("url_match failed")
+ return None
+ if not "TESSERACT_SUPPORT_DIR" in os.environ:
+ return None
+ tesseract_support = os.environ["TESSERACT_SUPPORT_DIR"]
+ filename = os.path.join(tesseract_support, os.path.normpath(url_match.group(1)))
+ ret = SimpleLocatedResource(url, filename, self)
+ return ret
+ except:
+ traceback.print_exc()
+
+
+t_env = Environment()
+
+# locator must be kept alive by maintaining a reference
+locator=TesseractSupportResourceLocator()
+t_env.init(shapes_urdf, locator)
+
+ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ssl_context.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
+viewer = TesseractViewer(("0.0.0.0", 8000), ssl_context)
+
+viewer.update_environment(t_env, [0,0,0])
+
+viewer.start_serve_background()
+
+if sys.version_info[0] < 3:
+ raw_input("press enter")
+else:
+ input("press enter")
+
diff --git a/tesseract_viewer_python/package.xml b/tesseract_viewer_python/package.xml
index 6be04512..cd91e879 100644
--- a/tesseract_viewer_python/package.xml
+++ b/tesseract_viewer_python/package.xml
@@ -1,7 +1,7 @@
tesseract_viewer_python
- 0.4.0
+ 0.5.0
The tesseract_viewer_python package
John Wason
Apache 2.0
diff --git a/tesseract_viewer_python/setup.py b/tesseract_viewer_python/setup.py
index e692be23..c2b5f40a 100644
--- a/tesseract_viewer_python/setup.py
+++ b/tesseract_viewer_python/setup.py
@@ -14,7 +14,7 @@
description='Tesseract Viewer Python Library',
author='John Wason',
author_email='wason@wasontech.com',
- url='http://robotraconteur.com/',
+ url='https://github.com/tesseract-robotics/tesseract_python',
packages=['tesseract_robotics_viewer','tesseract_robotics_viewer.resources'],
package_data={'tesseract_robotics_viewer.resources':['static/index.html','static/app.js','geometries.json']},
license='Apache-2.0',
diff --git a/tesseract_viewer_python/tesseract_robotics_viewer/resources/static/app.js b/tesseract_viewer_python/tesseract_robotics_viewer/resources/static/app.js
index 94e5b62f..d78324b4 100644
--- a/tesseract_viewer_python/tesseract_robotics_viewer/resources/static/app.js
+++ b/tesseract_viewer_python/tesseract_robotics_viewer/resources/static/app.js
@@ -6,6 +6,7 @@ import { VRButton } from 'https://unpkg.com/three@0.153.0/examples/jsm/webxr/VRB
import { LineMaterial } from 'https://unpkg.com/three@0.153.0/examples/jsm/lines/LineMaterial.js'
import { Line2 } from 'https://unpkg.com/three@0.153.0/examples/jsm/lines/Line2.js'
import { LineGeometry } from 'https://unpkg.com/three@0.153.0/examples/jsm/lines/LineGeometry.js'
+import { XRControllerModelFactory } from 'https://unpkg.com/three@0.153.0/examples/jsm/webxr/XRControllerModelFactory.js';
import 'https://cdn.jsdelivr.net/npm/robust-websocket@1.0.0/robust-websocket.min.js';
class TesseractViewer {
@@ -31,6 +32,17 @@ class TesseractViewer {
this._update_trajectory_timer = null;
this._update_markers_timer = null;
this._update_scene_timer = null;
+ this._xr_dolly = null;
+ this._xr_controller1_grip = null;
+ this._xr_controller2_grip = null;
+ this._xr_controller1_model = null;
+ this._xr_controller2_model = null;
+ this._xr_gamepad1 = null;
+ this._xr_gamepad2 = null;
+ this._controls = null;
+ this._xr_drag_controller_start = null;
+ this._xr_drag_controller_orientation = null;
+ this._xr_drag_dolly_start = null;
}
@@ -39,9 +51,7 @@ class TesseractViewer {
this._clock = new THREE.Clock();
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth/window.innerHeight, 0.1, 1000 );
- camera.position.x = 3;
- camera.position.y = 3;
- camera.position.z = -1.5;
+ camera.position.set(3, 3, -1.5)
this._camera = camera;
const renderer = new THREE.WebGLRenderer( { antialias: true } );
@@ -71,7 +81,9 @@ class TesseractViewer {
document.body.appendChild( renderer.domElement );
- const controls = new OrbitControls( camera, renderer.domElement );
+ this._controls = new OrbitControls( camera, renderer.domElement );
+
+ let _this = this;
// Only add VR button if it is supported
if ( 'xr' in navigator )
@@ -79,6 +91,10 @@ class TesseractViewer {
if (await navigator.xr.isSessionSupported( 'immersive-vr' ))
{
document.body.appendChild( VRButton.createButton( renderer ) );
+
+ renderer.xr.addEventListener('sessionstart', function() {
+ _this.enterXR();
+ });
}
}
@@ -101,7 +117,6 @@ class TesseractViewer {
await this.updateScene();
- let _this = this;
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
let do_update = true;
@@ -119,7 +134,19 @@ class TesseractViewer {
createWebSocket() {
// Create a new WebSocket instance
- const socket = new RobustWebSocket('ws://localhost:8000/websocket', null, {
+
+ // Get host and port from current URL
+ const host = window.location.hostname;
+ const port = window.location.port;
+ let ws_protocol = "ws";
+
+ if (window.location.protocol === "https:") {
+ ws_protocol = "wss";
+ }
+
+ const ws_url = ws_protocol + "://" + host + ":" + port + "/websocket";
+
+ const socket = new RobustWebSocket(ws_url, null, {
shouldReconnect: (event,ws) => { return 1000; }
});
@@ -157,7 +184,9 @@ class TesseractViewer {
var delta = this._clock.getDelta();
if ( this._animation_mixer ) this._animation_mixer.update( delta );
- };
+
+ this.xrLocomotion();
+ }
async fetchIfModified(url, etag) {
let fetch_res;
@@ -704,8 +733,147 @@ class TesseractViewer {
tf.parent.remove(tf);
});
}
+
+ initXRControllers(dolly) {
+ const controller1 = this._renderer.xr.getController(0);
+ const controller2 = this._renderer.xr.getController(1);
+
+ dolly.add(controller1);
+ dolly.add(controller2);
+
+ const controllerModelFactory = new XRControllerModelFactory();
+
+ const controllerGrip1 = this._renderer.xr.getControllerGrip(0);
+ this._xr_controller1_grip = controllerGrip1;
+ this._xr_controller1_model = controllerModelFactory.createControllerModel(controllerGrip1);
+ controllerGrip1.add(this._xr_controller1_model);
+ dolly.add(controllerGrip1);
+
+ const controllerGrip2 = this._renderer.xr.getControllerGrip(1);
+ this._xr_controller2_grip = controllerGrip2;
+ this._xr_controller2_model = controllerModelFactory.createControllerModel(controllerGrip2);
+ controllerGrip2.add(this._xr_controller2_model);
+ dolly.add(controllerGrip2);
+
+ let _this = this;
+
+ controllerGrip1.addEventListener("connected", (e)=> {
+ _this._xr_gamepad1 = e.data.gamepad;
+ })
+
+ controllerGrip2.addEventListener("connected", (e)=> {
+ _this._xr_gamepad2 = e.data.gamepad;
+ })
+ }
+
+ enterXR() {
+ this._controls.saveState();
+ this._xr_dolly = new THREE.Object3D();
+ this.initXRControllers(this._xr_dolly)
+ this._xr_dolly.add(this._camera);
+ this._camera.position.set(0, 0, 0);
+ this._camera.rotation.set(0, 0, 0);
+ this._scene.add(this._xr_dolly);
+ this._xr_dolly.position.set(2.5,0,0);
+ this._xr_dolly.rotateY(Math.PI / 2.0);
+
+ this._renderer.xr.getSession().addEventListener('end', () => {
+ this._scene.add(this._camera);
+ this._scene.remove(this._xr_dolly);
+ this._xr_dolly.remove(this._camera);
+ this._xr_dolly = null;
+ this._xr_controller1_grip.remove(this._xr_controller1_model);
+ this._xr_controller2_grip.remove(this._xr_controller2_model);
+ this._xr_controller1_grip = null;
+ this._xr_controller2_grip = null;
+ this._xr_controller1_model = null;
+ this._xr_controller2_model = null;
+ this._controls.reset();
+ this._controls.update();
+ });
+
+
+ }
+
+ xrLocomotion()
+ {
+ if (this._xr_dolly && this._xr_gamepad2) {
+ try
+ {
+
+ if (this._xr_gamepad2.buttons[5].pressed)
+ {
+ if (!this._xr_drag_controller_start)
+ {
+ this._xr_drag_controller_start = this._xr_controller2_grip.position.clone();
+ let world_quat = new THREE.Quaternion();
+ this._xr_drag_controller_orientation = this._camera.getWorldQuaternion(world_quat);
+
+ this._xr_drag_dolly_start = this._xr_dolly.position.clone();
+ }
+
+ let controller_diff = this._xr_controller2_grip.position.clone().sub(this._xr_drag_controller_start);
+ controller_diff.applyQuaternion(this._xr_drag_controller_orientation.clone());
+ let y_diff = controller_diff.y;
+ y_diff = Math.floor(y_diff / 0.1);
+ if (y_diff < 0) {
+ y_diff = y_diff + 1;
+ }
+ controller_diff.y = y_diff * 0.1;
+ let dolly_pos = this._xr_drag_dolly_start.clone().sub(controller_diff);
+ this._xr_dolly.position.copy(dolly_pos);
+ }
+ else
+ {
+ if (this._xr_drag_controller_start)
+ {
+ this._xr_drag_controller_start = null;
+ this._xr_drag_dolly_start = null;
+ }
+ }
+
+ }
+ catch (e)
+ {
+ console.log(e);
+ }
+
+ if (this._xr_gamepad2.axes.length == 4) {
+ let axis_2 = this._xr_gamepad2.axes[2];
+ if (axis_2 > 0.2)
+ {
+ let scale = -(axis_2 - 0.2)/0.8;
+ this._xr_dolly.rotateY(0.01 * scale);
+ }
+ if (axis_2 < -0.2)
+ {
+ let scale = -(axis_2 + 0.2)/0.8;
+ this._xr_dolly.rotateY(0.01 * scale);
+ }
+
+ let axis_3 = this._xr_gamepad2.axes[3];
+ let dolly_world_quat = new THREE.Quaternion();
+ this._xr_dolly.getWorldQuaternion(dolly_world_quat);
+ let dolly_forward = new THREE.Vector3(0,0,-1);
+ dolly_forward.applyQuaternion(dolly_world_quat);
+
+ if (axis_3 > 0.2)
+ {
+ let scale = -(axis_3 - 0.2)/0.8;
+ this._xr_dolly.position.add(dolly_forward.clone().multiplyScalar(0.01 * scale));
+ }
+ if (axis_3 < -0.2)
+ {
+ let scale = -(axis_3 + 0.2)/0.8;
+ this._xr_dolly.position.add(dolly_forward.clone().multiplyScalar(0.01 * scale));
+ }
+ }
+ }
+ };
}
+
+
window.addEventListener("DOMContentLoaded", async function () {
let viewer = new TesseractViewer();
window.tesseract_viewer = viewer;
diff --git a/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_env_to_gltf.py b/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_env_to_gltf.py
index d46d53c8..776ac410 100644
--- a/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_env_to_gltf.py
+++ b/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_env_to_gltf.py
@@ -321,7 +321,6 @@ def _append_link_visual(gltf_dict, gltf_buf_io, link_name, visual, visual_i, sha
visual_node1["mesh"] = mesh_ind1
visual_nodes.append(visual_node1)
visual_inds.append(visual_ind1)
- print(mesh_dict1)
mesh_count += 1
return visual_nodes, visual_inds
@@ -551,8 +550,6 @@ def _append_trajectory_animation(gltf_dict, gltf_buf_io, tesseract_trajectory):
_append_dict_list(gltf_dict, "animations", animation)
-
- print(animation)
def _append_shapes_mesh_accessors(gltf_dict, gltf_buf_io, name):
o = _geometry_shapes[name]
diff --git a/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer.py b/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer.py
index b8471fa9..fcb98478 100644
--- a/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer.py
+++ b/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer.py
@@ -47,11 +47,13 @@ class TesseractViewer():
:param server_address: The address to bind the websocket server to. Defaults to ('127.0.0.1',8000)
:type server_address: Tuple[str,int], optional
+ :param ssl_context: The SSL context to use for the server. Default is None.
+ :type ssl_context: ssl.SSLContext, optional
"""
- def __init__(self, server_address = ('127.0.0.1',8000)):
+ def __init__(self, server_address = ('127.0.0.1',8000), ssl_context = None):
self.server_address = server_address
- self.aio_viewer = tesseract_viewer_aio.TesseractViewerAIO(self.server_address)
+ self.aio_viewer = tesseract_viewer_aio.TesseractViewerAIO(self.server_address, ssl_context)
self.scene_json = None
self.scene_glb = None
diff --git a/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer_aio.py b/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer_aio.py
index 51785cb6..abe0d9be 100644
--- a/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer_aio.py
+++ b/tesseract_viewer_python/tesseract_robotics_viewer/tesseract_viewer_aio.py
@@ -45,7 +45,7 @@ def hash_bytes(self, data):
data = data.encode("utf8")
return hashlib.sha256(data).hexdigest()
- async def start(self, host="127.0.0.1", port=8000):
+ async def start(self, host="127.0.0.1", port=8000, ssl_context = None):
try:
self._app = aiohttp_web.Application()
self._app.add_routes([aiohttp_web.get("/", self.index), aiohttp_web.get("/index.html", self.index)])
@@ -62,7 +62,7 @@ async def start(self, host="127.0.0.1", port=8000):
self._runner = aiohttp_web.AppRunner(self._app)
await self._runner.setup()
- self._site = aiohttp_web.TCPSite(self._runner, host, port)
+ self._site = aiohttp_web.TCPSite(self._runner, host, port, ssl_context = ssl_context)
await self._site.start()
except:
@@ -293,9 +293,12 @@ class TesseractViewerAIO:
:param server_address: The address to listen on. Default is localhost:8000.
:type server_address: tuple
+ :param ssl_context: The SSL context to use for the server. Default is None.
+ :type ssl_context: ssl.SSLContext
"""
- def __init__(self, server_address = ("localhost", 8080)):
+ def __init__(self, server_address = ("localhost", 8080), ssl_context = None):
self.server_address = server_address
+ self.ssl_context = ssl_context
self.server = _TesseractViewerAIOServer()
self._lock = asyncio.Lock()
self.server_task = None
@@ -367,7 +370,7 @@ async def start(self):
close() is called.
"""
async with self._lock:
- self.server_task = asyncio.create_task(self.server.start(self.server_address[0], self.server_address[1]))
+ self.server_task = asyncio.create_task(self.server.start(self.server_address[0], self.server_address[1], self.ssl_context))
async def close(self):
"""