Android 使用OpenGLES制作天空盒

我们都知道,像Unity3D等游戏引擎都有许多天空盒的资源,通过天空盒,我们能够制作许多精美的场景,比如我们玩游戏的背景,跟随玩家的视点的转动而转动。这样的场景我们再熟悉不过了。那么,下面开始介绍使用OpenGLES来制作一个天空盒。
首先,在制作天空盒之前我们需要准备一些天空盒的资源,这里有个网址,上面有许多天空盒的资源供我们做实验:
http://www.custommapmakers.org/skyboxes.php

此外,制作天空盒,我们需要知道天空盒的原理。天空盒的原理就是在三维空间中放置一个正方体,然后将我们的相机放置在正方体内,当我们的视点转动,相机跟着转动。我们就可以看到相应的景色的变换了。那么OpenGLES 中,怎么实现天空盒呢?天空盒本质上是一个立方体,通过查找资源,我们可以知道OpenGLES 是支持GL_TEXTURE_CUBE_MAP的Texture的。GL_TEXTURE_CUBE_MAP是有六个mipmap组成的Texture,分别是:
左 —— GL_TEXTURE_CUBE_MAP_NEGATIVE_X
右 —— GL_TEXTURE_CUBE_MAP_POSITIVE_X
下 —— GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
上 —— GL_TEXTURE_CUBE_MAP_POSITIVE_Y
前 —— GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
后 —— GL_TEXTURE_CUBE_MAP_NEGATIVE_Z

我们可以通过给这六个面设置不同的mipmap贴图,就可以实现天空盒的效果了,
废话不多说,直接进入主题吧:
1、新建一个Activity,并将GLSurfaceView加入到ContentView中,然后加入方向向量传感器的监听,代码如下:

public class SkyBoxActivity extends Activity implements  SensorEventListener {

    private GLSurfaceView mGlSurfaceView;
    private SkyBoxRenderer mSkyBoxRenderer;
    private boolean rendererSet = false;

    private SensorManager mSensorManager;
    private Sensor mRotationSensor;
    private float[] mRotationMatrix = new float[16];

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mGlSurfaceView = new GLSurfaceView(this);

        ActivityManager activityManager = 
            (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        ConfigurationInfo configurationInfo = activityManager
            .getDeviceConfigurationInfo();
        final boolean supportsEs2 =
            configurationInfo.reqGlEsVersion >= 0x20000
                || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
                 && (Build.FINGERPRINT.startsWith("generic")
                  || Build.FINGERPRINT.startsWith("unknown")
                  || Build.MODEL.contains("google_sdk")
                  || Build.MODEL.contains("Emulator")
                  || Build.MODEL.contains("Android SDK built for x86")));

        mSkyBoxRenderer = new SkyBoxRenderer(this);

        if (supportsEs2) {
            mGlSurfaceView.setEGLContextClientVersion(2);
            mGlSurfaceView.setRenderer(mSkyBoxRenderer);
            rendererSet = true;
        } else {
            Toast.makeText(this, "This device does not support OpenGL ES 2.0.",
                Toast.LENGTH_LONG).show();
            return;
        }


        setContentView(mGlSurfaceView);

        // 方向向量s
        mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
        mRotationSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        Matrix.setIdentityM(mRotationMatrix, 0);
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        
        if (rendererSet) {
            mGlSurfaceView.onPause();
        }

        mSensorManager.unregisterListener(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        
        if (rendererSet) {
            mGlSurfaceView.onResume();
        }

        mSensorManager.registerListener(this, mRotationSensor, SensorManager.SENSOR_DELAY_GAME);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        SensorManager.getRotationMatrixFromVector(mRotationMatrix, event.values);
        if (mGlSurfaceView != null) {
            mGlSurfaceView.queueEvent(new Runnable() {
                @Override
                public void run() {
                    mSkyBoxRenderer.setRotationMatrix(mRotationMatrix);
                }
            });
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }
}

接下来我们来看下SkyRenderer类的实现:

public class SkyBoxRenderer implements GLSurfaceView.Renderer {
    private final Activity mContext;

