简介:一份面向Android开发者的开源全景图库PanoramaGL项目资源包,用于在移动端实现360度沉浸式全景展示与交互,可应用于虚拟现实、旅游导航、室内设计等场景。压缩包共667个文件,大小15.16MB,其中以SVN版本控制元数据、Java源码、图像资源及XML配置为主,同时包含C/C++原生代码、动态库、JAR依赖及工程文件,便于直接导入研究与二次开发。已有605人浏览/学习,对开源类库研究具有一定参考热度。该库支持球形、立方体、圆柱体三类全景模式,基于OpenGL ES硬件加速,能够流畅渲染;集成重力感应与触摸手势控制,用户倾斜设备或双指缩放即可调整视角。包内附带示例图片、文字说明与构建脚本,可帮助开发者快速掌握核心接口,理解预处理、缓存策略与内存优化思路,进而定制属于自己的全景应用。
1. 为什么做全景展示不用自己写OpenGL:PanoramaGL这类库解决的不只是画球体
做房产看房App的时候,产品经理给我一张2:1的全景图,要求用户能手指拖动上下左右看,还要有惯性滑动。我第一反应是拿OpenGL ES自己画一个球体网格,把纹理贴上去。真动手才发现,问题全在细节:球体的经纬线密度怎么设,UV映射怎么对齐,手势滑动到边缘怎么处理。折腾了两天,球的旋转还带漂移。后来用上了PanoramaGL,一个把360度全景展示封装成Android类库的开源项目,才发现它把球形、立方体、圆柱三种投影都做成了现成的View,连触摸、缩放、加速度都内置了。这篇笔记就记一下我是怎么把这个类库接到工程里、调好参数、绕开各种坑的。适合那些要在Android上快速实现全景浏览、或者手头有现成全景图但不想从零写渲染器的开发者。
2. 类库与渲染管线:三种投影模型和View继承结构决定了你能做什么
2.1 三种投影模型:球形、立方体、圆柱,选错模型会直接歪
PanoramaGL 把全景展示拆成了三种几何体,分别对应三种纹理输入。最常用的是球形投影,对应PLSphericalView。它的原理是生成一个UV球的网格,把一张等距圆柱投影图(长宽比2:1)环绕映射到球体内部,摄像机放在球心,用户看到的任何方向都对应当前画面上的一块纹理。这种方式效果最自然,适合室外全景、街景、房间看房。球形网格的经线和纬线密度很关键,我一般在复制源码包后去看PLSphere的创建代码,里面的slices和stacks参数就是经线纬线数量。默认值可能只够展示,但如果你观察画面边缘有扭曲或闪烁,可以适当提高这两个值,不过代价是顶点数和内存都会变大。
立方体投影对应PLCubeView,是把六张面图贴在立方体内表面。它的好处是纹理本身没有畸变,边缘清晰,特别适合用美术资源做游戏场景或虚拟展厅。PanoramaGL在加载立方体时需要六张纹理分开传入,你可以从文件名上分辨前后左右上下。立方体验证的另一个细节是立方体网格的每条边都必须对齐到纹理边界,否则接缝处会出现一条明显的黑线。我见过有人拿单张全景图硬塞给PLCubeView,结果画面四分五裂,因为他没理解这个模型本身要求的是六张独立纹理。
圆柱投影对应PLCylinderView,只有水平360度,垂直方向视野受限,适合走廊、隧道这类不需要抬头低头看天看地的场景。圆柱投影的垂直视场角一般比球形窄,我通常会在需要展示天花板和地板时避开它,因为客户会吐槽抬头就穿帮。三种模型的复杂度排序是球形大于圆柱大于立方体,但这里说的复杂度不是渲染性能,而是纹理映射的直观程度。球形的好处是一张图就能表达所有方向,坏处是极地地区的纹理拉伸非常严重;立方体正好相反,每个面都平整,但你需要六张图且要管理好拼接缝隙。
选模型这件事没有绝对标准,但有一条经验:单张全景图优先球形,六张面图优先立方体,只需要看环视水平一圈就选圆柱。PanoramaGL的视图类名已经写得很清楚,你直接new对应的View就行,不需要关心内部的网格生成细节。如果你想在同一个页面切换三种模型,可以把三种View叠在一起,切换可见性,但要注意每个View都有自己的GLSurfaceView,叠加时会有OpenGL上下文冲突,我一般更倾向于销毁重建而不是叠加。下表梳理了我常用的选型场景,记不住的时候拿出来看一眼。
| 全景内容 | 推荐投影 | 对应View | 纹理数量 |
|---|---|---|---|
| 单张等距圆柱全景图 | 球形 | PLSphericalView | 1张2:1图 |
| 六面立方体图 | 立方体 | PLCubeView | 6张正方形图 |
| 水平环绕、垂直受限 | 圆柱 | PLCylinderView | 1张长条图 |
| 室内房间、无天花板需求 | 圆柱 | PLCylinderView | 1张环视图 |
2.2 从PLView到具体的视图:类库的继承结构和生命周期
PanoramaGL的所有全景视图都继承自PLView。PLView本身是一个封装好的GLSurfaceView实现,它负责创建OpenGL ES环境、管理绘图线程、把触摸事件分发给内部的PLScene。你可以在PLView里找到onPause和onResume方法,这两个必须跟随Activity的生命周期调用,否则GLThread不会释放,你退出页面后电量会掉得很快。PLSphericalView继承PLView之后,在内部创建了一个PLScene和一个PLCamera,scene承载网格模型,camera控制视角。这种设计很像我见过的其他3D框架,只不过PanoramaGL把大量配置项放在了私有变量里,你必须通过setter方法去改,而不是直接访问。
我一开始直接往RelativeLayout里塞了一个PLSphericalView,运行后发现Activity调onStop后画面还在转,后来才意识到onPause没生效。正确做法是在Activity的onPause和onResume里显式调用view.onPause()和view.onResume(),顺序不能反。如果你用Fragment,要放到Fragment的相应回调里,并且注意在视图被移除之前调用onPause,否则GLSurfaceView持有的EGLContext会迟迟不释放,下次进入页面时会黑屏几秒。还有一个细节是onPause里先调用view.onPause(),再调用super.onPause(),这样可以保证GL线程先停下,不会在Activity进入后台瞬间还在渲染。
类库的另一个关键是PLRenderer,它负责每一帧绘制。PanoramaGL默认提供的渲染器已经够用,但如果你想加滤镜或实现淡入淡出效果,就需要继承PLRenderer并重写drawScene方法。重写时要注意先调用super.drawScene,否则场景的模型变换丢失。我还试过直接渲染到继承的GLSurfaceView上,但每次屏幕旋转都会重建Surface,导致纹理丢失,所以不要轻易绕过PLRenderer。Renderer里的流程是:清除颜色缓冲和深度缓冲,设置相机矩阵,绘制场景中的网格对象,最后交换缓冲。PanoramaGL把这一套固定下来,你只需要关心纹理和相机,而不需要处理GL状态的入栈出栈。
生命周期的最容易踩的坑是,在Activity的onDestroy里没有释放纹理。虽然GLSurfaceView销毁时会释放GL资源,但PanoramaGL的Texture对象还持有Bitmap引用,如果你在onDestroy前做了bitmap.recycle,那问题不大,如果没做,就会出现短时内存峰值。我一般会在onDestroy里显式调用view.requestRender和view.onPause,然后把Texture对象置为null。这样做对系统内存回收更友好,也避免快速进出页面时出现一股强烈的内存抖动。
3. 在Android工程里集成PanoramaGL:Gradle配置与第一个全景页面
3.1 从源码包构建module而不是直接依赖jar
PanoramaGL的源码包下载下来之后,通常是一个Eclipse风格的Android项目结构,目录里只有一个AndroidManifest.xml和java源码包,没有Gradle脚本。你直接把这个目录拖进Android Studio是没法编译的。常见做法是在工程根目录的libs下建一个panoramagl文件夹,把src和AndroidManifest放进去,然后手动写一个build.gradle,声明成library module。我在实际集成时会这样操作:先下载源码包解压,把里面的src目录整个拷到新module的src/main/java下,把AndroidManifest.xml拷到src/main/下,然后在module的build.gradle里填上依赖Android支持库的声明,因为PanoramaGL的触摸处理用到了旧版support库的接口。
为了让主工程能引用它,需要在settings.gradle里include这个module,并指定项目路径。下面这段是settings.gradle的写法:
// settings.gradle rootProject.name = "PanoDemo" include ':app' include ':panoramagl' project(':panoramagl').projectDir = new File('libs/panoramagl')app的build.gradle加上:
dependencies { implementation project(':panoramagl') }这段的逻辑是告诉Gradle把libs/panoramagl作为一个独立编译的library工程,而不是当成资源目录。参数说明里,projectDir这行的作用是把module路径指到源码包所在位置,如果你的源码包解压在别的目录,把它改成相对路径或绝对路径都行,但建议用相对路径,方便同事在同样环境里拉代码。注意include和projectDir要配对出现,如果你只写了include而忘掉projectDir,Gradle会默认去根目录下的panoramagl文件夹找,找不到就报错。
这里有一个容易出错的地方:PanoramaGL的源码里可能引用到了旧版android.support.v4的类,如果你的项目用了AndroidX,Gradle编译时会告诉你找不到android.support.annotation一类错误。解决方法是给panoramagl模块的build.gradle显式添加android.useAndroidX=true,或者干脆把源码里的support依赖改成androidx。我一般选择后者,因为新工程几乎都走AndroidX,强制兼容旧库会引出更多问题。如果你不想改源码,也可以在panoramagl的build.gradle里加上implementation 'androidx.appcompat:appcompat:1.0.0',利用依赖传递把support库带进来,但这样容易和主工程的其他依赖冲突。
源码包里的res目录如果不完整,可能在编译时出现资源找不到。PanoramaGL大部分资源是ID,直接使用代码生成,所以一般问题不大,但如果你看到R文件报错,建议去检查模块的src/main/res是否存在。实在不行,可以只保留android manifest里的package声明,把res目录清空,PanoramaGL本身没有太多必须的资源。我在一次移植中还遇到一个奇怪问题:模块编译成功,但运行时提示PLView类不存在。仔细看发现是proguard把类混淆掉了,开发环境没问题,release包必现。这个问题后面在避坑章会专门说。
3.2 布局中放置全景View并加载一张远端全景图
模块编译通过后,就可以在页面里放全景视图了。PanoramaGL的视图本身就是View,你可以在XML里声明,也可以在代码里动态创建。我在XML里用了一个RelativeLayout,把全景View放到底层,上面再叠加一个加载进度条,避免图片加载时白屏让用户以为卡死。布局文件看起来是这样的:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <net.glview.gllayer.PLView android:id="@+id/pano_view" android:layout_width="match_parent" android:layout_height="match_parent" /> <ProgressBar android:id="@+id/loading" android:layout_centerInParent="true" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout>注意布局里PLView的完整类名要跟源码包里的包名一致,我记得在PanoramaGL源码中主包名是net.glview.gllayer,但不同版本可能有差异。你下载下来之后先去源码根目录确认一下包名,再填入布局,否则编译直接报找不到类。在Activity里,拿到PLView对象后要先构造好纹理对象,再加载图片。代码如下:
public class PanoActivity extends Activity { private PLView panoView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 设置全屏,沉浸式显示效果更好 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.activity_pano); panoView = findViewById(R.id.pano_view); // 加载assets目录下的pano.jpg,也可以从网络加载后写入缓存 Bitmap bitmap = loadBitmapFromAssets("pano.jpg"); Texture texture = PLTextureLoader.loadTexture(bitmap); panoView.setTexture(texture); panoView.setTouchEnabled(true); } private Bitmap loadBitmapFromAssets(String fileName) { try { InputStream is = getAssets().open(fileName); // 用inSampleSize控制采样率,避免图片过大导致OOM BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, options); options.inSampleSize = calculateSampleSize(options.outWidth, options.outHeight); is.close(); is = getAssets().open(fileName); options.inJustDecodeBounds = false; return BitmapFactory.decodeStream(is, null, options); } catch (IOException e) { return null; } } private int calculateSampleSize(int width, int height) { int sample = 1; while (width / sample > 4096 || height / sample > 4096) { sample *= 2; } return sample; } }这里关键的一行是PLTextureLoader.loadTexture,它会把Bitmap复制到OpenGL纹理里,之后Bitmap就可以回收了。参数说明里,setTexture会触发PLView重新渲染,所以一定要在setTouchEnabled之前调用,避免用户在纹理还没就绪时就操作手势,产生黑屏或崩溃。calculateSampleSize是为了让图片最大边不超过4096像素,这个数值对应大多数Android设备的GL_MAX_TEXTURE_SIZE,如果超过会直接报纹理尺寸错误。注意这里的loadBitmapFromAssets返回的Bitmap有可能是null,后面setTexture之前一定要判空,否则空指针是跑不掉的。
加载网络图片时,我一般会先用一个异步任务下载到本地临时文件,再复用上面的bitmap加载逻辑,这样比直接传入流更可控。PanoramaGL没有内置网络加载能力,它只负责纹理上传和渲染,所以把图片解码和缓存这层逻辑放在外面是合理的。下面是一个AsyncTask的简化写法,它把下载和解码放到后台线程,避免阻塞主线程:
private class DownloadPanoTask extends AsyncTask<String, Void, Bitmap> { @Override protected Bitmap doInBackground(String... urls) { try { URL url = new URL(urls[0]); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); InputStream is = conn.getInputStream(); File cacheFile = new File(getCacheDir(), "pano_cache.jpg"); FileOutputStream fos = new FileOutputStream(cacheFile); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.close(); is.close(); return loadBitmapFromFile(cacheFile); } catch (IOException e) { return null; } } @Override protected void onPostExecute(Bitmap bitmap) { if (bitmap != null) { Texture texture = PLTextureLoader.loadTexture(bitmap); panoView.setTexture(texture); panoView.setTouchEnabled(true); } } }这段代码里,downloadPanoTask.execute(url)后会在后台完成下载,onPostExecute回到主线程设置纹理。注意在doInBackground里不能碰PLView,因为OpenGL的EGLContext是绑定在GLSurfaceView所在线程的,后台线程调用setTexture会直接崩溃。这个坑在接网络图时非常容易踩,一定要把纹理操作放到UI线程的onPostExecute里,或者放到PanoramaGL提供的GL队列中。如果你的图片URL需要带cookie验证,也不要忘记在HttpURLConnection里手动加请求头。
4. 手势参数与场景控制:把视角转换做得像手机原生地图一样顺滑
4.1 触摸事件处理与加速度参数
PanoramaGL内置了触摸监听,拖动时改变相机朝向,双指缩放改变视场角。默认行为能跑,但体验很生硬。我拿到源码包后,第一件事就是去找触摸处理的实现,通常在PLTouchHandler的touchProcess或者类似的方法里。那里有一个旋转速度系数和惯性衰减系数,直接决定手指滑动全景时是轻飘飘还是顿挫感强。这两个系数不是对外暴露的参数,需要你在源码里改。我一般在PLTouchHandler的onTouchEvent里打印出当前的event值,反复试几次找到顺手的手感。
我一般会在不改变原逻辑的情况下,把旋转增量乘以一个0.8左右的系数,让视图跟随手指更紧,而不是滑动结束后画面还在自己转。PanoramaGL的惯性算法问题不大,但衰减值偏低,松手后能滑出去老远,在展示场景里容易让用户眩晕。我习惯把衰减系数调大一点,比如把每次位移乘以0.92,让它在不到半秒内停住。这里没有标准值,你可以在onTouch事件里打印出速度来调试,看多少更舒服。如果项目里包含实时性能监控,你可以把触摸事件的响应延时也记录下来,正常情况下手指移动和画面更新不应超过16毫秒。
如果你用的是PLView基类,触摸翻转的方向还受设备屏幕坐标和世界坐标的差异影响。有的开发者反馈上下滑动时画面反了,这通常是因为相机up向量指向错了。PanoramaGL里可以通过setUpVector或setUserVector调整,但不同版本API不一样。我经常在源码里直接修改PLCamera的upX/upY/upZ,让Y轴朝上,这样上下拖动才会符合直觉。如果你发现左右反了,就把左右移动方向取反。在修改之前,先确认设备自然方向是竖屏还是横屏,因为屏幕坐标的Y轴在横竖屏之间会互换,这也是很多人说"代码一模一样的,横屏就反了"的根本原因。
触摸缩放方面,PanoramaGL的默认逻辑可能有限制,比如最小视场角被锁定在20度,最大视场角在90度左右。如果你想放大到某个细节区域,需要放宽最小值限制。在源码里找camera.setFieldOfView的处理逻辑,看看它是否对传入参数做了clamp。一旦你放开了限制,要同时防止用户缩放到太近导致画面翻转,因为视场角过小时相机可能穿过球体背面。我建议把最小视场角设为10度,最大设为100度,这样既能看清细节,又不会穿帮。
4.2 视场角、近远裁剪面与初始朝向
视场角是全景体验最核心的参数,它决定了用户能看到多大的视野,也间接影响了陀螺仪场景下的沉浸感。PanoramaGL的PLView提供了setFieldOfView方法,默认值通常在60到90度之间,但我用下来感觉60度像望远镜,90度又让边缘畸变明显。我在手机竖屏上习惯用70度,横屏上75度,这样既能看到足够大的内容,又不会太夸张。视场角的单位是度,不是弧度,直接传数字就可以。
近远裁剪面方面,球体半径是固定的,所以near和far参数不是那么关键,但如果遇到球体被裁切或看不到纹理,可以去检查camera的setNear和setFar。近裁剪面太大会把球体局部切掉,远裁剪面太小会把整个球体裁没。我一般把near设成0.1,far设成10,把球体半径设为1,这样肯定在范围内。你可能会问,为什么球体半径是1?因为PanoramaGL的内部坐标系默认以1为基准,相机位于原点,球体表面在距离原点1的位置,near小于1才能看到球体,far大于1才能让整个球体进入可视范围。
初始朝向则决定了App打开后第一眼看到的方向,PanoramaGL提供了setLookAt或setRotation方法,我通常在onStart回调里把相机旋转到正前方,避免用户进入时面对一个天空或黑边。如果你做的是房间展示,正前方应该是对着客厅窗户,不是一面白墙。改这些参数都需要在GL线程里进行,所以我在代码里会先拿到PLCamera对象,再做修改。示例:
PLCamera camera = panoView.getCamera(); camera.setFieldOfView(70); camera.setNear(0.1f); camera.setFar(10.0f); // 让相机看向(0, 0, -1)方向,也就是场景正面 camera.setLookAt(0, 0, -1, 0, 1, 0);这些调用的逻辑是告诉OpenGL渲染器把观察点放在球心,朝向Z轴负方向,up向量为Y轴正方向。参数说明里,fieldOfView越大,视野越开阔,但边缘拉伸越严重。near和far决定了深度存储的精度,在手机上没必要设太大范围。setLookAt可能在不同版本里参数类型不同,有的用三个点组,有的用连续参数,编译时报错的话去搜一下源码里有没有lookAt相关方法。注意setLookAt的最后三个参数是up向量,不是眼睛位置,我第一次传入时把它当成了目标点,结果画面直接朝天。
我自己的习惯是把这些相机设置封装成一个resetCamera方法,在页面创建和用户点击复位按钮时调用,这样用户迷失方向后能一键回到默认视角。这个习惯在接入陀螺仪后尤其有用,因为传感器数据经常会漂移,复位按钮就是后悔药。另外,如果你在一个Activity里只创建一个PLView,可以在onResume里把相机的fov和lookAt重新设置一遍,防止系统在应用切后台时把GL状态给回收掉。这样即使EGLContext重新创建,画面也能立即恢复到用户熟悉的视角。
5. 避坑指南:从黑屏、OOM到触摸失效的五个常见问题
5.1 纹理与GL线程生命周期:黑屏、白屏、OOM其实都跟这条链有关
现象1:布局加载后画面一片黑。 原因:PLView的默认背景是黑色,纹理没有成功上传,或者Bitmap为null。解决:在setTexture之前检查Bitmap是否非空,并把纹理加载过程放到主线程,不要放到后台线程。如果Bitmap正常但画面仍黑,用logcat看GLSurfaceView有没有报EGL_BAD_CONFIG,多半是OpenGL版本不匹配。
现象2:退回到上一个页面再进入时白屏。 原因:GLSurfaceView的EGLContext已经销毁,但Texture对象还引用着旧纹理ID。解决:在Activity的onResume里重新调用setTexture,而不是只在onCreate里设置。因为onCreate只在页面首次创建时执行,之后的返回页面走的是onResume。
现象3:加载高分辨率全景图时OOM崩溃。 原因:一张8000x4000的RGBA8888图,裸大小128MB,直接解码必炸。解决:用BitmapFactory.Options的inSampleSize做采样,把图片最长边压到4096以内。同时改用RGB565格式,因为全景展示对色彩精度不敏感,省一半内存。在纹理上传成功后,立即调用bitmap.recycle(),把Java层的Bitmap内存释放掉。下面的代码演示了如何通过inPreferredConfig降低内存占用:
BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inPreferredConfig = Bitmap.Config.RGB_565; opts.inSampleSize = calculateSampleSize(width, height); Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts);这段代码的逻辑是告诉解码器用RGB565编码,并对超过4096的边做降采样。参数里inSampleSize必须是2的幂,如果计算出来的值是3,解码器会四舍五入到最近的2的幂。你可以在decode回来后再检查bitmap的宽度高度,如果仍然超过GL_MAX_TEXTURE_SIZE,就把采样率增高一倍重新解码。许多全景图工具输出的PNG不带alpha通道,用RGB565毫无影响,但如果你要用透明全景,还是得保留RGBA8888。
5.2 手势、混淆与事件冲突:看似正常的设置会在Release包上翻车
现象1:触摸事件能触发,但方向反了或惯性太夸张。 原因:默认的衰减系数偏低,每个设备触摸采样率也不一样。解决:在PLTouchHandler源码里把速度衰减从0.85调到0.94附近,把旋转增量乘一个0.8的系数。如果方向反了,检查PLCamera的up向量,确保Y轴朝上,并把相机的roll归零。我常用一个简单办法:把屏幕dx为负但相机yaw变大的场景反转,加一个负号即可。
现象2:Debug包一切正常,Release包一进全景页就crash。 原因:混淆器把PanoramaGL的类名和成员名改掉了,GameActivity在运行时找不到PLView。解决:在proguard-rules.pro里添加下述规则,把net.glview包名全部保留:
-keep class net.glview.gllayer.** { *; } -dontwarn net.glview.gllayer.**这段规则说明里,-keep让指定包下所有类及其成员保持原样,dontwarn是为了忽略该包内可能存在的不被支持的引用。参数这里其实不需要调整,直接复制即可。如果你还用到PLUtils等辅助类,建议把整个包名都锁住,不要只做部分保留。我在项目里就遇到过只keep了PLView,但崩溃在PLTextureLoader的情况,后来干脆整包保留。
现象3:页面里加了按钮,点按钮的同时全景画面会抖动或按钮点不中。 原因:PLView的onTouchEvent消费了所有触摸事件,没有把事件传递下去。解决:把按钮放到全景View的上层,而不是与它重叠。如果必须重叠,就在PLView的dispatchTouchEvent里做事件分发,命中按钮时返回false。我一般用全屏容器放按钮并让容器可点击,这样PLView只能收到未被处理的事件。注意,如果PLView设置了setTouchEnabled(true),它的onTouchEvent默认返回true,这会阻止ViewGroup继续派发事件。你可以先关闭触摸,然后在Activity里通过setOnTouchListener自己处理手势,这样可以把控制权完全拿回来。
6. 进阶技巧:用传感器融合消除漂移,并验证全景画面的畸变效果
6.1 接入陀螺仪让用户"转头看全景"
PanoramaGL本身不处理传感器,但你可以把SensorManager的旋转矢量数据映射到相机朝向。常见做法是在onSensorChanged里拿到旋转矩阵的orientation角度值,再把roll/pitch直接赋给相机的旋转向量,最后调用panoView.setNeedsDisplay触发重绘。代码大致是:
@Override public void onSensorChanged(SensorEvent event) { SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values); SensorManager.getOrientation(rotationMatrix, orientation); // orientation[0]是方位角, [1]是俯仰角, [2]是翻滚角 camera.setRotation(-orientation[0], -orientation[1], 0); panoView.setNeedsDisplay(); }这段代码你需要注册SensorManager.TYPE_ROTATION_VECTOR监听器,并在Activity的onResume里开始监听,onPause里停止监听。注意传感器数值范围是-π到π,而PanoramaGL的旋转向量单位需要的是弧度,所以不需要额外换算。如果画面朝向跟手机转动方向相反,就把负号调整成相反方向。接入传感器之后,手势拖动和传感器会抢相机控制权,我的习惯是拖动时暂时注销传感器监听,松手后再恢复,这样不会出现你推左转它右转的问题。你可以用一个boolean变量控制,在PLView的onTouchEvent里置为false,在action_up里置为true。
6.2 用一张测试网格图判断纹理映射和镜头畸变
在集成的最后阶段,我会生成一张带网格和色块的全景测试图,用来判断纹理映射是否正常。你可以用一张2:1的图片,在左边画一条红色竖线,右边画一条蓝色竖线,加载到球形视图里,然后旋转到正前方,看看红线和蓝线是不是分别出现在左右两端。如果有扭曲,说明球体经线密度不够或UV映射方向错了。这个方法比反复看实拍图高效得多,因为实拍场景你记不住边缘细节。每改一次参数,就用测试图固定视角截图对比。
我还会用九宫格覆盖测试图,把图片均匀分为九个区域,每个区域填充不同颜色,加载后在屏幕上检查颜色顺序是否正确。如果相邻区域颜色渐变,说明UV坐标有偏移,这时候去源码里找PLTexture的tile和mirror设置。PanoramaGL默认的纹理坐标范围是0到1,如果你的全景图是拼接过的,可能需要把水平偏移参数调整到0.5或其他值。你可以用setTextureOffset来调整,但不同版本的实现可能不同,建议在Texture类里搜一下offset相关字段。
从那以后,我每次集成全景库都会先跑一遍测试图,确认纹理、朝向、视场角都没问题,再丢给美术出正式素材。这个习惯帮我避开了至少三次因为全景图尺寸过大而导致的纹理黑屏问题。希望帮到你。
本文还有配套的精品资源,点击获取