三種前端實現VR全景看房的方案!說不定哪天就用得上!

前言

事情是這樣的,前幾天我接到一個外包工頭的新需求,某品牌要搭建一個在線VR展廳,用戶能夠在手機上經過陀螺儀或者拖動來360度全景參觀展廳,這個VR展廳裏會有一些信息點,點擊以後能夠呈現更多信息(視頻,圖文等)...javascript

image.png

我第一反應是用3D引擎,由於我不久前剛用three.js作過一個BMW的在線展廳,基本把three.js摸熟了。css

2021-06-03 11_01_41.gif

這裏另有一篇文章教你們用threejs作這個BMW在線DIY~html

方案一:WebGL3D引擎

使用3D引擎先搭一個基本的3D場景,下面的演示使用three.js,同類的3D引擎我還調研過babylon.jsplaycanvas,使用都差不太多,學會一個基本都通的java

var scene, camera, renderer;

function initThree(){
    //場景
    scene = new THREE.Scene();
    //鏡頭
    camera = new THREE.PerspectiveCamera(90, document.body.clientWidth / document.body.clientHeight, 0.1, 100);
    camera.position.set(0, 0, 0.01);
    //渲染器
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(document.body.clientWidth, document.body.clientHeight);
    document.getElementById("container").appendChild(renderer.domElement);
    //鏡頭控制器
    var controls = new THREE.OrbitControls(camera, renderer.domElement);
    
    //一下子在這裏添加3D物體

    loop();
}

//幀同步重繪
function loop() {
    requestAnimationFrame(loop);
    renderer.render(scene, camera);
}

window.onload = initThree;
複製代碼

如今咱們能看到一個黑乎乎的世界,由於如今scene裏什麼都沒有,接着咱們要把三維物體放進去了,使用3D引擎的實現方式無非都是如下幾種css3

使用立方體(box)實現

這種方式最容易理解,咱們在一個房間裏,看向天花板,地面,正面,左右兩面,背面共計六面。咱們把全部六個視角拍成照片就獲得下面六張圖git

image.png

如今咱們直接使用立方體(box)搭出這樣一個房間程序員

