簡介html
你們好我是張鵬輝(道長)人如其名,我是天橋上算命的,轉發這條博文,接下來一個月會有意想不到的驚喜發生。
最近微博上的全景圖火了,因此決定實現一下。前端
工程裏面圖片資源來自網絡,若有侵權請聯繫我,立刻刪除
固然實現的方式不少好比OpenCV、u3d等。
這裏提供三種方式實現:android
先看下三種實現的效果:git
1.OpenGL ESgithub
2.Google VR(全景圖模塊)web
3.Three.js(利用前端姿式)WebView混合開發算法
第一種方式使用OpenGL來實現(上面gif圖截取由於博客限制上傳圖片的大小,我壓縮了,看起來有些卡其實很流暢的)
能夠看到支持旋轉手機查看、或者拖動圖片查看、能夠看到右邊中心部分有個指示器會隨着角度變化而變化而且點擊能夠還原起始位置。小程序
有些小夥伴懶得看原理,直接就想拿來用因此我先說集成方式吧!服務器
在build.gradle 文件中添加庫依賴:網絡
allprojects { repositories { maven { url 'https://jitpack.io' } } }
在 build.gradle 文件中添加庫依賴:
dependencies { compile 'com.github.CN-ZPH:weibo360panorama:v1.0.1' }
build.gradle 完整代碼:
apply plugin: 'com.android.application' android { compileSdkVersion 26 buildToolsVersion "26.0.1" defaultConfig { applicationId "com.zph.three360panorama" minSdkVersion 19 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } allprojects { repositories { maven { url 'https://jitpack.io' } } } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:26.+' compile 'com.android.support.constraint:constraint-layout:1.0.2' compile 'com.android.support:design:26.+' compile 'com.github.CN-ZPH:weibo360panorama:v1.0.1' compile 'com.google.vr:sdk-panowidget:1.80.0' testCompile 'junit:junit:4.12' compile files('libs/tbs_sdk_thirdapp_v3.3.0.1045_43300.jar') }
<com.zph.glpanorama.GLPanorama android:id="@+id/mGLPanorama" android:layout_width="match_parent" android:layout_height="match_parent"></com.zph.glpanorama.GLPanorama>
*R.drawable.imggugong 這張全景圖傳到控件裏面
public class MainActivity extends AppCompatActivity { private GLPanorama mGLPanorama; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化全景控件 mGLPanorama= (GLPanorama) findViewById(R.id.mGLPanorama); //傳入你的全景圖 mGLPanorama.setGLPanorama(R.drawable.imggugong); } }
首先咱們須要瞭解全景圖是什麼東西,全景圖是一種廣角圖。經過全景播放器可讓觀看者身臨其境地進入到全景圖所記錄的場景中去,一般標準的全景圖是一張2:1的圖像,其背後的實質就是等距圓柱投影。等距圓柱投影是一種將球體上的各個點投影到圓柱體的側面上的一種投影方式,投影完以後再將它展開就是一張2:1的長方形的圖像。比較常見的就是應用在地圖上的投影。
獲得全景圖後那咱們就須要展現了,看到旁邊地球了嗎?
怎麼展現呢簡單來講就是把全景圖片整個貼到一個球體上。
好了知道原理那咱們就該考慮在android上怎麼實現了,在android中繪製3d圖形可使用OpenGL (就不說OpenGL 基礎了想看的本身百度一大堆資料)。
引用tim_shadow大佬的關於全景圖一篇文章介紹
在OpenGL ES中基本上全部的立體圖像都是經過一個個的小三角形拼接而成咱們知道球面上面的每個點(P(x,y,z))都會知足方程組(球的極座標方程):
x = r sin(a) cos(b)
y = r * cos(a)
z = r sin(a)sin(b)
其中 r爲球的半徑,a爲線段 OP與 z軸正方向所夾角,b爲 OP在xoy平面的投影 OP‘ 與x的正方向所夾角
咱們能夠根據這個方程組,經過控制∠a和∠b的變化,從上到下,逆時針的取得咱們須要用來組合稱三角形的點,而後咱們須要將全景圖片上的點與咱們在球上面選取的點一一對應起來(注意:球的座標是3維座標,圖片的座標是2維座標)
球上面的點與圖片上面的點一一對應起來。
紋理和圖片綁定繪製到屏幕上
int[] textures = new int[1]; glGenTextures(1, textures, 0); int textureId = textures[0]; glBindTexture(GL_TEXTURE_2D, textureId); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); InputStream is = context.getResources().openRawResource(drawableId); Bitmap bitmapTmp; try { bitmapTmp = BitmapFactory.decodeStream(is); } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmapTmp, 0); bitmapTmp.recycle();
第一想到的就是重力感應傳感器,但是隻能得到咱們向那個位置偏移的方向,顯然不可能知足咱們旋轉的需求,使用陀螺儀傳感器。
陀螺儀就是內部有一個陀螺,它的軸因爲陀螺效應始終與初始方向平行,這樣就能夠經過與初始方向的誤差計算出實際方向。
陀螺儀對設備旋轉角度的檢測是瞬時的並且是很是精確的。
首先註冊陀螺儀傳感器根據具體須要本身設置靈敏度,固然越靈敏,越耗電。
- 註冊陀螺儀傳感器,並設定傳感器嚮應用中輸出的時間間隔類型是SensorManager.SENSOR_DELAY_GAME(20000微秒)
- SensorManager.SENSOR_DELAY_FASTEST(0微秒):最快。最低延遲,通常不是特別敏感的處理不推薦使用,該模式可能在成手機電力大量消耗,因爲傳遞的爲原始數據,算法不處理好會影響遊戲邏輯和UI的性能
- SensorManager.SENSOR_DELAY_GAME(20000微秒):遊戲。遊戲延遲,通常絕大多數的實時性較高的遊戲都是用該級別
- SensorManager.SENSOR_DELAY_NORMAL(200000微秒):普通。標準延時,對於通常的益智類或EASY級別的遊戲可使用,但太低的採樣率可能對一些賽車類遊戲有跳幀現象
- SensorManager.SENSOR_DELAY_UI(60000微秒):用戶界面。通常對於屏幕方向自動旋轉使用,相對節省電能和邏輯處理,通常遊戲開發中不使用
我這裏爲了測試設置了SENSOR_DELAY_FASTEST,實際使用建議用SENSOR_DELAY_GAME
private void initSensor() { sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); sensorManager.registerListener(this, gyroscopeSensor, SensorManager.SENSOR_DELAY_FASTEST); }
當傳感器的值發生變化時,例如磁阻傳感器方向改變時會調用OnSensorChanged(). 當傳感器的精度發生變化時會調用OnAccuracyChanged()方法。
從 x、y、z 軸的正向位置觀看處於原始方位的設備,若是設備逆時針旋轉,將會收到正值;不然,爲負值
獲得兩次檢測到手機旋轉的時間差(納秒),並將其轉化爲秒
將手機在各個軸上的旋轉角度相加,便可獲得當前位置相對於初始位置的旋轉弧度,將弧度轉化爲角度
@Override public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == Sensor.TYPE_GYROSCOPE) { if (timestamp != 0) { final float dT = (sensorEvent.timestamp - timestamp) * NS2S; angle[0] += sensorEvent.values[0] * dT; angle[1] += sensorEvent.values[1] * dT; angle[2] += sensorEvent.values[2] * dT; float anglex = (float) Math.toDegrees(angle[0]); float angley = (float) Math.toDegrees(angle[1]); float anglez = (float) Math.toDegrees(angle[2]); Sensordt info = new Sensordt(); info.setSensorX(angley); info.setSensorY(anglex); info.setSensorZ(anglez); Message msg = new Message(); msg.what = 101; msg.obj = info; mHandler.sendMessage(msg); } timestamp = sensorEvent.timestamp; } }
每次得到角度數據後只須要y,x的值計算位移的值
由於全景圖上下旋轉會翻轉整個圖因此我這裏設置了上下只能偏移50f,若是不限制你能夠去掉
mBall.yAngle += dx 2.0f;這裏2.0也就是陀螺儀傳過來的值乘以得出偏移的角度,數值越大,每次偏移更快!
Sensordt info = (Sensordt) msg.obj; float y = info.getSensorY(); float x = info.getSensorX(); float dy = y - mPreviousY;// 計算觸控筆Y位移 float dx = x - mPreviousX;// 計算觸控筆X位移 mBall.yAngle += dx * 2.0f;// 設置填充橢圓繞y軸旋轉的角度 mBall.xAngle += dy * 0.5f;// 設置填充橢圓繞x軸旋轉的角度 if (mBall.xAngle < -50f) { mBall.xAngle = -50f; } else if (mBall.xAngle > 50f) { mBall.xAngle = 50f; } mPreviousY = y; mPreviousX = x;
加入手勢這裏沒什麼好說的了,就是重寫onTouchEvent()方法。
這裏惟一要注意的就是,當手指點擊屏幕的時候要關閉陀螺儀傳感器的監聽否則會引發衝突。當手指離開屏幕,從新監聽陀螺儀傳感器。
和上面也同樣只是這裏換成獲取手指偏移角度,而不是傳感器的數值,直接看代碼。
public boolean onTouchEvent(MotionEvent e) { sensorManager.unregisterListener(this); float y = e.getY(); float x = e.getX(); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: float dy = y - mPreviousYs;// 計算觸控筆Y位移 float dx = x - mPreviousXs;// 計算觸控筆X位移 mBall.yAngle += dx * 0.3f;// 設置填充橢圓繞y軸旋轉的角度 mBall.xAngle += dy * 0.3f;// 設置填充橢圓繞x軸旋轉的角度 if (mBall.xAngle < -50f) { mBall.xAngle = -50f; } else if (mBall.xAngle > 50f) { mBall.xAngle = 50f; } Log.i("zphsas", "mHandler *** mPreviousY" + mBall.yAngle); Log.i("zphsas", "mHandler *** mPreviousx" + mBall.xAngle); rotate(); break; case MotionEvent.ACTION_UP: sensorManager.registerListener(this, gyroscopeSensor, SensorManager.SENSOR_DELAY_FASTEST); break; } mPreviousYs = y;// 記錄觸控筆位置 mPreviousXs = x;// 記錄觸控筆位置 return true; }
指示器這裏弄了一個角標指示當前在全景圖的角度,而且點擊還原起始角度。
能夠想象一樣是獲取角度,咱們直接放在全景圖改變的地方,讓指示器一塊兒改變,而咱們改變的地方只有2個陀螺儀和拖動屏幕。
我這裏指示器放了一張圖也就是一個 ImageView 控件
1.爲指示器加入動畫跟隨全景圖一塊兒轉
private void rotate() { RotateAnimation anim = new RotateAnimation(predegrees, -mBall.yAngle, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); anim.setDuration(200); img.startAnimation(anim); predegrees = -mBall.yAngle;//記錄這一次的起始角度做爲下次旋轉的初始角度 }
2.點擊指示器還原起始位置
當點擊還原的時候,我一開始是直接恢復起始位置但是太生硬了,經過獲取當前旋轉的角度,逆向旋轉,慢慢還原,讓其有個過渡的效果。
Y軸=旋轉的角度-90f(起始角度)/10f(每次偏移多少,通過我屢次嘗試10f在個人手機上剛恰好);
獲得咱們總共偏移幾回能夠復位;
X軸同理,由於我上面限制了X軸的最大偏移,這裏就不就算X軸了,不過在完成的同時直接復位X軸。(只是沒有過渡的效果),你能夠加上。
我設置的起始角度是90f和0f,也就是X,Y軸的起始點
mHandlers.postDelayed(this, 16);
這行代碼就是多少毫秒復位一次。
看代碼:
private void zero() { yy = (int) ((mBall.yAngle - 90f) / 10f); mHandlers.post(new Runnable() { @Override public void run() { if (yy != 0) { if (yy > 0) { mBall.yAngle = mBall.yAngle - 10f; mHandlers.postDelayed(this, 16); yy--; } if (yy < 0) { mBall.yAngle = mBall.yAngle + 10f; mHandlers.postDelayed(this, 16); yy++; } } else { mBall.yAngle = 90f; } mBall.xAngle = 0f; } }); }
第二種也就是谷歌官方爲移動平臺下VR解決方案,有興趣的能夠點開下面連接玩玩,咱們只使用其中全景圖模塊。
Google VR主頁:https://developers.google.com...
Google VR for Android github地址:https://github.com/googlevr/g...
目前GitHub上最新版本號爲1.8.0,我這裏也用最新的了。
最低支持到 minSdkVersion 19 也就是Android 4.4.0
在 build.gradle 文件中添加庫依賴:
dependencies { compile 'com.google.vr:sdk-panowidget:1.80.0' }
<com.google.vr.sdk.widgets.pano.VrPanoramaView android:id="@+id/mVrPanoramaView" android:layout_width="match_parent" android:layout_height="250dip"/>
<!-- These permissions are used by Google VR SDK to get the best Google VR headset profiles. !--> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> //由於全景圖較大,因此在application下申請更多空間,可是做爲一個有節操的碼農建議你不要這麼幹。 <application android:largeHeap="true" </application>
//初始化VR圖片 private void initVrPaNormalView() { mVrPanoramaView = (VrPanoramaView) findViewById(R.id.mVrPanoramaView); paNormalOptions = new VrPanoramaView.Options(); paNormalOptions.inputType = VrPanoramaView.Options.TYPE_STEREO_OVER_UNDER; // mVrPanoramaView.setFullscreenButtonEnabled (false); //隱藏全屏模式按鈕 mVrPanoramaView.setInfoButtonEnabled(false); //設置隱藏最左邊信息的按鈕 mVrPanoramaView.setStereoModeButtonEnabled(false); //設置隱藏立體模型的按鈕 mVrPanoramaView.setEventListener(new ActivityEventListener()); //設置監聽 //加載本地的圖片源 mVrPanoramaView.loadImageFromBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.andes), paNormalOptions); //設置網絡圖片源 // panoWidgetView.loadImageFromByteArray(); } private class ActivityEventListener extends VrPanoramaEventListener { @Override public void onLoadSuccess() {//圖片加載成功 } @Override public void onLoadError(String errorMessage) {//圖片加載失敗 } @Override public void onClick() {//當咱們點擊了VrPanoramaView 時候觸發 super.onClick(); } @Override public void onDisplayModeChanged(int newDisplayMode) { super.onDisplayModeChanged(newDisplayMode); } }
Three.js是JavaScript編寫的WebGL第三方庫。提供了很是多的3D顯示功能。
Android下相信不少人都多少作過前端開發,如今不少應用程序都是基於前端H5/RN/小程序等來玩的。
固然咱們全景圖也能夠放到前端來實現,套個WebView利用JavaScript與Android交互來實現一部分功能。
考慮到在多種機型兼容性,還有原生WebView的一些坑,我這裏使用騰訊的X5內核的WebView。
到x5官網下載最新的sdk獲得一個jar包
我在這的是3.3.0版本的。
將下載好的jar包放到你的工程libs目錄下
在 build.gradle 文件中添加庫依賴:
dependencies { compile files('libs/tbs_sdk_thirdapp_v3.3.0.1045_43300_sharewithdownload_withoutGame_obfs_20170605_170212.jar') }
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
public class APPAplication extends Application { @Override public void onCreate() { // TODO Auto-generated method stub super.onCreate(); //蒐集本地tbs內核信息並上報服務器,服務器返回結果決定使用哪一個內核。 QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() { @Override public void onViewInitFinished(boolean arg0) { // TODO Auto-generated method stub //x5內核初始化完成的回調,爲true表示x5內核加載成功,不然表示x5內核加載失敗,會自動切換到系統內核。 } @Override public void onCoreInitFinished() { // TODO Auto-generated method stub } }; //x5內核初始化接口 QbSdk.initX5Environment(getApplicationContext(), cb); } }
<com.tencent.smtt.sdk.WebView android:id="@+id/web" android:layout_width="match_parent" android:layout_height="match_parent"></com.tencent.smtt.sdk.WebView>
下載地址:https://threejs.org/
或者去GitHub從個人項目中找今天代碼都會放到GitHub上
<script src="js/three.min.js"></script> <script src="js/photo-sphere-viewer.min.js"></script>
在 assets 目錄下建立一個html文件展現全景圖
引入Threejs
panorama:'https://gw.alicdn.com/tfs/TB1...', 這行就是你的全景圖地址
你可使用js交互將你的地址傳到HTML上
直接上代碼了:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Photo Sphere Viewer</title> <meta name="viewport" content="initial-scale=1.0" /> <script src="js/three.min.js"></script> <script src="js/photo-sphere-viewer.min.js"></script> <style> html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; } #container { width: 100%; height: 100%; } </style> </head> <body> <div id="container"></div> <script> var div = document.getElementById('container'); var PSV = new PhotoSphereViewer({ panorama: 'https://gw.alicdn.com/tfs/TB1WSInRFXXXXXlXpXXXXXXXXXX-1200-600.jpg', container: div, time_anim: false, navbar: true, navbar_style: { backgroundColor: 'rgba(58, 67, 77, 0.7)' }, }); </script> </body> </html>
很簡單就是把系統的WebView換成Tencent_Webview其餘相似
public class WebViewActivity extends AppCompatActivity { private com.tencent.smtt.sdk.WebView tencent_webview; private String url = "file:///android_asset/admin.html"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_view); initView(); } @SuppressLint("SetJavaScriptEnabled") private void initView() { tencent_webview = (WebView) findViewById(R.id.web); tencent_webview.loadUrl(url); WebSettings webSettings = tencent_webview.getSettings(); webSettings.setJavaScriptEnabled(true); tencent_webview.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } }); } }
最後附上插件的可配置參數:
panorama:必填參數,全景圖的路徑。 container:必填參數,放置全景圖的div元素。 autoload:可選,默認值爲true,true爲自動調用全景圖,false爲在後面加載全景圖(經過.load()方法)。 usexmpdata:可選,默認值爲true,若是Photo Sphere Viewer必須讀入XMP數據則爲true。 default_position:可選,默認值爲{},定義默認的位置,及用戶看見的第一個點,例如:{long: Math.PI, lat: Math.PI/2}。 min_fov:可選,默認值爲30,觀察的最小區域,單位degrees,在1-179之間。 max_fov:可選,默認值爲90,觀察的最大區域,單位degrees,在1-179之間。 allow_user_interactions:可選,默認值爲true,設置爲false則禁止用戶和全景圖交互(導航條不可用)。 tilt_up_max:可選,默認值爲Math.PI/2,向上傾斜的最大角度,單位radians。 tilt_down_max:可選,默認值爲Math.PI/2,向下傾斜的最大角度,單位radians。 zoom_level:可選,默認值爲0,默認的縮放級別,值在0-100之間。 long_offset:可選,默認值爲PI/360,mouse/touch移動時每像素通過的經度值。 lat_offset:可選,默認值爲PI/180,mouse/touch移動時每像素通過的緯度值。 time_anim:可選,默認值爲2000,全景圖在time_anim毫秒後會自動進行動畫。(設置爲false禁用它) theta_offset:過期的選項,可選,默認值爲1440,自動動畫時水平方向的速度。 anim_speed:可選,默認值爲2rpm,動畫的速度,每秒/分鐘多少radians/degrees/revolutions。 navbar:可選值,默認爲false。顯示導航條。 navbar_style:可選值,默認爲{}。導航條的自定義樣式。下面是可用的樣式列表: backgroundColor:導航條的背景顏色,默認值爲rgba(61, 61, 61, 0.5)。 buttonsColor:按鈕的前景顏色,默認值爲transparent。 activeButtonsBackgroundColor:按鈕激活狀態的背景顏色,默認值爲rgba(255, 255, 255, 0.1)。 buttonsHeight:按鈕的高度,單位像素,默認值爲20。 autorotateThickness:autorotate圖標的厚度,單位像素,默認值爲1。 zoomRangeWidth:縮放的範圍,單位顯示,默認值50。 zoomRangeThickness:縮放的範圍的厚度,單位像素,默認值1。 zoomRangeDisk:縮放範圍的圓盤直徑,單位像素,默認值爲7。 fullscreenRatio:全屏圖標的比例,默認值爲3/4。 fullscreenThickness:全屏圖標的厚度,單位像素,默認值爲2。 loading_msg:可選,默認值爲Loading…,圖片加載時的提示文字。 loading_img:可選,默認值爲null,在加載時顯示的圖片的路徑。 size:可選,默認值null,全景圖容器的最終尺寸。例如:{width: 500, height: 300}。 onready:可選值,默認值爲null。當全景圖準備就緒而且第一張圖片顯示時的回調函數。
三種方式都實現完了,不用擔憂今天全部代碼都會放在GitHub上。
三種方式具體你使用哪一種我仍是沒有推薦的
這裏只是一張圖,你能夠多張圖實現來完成簡單的全景街景功能!點擊圖片某個區域,跳轉到下一個街景的圖,包括百度地圖裏面也是一張張全景圖拼接而成。
- 第一種我會在後續繼續完善加入更多的可選參數,大家有興趣也能夠本身優化。
- 第二種是谷歌VR模塊的沒什麼好說的,畢竟官方倆字就夠了。
- 第三種跨平臺最好的,畢竟是個網頁。而咱們第三種使用了騰訊X5內核來玩,可是還能夠在優化,消耗不小,我建議你單獨給WebView分配一個進程和你的業務分離。
- 拿着個人保溫杯,泡一杯枸杞,咱們下篇文章再會
https://github.com/CN-ZPH/ 以爲不錯請點一個star蛤! 有問題下面留言評論,我看到會回覆。