iOS视觉-- (04) OpenGL ES+GLSL实现金字塔解析

129 阅读10分钟

前面一篇我们了解了OpenGL渲染一张图片的过程。接下来我们要了解的是使用GLSL如何渲染金字塔以及一些简单的变换。先看效果图 效果图

步骤还是和之前是一样的。

    1. 日常开发中OpenGL开发流程
  • 1.设置图层
  • 2.设置图形上下文
  • 3.设置渲染缓冲区(renderBuffer)
  • 4.设置帧缓冲区(frameBuffer)
  • 5.编译、链接着色器(shader)
  • 6.设置VBO (Vertex Buffer Objects)
  • 7.设置纹理
  • 8.渲染

前5步除了着色器外和第7步是一致的代码,就不贴了。

  • 顶点着色器代码:
attribute vec4 position;
attribute vec4 positionColor; //顶点颜色
attribute vec2 textCoordinate; //纹理坐标
uniform mat4 projectionMatrix; //投影矩阵
uniform mat4 modelViewMatrix;  //模型视图矩阵

varying lowp vec4 varyColor; //顶点颜色
varying lowp vec2 varyTextCoord; //传递给片元着色器纹理坐标

void main()
{
    varyColor = positionColor;
    varyTextCoord = textCoordinate;
    
    vec4 vPos;
    vPos = projectionMatrix * modelViewMatrix * position;
    gl_Position = vPos;
}
  • 片元着色器代码:
varying lowp vec4 varyColor; //顶点颜色
varying lowp vec2 varyTextCoord; //顶点着色器传递过来的纹理坐标

uniform sampler2D colorMap; //纹理

void main()
{
    gl_FragColor = texture2D(colorMap, varyTextCoord) * varyColor;
}
    1. 设置VBO (Vertex Buffer Objects)

金字塔一共有5个面:4面+底面(正方形) = 6个三角形

框架图

    //6.设置VBO (Vertex Buffer Objects)
    func setupVBO() {
        //6.设置顶点、纹理坐标
        //顶点数组
        //前3个元素,是顶点数据;中间3个元素,是顶点颜色值,最后2个是纹理坐标
        let attrArr: [GLfloat] = [
            -0.5, 0.5, 0.0,      0.0, 0.0, 0.5,       0.0, 1.0,//左上
            0.5, 0.5, 0.0,       0.0, 0.5, 0.0,       1.0, 1.0,//右上
            -0.5, -0.5, 0.0,     0.5, 0.0, 1.0,       0.0, 0.0,//左下
            0.5, -0.5, 0.0,      0.0, 0.0, 0.5,       1.0, 0.0,//右下
            0.0, 0.0, 1.0,       1.0, 1.0, 1.0,       0.5, 0.5,//顶点
        ]
        
        //创建绘制索引数组
        let indices: [GLuint] = [
            0, 3, 2,
            0, 1, 3,
            0, 2, 4,
            0, 4, 1,
            2, 3, 4,
            1, 4, 3,
        ]
        self.indices = indices
        
        //-----处理顶点数据--------
        //顶点缓存区
        var attrBuffer: GLuint = 0
        //申请一个缓存区标识符
        glGenBuffers(1, &attrBuffer)
        //将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), attrBuffer)
        //把顶点数据从CPU拷贝到GPU上
        glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * attrArr.count, attrArr, GLenum(GL_DYNAMIC_DRAW))
    }
  • 8.渲染绘制