    private int mViewWidth;
    private int mViewHeight;

    // 天空盒滤镜
    private SkyBoxFilter mSkyBoxFilter;


    public SkyBoxRenderer(Activity context) {
        mContext = context;

    }

    /**
     * 设置旋转矩阵
     * @param matrix
     */
    public void setRotationMatrix(float[] matrix) {
        if (mSkyBoxFilter != null) {
            mSkyBoxFilter.setRotationMatrix(matrix);
        }
    }

    @Override
    public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

        // 创建天空盒
        mSkyBoxFilter = new SkyBoxFilter(mContext);
        mSkyBoxFilter.createProgram();

        
    }

    @Override
    public void onSurfaceChanged(GL10 glUnused, int width, int height) {                
        GLES20.glViewport(0, 0, width, height);
        mViewWidth = width;
        mViewHeight = height;
        // 调整视图大小
        if (mSkyBoxFilter != null) {
            mSkyBoxFilter.setViewSize(width, height);
        }
        
    }

    @Override    
    public void onDrawFrame(GL10 glUnused) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        // 绘制天空盒
        drawSkybox();
        
    }


    /**
     * 绘制天空盒
     */
    private void drawSkybox() {
        if (mSkyBoxFilter != null) {
            mSkyBoxFilter.drawSkyBox();
        }
    }
}

Renderer类主要是调用了SkyBoxFilter类实现天空盒的绘制,SkyBoxFilter类的实现过程如下:

/**
 * 天空盒滤镜
 * Created by cain.huang on 2017/8/31.
 */
public class SkyBoxFilter {

    private static final int COORDS_PER_VERTEX = 3;

    // 立方体坐标
    private static final float[] CubeCoords = new float[] {
            -1,  1,  1,     // (0) Top-left near
            1,   1,  1,     // (1) Top-right near
            -1, -1,  1,     // (2) Bottom-left near
            1,  -1,  1,     // (3) Bottom-right near
            -1,  1, -1,     // (4) Top-left far
            1,   1, -1,     // (5) Top-right far
            -1, -1, -1,     // (6) Bottom-left far
            1,  -1, -1      // (7) Bottom-right far
    };

    // 立方体索引
    private static final byte[] CubeIndex = new byte[] {
            // Front
            1, 3, 0,
            0, 3, 2,

            // Back
            4, 6, 5,
            5, 6, 7,

            // Left
            0, 2, 4,
            4, 2, 6,

            // Right
            5, 7, 1,
            1, 7, 3,

            // Top
            5, 1, 4,
            4, 1, 0,

            // Bottom
            6, 2, 7,
            7, 2, 3
    };

    private Context mContext;

    // 视图宽高
    private int mViewWidth;
    private int mViewHeight;

    private FloatBuffer mVertexBuffer;
    private ByteBuffer mIndexBuffer;

    private int mProgramHandle;

    private int muMatrixHandle;
    private int muTextureUnitHandle;
    private int maPositionHandle;

    // Cube纹理
    private int mSkyboxTexture;

    // 变换矩阵
    private float[] mRotationMatrix = new float[16]; // 旋转矩阵
    private float[] mViewMatrix = new float[16];    // 视图矩阵
    private float[] mProjectionMatrix = new float[16]; // 投影矩阵
    private float[] mMVPMatrix = new float[16]; // 总变换矩阵


    public SkyBoxFilter(Context context) {
        mContext = context;
        mVertexBuffer = GlUtil.createFloatBuffer(CubeCoords);
        mIndexBuffer = GlUtil.createByteBuffer(CubeIndex);
        initMatrix();
    }

    /**
     * 初始化Matrix
     */
    private void initMatrix() {
        Matrix.setIdentityM(mRotationMatrix, 0);
        Matrix.setIdentityM(mViewMatrix, 0);
        Matrix.setIdentityM(mProjectionMatrix, 0);
        Matrix.setIdentityM(mMVPMatrix, 0);
    }

