VR 概念与设备
发展历程
计算机科学家、艺术家、哲学家、虚拟现实之父杰伦·拉尼尔在1987年创造了一个词组“virtual reality”,也就是我们所谓的VR,虚拟现实。从这一年起,这一研究领域终于有了一个正式的名字。
但早在这一概念出现以前,有关 VR 的探索就已经开始
理论萌芽
1838年,英国著名的物理学家查尔斯·惠斯通爵士,于1838年发现并确定立体图原理(Principle of stereo graph),为后续VR视觉模拟奠定了理论基础。
对这个理论的一个简单验证:
使用手机屏幕可以获得比较好的效果。全屏模式下手机距离眼睛大约一臂长, 调节双眼视焦直到左右画面重合。
核心在于 mock 人的左右眼的输入,来欺骗大脑形成空间感。
1935年,小说家斯坦利·G·温鲍姆发表短篇科幻小说《皮格马利翁的眼镜》。其中描述了一款VR眼镜,能够同时模拟视觉、触觉、嗅觉、味觉,并允许眼镜佩戴者和画面中的世界发生交互行为,并影响镜中世界的历史进程。这篇小说发表时,距离计算机的诞生还有 11 年。
这本小说被看作 VR 技术的预言书,直到今天,VR 设备仍不足以还原小说中的构想。
早期实验
1957年,摩登·海里戈(Morton Heilig) 的沉浸式电影体验设备。
摄影师摩登·海里戈分别于 1957、1960 年发明出两套 VR 设备,1960 年的这套,可以佩戴在头上,提供 3D 视觉与音频效果,外观上已经非常接近现在的 VR 头显。
1961年,美国飞歌公司两名工程师实现了一种动作追踪技术,基于这种技术,产生了许多军用设备和科研设备,但最初并没有用于 VR 领域。
1963年,雨果·根斯巴克(就是雨果奖的那个雨果)的头戴电视机,强调了画面的”实时性“。
蓝图
图灵奖获得者,计算机图形学之父伊凡·苏泽兰在1965年定义了所谓“终极显示器”的概念。此概念规定,某一种“终极显示器”所提供的内容可以让使用者无法区分与现实世界的差异:
-
通过头戴显示器可以展现3D的视觉和声音效果,能够提供触觉反馈;
-
由电脑提供图像并保证实时性;
-
用户能够通过与现实相同的方法与虚拟世界的物体进行互动。
三年后,伊凡带领学生尝试制作出“终极显示器”的雏形:达摩克里斯之剑。
应用
略
关键技术
成像
zhuanlan.zhihu.com/p/486154341
实时渲染
略
位置追踪
支持位置追踪的 VR 设备可以带来更好的用户体验。然而若位置追踪速度跟不上渲染速度,就会造成眩晕感。
outside-in
这是一种依赖于外接定位/探测设备的位置追踪技术。如下图所示,
在这种模式下,需要有相对于空间静止不动的定位基站作为空间参考坐标,不断扫描所在场景。而用户穿戴的运动设备上有可被探测的标记点,用来确定其空间位置。
inside-out
不借助外接设备,仅靠环境图像的特征比对/陀螺仪信息,通过算法推算当前设备相比于上一帧的旋转和位移。
VR 开发
环境:PICO SDK + Unity
VR 设备上的软件是一种接收并处理 VR 设备复杂的输入数据,并将处理结果依照软件内置逻辑,以图形化方式渲染在 VR 成像装置上的系统。
平台 SDK
各个 VR 硬件厂商的产品各有特点,一般都会对主流 3D 引擎提供 SDK,来解决软件的跨平台移植问题。其中最重要的是处理系统输入,将系统输入映射为对应游戏引擎的输入。除此之外,PICO Unity SDK 还提供一些高级功能,如场景渐变、混合现实捕捉、应用内购买、平台公共能力接口等。
使用 PICO + Unity 的开发模式,按照文档操作,可快速建立好开发环境:
developer-cn.pico-interactive.com/document/un…
Unity 引擎
Unity 引擎的学习是一个很大的话题~ 此处仅从 PICO VR 开发的角度切入,提一下务须了解的信息
Action
action 是 Unity 输入系统中的一个概念,介于物理设备输入信号 与 Unity 代码逻辑之间,是一个中间层。引入 action 的目的是使设备输入与代码逻辑解耦,所有的 action 统一在 Unity 提供的 action map 结构中定义触发条件,并且可以在运行时动态修改:
如上图,我们在 Action Map 中指定 action4 的触发条件为:
-
左手柄摇杆向前推,且
-
左手柄主按钮被按下
则当满足以上条件时,action4 将会被触发,并进入 action 的生命周期循环,触发相关的回调函数
图中 action 的每个生命周期都会触发对应的事件。
对于用户编写的脚本而言,action 是整个输入过程中能够获取到的最上游信息。
Component
一个 component = 一种能力
Unity 通过 gameObject + components 来构建整个场景。gameObject 是一个具有 transform(位置、旋转、缩放) 属性的对象,在这个对象上可以挂载其他各种不同种类的组件:
-
渲染
-
物理
-
动画
-
脚本
-
...
Event
Event 的概念贯穿整个开发流程。前文提到的 action 的各个生命周期,就是 event。在 Unity 中,event 本质是一个回调函数队列。它可以作为某个组件的公共属性存在,任何拿到这个属性的其他组件都可以向队列中添加新的回调函数。
基于 C# 的 delegate 来实现:
using System;
class A {
// delegate 是一种“类型安全”的函数指针(队列)。
// 可以通过运算符 + 和 - 来操作这个队列。
// 比如:
// public MyDelegate d;
// d += (int a, int b) => {Console.WriteLine(a + b)};
// d(1, 2); ==> 输出 3
// d += (int a, int b) => {Console.WriteLine(a + b + 1)};
// d(1, 2); ==> 输出 3 4
private delegate int MyDelegate(int a, int b);
// event 是对 delegate 的一种封装。
// 操作与 delegate 相统,但可以进一步添加 add / remove 方法来控制访问权限
public event MyDelegate myEvent;
}
事件无处不在,大致可以分为外部事件(设备输入的 action 生命周期 event) 和 内部事件(脚本自己注册的事件)
实例
用 action / component / event 三个基本元素组合的视角来梳理这个 demo 中发生的事情:
剑:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class WeaponAttractable : MonoBehaviour
{
public InputActionProperty attractTriggerActionProperty;
public InputActionProperty releaseTriggerActionProperty;
public GameObject attractOriginObject;
public float attractSpeed = 1;
private bool _isInHand = false;
private bool _isInAttract = false;
private Vector3 _weaponOriginPosition;
private Quaternion _weaponOriginRotation;
// Start is called before the first frame update
void Start()
{
_weaponOriginPosition = gameObject.transform.position;
_weaponOriginRotation = gameObject.transform.rotation;
attractTriggerActionProperty.action.performed += attract;
attractTriggerActionProperty.action.canceled += attractCanceled;
releaseTriggerActionProperty.action.started += release;
}
private void release(InputAction.CallbackContext obj)
{
_isInAttract = false;
_isInHand = false;
}
private void attractCanceled(InputAction.CallbackContext obj)
{
_isInAttract = false;
Debug.Log("action canceled");
}
private void attract(InputAction.CallbackContext obj)
{
_isInAttract = true;
Debug.Log("action triggered");
}
// Update is called once per frame
void Update()
{
if (_isInHand)
{
gameObject.transform.position = attractOriginObject.transform.position;
gameObject.transform.rotation = attractOriginObject.transform.rotation;
}
else if (_isInAttract)
{
gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, attractOriginObject.transform.position, attractSpeed * Time.deltaTime);
gameObject.transform.rotation = Quaternion.Lerp(gameObject.transform.rotation, attractOriginObject.transform.rotation, attractSpeed * Time.deltaTime);
}
else if (!_isInAttract && !_isInHand && gameObject.transform.position != _weaponOriginPosition)
{
// 需要把这个武器归位到 attract 之前的位置
gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, _weaponOriginPosition, attractSpeed * Time.deltaTime);
gameObject.transform.rotation = Quaternion.Lerp(gameObject.transform.rotation, _weaponOriginRotation, attractSpeed * Time.deltaTime);
}
}
private void OnCollisionEnter(Collision collision)
{
Debug.Log("weapon collision enter");
}
private void OnCollisionExit(Collision collision)
{
Debug.Log("weapon collision exit");
}
private void OnTriggerEnter(Collider other)
{
Debug.Log("weapon trigger enter");
if (other.gameObject.tag == "Hand")
{
_isInHand = true;
_isInAttract = false;
}
}
private void OnTriggerExit(Collider other)
{
Debug.Log("weapon trigger exit");
}
}
小细节:位移终点如何确定?
右手:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class handMove : MonoBehaviour
{
public InputActionProperty handAniActionProperty;
public InputActionProperty handPartAniActionProperty;
public float speed = 10;
private Animator animator;
private float currentAnimationProgress = 0;
private float currentPartAnimationProgress = 0;
private bool somethingInHand;
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
}
// Update is called once per frame
void Update()
{
if (somethingInHand)
{
float targetProgress = 1;
float targetPartProgress = 1;
float newProgress = Mathf.MoveTowards(currentAnimationProgress, targetProgress, Time.deltaTime * speed);
float newPartProgress = Mathf.MoveTowards(currentPartAnimationProgress, targetPartProgress, Time.deltaTime * speed);
animator.SetFloat("handleAnimationProgress", newProgress);
animator.SetFloat("partAnimationProgress", newPartProgress);
currentAnimationProgress = newProgress;
currentPartAnimationProgress = newPartProgress;
}
else
{
float targetProgress = 0;
float targetPartProgress = 0;
float newProgress = Mathf.MoveTowards(currentAnimationProgress, targetProgress, Time.deltaTime * speed);
float newPartProgress = Mathf.MoveTowards(currentPartAnimationProgress, targetPartProgress, Time.deltaTime * speed);
animator.SetFloat("handleAnimationProgress", newProgress);
animator.SetFloat("partAnimationProgress", newPartProgress);
currentAnimationProgress = newProgress;
currentPartAnimationProgress = newPartProgress;
}
}
private void OnCollisionEnter(Collision collision)
{
Debug.Log("hand collision enter");
}
private void OnCollisionExit(Collision collision)
{
Debug.Log("hand collision exit");
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Weapon")
{
somethingInHand = true;
}
Debug.Log("hand trigger enter");
}
private void OnTriggerExit(Collider other)
{
Debug.Log("hand trigger exit");
if (other.gameObject.tag == "Weapon")
{
somethingInHand = false;
}
}
};
Web VR
想要实现一个运行在浏览器上的 VR 程序,需要解决输入和渲染两个问题。
渲染方面,目前已经有较为成熟的 3d 图形库 与 物理引擎
输入方面,不同于 native vr 可以集成不同平台的 SDK 来分别发布,web 需要考虑跨平台
OpenXR 正是解决跨平台输入的方案。目前实验版本的 chrome 和 firefox 浏览器已经尝试接入了该规范,有理由相信,在不久的将来,将能够使用 js 语言在浏览器上实现简单的 VR 交互。