基于three.js的全景图播放器

之前我有一个项目就是制作一个全景图展示的页面,我们使用的是中国一位大神的开源项目,这个高手真的是有一点厉害。人家不仅仅完整开发了一套几乎可以直接商用的工具,全部基于three.js而且没有使用任何插件什么的,连很多图片都base64编辑代码里面了。总之他这个开源项目是我比较服的地址是:https://github.com/pchen66/panolens.js.git,大家可以看看。

虽然是NB但是他所维护的这个项目其实已经经历了多个大版本更新了,代码到现在已经有点重型了,而且有一个诡异无法解决的问题,那就是微信上面加载多张图片的时候很容易黑屏,而且当时我根本就不知道怎么解决。

所幸three.js我还是会一点点,大神的代码如果我直接拿来修改的话是非常浪费时间的,而且说实话走读他的代码需要你慢慢来不是什么半个小时就可以搞定的东西。它的逻辑是将一张2:1的全景图切成六个面然后放到一个box的六面上面去,问题是这个切法是靠UV计算来实现的如果着色器初始化一旦出现问题那么就黑屏无法显示,这也就是为什么微信上面会黑掉。由于我之前尝试做过很多较大的webgl场景在微信上面分享使用所以确定问题不出在微信与three就是这些高级着色器破坏了微信浏览器的渲染逻辑(虽然微信这个东西也不是什么好定西)。

又没有那么多时间走读、修改他的代码那就只有我自己写写了,所幸我需要的功能仅仅是全景图播放器而已所以我便自己写了。目前实现了的功能有对象化管理场景、添加多张全景图、全景图之间的导航(无需刷新)、全景图上的标记点(html内容)、手机陀螺仪跟踪摄像机。代码就是下面这些。

/**
 * xunshi_panolens.js
 * @author 唐玥璨
 * @namespace XUNSHI
 */

var XUNSHI = {VERSION:'0.1'};
/**
 * 信息点的初始化
 * @property {object} html - 图标的html对象,可以是任何的html标签,通过js元素写法得到的html如getElementbyID这种,同时所有的html元素必须是绝对定位
 * @property {String} position - 图标的位置,这个位置是一个坐标格式为json,例:{x:165.161,y:15.16156,z:4565.156}
 */
XUNSHI.InfoSpot = function(html,position)
{
    var point = new THREE.Vector3(position.x,position.y,position.z);
    this.htmlelement = html;
    this.position = point;
    return this;
}
/**
 * 全景图的初始化
 * @property {String} id - 全局图的id,必须全局唯一的大小
 * @property {String} img - 图片的路径
 */
XUNSHI.Panolen = function(id,img)
{
    this.img = img;
    this.id = id;
    return this;
}
/**
 * 陀螺仪控制器的初始化
 * @property {object} camera - 相机的对象
 * @property {object} domElement - 原生js的html对象
 */
