OpenGL之天空盒

OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
OpenGL之三维
OpenGL之纹理
OpenGL之构建简单物体
OpenGL之触控反馈
OpenGL之粒子

创建天空盒

天空盒是表达三维全景的一个方法,不管你的头转向哪边,这个全景在任何方向上都能被看见。渲染天空盒最经典的方法之一是使用一个环绕观察者的立方体,并在其每个面上都附上一个细致的纹理,这也称为立方体贴图

加载立方体贴图

/**
 * 加载立方贴图,天空盒
 *
 * @param context
 * @param resIds 左、右、下、上、前、后
 * @return
 */
public static int loadCubeTexture(Context context, int[] resIds) {
    // 获取图像原始bitmap数据
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inScaled = false;
    Bitmap[] bitmaps = new Bitmap[resIds.length];
    for (int i = 0; i < resIds.length; i++) {
        bitmaps[i] = BitmapFactory.decodeResource(context.getResources(), resIds[i], options);
        if (bitmaps[i] == null) {
            Log.d(TAG, "loadTexture: decodeResource failed!");
            return 0;
        }
    }

    // 生成纹理
    int textureIds[] = new int[1];
    glGenTextures(textureIds.length, textureIds, 0);

    // 绑定,后面的OpenGL调用应用于这个纹理
    glBindTexture(GL_TEXTURE_CUBE_MAP, textureIds[0]);
    //设置纹理过滤参数,缩小和放大
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // 加载位图数据到OpenGL
    //左
    GLUtils.texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, bitmaps[0], 0);
    //右
    GLUtils.texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, bitmaps[1], 0);
    //下
    GLUtils.texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, bitmaps[2], 0);
    //上
    GLUtils.texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, bitmaps[3], 0);
    //前
    GLUtils.texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, bitmaps[4], 0);
    //后
    GLUtils.texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, bitmaps[5], 0);

    // 回收bitmap
    for (Bitmap bitmap : bitmaps) {
        bitmap.recycle();
    }

    //解绑
    glBindTexture(GL_TEXTURE_2D, 0);
    return textureIds[0];
}

创建立方体

public class SkyBox {
    private VertexArray mVertexArray;
    private ByteBuffer mIndexBuffer;
    // 顶点数据数组
    private static final float[] VERTEX_DATA = new float[]{
            -1f, 1f, 1f,   //0左上前
            1f, 1f, 1f,    //1右上前
            -1f, -1f, 1f,  //2左下前
            1f, -1f, 1f,   //3右下前
            -1f, 1f, -1f,  //4左上后
            1f, 1f, -1f,   //5右上后
            -1f, -1f, -1f, //6左下后
            1f, -1f, -1f   //7右下后

    };
    // 顶点数据索引,这个索引数组使用索引偏移值指向每个顶点,比如,0指向顶点数组中的第一个顶点,1指向第二个顶点
    // 通过这个索引数组,把所有顶点分别绑定成三角形组,每个三角形组,包含2个三角形,代表立方体上的某一面
    private static final byte[] INDEX = 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
    };

    public SkyBox() {
        mVertexArray = new VertexArray(VERTEX_DATA);
        mIndexBuffer = ByteBuffer.allocateDirect(6 * 2 * 3).put(INDEX);
        mIndexBuffer.position(0);
    }

    public void bindData(SkyProgram skyProgram) {
        mVertexArray.setVertexAttribPointer(
                0,
                skyProgram.getAPosition(),
                Constants.POSITION_COMPONENT_COUNT,
                0
        );
    }

    /**
     * 绘制,glDrawElements告诉OpenGL绘制我们在bindData()中绑定的顶点
     * 并且要使用mIndexBuffer所定义的索引数组,并把这个数组解释为无符号字节数
     */
    public void draw() {
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, 36, GLES20.GL_UNSIGNED_BYTE, mIndexBuffer);
    }
}

天空盒着色器

顶点着色器

uniform mat4 u_Matrix;

attribute vec3 a_Position;

varying vec3 v_Position;

