OpenGL ES 顶点着色器和片元着色器代码的编译、链接和使用

625 阅读11分钟

今天来探究一下OpenGL ES如何编写顶点着色器和片元着色器的代码,以及如何编译、链接和使用。

一、编写顶点着色器和片元着色器代码

第一步:创建Xcode工程,并创建shaderv.vsh和shaderf.fsh文件

选择File->New->File->Empty 将文件重命名为.vsh后缀的文件,此文件的.vsh后缀名不是给xcode标记为什么文件,而是告诉开发者,此文件是顶点着色器文件。 同样的,生成.fsh后缀的片元着色器文件

第二步:编写顶点着色器代码和片元着色器代码

顶点着色器代码

attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main()
{
    varyTextCoord = textCoordinate;
    gl_Position = position;
}

  • attribute 是修饰类型关键字,只能在顶点着色器代码中使用,用来传递数据; vec4 表示四维向量数据类型;position表示变量名称,用来传递顶点坐标。
  • vec2 表示二维向量数据类型,textCoordinate表示变量名称,用来传递纹理坐标。纹理坐标不会在顶点着色器中使用,这里只是通过顶点着色器桥接给片元着色器。
  • varying 是修饰类型关键字,修饰的变量要从顶点着色器传递到片元着色器;lowp也是修饰类型关键字,表示修饰的vec2使用低精度;varyTextCoord表示要传递片元着色器的纹理坐标
  • varyTextCoord = textCoordinate;表示把外面传进来的纹理坐标传递到片元着色器去。
  • gl_Position = position;表示将外面传进来的顶点坐标赋值给内置变量gl_Position,得到顶点坐标的处理结果。

片元着色器代码

precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

void main()
{
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}
  • precision 修饰类型关键字,highp 修改高精度,用他们来修饰float表示这个文件的float数据使用高精度。
  • varying lowp vec2 varyTextCoord;用来接收顶点着色器传递过来的纹理坐标,这里的定义必须和顶点着色器中的定义完全一样,不然数据就传递不过来。
  • uniform修改类型关键字,smampler2D 修饰的colorMap表示传递进来的纹理数据。
  • gl_FragColor 是片元着色器的内置变量,接收处理后的片元数据。texture2D函数将纹理数据映射到纹理坐标上。

二、编译、链接和使用着色器代码

首先编写加载着色器代码

-(GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
    //1.定义2个零时着色器对象
    GLuint verShader, fragShader;
    //创建program
    GLint program = glCreateProgram();
    
    //2.编译顶点着色程序、片元着色器程序
    //参数1:编译完存储的底层地址
    //参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
    //参数3:文件路径
    [self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    
    //3.创建最终的程序
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    //4.释放不需要的shader
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    return program;
}

编译着色器的代码

//编译shader
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
    
    //1.读取文件路径字符串
    NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    const GLchar* source = (GLchar *)[content UTF8String];
    
    //2.创建一个shader(根据type类型)
    *shader = glCreateShader(type);
    
    //3.将着色器源码附加到着色器对象上。
    //参数1:shader,要编译的着色器对象 *shader
    //参数2:numOfStrings,传递的源码字符串数量 1个
    //参数3:strings,着色器程序的源码(真正的着色器程序源码)
    //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
    glShaderSource(*shader, 1, &source,NULL);
    
    //4.把着色器源代码编译成目标代码
    glCompileShader(*shader);
}

顶点着色器代码和片元着色器代码不像.m文件编写的代码一样,它其实是字符串,我们要想使用它,要进行编译。

编译着色器代码

-(void)comileShader:(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);
    //将着色器代码附着到着色器对象上
    /*
     //参数1:shader,要编译的着色器对象 *shader
     //参数2:numOfStrings,传递的源码字符串数量 1个
     //参数3:strings,着色器程序的源码(真正的着色器程序源码)
     //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
     */
    glShaderSource(*shader, 1, &source, NULL);
    //编译着色器代码
    glCompileShader(*shader);
}

我们已经写好了着色器代码的编译程序,那我们如何使用呢?有以下几个步骤,这几个步骤基础是确定的,不会变化太多

  • 1.设置图层
  • 2.设置上下文
  • 3.清空缓冲区
  • 4.设置RenderBuffer
  • 5.设置FrameBuffer
  • 6.开始使用

下面我们一步一步的实现

导入头文件

#import <OpenGLES/ES2/gl.h>

定义属性

//在iOS和tvOS上绘制OpenGL ES内容的图层,继承与CALayer
@property (nonatomic, strong) CAEAGLLayer *myEagLayer;
//上下文
@property (nonatomic, strong) EAGLContext *myContext;
@property (nonatomic, assign) GLuint myColorRenderBuffer;
@property (nonatomic, assign) GLuint myColorFrameBuffer;
@property (nonatomic, assign) GLuint myPrograme;

1.设置图层

//MARK: 1.设置图层
-(void)setupLayer{
//    重写layerClass,将CCView返回的图层从CALayer替换成CAEAGLLayer
    self.myEagLayer = (CAEAGLLayer *)self.layer;
    
    //设置scale
    [self setContentScaleFactor:[UIScreen mainScreen].scale];
    //设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
    self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
}

