Android OpenGLES 360全景图片渲染(球体内部)

概述

360度全景图是一种虚拟现实技术,它通过对现实场景进行多角度拍摄后,利用计算机软件将这些照片拼接成一个完整的全景图像。这种技术能够让观看者在虚拟环境中以交互的方式查看整个周围环境,就好像他们真的站在那个位置一样。在Android设备上查看360度全景图片,可以使用一些专门的app, 不如Google相册, Google 街景, 第三方的全景图片查看应用。这些应用程序能够识别并以交互方式展示360度全景图像,让用户可以旋转、缩放和平移来探索整个场景。

360全景图片渲染可以使用openGLES来轻松实现.

实现


使用OpenGL 创建一个球体, 并将图片纹理按一定的规则贴到球体的内部. 如上图, 可以考虑将图片等比例铺满球面展开的面积即可, 方法有很多, 可以按经度, 也可以按维度, 不同顺序对结果没影响.

参考代码:

主界面

java 复制代码
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import com.android.apitester.SphereView;

public class SphereViewer extends Activity {
    SphereView sphereView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bitmap bm = BitmapFactory.decodeFile("/sdcard/sphere.jpg");
        sphereView = new SphereView(this);
        sphereView.setBitmap(bm);
        setContentView(sphereView);
    }
}

自定义GLSurfaceView

java 复制代码
import android.content.Context;
import android.graphics.Bitmap;
import android.hardware.SensorManager;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;

