前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >音视频开发之旅(41)-天空盒

音视频开发之旅(41)-天空盒

原创
作者头像
音视频开发之旅
修改2021-04-12 10:55:40
1.1K0
修改2021-04-12 10:55:40
举报
文章被收录于专栏:音视频开发之旅

目录

  1. 天空盒的实现原理
  2. 具体代码实现
  3. 资料
  4. 收获

效果如下

今天我们学习实践天空盒,天空盒的技术本身比较简单,但是却可以做出来很多比较天空、大山、大海、以及VR看房等效果。可以作为背景动态移动,也可以跟随手势或者传感器等进行移动变换。

一、立方体贴图和天空盒

所谓的天空盒其实就是将一个立方体展开,然后在六个面上贴上相应的贴图

天空盒的效果正如开篇动画中展示的效果一样,从一个视点,旋转视角看天空,呈现出来不同画面。我们可以想象成我们自己就位于一个三维空间的内部中心点,四周是一个大的立方体,包含上下、左右、前后 六个平面,我们旋转我们的视角就会看到不同的画面。

因此我们可以采用上面的原理,在一个立方体进行立方体贴图

在实际的渲染中,将这个立方体始终罩在摄像机的周围,让摄像机始终处于这个立方体的中心位置,然后根据视线与立方体的交点的坐标,来确定究竟要在哪一个面上进行纹理采样。具体的映射方法为:设视线与立方体的交点为(x,y,z)(x,y,z),在x、y、zx、y、z中取绝对值最大的那个分量,根据它的符号来判定在哪个面上采样。

然后让其他两个分量都除以最大分量的绝对值,这样就让另外两个分量都映射到了[0,1]内,然后就可以直接在对应的纹理上做纹理映射就行了,这个方法就是所谓的Cube Map,是天空盒方法的核心

立方体贴图是和2D纹理创建流程一样

代码语言:javascript
复制
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, skyBoxTexture)
        GLES20.glUniform1i(uTextureLoc, 0)

立方体纹理贴图的加载如下

代码语言:javascript
复制
 /**
     * 加载立方体纹理贴图
     * @param context
     * @param cubeResources
     * @return
     */
    public static int loadCubeMap(Context context, int[] cubeResources) {
        final int[] textureObjectIds = new int[1];
        glGenTextures(1, textureObjectIds, 0);

        if (textureObjectIds[0] == 0) {
            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) {
                Log.w(TAG, "Resource ID " + cubeResources[i]
                        + " could not be decoded.");
                glDeleteTextures(1, textureObjectIds, 0);
                return 0;
            }
        }
        // Linear filtering for minification and magnification
        //注意这里不是GL_TEXTURE_2D,而是GL_TEXTURE_CUBE_MAP,使用六张纹理组合成一个立方体纹理
        glBindTexture(GL_TEXTURE_CUBE_MAP, textureObjectIds[0]);

        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        //左右、下上、前后---》注意 使用的是左手坐标系
        texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, cubeBitmaps[0], 0);
        texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, cubeBitmaps[1], 0);

        texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, cubeBitmaps[2], 0);
        texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, cubeBitmaps[3], 0);

        texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, cubeBitmaps[4], 0);
        texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, cubeBitmaps[5], 0);

        glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

        //把纹理复制到GPU后就可以回收原理的bitmap了
        for (Bitmap bitmap : cubeBitmaps) {
            bitmap.recycle();
        }

        return textureObjectIds[0];
    }

OpenGL给我们提供了6个特殊的纹理目标,专门对应立方体贴图的一个面。

GL_TEXTURE_CUBE_MAP_POSITIVE_X 右 GL_TEXTURE_CUBE_MAP_NEGATIVE_X 左 GL_TEXTURE_CUBE_MAP_POSITIVE_Y 上 GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 下 GL_TEXTURE_CUBE_MAP_POSITIVE_Z 后 GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 前

另外在着色器上使用立方体纹理

代码语言:javascript
复制
//使用立方体纹理
uniform samplerCube uTexture;
varying vec3 vPosition;

void main() {
    gl_FragColor = textureCube(uTexture,vPosition);
}

二、具体代码实现

通过上面小节,我们了解到天空盒的实现原理比较简单,下面我们开始具体的代码实现。

首先,写着色器代码

代码语言:javascript
复制
uniform mat4 uMatrix;
attribute vec3 aPosition;
varying vec3 vPosition;

void main() {
    vPosition = aPosition;
    gl_Position = uMatrix*vec4(aPosition, 1.0);
    //注意这里
    gl_Position = gl_Position.xyww;
}

z = w 在投影变换之后,会做一步透视除法,即让四元向量的所有分量都除以它的W分量,从而使视锥体内的区域的x、y映射到[−1,1][−1,1],z映射到[0,1][0,1],从而根据透视除法之后的x、y、zx、y、z的范围直接剔除掉那些不可见的顶点,如果令z=wz=w,就表示透视除法后的z=1z=1,也就是让天空盒始终处于远平面的位置

代码语言:javascript
复制
//使用立方体纹理
uniform samplerCube uTexture;
varying vec3 vPosition;

void main() {
    gl_FragColor = textureCube(uTexture,vPosition);
}

接着我们重点来看下Render的实现

代码语言:javascript
复制
package com.av.mediajourney.skybox

import android.content.Context
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import com.av.mediajourney.R
import com.av.mediajourney.opengl.ShaderHelper
import com.av.mediajourney.particles.android.util.TextureHelper
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

class SkyBoxRender(var context: Context) : GLSurfaceView.Renderer {

    lateinit var skyBox: SkyBox;
    var mProgram = -1