注意,必须重写如下方法,否则self.layer无法强转成CAEAGLLayer

+(Class)layerClass{
    return [CAEAGLLayer class];
}

2.设置上下文

-(void)setupContext{
    //创建上下文
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:(kEAGLRenderingAPIOpenGLES2)];
    if (!context) {
        NSLog(@"Create context failed!");
        return;
    }
    //设置上下文
    BOOL result = [EAGLContext setCurrentContext:context];
    if (result == NO) {
        NSLog(@"setCurrentContext failed!");
        return;
    }

    self.myContext = context;
}

3.清空缓存区

-(void)deleteRenderAndFrameBuffer{
    /*
    buffer分为frame buffer 和 render buffer2个大类。
    其中frame buffer 相当于render buffer的管理者。
    frame buffer object即称FBO。
    render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
    */
    glDeleteBuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
    
    glDeleteFramebuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
}

4.设置RenderBuffer

-(void)setupRenderBuffer{
    //定义一个bufferID
    GLuint buffer;
    //申请一个bufferID
    glGenRenderbuffers(1, &buffer);
    
    self.myColorRenderBuffer = buffer;
    
    
    //将bufferID绑定到renderBuffer
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    //调用上下文,将图层绑定到RenderBuffer
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
    
}

RenderBuffer必须要通过上下文与FrameBuffer进行绑定,由FrameBuffer来管理

5.设置FrameBuffer

-(void)setupFrameBuffer{
    //定义一个bufferID
    GLuint buffer;
    //申请一个bufferID;
    glGenFramebuffers(1, &buffer);
    self.myColorFrameBuffer = buffer;
    
    //将bufferID绑定到FrameBuffer
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    //将RendBuffer绑定到GL_COLOR_ATTACHMENT0上,并使用FrameBuffer来管理
    /*
     生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
     调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
    */
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}

6.开始绘制

首先实现从图片上加载纹理

-(GLuint)setupTexture:(NSString *)fileName{
    //将UIImage转换成CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    //判断图片是否获取成功
    if (spriteImage == nil) {
        NSLog(@"Failed to load image %@",fileName);
        exit(1);
    }
    
    //读取图片信息
    //宽高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    //字节数,宽X高X4
    GLubyte *spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
    
    //创建上下文
    /*
    参数1:data,指向要渲染的绘制图像的内存地址
    参数2:width,bitmap的宽度,单位为像素
    参数3:height,bitmap的高度,单位为像素
    参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
    参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
    参数6:colorSpace,bitmap上使用的颜色空间
     kCGImageAlphaPremultipliedLast:RGBA
    */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
    //在CGContextRef上将图片绘制出来
    CGRect rect = CGRectMake(0, 0, width, height);
    //使用默认的方式绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
    //释放上下文
    CGContextRelease(spriteContext);
    //绑定纹理到默认Id,默认打为ID为0的纹理
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //设置纹理参数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//GL_LINEAR线性过滤
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);//邻近过滤
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    //载入纹理数据
    float fw = width,fh = height;
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    //释放
    free(spriteData);
    //这里返回0,表示返回默认打开的纹理Id
    return 0;
}
-(void)renderLayer{
    //清空颜色
    glClearColor(0.3f, 0.45f, 0.5f, 1.0f);
    
    //清空颜色缓冲区
    glClear(GL_COLOR_BUFFER_BIT);
    
    //设置视口大小
    CGFloat scale = [UIScreen mainScreen].scale;
    //UIscreen获得的是逻辑大小 openGL的作用范围是像素 所以要乘缩放因子
    glViewport(self.frame.origin.x*scale, self.frame.origin.y*scale, self.frame.size.width*scale, self.frame.size.height*scale);
    //读取顶点着色器和片元着色器程序
    NSString *vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
    //加载着色器
    self.myPrograme = [self loadShaders:vertFile withFrag:fragFile];
    
    //链接着色器代码
    glLinkProgram(self.myPrograme);
    //获取链接结果
    GLint linkStatus;
    glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        //获取错误信息,并打印
        GLchar message[512];
        glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
        NSString *msgString = [NSString stringWithUTF8String:message];
        NSLog(@"Programe Link Error:%@",msgString);
        return;
    }
    NSLog(@"Program link Success!");
    
    //使用program
    glUseProgram(self.myPrograme);

    //设置顶点坐标和纹理坐标
    GLfloat attrArr[] = {
        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,
    };
    //将顶点数据和纹理数据copy到缓冲区
    GLuint attrBuffer;
    glGenBuffers(1, &attrBuffer);
    //将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
    glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
    //把顶点数据从CPU内存复制到GPU上
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
    
    //8.将顶点数据通过myPrograme中的传递到顶点着色程序的position
    //1.glGetAttribLocation,用来获取vertex attribute的入口的.
    //2.告诉OpenGL ES,通过glEnableVertexAttribArray,
    //3.最后数据是通过glVertexAttribPointer传递过去的。
    //将顶点坐标通过myPrograme传递到顶点着色器的position上
    GLuint position = glGetAttribLocation(self.myPrograme, "position");
    //打开读取数据的通道
    glEnableVertexAttribArray(position);
    
    //设置读取的格式
    /**
     //参数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(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
    
    //设置纹理数据
    //(1).glGetAttribLocation,用来获取vertex attribute的入口的.
    //注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
    GLuint textCoord = glGetAttribLocation(self.myPrograme, "textCoordinate");
    //置合适的格式从buffer里面读取数据
    glEnableVertexAttribArray(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(textCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL+3);
    
    
    //加载纹理
    [self setupTexture:@"kunkun"];
    //设置纹理采样器
    glUniform1i(self.myPrograme, "colorMap");
//    绘制
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    //从渲染缓冲区显示到屏幕上
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}

最后重写layoutSubViews

-(void)layoutSubviews{
    //1.设置图层
    [self setupLayer];
    //2.设置上下文
    [self setupContext];
    //3.清空缓冲区
    [self deleteRenderAndFrameBuffer];
    //4.设置RenderBuffer
    [self setupRenderBuffer];
    //5.设置FrameBuffer
    [self setupFrameBuffer];
    //6.开始使用
    [self renderLayer];
}

这样我们就成功编译、链接并使用了顶点着色器和片元着色器的代码来加载一张图片,效果如下:

可是图片居然是倒过来的,下面我们来解决纹理倒置的问题

解决纹理倒置的几个方法

将顶点坐标翻转180度

1.修改顶点着色器,传一个旋转矩阵

attribute vec4 position;
attribute vec2 textCoordinate;
uniform mat4 rotateMatrix;
varying lowp vec2 varyTextCoord;

void main(){
    varyTextCoord = textCoordinate;
    vec4 vPos = position;
    vPos = vPos * rotateMatrix;
    gl_Position = vPos;
}

2.实现旋转矩阵,并将其传递给顶点着色器

/*
 注意,想要获取shader里面的变量,这里记得要在glLinkProgram后面,后面,后面!
 */