我这么这里是通过索引来绘制的,这里我们绘制的是一个立体的图形,所以我们引入了,OpenGL的另一个知识点,就是投影和变换,都是通过矩阵来实现的。这里有两个重点的知识点。

    1. 投影有两种方式:正交投影(2D)、透视投影(3D)
    1. MVP矩阵模型矩阵、观察矩阵、投影矩阵 视图矩阵:将虚拟空间中的所有的物体都做缩放,旋转,移动的操作。视图矩阵和 model 矩阵的不同点在于 model 矩阵作用于一个物体上,视图矩阵作用于空间中所有的物体上。 setLookAtM(viewMatrix, 0, 0f, 0f, 1f, // 眼睛的位置 0f, 0f, -1f, // 眼睛看的方向 0f, 1f, 0f // 视线的法线 ); // 获得一个视图矩阵 MVP矩阵的推导
    //8.开始绘制
    func renderLayer() {
        //设置清屏颜色
        glClearColor(0.0, 0.0, 1.0, 1.0)
        //清除屏幕
        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
        
        //1.设置视口大小
        let scale = UIScreen.main.scale
        glViewport(GLint(self.frame.origin.x * scale), GLint(self.frame.origin.y * scale), GLsizei(self.frame.size.width * scale), GLsizei(self.frame.size.height * scale))

        //使用着色器
        glUseProgram(myProgram)

#warning("注意⚠️:想要获取shader里面的变量,这里要记住要在glLinkProgram后面、后面、后面")
        /*
         一个一致变量在一个图元的绘制过程中是不会改变的,所以其值不能在glBegin/glEnd中设置。一致变量适合描述在一个图元中、一帧中甚至一个场景中都不变的值。一致变量在顶点shader和片段shader中都是只读的。首先你需要获得变量在内存中的位置,这个信息只有在连接程序之后才可获得。
         */
        
        //--------处理顶点数据-------
        //1.将顶点数据通过myProgram中的传递到顶点着色程序的position
        let position = glGetAttribLocation(myProgram, "position")
        //2.
        glEnableVertexAttribArray(GLuint(position))
        
        //3.设置读取方式
        //参数1:index,顶点数据的索引
        //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
        //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
        //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
        //参数5:stride,连续顶点属性之间的偏移量,默认为0;
        //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
        glVertexAttribPointer(GLuint(position), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 8), nil)
        
        //--------处理顶点颜色值-------
        //1.将顶点数据通过myProgram中的传递到顶点着色程序的positionColor
        let positionColor = glGetAttribLocation(myProgram, "positionColor")
        glEnableVertexAttribArray(GLuint(positionColor))
        glVertexAttribPointer(GLuint(positionColor), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 8), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 3))
        
        
        //----处理纹理数据-------
        //1.glGetAttribLocation,用来获取vertex attribute的入口的.
        //注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
        let textCoord = glGetAttribLocation(myProgram, "textCoordinate")
        
        //设置合适的格式从buffer里面读取数据
        glEnableVertexAttribArray(GLuint(textCoord))
        
        //3.设置读取方式
        //参数1:index,顶点数据的索引
        //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
        //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
        //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
        //参数5:stride,连续顶点属性之间的偏移量,默认为0;
        //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
        glVertexAttribPointer(GLuint(textCoord), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout<GLfloat>.size * 8), UnsafeRawPointer(bitPattern: MemoryLayout<GLfloat>.size * 6))
        
        
        //----处理矩阵数据-------
        //找到myProgram中的projectionMatrix、modelViewMatrix 2个矩阵的地址。如果找到则返回地址,否则返回-1,表示没有找到2个对象。
        let projectionMatrixSlot = glGetUniformLocation(myProgram, "projectionMatrix")
        let modelViewMatrixSlot = glGetUniformLocation(myProgram, "modelViewMatrix")
        
        let width = self.frame.size.width
        let height = self.frame.size.height
        
        //创建4 * 4矩阵 获取单元矩阵
        var _projectionMatrix: GLKMatrix4 = GLKMatrix4Identity
        
        //计算纵横比例 = 长/宽
        let aspect = width / height; //长宽比
        
        //获取透视矩阵
        /*
         参数1:矩阵
         参数2:视角,度数为单位
         参数3:纵横比
         参数4:近平面距离
         参数5:远平面距离
         参考PPT
         */
        //源码实现:在这里面
        //        ksPerspective(<#T##result: UnsafeMutablePointer<KSMatrix4>!##UnsafeMutablePointer<KSMatrix4>!#>, <#T##fovy: Float##Float#>, <#T##aspect: Float##Float#>, <#T##nearZ: Float##Float#>, <#T##farZ: Float##Float#>)
        let perspectiveMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(30), Float(aspect), 5, 20)
        _projectionMatrix = GLKMatrix4Multiply(_projectionMatrix, perspectiveMatrix)
        
        //设置glsl里面的投影矩阵
        /*
         void glUniformMatrix4fv(GLint location,  GLsizei count,  GLboolean transpose,  const GLfloat *value);
         参数列表:
         location:指要更改的uniform变量的位置
         count:更改矩阵的个数
         transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
         value:执行count个元素的指针,用来更新指定uniform变量
         */
