翻譯:瘋狂的技術宅
在本教程中,我們將建立三維物件併為它們新增簡單的互動。此外,你還可以學到如何在客戶端和伺服器之間建立簡單的訊息傳遞系統。
虛擬現實(VR)是一種依賴計算機生成環境的體驗,其應用範圍廣泛:美國利用虛擬現實進行冬季奧運會的運動訓練;外科醫生正在試驗用虛擬進行醫學培訓;把虛擬現實用於遊戲是最常見的一種應用。
我們將把目光放在最後一類程式上,並將專注於點選式冒險遊戲。這是一種休閒類遊戲,遊戲的目標是通過選擇場景中的三維物件來完成拼圖。在本教程中,我們將在虛擬現實中構建一個簡單的版本。這是一篇關於三維程式設計的介紹,是在 Web 上部署虛擬現實模型的獨立入門指南。你將使用 webVR 進行構建,這個框架具有雙重優勢 —— 使用者可以在VR中玩遊戲,而沒有VR眼鏡的使用者也可以在手機或桌面上玩。
在本教程的後半部分中,你將為桌面構建一個“映象”。這意味著在移動裝置上進行的所有移動都將會在桌面預覽中進行映象。這樣你可以看到玩家所看到的內容,允許你提供指導、記錄遊戲,或只是讓客人娛*樂。
至於為什麼“娛”和“樂”中間有一個奇怪的符號,因為,這兩個字連在一起在掘金是違規的,哈哈,不信你發文的時候可以試試~
前提條件
在開始之前你需要準備以下內容。對於本教程的後半部分,你將需要一臺Mac OSX。雖然程式碼可以應用於任何平臺,但下面依賴項的安裝說明適用於Mac。
- 網際網路接入,特別是glitch.com;
- VR 眼鏡(可選,推薦)。我用的是Google Cardboard,每個售價15美元。
步驟1:設定虛擬現實(VR)模型
在此步驟中,我們將設定一個包含單個靜態 HTML 頁面的網站。這樣可以允許你從桌面進行編碼並自動部署到Web上,然後可以將部署的網站載入到手機上並放入VR眼鏡內。或者部署的網站可以由獨立的 VR 眼鏡載入。首先開啟https://glitch.com/。然後
- 單擊右上角的 “New Project” 。
- 單擊下拉選單中的“hello-express”。
接下來,單擊左側邊欄中的 views/index.html。我們將此稱為你的“編輯器”。
要預覽網頁,請單擊左上角的“Preview”。我們將此作為你的預覽。請注意,編輯器中的任何更改都將會自動反映在預覽中,除非出現錯誤或不受支援的瀏覽器。
返回編輯器,將當前HTML替換為下面 VR 模型的程式碼框架。
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!-- blue sky -->
<a-sky color="#a3d0ed"></a-sky>
<!-- camera with wasd and panning controls -->
<a-entity camera look-controls wasd-controls position="0 0.5 2" rotation="0 0 0"></a-entity>
<!-- brown ground -->
<a-box shadow id="ground" shadow="receive:true" color="#847452" width="10" height="0.1" depth="10"></a-box>
<!-- start code here -->
<!-- end code here -->
</a-scene>
</body>
</html>
複製程式碼
之後可以看到以下內容:
要在VR眼鏡上預覽此功能,請使用 omnibar 中的URL。在上圖中,URL 為 https://point-and-click-vr-game.glitch.me/
。你的工作環境現在已建立,可以隨時與家人和朋友分享這個URL。在下一步中,你將建立一個虛擬現實模型。
步驟2:建立一個樹的模型
現在,我們將用 aframe.io 中的 primitives 建立一個樹。這是 Aframe 為便於使用而預程式設計的一些標準物件。具體來說,Aframe 將物件稱為實體(entities)。與實體相關的概念有三個:
- 幾何和材質,
- 轉換軸,
- 相對轉換。
首先,幾何和材質是程式碼中所有三維物件的兩個構建塊。幾何定義了一系列的“形狀” —— 立方體,球體,金字塔等。材質定義了形狀的靜態屬性,例如顏色、反射率、粗糙度。
Aframe 通過定義基元來簡化這個概念,例如 <a-box>
,<a-sphere>
,<a-cylinder>
以及許多其他基本原理來簡化幾何體及其材料。首先定義一個綠色球體。在程式碼的第19行,也就是 <!-- start code here -->
之後新增以下內容。
<!-- start code here -->
<a-sphere color="green" radius="0.5"></a-sphere> <!-- new line -->
<!-- end code here -->
複製程式碼
其次,有三個軸可以轉換物件。 x
軸是水平運動的,當我們向右移動時,x 值會增加。 y
軸垂直執行,y 值隨著我們向上移動而增加。 z
軸用垂直你的螢幕,當物件向你移動時,z 值會增加。我們可以沿這三個軸平移,旋轉或縮放實體。
例如,要將物件向“右”移動,我們需要增加其x值。要向上旋轉物件,我們需要沿 y 軸旋轉它。下面我們修改第19行來“向上”移動球體 —— 這意味著你需要增加球體的 y 值。請注意,所有轉換都指定為 <x> <y> <z>
,意味著要增加其y值,需要增加第二個值。預設情況下,所有物件都位於 0,0,0 位置。在下面新增 position
。
<!-- start code here -->
<a-sphere color="green" radius="0.5" position="0 1 0"></a-sphere> <!-- edited line -->
<!-- end code here -->
複製程式碼
第三,所有變換都相對於其父物件。要在樹中新增樹幹,就在上方球體內新增圓柱體,這樣可確定樹幹相對於球體的位置,還可以將你的樹木整合為一個單元。在<a-sphere ...>
和</ a-sphere>
標籤之間新增<a-cylinder>
實體。
<a-sphere color="green" radius="0.5" position="0 1 0">
<a-cylinder color="#84651e" position="0 -0.9 0" radius="0.05"></a-cylinder> <!-- new line -->
</a-sphere>
複製程式碼
接著新增兩個的綠色球體作為更多的葉子。
<a-sphere color="green" radius="0.5" position="0 0.75 0">
<a-cylinder color="#84651e" position="0 -0.9 0" radius="0.05"></a-cylinder>
<a-sphere color="green" radius="0.35" position="0 0.5 0"></a-sphere> <!-- new line -->
<a-sphere color="green" radius="0.2" position="0 0.8 0"></a-sphere> <!-- new line -->
</a-sphere>
複製程式碼
切換回預覽,你將看到下面這顆樹:
重新載入VR眼鏡上的網站預覽並檢視。在下一節中,我們將使這棵樹具有互動性。
步驟3:將Click Interaction新增到Model
要使實體具有互動性,你需要:
- 新增動畫,
- 點選時觸發動畫。
由於終端使用者使用VR眼鏡,點選動作相當於凝視:換句話說,盯著一個物件就是“點選”它。要實現這些更改,我們將從游標開始。用以下內容替換第13行來重新定義相機。
<a-entity camera look-controls wasd-controls position="0 0.5 2" rotation="0 0 0">
<a-entity cursor="fuse: true; fuseTimeout: 250"
position="0 0 -1"
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03"
material="color: black; shader: flat"
scale="0.5 0.5 0.5"
raycaster="far: 20; interval: 1000; objects: .clickable">
<!-- add animation here -->
</a-entity>
</a-entity>
複製程式碼
上面的程式碼新增了一個可以觸發單擊操作的遊標。注意 objects: .clickable
屬性。這意味著具有“可點選”類的所有物件將觸發動畫,並在適當的時候接收“單擊”命令。我們還將向單擊游標新增動畫,以便使使用者知道游標何時觸發單擊。當指向可點選的物件時,游標將緩慢收縮,在一秒鐘後捕捉以表示已單擊的物件。用以下程式碼替換註釋 <!-- add animation here -->
:
<a-animation begin="fusing" easing="ease-in" attribute="scale"
fill="backwards" from="1 1 1" to="0.2 0.2 0.2" dur="250"></a-animation>
複製程式碼
將樹向右移動 2 個單位,並修改第29行為以下內容將類 “clickable” 新增到樹中。
<a-sphere color="green" radius="0.5" position="2 0.75 0" class="clickable">
複製程式碼
接下來,我們將:
- 指定動畫,
- 點選即可觸發動畫。
感謝 Aframe 易於使用的動畫實體,這兩個步驟都可以快速連續完成。
在第33行新增一個 <a-animation>
標記,緊跟在 <a-cylinder>
標記之後但在 </a-sphere>
結尾之前。
<a-animation begin="click" attribute="position" from="2 0.75 0" to="2.2 0.75 0" fill="both" direction="alternate" repeat="1"></a-animation>
複製程式碼
上述屬性指定了動畫的許多配置。動畫:
- 由“click”事件觸發
- 修改樹的
position
- 從原始位置
2 0.75 0
開始 - 結束於
2.2 0.75 0
(向右移動0.2個單位) - 往返目的地時的動畫
- 在往返目的地之間的交替動畫
- 重複此動畫一次。這意味著物件動畫總共播放兩次: 一次到目的地,一次回到原始位置。
最後,切換到預覽,然後從游標拖動到樹。一旦黑色圓圈放在樹上,樹就會向右和向後移動。
這就結束了在虛擬現實中構建點選式冒險遊戲所需的所有基礎知識。要檢視和播放此遊戲的更完整版本,請參閱以下短片(alvinwan.com/shift/scene…
接下來,我們設定一個簡單的nodeJS伺服器來提供靜態演示。
步驟4:設定NodeJS伺服器
在此步驟中,我們將設定一個基本的、功能性的nodeJS伺服器,為你現有的VR模型提供服務。在編輯器的左側邊欄中,選擇package.json
。
首先刪除第2 - 4行。
"//1": "describes your app and its dependencies",
"//2": "https://docs.npmjs.com/files/package.json",
"//3": "updating this file will download and update your packages",
複製程式碼
將名稱改為mirrorvr
。
{
"name": "mirrorvr", // change me
"version": "0.0.1",
...
複製程式碼
在dependencies
下,新增socket.io
。
"dependencies": {
"express": "^4.16.3",
"socketio": "^1.0.0",
},
複製程式碼
更新儲存庫URL以匹配當前的glitch。示例glitch專案名為 point-and-click-vr-game
。用你的glitch專案名稱替換它。
"repository": {
"url": "https://glitch.com/edit/#!/point-and-click-vr-game"
},
複製程式碼
最後,將 "glitch"
標籤改為 "vr"
。
"keywords": [
"node",
"vr", // change me
"express"
]
複製程式碼
仔細檢查你的package.json
是否和以下內容一致。
{
"name": "mirrorvr",
"version": "0.0.1",
"description": "Mirror virtual reality models",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.16.3",
"socketio": "^1.0.0"
},
"engines": {
"node": "8.x"
},
"repository": {
"url": "https://glitch.com/edit/#!/point-and-click-vr-game"
},
"license": "MIT",
"keywords": [
"node",
"vr",
"express"
]
}
複製程式碼
在views/index.html
中仔細檢查上一部分的程式碼是否與以下內容一致。
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!-- blue sky -->
<a-sky color="#a3d0ed"></a-sky>
<!-- camera with wasd and panning controls -->
<a-entity camera look-controls wasd-controls position="0 0.5 2" rotation="0 0 0">
<a-entity cursor="fuse: true; fuseTimeout: 250"
position="0 0 -1"
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03"
material="color: black; shader: flat"
scale="0.5 0.5 0.5"
raycaster="far: 20; interval: 1000; objects: .clickable">
<a-animation begin="fusing" easing="ease-in" attribute="scale"
fill="backwards" from="1 1 1" to="0.2 0.2 0.2" dur="250"></a-animation>
</a-entity>
</a-entity>
<!-- brown ground -->
<a-box shadow id="ground" shadow="receive:true" color="#847452" width="10" height="0.1" depth="10"></a-box>
<!-- start code here -->
<a-sphere color="green" radius="0.5" position="2 0.75 0" class="clickable">
<a-cylinder color="#84651e" position="0 -0.9 0" radius="0.05"></a-cylinder>
<a-sphere color="green" radius="0.35" position="0 0.5 0"></a-sphere>
<a-sphere color="green" radius="0.2" position="0 0.8 0"></a-sphere>
<a-animation begin="click" attribute="position" from="2 0.75 0" to="2.2 0.75 0" fill="both" direction="alternate" repeat="1"></a-animation>
</a-sphere>
<!-- end code here -->
</a-scene>
</body>
</html>
複製程式碼
接著修改server.js
。
首先匯入幾個 NodeJS 包。
-
Express 這是伺服器的Web框架。
-
http
這允許我們啟動一個守護程式,監聽各種埠上的活動。
-
socket.io 用套接字實現允許我們可以實時地在客戶端和伺服器端之間進行通訊。
在匯入這些包時,我們還會初始化 ExpressJS 程式。請注意,前兩行已經為你編寫好了。
var express = require('express');
var app = express();
/* start new code */
var http = require('http').Server(app);
var io = require('socket.io')(http);
/* end new code */
// we've started you off with Express,
複製程式碼
載入包後,伺服器會返回 index.html
作為主頁。請注意,下面沒有新的程式碼;這只是對現有原始碼的解釋。
// http://expressjs.com/en/starter/basic-routing.html
app.get('/', function(request, response) {
response.sendFile(__dirname + '/views/index.html');
});
複製程式碼
最後,現有的原始碼指示程式繫結並偵聽預設情況下為3000的埠,除非另有說明。
// listen for requests :)
var listener = app.listen(process.env.PORT, function() {
console.log('Your app is listening on port ' + listener.address().port);
});
複製程式碼
完成編輯後,Glitch會自動重新載入伺服器。單擊左上角的“Show”預覽你的應用程式。
你的Web程式現已啟動並執行。接下來,我們將從客戶端向伺服器傳送訊息。
步驟5:從客戶端向伺服器傳送資訊
在此步驟中,我們將用客戶端初始化與伺服器的連線。客戶端還將通知伺服器它是手機還是桌面。首先,在 views/index.html
中匯入即將新增的Javascript檔案。
在第4行之後,包含一個新指令碼。
<script src="/client.js" type="text/javascript"></script>
複製程式碼
在第14行,將 camera-listener
新增到相機實體的屬性列表中。
<a-entity camera-listener camera look-controls...>
...
</a-entity>
複製程式碼
然後,切換到左側邊欄中的 public/client.js
。刪除此檔案中所有的Javascript程式碼。然後,定義一個工具函式,用於檢查客戶端是否是移動裝置。
/**
* Check if client is on mobile
*/
function mobilecheck() {
var check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
return check;
};
複製程式碼
接下來,我們將定義一系列與伺服器端交換的訊息。定義一個新的 socket.io 物件來表示客戶端與伺服器的連線。套接字連線後,將訊息記錄到控制檯。
var socket = io();
socket.on('connect', function() {
console.log(' * Connection established');
});
複製程式碼
檢查是否為移動裝置,並用 emit
函式將相應的資訊傳送到伺服器。
if (mobilecheck()) {
socket.emit('newHost');
} else {
socket.emit('newMirror');
}
複製程式碼
這樣就結束了客戶端的訊息傳送。現在修改伺服器程式碼,使其能夠接收此訊息並做出適當的反應。開啟伺服器端 server.js
檔案。
處理新連線,並立即偵聽客戶端型別。在檔案末尾新增以下內容。
/**
* Handle socket interactions
*/
io.on('connection', function(socket) {
socket.on('newMirror', function() {
console.log(" * Participant registered as 'mirror'")
});
socket.on('newHost', function() {
console.log(" * Participant registered as 'host'");
});
});
複製程式碼
再次通過單擊左上角的“Show”來預覽程式。在移動裝置上載入相同的網址。在你的終端中,你將看到以下內容。
listening on *: 3000
* Participant registered as 'host'
* Participant registered as 'mirror'
複製程式碼
這是第一個簡單的訊息傳遞,我們的客戶端將資訊傳送回伺服器。退出正在執行的 NodeJS 程式。對於此步驟的最後一部分,我們將讓客戶端將相機資訊傳送回伺服器。開啟public/client.js
。
在檔案的最後,新增以下內容。
var camera;
if (mobilecheck()) {
AFRAME.registerComponent('camera-listener', {
tick: function () {
camera = this.el.sceneEl.camera.el;
var position = camera.getAttribute('position');
var rotation = camera.getAttribute('rotation');
socket.emit('onMove', {
"position": position,
"rotation": rotation
});
}
});
}
複製程式碼
儲存並關閉。開啟你的伺服器程式碼檔案 server.js
來監聽這個 onMove
事件。
在套接字程式碼的newHost
塊中新增以下內容:
socket.on('newHost', function() {
console.log(" * Participant registered as 'host'");
/* start new code */
socket.on('onMove', function(data) {
console.log(data);
});
/* end new code */
});
複製程式碼
再次在桌面和移動裝置上載入預覽。連線移動客戶端後,伺服器將立即開始記錄從客戶端傳送到伺服器的攝像機位置和旋轉資訊。接下來實現相反的操作,從伺服器將資訊傳送回客戶端。
步驟6:從伺服器向客戶端傳送資訊
在此步驟中,你將向所有映象傳送主機的攝像機資訊。開啟主伺服器原始碼檔案 server.js
。
將 onMove
事件處理更改為以下內容:
socket.on('onMove', function(data) {
console.log(data); // delete me
socket.broadcast.emit('move', data)
});
複製程式碼
broadcast
修飾符能夠確保伺服器將此資訊傳送給連線到套接字的所有客戶端。將此資訊傳送到客戶端後,你需要相應地設定映象的相機。開啟客戶端指令碼 public/client.js
。
在這裡檢查客戶端是否為桌面。如果是,則接收移動資料並相應地記錄。
if (!mobilecheck()) {
socket.on('move', function(data) {
console.log(data);
});
}
複製程式碼
在桌面和移動裝置上載入預覽。在桌面瀏覽器中,開啟開發控制檯。然後,在手機上載入應用程式。一旦手機成功載入程式,桌面上的開發控制檯就會顯示相機位置和旋轉等資訊。
再次開啟客戶端指令碼 public/client.js
。我們最後將根據傳送的資訊調整客戶端攝像頭。
修改上面的事件處理程式以獲取 move
事件。
socket.on('move', function(data) {
/* start new code */
camera.setAttribute('rotation', data["rotation"]);
camera.setAttribute('position', data["position"]);
/* end new code */
});
複製程式碼
在桌面和手機上載入程式。你手機上的每個動作都會反映在桌面上相應的映象中!這樣就結束了程式的映象部分。作為桌面使用者,你現在可以預覽手機使用者看到的內容。本節介紹的概念對於進一步開發此遊戲至關重要,因為我們還會將單人遊戲轉變為多人遊戲。
結論
在本教程中,我們建立了三維物件併為這些物件新增了簡單的互動。還在客戶端和伺服器之間構建了一個簡單的訊息傳遞系統,以實現能對使用者看到的內容的在桌面進行預覽。
這些概念甚至超越了webVR,因為幾何和材料的概念擴充套件到了 iOS 上的 SceneKit(與ARKit相關),Three.js(Aframe的主幹)以及其他三維庫。這些簡單的構建塊組合在一起,使我們能夠靈活的建立一個完全成熟的點選式冒險遊戲。更重要的是,它們允許我們使用基於點選的介面建立任何遊戲。
以下是供你進一步探索的幾個資源和示例:
- MirrorVR 上面實時預覽功能的完全實現。只需一個Javascript連結,即可將移動裝置上的任何虛擬現實模型的實時預覽新增到桌面。
- Bit by Bit 兒童畫畫廊的虛擬現實模型。
- Aframe 虛擬現實開發的例子、開發人員文件和其它資源。
- Google Cardboard Experiences 為教師提供定製工具。
下一次我們將構建一個完整的遊戲,使用網路套接字來實現虛擬現實遊戲中玩家之間的實時通訊。