XUNSHI.DeviceOrientationControls = function( camera, domElement ) {
  var scope = this;
  var changeEvent = { type: 'change' };
  var rotY = 0;
  var rotX = 0;
  var tempX = 0;
  var tempY = 0;
  this.camera = camera;
  this.camera.rotation.reorder( "YXZ" );
  this.domElement = ( domElement !== undefined ) ? domElement : document;
  this.enabled = true;
  this.deviceOrientation = {};
  this.screenOrientation = 0;
  this.alpha = 0;
  this.alphaOffsetAngle = 0;
  //绑定陀螺仪数据变动事件
  var onDeviceOrientationChangeEvent = function( event ) {
    scope.deviceOrientation = event;
  };
  //绑定屏幕空间变动事件
  var onScreenOrientationChangeEvent = function() {
    scope.screenOrientation = window.orientation || 0;
  };
  //陀螺仪操作同时支持触控,触控开始绑定
  var onTouchStartEvent = function (event) {
    event.preventDefault();
    event.stopPropagation();
    tempX = event.touches[ 0 ].pageX;
    tempY = event.touches[ 0 ].pageY;

  };
  //触控移动绑定
  var onTouchMoveEvent = function (event) {
    event.preventDefault();
    event.stopPropagation();
    rotY += THREE.Math.degToRad( ( event.touches[ 0 ].pageX - tempX ) / 4 );
    rotX += THREE.Math.degToRad( ( tempY - event.touches[ 0 ].pageY ) / 4 );
    scope.updateAlphaOffsetAngle( rotY );
    tempX = event.touches[ 0 ].pageX;
    tempY = event.touches[ 0 ].pageY;

  };
  //设置相机的旋转坐标,这个就是欧拉角
  var setCameraQuaternion = function( quaternion, alpha, beta, gamma, orient ) {
    var zee = new THREE.Vector3( 0, 0, 1 );
    var euler = new THREE.Euler();
    var q0 = new THREE.Quaternion();
    var q1 = new THREE.Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis
    var vectorFingerY;
    var fingerQY = new THREE.Quaternion();
    var fingerQX = new THREE.Quaternion();
    if ( scope.screenOrientation == 0 ) {
      vectorFingerY = new THREE.Vector3( 1, 0, 0 );
      fingerQY.setFromAxisAngle( vectorFingerY, -rotX );
    } else if ( scope.screenOrientation == 180 ) {
      vectorFingerY = new THREE.Vector3( 1, 0, 0 );
      fingerQY.setFromAxisAngle( vectorFingerY, rotX );
    } else if ( scope.screenOrientation == 90 ) {
      vectorFingerY = new THREE.Vector3( 0, 1, 0 );
      fingerQY.setFromAxisAngle( vectorFingerY, rotX );
    } else if ( scope.screenOrientation == - 90) {
      vectorFingerY = new THREE.Vector3( 0, 1, 0 );
      fingerQY.setFromAxisAngle( vectorFingerY, -rotX );
    }
    q1.multiply( fingerQY );
    q1.multiply( fingerQX );
    euler.set( beta, alpha, - gamma, 'YXZ' ); // 手机获取的坐标是'ZXY'将其设置为'YXZ'给three用
    quaternion.setFromEuler( euler ); //拿到手机的方向
    quaternion.multiply( q1 ); //拿到对应摄像机的方向
    quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); //调整屏幕的方向
  };

  this.connect = function() {
    onScreenOrientationChangeEvent();
    window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
    window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
    window.addEventListener( 'deviceorientation', this.update.bind( this ), false );
    scope.domElement.addEventListener( "touchstart", onTouchStartEvent, false );
    scope.domElement.addEventListener( "touchmove", onTouchMoveEvent, false );
    scope.enabled = true;

  };

  this.disconnect = function() {
    window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
    window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
    window.removeEventListener( 'deviceorientation', this.update.bind( this ), false );
    scope.domElement.removeEventListener( "touchstart", onTouchStartEvent, false );
    scope.domElement.removeEventListener( "touchmove", onTouchMoveEvent, false );
    scope.enabled = false;
  };

  this.update = function( ignoreUpdate ) {
    if ( scope.enabled === false ) return;
    var alpha = scope.deviceOrientation.alpha ? THREE.Math.degToRad( scope.deviceOrientation.alpha ) + this.alphaOffsetAngle : 0; // Z方向的数据
    var beta = scope.deviceOrientation.beta ? THREE.Math.degToRad( scope.deviceOrientation.beta ) : 0; // X方向的数据
    var gamma = scope.deviceOrientation.gamma ? THREE.Math.degToRad( scope.deviceOrientation.gamma ) : 0; // Y方向的数据
    var orient = scope.screenOrientation ? THREE.Math.degToRad( scope.screenOrientation ) : 0; //矩阵里面的第三个
    setCameraQuaternion( scope.camera.quaternion, alpha, beta, gamma, orient );
    this.alpha = alpha;
  };

  this.updateAlphaOffsetAngle = function( angle ) {
    this.alphaOffsetAngle = angle;
    this.update();
  };

  this.dispose = function() {
    this.disconnect();
  };

};