    private val projectionMatrix = FloatArray(16)
    private val viewMatrix = FloatArray(16)
    private val viewProjectionMatrix = FloatArray(16)

    private var aPositionLoc = -1;
    private var uMatrixLoc = -1;
    private var uTextureLoc = -1;
    private var skyBoxTexture = -1;


    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0f, 0f, 0f, 1f)
        skyBox = SkyBox()
        val vertexStr = ShaderHelper.loadAsset(context.resources, "sky_box_vertex.glsl")
        val fragStr = ShaderHelper.loadAsset(context.resources, "sky_box_fragment.glsl")

        mProgram = ShaderHelper.loadProgram(vertexStr, fragStr)

        aPositionLoc = GLES20.glGetAttribLocation(mProgram, "aPosition")

        uMatrixLoc = GLES20.glGetUniformLocation(mProgram, "uMatrix")

        uTextureLoc = GLES20.glGetUniformLocation(mProgram, "uTexture")

        skyBoxTexture = TextureHelper.loadCubeMap(context, intArrayOf(R.drawable.left2, R.drawable.right2,
                R.drawable.bottom2, R.drawable.top2,
                R.drawable.front2, R.drawable.back2))

    }


    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
        val whRadio = width / (height * 1.0f)
        Matrix.setIdentityM(projectionMatrix, 0)
        Matrix.perspectiveM(projectionMatrix, 0, 105f, whRadio, 1f, 10f)
    }

    var frameIndex: Int = 0

    override fun onDrawFrame(gl: GL10?) {

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glClearColor(0f, 0f, 0f, 1f)

        //自动旋转的
        val xRotationAuto = frameIndex / 8f

        //整体旋转的值 = 自旋转+滑动触摸触发的旋转值
        val xRotationT = xRotationAuto +xRotation

        frameIndex++

        Matrix.setIdentityM(viewMatrix, 0)


        //采用移动的方式,可以看到立方体的6个面上的纹理图片
//        Matrix.translateM(viewMatrix,0, xRotation,0f,0f)

        //采用旋转的方式,只能采用旋转的方式,进行实现视角变换,达到移动的效果
        Matrix.rotateM(viewMatrix, 0, xRotationT, 0f, 1f, 0f)
//        Matrix.rotateM(viewMatrix, 0, yRotation, 1f, 0f, 0f)


        Matrix.multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0)

        GLES20.glUseProgram(mProgram)

        //传mvp矩阵数据
        GLES20.glUniformMatrix4fv(uMatrixLoc, 1, false, viewProjectionMatrix, 0)
        //传纹理数据
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, skyBoxTexture)
        GLES20.glUniform1i(uTextureLoc, 0)


        GLES20.glEnableVertexAttribArray(aPositionLoc)
        skyBox.vertexArrayBuffer.position(0);
        GLES20.glVertexAttribPointer(aPositionLoc, SkyBox.POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT, false, 0, skyBox.vertexArrayBuffer)

        GLES20.glDrawElements(GLES20.GL_TRIANGLES, 36, GLES20.GL_UNSIGNED_BYTE, skyBox.indexArrayBuffer)
    }


    private var xRotation = 0f
    private var yRotation = 0f

    fun handleTouchMove(deltaX: Float, deltaY: Float) {
        xRotation += deltaX / 16f
        yRotation += deltaY / 16f

        if (yRotation < -90f) {
            yRotation = -90f
        } else if (yRotation > 90) {
            yRotation = 90f
        }
    }

}

具体的流程和逻辑详见代码注释。 这里说明下为什么采用旋转的方式,而不是位移的方式进行视角的切换,因为我们不是在一个平面中,而是位于一个立方体的中央,沿着某个方向(比如Y轴)进行选择,即可实现天空移动的效果,如果采用位移的方式看到的是立方体的移动。 对比效果如下:

另外关于移动,可以自动旋转,也可以加入触碰旋转的实现,通过glSurfaceView.queueEvent给render刷新旋转的大小,即可相应跟随手势旋转的效果

代码语言:javascript
复制
        glSurfaceView.setOnTouchListener(object : OnTouchListener {
            var lastX = 0f;
            var lastY = 0f;
            override fun onTouch(v: View?, event: MotionEvent?): Boolean {

                if (event == null) {
                    return false
                }
                if (MotionEvent.ACTION_DOWN == event.action) {
                    lastX = event.x;
                    lastY = event.y;
                } else if (MotionEvent.ACTION_MOVE == event.action) {
                    val deltaX = event.x - lastX
                    val deltaY = event.y - lastY

                    lastX = event.x
                    lastY = event.y

                    glSurfaceView.queueEvent {
                        skyBoxRender.handleTouchMove(deltaX, deltaY)
                    }
                }
                return true
            }
        })

详细代码请查看 github https://github.com/ayyb1988/mediajourney

三、资料

  1. 天空盒(SkyBox)的实现原理与细节
  2. NDK OpenGL ES 3.0 开发(十五):立方体贴图(天空盒)
  3. 立方体贴图
  4. OpenGL 图形库的使用(二十六)—— 高级OpenGL之立方体贴图Cubemaps
  5. opengl渲染管线 不能再详细了

四、收获

  1. 了解天空盒的原理
  2. 立方体贴图的实现
  3. 具体代码实现

感谢你的阅读 要让渲染的内容更加逼真,反射、折射等的应用必不可少 下一篇我们进入光照部分的学习实践,欢迎关注公众号“音视频开发之旅”,一起学习成长。 欢迎交流

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • 一、立方体贴图和天空盒
  • 二、具体代码实现
  • 三、资料
  • 四、收获
相关产品与服务
GPU 云服务器
GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于生成式AI,自动驾驶,深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档