硬核看房利器——Web 全景的实现
Posted 凹凸实验室
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核看房利器——Web 全景的实现相关的知识,希望对你有一定的参考价值。
preserve-3d
,意味着浏览器以这个容器为单位建立了一个三维空间体系,这个容器下的子元素的样式属性都将按照这个三维体系下的空间关系渲染。MDN 上的这个例子可以很好地体现二维体系与三维体系中元素空间关系的区别:https://developer.mozilla.org/en-US/docs/Web/CSS/transform-styleperspective
的值在三维体系中表示观察点距离 z 轴 0 坐标位置的距离,在视觉上的体现则为值越大,透视效果越弱。这么说可能很难想象,可以参考大家在坐车的时候往窗外看,近景物体与远景物体移动速度的差别,则为 perspective
值之间的差别,值越小意味着物体离你越近。默认值为 1000px,如果觉得透视效果不够明显,可以适当地减少 perspective
的值。
demo 来源:https://developer.mozilla.org/en-US/docs/Web/CSS/perspective
需要注意的是,这里一共需要两层 transform-style
的设置。包裹着所有切片的容器需要设置,整个场景的 3D 旋转操作就是在这个容器上。为了让这个容器的旋转也产生 3D 的效果,需要在这个容器的外层再添加一个带 transform-style
属性的容器。
三维体系与透视值设定好了之后,就可以开始对子元素进行布局了。与 3D 布局相关的属性基本集中在 transform
中,通过 translateX
、translateY
、translateZ
、rotateX
、rotateY
、rotateZ
则可以满足我们想要建立的三维场景。
以较为简单的圆柱型场景为例,我们需要确定切片的数量,然后通过计算确定切片的旋转角度与位移距离。因为我们有旋转场景的需求,因此以 (0, 0, 0)
为场景中心点是较为好操作的。首先大家需要明确一个点,transform
中的各函数值先后顺序不同所展示的样式也是不同的。因此先设定 rotate
与先设定 translate
,对应的函数值也不同。如果是先设定 rotate
,则在位移上可以统一使用一个值,这里我们使用 translateZ
;如果先设定 translate
,在位移的计算上则复杂了不少。因此我们采用先旋转再位移的形式来布局全景场景的切片。
从 y 轴视角看切片,是一个正多边形。每一切片间的间隔角度为360度除以切片数量,而切片的宽度则需要通过三角函数来计算。
下面的代码段中的计算,radius 值固定,因此根据边数的不同,切片的高度将发生变化。也可以固定 d 值或是切片高度来进行相应值的计算。
圆柱型场景使用的贴图为首尾相接的『圆柱型』或『矩形球面投影』类型的,我们要做的就是将这张图平均、无缝分布到每一片切片上。这里使用 background-image
背景图进行贴图的话,就需要针对 background-position
进行计算。
position = 方法新增元素。镜头充当着“眼”的功能,放置了镜头才能“看到”场景中的元素,同时通过设置 position
参数调整镜头的摆放位置,通过 lookAt
方法调整镜头聚焦的位置。阿三提供了两种模式的摄像机——正交投影摄像机(OrthographicCamera)与透视投影摄像机(PerspectiveCamera)。区别在于针对不同距离的元素是否有做透视处理,如果需要透视处理,选择透视投影摄像机即可,这也是比较常用的摄像机。
camera.position.set(camerax, cameray, cameraz)
camera.lookAt(scene.position)
透视摄像机需要针对我们传统意义上的视野进行一些参数设置,一共4个参数,决定了摄像机的最终视野。
THREE.WebGLRenderer( 属性,可以实现元素的显隐,这一点可以应用于多全景场景之间的切换效果。
sphereMaterial.transparent = vx = (originalEndX - startX) / duration
endX = vx * 200
这里我们结合 gsap 的时间轴,利用其动画计算可以基于当前状态值的特性,对整体动效进行平滑过渡处理,因为对于交互动作监听的时间间隔够短,在结束之前的数值缓动效果可以忽略不计,但起始与终止的缓动却可以保留,堪称完美。
完整缓动算法 demo:https://codesandbox.io/s/move-ani-compare-xvi26?file=/index.js
有了这个算法,我们就可以针对每一种方案进行场景的交互处理了。
CSS 3D 场景
在 CSS 3D 场景中,要实现全景场景的浏览,需要做的是旋转整个三维体系容器,也就是使用 transform 中的 rotate 函数。针对交互数值变化与 rotate 数值做一个置换。
完整 demo:https://codesandbox.io/s/css-3d-panorama-with-mouse-event-jp2jl?file=/src/Pano.js
ThreeJS 场景
在阿三场景中,我们需要改变的是摄像机聚焦点的位置,又或者将摄像机聚焦点固定在球体中心,移动摄像机的位置。摄像机移动的范围就在以球体中心为球心的球面上。
具体的鼠标位移与球体经纬度的转化公式可查看完整 demo:https://codesandbox.io/s/threejs-panorama-dy1lt?file=/src/event.js
这样,一个完整的全景体验的 web 全景就开发完成了。
主要挑战当我们把全景的框架搭建好之后,理论上只要替换图片,则可以实现无数场景的全景体验。但稍微上网一搜,大家会发现,这类全景图想要找到既高清又符合首尾相接标准的,真的很难,要么带水印,要么不够清晰,要么就收费。如果你想自己拍,又会发现拍摄这一类照片所需要的设备要求还蛮高,非专业机构没什么必要购置。因此此类全景场景最大的挑战就在于全景图的获取,纯体力活,需要消耗大量人力与时间。而像贝壳找房这类受疫情冲击的行业,通过推动 VR 看房,结合便利、使用门槛低的设备,既能消化闲置的人力,同时还维系了业务的运转。
同时大家也能看出,文章开头的 VR 看房视频,并不是单纯的 Web 全景就能实现,其中还包含了测距、路径切换等。而这一套服务,是由如视(https://realsee.com/)服务商提供的,他们售卖的方案中包含了自研硬件扫描仪与线上三维重建技术等软件的打包,解决了内容创作门槛这个痛点,同时通过 C 端设备降级方案,由保有量较少的 VR 眼镜转向了体验稳定的 PC 与手机,这或许将成为近一段时期 VR 消费端的通用推广方式。
VR 的未来一项新兴技术的成熟曲线往往要经历新技术诞生、期望膨胀、泡沫化、稳步爬升、实质生产这5个阶段,目前 VR 正处于启蒙期阶段,内容创作成本尚未降低,设备保有量尚未形成规模。
图片来源:《聊聊 VR 虚拟现实(一):VR 的发展史》
在 2016 年高盛发布的《VR 与 AR 报告:下一个通用计算平台》中,对于 2025 年 VR/AR 9大应用领域规模的预期,只有视频游戏、事件直播和视频娱乐3大领域将完全由消费者推动,占整体 VR/AR 营收预期的 60%,剩余 40% 由企业和公共部门推动。
正如前文提到的,从文字到图片到声音再到视频,是目前所广泛使用的信息传播途径,未来的模式一定是更加降低门槛、减少失真,虚拟化将成为必然的趋势,加上 AI 的持续推进,以及 5G 的未来,VR 的想象空间还有很大。
参考文档—浅谈 WebVR:https://aotu.io/notes/2016/08/24/2016-8-24-webvr/ A-Frame WebVR 试玩报告:https://aotu.io/notes/2016/10/08/aframe/ CSS 3D Panorama - 淘宝造物节技术剖析:https://aotu.io/notes/2016/08/24/2016-8-24-css-3d-panorama/ krpano:https://krpano.com/home/ 使用 ThreeJS 在浏览器中展示全景图:https://aotu.io/notes/2016/01/02/3D-panorama/
Pano2VR:https://ggnome.com/pano2vr/ VR 看房是如何实现的?:https://cloud.tencent.com/developer/ask/142336 十分钟打造 3D 物理世界:https://aotu.io/notes/2018/10/18/cannonjs/ 高盛 VR 与 AR 报告:下一个通用计算平台:https://tech.qq.com/a/20160202/011274.htm Google AR&VR:https://arvr.google.com/ The Best VR Headsets for 2020:https://www.pcmag.com/picks/the-best-vr-headsets 贝壳x如视案例:https://realsee.com/website/public/demo/fwmm/ryQkazl8xKdesBhKhVTkqqxTVLX6Rn9Km.html 聊聊VR虚拟现实(一):VR的发展史:https://zhuanlan.zhihu.com/p/26592125 VR眼镜的分类:https://zhuanlan.zhihu.com/p/31649178 也说5G与云VR:https://zhuanlan.zhihu.com/p/83623971 5G专网为“江南皮革厂”带来了什么?:https://mp.weixin.qq.com/s/BBwI2_Tnp_5cO2ngr4eh1w
三种前端实现VR全景看房的方案,收藏吧说不定哪天就用得上
前言
=====
事情是这样的,前几天我接到一个外包工头
的新需求,某品牌要搭建一个在线VR展厅,用户可以在手机上通过陀螺仪或者拖动来360度全景参观展厅,这个VR展厅里会有一些信息点,点击之后可以呈现更多信息(视频,图文等)…
image.png
我第一反应是用3D引擎,因为我不久前刚用three.js
做过一个BMW
的在线展厅,基本把three.js
摸熟了。
2021-06-03 11_01_41.gif
会另写一篇文章教大家用threejs做这个[BMW在线DIY],感兴趣的小伙伴请关注我吧~
方案一:WebGL3D引擎
使用3D引擎先搭一个基本的3D场景,下面的演示使用three.js,同类的3D引擎我还调研过babylon.js,playcanvas,使用都差不太多,学会一个基本都通的
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引擎的实现方式无非都是以下几种
使用立方体(box)实现
这种方式最容易理解,我们在一个房间里,看向天花板,地面,正面,左右两面,背面共计六面。我们把所有六个视角拍成照片就得到下面六张图
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全景就实现了。
box.geometry.scale( 1, 1, -1 );
现在我们进入了这个盒子!!
2021-06-14 19_41_37.gif
threejs官方立方体全景示例
使用球体(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
,并且在多个大牌商业项目中应用
-
淘宝造物节 https://shrek.imdevsh.com/show/zwj/
-
adidas绝不凋谢 https://shrek.imdevsh.com/show/drose/
-
adidas胜势全开 https://shrek.imdevsh.com/show/bbcny/
-
adidas绝不跟随 https://shrek.imdevsh.com/show/crazylight/
使用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,我们非常容易添加鼠标点击交互等效果
![图片](https://mmbiz.qpic.cn/mmbiz_png/t1ynS50Irh3H6sn2Pc31b8LLcjL2Ht5ZoZiaHPARu2sZK6j80sU6cON6FlsmeicUVCT1MXH939IfolSwpXwIYZibQ/640?wx_fmt=png)
image.png
不过,`bothsides`属性为true时,背面的信息点图片是反的。
![图片](https://mmbiz.qpic.cn/mmbiz_png/t1ynS50Irh3H6sn2Pc31b8LLcjL2Ht5ZzSdsibZk0RFGWeeIAtkhBp0d7Vw5UZibSOQCKt4RymZOvj9U8NL56QoA/640?wx_fmt=png)
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();
需求升级了!
======
以上两个方案,我以为可以给客户交差了。但客户又提出了一些想法
* **全景图质量需要更高,但加载速度不允许更慢**
* **每个场景的信息点挺多的,坐标编辑太麻烦了**
当时我心里想,总共才收你万把块钱,难不成还得给你定制个引擎,再做个可视化编辑器?
![图片](https://mmbiz.qpic.cn/mmbiz_png/t1ynS50Irh3H6sn2Pc31b8LLcjL2Ht5ZuvWUIWrbxbxPic6eENNodMP4WEicexf8FVPyJvE1uPTBZrPdjzUoPCZA/640?wx_fmt=png)
直到客户发过来一个参考链接,我看完`惊呆了`,全景图非常清晰,但首次加载速度极快,像百度地图一样,是一块块从模糊到清晰被加载出来的。
![图片](https://mmbiz.qpic.cn/mmbiz_gif/t1ynS50Irh3H6sn2Pc31b8LLcjL2Ht5Zic1oKjJhuYVpDBhPOWYiaXYmuo2IVV5U78CyKCTkibUlgnbMlRYQI8u8A/640?wx_fmt=gif)
2021-06-14 23\\_31\\_28.gif
通过检查参考链接网页的代码,发现了方案三
方案三:pano2vr
===========
![图片](https://mmbiz.qpic.cn/mmbiz_png/t1ynS50Irh3H6sn2Pc31b8LLcjL2Ht5Z3WzDvhFfhGYnFsSk0De26v1icBO2RUmDXyh2hjR0FNPRhNr2vS8xhBw/640?wx_fmt=png)
image.png
pano2vr是一款所见即所得的全景VR制作软件(正版149欧元),功能挺强大的,可以直接输出成HTML5静态网页,体验非常不错。
而其核心库`pano2vr_player.js`代码量也只有`238kb`。
![图片](https://mmbiz.qpic.cn/mmbiz_png/t1ynS50Irh3H6sn2Pc31b8LLcjL2Ht5Z3OhqPFq0TuMjjdv8QFdIzSqt10YAM4wWI2D366NBpY9YKw2vAauA9g/640?wx_fmt=png)
image.png
我们可以直接使用这个软件来可视化的添加信息点,输出成HTML5后,除了静态图片以外,所有配置信息都在这个`pano.xml`文件里
![图片](https://mmbiz.qpic.cn/mmbiz_png/t1ynS50Irh3H6sn2Pc31b8LLcjL2Ht5Zy0tBacniaJPXAbELib4DibId7wELeeDxOzGNyBTL32qVhkf51PObkNAkQ/640?wx_fmt=png)
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 = 207hotScale+“px”;
hotsopt.div.firstChild.style.height = 162hotScale+“px”;
hotsopt.div.onmouseover = null;
hotsopt.div.setAttribute(“ids”,ids);
hotsopt.div.οnclick=function()
//在这里可以响应信息点的点击事件啦
console.log(this.getAttribute(“ids”));
;
);
哈哈,没想到最终的方案不仅极其简单的就实现了体验良好的VR全景,还附送了非常方便的信息点编辑。除去第一次开发的耗时,以后再制作新的VR场景也就是花个10分钟即可搞定。
但想到`外包工头`经常_压榨我的报价,压缩我的工期,无理变更需求_
收到工程款的时候他请我去K歌,坐在KTV的包间里我没有告诉他使用pano2vr的事,而是对他说
**每个VR场景的信息点都要花1天时间编辑**
**每制作一个新的VR场景,你收品牌方8k**
**我每个场景收你3k,你躺赚5k**
**毕竟咱们老朋友了,我够意思吧**
他豪爽的干掉手中的啤酒说:“好兄弟,我给你唱一首!”
![图片](https://img-blog.csdnimg.cn/img_convert/e10d17c70a0e55f074f88a6ebe4ecfd3.png)
image.png
* * *
本故事纯属虚构,文末配图如有侵权,请联系我跟老板大哥喝一杯
查看本文配套视频教程
源码
==
微信搜索并关注“`大帅老猿`”,回复“`webvr`”获得本文中三种方案的实现源码
> 如果觉得我是个有趣的程序员,请关注我;如果觉得本文还不错,记得点赞收藏哦,说不定哪天就用得上!
以上是关于硬核看房利器——Web 全景的实现的主要内容,如果未能解决你的问题,请参考以下文章