[关闭]
@gnat-xj 2017-04-07T02:48:55.000000Z 字数 7842 阅读 2474

全景程序 Demo


阅读列表


全景漫游技术是一种基于图像渲染(Image-Based
Rendering)的虚拟现实技术,是一个非常充满活力、具有很大发展潜力的实用技术。将某视点全方位的多幅真实图像映射到立方体、圆柱体和球体等几何体表面,从而创建的逼真虚拟场景。
相比传统的基于三维几何建模的虚拟场景,它的数据采集、处理、传输成本低,渲染效果好,沉浸感强;又因其场景的绘制独立于场景的复杂性,在生成复杂场景时,全景技术在真实感方面和制作成本及周期方面都有着无可比拟的优势。相比互联网上单调的交互性差的二维图片,它能提供更好沉浸感和交互性。简而言之,全景漫游系统是一个性价比极高的漫游系统解决方案。

全景漫游 所介绍的,本 Demo 用了 Three.js (WebGL 库)。

JavaScript 库依赖

场景、相机

  1. {camera},
  2. \
  3. => scene => renderer => DOM 元素
  4. /
  5. {objects (mesh = geometry + material)}

基于 three.js / examples #webgl_panorama_equirectangular

全景漫游主体代码如下:

  1. // camera 是相机,好比人眼,scene 是场景,包含了用于漫游的全景球
  2. var camera, scene, renderer;
  3. var panoramamaterial;
  4. var isUserInteracting = false,
  5. onMouseDownMouseX = 0, onMouseDownMouseY = 0,
  6. lon = 0, onMouseDownLon = 0,
  7. lat = 0, onMouseDownLat = 0,
  8. phi = 0, theta = 0;
  9. var needUpdateLonLat = false;
  10. // 由于移动端无法方便地打开 console 看打印信息,我们只能输出到屏幕上
  11. var log = document.getElementById( 'loginfo' );
  12. var touchmoveX = 0, touchmoveY = 0, touchmoveId = 0;
  13. init();
  14. animate();

初始化 camera 和 scene:

  1. function init() {
  2. var container, mesh;
  3. // 用于显示全景的 DOM 元素
  4. container = document.getElementById( 'container' );
  5. // 透视投影相机
  6. camera = new THREE.PerspectiveCamera(
  7. 75, // fov
  8. window.innerWidth / window.innerHeight, // aspect ratio
  9. 1, // near
  10. 1100 // far
  11. );
  12. camera.target = new THREE.Vector3( 0, 0, 0 );
  13. scene = new THREE.Scene();
  14. // 全景球
  15. var geometry = new THREE.SphereGeometry( 500, 60, 40 );
  16. geometry.scale( - 1, 1, 1 );
  17. // 全景纹理
  18. var material = new THREE.MeshBasicMaterial( {
  19. map: new THREE.TextureLoader().load( textureURL )
  20. } );
  21. panoramamaterial = material;
  22. // 把全景纹理贴到全景球上,作为我们的观察球
  23. mesh = new THREE.Mesh( geometry, material );
  24. // 添加到场景中
  25. scene.add( mesh );
  26. // 渲染器渲染场景,并通过相机观察这个场景,呈现在 DOM 元素上
  27. renderer = new THREE.WebGLRenderer();
  28. renderer.setPixelRatio( window.devicePixelRatio );
  29. renderer.setSize( window.innerWidth, window.innerHeight );
  30. container.appendChild( renderer.domElement );
  31. // 事件绑定
  32. ...
  33. }

渲染循环:

  1. function animate() {
  2. // 规范化 lon、lat
  3. if (lon > 180) {
  4. lon -= 360;
  5. } else if (lon < -180) {
  6. lon += 360;
  7. }
  8. lat = Math.max( - 85, Math.min( 85, lat ) );
  9. // 更新页面上的 debug 信息
  10. if (needUpdateLonLat) {
  11. log.innerHTML = JSON.stringify({
  12. "lon": lon,
  13. "lat": lat
  14. }, null, 4);
  15. needUpdateLonLat = false;
  16. }
  17. // 计算 camera 朝向
  18. phi = THREE.Math.degToRad( 90 - lat );
  19. theta = THREE.Math.degToRad( lon );
  20. camera.target.x = 500 * Math.sin( phi ) * Math.cos( theta );
  21. camera.target.y = 500 * Math.cos( phi );
  22. camera.target.z = 500 * Math.sin( phi ) * Math.sin( theta );
  23. camera.lookAt( camera.target );
  24. // renderer 拿到 scene 这个场景和 camera 这个相机,
  25. // 从 camera 的视角渲染更新 DOM 元素
  26. renderer.render( scene, camera );
  27. // 请求下一次渲染
  28. requestAnimationFrame( animate );
  29. }

