[TOC]
## 概況
### 背景
在嘗試一個晚上的開發環境搭建后,我放棄了開發原生應用的想法。一是沒有屬于自己的電腦(如果Raspberry Pi II不算的話)——沒有Windows、沒有GNU/Linux,二是公司配的電腦是Mac OS。對于嵌入式開發和游戲開發來說,Mac OS簡直是手機中的Windows Phone——坑爹的LLVM、GCC(Mac OS )、OpenGL、OGLPlus、C++11。并且官方對Mac OS和Linux的SDK的支持已經落后了好幾個世紀。
說到底,還是Web的開發環境到底還是比較容易搭建的。
### Showcase
這個repo的最后效果圖如下所示:

最后效果圖

Three.js Oculus Effect
效果:
1. WASD控制前進、后退等等。
2. 旋轉頭部 = 真實的世界。
3. 附加效果: 看久了頭暈。
### 框架: Oculus Rift & Node NMD
> Oculus Rift 是一款為電子游戲設計的頭戴式顯示器。這是一款虛擬現實設備。這款設備很可能改變未來人們游戲的方式。
## 步驟
現在,讓我們開始構建吧。
### Step 1: Node Oculus Services
這里,我們所要做的事情便是將傳感器返回來的四元數(Quaternions)與歐拉角(Euler angles)以API的形式返回到前端。
#### 安裝Node NMD
Node.js上有一個Oculus的插件名為node-hmd,hmd即面向頭戴式顯示器。它就是Oculus SDK的Node接口,雖說年代已經有些久遠了,但是似乎是可以用的——官方針對 Mac OS和Linux的SDK也已經很久沒有更新了。
在GNU/Linux系統下,你需要安裝下面的這些東西的
~~~
freeglut3-dev
mesa-common-dev
libudev-dev
libxext-dev
libxinerama-dev
libxrandr-dev
libxxf86vm-dev
~~~
Mac OS如果安裝失敗,請使用Clang來,以及GCC的C標準庫(PS: 就是 Clang + GCC的混合體,它們之間就是各種復雜的關系。。):
~~~
export CXXFLAGS=-stdlib=libstdc++
export CC=/usr/bin/clang
export CXX=/usr/bin/clang++
~~~
(PS: 我使用的是Mac OS El Captian + Xcode 7.0\. 2)clang版本如下:
~~~
Apple LLVM version 7.0.2 (clang-700.1.81)
Target: x86_64-apple-darwin15.0.0
Thread model: posix
~~~
反正都是會報錯的:
~~~
ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Service/Service_NetClient.o) was built for newer OSX version (10.7) than being linked (10.5)
ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Tracking/Tracking_SensorStateReader.o) was built for newer OSX version (10.7) than being linked (10.5)
ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_ImageWindow.o) was built for newer OSX version (10.7) than being linked (10.5)
ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_Interface.o) was built for newer OSX version (10.7) than being linked (10.5)
ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_LatencyTest2Reader.o) was built for newer OSX version (10.7) than being linked (10.5)
ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_Render_Stereo.o) was built for newer OSX version (10.7) than being linked (10.5)
node-hmd@0.2.1 node_modules/node-hmd
~~~
不過,有最后一行就夠了。
### Step 2: Node.js Oculus Hello,World
現在,我們就可以寫一個Hello,World了,直接來官方的示例~~。
~~~
var hmd = require('node-hmd');
var manager = hmd.createManager("oculusrift");
manager.getDeviceInfo(function(err, deviceInfo) {
if(!err) {
console.log(deviceInfo);
}
else {
console.error("Unable to retrieve device information.");
}
});
manager.getDeviceOrientation(function(err, deviceOrientation) {
if(!err) {
console.log(deviceOrientation);
}
else {
console.error("Unable to retrieve device orientation.");
}
});
~~~
運行之前,記得先連上你的Oculus。會有類似于下面的結果:
~~~
{ CameraFrustumFarZInMeters: 2.5,
CameraFrustumHFovInRadians: 1.29154372215271,
CameraFrustumNearZInMeters: 0.4000000059604645,
CameraFrustumVFovInRadians: 0.942477822303772,
DefaultEyeFov:
[ { RightTan: 1.0923680067062378,
LeftTan: 1.0586576461791992,
DownTan: 1.3292863368988037,
UpTan: 1.3292863368988037 },
{ RightTan: 1.0586576461791992,
LeftTan: 1.0923680067062378,
DownTan: 1.3292863368988037,
UpTan: 1.3292863368988037 } ],
DisplayDeviceName: '',
DisplayId: 880804035,
DistortionCaps: 66027,
EyeRenderOrder: [ 1, 0 ],
...
~~~
接著,我們就可以實時返回這些數據了。
### Step 3: Node Oculus WebSocket
在網上看到[http://laht.info/WebGL/DK2Demo.html](http://laht.info/WebGL/DK2Demo.html)這個虛擬現實的電影,并且發現了它有一個WebSocket,然而是Java寫的,只能拿來當參考代碼。
現在我們就可以寫一個這樣的Web Services,用的仍然是Express + Node.js + WS。
~~~
var hmd = require("node-hmd"),
express = require("express"),
http = require("http").createServer(),
WebSocketServer = require('ws').Server,
path = require('path');
// Create HMD manager object
console.info("Attempting to load node-hmd driver: oculusrift");
var manager = hmd.createManager("oculusrift");
if (typeof(manager) === "undefined") {
console.error("Unable to load driver: oculusrift");
process.exit(1);
}
// Instantiate express server
var app = express();
app.set('port', process.env.PORT || 3000);
app.use(express.static(path.join(__dirname + '/', 'public')));
app.set('views', path.join(__dirname + '/public/', 'views'));
app.set('view engine', 'jade');
app.get('/demo', function (req, res) {
'use strict';
res.render('demo', {
title: 'Home'
});
});
// Attach socket.io listener to the server
var wss = new WebSocketServer({server: http});
var id = 1;
wss.on('open', function open() {
console.log('connected');
});
// On socket connection set up event emitters to automatically push the HMD orientation data
wss.on("connection", function (ws) {
function emitOrientation() {
id = id + 1;
var deviceQuat = manager.getDeviceQuatSync();
var devicePosition = manager.getDevicePositionSync();
var data = JSON.stringify({
id: id,
quat: deviceQuat,
position: devicePosition
});
ws.send(data, function (error) {
//it's a bug of websocket, see in https://github.com/websockets/ws/issues/337
});
}
var orientation = setInterval(emitOrientation, 1000);
ws.on("message", function (data) {
clearInterval(orientation);
orientation = setInterval(emitOrientation, data);
});
ws.on("close", function () {
setTimeout(null, 500);
clearInterval(orientation);
console.log("disconnect");
});
});
// Launch express server
http.on('request', app);
http.listen(3000, function () {
console.log("Express server listening on port 3000");
});
~~~
總之,就是連上的時候不斷地發現設備的數據:
~~~
var data = JSON.stringify({
id: id,
quat: deviceQuat,
position: devicePosition
});
ws.send(data, function (error) {
//it's a bug of websocket, see in https://github.com/websockets/ws/issues/337
});
~~~
上面有一行注釋是我之前一直遇到的一個坑,總之需要callback就是了。
### Step 4: Oculus Effect + DK2 Control
在之前的版本中,Three.js都提供了Oculus的Demo,當然只能用來看。并且交互的接口是HTTP,感覺很難玩~~。
**Three.js DK2Controls**
這時,我們就需要根據上面傳過來的`四元數`(Quaternions)與歐拉角(Euler angles)來作相應的處理。
~~~
{
"position": {
"x": 0.020077044144272804,
"y": -0.0040545957162976265,
"z": 0.16216422617435455
},
"quat": {
"w": 0.10187230259180069,
"x": -0.02359195239841938,
"y": -0.99427556991577148,
"z": -0.021934293210506439
}
}
~~~
**歐拉角與四元數**
(ps: 如果沒copy好,麻煩提出正確的說法,原諒我這個掛過高數的人。我只在高中的時候,看到這些資料。)
> 歐拉角是一組用于描述剛體姿態的角度,歐拉提出,剛體在三維歐氏空間中的任意朝向可以由繞三個軸的轉動復合生成。通常情況下,三個軸是相互正交的。
對應的三個角度又分別成為roll(橫滾角),pitch(俯仰角)和yaw(偏航角),就是上面的postion里面的三個值。。
~~~
roll = (rotation about Z);
pitch = (rotation about (Roll ? Y));
yaw = (rotation about (Pitch ? Raw ? Z));”
~~~
– 引自《Oculus Rift In Action》
轉換成代碼。。
~~~
this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10);
~~~
> 四元數是由愛爾蘭數學家威廉·盧云·哈密頓在1843年發現的數學概念。
從明確地角度而言,四元數是復數的不可交換延伸。如把四元數的集合考慮成多維實數空間的話,四元數就代表著一個四維空間,相對于復數為二維空間。
反正就是用于`描述三維空間的旋轉變換`。
結合下代碼:
~~~
this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10);
this.headQuat.set(sensorData.quat.x, sensorData.quat.y, sensorData.quat.z, sensorData.quat.w);
this.camera.setRotationFromQuaternion(this.headQuat);
this.controller.setRotationFromMatrix(this.camera.matrix);
~~~
就是,我們需要設置camera和controller的旋轉。
這使我有足夠的理由相信Oculus就是一個手機 + 一個6軸運動處理組件的升級板——因為,我玩過MPU6050這樣的傳感器,如圖。。。

