全景虚拟漫游其实看到很多例子,比如地图上的全景,校园的全景,之前在朋友圈流行转发的公司全景。但真的想起来要去研究或者实现一下,是前几日说工作上可能会有这样的一个需求。觉悟来得太晚,好奇心也不够重,这么好玩新奇的东西怎么一开始没想到去尝试呢?

由于对这方面的了解是一篇空白的,于是直接在google里搜,发现资料很少,说的稍微全一些的就属《打造H5里的“3D全景漫游”秘籍 》。几种方案中,打算决定采用ThreeJS,因为之前也稍微看过一点,也打算学习,这正好是一个机会。但是看到这篇里面的教程其实说的比较含糊,关键步骤应该都展示出来了,但是没有完整的代码还是不能让我有个很好的概念的。然后,我就愣住了。。。直到我问了一下公司的同事,他告诉我ThreeJS官网有现成的例子!!! 一下子又被自己的智商惊呆了。。。还是没有养成什么都先去官网看看的习惯。

本文以官网的例子为例,具体分析全景虚拟漫游的原理和实现细节。

基本概念

用自己的话来说,ThreeJS是一个3D的JS库,封装了WebGL的功能。而WebGL(Web Graphic Library)又是什么呢,简而言之,就是在浏览器端开发3D图形相关的程序的一个库或者说一个标准。

开发3D程序的过程好比拍照或者录制视频,首先要有场景,然后场景中可能有几个物体,接着把相机放置在某处,并且摆放好位置对准某个地方,这样就能记录下来了。想要在浏览器中显示出来,还需要一个渲染器。至于渲染器到底做了什么或者原理是什么,我们可能先不要管。总的来说,相机,场景,渲染器是开启一个3D程序必须的三个要素。三者的关系如下图所示(虽然我对这张图不是特别得满意,但没有找到其他合适的,将就着用)。

入门例子

直接上个简单例子。

//创建场景
var scene = new THREE.Scene();
//创建透视相机
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window,innerHeight, 1, 1000);
//创建一个渲染器,添加到body元素中
var renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);
//创建一个物体,几何+材质 =》三维物体
var geometry = new THREE.CubeGeometry(1,1,1);
var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
var cube = new THREE.Mesh(geometry, material);
//将物体添加到场景中
scene.add(cube);//定义渲染函数
function render(){requestAnimationFrame(render);cube.rotation.x += 0.1;cube.rotation.y += 0.1;renderer.render(scene, camera);
}
//实时渲染
render();

这样就创建了一个渲染的立方体的3D程序,是不是挺简单的。

其中需要稍微说一下的是,相机创建的时候带的那些参数。相机分为正交相机和透视相机。透视相机下的世界是真实的效果,有着远小近大的关系。如下图,你很容易看出二者的区别。

透视相机的参数如下

PerspectiveCamera( fov, aspect, near, far )
fov — Camera frustum(截面椎体) vertical field of view.
aspect — Camera frustum aspect ratio.
near — Camera frustum near plane.
far — Camera frustum far plane.

用张图直观地表示。

全景虚拟漫游分析

人的眼睛其实就相当于相机。那么做虚拟漫游重点就在于如何构造720度无死角的场景,而相机处于正中间,也就是坐标系原点(未必,但这样更方便)。由于人在不同的方向看到的最远距离是一样,所以理论上全景相对于人眼来说,是一个球形。但是如果将场景映射到球形上,似乎有些复杂。这里我们把全景抽象出一个立方体,对我们来说有上下左右前后六个面。原理分析清楚了,关键的问题就是如下几个了:

  • 如何把六张图片拼接成一个立方体
  • 渲染器如何将立方体的效果处理得逼真
  • 如何表示和实现相机角度的移动

首先我们要清楚,ThreeJS使用的坐标系是右手坐标系, x轴向右,y轴向上,z轴向前。创建相机或者物体时的默认位置是坐标系的原点。所以想要把六张图拼接成一个立方体,只要进行一些平移旋转变化就行了。主要注意的是,图片的朝向要一致向原点,这样相机才能都看得到。

