作者:robertolopezmendez,文章出处:ARM Mali Graphics,发表时间:2016 年 4 月 20 日 08:31:41
冰洞演示是由 ARM® 生态系统发布的 Unity 演示。通过此演示,我们希望展示在配备 ARM Cortex® CPU 和 ARM Mali™ GPU 的现有移动设备上,是如何实现高质量视觉内容的。针对此演示,我们开发了许多高度优化的渲染效果。
在发布演示之后,我们决定使用 Unity 原生 VR 实现将该演示移植到 Samsung Gear VR 上。在移植工作期间,我们进行了多项修改,因为原始演示并非所有功能都适用于 VR。我们还增加了一些新功能,其中一项功能是能够将来自 Samsung Gear VR 头盔的内容镜像到第二台设备。在活动现场,这可以将 Samsung Gear VR 头盔用户实时看到的内容展现给其他人,我们认为这非常有趣。结果甚至超出了我们的预期。
图 1. 2016 年 Unity AR/VR Vision 峰会上来自 Samsung Gear VR 的冰洞 VR 镜像。
在每次活动中,只要我们展示运行在 Samsung Gear VR 上的冰洞 VR 演示镜像,就会被问到是如何实现这一点的。这篇简短的博客文章回答了这个问题。
我觉得这项技术引起广泛关注的原因是,我们希望将个人 VR 体验社交化,而其他人则单纯是对 VR 用户的体验感到好奇。这种分享体验的需求是双向的。对于开发人员来说,了解用户如何试玩和感受游戏不仅非常重要,同时也会有所裨益。
可选的方案
2014 年,Samsung 公开发布了 AllShare Cast Dongle,可用于通过 Samsung Gear VR 进行内容镜像。Dongle 可连接到任意 HMDI 显示设备,然后将智能手机的显示内容镜像到第二个屏幕,工作方式与 Google Chromecast 相似。不过,我们希望使用自己的设备,并打算尝试 Katie Goode(创意总监)在 ARM 研讨会上介绍的一个想法,我们听说这个想法已被 Triangular Pixels 证明可行。
这个想法非常简单:在第二台设备上运行应用程序的非 VR 版本,并通过 Wifi 发送所需的信息来同步两个应用程序。在我们测试时,只需要发送摄像机的位置和方向。
图 2. 镜像的基本想法。
实现
下面介绍的单个脚本可以管理客户端和服务器的所有网络连接。服务器是运行在 Samsung Gear VR 头盔上的 VR 应用程序,客户端是在第二台设备上运行的相同应用程序的非 VR 版本。
该脚本附加到摄像机游戏对象 (Game Object),并且有一个公共变量 isServer 定义了在构建 Unity 项目时脚本适用于服务器还是客户端。服务器的网络 IP 存储在配置文件中。当客户端应用程序启动时,它会读取服务器 IP 地址并等待服务器建立连接。
以下代码片段执行基本操作来设置网络连接,并使用函数 getInfoFromSettingsFile 读取服务器的 IP 网络地址和端口。请注意,客户端以暂停状态启动 (Time.timeScale = 0),因为它将等待服务器启动,然后再建立连接。
void Start(){
getInfoFromSettingsFile();
ConnectionConfig config = new ConnectionConfig();
commChannel = config.AddChannel(QosType.Reliable);
started = true;
NetworkTransport.Init();
// Maximum default connections = 2
HostTopology topology = new HostTopology(config, 2);
if (isServer){
hostId = NetworkTransport.AddHost(topology, port, null);
}
else{
Time.timeScale = 0;
hostId = NetworkTransport.AddHost(topology, 0);
当服务器应用程序开始运行时,它通过网络连接发送每一帧的摄像机位置和方向数据,这些数据可由客户端读取。此过程在 Update 函数中执行,具体实现方式如下。
void Update ()
{
if (!started){
return;
int recHostId;
int recConnectionId;
int recChannelId;
byte[] recBuffer = new byte[messageSize];
int recBufferSize = messageSize;
int recDataSize;
byte error;
NetworkEventType networkEvent;
do {
networkEvent = NetworkTransport.Receive(out recHostId, out recConnectionId,
out recChannelId, recBuffer, recBufferSize, out recDataSize, out error);
switch(networkEvent)
case NetworkEventType.Nothing:
break;
case NetworkEventType.ConnectEvent:
connected = true;
connectionId = recConnectionId;
Time.timeScale = 1; //client connected; unpause app.
if(!isServer){
clientId = recHostId;
case NetworkEventType.DataEvent:
rcvMssg(recBuffer);
case NetworkEventType.DisconnectEvent:
connected = false;
} while(networkEvent!=NetworkEventType.Nothing);
if (connected && isServer){ //Server
send();
if (!connected && !isServer){ // Client
tryToConnect();
在 Update 函数中处理不同类型的网络事件。一旦建立连接,客户端应用程序就会将其状态从“暂停”更改为“正在运行”(Time.timeScale = 1)。如果发生断开连接事件,则客户端会再次暂停。例如,在从 Samsung Gear VR 头盔上取下设备或者用户取下了头盔并且设备检测到这种情况时,就会发生断开连接事件并进入暂停模式。
客户端应用程序接收服务器在 NetworkEventType.DataEvent 情况下发送的数据。下面显示了读取数据的函数:
void rcvMssg(byte[] data)
var coordinates = new float[data.Length / 4];
Buffer.BlockCopy(data, 0, coordinates, 0, data.Length);
transform.position = new Vector3 (coordinates[0], coordinates[1], coordinates[2]);
// To provide a smooth experience on the client, average the change
// in rotation across the current and last frame
Quaternion rotation = avgRotationOverFrames (new Quaternion(coordinates [3], coordinates [4],
coordinates [5], coordinates [6]));
transform.rotation = rotation;
lastFrame = rotation;
这里有趣的一点是,客户端不直接使用收到的与摄像机位置和方向相关的数据。相反,将描述当前帧中摄像机旋转的四元数插值到上一帧的四元数中,以便实现平滑的摄像机旋转,避免在跳帧时的突然变化。函数 avgRotationOverFrames 执行四元数插值。
Quaternion avgRotationOverFrames(Quaternion currentFrame)
return Quaternion.Lerp(lastFrame,currentFrame, 0.5f);
如 Update 函数中所示,服务器通过网络发送每一帧的摄像机数据。下面显示了函数 send 的实现:
public void send()
byte[] buffer = new byte[messageSize];
buffer = createMssg();
try{
NetworkTransport.Send (hostId, connectionId, commChannel, buffer, buffer.Length, out error);
catch (Exception e){
Debug.Log("I'm Server error: +++ see below +++");
Debug.LogError(e);
函数 createMssg 准备一个由七个浮点数构成的数组;三个浮点数来自于摄像机位置坐标,四个浮点数来自于描述摄像机方向的摄像机四元数。
byte[] createMssg()
var coordinates = new float[] { transform.position.x, transform.position.y, transform.position.z,
transform.rotation.x, transform.rotation.y, transform.rotation.z,
transform.rotation.w};
var data = new byte[coordinates.Length * 4];
Buffer.BlockCopy(coordinates, 0, data, 0, data.Length);
return data;
此脚本附加到服务器和客户端应用程序的摄像机游戏对象。对于服务器,必须选中公共变量 isServer。此外,在构建客户端应用程序时,必须取消选中选项“生成设置”->“玩家设置”->“其他设置”->“支持虚拟现实”,因为客户端应用程序是运行在 Samsung Gear VR 上的应用程序的非 VR 版本。
为了确保实现方式尽可能简单,服务器 IP 地址和端口存储在客户端设备上的配置文件中。设置镜像系统时,第一步是启动客户端非 VR 应用程序。客户端应用程序从配置文件中读取网络数据,然后进入暂停状态,等待服务器启动来建立连接。
由于时间限制,我们无法将太多精力用于改善本博客文章中所介绍的镜像实现。我们非常希望听到任何反馈或改进建议,这样我们可以与其他开发人员分享这些内容。
下面的图片展示了我们在显示 Samsung Gear VR 头盔用户实时看到的内容时使用的镜像系统。使用 HDMI 适配器将视频信号输出到大型平板显示器,以便与其他人分享 Samsung Gear VR 用户体验。
图 3. 运行在 Samsung Gear VR 上的冰洞 VR 的镜像。VR 服务器应用程序运行在基于 Exynos 7 Octa 7420 SoC 的 Samsung Galaxy S6 上
(4x ARM Cortex-A57 + 4x Cortex-A53 和 ARM Mali-T760 MP8 GPU)非 VR 客户端应用程序运行在基于 Exynos 7 Octa 5433 SoC 的 Samsung Galaxy Note 4 上
(4x ARM Cortex-A57 + 4x Cortex-A53 和 ARM Mali-T760 MP6 GPU)。
总结
Unity 网络 API 可以简单而直接地将运行在 Samsung Gear VR 上的 VR 应用程序镜像到运行非 VR 版本应用程序的第二台设备上。而每一帧只发送摄像机位置和方向数据的这一实际情况,确保了对任何一台设备都不会施加过多的额外负载。
根据应用程序,可能需要发送/接收更多数据来同步服务器和客户端的环境,但需要遵循的原则是相同的:对于每个需要同步的对象,发送变换数据并进行插值。
本博客文章中介绍的镜像技术也适用于多玩家游戏环境。根据镜像设置的类型,服务器/客户端角色的名称可以交换。我们可以有多个 VR 头盔和一个屏幕,或者多个屏幕用于一个 VR 头盔,甚至是多个屏幕和多个 VR 头盔。同样,运行在 Samsung Gear VR 上的每个设备可将同步数据发送到一个或多个设备,这些设备可在一个大屏幕上共享视图。每个镜像应用程序必须实例化与之连接的每个玩家,更新围绕同一个接收方的所有同步对象的变换,并显示单一的摄像机视图。这可以是来自任意玩家的视图或任何其他合适的视图。发送额外的数据以保持镜像环境同步对于性能应该不会有明显的影响,因为每个对象需要更新的信息量确实很少。