基于three.js的全景图播放器
之前我有一个项目就是制作一个全景图展示的页面,我们使用的是中国一位大神的开源项目,这个高手真的是有一点厉害。人家不仅仅完整开发了一套几乎可以直接商用的工具,全部基于three.js而且没有使用任何插件什么的,连很多图片都base64编辑代码里面了。总之他这个开源项目是我比较服的地址是:https://github.com/pchen66/panolens.js.git,大家可以看看。
虽然是NB但是他所维护的这个项目其实已经经历了多个大版本更新了,代码到现在已经有点重型了,而且有一个诡异无法解决的问题,那就是微信上面加载多张图片的时候很容易黑屏,而且当时我根本就不知道怎么解决。
所幸three.js我还是会一点点,大神的代码如果我直接拿来修改的话是非常浪费时间的,而且说实话走读他的代码需要你慢慢来不是什么半个小时就可以搞定的东西。它的逻辑是将一张2:1的全景图切成六个面然后放到一个box的六面上面去,问题是这个切法是靠UV计算来实现的如果着色器初始化一旦出现问题那么就黑屏无法显示,这也就是为什么微信上面会黑掉。由于我之前尝试做过很多较大的webgl场景在微信上面分享使用所以确定问题不出在微信与three就是这些高级着色器破坏了微信浏览器的渲染逻辑(虽然微信这个东西也不是什么好定西)。
又没有那么多时间走读、修改他的代码那就只有我自己写写了,所幸我需要的功能仅仅是全景图播放器而已所以我便自己写了。目前实现了的功能有对象化管理场景、添加多张全景图、全景图之间的导航(无需刷新)、全景图上的标记点(html内容)、手机陀螺仪跟踪摄像机。代码就是下面这些。
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
/**
* T_panolens.js
* @author 唐玥璨
* @namespace T
*/
var T= {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}
*/
T.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 - 图片的路径
*/
T.Panolen = function(id,img)
{
this.img = img;
this.id = id;
return this;
}
/**
* 陀螺仪控制器的初始化
* @property {object} camera - 相机的对象
* @property {object} domElement - 原生js的html对象
*/
T.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
*/
T.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 T.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还有一段路要走。