-(void)rotateTextureImage{
    //思路:将顶点旋转180度
    //获取旋转的弧度
    float radius = 180.0 * M_PI / 180.0;
    //得到对应的sin和cos
    float s = sin(radius);
    float c = cos(radius);
    /*
     OpenGL ES是列向量,如:(x,
                           y,
                           z,
                           a)
     要想使得它乘以一个矩阵后x,y,变化,而z,a不变,根据矩阵的乘法原则,则这个矩阵要符合如下结构
     { i,j,0,0
       w,v,0,0
       0,0,1,0
       0,0,0,1
     }
     */
    GLfloat zRotation[16] = {
        c,-s,0,0,
        s,c,0,0,
        0,0,1,0,
        0,0,0,1
    };
    //拿到着色器传递旋转矩阵的通道,"rotateMatrix"就是shaderv.vsh文件里面的“uniform mat rotateMatrix”
    GLuint rotate = glGetUniformLocation(self.myPrograme, "rotateMatrix");
    //设置旋转矩阵
    /*
     location : 对于shader 中的ID
     count : 个数
     transpose : 转置
     value : 指针
     */
    glUniformMatrix4fv(rotate, 1, GL_FALSE, zRotation);
    
}

3.在绘制之前调用上面方法

-(void)renderLayer{
   ...
    [self rotateTextureImage];
    //绘制
    glDrawArrays(GL_TRIANGLES, 0, 6);
   ...
}

这种方式虽然能解决问题,但是每次渲染时都要对所有的顶点进行一次旋转,增加了大量的计算,而且修改顶点着色器的代码,顶点着色器的代码应该逻辑越简单越好,计算越少越好!

获取纹理数据时将纹理进行旋转,得到正纹理

在绘制之前,先平移,再翻转,再平移

-(GLuint)setupTexture:(NSString *)fileName{
    ...
    //解决纹理倒置
    CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);
    CGContextScaleCTM(spriteContext, 1.0, -1.0);
    CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
    //使用默认的方式绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
    ...
}

以上四个步骤对应如下图:

先将纹理平移至要显示的位置,再沿y轴方向平移图片的高度,再等比例缩放,缩放时1代表等比例,-1翻转,最后再平称到显示的位置上。

在片元点着色器中,将纹理坐标进行翻转

varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

void main()
{
    //gl_FragColor = texture2D(colorMap, varyTextCoord);
    gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));
}

在顶点着色器中,将纹理坐标进行翻转

attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main()
{
    //varyTextCoord = textCoordinate;
    varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y);
    gl_Position = position;
}

绘制的时候,对纹理坐标进行翻转

//6.开始绘制
-(void)renderLayer{
    ... 
    
     //6.设置顶点、纹理坐标
    //前3个是顶点坐标,后2个是纹理坐标
//    GLfloat attrArr[] =
//    {
//        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,
//    };
    
    GLfloat attrArr[] =
    {
        0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
        -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
        -0.5f, -0.5f, -1.0f,    0.0f, 1.0f,
        
        0.5f, 0.5f, -1.0f,      1.0f, 0.0f,
        -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
        0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
    };
    ...
}

代码下载地址:链接: pan.baidu.com/s/15LqW8KZw… 密码: w4so