import com.ansondroider.acore.Logger;
import com.ansondroider.acore.opengl.EGLHelper;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class SphereView  extends GLSurfaceView implements GLSurfaceView.Renderer {
    final String TAG = "SphereView";
    private String vertextShaderSource = "attribute vec4 aPosition;" +
            "precision mediump float;" +
            "uniform mat4 uMatrix;" +
            "attribute vec2 aCoordinate;" +
            "varying vec2 vCoordinate;" +
            "attribute float alpha;" +
            "varying float inAlpha;" +
            "void main(){" +
            "	gl_Position = uMatrix*aPosition;\n" +
            //"	gl_Position = aPosition;" +
            " 	gl_PointSize = 10.0;" +
            "	vCoordinate = aCoordinate;" +
            "	inAlpha = alpha;" +
            "}";
    private String fragmentShaderSource = "#extension GL_OES_EGL_image_external : require\n" +
            "precision mediump float;" +
            "varying vec2 vCoordinate;" +
            "varying float inAlpha;" +
            //"uniform samplerExternalOES uTexture;" +
            "uniform sampler2D uTexture;" +
            "uniform bool isPoint;" +
            "void main() {" +
            "	vec4 color = texture2D(uTexture, vCoordinate);" +
            "	if(isPoint){" +
            " 	   gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
            "	}else{" +
            "		gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
            "	}" +
            "}";

    //纹理ID
    private int mTextureId = -1;
    // 定义OpenGL 程序ID
    private int mProgram = -1;
    //矩阵变换接受者(shader中)
    private int mVertexMatrixHandler = -1;
    //顶点坐标接收者
    private int mVertexPosHandler = -1;
    //纹理坐标接受者
    private int mTexturePosHandler = -1;
    //纹理接受者
    private int mTextureHandler = -1;
    //半透明值接受者
    private int mAlphaHandler = -1;
    private int mPointHandler = -1;
    //矩阵
    private float[] mMatrix = new float[16];
    private float[] projectionMatrix = new float[16];
    //透明度
    private float mAlpha = 1f;
    public SphereView(Context context) {
        super(context);
        //A        #16 pc 000000000000d348  [anon:dalvik-classes2.dex extracted in memory from /data/app/~~Kkld4fhSS0RUjHOLDPgB7w==/com.ansondroider.apitester-9-2nRYL85FvddCKlwbFEFw==/base.apk!classes2.dex] (com.ansondroider.apitester.sphere.SphereView.createGLPrg+36)
        //A        #18 pc 000000000000d74a  [anon:dalvik-classes2.dex extracted in memory from /data/app/~~Kkld4fhSS0RUjHOLDPgB7w==/com.ansondroider.apitester-9-2nRYL85FvddCKlwbFEFw==/base.apk!classes2.dex] (com.ansondroider.apitester.sphere.SphereView.onDrawFrame+138)
        setEGLContextClientVersion(2);
        setRenderer(this);
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                updateRotate(event);
                return true;
            }
        });
    }

    //if touching, stop sensor rotate.
    boolean touching;
    float dx, dy;
    float angStartX, angStartY;
    float angelTouchX, angelTouchY;
    public void updateRotate(MotionEvent e){
        if(e.getAction() == MotionEvent.ACTION_DOWN){
            touching = true;
            dx = e.getX();
            dy = e.getY();
            //float[] rot = {0, 0, 0};
            //provider.getAngle(rot);
            angStartX = angelTouchY;//rot[0];
            angStartY = angelTouchX;//rot[1];
        }else if(e.getAction() == MotionEvent.ACTION_MOVE){
            angelTouchY = angStartX + 180f * (e.getX() - dx) / getWidth();
            angelTouchX = angStartY + 180f * (e.getY() - dy) / getHeight();
        }else{
            touching = false;
        }
    }

    Bitmap tmpBm = null;
    boolean rebindTexture = false;
    public void setBitmap(Bitmap bm){
        //check mTextureId is -1, save bm to tmpBm;
        if(bm == null || bm.isRecycled())return;
        else tmpBm = bm;

        if(mTextureId >= 0){
            rebindTexture = true;
        }
    }

    float ratio = 1;
    //float frustumSize 	= 2.5f;
    float[] NEAR_FAR 	= {1.7f,	0.5f, 	20f, 	17f};
    float[] EYE_Z 		= {-0.06f, 	-3f, 	3};
    float[] SCALE 		= {5f, 		0.5f, 	10};
    float[] FRUSTUM 	= {1, 		1, 		3};
    float[] RADIUS 		= {2.5f, 	2, 		3};

    /**
     * 初始化矩阵变换,主要是防止视频拉伸变形
     */
    private void initDefMatrix() {
        //Log.d(TAG, "initDefMatrix");
        //设置相机位置
        float[] viewMatrix = new float[16];
        Matrix.setLookAtM(
                viewMatrix, 0,
                0f, 0f, EYE_Z[0],
                0f, 0f, 0,
                0f, 1.0f, 0f
        );

        float[] rotSensor = new float[16];
        Matrix.setIdentityM(rotSensor, 0);
        float[] rotTouch = new float[16];
        Matrix.setIdentityM(rotTouch, 0);
        Matrix.rotateM(rotTouch, 0, -angelTouchX, 1, 0, 0);
        float rotY = -angelTouchY + (vtSphere != null ? vtSphere.getStartRotateY() : 0);
        Matrix.rotateM(rotTouch, 0, rotY, 0, 1, 0);
        Matrix.multiplyMM(viewMatrix, 0, rotSensor, 0, rotTouch, 0);


        // 参数解释:
        //result: 存储乘法结果的矩阵数组。
        //resultOffset: 存储结果矩阵的数组起始偏移量。
        //lhs: 左操作数矩阵(left-hand side)的数组。
        //lhsOffset: 左操作数矩阵的数组起始偏移量。
        //rhs: 右操作数矩阵(right-hand side)的数组。
        //rhsOffset: 右操作数矩阵的数组起始偏移量。
        //Matrix.multiplyMM(mMatrix, 0, projectionMatrix, 0, viewMatrix, 0);

        Matrix.multiplyMM(mMatrix, 0, projectionMatrix, 0, viewMatrix, 0);

        Matrix.scaleM(mMatrix, 0, SCALE[0], SCALE[0], SCALE[0]);

    }

    @Override
    public void setAlpha(float alpha) {
        super.setAlpha(alpha);
        mAlpha = alpha;
    }

    /**
     * 创建并使用opengles程序
     */
    private void createGLPrg() {
        //Logger.d(TAG, "createGLPrg");
        if (mProgram == -1) {
            int vertexShader = EGLHelper.compileVertexShader(vertextShaderSource);
            int fragmentShader = EGLHelper.compileFragmentShader(fragmentShaderSource);
            //创建programe陈谷
            mProgram = GLES20.glCreateProgram();
            //将顶点着色器加入程序
            GLES20.glAttachShader(mProgram, vertexShader);
            //将片元着色器加入程序
            GLES20.glAttachShader(mProgram, fragmentShader);
            GLES20.glLinkProgram(mProgram);
            //从程序中获取句柄
            mVertexMatrixHandler = GLES20.glGetUniformLocation(mProgram, "uMatrix");
            mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition");
            mTextureHandler = GLES20.glGetUniformLocation(mProgram, "uTexture");
            mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate");
            mAlphaHandler = GLES20.glGetAttribLocation(mProgram, "alpha");
            mPointHandler = GLES20.glGetUniformLocation(mProgram, "isPoint");
        }
        //使用opengl程序
        if (mProgram != -1) GLES20.glUseProgram(mProgram);

    }

    @Override
    protected void onDetachedFromWindow() {
        Logger.d(TAG, "onDetachedFromWindow");
        super.onDetachedFromWindow();
        GLES20.glDisableVertexAttribArray(mVertexPosHandler);
        GLES20.glDisableVertexAttribArray(mTexturePosHandler);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
        GLES20.glDeleteTextures(1, new int[]{mTextureId}, 0);
        GLES20.glDeleteProgram(mProgram);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Logger.d(TAG, "onSurfaceCreated");
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Logger.d(TAG, "onSurfaceChanged");
        GLES20.glViewport(0, 0, width, height);
        ratio = (float) width / height;

        //m: 存储结果矩阵的数组。
        //offset: 存储结果矩阵的数组起始偏移量。
        //left	: 近平面左边界的 X 坐标值。
        //right	: 近平面右边界的 X 坐标值。
        //bottom: 近平面下边界的 Y 坐标值。
        //top	: 近平面上边界的 Y 坐标值。
        //near	: 近平面的 Z 坐标值(必须为正数)。
        //far	: 远平面的 Z 坐标值(必须为正数)。
        Matrix.frustumM(projectionMatrix, 0,
                -ratio*FRUSTUM[0], ratio*FRUSTUM[0],
                -FRUSTUM[0], FRUSTUM[0],
                NEAR_FAR[0], NEAR_FAR[3]);
    }

    void prepareTexture(){
        if((mTextureId < 0 || rebindTexture) && tmpBm != null){
            Logger.d(TAG, "setBitmap bind bitmap to texture " + mTextureId);
            if(mTextureId >= 0) {
                //check texture is bind, unbind.
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
                GLES20.glDeleteTextures(1, new int[]{mTextureId}, 0);
            }
            //create texture and bind bitmap to it
            int[] textures = new int[1];
            GLES20.glGenTextures(1, textures, 0);
            mTextureId = textures[0];
            rebindTexture = false;


            //mTextureId is not -1, then bind Bitmap to mTextureId
            // 将 Bitmap 加载到纹理
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
            // 设置纹理参数
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
            GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, tmpBm, 0);
            tmpBm.recycle(); // 释放 Bitmap 资源
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        //Logger.d(TAG, "onDrawFrame");
        GLES20.glClearColor(bgRGBA[0], bgRGBA[1], bgRGBA[2], bgRGBA[3]);
        long ct = SystemClock.uptimeMillis();
        if(ct - last_fps > 1000) {
            last_fps = ct;
            fps = 0;
        }
        fps ++;
        prepareTexture();
        //drawBitmap_initBuffer();
        //清除颜色缓冲和深度缓冲
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        ///GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        if (mTextureId != -1) {
            initDefMatrix();
            //2/创建、编译、启动opengles着色器
            createGLPrg();
            //3.激活并绑定纹理单元
            ///activateTexture();
            //5.开始绘制渲染
            doDraw();
        }
    }

    float[] bgRGBA = {0, 0, 0, 1};
    int fps = 0;
    long last_fps = SystemClock.uptimeMillis();

    /**
     * 开始绘制渲染
     */
    SphereTexture vtSphere;
    public void doDraw() {
        //Logger.d(TAG, "doDraw");
        if(mMatrix == null)return;
        if(vtSphere == null){
            vtSphere = new SphereTexture(RADIUS[0], 32, 32,
                    false);
        }

        // 绑定纹理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
        GLES20.glUniform1i(mTextureHandler, 0);

        GLES20.glUniformMatrix4fv(mVertexMatrixHandler, 1, false, mMatrix, 0);
        GLES20.glVertexAttrib1f(mAlphaHandler, mAlpha);
        GLES20.glUniform1i(mPointHandler, vtSphere.isPoint() ? 1 : 0);
        vtSphere.draw(mVertexPosHandler, mTexturePosHandler);
    }
    
}//Class SphereView end