From MDN:

setInterval:

Calls a function or executes a code snippet repeatedly, with a fixed time delay between each call to that function.

requestAnimationFrame:

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes as an argument a callback to be invoked before the repaint.

Use the latter for animation and the former any time you want to call a function at a specified interval.

11.2.1.c. 交互:鼠标、触屏、重力感应

所有的交互都通过事件监听完成。

PC 端的鼠标交互:

  1. // 单击鼠标开始拖拽页面
  2. container.addEventListener( 'mousedown', onDocumentMouseDown, false );
  3. // 按住鼠标可以拖拽
  4. container.addEventListener( 'mousemove', onDocumentMouseMove, false );
  5. // 释放鼠标结束拖拽页面
  6. container.addEventListener( 'mouseup', onDocumentMouseUp, false );
  7. function onDocumentMouseDown( event ) {
  8. // 只有 canvas 才接受响应
  9. if (!$(event.target).is('canvas')) { return; }
  10. event.preventDefault();
  11. // 设置标志 isUserInteracting 为真
  12. isUserInteracting = true;
  13. // 记录下 DOM 元素的坐标,以及当前视场中心的经纬度
  14. onPointerDownPointerX = event.clientX;
  15. onPointerDownPointerY = event.clientY;
  16. onPointerDownLon = lon;
  17. onPointerDownLat = lat;
  18. }
  19. function onDocumentMouseMove( event ) {
  20. // 只有标志 isUserInteracting 为 true,才更新 lon 和 lat
  21. // lon、lat 会影响 camera 的朝向,这样就更新了视线方向
  22. if ( isUserInteracting === true ) {
  23. lon=(onPointerDownPointerX-event.clientX)*0.1+onPointerDownLon;
  24. lat=(event.clientY-onPointerDownPointerY)*0.1+onPointerDownLat;
  25. needUpdateLonLat = true;
  26. }
  27. }
  28. function onDocumentMouseUp( event ) {
  29. // 鼠标释放,标志位清为 false
  30. isUserInteracting = false;
  31. }

用鼠标滚轮改变 fov 的大小:

  1. container.addEventListener( 'wheel', onDocumentMouseWheel, false );
  2. function onDocumentMouseWheel( event ) {
  3. camera.fov += event.deltaY * 0.05; // 更新 fov 的值
  4. camera.updateProjectionMatrix(); // 使生效
  5. }

在移动端上,没有“mousedown”、 “mousemove”和 “mouseup”等事件。
需要通过 touch event(触屏事件)
来交互,下面的代码仅在 iPad 上 safari 浏览器中测试过:

  1. // touchstart 和 touchend 不作处理
  2. container.addEventListener('touchstart',function(e) { }, false);
  3. container.addEventListener('touchend',function(e) { }, false);
  4. // touchmove 中更新 lon、lat(在 iOS,safari 上测试可用)
  5. container.addEventListener('touchmove',function(e) {
  6. // 更新 debug 信息
  7. log.innerText = JSON.stringify(e.touches[0], null, 4);
  8. if (e.touches[0] && touchmoveId === e.touches[0].identifier) {
  9. var dx = e.touches[0].clientX - touchmoveX;
  10. var dy = e.touches[0].clientY - touchmoveY;
  11. lon += -dx*0.1;
  12. lat += dy*0.1;
  13. } else {
  14. touchmoveId = e.touches[0].identifier;
  15. }
  16. touchmoveX = e.touches[0].clientX;
  17. touchmoveY = e.touches[0].clientY;
  18. }, false);

通过拖放图片来加载新的全景图:

  1. document.addEventListener( 'dragover', function ( event ) {
  2. event.preventDefault();
  3. event.dataTransfer.dropEffect = 'copy';
  4. }, false );
  5. document.addEventListener( 'dragenter', function ( event ) {
  6. document.body.style.opacity = 0.5;
  7. }, false );
  8. document.addEventListener( 'dragleave', function ( event ) {
  9. document.body.style.opacity = 1;
  10. }, false );
  11. document.addEventListener( 'drop', function ( event ) {
  12. event.preventDefault();
  13. var reader = new FileReader();
  14. reader.addEventListener( 'load', function ( event ) {
  15. material.map.image.src = event.target.result;
  16. material.map.needsUpdate = true;
  17. }, false );
  18. reader.readAsDataURL( event.dataTransfer.files[ 0 ] );
  19. document.body.style.opacity = 1;
  20. }, false );