//        let count = MemoryLayout.size(ofValue: _projectionMatrix.m) / MemoryLayout.size(ofValue: _projectionMatrix.m.0)
//        withUnsafePointer(to: &_projectionMatrix.m) { (pointer) in
//            pointer.withMemoryRebound(to: GLfloat.self, capacity: count, { (pon) in
//                glUniformMatrix4fv(projectionMatrixSlot, 1, GLboolean(GL_FALSE), pon)
//            })
//        }
        glUniformMatrix4fv(projectionMatrixSlot, 1, GLboolean(GL_FALSE), &_projectionMatrix.m.0)
        
        //开启剔除操作效果 (三角形逆时针方向为正面)
        glEnable(GLenum(GL_CULL_FACE))
        
        //创建一个4 * 4 矩阵,模型视图
        var _modelViewMatrix: GLKMatrix4 = GLKMatrix4Identity
        //平移,z轴平移-10
        _modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix, 0.0, 0.0, -10.0)
        
        //创建一个4 * 4 矩阵,旋转矩阵
        var _rotationMatrix: GLKMatrix4 = GLKMatrix4Identity
        //旋转
        _rotationMatrix = GLKMatrix4Rotate(_rotationMatrix, GLKMathDegreesToRadians(xDegree), 1.0, 0.0, 0.0)
        _rotationMatrix = GLKMatrix4Rotate(_rotationMatrix, GLKMathDegreesToRadians(yDegree), 0.0, 1.0, 0.0)
        _rotationMatrix = GLKMatrix4Rotate(_rotationMatrix, GLKMathDegreesToRadians(zDegree), 0.0, 0.0, 1.0)
        
        //注意⚠️⚠️⚠️:把变换矩阵相乘,注意先后顺序 ,将平移矩阵与旋转矩阵相乘,结合到模型视图
        _modelViewMatrix = GLKMatrix4Multiply(_modelViewMatrix, _rotationMatrix)
        
        // 加载模型视图矩阵 modelViewMatrixSlot
        //设置glsl里面的投影矩阵
        /*
         void glUniformMatrix4fv(GLint location,  GLsizei count,  GLboolean transpose,  const GLfloat *value);
         参数列表:
         location:指要更改的uniform变量的位置
         count:更改矩阵的个数
         transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
         value:执行count个元素的指针,用来更新指定uniform变量
         */
//        let count1 = MemoryLayout.size(ofValue: _modelViewMatrix.m) / MemoryLayout.size(ofValue: _modelViewMatrix.m.0)
//        withUnsafePointer(to: &_modelViewMatrix.m) { (pointer) in
//            pointer.withMemoryRebound(to: GLfloat.self, capacity: count1, { (pon) in
//                glUniformMatrix4fv(modelViewMatrixSlot, 1, GLboolean(GL_FALSE), pon)
//            })
//        }
        glUniformMatrix4fv(modelViewMatrixSlot, 1, GLboolean(GL_FALSE), &_modelViewMatrix.m.0)
        
        //使用索引绘图
        /*
         void glDrawElements(GLenum mode,GLsizei count,GLenum type,const GLvoid * indices);
         参数列表:
         mode:要呈现的画图的模型
                    GL_POINTS
                    GL_LINES
                    GL_LINE_LOOP
                    GL_LINE_STRIP
                    GL_TRIANGLES
                    GL_TRIANGLE_STRIP
                    GL_TRIANGLE_FAN
         count:绘图个数
         type:类型
                 GL_BYTE
                 GL_UNSIGNED_BYTE
                 GL_SHORT
                 GL_UNSIGNED_SHORT
                 GL_INT
                 GL_UNSIGNED_INT
         indices:绘制索引数组

         注意:⚠️⚠️⚠️
         glArrayElements()、glDrawElements()和glDrawRangeElements()能够对数据数组进行随机存取,
         但是glDrawArrays()只能按顺序访问它们。因为前者支持顶点索引的机制
         */
        let dotCount = MemoryLayout<GLfloat>.size * indices.count / MemoryLayout<GLfloat>.size
        glDrawElements(GLenum(GL_TRIANGLES), GLsizei(dotCount), GLenum(GL_UNSIGNED_INT), indices)
        
        myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
        
    }

