美文网首页
OpenGL ES学习笔记2: 使用shader(着色器)展现图

OpenGL ES学习笔记2: 使用shader(着色器)展现图

作者: 南华coder | 来源:发表于2017-02-22 10:33 被阅读465次

    <h5>1、先上效果图</h5>

    效果图.png

    <h5> 背景知识</h5>

    • 不采用GLKit框架,编译链接自定义的着色器(shader),用glsl语言来实现着色器,并做简单的图形变换。
    • 着色器有两个:一个是顶点着色器,一个是片段着色器。顶点着色器负责顶点的位置、颜色和纹理坐标等属性,片元(fragment)着色器处理像素点颜色。

    <h5>2、实现思路</h5>

    1)设置CAEAGLLayer图层
    2)创建OpenGL ES上下文

    1. 设置渲染和帧缓存区
      4)编译顶点着色器和片段着色器,链接成着色器程序
      5)为着色器中的输入参数赋值
      6)加载纹理
      7)渲染

    <h5>3、主要代码</h5>

    - (instancetype)initWithFrame:(CGRect)frame{
    
        self = [super initWithFrame:frame];
        if (self) {
            [self setupLayer];
            [self setupContext];
            [self setupRenderBuffer];
            [self setupFrameBuffer];
            GLuint shader = [self compileShader];
            [self setupValueForShader:shader];
            [self setupTexture:@"ic_dog.jpeg"];
            [self render];
        }
        return self;
    }
    

    <h5>4、主要代码分析</h5>

    • 代码中最关键的是:编译和链接着色器程序 + 加载纹理

    <h5>4-1、主要代码分析之 编译着色器,链接成着色器程序 </h5>

    1) 顶点着色器

    attribute vec4 position; //输入参数1 (位置坐标)
    attribute vec2 textCoordinate;  //输入参数2 (纹理坐标)
    uniform mat4 rotateMatrix;  //全局参数
    varying lowp vec2 varyTextCoord; //纹理坐标
    
    void main()
    {
        varyTextCoord = vec2(textCoordinate.x,1.0 - textCoordinate.y); //解决纹理上下颠倒的问题
        gl_Position = position * rotateMatrix;
    }
    

    2)片元着色器

    varying lowp vec2 varyTextCoord;
    uniform sampler2D colorMap; //2d纹理采样器,默认的激活纹理单元(0),没有分配值
    void main()
    {
        gl_FragColor = texture2D(colorMap, varyTextCoord); //采样纹理的颜色,第一个参数是纹理采样器,第二个参数是对应的纹理坐标
    }
    

    着色器代码说明

    • 每个着色器都是各自独立的小程序,但是它们都是整体的一部分,每个着色器都有输入和输出。attribute修饰的是输入参数。varyTextCoord,gl_Position是顶点着色器的输出,gl_FragColor是片元着色器的输出
    • 顶点着色器和片元着色器有一个相同的声明变量varying lowp vec2 varyTextCoord,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

    3)着色器编译和链接部分的代码

    /**
     *  glsl的编译过程主要有glCompileShader、glAttachShader、glLinkProgram三步;
     *
     *  @return 编译成功的shaders
     */
    - (GLuint)compileShader{
    
        GLuint verShader, fragShader;
        GLint program = glCreateProgram();
    
        //1、编译shader
        NSString* vertFilePath = [[NSBundle mainBundle] pathForResource:kVertexFileName ofType:@"glsl"];
        [self compileShader:&verShader type:GL_VERTEX_SHADER file:vertFilePath];
    
        NSString* fragFilePath = [[NSBundle mainBundle] pathForResource:kFragmentFileName ofType:@"glsl"];
        [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragFilePath];
    
        glAttachShader(program, verShader);
        glAttachShader(program, fragShader);
    
        //2、链接
        glLinkProgram(program);
        GLint linkSuccess;
        glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
        if (linkSuccess == GL_FALSE) { //连接错误
            GLchar messages[256];
            glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
            NSString *messageString = [NSString stringWithUTF8String:messages];
            NSLog(@"error%@", messageString);
            return 0;
        }
        else {
            NSLog(@"link ok");
            glUseProgram(program); //激活着色器,成功便使用,避免由于未使用导致的的bug
        }
    
        //3、释放不需要的shader
        glDeleteShader(verShader);
        glDeleteShader(fragShader);
    
        return program;
    }
    
     /**
      编译shader功能函数
     */
    - (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file {
        //读取字符串
        NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
        const GLchar* source = (GLchar *)[content UTF8String];
    
        *shader = glCreateShader(type);
        glShaderSource(*shader, 1, &source, NULL);
       glCompileShader(*shader);
    
        GLint compileSuccess;
        glGetShaderiv(*shader, GL_COMPILE_STATUS, &compileSuccess);
        if (compileSuccess == GL_FALSE) {
            GLchar messages[256];
            glGetShaderInfoLog(*shader, sizeof(messages), 0, &messages[0]);
            NSString *messageString = [NSString stringWithUTF8String:messages];
            NSLog(@"%@", messageString);
            exit(1);
        }
    }
    

    <h5>4-2、主要代码分析之 加载纹理 </h5>

    • 纹理(texture):纹理是一个用来保存图像的颜色元素值的OpenGL ES缓存。可以用来添加物体的细节。纹理可以使用任意图像。但是纹理的缓存中保存的颜色值可能要耗费很大的内存。应尽量使用最小的图像来产生可以接受的渲染结果。

    • 纹素(texel):保存颜色数据,不同于像素是计算机屏幕上一个实际的颜色点,它是存在于一个虚拟的没有尺寸的数学坐标系中。

    • 纹理坐标系:有S轴和T轴,S轴和T轴上取值都是0.0-1.0。

    • 片元(fragment):帧缓存中的颜色像素。当OpenGL ES没有使用纹理时候,GPU根据包含该片元的对象顶点(信息)来计算每个片元的颜色。当设置了纹理后,使用当前绑定的纹理缓存中的纹素来计算每个片元的颜色。

    • 采样(Sampling): GPU根据计算出片元的s,t位置,从绑定的纹理中选择纹素。

    • 加载纹理需要四步

    1) 对齐纹理和顶点,以便GPU知道每个片元的颜色由哪些纹理决定。这种对齐又叫映射(mapping)。扩展每个顶点的数据来实现:x,y,z坐标之外增加是s,t坐标。x,y,z坐标取值[-1,1],s,t坐标取值[0,1]。如果s,t超出[0,1],就需要设置纹理环绕

    //前三个是顶点坐标(x,y,z), 后面两个是纹理坐标(s,t)
    static GLfloat vertices[] =
    {
          0.5f, -0.5f, -1.0f,     1.0f, 0.0f,  //右下
          -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,  //左上
          -0.5f, -0.5f, -1.0f,    0.0f, 0.0f,  //左下
          0.5f, 0.5f, -1.0f,      1.0f, 1.0f,  //右上
          -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,  //左上
          0.5f, -0.5f, -1.0f,     1.0f, 0.0f,  //右下
    };
    

    2) 使用CoreGraphics把图像转换成bitmap data

    // 获取图片的CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }
    
    // 读取图片的大小
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte)); //rgba共4个byte
    
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,
                                                       CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    //在CGContextRef上绘图
    CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
    CGContextRelease(spriteContext);
    

    3) 纹理采样(设置纹理环绕和过滤方式)

    GLuint texture;
    glGenBuffers(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    
    //纹理环绕
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);   //超出s轴的部分,会重复纹理坐标的边缘
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);   //超出t轴的部分,会重复纹理坐标的边缘
    
    //纹理过滤
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );   //纹理缩小,采用邻近过滤(GL_NEAREST)
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );   //纹理放大,采用线性过滤(GL_LINEAR)
    

    <h5>重要函数说明</h5>

    • glTexParameteri (GLenum target, GLenum pname, GLint param);
    设置把纹素映射成像素的方式.
    //参数1:  指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D
    //参数2:  纹理参数名称,可以是GL_TEXTURE_MAG_FILTER(放大过滤) 、GL_TEXTURE_MIN_FILTER(缩小过滤) 、GL_TEXTURE_WRAP_S((环绕)纹理S轴)、GL_TEXTURE_WRAP_T((环绕)纹理T轴)     
    //参数3:纹理过滤方式或环绕方式              
    

    a) 环绕方式说明

    纹理坐标的范围通常是从(0, 0)到(1, 1),如果我们把纹理坐标设置在范围之外,OpenGL ES默认的行为是重复这个纹理图像(GL_REPEAT),
    这种行为成为环绕方式。支持的环绕方式如下:

    TextureWrapMode 描述
    GL_REPEAT 对纹理的默认行为。重复纹理图像。
    GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
    GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。

    b) 环绕过滤说明

    环绕过滤是用来处理纹素的数量与需要被着色的片元数量之间不匹配的情况,过滤方式有:

    TextureMagFilter 描述
    GL_NEAREST(Nearest Neighbor Filtering) 邻近过滤,默认的纹理过滤方式,OpenGL会选择中心点最接近纹理坐标的那个纹素的颜色。
    GL_LINEAR(linear Filtering) 线性过滤,OpenGL会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。

    总之
    为GL_TEXTURE_MIN_FILTER设置GL_NEAREST时候,表明当多个纹素对应一个片元时候,从多纹素个中取色,选择中心点最接近纹理坐标的纹素的颜色作为片元的颜色;
    为GL_TEXTURE_MAG_FILTER设置GL_LINEAR时候,表明没有足够纹素映射到片元的时候,混合附近纹素的颜色来计算片元的颜色。

    4) 加载图片形成纹理

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    

    <h5>重要函数说明</h5>

    • glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid pixels)*

      加载图片形成纹理
      //参数1:指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
      //参数2:纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
      //参数3:告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
      //参数4,5设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
      //参数6: 总是被设为0(历史遗留问题)。
      //参数7和8定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组。
      //参数9:真正的图像数据。

    具体代码请看:QSOpenGLES002

    参考
    iOS开发-OpenGL ES入门教程2
    (译)OpenGL ES2.0 – Iphone开发指引
    OpenGL纹理上下颠倒翻转的三种解决办法

    相关文章

      网友评论

          本文标题:OpenGL ES学习笔记2: 使用shader(着色器)展现图

          本文链接:https://www.haomeiwen.com/subject/vybuwttx.html