窗口改变大小后,自动更新 aspect 比例(移动端屏幕旋转后可自适应):

  1. window.addEventListener( 'resize', onWindowResize, false );
  2. function onWindowResize() {
  3. // 重新设置 aspect 比例
  4. camera.aspect = window.innerWidth / window.innerHeight;
  5. // 生效
  6. camera.updateProjectionMatrix();
  7. // 调整渲染得到的 DOM 元素
  8. renderer.setSize( window.innerWidth, window.innerHeight );
  9. }

针对不同移动端的 touch event 的效果不一致的问题,我们使用陀螺仪来交互:

  1. // 仅当 PC 以及没有陀螺仪的移动设备,设置 usegyro 为 false
  2. var usegyro = true;
  3. // 默认关闭绝对定位
  4. var gyroabsolute = false;
  5. var gn = new GyroNorm();
  6. var gyro = {
  7. "x": 0.0,
  8. "y": 0.0,
  9. "x0": 0.0,
  10. "y0": 0.0,
  11. "inited": false
  12. };
  13. gn.init().then(function(){
  14. gn.start(function(data){
  15. if (!usegyro) { return; }
  16. log.innerHTML = JSON.stringify(data, null, 4);
  17. if (gyroabsolute) {
  18. // 绝对定位,直接取 alpha、beta 值
  19. lon = 180-data.do.alpha;
  20. lat = data.do.beta - 90;
  21. log.innerHTML += "\n(绝对)\n";
  22. } else {
  23. // 相对定位,根据差量“调整” lon、lat
  24. log.innerHTML += "\n(相对)\n";
  25. if (gyro.inited) {
  26. if (Math.abs(data.do.gamma - gyro.x0) > 5) {
  27. lon += (data.do.gamma < gyro.x0)? 1.0 : -1.0;
  28. }
  29. if (Math.abs(data.do.beta - gyro.y0) > 5) {
  30. lat += (data.do.beta > gyro.y0)? 1.0 : -1.0;
  31. }
  32. } else {
  33. gyro.x0 = data.do.gamma;
  34. gyro.y0 = data.do.beta;
  35. gyro.inited = true;
  36. }
  37. }
  38. });
  39. }).catch(function(e){
  40. // Catch if the DeviceOrientation or DeviceMotion is not supported
  41. // by the browser or device
  42. $("#usegyro").attr("disabled", true);
  43. $("#usegyro").attr("checked", false);
  44. usegyro = false;
  45. $("#gyroabsolute").attr("disabled", true);
  46. $("#gyroabsolute").attr("checked", false);
  47. gyroabsolute = false;
  48. });

上面的 data 是陀螺仪传感器的输出数据,内容如下:

  1. // 当前方向
  2. // data.do.alpha ( deviceorientation event alpha value )
  3. // data.do.beta ( deviceorientation event beta value )
  4. // data.do.gamma ( deviceorientation event gamma value )
  5. // data.do.absolute ( deviceorientation event absolute value )
  6. // 三个方向的加速度
  7. // data.dm.x ( devicemotion event acceleration x value )
  8. // data.dm.y ( devicemotion event acceleration y value )
  9. // data.dm.z ( devicemotion event acceleration z value )
  10. // 考虑了重力的加速度
  11. // data.dm.gx ( devicemotion event accelerationIncludingGravity x value )
  12. // data.dm.gy ( devicemotion event accelerationIncludingGravity y value )
  13. // data.dm.gz ( devicemotion event accelerationIncludingGravity z value )
  14. // 角速度
  15. // data.dm.alpha ( devicemotion event rotationRate alpha value )
  16. // data.dm.beta ( devicemotion event rotationRate beta value )
  17. // data.dm.gamma ( devicemotion event rotationRate gamma value )

智能手机的 x-y-z 坐标系

绕着 z、y、x 轴正方向旋转角度分别为 alpha、beta 和 gamma。
详细见 DeviceOrientation Event Specification

这三个方向符合预期。

可以借鉴的库:parallax.js

全景漫游的完整代码可以在 district10/poicreator: Create POIs 查看。

在线:Panorama


Three.js 文档:three.js - documentation - Reference - TextureLoader

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注