OpenGL 十

案例:使用编译链接自定义的着色器(shader),用简单的 glsl 语言来实现顶点、片元着色器,绘制图形并进行简单的变换。

思路:

 1.创建图层

 2.创建上下文

 3.清空缓存区

 4.设置 RenderBuffer

 5.设置 FrameBuffer

 6.开始绘制

Demo 

一、准备工作

步骤 1~5

 1 - (void)layoutSubviews {
 2     
 3     // 1. 创建设置图层
 4     // 设置 layer
 5     self.myEGLLayer = (CAEAGLLayer *)self.layer;
 6     
 7     // 设置 scale
 8     [self setContentScaleFactor:[[UIScreen mainScreen] scale]];
 9     
10     // 设置属性
11     /*
12      kEAGLDrawablePropertyRetainedBacking:绘图表面显示后,是否保留其内容。
13      kEAGLDrawablePropertyColorFormat:可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
14      
15      kEAGLColorFormatRGBA8:32位RGBA的颜色,4*8=32位
16      kEAGLColorFormatRGB565:16位RGB的颜色,
17      kEAGLColorFormatSRGBA8:sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素,sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。
18      */
19 //    self.myEGLLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@(NO),kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
20     self.myEGLLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];
21 
22     
23     // 2. 设置上下文
24     self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
25     if (!self.myContext) {
26         NSLog(@"create context failed!");
27         return;
28     }
29     BOOL isSetSuccess = [EAGLContext setCurrentContext:self.myContext];
30     if (!isSetSuccess) {
31         return;
32     }
33     
34     
35     // 3. 清空缓冲区
36     glDeleteBuffers(1, &_myColorRenderBuffer);
37     self.myColorRenderBuffer = 0;
38     glDeleteBuffers(1, &_myColorFrameBuffer);
39     self.myColorFrameBuffer = 0;
40     
41     
42     // 4. 设置渲染缓冲区 renderBuffer
43     // 生成缓冲区 ID
44     GLuint rb;
45     glGenRenderbuffers(1, &rb);
46     self.myColorRenderBuffer = rb;
47     // 绑定缓冲区
48     glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
49     
50     // 绑到 context: contect 与 eagllayer绑定在一起
51     [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEGLLayer];
52     
53     
54     // 5. 设置帧缓冲区 FrameBuffer
55     glGenBuffers(1, &_myColorFrameBuffer);
56     glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
57     
58     // 渲染缓冲区 与 帧缓冲区绑在一起
59     /*
60      target:
61      attachment:将 renderBuffer 附着到frameBuffer的哪个附着点上
62      renderbuffertarget
63      renderbuffer
64      */
65     //    glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
66     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
67     
68     // 开始绘制
69     [self renderLayer];
70     
71 }

二、开始绘制

1、首先获取并使用 链接后着⾊器对象,过程:

  a1、创建⼀个顶点着⾊器对象和⼀个⽚段着⾊器对象

  b1、将源代码链接到每个着⾊器对象

  c1、编译着⾊器对象

  d1、创建⼀个程序对象

  e1、将编译后的着⾊器对象连接到程序对象

  f1、链接程序对象

a1、着色器:

顶点着色器代码:

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

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

片元着色器代码:

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

void main() {
    
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}

注意:使用 .vsh / .fsh 区分顶点、片元着色器 --> .vsh --> 顶点着色器 / .fsh --> 片元着色器 --> .vsh / .fsh 文件,只是用来给开发者区分着色器代码。其本质是一串字符串。

 

b1 ~ f1 过程:

 1 - (void)renderLayer {
 2     
 3     glClearColor(0.7, 0.7, 0.7, 1);
 4     glClear(GL_COLOR_BUFFER_BIT);// 清空颜色缓冲区
 5     
 6     /// 1. 设置视口
 7     CGFloat mainScale = [UIScreen mainScreen].scale;
 8     glViewport(self.frame.origin.x * mainScale, self.frame.origin.y * mainScale, self.frame.size.width * mainScale, self.frame.size.height * mainScale);
 9     
10     /// 2. 读取着色器代码
11     // 路径
12     NSString *verPath = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
13     NSString *fragPath = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
14     
15     /// 3. 加载着色器
16     self.myProgram = [self loadShadersWithVertex:verPath Withfrag:fragPath];
17     
18     /// 4. 链接 program
19     glLinkProgram(self.myProgram);
20     // 获取连接状态
21     GLint linkStatus;
22     glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkStatus);
23     if (linkStatus == GL_FALSE) {// 链接出错
24         // 获取错误信息 log
25         GLchar message[512];
26         glGetProgramInfoLog(self.myProgram, sizeof(message), 0, &message[0]);
27         NSString *messageString = [NSString stringWithUTF8String:message];
28         NSLog(@"Program Link Error:%@",messageString);
29         return;
30     }
31     
32     /// 5. 使用 program
33     glUseProgram(self.myProgram);
34  }