到此,金字塔就完成了。那么正方体又该如何渲染呢?聪明的同学已经知道了。那就是,修改我们的顶点数据。接下来一起走进正方体的渲染。 金字塔Demo


    //6.设置VBO (Vertex Buffer Objects)
    func setupVBO() {
        //------------- 正方体 -------------
        let attrArr: [GLfloat] = [
            // 顶点:(x, y, z)      颜色:(r, g, b)      纹理: (s, t)
            // 前面
            -0.5, 0.5, 0.5,        1.0, 1.0, 1.0,       0.0, 0.0, // 前左上 0
            -0.5, -0.5, 0.5,       1.0, 1.0, 1.0,       0.0, 1.0, // 前左下 1
            0.5, -0.5, 0.5,        1.0, 1.0, 1.0,       1.0, 1.0, // 前右下 2
            0.5, 0.5, 0.5,         1.0, 1.0, 1.0,       1.0, 0.0, // 前右上 3
            // 后面
            -0.5, 0.5, -0.5,        1.0, 1.0, 1.0,       0.0, 1.0, // 后左上 4
            -0.5, -0.5, -0.5,       1.0, 1.0, 1.0,       0.0, 0.0, // 后左下 5
            0.5, -0.5, -0.5,        1.0, 1.0, 1.0,       1.0, 0.0, // 后右下 6
            0.5, 0.5, -0.5,         1.0, 1.0, 1.0,       1.0, 1.0, // 后右上 7
            // 左面
            -0.5, 0.5, -0.5,        1.0, 1.0, 1.0,       0.0, 0.0, // 后左上 8
            -0.5, -0.5, -0.5,       1.0, 1.0, 1.0,       0.0, 1.0, // 后左下 9
            -0.5, 0.5, 0.5,        1.0, 1.0, 1.0,       1.0, 0.0, // 前左上 10
            -0.5, -0.5, 0.5,       1.0, 1.0, 1.0,       1.0, 1.0, // 前左下 11
            // 右面
            0.5, 0.5, 0.5,         1.0, 1.0, 1.0,       0.0, 0.0, // 前右上 12
            0.5, -0.5, 0.5,        1.0, 1.0, 1.0,       0.0, 1.0, // 前右下 13
            0.5, -0.5, -0.5,        1.0, 1.0, 1.0,       1.0, 1.0, // 后右下 14
            0.5, 0.5, -0.5,         1.0, 1.0, 1.0,       1.0, 0.0, // 后右上 15
            // 上面
            -0.5, 0.5, -0.5,        1.0, 1.0, 1.0,       0.0, 0.0, // 后左上 16
            -0.5, 0.5, 0.5,        1.0, 1.0, 1.0,       0.0, 1.0, // 前左上 17
            0.5, 0.5, 0.5,         1.0, 1.0, 1.0,       1.0, 1.0, // 前右上 18
            0.5, 0.5, -0.5,         1.0, 1.0, 1.0,       1.0, 0.0, // 后右上 19
            // 下面
            -0.5, -0.5, 0.5,       1.0, 1.0, 1.0,       0.0, 0.0, // 前左下 20
            0.5, -0.5, 0.5,        1.0, 1.0, 1.0,       1.0, 0.0, // 前右下 21
            -0.5, -0.5, -0.5,       1.0, 1.0, 1.0,       0.0, 1.0, // 后左下 22
            0.5, -0.5, -0.5,        1.0, 1.0, 1.0,       1.0, 1.0, // 后右下 23
        ]
        
        //创建绘制索引数组
        let indices: [GLuint] = [
            // 前面
            0, 1, 2,
            0, 2, 3,
            // 后面
            4, 6, 5,
            4, 7, 6,
            // 左面
            8, 9, 11,
            8, 11, 10,
            // 右面
            12, 13, 14,
            12, 14, 15,
            // 上面
            16, 17, 18,
            16, 18, 19,
            // 下面
            20, 22, 23,
            20, 23, 21,
        ]
        
        
        self.indices = indices
        
        //-----处理顶点数据--------
        //顶点缓存区
        var attrBuffer: GLuint = 0
        //申请一个缓存区标识符
        glGenBuffers(1, &attrBuffer)
        //将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), attrBuffer)
        //把顶点数据从CPU拷贝到GPU上
        glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<GLfloat>.size * attrArr.count, attrArr, GLenum(GL_DYNAMIC_DRAW))
    }