至于渲染器如何把立方体变成全景漫游的效果,ThreeJS作者新定义了一个渲染器叫CSS3DRenderer封装渲染的过程。我大概看了一点,没太看懂,感觉大致是用CSS3的translate3d,scale属性将图片进行变形,以达到迷惑人眼的效果。

搜了一些资料,因为一直有点不明白为什么cubemap可以实现全景图的效果,原来是因为实现了球面到立方体的映射。如下图,

至于这样的图片是怎么做出来,就涉及到图像处理以及算法的问题了,暂时不在本文的考虑范围内。下图是一张完整的cubemap图。

最后一个问题,相机角度的移动明确地说是相机聚焦点的移动。此时,我们可以把720度的场景抽象出一个球。由于相机的焦距是确定的,只要确定相机的角度也就确定了相机的聚焦点。只要确定经度纬度即可确定角度,然后通过一些数学计算算出最后的聚焦点位置。而场景的运动是由相机视角的变动而相对运动起来的。所以当鼠标在横向移动时,经度跟着移动;而在鼠标纵向移动时,纬度进行变化。直角坐标与球坐标的转化关系为(以左手坐标系为例):

  x = r*sin( theta  ) *cos( phi);z = r*cos( theta );y = r*sin( theta ) * sin( phi );

全景虚拟漫游实现

结合上述的分析,以下代码是官网的代码,加上了注释。

 <script src="js/three.js"></script><script src="js/renderers/CSS3DRenderer.js"></script><script>//定义相机,场景,渲染器,是3D场景形成的三大要素var camera, scene, renderer;//定义几何体,材质,以及几何体加材质之后形成的网格var geometry, material, mesh;//生成三维向量(0,0,0),相机的目标点var target = new THREE.Vector3();//lon 经度 竖着的 有东经 西经 ;lat 维度 横着的 有南纬 北纬//该经纬表示相机的聚焦点,初始状态在前面var lon = 90, lat = 0;//同样是相机的聚焦点,上面是角度,此处转化为弧度制var phi = 0, theta = 0;//移动端用户输入的x,y var touchX, touchY;init();animate();function init() {//相机的默认位置在坐标系的原点camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );scene = new THREE.Scene();//右手坐标系,z朝向观察者,即相机。下面是将六个面拼接成立方体,分别对应var sides = [{url: 'res/Bridge2/posx.jpg', //左侧position: [ -512, 0, 0 ],rotation: [ 0, Math.PI / 2, 0 ]},{url: 'res/Bridge2/negx.jpg', //右侧position: [ 512, 0, 0 ],rotation: [ 0, -Math.PI / 2, 0 ]},{url: 'res/Bridge2/posy.jpg', //上侧position: [ 0,  512, 0 ],rotation: [ Math.PI / 2, 0, Math.PI  ]},{url: 'res/Bridge2/negy.jpg', //下侧position: [ 0, -512, 0 ],rotation: [ -Math.PI / 2, 0, Math.PI ]},{url: 'res/Bridge2/posz.jpg', //前position: [ 0, 0,  512 ],rotation: [ 0, Math.PI, 0 ]},{url: 'res/Bridge2/negz.jpg', //后position: [ 0, 0, -512 ],rotation: [ 0, 0, 0 ]}];//将六个图片添加到场景中for ( var i = 0; i < sides.length; i ++ ) {var side = sides[ i ];var element = document.createElement( 'img' );element.width = 1026; // 2 pixels extra to close the gap.element.src = side.url;//CSS3DObject 是拓展出去的方法,原型是object3D,见CSS3DRenderer.jsvar object = new THREE.CSS3DObject( element );object.position.fromArray( side.position );object.rotation.fromArray( side.rotation );scene.add( object );}//渲染器也是拓展出来的方法,见CSS3DRenderer.jsrenderer = new THREE.CSS3DRenderer();renderer.setSize( window.innerWidth, window.innerHeight );document.body.appendChild( renderer.domElement );//添加鼠标,手势,窗口事件document.addEventListener( 'mousedown', onDocumentMouseDown, false );document.addEventListener( 'wheel', onDocumentMouseWheel, false );document.addEventListener( 'touchstart', onDocumentTouchStart, false );document.addEventListener( 'touchmove', onDocumentTouchMove, false );window.addEventListener( 'resize', onWindowResize, false );}function onWindowResize() {//窗口缩放的时候,保证场景也跟随着一起缩放camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize( window.innerWidth, window.innerHeight );}function onDocumentMouseDown( event ) {event.preventDefault();//保证监听拖拽事件document.addEventListener( 'mousemove', onDocumentMouseMove, false );document.addEventListener( 'mouseup', onDocumentMouseUp, false );}function onDocumentMouseMove( event ) {//鼠标的移动距离  currentEvent.movementX = currentEvent.screenX - previousEvent.screenXvar movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;lon -= movementX * 0.1;lat += movementY * 0.1;}function onDocumentMouseUp( event ) {//保证监听拖拽事件document.removeEventListener( 'mousemove', onDocumentMouseMove );document.removeEventListener( 'mouseup', onDocumentMouseUp );}function onDocumentMouseWheel( event ) {//相机的视觉随着鼠标滚动的距离拉进或者远离camera.fov += event.deltaY * 0.05;camera.updateProjectionMatrix();}function onDocumentTouchStart( event ) {event.preventDefault();//移动端没有movement,所以直接用touchX touchY去计算移动的距离var touch = event.touches[ 0 ];touchX = touch.screenX;touchY = touch.screenY;}function onDocumentTouchMove( event ) {event.preventDefault();var touch = event.touches[ 0 ];lon -= ( touch.screenX - touchX ) * 0.1;lat += ( touch.screenY - touchY ) * 0.1;touchX = touch.screenX;touchY = touch.screenY;}//开启动画function animate() {requestAnimationFrame( animate );lon +=  0.1;lat = Math.max( - 85, Math.min( 85, lat ) );phi = THREE.Math.degToRad( 90 - lat ); //角度转为弧度制theta = THREE.Math.degToRad( lon );//在球坐标系中算出相机的聚焦点的坐标target.x = Math.sin( phi ) * Math.cos( theta );target.y = Math.cos( phi );target.z = Math.sin( phi ) * Math.sin( theta );camera.lookAt( target );renderer.render( scene, camera );}

