Threejs 实现简单的全景图

360 度、720 度全景

360 全景特点:

1、全:全方位,全面的展示了 360 度球型范围内的所有景致;可在例子中用鼠标左键按住拖动,观看场景的各个方向;
2、景:实景,真实的场景,三维实景大多是在照片基础之上拼合得到的图像,最大限度的保留了场景的真实性;
3、360:360 度环视的效果,虽然照片都是平面的,但是通过软件处理之后得到的 360 度实景,却能给人以三维立体的空间感觉,使观者犹如身在其中。

720° 全景特点

720° 全景则是 720° 的视角,视觉范围超过人眼视觉范围的全景图像,720° 一般我们所说的全景是指横向 360 度及纵向 180 度都可以观看的,能看周围上下全部景象。

VR 概念

VR(Virtual Reality)是利用电脑模拟产生一个三维空间的虚拟世界,提供用户关于视觉等感官的模拟,让用户感觉仿佛身历其境,可以及时、没有限制地观察三维空间内的事物。用户进行位置移动时,电脑可以立即进行复杂的运算,将精确的三维世界视频传回产生临场感。—— 维基百科

与基于现实场景进行增强效果的 AR(Augmented Reality)的区别在于,VR 的场景需要完全重建,类似于进入另一个世界。

应用:如军事中的军事演习,体育界的沉浸式赛事直播,汽车产商可提供车辆在线虚拟配置直销的服务,医疗界的恐惧症治疗方案,贝壳 VR 看房等

全景图原理

通过创造一个球体/正方体,将全景图片作为纹理整体贴合到一个球体上或将全景图片切图为 6 张子图贴到六面体的六个面上,然后将相机放在球体/正方体的中心,监听手指拖动/陀螺仪移动来改变相机的面向,从而实现全景图。

Three.js 创建全景图

创建相机

透视相机参数:fov(相机摄像角度,可用于放大和缩小)、width/height(宽高比)、neer(近距离界限)、far(远距离界限)。

1
2
camera = new THREE.PerspectiveCamera(opt.fov, opt.width / opt.height, 1, 10000);
camera.target = new THREE.Vector3(0, 0, 0);

创建球体

1
2
3
4
// 球体
let geometry = new THREE.SphereBufferGeometry(opt.radius, 60, 60);
// 翻转 X 轴使所有的面都朝里(改变了法向量的方向)
geometry.scale(-1, 1, 1);

添加材质

纹理贴图操作是一个异步的,如果发现纯色能正确渲染,而纹理一片黑色,需要在回调中将物体添加到 scene 中,并且在这个回调里 render

1
2
3
4
5
6
new THREE.TextureLoader().load(holeImage, (texture) => {
materail = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
});
})

创建场景

1
2
3
4
5
// 场景
var mesh = new THREE.Mesh(geometry, material);
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf0f0f0 );
scene.add(mesh);

渲染器

设置好 dpr、画布宽高,Three.js 就会生成一个 canvas。

1
2
3
4
5
const renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(opt.width, opt.height);
const canvas = renderer.domElement;
opt.container.appendChild(canvas);

实时渲染,requestAnimationFrame,让 canvas 实时更新,进行相机视角变化后的渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 动态渲染
const render = function () {
requestAnimationFrame( render );
lat -= orientLat;
lon -= orientLon;

camera.rotation.x = lat * Math.PI / 180;
camera.rotation.y = lon * Math.PI / 180;
// Three.js自带有换算角度的方法,两种写法都可以
// camera.rotation.x = THREE.Math.degToRad(lat);
// camera.rotation.y = THREE.Math.degToRad(lon);
camera.rotation.z = 0;

renderer.render(scene, camera);
};
render();

还可以使用 camera.lookAt(x, y, z)让相机看向某个点
如图球面上任意一个点 A 在赤道面上的投影 为 B, OAB 与赤道平面的夹角是纬度(lat), OB 与水平轴的夹角是经度(lon)。 默认的经纬度为零 (lon=0, lat=0), 当我们移动鼠标时根据鼠标移动的距离改变 lat, lon 的值,然后再根据 lat, lon 计算出 A 点的坐标,让相机指向 A 就行了。

监听移动

移动端监听 touch 事件、PC 端监听鼠标 mouse 事件,来判断手指划过的距离,并以此计算出相机应该在 x 轴和 y 轴方向上各移动多少角度。也可以用 Three.js 自身提供到轨道控制器(OrbitControls),它可以使得相机围绕目标进行轨道运动,使用前需要显示引入文件。

方式一:轨道控制器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 以下是关键代码

// OrbitControls需要手动引入
import { OrbitControls } from "./OrbitControls";

// 轨道控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
controls.dampingFactor = 0.05;

controls.screenSpacePanning = false;

controls.minDistance = 100;
controls.maxDistance = 500;

controls.maxPolarAngle = Math.PI / 2;

//渲染
render();

function render() {
renderer.clear();
renderer.render(scene, camera);
controls.update();
requestAnimationFrame(render);
}

方式二:自己手动实现移动函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var scene, camera, renderer;
var isUserInteracting = false,
onMouseDownX = 0,
onMouseDownY = 0,
lon = 0,
lat = 0,
phi = 0,
theta = 0;

document.addEventListener("mousedown", onMouseDown, false);
document.addEventListener("mousemove", onMouseMove, false);
document.addEventListener("mouseup", onMouseUp, false);

function onMouseDown(event) {
event.preventDefault();
isUserInteracting = true;
onMouseDownX = event.clientX;
onMouseDownY = event.clientY;
}

function onMouseMove(event) {
if (isUserInteracting === true) {
lon -= (event.clientX - onMouseDownX) * 0.1; // 经度
lat += (event.clientY - onMouseDownY) * 0.1; // 纬度

onMouseDownX = event.clientX;
onMouseDownY = event.clientY;
}
}

function onMouseUp(event) {
isUserInteracting = false;
}

function animate() {
requestAnimationFrame(animate);
updateCamera();
renderer.render(scene, camera);
}

function updateCamera() {
lat = Math.max(-85, Math.min(85, lat)); // 纬度限定在 [-85,85]
phi = THREE.Math.degToRad(90 - lat); // 90 - 纬度
theta = THREE.Math.degToRad(lon); // 经度

// 通过经纬度计算球面上点的坐标
camera.target.x = 500 * Math.sin(phi) * Math.cos(theta);
camera.target.y = 500 * Math.cos(phi);
camera.target.z = 500 * Math.sin(phi) * Math.sin(theta);
// 调整相机指向
camera.lookAt(camera.target);
}

参考文章

three.js 文档
一步步带你实现 web 全景看房——three.js
聊一聊全景图
Web 全景图的原理及实现
如何用 Three.js 快速实现全景图
球体全景图