    /**
     * 创建Program
     */
    public void createProgram() {
        mProgramHandle = GlUtil.createProgram(ResourceUtil
                        .readTextFileFromResource(mContext, R.raw.vertex_skybox),
                ResourceUtil
                        .readTextFileFromResource(mContext, R.raw.fragment_skybox));
        muMatrixHandle = glGetUniformLocation(mProgramHandle, "u_Matrix");
        muTextureUnitHandle = glGetUniformLocation(mProgramHandle, "u_TextureUnit");
        maPositionHandle = glGetAttribLocation(mProgramHandle, "a_Position");
        mSkyboxTexture =  TextureHelper.loadCubeMap(mContext,
                new int[] {
                        R.drawable.left, R.drawable.right,
                        R.drawable.bottom, R.drawable.top,
                        R.drawable.front, R.drawable.back,
                });
    }

    /**
     * 更新视图大小,用于设置视图矩阵和透视矩阵
     * @param width
     * @param height
     */
    public void setViewSize(int width, int height) {
        mViewWidth = width;
        mViewHeight = height;

        //设置相机位置
        Matrix.setLookAtM(mViewMatrix, 0,
                0.0f, 0.0f, 0.0f,
                0.0f, 0.0f, -1.0f,
                0f, 1.0f, 0.0f);

        // 设置透视矩阵
        float ratio = (float) width / (float) height;
        MatrixHelper.perspectiveM(mProjectionMatrix, 45, ratio, 1f, 300f);
    }

    /**
     * 绘制天空盒
     */
    public void drawSkyBox() {

        GLES20.glUseProgram(mProgramHandle);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, mSkyboxTexture);
        calculateMatrix();
        GLES20.glUniformMatrix4fv(muMatrixHandle, 1, false, mMVPMatrix, 0);
        GLES20.glUniform1i(muTextureUnitHandle, 0);

        GLES20.glEnableVertexAttribArray(maPositionHandle);
        GLES20.glVertexAttribPointer(maPositionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false, 0, mVertexBuffer);
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, 36, GLES20.GL_UNSIGNED_BYTE, mIndexBuffer);
        GLES20.glUseProgram(0);
    }

    /**
     * 计算总变换
     */
    private void calculateMatrix() {
        // 计算综合矩阵
        Matrix.setIdentityM(mViewMatrix, 0);
        Matrix.setLookAtM(mViewMatrix, 0,
                0.0f, 0.0f, 0.0f,
                0.0f, 0.0f, -1.0f,
                0f, 1.0f, 0.0f);
        Matrix.multiplyMM(mViewMatrix, 0, mViewMatrix, 0, mRotationMatrix, 0);
        Matrix.rotateM(mViewMatrix, 0, 90, 1f, 0f, 0f);
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
    }

    /**
     * 设置旋转矩阵
     * @param matrix
     */
    public void setRotationMatrix(float[] matrix) {
        mRotationMatrix = matrix;
    }
}

该类主要是创建Program、创建CubeMap的Texture、绑定GLSL的属性、绘制等。其中TextureHelper是一个用于加载CubeMap的类,实现如下:

public class TextureHelper {
    private static final String TAG = "TextureHelper";

    /**
     * Loads a texture from a resource ID, returning the OpenGL ID for that
     * texture. Returns 0 if the load failed.
     * 
     * @param context
     * @param resourceId
     * @return
     */
    public static int loadTexture(Context context, int resourceId) {
        final int[] textureObjectIds = new int[1];
        GLES20.glGenTextures(1, textureObjectIds, 0);
        if (textureObjectIds[0] == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Could not generate a new OpenGL texture object.");
            }
            return 0;
        }
        
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;

        // Read in the resource
        final Bitmap bitmap = BitmapFactory.decodeResource(
            context.getResources(), resourceId, options);