参考文献:
https://threejs.org/ ThreeJS官网
https://isux.tencent.com/3d.html 打造H5里的“3D全景漫游”秘籍
http://www.hewebgl.com/article/getarticle/27 WebGL中文网ThreeJS教程
https://zh.wikipedia.org/wiki/%E7%90%83%E5%BA%A7%E6%A8%99%E7%B3%BB 球坐标系
http://stackoverflow.com/questions/29678510/convert-21-equirectangular-panorama-to-cube-map

全景虚拟漫游实现(three.js)相关推荐

  1. 全景虚拟漫游技术实现(three.js vs ThingJS) Javascript 3D开发 前端 物联网 webgl 三维建模 3D模型 虚拟 全景

    三维建模无非就是通过专业技能加工成立体图形,使之图形成为直观.易懂,容易判读的立体图件.对于开发者来说,选择一个好的3D开发框架,在全景虚拟漫游场景上实现3D动效,ThingJS vs three.j ...

  2. 360度全景虚拟漫游导览在地产景区的应用优势

    除了景区景点外,古典园林因乡村振兴.城镇化发展得到市场关注和加速发展.特别是具有古风古韵的古典园林也成为大众散心的新去向.古典园林也与时俱进深度融合了VR全景展示技术,打造线上古典园林720全景漫游虚 ...

  3. 全景效果实现(Photo Sphere,photo-sphere-viewer全景虚拟漫游)

    简述:全景效果我们都知道,常用于虚拟场景体验,具体怎么实现,这里给大家推荐一款组件,便于实现全景的真实效果,Photo Sphere: 使用详细: 1.下载依赖: npm install photo- ...

  4. Krpano(虚拟漫游)

    Krpano --> 虚拟漫游 虚拟漫游是什么? www.5uvr.com 看一下官网里面的示例 简单来说,虚拟漫游就是由多个vr全景和多媒体技术组成的一个整合体 Krpano就是一门讲VR全景 ...

  5. 山西数字博物馆vr全景虚拟制作

    博物馆是一座典藏国家文化文物瑰宝的科普性的"学校",由于博物馆在建造.保护及运维等方面受物理条件限制大,特别是限流及关闭政策出现后,未能发挥到预期的社会教育作用,整合了多种新兴技术 ...

  6. Unity3D制作3D虚拟漫游场景(二)

    传送门: Unity3D制作3D虚拟漫游场景(一) -------------------------------------------------------------------------- ...

  7. 虚幻4 虚拟漫游场景 制作过程

    先看看渲染效果. 虚幻场景中的物品其实也需要全部由3Dmax制作好导入 虚幻由于有一部分是动态光影,还需要预渲染,所以模型的面数要尽可能降低. 原来3000多个面降到41个面. 3Dmax自身渲染的话 ...

  8. 暴风魔镜之虚拟漫游(更新中。。。)

    暴风魔镜,是暴风影音正式发布的一款硬件产品,是一款VR头显(虚拟现实头戴式显示设备),在使用时需要配合暴风影音开发的专属魔镜应用,在手机上实现IMAX效果,普通的电影即可实现影院观影效果.2014年9 ...

  9. 软件园三区VR虚拟漫游实训项目规划

    上周由于清明节假期,没有做好博客的更新工作,在这里总结工作,以及项目开发的规划. 游戏是另一个世界,VR则是其中一个虫洞.但没有人能够穿过幽深晦暗的隧道洞见尽头的风景,也没有人能在虫洞的质量面前保证自 ...

