iOS-OpenGLES-进阶-天空盒

这是一篇OpenGlES 系统学习教程,记录自己的学习过程。
环境: Xcode10.2.1 + OpenGL ES 3.0
目标: 天空盒
代码已上传githubTutorial-06-天空盒,你的star和fork是对我最好的支持和动力。

概述

所谓天空盒是纹理的一种应用方式,它可以将整个场景高效的封装到一个立方体的大盒子里,同时确保观察者位于盒子的正中央。在场景渲染的时候,场景内任何没有被遮挡的物体都会出现在盒子的内部。通过选择合适的纹理内容,我们就可以让整个立方体从观察者的角度看起来就是环境本身。例如:吃鸡中的场景,会随着玩家的视点转动而转动。制作天空盒之前我们需要准备一下天空盒的资源,可以在这里获取自己喜欢的场景。

效果展示

创建cubemap

天空盒的专业术语叫立体贴图。OpenGlES支持GL_TEXTURE_CUBE_MAP的Texture。GL_TEXTURE_CUBE_MAP是由六个2D纹理绑定到GL_TEXTURE_CUBE_MAP目标而创建的纹理。

绑定目标 纹理方向
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 前面

如下图所示,形成一个立方体纹理



在构建cubemaps,一般利用枚举常量递增的特性,一次绑定到上述6个目标。例如在OpenGLES中枚举常量定义为(虽然在swift中并不是枚举,但值是一样的):

public var GL_TEXTURE_CUBE_MAP_POSITIVE_X: Int32 { get }    //  34069
public var GL_TEXTURE_CUBE_MAP_NEGATIVE_X: Int32 { get }    //  34070
public var GL_TEXTURE_CUBE_MAP_POSITIVE_Y: Int32 { get }    //  34071
public var GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: Int32 { get }    //  34072
public var GL_TEXTURE_CUBE_MAP_POSITIVE_Z: Int32 { get }    //  34073
public var GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: Int32 { get }    //  34074

可以看到值是依次递增的,我们可以使用循环来创建这个立方体纹理,如下:

    fileprivate func loadCubeMapTexture(fileNames:[String]) -> GLuint {
        
        var textId: GLuint = 0
        glGenTextures(1, &textId)
        glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), textId)
        
        
        
        for (index,name) in fileNames.enumerated() {
            guard let spriteImage = UIImage(named: name)?.cgImage else {
                print("Failed to load image \(name)")
                return textId
            }
            
            let width = spriteImage.width
            let height = spriteImage.height
            
            let spriteData = calloc(width * height * 4, MemoryLayout<GLubyte>.size)
            
            let spriteContext = CGContext(data: spriteData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width*4, space: spriteImage.colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
            
            spriteContext?.draw(spriteImage, in: CGRect(x: 0, y: 0, width: width, height: height))
            
            // 使用GL_TEXTURE_CUBE_MAP_POSITIVE_X + i的方式来一次创建了6个2D纹理
            glTexImage2D(GLenum(GL_TEXTURE_CUBE_MAP_POSITIVE_X + Int32(index)), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), spriteData)
            free(spriteData)
        }
        
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_R), GL_CLAMP_TO_EDGE)
        
        glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
        
        return textId
    }

使用cubemaps

cubemaps采样不同于2D纹理使用的纹理坐标(s,t),这边需要使用三维纹理坐标(s,t,r),如下图所示:


首先根据(s,t,r)中模最大的分量决定在哪个面采样,然后使用剩下的2个坐标在对应的面上做2D纹理采样。具体的计算过程可以参考cubemaps

当纹理坐标超出[0,1]范围时的纹理采样方式。上述代码中,我们使用

        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_R), GL_CLAMP_TO_EDGE)

其中参数GL_CLAMP_TO_EDGE主要用于指定,当(s,t,r)坐标没有落在哪个面,而是落在两个面之间时的纹理采样,使用GL_CLAMP_TO_EDGE参数表明,当在两个面之间采样时使用边缘的纹理值。同时OpenGlES默认是打开了无缝立方体映射滤波的。
顶点着色器:

attribute vec3 position;

uniform highp mat4 u_mvpMatrix;

varying lowp vec3 TextCoord;

