如何構建具有實時跨設備預覽的虛擬現實模型
已發表: 2022-03-10虛擬現實(VR)是一種基於計算機生成環境的體驗; 許多不同的VR產品成為頭條新聞,其應用範圍廣泛:在冬奧會上,美國隊利用虛擬現實進行運動訓練; 外科醫生正在嘗試使用虛擬現實進行醫療培訓; 最常見的是,虛擬現實被應用於遊戲。
我們將專注於最後一類應用,並將特別關注點擊式冒險遊戲。 此類游戲屬於休閒類游戲; 目標是指向並單擊場景中的對象,以完成拼圖。 在本教程中,我們將在虛擬現實中構建此類游戲的簡單版本。 這是對三個維度編程的介紹,是在網絡上部署虛擬現實模型的獨立入門指南。 你將使用 webVR 進行構建,這是一個具有雙重優勢的框架——用戶可以在 VR 中玩你的遊戲,而沒有 VR 耳機的用戶仍然可以在手機或桌面上玩你的遊戲。
為虛擬現實開發
如今,任何開發者都可以為 VR 創建內容。 為了更好地了解 VR 開發,製作演示項目會有所幫助。 閱讀相關文章 →
在本教程的後半部分,您將為您的桌面構建一個“鏡像”。 這意味著玩家在移動設備上所做的所有動作都將反映在桌面預覽中。 這讓您可以看到玩家看到的內容,讓您可以提供指導、錄製遊戲或只是讓客人開心。
先決條件
要開始,您將需要以下內容。 對於本教程的後半部分,您將需要 Mac OSX。 雖然代碼可以適用於任何平台,但下面的依賴項安裝說明適用於 Mac。
- 互聯網訪問,特別是 glitch.com;
- 虛擬現實耳機(可選,推薦)。 我使用 Google Cardboard,每張 15 美元。
第 1 步:建立虛擬現實 (VR) 模型
在這一步中,我們將建立一個具有單個靜態 HTML 頁面的網站。 這使我們能夠從您的桌面編碼並自動部署到網絡。 然後可以將部署的網站加載到您的手機上並放置在 VR 耳機中。 或者,部署的網站可以由獨立的 VR 耳機加載。 通過導航到 glitch.com 開始。 然後,
- 點擊右上角的“新建項目”。
- 點擊下拉菜單中的“hello-express”。
接下來,單擊左側邊欄中的views/index.html 。 我們將其稱為您的“編輯器”。
要預覽網頁,請單擊左上角的“預覽”。 我們將此稱為您的預覽。 請注意,編輯器中的任何更改都將自動反映在此預覽中,除非出現錯誤或不受支持的瀏覽器。
回到您的編輯器,將當前的 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 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 耳機上預覽此內容,請使用多功能欄中的 URL。 在上圖中,URL 是https://point-and-click-vr-game.glitch.me/
。 您的工作環境現已建立; 隨時與家人和朋友分享此 URL。 在下一步中,您將創建一個虛擬現實模型。
第 2 步:構建樹模型
您現在將使用 aframe.io 中的原語創建一棵樹。 這些是 Aframe 為易於使用而預先編程的標準對象。 具體來說,Aframe 將對象稱為實體。 有三個與所有實體相關的概念來組織我們的討論:
- 幾何和材料,
- 轉換軸,
- 相對變換。
首先,幾何和材料是代碼中所有 3D 對象的兩個構建塊。 幾何定義了“形狀”——立方體、球體、金字塔等。 材質定義了形狀的靜態屬性,例如顏色、反射率、粗糙度。
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 步:將點擊交互添加到模型
要使實體具有交互性,您需要:
- 添加動畫,
- 點擊時觸發此動畫。
由於最終用戶使用的是虛擬現實耳機,因此點擊相當於盯著看:換句話說,盯著一個物體“點擊”它。 要實現這些更改,您將從光標開始。 通過將第 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>
通過修改第 29 行以匹配以下內容,將樹向右移動 2 個單位並將類“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 個單位) - 往返目的地時的動畫
- 在往返目的地之間交替動畫
- 重複此動畫一次。 這意味著對象總共動畫兩次——一次到目的地,一次回到原始位置。
最後,導航到您的預覽,然後從光標拖到您的樹上。 一旦黑色圓圈停在樹上,樹就會向右和向後移動。
這總結了在虛擬現實中構建點擊式冒險遊戲所需的基礎知識。 要查看和玩此遊戲的更完整版本,請參閱以下簡短場景。 任務是通過點擊場景中的各種物體來打開大門並隱藏大門後面的樹。
接下來,我們設置一個簡單的 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 以匹配您當前的故障。 示例故障項目名為point-and-click-vr-game
。 將其替換為您的故障項目的名稱。
"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 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 實用程序。
- 表示
這是我們將用來運行服務器的 Web 框架。 - http
這允許我們啟動一個守護進程,監聽各種端口上的活動。 - 套接字.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
作為主頁。 請注意,下面沒有編寫新代碼; 這只是對現有源代碼的解釋。
// https://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 會自動重新加載服務器。 單擊左上角的“顯示”以預覽您的應用程序。
您的 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|\-[aw])|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'"); }); });
同樣,通過單擊左上角的“顯示”來預覽應用程序。 在您的移動設備上加載相同的 URL。 在您的終端中,您將看到以下內容。
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 */ });
在您的桌面和手機上加載該應用程序。 手機的每一個動作都會反映在桌面對應的鏡子上! 您的應用程序的鏡像部分到此結束。 作為桌面用戶,您現在可以預覽移動用戶看到的內容。 本節介紹的概念對於這款遊戲的進一步開發至關重要,因為我們將單人遊戲轉變為多人遊戲。
結論
在本教程中,我們編寫了 3D 對象並為這些對象添加了簡單的交互。 此外,您在客戶端和服務器之間構建了一個簡單的消息傳遞系統,以實現移動用戶所見內容的桌面預覽。
這些概念甚至超出了 webVR,因為幾何和材質的概念擴展到 iOS 上的 SceneKit(與 ARKit 相關)、Three.js(Aframe 的主幹)和其他三維庫。 這些簡單的構建塊組合在一起,讓我們在創建成熟的點擊式冒險遊戲時具有足夠的靈活性。 更重要的是,它們允許我們創建任何帶有基於點擊的界面的遊戲。
以下是一些可供進一步探索的資源和示例:
- 鏡像VR
上面構建的實時預覽的完整實現。 只需一個 Javascript 鏈接,即可將移動設備上任何虛擬現實模型的實時預覽添加到桌面。 - 逐步地
一個兒童繪畫畫廊和每幅繪畫對應的虛擬現實模型。 - 一個框架
用於虛擬現實開發的示例、開發人員文檔和更多資源。 - 谷歌紙板體驗
為教育工作者提供定制工具的課堂體驗。
下一次,我們將構建一個完整的遊戲,在虛擬現實遊戲中使用 web sockets 來促進玩家之間的實時通信。 隨意在下面的評論中分享您自己的模型。