如何构建具有实时跨设备预览的虚拟现实模型
已发表: 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 来促进玩家之间的实时通信。 随意在下面的评论中分享您自己的模型。