        if (bitmap == null) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Resource ID " + resourceId
                    + " could not be decoded.");
            }

            GLES20.glDeleteTextures(1, textureObjectIds, 0);

            return 0;
        } 
        
        // Bind to the texture in OpenGL
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]);

        // Set filtering: a default must be set, or the texture will be
        // black.
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        // Load the bitmap into the bound texture.
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

        // Note: Following code may cause an error to be reported in the
        // ADB log as follows: E/IMGSRV(20095): :0: HardwareMipGen:
        // Failed to generate texture mipmap levels (error=3)
        // No OpenGL error will be encountered (glGetError() will return
        // 0). If this happens, just squash the source image to be
        // square. It will look the same because of texture coordinates,
        // and mipmap generation will work.

        GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);

        // Recycle the bitmap, since its data has been loaded into
        // OpenGL.
        bitmap.recycle();

        // Unbind from the texture.
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
        return textureObjectIds[0];        
    }
    /**
     * Loads a cubemap texture from the provided resources and returns the
     * texture ID. Returns 0 if the load failed.
     * 
     * @param context
     * @param cubeResources
     *            An array of resources corresponding to the cube map. Should be
     *            provided in this order: left, right, bottom, top, front, back.
     * @return
     */
    public static int loadCubeMap(Context context, int[] cubeResources) {       
        final int[] textureObjectIds = new int[1];
        GLES20.glGenTextures(1, textureObjectIds, 0);

        if (textureObjectIds[0] == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Could not generate a new OpenGL texture object.");
            }
            return 0;
        }      
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;
        final Bitmap[] cubeBitmaps = new Bitmap[6];
        for (int i = 0; i < 6; i++) {
            cubeBitmaps[i] =
                BitmapFactory.decodeResource(context.getResources(),
                    cubeResources[i], options);

            if (cubeBitmaps[i] == null) {
                if (LoggerConfig.ON) {
                    Log.w(TAG, "Resource ID " + cubeResources[i]
                        + " could not be decoded.");
                }
                GLES20.glDeleteTextures(1, textureObjectIds, 0);
                return 0;
            }
        }
        // Linear filtering for minification and magnification
        GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, textureObjectIds[0]);

        GLES20.glTexParameteri(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);


        GLUtils.texImage2D(GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, cubeBitmaps[0], 0); // 左
        GLUtils.texImage2D(GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, cubeBitmaps[1], 0); // 右

        GLUtils.texImage2D(GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, cubeBitmaps[2], 0); // 下
        GLUtils.texImage2D(GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, cubeBitmaps[3], 0); // 上

        GLUtils.texImage2D(GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, cubeBitmaps[4], 0); // 前
        GLUtils.texImage2D(GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, cubeBitmaps[5], 0); // 后
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

        // 释放图片
        for (Bitmap bitmap : cubeBitmaps) {
            bitmap.recycle();
        }

        return textureObjectIds[0];        
    }
}

接下来我们来看看GLSL的代码:
VertexShader:

uniform mat4 u_Matrix;
attribute vec3 a_Position;
varying vec3 v_Position;

void main()                    
{                                                 
    v_Position = a_Position;    
    // Make sure to convert from the right-handed coordinate system of the
    // world to the left-handed coordinate system of the cube map, otherwise,
    // our cube map will still work but everything will be flipped.
    v_Position.z = -v_Position.z;

    gl_Position = u_Matrix * vec4(a_Position, 1.0);
    gl_Position = gl_Position.xyww;
}  

FragmentShader:

precision mediump float; 

uniform samplerCube u_TextureUnit;
varying vec3 v_Position;
                                            
void main()                         
{
    gl_FragColor = textureCube(u_TextureUnit, v_Position);    
}

大致的过程就是这样。整个过程都非常简单,主要是要理解相机的视点(视图矩阵)、CubeMap的应用等内容。整个过程与一般的OpenGLES 开发并没有任何差异。
如果没有意外,我们就能看到天空盒的效果了,转动手机,就能够看到不同的位置的贴图了:

Screenshot_20170831-171555.png
Screenshot_20170831-171559.png
Screenshot_20170831-171614.png

项目地址:
https://github.com/CainKernel/SkyBox

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容