void main()
{
    gl_Position = u_mvpMatrix * vec4(position, 1.0);
    TextCoord = position;
}

需要注意的是:我们使用位于模型局部坐标系下的位置坐标作为 3D 纹理坐标,这样做可行的原因是在对 cubemap 采样的过程中也是通过从圆心发出一条射线到立方体或者球体表面上,所以这里天空盒的位置坐标就可以直接作为我们的纹理坐标。之后我们将这些数据都传递到片元着色器中。

片元着色器:

varying lowp vec3 TextCoord;
uniform samplerCube skybox;

void main()
{
    gl_FragColor = textureCube(skybox, TextCoord);
}

要渲染一个天空盒需要这些组件——一个着色器对象、一个cubemap 纹理对象和一个立方体盒子(模型),以及模型位置转换过程(从这篇文章开始矩阵转换会使用GLKEffectPropertyTransform),我们将这些组件都封装在同一个类中。这里有使用VAO(顶点数组对象),相关细节可以查看learnopengl
为了让天空盒效果看起来比较逼真,我们需要把天空盒中心移到观察者中心,同时以一定比例缩放天空盒,如下:

var modelView = GLKMatrix4Translate(transform.modelviewMatrix, center.x, center.y, center.z)  // 移到观察者中心位置
modelView = GLKMatrix4Scale(modelView, xSize, ySize, zSize)  // 缩放

let modelViewProjection = GLKMatrix4Multiply(transform.projectionMatrix, modelView)

glUniformMatrix4fv(glGetUniformLocation(skyBoxProgram, "u_mvpMatrix"), 1, GLboolean(GL_FALSE), modelViewProjection.array)

设置错的话会出现一些视觉bug,其实天空盒效果相当来说比较简单,重点在于理解坐标系和物体之间的坐标联动。不太明白的可以先看看坐标系和摄像机系统的一些知识传送门还是老地方。

渲染

由于相机是放在天空盒内部的,所以我们要禁止背面剔除。其次我们需要关闭深度测试。

glClearColor(0.18, 0.04, 0.14, 1.0)
glClear(UInt32(GL_COLOR_BUFFER_BIT) | UInt32(GL_DEPTH_BUFFER_BIT))
glViewport(0, 0, GLsizei(frame.size.width), GLsizei(frame.size.height))

let width = frame.size.width
let height = frame.size.height

baseEffect.transform.projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(85.0), Float(width/height), 0.1, 20.0)

baseEffect.transform.modelviewMatrix = GLKMatrix4MakeLookAt(eyePosition.x,
                                                            eyePosition.y,
                                                            eyePosition.z,
                                                            targetPosition.x,
                                                            targetPosition.y, targetPosition.z,
                                                            upVector.x,
                                                            upVector.y,
                                                            upVector.z)
skyboxEffect?.center = eyePosition
skyboxEffect?.transform.projectionMatrix = baseEffect.transform.projectionMatrix
skyboxEffect?.transform.modelviewMatrix = baseEffect.transform.modelviewMatrix

// 绘制天空盒
skyboxEffect?.prepareToDraw()
glDepthMask(GLboolean(GL_FALSE))    // 关闭深度缓存
glDisable(GLenum(GL_CULL_FACE))     // 关闭背面剔除
skyboxEffect?.draw()
glDepthMask(GLboolean(GL_TRUE))     // 开启深度缓存
glEnable(GLenum(GL_CULL_FACE)) 

// 绘制物体
glUseProgram(sceneProgram)

let modelViewProjection = GLKMatrix4Multiply(baseEffect.transform.projectionMatrix, baseEffect.transform.modelviewMatrix)

glUniformMatrix4fv(glGetUniformLocation(sceneProgram, "u_mvpMatrix"), 1, GLboolean(GL_FALSE), modelViewProjection.array)

// 绑定顶点数组对象
glBindVertexArray(cubeVAOId)
glActiveTexture(GLenum(GL_TEXTURE0))
glBindTexture(GLenum(GL_TEXTURE_2D), cubeTextId)
glUniform1i(glGetUniformLocation(sceneProgram, "colorMap"), 0)
glDrawArrays(GLenum(GL_TRIANGLES), 0, 36)

myContext?.presentRenderbuffer(Int(GL_RENDERBUFFER))

参考

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