Oculus + Node.js + Three.js 打造VR世界

Phodal發表於2015-11-24

Oculus Rift 是一款為電子遊戲設計的頭戴式顯示器。這是一款虛擬現實裝置。這款裝置很可能改變未來人們遊戲的方式。

週五Hackday Showcase的時候,突然有了點小靈感,便將閒置在公司的Oculus DK2借回家了——已經都是灰塵了~~。

在嘗試一個晚上的開發環境搭建後,我放棄了開發原生應用的想法。一是沒有屬於自己的電腦(如果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的開發環境到底還是比較容易搭建的。這個repo的最後效果圖如下所示:

最後效果圖

效果:

  1. WASD控制前進、後退等等。
  2. 旋轉頭部 = 真實的世界。
  3. 附加效果: 看久了頭暈。

現在,讓我們開始構建吧。

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

不過,有最後一行就夠了。

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 ],
  ...

接著,我們就可以實時返回這些資料了。

Node Oculus WebSocket

在網上看到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就是了。

Three.js + Oculus Effect + DK2 Control

在最後我們需要如下的畫面:

Three.js Oculus Effect

當然,如果你已經安裝了Web VR這一類的東西,你就不需要這樣的效果了。如標題所說,你已經知道要用Oculus Effect,它是一個Three.js的外掛。

在之前的版本中,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就好了~~。

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試試吧~~

結語

如我在《RePractise前端篇: 前端演進史》一文中所說的,這似乎就是新的"前端"。

相關文章