球面顶点和纹理坐标的生成与渲染

java 复制代码
import android.graphics.RectF;
import android.opengl.GLES20;
import android.opengl.Matrix;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

public class SphereTexture extends VertexTexture{
    private float mRadius;
    private int mLat, mLong;
    private boolean mIs180;

    private float[] degreeLongitude;
    private float[] degreeLatitude;
    public SphereTexture(float radius, int latitudeBands, int longitudeBands,
                         boolean is180) {
        mRadius = radius;
        mLat = latitudeBands;
        mLong = longitudeBands;
        mIs180 = is180;
        init();
    }

    @Override
    public float getStartRotateY() {
        return 0;//mIs180 ? 180f: -90f;
    }

    @Override
    protected void initVertexTextureCoordinate(){
        if(degreeLongitude == null) {
            degreeLongitude = new float[]{-_PI, 	_PI, 	_PI * 2};
            degreeLatitude 	= new float[]{-0, 		_PI, 	_PI};
        }

        mVertexCoors = new float[(mLat + 1) * (mLong + 1) * 3];
        mTextureCoors = new float[(mLat + 1) * (mLong + 1) * 2];
        mIndices = new short[mLat * mLong * 6];

        int vIndex = 0;
        int tIndex = 0;
        int iIndex = 0;
        for (int lat = 0; lat <= mLat; lat++) {
            float theta = degreeLatitude[0] +  (float) lat / mLat * degreeLatitude[2];
            float sinTheta = (float) Math.sin(theta);
            float cosTheta = (float) Math.cos(theta);

            for (int lon = 0; lon <= mLong; lon++) {
                float phi = degreeLongitude[0] + (float) lon / mLong * degreeLongitude[2];
                //float phi =  (float) lon / longitudeBands * 2 * (MAX_DEGREE_X);
                float sinPhi = (float) Math.sin(phi);
                float cosPhi = (float) Math.cos(phi);

                float x = cosPhi * sinTheta;
                float y = cosTheta;
                float z = sinPhi * sinTheta;

                mVertexCoors[vIndex] = mRadius * x;
                mVertexCoors[vIndex + 1] = mRadius * y;
                mVertexCoors[vIndex + 2] = mRadius * z;

                // 生成纹理坐标

                float u = (float) lon / mLong;
                float v = (float) lat / mLat;
                // 生成纹理坐标
				/*mTextureCoors[tIndex] = u;//x
				mTextureCoors[tIndex + 1] = v;//y*/
                RectF r = getTextureArea();
                mTextureCoors[tIndex] = r.left + r.width() * u;//x
                mTextureCoors[tIndex + 1] = r.top + r.height() * v;//y

                if (lat < mLat && lon < mLong) {
                    int first = lat * (mLong + 1) + lon;
                    int second = first + mLong + 1;

                    mIndices[iIndex++] = (short) first;
                    mIndices[iIndex++] = (short) second;
                    mIndices[iIndex++] = (short) (first + 1);

                    mIndices[iIndex++] = (short) second;
                    mIndices[iIndex++] = (short) (second + 1);
                    mIndices[iIndex++] = (short) (first + 1);


						/*
							first		first + 1		...
								|----------|-------------|
								|		   |			 |
								|		   |			 |
						  second|----------|-------------|
						  		|       second + 1       |
						  		|          |             |
						  		|          |             |
						  		|----------|-------------|
						 */
                }

                vIndex += 3;
                tIndex += 2;
            }
        }
    }