var materials = [];
//根據左右上下先後的順序構建六個面的材質集
var texture_left = new THREE.TextureLoader().load( './images/scene_left.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_left} ) );

var texture_right = new THREE.TextureLoader().load( './images/scene_right.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_right} ) );

var texture_top = new THREE.TextureLoader().load( './images/scene_top.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_top} ) );

var texture_bottom = new THREE.TextureLoader().load( './images/scene_bottom.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_bottom} ) );

var texture_front = new THREE.TextureLoader().load( './images/scene_front.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_front} ) );

var texture_back = new THREE.TextureLoader().load( './images/scene_back.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_back} ) );

var box = new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), materials );
scene.add(box);
複製代碼

2021-06-14 19_51_17.gif

好,如今咱們把鏡頭camera(也就是人的視角),放到box內,而且讓全部貼圖向內翻轉後,VR全景就實現了。github

box.geometry.scale( 1, 1, -1 );
複製代碼

如今咱們進入了這個盒子!!web

2021-06-14 19_41_37.gif

threejs官方立方體全景示例canvas

使用球體(sphere)實現

咱們將房間360度球形範圍內全部的光捕捉到一個圖片上,再將這張圖片展開爲矩形,就能獲得這樣一張全景圖片

image.png

var sphereGeometry = new THREE.SphereGeometry(/*半徑*/1, /*垂直節點數量*/50, /*水平節點數量*/50);//節點數量越大,須要計算的三角形就越多,影響性能

var sphere = new THREE.Mesh(sphereGeometry);
sphere.material.wireframe  = true;//用線框模式你們能夠看得清楚是個球體而不是圓形
scene.add(sphere);
複製代碼

image.png

如今咱們把這個全景圖片貼到這個球體上

var texture = new THREE.TextureLoader().load('./images/scene.jpeg');
var sphereMaterial = new THREE.MeshBasicMaterial({map: texture});

var sphere = new THREE.Mesh(sphereGeometry,sphereMaterial);
// sphere.material.wireframe = true;
複製代碼

2021-06-14 14_54_38.gif

和以前同樣,咱們把鏡頭camera(也就是人的視角),放到球體內,而且讓全部貼圖向內翻轉後,VR全景就實現了

如今咱們進入了這個球體!!

var sphereGeometry = new THREE.SphereGeometry(/*半徑*/1, 50, 50);
sphereGeometry.scale(1, 1, -1);
複製代碼

2021-06-14 15_15_28.gif

threejs官方球體全景示例

添加信息點

在VR全景中,咱們須要放置一些信息點,用戶點擊以後作一些動做。

如今咱們創建這樣一個點的數組

var hotPoints=[
    {
        position:{
            x:0,
            y:0,
            z:-0.2
        },
        detail:{
            "title":"信息點1"
        }
    },
    {
        position:{
            x:-0.2,
            y:-0.05,
            z:0.2
        },
        detail:{
            "title":"信息點2"
        }
    }
];
複製代碼

遍歷這個數組,並將信息點的指示圖添加到3D場景中

var pointTexture = new THREE.TextureLoader().load('images/hot.png');
var material = new THREE.SpriteMaterial( { map: pointTexture} );

for(var i=0;i<hotPoints.length;i++){
    var sprite = new THREE.Sprite( material );
    sprite.scale.set( 0.1, 0.1, 0.1 );
    sprite.position.set( hotPoints[i].position.x, hotPoints[i].position.y, hotPoints[i].position.z );

   scene.add( sprite );
}
複製代碼

看到HOT指示圖了嗎?

2021-06-14 20_22_12.gif

添加點擊事件,首先將所有的sprite放到一個數組裏

sprite.detail = hotPoints[i].detail;
poiObjects.push(sprite);
複製代碼

而後咱們經過射線檢測(raycast),就像是鏡頭中心向鼠標所點擊的方向發射出一顆子彈,去檢查這個子彈最終會打中哪些物體。

2021-06-15 01_35_14.gif

document.querySelector("#container").addEventListener("click",function(event){
    event.preventDefault();

    var raycaster = new THREE.Raycaster();
    var mouse = new THREE.Vector2();

    mouse.x = ( event.clientX / document.body.clientWidth ) * 2 - 1;
    mouse.y = - ( event.clientY / document.body.clientHeight ) * 2 + 1;

    raycaster.setFromCamera( mouse, camera );

    var intersects = raycaster.intersectObjects( poiObjects );
    if(intersects.length>0){
        alert("點擊了熱點"+intersects[0].object.detail.title);
    }
});
複製代碼

方案二:CSS3D

threejs等3d引擎太強大了,這些引擎的代碼量都有大幾百K,在今天的網速下顯得無所謂,但在幾年前我接到需求時仍然是重要的考量因素。既然咱們只用到3D引擎的一點點功能,那麼可否找到一個更加輕量的3D引擎呢。

有!css3d-engine,這個3d引擎只有14kb,而且在多個大牌商業項目中應用

使用skybox實現

window.onload=initCSS3D;

function initCSS3D(){
    var s = new C3D.Stage();
    s.size(window.innerWidth, window.innerHeight).update();
    document.getElementById('container').appendChild(s.el);

    var box = new C3D.Skybox();
    box.size(954).position(0, 0, 0).material({
        front: {image: "images/scene_front.jpeg"},
        back: {image: "images/scene_back.jpeg"},
        left: {image: "images/scene_right.jpeg"},
        right: {image: "images/scene_left.jpeg"},
        up: {image: "images/scene_top.jpeg"},
        down: {image: "images/scene_bottom.jpeg"},

    }).update();
    s.addChild(box);

    function loop() {
        angleX += (curMouseX - lastMouseX + lastAngleX - angleX) * 0.3;
        angleY += (curMouseY - lastMouseY + lastAngleY - angleY) * 0.3;

        s.camera.rotation(angleY, -angleX, 0).updateT();
        requestAnimationFrame(loop);
    }

    loop();

    var lastMouseX = 0;
    var lastMouseY = 0;
    var curMouseX = 0;
    var curMouseY = 0;
    var lastAngleX = 0;
    var lastAngleY = 0;
    var angleX = 0;
    var angleY = 0;

    document.addEventListener("mousedown", mouseDownHandler);
    document.addEventListener("mouseup", mouseUpHandler);

    function mouseDownHandler(evt) {
        lastMouseX = curMouseX = evt.pageX;
        lastMouseY = curMouseY = evt.pageY;
        lastAngleX = angleX;
        lastAngleY = angleY;

        document.addEventListener("mousemove", mouseMoveHandler);
    }

    function mouseMoveHandler(evt) {
        curMouseX = evt.pageX;
        curMouseY = evt.pageY;
    }

    function mouseUpHandler(evt) {
        curMouseX = evt.pageX;
        curMouseY = evt.pageY;

        document.removeEventListener("mousemove", mouseMoveHandler);
    }
}
複製代碼

方案二的好處除了庫很小之外,仍是div+css來搭建三維場景的。但這個庫的做者幾乎不維護,遇到問題必須得本身想辦法解決,好比使用在電腦上會看到明顯的面片邊緣

image.png

可是在手機上瀏覽的話表現仍是至關完美的

2021-06-14 22_20_26.gif

添加信息點

咱們繼續爲它添加可交互的信息點

var hotPoints=[
    {
        position:{
            x:0,
            y:0,
            z:-476
        },
        detail:{
            "title":"信息點1"
        }
    },
    {
        position:{
            x:0,
            y:0,
            z:476
        },
        detail:{
            "title":"信息點2"
        }
    }
];
複製代碼
function initPoints(){
    var poiObjects = [];
    for(var i=0;i<hotPoints.length;i++){
        var _p = new C3D.Plane();

        _p.size(207, 162).position(hotPoints[i].position.x,hotPoints[i].position.y,hotPoints[i].position.z).material({
            image: "images/hot.png",
            repeat: 'no-repeat',
            bothsides: true,//注意這個兩面貼圖的屬性
        }).update();
        s.addChild(_p);

        _p.el.detail = hotPoints[i].detail;

        _p.on("click",function(e){
            console.log(e.target.detail.title);
        })
    }
}
複製代碼

這樣就能夠顯示信息點了,而且因爲是div,咱們很是容易添加鼠標點擊交互等效果

image.png

不過,bothsides屬性爲true時,背面的信息點圖片是反的。

image.png

因此咱們這裏要作一點處理,根據其與相機的夾角重置一下信息點的旋轉角度。(若是是那種怎麼旋轉都無所謂的圖片,好比圓點則無需處理

var r = Math.atan2(hotPoints[i].position.z-0,0-0) * 180 / Math.PI+90;
_p.size(207, 162).position(hotPoints[i].position.x,hotPoints[i].position.y,hotPoints[i].position.z).material({
            image: "images/hot.png",
            repeat: 'no-repeat',
            bothsides: false,
        }).update();
複製代碼

需求升級了!

以上兩個方案,我覺得能夠給客戶交差了。但客戶又提出了一些想法

  • 全景圖質量須要更高,但加載速度不容許更慢

  • 每一個場景的信息點挺多的,座標編輯太麻煩了

當時我內心想,總共才收你萬把塊錢,難不成還得給你定製個引擎,再作個可視化編輯器?

直到客戶發過來一個參考連接,我看完驚呆了,全景圖很是清晰,但首次加載速度極快,像百度地圖同樣,是一塊塊從模糊到清晰被加載出來的。

2021-06-14 23_31_28.gif

經過檢查參考連接網頁的代碼,發現了方案三

方案三:pano2vr

image.png

pano2vr是一款所見即所得的全景VR製做軟件(正版149歐元),功能挺強大的,能夠直接輸出成HTML5靜態網頁,體驗很是不錯。

而其核心庫pano2vr_player.js代碼量也只有238kb

image.png

咱們能夠直接使用這個軟件來可視化的添加信息點,輸出成HTML5後,除了靜態圖片之外,全部配置信息都在這個pano.xml文件裏

image.png

修改信息點圖片

總體的交互體驗都很是好,但默認的信息點樣式不喜歡,咱們能夠經過下面的代碼來修改信息點圖片

pano.readConfigUrlAsync("pano.xml",()=>{
    var pois=pano.getPointHotspotIds();

    var hotScale = 0.2;

    for(var i=0;i<pois.length;i++){
            var ids=pois[i];
            var hotsopt=pano.getHotspot(ids);
            hotsopt.div.firstChild.src="images/hot.png";
            hotsopt.div.firstChild.style.width = 207*hotScale+"px";
            hotsopt.div.firstChild.style.height = 162*hotScale+"px";
            hotsopt.div.onmouseover = null;
            hotsopt.div.setAttribute("ids",ids);
            hotsopt.div.onclick=function() {
                   //在這裏能夠響應信息點的點擊事件啦
                   console.log(this.getAttribute("ids"));
            };
    }
});
複製代碼

哈哈,沒想到最終的方案不只極其簡單的就實現了體驗良好的VR全景,還附送了很是方便的信息點編輯。除去第一次開發的耗時,之後再製做新的VR場景也就是花個10分鐘便可搞定。

但想到外包工頭常常壓榨個人報價,壓縮個人工期,無理變動需求

收到工程款的時候他請我去K歌,坐在KTV的包間裏我沒有告訴他使用pano2vr的事,而是對他說

每一個VR場景的信息點都要花1天時間編輯

每製做一個新的VR場景,你收品牌方8k

我每一個場景收你3k,你躺賺5k

畢竟我們老朋友了,我夠意思吧

他豪爽的幹掉手中的啤酒說:「好兄弟,我給你唱一首!」

image.png


本故事純屬虛構,文末配圖若有侵權,請聯繫我跟老闆大哥喝一杯

查看本文配套視頻教程

源碼

微信搜索並關注「大帥老猿」,回覆「webvr」得到本文中三種方案的實現源碼

若是以爲我是個有趣的程序員,請關注我;若是以爲本文還不錯,記得點贊收藏哦,說不定哪天就用得上!

相關文章
相關標籤/搜索