void main(){
    v_Position=a_Position;
    v_Position.z=-v_Position.z;

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

先把顶点的位置传递给片段着色器,接着反转其z分量,这个传递给片段着色器的位置就是立方体上每个面之间将被插值的位置,可以使用这个位置查看天空盒的纹理上正确的部分。其z分量被反转了,使得我们可以把世界的右手坐标空间转换为天空盒所期望的左手坐标空间

通过用a_Position乘以矩阵把那个位置投影到剪裁空间坐标之后,要用下面的代码把其z分量设置为与其w分量相等的值:

gl_Position=gl_Position.xyww;

确保天空盒的每一部分都将位于归一化设备坐标的远平面上以及场景中的其他一切后面。这个技巧能够奏效,是因为透视除法把一切都除以w,并且w除以它自己,结果等于1。透视除法之后,z最终就在值为1的远平面上了

片段着色器

precision mediump float;

varying vec3 v_Position;

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

为了使用立方体纹理绘制这个天空盒,我们调用textureCube()时,把那个被插值的立方体面的位置作为那个片段的纹理坐标

封装着色器程序

public class SkyProgram extends BaseProgram {
    private final int mUMatrix;
    private final int mUTextureUnit;

    private final int mAPosition;

    public SkyProgram(Context context) {
        super(context, R.raw.particles8_sky_vertex, R.raw.particles8_sky_fragment);
        mUMatrix = glGetUniformLocation(mProgram, U_MATRIX);
        mUTextureUnit = glGetUniformLocation(mProgram, U_TEXTURE_UNIT);

        mAPosition = glGetAttribLocation(mProgram, A_POSITION);
    }

    public void setUniforms(float[] matrix, int textureId) {
        glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_CUBE_MAP, textureId);
        glUniform1i(mUTextureUnit, 0);
    }

    public int getAPosition() {
        return mAPosition;
    }
}

修改渲染器,加入天空盒

在onSurfaceCreated方法中,创建天空盒相关对象

mSkyProgram = new SkyProgram(mContext);
mSkyBox = new SkyBox();
mCubeTextureId = TextureHelper.loadCubeTexture(mContext, SKY_ID);

在onDrawFrame方法中,绘制天空盒,因为使用了天空盒,不能把平移矩阵应用到天空盒上。出于这个原因,我们需要为天空盒和粒子使用一个不同的矩阵

// 天可盒使用的模型视图投影矩阵
setIdentityM(modelMatrix, 0);
multiplyMM(modelViewProjectionMatrix, 0, viewProjectionMatrix, 0, modelMatrix, 0);

mSkyProgram.useProgram();
mSkyProgram.setUniforms(modelViewProjectionMatrix, mCubeTextureId);
mSkyBox.bindData(mSkyProgram);
mSkyBox.draw();

通过拖拽旋转天空盒

在Activity中将拖拽事件传给渲染器处理

/**
 * 处理天空盒拖拽旋转,将拖拽的距离传给渲染器处理
 *
 * @param myRenderer
 */
private void dealSkyDrag(com.test.opengl.particles8.MyRenderer myRenderer) {
    glSurfaceView.setOnTouchListener(new View.OnTouchListener() {
        // 记录前一刻手指的位置
        float previousX;
        float previousY;

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event != null) {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    previousX = event.getX();
                    previousY = event.getY();
                } else if (event.getAction() == MotionEvent.ACTION_MOVE) {

                    // 计算拖拽的距离
                    float deltaX = event.getX() - previousX;
                    float deltaY = event.getY() - previousY;

                    // 更新前一刻手指的位置
                    previousX = event.getX();
                    previousY = event.getY();
                    glSurfaceView.queueEvent(new Runnable() {
                        @Override
                        public void run() {
                            // 将拖拽的距离传给渲染器处理
                            myRenderer.handleDrag(deltaX, deltaY);
                        }
                    });
                }
                return true;
            }
            return false;
        }
    });
}

在渲染器中,通过用户在每个方向上拖动的距离,把它加到xRotation和yRotation 上,这两个变量表示以度为单位的旋转,不想让触控过于灵敏,因此用16缩减了拖动的效果,也不想上下旋转的角度过大,因此把y旋转限制在+90度和-90度之间

/**
 * 处理拖拽事件
 *
 * @param deltaX
 * @param deltaY
 */
public void handleDrag(float deltaX, float deltaY) {
    xRotate += deltaX / 16f;
    yRotate += deltaY / 16f;
    if (yRotate > 90) {
        yRotate = 90;
    }
    if (yRotate < -90) {
        yRotate = -90;
    }
    updateViewProjectionMatrix();
}

/**
 * 更新视图矩阵
 */
private void updateViewProjectionMatrix() {
    setIdentityM(viewMatrix, 0);
    Matrix.rotateM(viewMatrix, 0, -xRotate, 0f, 1f, 0f);
    Matrix.rotateM(viewMatrix, 0, -yRotate, 1f, 0f, 0f);
    multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
}

效果

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

推荐阅读更多精彩内容