加载编译着色器:

 1 // 加载着色器
 2 // 顶点着色器 和 片元着色器 的代码传进来(.vsh  .fsh)
 3 -(GLuint)loadShadersWithVertex:(NSString *)vert Withfrag:(NSString *)frag {
 4     
 5     // 1.定义 着色器
 6     GLuint verShader, fragShader;
 7     
 8     // 2.创建程序 program
 9     GLint program = glCreateProgram();// 创建一个空的程序对象
10     
11     // 3.编译着色器 --> 封装一个方法 compileShaderWithShader:
12     [self compileShaderWithShader:&verShader shaderType:GL_VERTEX_SHADER filePath:vert];
13     [self compileShaderWithShader:&fragShader shaderType:GL_FRAGMENT_SHADER filePath:frag];
14     
15     // 4.attach shader, 将shader附着到 程序
16     glAttachShader(program, verShader);
17     glAttachShader(program, fragShader);
18     
19     //5.已附着好的 shader 删掉,避免不必要的内存占用
20     glDeleteShader(verShader);
21     glDeleteShader(fragShader);
22     
23     return program;// 返回编译好的程序
24 }
25 // 编译着色器
26 /*
27  shader: 着色器 ID
28  type: 着色器类型
29  path: 着色器代码文件路径
30  */
31 - (void)compileShaderWithShader:(GLuint *)shader shaderType:(GLenum)type filePath:(NSString *)path {
32     
33     // 1.读取文件路径
34     NSString *file = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
35     // NSString 转 C 的 char
36     const GLchar *source = (GLchar *)[file UTF8String];
37     
38     // 2.创建对应类型的shader
39     *shader = glCreateShader(type);
40     
41     // 3.读取着色器源码 将其附着到着色器对象上面
42     /* params:
43      shader: 要编译的着色器对象 *shader
44      numOfStrings: 传递的源码字符串数量 1个
45      参数3:strings: 着色器程序的源码(真正的着色器程序源码)
46      参数4:lenOfStrings: 长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
47      */
48     //    glShaderSource(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length)
49     glShaderSource(*shader, 1, &source,NULL);
50     
51     // 4. 编译
52     glCompileShader(*shader);
53 }

2、绘制型关信息的设置,过程:

  a2、设置坐标

  b2、加载纹理

  c2、draw 绘制