/**
 * 全局初始化
 * @type {object}
 * @property {HTMLElement} options.container - html元素,这个就是主要的容器元素了,它必须是overflow:hidden
 * @property {Number} options.fov - 相机的焦距
 * @property {boolean} options.debug - 是否开启调试信息,开启对应实现点击console坐标点,关闭则不打印
 * @property {boolean} options.zoomspeed - 缩放的速度,默认为1
 */
XUNSHI.View = function(option)
{
    //配置信息的录入
    this.options = option;
    this.zoomspeed = 1;
    if(this.options.zoomspeed!=null)
    {
        this.zoomspeed = this.options.zoomspeed;
    }
    //创建导入器
    this.loader = new  THREE.TextureLoader()
    //初始化场景
    this.scene = new THREE.Scene();
    this.geometry = new THREE.SphereBufferGeometry( 500, 320, 320 );
    this.material = new THREE.MeshBasicMaterial();
    this.material.side = THREE.DoubleSide;
    this.sphere = new THREE.Mesh( this.geometry, this.material );
    this.panolens = [];
    this.idnow = null;
    this.scene.add( this.sphere );
    //初始化相机
    this.camera = new THREE.PerspectiveCamera(this.options.fov, this.options.container.clientWidth / this.options.container.clientHeight, 0.1, 1000);
    this.camera.position.x = 0;
    this.camera.position.y = 0;
    this.camera.position.z = 0;
    this.camera.lookAt({x:0,y:0,z:0});
    //初始化渲染器
    this.renderer = new THREE.WebGLRenderer( { antialias: true ,precision: "lowp"} );
  this.renderer.setPixelRatio( window.devicePixelRatio );
  this.renderer.setSize( this.options.container.clientWidth, this.options.container.clientHeight );
    this.renderer.setClearColor( 0x1b1b1b, 1 );
    this.renderer.gammaInput = true;
  this.renderer.gammaOutput = true;
    this.options.container.appendChild( this.renderer.domElement );
    //初始化控制器
    this.control = new THREE.OrbitControls(this.camera);
    this.control.panningMode = THREE.HorizontalPanning;
    this.control.minDistance = 0.0001;
    this.control.maxDistance = 0.0001;
    this.control.rotateSpeed = -0.3;

    //是否开启陀螺仪
    this.BeEnable = false;

    this.DeviceController = new XUNSHI.DeviceOrientationControls(this.camera,this.container);

    //初始化一个射线扫描器
    this.raycaster = new THREE.Raycaster();

    //缩放函数,一个是缩一个是放
    this.zoomin = function()
    {
        if(that.camera.fov>60)
        {
         that.camera.fov = that.camera.fov - (2*that.zoomspeed);
        }
    }
    this.zoomout = function()
    {
        if(that.camera.fov<120)
        {
         that.camera.fov = that.camera.fov + (2*that.zoomspeed);
        }
    }
    var that = this;
    //监听鼠标滚轮实现缩放
    this.options.container.addEventListener('mousewheel',function(e){
        if(e.wheelDelta>0){
            that.zoomin();
        }
        else{
            that.zoomout();
        }
    })
    //监听点击事件实现对点击点坐标的输出
    this.options.container.addEventListener('click',function(e){
        if(that.options.debug){
            var mouse = new THREE.Vector2();
            mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
            mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
            that.raycaster.setFromCamera( mouse, that.camera );
            var intersects = that.raycaster.intersectObjects( [that.sphere] );
            if(intersects.length>0){
                console.log(intersects[0].point);
            }
        }
    })

    this.TouchZoom = function(event){
        console.log('xxxx')
    }

    //开启陀螺仪
    this.EnableGyroscope = function(){
        this.BeEnable = true;
        this.DeviceController.connect();
    }
    //关闭陀螺仪
    this.DisableGyroscope = function(){
        this.BeEnable = false;
        this.DeviceController.disconnect();
    }
    var change = 0;
    this.options.container.addEventListener( 'touchstart', function(){change = 0;}, false );
  this.options.container.addEventListener( 'touchmove', onTouchMove, false );
  this.options.container.addEventListener( 'touchend', function(){change = 0;}, false );
  function onTouchMove(event)
  {
    if(event.changedTouches.length > 1)
    {
      if(change == 0)
      {
        var a = new THREE.Vector2( event.changedTouches[0].clientX, event.changedTouches[0].clientY );
        var b = new THREE.Vector2( event.changedTouches[1].clientX, event.changedTouches[1].clientY );
        change = a.distanceTo(b);
      }
      else
      {
        var a = new THREE.Vector2( event.changedTouches[0].clientX, event.changedTouches[0].clientY );
        var b = new THREE.Vector2( event.changedTouches[1].clientX, event.changedTouches[1].clientY );
        var distance = change - a.distanceTo(b);
        if ( distance > 0 ) {
          that.camera.fov = ( that.camera.fov < 120 ) 
            ? that.camera.fov + 2*that.zoomspeed
            : 120;
                        that.camera.updateProjectionMatrix();
        } else if ( distance < 0 ) {
          that.camera.fov = ( that.camera.fov > 60 ) 
            ? that.camera.fov - 2*that.zoomspeed
            : 60;
                        that.camera.updateProjectionMatrix();
        }
        that.control.update();
        change = a.distanceTo(b);
      }
    }
    }
    
    this.projector = new THREE.Projector();
    
    //渲染tick
    this.Render = function()
    {
        if(that.InfoSpots.length>0){
            for(var i=0;i<that.InfoSpots.length;i++){
                var pos = that.InfoSpots[i].position.clone();
                var vector2d = pos.project(that.camera)
                var halfWidth = window.innerWidth / 2;
                var halfHeight = window.innerHeight / 2;
                that.InfoSpots[i].htmlelement.style.top = -vector2d.y * halfHeight + halfHeight + 'px';
                that.InfoSpots[i].htmlelement.style.left = vector2d.x * halfWidth + halfWidth + 'px';
            }
        }
        that.camera.updateProjectionMatrix();
        if(!that.BeEnable){
            that.control.update();
        }
        requestAnimationFrame(that.Render);
        that.renderer.render(that.scene, that.camera);
    }
    //开始渲染
    this.StartRender = function()
    {
        if(that.panolens.length>0){
            that.loader.load(that.panolens[0].img,function(data){
                that.material.map = data
                that.idnow = that.panolens[0].id;
                that.material.needsUpdate = true;
                that.material.map.needsUpdate = true;
            })
        }else
        {
            console.error("全景图数组不能为空")
            return;
        }
        that.Render()
    }
    //显示某一张全景图
    this.ShowPanolen = function(id){
        if(id==that.idnow)
        {
            return;
        }
        for(var i=0;i<that.panolens.length;i++)
        {
            if(that.panolens[i].id==id)
            {
                that.loader.load(that.panolens[i].img,function(data){
                    that.material.map = data;
                    that.idnow = id;
                    that.material.needsUpdate = true;
                })
            }
        }
    }
    //窗口大小重置
    this.onWindowResize = function()
    {
        that.camera.aspect = that.options.container.clientWidth / that.options.container.clientHeight;
        that.camera.updateProjectionMatrix();
        that.renderer.setPixelRatio(window.devicePixelRatio);
        that.renderer.setSize(that.options.container.clientWidth, that.options.container.clientHeight);
    }
    //获取全局的窗口缩放事件
    window.onresize = function(){
        that.onWindowResize()
    };
    //添加全景图对象
    this.add_panolens = function(panolen){
        this.panolens.push(panolen)
    }
    //所有的信息点,这个东西现在不由全景图管理,全部用视图去管理,因为就是html元素所有没有什么问题,也比较方便
    this.InfoSpots = [];
    //添加信息点
    this.AddInfoSpot = function(infospot){
        this.InfoSpots.push(infospot);
    }
    return this;
}

我将这个代码放到git上面了:
https://github.com/tan9710630/xunshi_panolens.git ,如果大家有空可以来一起维护,毕竟这个东西目前只是一个雏形,到真正成为可以使用SDK还有一段路要走。

留下回复