Oculus 6050
**Three.js DK2Controls**
雖然下面的代碼不是我寫的,但是還是簡單地說一下。
~~~
/*
Copyright 2014 Lars Ivar Hatledal
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
THREE.DK2Controls = function (camera) {
this.camera = camera;
this.ws;
this.sensorData;
this.lastId = -1;
this.controller = new THREE.Object3D();
this.headPos = new THREE.Vector3();
this.headQuat = new THREE.Quaternion();
var that = this;
var ws = new WebSocket("ws://localhost:3000/");
ws.onopen = function () {
console.log("### Connected ####");
};
ws.onmessage = function (evt) {
var message = evt.data;
try {
that.sensorData = JSON.parse(message);
} catch (err) {
console.log(message);
}
};
ws.onclose = function () {
console.log("### Closed ####");
};
this.update = function () {
var sensorData = this.sensorData;
if (sensorData) {
var id = sensorData.id;
if (id > this.lastId) {
this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10);
this.headQuat.set(sensorData.quat.x, sensorData.quat.y, sensorData.quat.z, sensorData.quat.w);
this.camera.setRotationFromQuaternion(this.headQuat);
this.controller.setRotationFromMatrix(this.camera.matrix);
}
this.lastId = id;
}
this.camera.position.addVectors(this.controller.position, this.headPos);
if (this.camera.position.y < -10) {
this.camera.position.y = -10;
}
if (ws) {
if (ws.readyState === 1) {
ws.send("get\n");
}
}
};
};
~~~
打開WebSocket的時候,不斷地獲取最新的傳感器狀態,然后update。誰在調用update方法?Three.js
我們需要在我們的初始化代碼里初始化我們的control:
~~~
var oculusControl;
function init() {
...
oculusControl = new THREE.DK2Controls( camera );
...
}
~~~
并且不斷地調用update方法。
~~~
function animate() {
requestAnimationFrame( animate );
render();
stats.update();
}
function render() {
oculusControl.update( clock.getDelta() );
THREE.AnimationHandler.update( clock.getDelta() * 100 );
camera.useQuaternion = true;
camera.matrixWorldNeedsUpdate = true;
effect.render(scene, camera);
}
~~~
最后,添加相應的KeyHandler就好了~~。
### Step 5: Three.js KeyHandler
KeyHandler對于習慣了Web開發的人來說就比較簡單了:
~~~
this.onKeyDown = function (event) {
switch (event.keyCode) {
case 87: //W
this.wasd.up = true;
break;
case 83: //S
this.wasd.down = true;
break;
case 68: //D
this.wasd.right = true;
break;
case 65: //A
this.wasd.left = true;
break;
}
};
this.onKeyUp = function (event) {
switch (event.keyCode) {
case 87: //W
this.wasd.up = false;
break;
case 83: //S
this.wasd.down = false;
break;
case 68: //D
this.wasd.right = false;
break;
case 65: //A
this.wasd.left = false;
break;
}
};
~~~
然后就是萬惡的if語句了:
~~~
if (this.wasd.up) {
this.controller.translateZ(-this.translationSpeed * delta);
}
if (this.wasd.down) {
this.controller.translateZ(this.translationSpeed * delta);
}
if (this.wasd.right) {
this.controller.translateX(this.translationSpeed * delta);
}
if (this.wasd.left) {
this.controller.translateX(-this.translationSpeed * delta);
}
this.camera.position.addVectors(this.controller.position, this.headPos);
if (this.camera.position.y < -10) {
this.camera.position.y = -10;
}
~~~
快接上你的HMD試試吧~~
### 練習建議