这是一篇OpenGlES 系统学习教程,记录自己的学习过程。
环境: Xcode10.2.1 + OpenGL ES 3.0
目标: 天空盒
代码已上传github
,Tutorial-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.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))
参考
- OpenGL编程指南(第9版)
- LearnOpenGL教程的中文翻译
- https://blog.csdn.net/wangdingqiaoit/article/details/52506893