    @Override
    public void updateZ(float z) {
        super.updateZ(z);
        mRadius = z;
        initVertexTextureCoordinate();
    }

    @Override
    public void draw(int mVertexPosHandler, int mTexturePosHandler) {
        // 绑定顶点缓冲区
        GLES20.glEnableVertexAttribArray(mVertexPosHandler);
        mVertexBuffer.position(0);
        GLES20.glVertexAttribPointer(mVertexPosHandler, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer);


        // 绑定纹理坐标缓冲区
        GLES20.glEnableVertexAttribArray(mTexturePosHandler);
        mTextureBuffer.position(0);
        GLES20.glVertexAttribPointer(mTexturePosHandler, 2, GLES20.GL_FLOAT, false, 0, mTextureBuffer);

        // 绑定纹理
        //if(textureId > -1)GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

        // 绘制球体
        if(!isPoint()) {
            GLES20.glDrawElements(GLES20.GL_TRIANGLES, getIndexCount(),
                    GLES20.GL_UNSIGNED_SHORT, mIndexBuffer);
        }else
            GLES20.glDrawArrays(GLES20.GL_POINTS, 0, getVertexCount());

        GLES20.glDisableVertexAttribArray(mVertexPosHandler);
        GLES20.glDisableVertexAttribArray(mTexturePosHandler);
    }

}

最终效果:

参考

  1. Android OpenGLES3绘图:球形视频播放器
  2. Android 使用 OpenGL ES 绘制球面
相关推荐
非凡ghost26 分钟前
LSPatch官方版:无Root Xposed框架,自由定制手机体验
android·智能手机·软件需求
_extraordinary_27 分钟前
MySQL 库的操作 -- 增删改查,备份和恢复,系统编码
android·mysql·oracle
西瓜本瓜@3 小时前
在Android中如何使用Protobuf上传协议
android·java·开发语言·git·学习·android-studio
似霰6 小时前
安卓adb shell串口基础指令
android·adb
fatiaozhang95278 小时前
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
android·adb·电视盒子·魔百盒刷机·魔百盒固件
CYRUS_STUDIO9 小时前
Android APP 热修复原理
android·app·hotfix
鸿蒙布道师10 小时前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师10 小时前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
大耳猫10 小时前
【解决】Android Gradle Sync 报错 Could not read workspace metadata
android·gradle·android studio
ta叫我小白10 小时前
实现 Android 图片信息获取和 EXIF 坐标解析
android·exif·经纬度