如何制作React VR应用






4.47/5 (5投票s)
React VR 是 Oculus 推出的一款框架,可让您构建虚拟现实 Web 应用。您可以利用它和一些 JavaScript 来制作自己的虚拟导览和用户界面。
引言
借助 React VR 框架,您现在可以构建 VR Web 应用。WebVR 是一项实验性 API,能够让您在浏览器中创建和查看 VR 体验。Oculus 推出的这项新技术的目的是让每个人都能访问虚拟现实,无论他们拥有何种设备。
制作 React VR 应用所需的唯一设备是头戴式显示器和兼容的浏览器。在仅仅查看 Web VR 应用时,甚至不需要头戴式显示器。React VR 是一个非常棒的框架,可以用 JavaScript 构建 VR 网站或应用。它利用了与 React Native 相同的设计,并允许您使用提供的组件制作虚拟现实导览和用户界面。
设置开发环境
在开始使用 React VR 之前,需要对将用于构建和管理 React VR 应用的依赖项进行设置。这些是 **Node.js** 和 **React VR CLI**。
安装 Node.js 后,请确保使用最新版本(至少 6.0)。如果不是,请执行以下操作:
- MacOS: 使用 Homebrew 安装 Node.js
- Windows: 从 nodejs.org 下载页面 进行安装
- Linux: 使用 **sudo apt-get install nodejs** 命令
然后,我们需要使用 **npm** 来安装 **React VR CLI**。
npm install -g react-vr-cli。
并检查您是否具有全局安装的 root 权限。
创建项目
导航到您希望创建新项目的目录,然后运行
react-vr init MyFirstReactVR
** 命令。
将目录名称更改为 MyFirstReactVR 之类的名称,然后运行 npm start
。
在浏览器中打开以下地址 https://:8081/vr/index.html,几秒钟后,您应该会看到 类似的动画。
单击并尝试拖动鼠标光标。此外,在支持 WebVR 的浏览器中,您将能够以完全虚拟现实模式佩戴头戴式显示器来探索这个 VR 环境。
使用 React VR 的 VR 导览示例
现在,让我们创建一个包含几个场景和导航的 VR 导览。首先,我们需要准备好架构。在我们的示例中,我们将在每个场景中创建用于导航的按钮,并在名为 **index.js.vr** 的 React 组件的构造函数中声明它们。
constructor(props) {
super(props);
scenes: [{
scene_image: 'initial.jpg',
step: 1,
navigations: [{
step: 2,
translate: [0.73, -0.15, 0.66],
rotation: [0, 36, 0]
}]
}, {
scene_image: 'step1.jpg',
step: 2,
navigations: [{
step: 3,
translate: [-0.43, -0.01, 0.9],
rotation: [0, 140, 0]
}]
}, {
scene_image: 'step2.jpg',
step: 3,
navigations: [{
step: 4,
translate: [-0.4, 0.05, -0.9],
rotation: [0, 0, 0]
}]
}, {
scene_image: 'step3.jpg',
step: 4,
navigations: [{
step: 5,
translate: [-0.55, -0.03, -0.8],
rotation: [0, 32, 0]
}]
}, {
scene_image: 'step4.jpg',
step: 5,
navigations: [{
step: 1,
translate: [0.2, -0.03, -1],
rotation: [0, 20, 0]
}]
}]
}
此外,我们在构造函数中将 current_scene
** 声明为 state。
constructor(props) {
this.state = {
...
current_scene: {}
...
}
对于渲染,我们如下修改 render()
** 方法:
render() {
return ( < View >
< Pano source = {
asset(this.state.current_scene['scene_image'])
}
style = {
{
transform: [{
translate: [0, 0, 0]
}]
}
}
/> {
this.state.current_scene['navigations'].map(function(item, i) {
return <Mesh key = {
i
}
style = {
{
layoutOrigin: [0.5, 0.5],
transform: [{
translate: item['translate']
}, {
rotateX: item['rotation'][0]
}, {
rotateY: item['rotation'][1]
}, {
rotateZ: item['rotation'][2]
}]
}
} >
< VrButton
style = {
{
width: 0.15,
height: 0.15,
borderRadius: 50,
backgroundColor: 'blue'
}
} >
< /VrButton> < /Mesh>
})
} < /View>
)
}
让此功能正常工作的最后一步是在 componentWillMount
** 函数中将 current_scene
** state 设置为 scenes
** 数组的第一个元素。
componentWillMount() {
this.setState({
current_scene: this.state.scenes[0]
});
}
结果应该是:
现在,我们为按钮添加一个简单的动画,并实现场景之间的导航逻辑。首先,对于导航,我们需要订阅 Mesh
** 元素的 onInput
** 事件,在构造函数中将其绑定到此事件,然后实现它。
...
constructor(props) {
...
this.onNavigationClick = this.onNavigationClick.bind(this);
...
}
...
onNavigationClick(item, e) {
if (e.nativeEvent.inputEvent.eventType === "mousedown" && e.nativeEvent.inputEvent.button === 0) {
var new_scene = this.state.scenes.find(i => i['step'] === item.step);
this.setState({
current_scene: new_scene
});
}
}
...
render() {
var that = this;
... < Mesh key = {
i
}
...
onInput = {
e => that.onNavigationClick(item, e)
}
.... >
...
}
然后,我们添加了一个简单的动画,试一试。为此,我们在 render
** 方法中将另一个按钮添加到了现有按钮的内部,并更改了它的大小。我们使用原生的 JS requestAnimationFrame
** 函数。
const DEFAULT_ANIMATION_BUTTON_RADIUS = 50;
const DEFAULT_ANIMATION_BUTTON_SIZE = 0.05;
constructor(props) {
...
this.state = {
...
animationWidth: DEFAULT_ANIMATION_BUTTON_SIZE,
animationRadius: DEFAULT_ANIMATION_BUTTON_RADIUS
...
}
...
this.animatePointer = this.animatePointer.bind(this);
}
...
componentWillUnmount() {
if (this.frameHandle) {
cancelAnimationFrame(this.frameHandle);
this.frameHandle = null;
}
}
componentDidMount() {
this.animatePointer();
}
animatePointer() {
var delta = this.state.animationWidth + 0.002;
var radius = this.state.animationRadius + 10;
if (delta >= 0.13) {
delta = DEFAULT_ANIMATION_BUTTON_SIZE;
radius = DEFAULT_ANIMATION_BUTTON_RADIUS;
}
this.setState({
animationWidth: delta,
animationRadius: radius
})
this.frameHandle = requestAnimationFrame(this.animatePointer);
}
...
render() {
... < VrButton
style = {
{
width: 0.15,
height: 0.15,
borderRadius: 50,
justifyContent: 'center',
alignItems: 'center',
borderStyle: 'solid',
borderColor: '#FFFFFF80',
borderWidth: 0.01
}
} >
< VrButton
style = {
{
width: that.state.animationWidth,
height: that.state.animationWidth,
borderRadius: that.state.animationRadius,
backgroundColor: '#FFFFFFD9'
}
} >
< /VrButton> < /VrButton>
...
}
结果看起来像这样:
现在,让我们实现与动画按钮及其旋转的交互。我们将围绕 X-Y-Z 轴旋转按钮。为此,我们需要订阅 **Pano** 组件的 onInput
** 事件,并通过 **向上箭头、向右箭头** 和 **向下箭头** 按钮更改旋转。
最后一项是实现 VR 线程和主线程之间的消息传递以交换数据。以下是接收消息以及在场景更改或图像开始/结束加载时发送消息的订阅代码。
componentWillMount() {
window.addEventListener('message', this.onMainWindowMessage);
...
}
onMainWindowMessage(e) {
switch (e.data.type) {
case 'newCoordinates':
var scene_navigation = this.state.current_scene.navigations[0];
this.state.current_scene.navigations[0]['translate'] = [e.data.coordinates.x, e.data.coordinates.y, e.data.coordinates.z]
this.forceUpdate();
break;
default:
return;
}
}
onNavigationClick(item, e) {
...
postMessage({
type: "sceneChanged"
})
this.state.animationWidth = DEFAULT_ANIMATION_BUTTON_SIZE;
this.state.animationRadius = DEFAULT_ANIMATION_BUTTON_RADIUS;
this.animatePointer();
...
}
sceneOnLoad() {
postMessage({
type: "sceneLoadStart"
})
}
sceneOnLoadEnd() {
postMessage({
type: "sceneLoadEnd"
})
}
render() {
... < Pano...
onLoad = {
this.sceneOnLoad
}
onLoadEnd = {
this.sceneOnLoadEnd
}
... / >
}
您可以在我们的 GitHub 存储库 中查看 index.vr.js。
在 client.js 中,我们通过鼠标滚轮实现缩放,通过双击实现位置更改。我们需要存储 VR
** 实例和 VRcamera
** 实例来实现上述逻辑。
function init(bundle, parent, options) {
const vr = new VRInstance(bundle, 'TMExample', parent, {
// Add custom options here
...options,
});
vr.render = function() {
// Any custom behavior you want to perform on each frame goes here
};
// Begin the animation loop
vr.start();
window.playerCamera = vr.player._camera;
window.vr = vr;
return vr;
}
然后,我们订阅 ondblclick
** 和 onmousewheel
**,并实现 **缩放** ** 和 **更改位置** ** 逻辑。
function onRendererDoubleClick() {
var x = 2 * (event.x / window.innerWidth) - 1;
var y = 1 - 2 * (event.y / window.innerHeight);
var coordinates = get3DPoint(window.playerCamera, x, y);
vr.rootView.context.worker.postMessage({
type: "newCoordinates",
coordinates: coordinates
});
}
function onRendererMouseWheel() {
if (event.deltaY > 0) {
if (window.playerCamera.zoom > 1) {
window.playerCamera.zoom -= 0.1;
window.playerCamera.updateProjectionMatrix();
}
} else {
if (window.playerCamera.zoom < 3) {
window.playerCamera.zoom += 0.1;
window.playerCamera.updateProjectionMatrix();
}
}
}
get3DPoint
** 是我们的自定义函数,用于使用 Three.js ** 将 **屏幕坐标** ** 转换为 **世界坐标** **,该函数实现在 cameraHelper.js ** 中。
import * as THREE from 'three';
export function get3DPoint(camera, x, y) {
var mousePosition = new THREE.Vector3(x, y, 0.5);
mousePosition.unproject(camera);
var dir = mousePosition.sub(camera.position).normalize();
return dir;
}
让我们 看看结果。
有时,加载场景图片需要时间。因此,我们实现了加载器来显示此过程。在 index.html 中,我们添加一个 **加载器** ** 和基于此 w3cschools 示例 ** 的 CSS。
<style>
body { margin: 0; }
#loader {
position: absolute;
left: 50%;
top: 50%;
z-index: 1;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.animate-bottom {
position: relative;
-webkit-animation-name: animatebottom;
-webkit-animation-duration: 1s;
animation-name: animatebottom;
animation-duration: 1s
}
@-webkit-keyframes animatebottom {
from { bottom:-100px; opacity:0 }
to { bottom:0px; opacity:1 }
}
@keyframes animatebottom {
from{ bottom:-100px; opacity:0 }
to{ bottom:0; opacity:1 }
}
#myDiv {
display: none;
text-align: center;
}
</style>
<body>
<div id='content' style="width:100%; height:100%">
<div id="loader"></div>
</div>
<script src="./client.bundle?platform=vr"></script>
<script>
ReactVR.init('../index.vr.bundle?platform=vr&dev=true', document.getElementById('content'));
</script>
</body>
别忘了 client.js 中来自 **VR 线程** ** 以启用/禁用动画的消息。
function init(bundle, parent, options) {
...
vr.rootView.context.worker.addEventListener('message', onVRMessage);
...
}
function onVRMessage(e) {
switch (e.data.type) {
case 'sceneChanged':
if (window.playerCamera.zoom != 1) {
window.playerCamera.zoom = 1;
window.playerCamera.updateProjectionMatrix();
}
break;
case 'sceneLoadStart':
document.getElementById('loader').style.display = 'block';
break;
case 'sceneLoadEnd':
document.getElementById('loader').style.display = 'none';
break;
default:
return;
}
}
请查看我们的 index.html 或 client.js。
整个项目代码示例可从 React VR GitHub ** 免费下载。