SpaceMouse liberated at last!

Hi all,

my impression is, that questions re. intuitive camera (or object) control in regular and/or VR/XR environments are coming in more frequently lately. Luckily, a much neglected and very affordable 6DoF input device has been around for decades now - it’s 3Dconnexion’s SpaceMouse, formerly also sold as “SpaceNavigator”.

If properly supported via software, it provides one-handed, ultra-smooth, simultaneous control of all 6 degrees of freedom (Tx, Ty, Tz, Rx, Ry, Rz)!

Until recently, I was put off by 3Dconnexion’s 3DxWare bloatware, which seriously cripples this fantastic device for use within a Web/JavaScript environment and hence: within Three.js.

But not any more! :nerd_face: :muscle: :sunglasses:

Below please find the full JavaScript code necessary to retrieve raw sensor data, including key-state changes and LED-control. 70 SLOC in total, no dependencies and no external code/libraries required.

Sounds too good to be true? Well, you’re right.

My code is based on WebHID, which is a very recent, some even say: experimental API and as such not widely supported by browsers yet. See caniuse on this. I developed and found my code to work on Google Chrome v100 on macOS 10.15.7 .

So here it is:

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>WebHID Playground</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
	</head>
	<body>

		<div id="info">
			WebHID Playground by <a href="https://vielzutun.ch" target="_blank" rel="noopener">vielzutun.ch</a> <br/>
		</div>

		<button onclick="selectDevice()">Request HID Device</button>
		<button onclick="ledOn()">LED On</button>
		<button onclick="ledOff()">LED Off</button>

		<script>

	let device;

	navigator.hid.addEventListener("connect", handleConnectedDevice);
	navigator.hid.addEventListener("disconnect", handleDisconnectedDevice);

	function handleConnectedDevice(e) {
		console.log("Device connected: " + e.device.productName);
	}

	function handleDisconnectedDevice(e) {
		console.log("Device disconnected: " + e.device.productName);
		console.dir(e);
	}

	function selectDevice() {

		navigator.hid.requestDevice({ filters: [{ vendorId: 0x046d }] })
		.then((devices) => {
			if (devices.length == 0) return;
			device = devices[0]
			if (!device.opened) device.open()		// avoid re-opening an already open device
			.then(() => {
		  		console.log("Opened device: " + device.productName);
		  		device.addEventListener("inputreport", handleInputReport);
			})
			.catch(error => { console.error(error)
			})
		});
	}

	function handleInputReport(e) {

		switch ( e.reportId ) {
			case 1:		// translation event
				const Tx = e.data.getInt16(0, true);		// 'true' parameter is for little endian data
				const Ty = e.data.getInt16(2, true);
				const Tz = e.data.getInt16(4, true);
				console.log("Tx: " + Tx + ", Ty: " + Ty + ", Tz: " + Tz);
				break;
				
			case 2:		// rotation event
				const Rx = e.data.getInt16(0, true);
				const Ry = e.data.getInt16(2, true);
				const Rz = e.data.getInt16(4, true);
				console.log("Rx: " + Rx + ", Ry: " + Ry + ", Rz: " + Rz);
				break;
				
			case 3:		// key press/release event
				const value = e.data.getUint8(0);
				/*
				 For my SpaceNavigator, a device having two (2) keys only:
				 value is a 2-bit bitmask, allowing 4 key-states:
				 value = 0: no keys pressed
				 value = 1: left key pressed
				 value = 2: right key pressed
				 value = 3: both keys pressed
				 */
				console.log("Left key " + ((value & 1) ? "pressed," : "released,") + "   Right key " + ((value & 2) ? "pressed, " : "released;"));
				break;
			
			default:		// just in case a device exhibits unexpected capabilities  8-)
				console.log(e.device.productName + ": Received UNEXPECTED input report " + e.reportId);
				console.log(new Uint8Array(e.data.buffer));
		}

	}


	function ledOn() {
		const outputReportId = 4;
		const outputReport = Uint8Array.from([1]);
		
		device.sendReport(outputReportId, outputReport)
		.then(() => {
			console.log("Sent output report " + outputReportId + ": " + outputReport);
		})
		.catch(error => { console.error(error)
		})
	}

	function ledOff() {
		const outputReportId = 4;
		const outputReport = Uint8Array.from([0]);
		
		device.sendReport(outputReportId, outputReport)
		.then(() => {
			console.log("Sent output report " + outputReportId + ": " + outputReport);
		})
		.catch(error => { console.error(error)
		})
	}
		

		</script>
	</body>
</html>

I’ve written an extensive blog post on the Why, How and some Caveats of this development effort, which is available both as a German and as an English version.

Feedback is welcome.

6 Likes

Hi all,

one of the biggest shortcomings of the above WebHID-based approach is its currently “sparse” support by various browser families.


Source: caniuse.com

I have filed an “idea” (see below) with the Mozilla development team through their idea submission channel, asking for WebHID support in Firefox, too.

If you feel this idea deserves some traction, your comment or like may make a difference in the allocation of Mozilla development resources.

Hi again,

I wrote a simple demo program for a SpaceMouse-with-Three.js application which processes raw sensor data of a SpaceMouse (see initial post of this thread) into an intuitive control of all six axes in 3D space.

Rotation:
Each axis of rotation can be independently controlled, without tainting the rotation of the remaining axes, and without sacrificing their simultaneous control. Control of Roll-axis can easily be disabled, by setting one variable to ‘false’. This is the default setting which I suggest novices at 6DoF devices keep until they have acquired the necessary level of opto-motorical coordination skills.

Attached to the camera is an (invisible) orthonormal coordinate system, aligned with the camera’s line-of-sight and up-direction, which I’m showing from a 3rd-person perspective for documentation purposes only:

Note the red/blue/green camera-axesHelper:

blue:  line-of-sight
green: up-vector
red:   side-vector

Translation:
When rendering the 1st-person view of the SpaceMouse-controlled camera, puck-displacement forward-backward corresponds to the camera dollying along the line of sight (blue). Likewise will a puck displacement along the red line effect a “horizontal” pan in screen space, and a puck-displacement along the green line a “vertical” pan in screen space.

Except for an import of Three.js the file is completely self contained and comprises approx. 300 LOC, including comments and minimal HTML.

Prerequisites:
SpaceMouse or SpaceNavigator
compatibel (i.e. WebHID enabled) browser.

WebHID.html (12.0 KB)