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/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_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): """