a2、设置坐标信息:

    /// 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,
    };
    
    /// 7. copy 到顶点缓冲区
    GLuint buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    // 顶点数据 copy 到缓冲区
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);

    /// 8. 打开通道 传递数据
    // 8.1 顶点数据
    // 获取通道 ID
    /*
     glGetAttribLocation(GLuint program, const GLchar *name)
     program:
     name: 给谁传 --> .vsh 的 position
     */
    GLuint position = glGetAttribLocation(self.myProgram, "position");
    // 打开通道
    glEnableVertexAttribArray(position);
    // 读数据
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);

    // 8.2 纹理数据 /*传给谁 --> .vsh 的 textCoordinate */
    GLuint texture = glGetAttribLocation(self.myProgram, "textCoordinate");
    glEnableVertexAttribArray(texture);
    glVertexAttribPointer(texture, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
    
    
    /// 9. 加载纹理
    [self loadTexture];

b2、加载纹理

// 加载纹理
- (void)loadTexture {
    
    // 9.0 image 转为 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:@"0002"].CGImage;
    // 图片是否获取成功
    if (!spriteImage) {
        NSLog(@"Failed to load image ");
        exit(1);// 退出程序
    }
    // 获取图片宽高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    // 获取图片字节数 宽*高*4(RGBA)
    GLubyte *spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    
    // 创建上下文
    /*
     data:指向要渲染的绘制图像的内存地址
     width:bitmap 的宽度,单位为像素
     height:bitmap 的高度,单位为像素
     bitPerComponent:内存中像素的每个组件的位数,比如 32 位 RGBA,就设置为 8
     bytesPerRow:bitmap 的没一行的内存所占的比特数
     colorSpace:bitmap 上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
     */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);

    // 在 CGContextRef 上 --> 将图片绘制出来
    /*
     CGContextDrawImage 使用的 Core Graphics 框架,坐标系与 UIKit 不一样。UIKit 框架的原点在屏幕的左上角,Core Graphics 框架的原点在屏幕的左下角。
     CGContextDrawImage(CGContextRef  _Nullable c, CGRect rect, CGImageRef  _Nullable image)
     c:绘图上下文
     rect:rect坐标
     image:绘制的图片
     */
    CGRect rect = CGRectMake(0, 0, width, height);
    CGContextDrawImage(spriteContext, rect, spriteImage);

    // 绘完 释放上下文
    CGContextRelease(spriteContext);
    
    // 9.1. 绑定纹理到默认的纹理ID
    glBindTexture(GL_TEXTURE_2D, 0);
    
    // 9.2. 设置纹理属性
    /*
     glTexParameteri(GLenum target, GLenum pname, GLint param)
     target:纹理维度
     pname:线性过滤; 为s,t坐标设置模式
     param:wrapMode; 环绕模式
     */
    // 过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 环绕方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        
    // 9.3 载入纹理
    /* 载入纹理 glTexImage2D
    参数1:纹理维度,GL_TEXTURE_2D
    参数2:mip贴图层次
    参数3:纹理单元存储的颜色成分(从读取像素图中获得)
    参数4:加载纹理宽度
    参数5:加载纹理的高度
    参数6:为纹理贴图指定一个边界宽度 0
    参数7、8:像素数据的数据类型, GL_UNSIGNED_BYTE无符号整型
    参数9:指向纹理图像数据的指针
    */
    float fw = width, fh = height;
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);

    // 9.4 释放 sprite
    free(spriteData);
}

c2、绘制

    /// 10. 设置纹理采样器
    glUniform1i(glGetUniformLocation(self.myProgram, "colorMap"), 0);
    
    /// 11.  绘制
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    // 12. 从渲染缓冲区显示到屏幕
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];

运行结果如下图:

图片是倒立的?如何解决呢?

UIKit 框架的原点是屏幕的左上角,Core Graphics 框架的原点是屏幕的左下角。

三、解决图片倒立问题

1、加载纹理的绘制图片过程中,将图片通过转换矩阵旋转

   // 矩阵转换 - 翻转图片
    // x、y 轴平移
    CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
    // y 平移
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);
    // Y 轴方向 Scale -1 翻转
    CGContextScaleCTM(spriteContext, 1.0, -1.0);
    // 平移回原点位置处
    CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
    // 重绘
    CGContextDrawImage(spriteContext, rect, spriteImage);

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,
    };

3、修改着色器代码进行翻转

3.1、纹理着色器中 对纹理 y 坐标进行翻转

 

纹理着色器代码:

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));
} 

3.2、顶点着色器中 对纹理坐标转换

原理同 3.1

代码:

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;
}

3.3、顶点着色器 传入旋转矩阵对顶点进行旋转

顶点着色器代码:

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; }

绘制前  旋转矩阵 --> 矩阵使用 uniform 修饰传递

    // 1. rotate等于shaderv.vsh中的 uniform 属性,rotateMatrix
    GLuint rotate = glGetUniformLocation(self.myPrograme, "rotateMatrix");
    
    // 2.获取渲旋转的弧度
    float radians = 180 * 3.14159f / 180.0f;
    // 3.求得弧度对于的sincos值
    float s = sin(radians);
    float c = cos(radians);
    
    // 4.因为在3D课程中用的是横向量,在OpenGL ES用的是列向量
    // 参考Z轴旋转矩阵
    GLfloat zRotation[16] = {
        c,-s,0,0,
        s,c,0,0,
        0,0,1,0,
        0,0,0,1
    };
    
    // 5.设置旋转矩阵
    /*
     glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)
     location : 对于shader 中的ID
     count : 个数
     transpose : 是否转置矩阵
     value : 指针
     */
    glUniformMatrix4fv(rotate, 1, GL_FALSE, zRotation);

一般使用第一种方式,修改着色器代码 每一个像素都要运行一次,成本太高。

 

原文地址:https://www.cnblogs.com/zhangzhang-y/p/13414166.html