最新文章

  1. 【cocos2d-js官方文档】九、cc.loader
  2. 为什么使用LM386可以直接收听调频电台节目?
  3. python变量类型-Python 变量类型详解
  4. linux 防火墙开机启动项,Ubuntu 9.10下实现Firestarter网络防火墙自启动
  5. 一个项目搞定支付宝,微信支付!
  6. php怎么删除表数据,php怎样删除数据表中的数据_后端开发
  7. 鸿蒙系统大疆,华为操作系统“鸿蒙OS”来了!
  8. java泛型程序设计——泛型类的静态上下文中类型变量无效+不能抛出或捕获泛型类的实例
  9. vs2013和vs2010的配置
  10. [精华] 讨论 Setsockopt选项
  11. 凸优化中:单纯形是一种多面体的证明
  12. Windows10自带应用的卸载和恢复
  13. entity framework 新手入门篇(2)-entity framework基本的增删改查
  14. 移动通信网络的构成思维导图
  15. 【家具CRM客户关系管理系统案例】数夫助力左右家私CRM客户关系管理系统正式上线
  16. ESP8266深度睡眠计时器唤醒
  17. 各种区块链浏览器的地址总汇
  18. 使用maven-sql-plugin实现持续数据库集成(CDBI) [ 光影人像 东海陈光剑 的博客 ]
  19. JS手写上传文件、React手写上传文件
  20. 洛谷P1489 猫狗大战

热门文章

  1. java中的四种代码块
  2. puppy linux中文设置,使用puppyLinux心得
  3. (转)活灵活现用Git--基础篇
  4. V4L2_Utils目标平台编译
  5. Python 函数 | sorted 函数详解
  6. Remote Development Tips and Tricks
  7. 人脸识别之人脸验证(二)--DeepID
  8. CLH、MCS锁的原理及实现
  9. python高级编程
  10. 讲个鬼故事:小灰的体检报告出来了。。。