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!
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.