但是会出现一种比较奇怪的现象:

奇怪现象1

原因:是开启了背面剔除

关闭之后还是有其他的奇怪现象,就是感觉被遮挡,如下图: 奇怪现象2

被遮挡了,怎么办?--> 开启深度测试。因为开启深度测试后, OpenGL 就不会再去绘制模型被遮挡的部分。

深度测试

    //4.设置FrameBuffer
    func setupFrameBuffer() {
        //1.定义一个缓存区
        var buffer: GLuint = 0
        //2.申请一个缓存区标志
        glGenFramebuffers(1, &buffer)
        //3.将标识符绑定到GL_FRAMEBUFFER
        glBindFramebuffer(GLenum(GL_FRAMEBUFFER), buffer)
        //4.
        frameBuffer = buffer
        
        //生成空间之后,则需要将renderbuffer跟framebuffer进行绑定,调用glFramebufferRenderbuffer函数进行绑定,后面的绘制才能起作用
        //5.将_renderBuffer 通过glFramebufferRenderbuffer函数绑定到GL_COLOR_ATTACHMENT0上。
        glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), renderBuffer)
        
        #warning("设置深度测试")
        // 设置深度调试
        var width: GLint = 0
        var height: GLint = 0
        glGetRenderbufferParameteriv(GLenum(GL_RENDERBUFFER), GLenum(GL_RENDERBUFFER_WIDTH), &width)
        glGetRenderbufferParameteriv(GLenum(GL_RENDERBUFFER), GLenum(GL_RENDERBUFFER_HEIGHT), &height)

        var depthRenderBuffer: GLuint = 0
        // 申请深度渲染缓存
        glGenRenderbuffers(1, &depthRenderBuffer)
        glBindRenderbuffer(GLenum(GL_RENDERBUFFER), depthRenderBuffer)
        // 设置深度测试的存储信息
        glRenderbufferStorage(GLenum(GL_RENDERBUFFER), GLenum(GL_DEPTH_COMPONENT16), width, height)

        // 将渲染缓存挂载到GL_DEPTH_ATTACHMENT这个挂载点上
        glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_DEPTH_ATTACHMENT), GLenum(GL_RENDERBUFFER), depthRenderBuffer)
        // GL_RENDERBUFFER绑定的是深度测试渲染缓存,所以要绑定回色彩渲染缓存
        glBindRenderbuffer(GLenum(GL_RENDERBUFFER), renderBuffer)
        
        //接下来,可以调用OpenGL ES进行绘制处理,最后则需要在EGALContext的OC方法进行最终的渲染绘制。这里渲染的color buffer,这个方法会将buffer渲染到CALayer上。- (BOOL)presentRenderbuffer:(NSUInteger)target;
    }

申请了深度缓冲区之后还要开启,默认是关闭的。所以在渲染的时候进行开启:

//清除屏幕
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
// 开启深度测试
glEnable(GLenum(GL_DEPTH_TEST))

效果图


  • 问题是解决了。但是为什么开启背后剔除还是不行? 奇怪现象

想到了是背后剔除,那就是可能顶点数据有问题。背面的三角形与正面的三角形的顶点顺序相反

顶点顺序相反

修改背面顶点索引顺序:

 // 后面
 4, 6, 5,
 4, 7, 6,

也是可以达到效果的,而且还开了背面剔除。那么应该不需要开启深度测试了为什么呢?

我猜想是因为没修改顶点顺序之前,如上图⬆️ 正面:[1, 2, 3],背面:[1,3, 2],OpenGL也以为是正面,但是实际是相反的。

效果图

到此正方体渲染就结束了。可能有些同学会想到**正方体每个面怎么贴不一样的图片呢?**那我们下回分解。