iOS-OpenGL ES入门教程(五)初识GLSL

前言

前面的基础文章列表

  1. iOS-零基础学习OpenGL ES入门教程(一)

  2. iOS-OpenGL ES入门教程(二)最简单的纹理Demo

  3. iOS-OpenGL ES入门教程(三)纹理取样,混合,多重纹理

  4. iOS-OpenGL ES入门教程(四)光照

写在前面:OpenGLES系列是自己在做音视频时候边学边记录的文章 距离最初记录已经三年有余,现在回头看补齐这个当初的学习系列也算是一种回溯吧,学习总是让人开心的,由陌生到熟练再到更进一步精通的过程充满着乐趣。希望自己抽出时间尽快将这个基础系列补齐。如今虽然OpenGL在iOS上已经被苹果废弃了,不过图形框架的基础原理和思路还是很相像的,熟悉opengl再熟悉metal会更快上手

初识GLSL

之前的章节提到过 我们使用GLKit框架所以避开了写繁琐的GLSL

GLSL是一个以C语言为基础的高阶着色语言 通过编写glsl 提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言

我们来看下怎么用GLSL
同样是一个最简单的绘制一张图片应用

准备绘制工作

不同于GLkit 我们直接使用GLkitView 我们先自定义一个UIView的子类 作为opengl绘制的载体View

@interface GLSLDemoView : UIView

@end

重载layerClass 指定为CAEAGLLayer iOS下只有CAEAGLLayer才支持opengl的绘制

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

初始化opengl上下文和配置好CAEAGLLayer的相关属性

//设置Layer
    self.mEagLayer = (CAEAGLLayer *) self.layer;
    self.contentScaleFactor = [UIScreen mainScreen].scale;
    self.mEagLayer.opaque = YES;
    self.mEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
    
    
    //设置openglES 上下文
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
    EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
    
    if (!context) {
        NSLog(@"error to init openglES context");
    }
    
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"error to set current openglES context");
    }
    self.mContext = context;

绑定framebuffer和renderbuffer

//Render Buffer
    GLuint renderBuffer;
    glGenRenderbuffers(1, &renderBuffer);
    self.mColorRenderBuffer = renderBuffer;
    glBindRenderbuffer(GL_RENDERBUFFER, self.mColorRenderBuffer);
    [self.mContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.mEagLayer];
    
    
    //Frame Buffer
    GLuint frameBuffer;
    glGenFramebuffers(1, &frameBuffer);
    self.mColorFrameBuffer = frameBuffer;
    glBindFramebuffer(GL_FRAMEBUFFER, self.mColorFrameBuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.mColorRenderBuffer);

常规的opengl渲染管线的准备工作 接下来 我们就要开始加载我们自己的shader(着色器),
之前我们提过opengl的渲染管线中

Vetex shader 顶点着色器
Fragment shader 片元着色器

加载着色器

着色器自然是一种编程语言 我们首先要做的是编写一个最简单的着色器 了解下着色器语法

GLSL的官方文档:https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.20.pdf

这里介绍下最简单的修饰符

const:常量值必须在声明是初始化。它是只读的不可修改的。

attribute:表示只读的顶点数据,只用在顶点着色器中。数据来自当前的顶点状态或者顶点数组。它必须是全局范围声明的,不能在函数内部。一个attribute可以是浮点数类型的标量,向量,或者矩阵。不可以是数组或者结构体

uniform:一致变量。在着色器执行期间一致变量的值是不变的。与const常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。一致变量在顶点着色器和片段着色器之间是共享的。它也只能在全局范围进行声明。

varying:顶点着色器的输出。例如颜色或者纹理坐标,(插值后的数据)作为片段着色器的只读输入数据。必须是全局范围声明的全局变量。可以是浮点数类型的标量,向量,矩阵。不能是数组或者结构体。

最常用的内置变量

gl_Position:vec4类型 输出属性-变换后的顶点的位置,用于后面的固定的裁剪等操作。所有的顶点着色器都必须写这个值。

gl_FragColor:vec4类型 片元着色器的输出颜色 可用于后续使用

知道了这些最简单的修饰符和内置变量 我们就可以编写起自己的shader啦

首先先编写顶点着色器:(一般顶点着色器文件以.vsh结尾)

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

rotateMatrix对应的旋转矩阵 可以用来向量运算 旋转矩阵

varing 定义了 varyTextCoord 用来传递纹理坐标给片元着色器

这里的lowp表示是精度 精度要求越低 性能消耗越低

gl_Position输出属性-变换后的顶点的位置 这里做了个最简单的矩阵变换 vPos * rotateMatrix

接下来编写片元着色器

varying lowp vec2 varyTextCoord;

uniform sampler2D colorMap;


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

sampler2D 二维纹理的句柄也就是当前绑定的纹理 这里我们根据顶点着色器传入的纹理坐标 仅仅做个texture2D 取色 返回纹理对应坐标的颜色

这样我们就编写好了顶点和片元shader 接下来我们来代码加载shader

+ (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
    
    NSError *error;
    NSString *content = [NSString stringWithContentsOfFile:file
                                                  encoding:NSUTF8StringEncoding
                                                     error:&error];
    
    if (error) {
        NSLog(@"加载shader 本地file失败");
        return;
    }
    
    const GLchar *source = (GLchar *)[content UTF8String];
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    glCompileShader(*shader);

}
+ (GLuint)loadShaders:(NSString *)vshFilePath
                  fsh:(NSString *)fshFilePath{
    
    GLuint vShader,fShader;
    GLint program = glCreateProgram();
    
    //编译
    [self compileShader:&vShader type:GL_VERTEX_SHADER file:vshFilePath];
    [self compileShader:&fShader type:GL_FRAGMENT_SHADER file:fshFilePath];
    
    glAttachShader(program, vShader);
    glAttachShader(program, fShader);
    
    glDeleteShader(vShader);
    glDeleteShader(fShader);

    return program;
}
 NSString* vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
    NSString* fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
    self.mProgram = [GLShaderUtils loadShaders:vertFile fsh:fragFile];

编译和绑定好了shader之后 我们开始link shader 其实这正是对应JIT类型的语言的编译 链接 运行

+ (BOOL)linkProgram:(GLuint)program{
    glLinkProgram(program);
    GLint linkRet;
    glGetProgramiv(program, GL_LINK_STATUS, &linkRet);
    
    if (linkRet == GL_FALSE) {
        GLchar messages[256];
        glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSLog(@"error%@", messageString);
        return NO;
    }
    else{
        return YES;
    }
}

如果shader写的有误 这里会报错 不过相比于xcode这些编译器的报错 给到的debug信息比较弱
可以看到 加载shader是比较常规的固定代码流程 适合封装到工具方法中 方便复用
接下来就是创建VBO 也就是顶点 并且绑定到我们的shader上 相当于将数据从cpu拷贝到gpu中

GLfloat attrArr[] =
    {
        1.0f, 1.0f, 0.0f,      1.0f, 1.0f,
         -1.0f, 1.0f, 0.0f,     0.0f, 1.0f,
         1.0f, -1.0f, 0.0f,     1.0f, 0.0f,
        1.0f, -1.0f, 0.0f,     1.0f, 0.0f,
        -1.0f, 1.0f, 0.0f,     0.0f, 1.0f,
        -1.0f, -1.0f, 0.0f,    0.0f, 0.0f,
    };
    
    GLuint attrBuffer;
    glGenBuffers(1, &attrBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
    
    GLuint position = glGetAttribLocation(self.mProgram, "position");
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
    glEnableVertexAttribArray(position);
    
    GLuint textCoor = glGetAttribLocation(self.mProgram, "textCoordinate");
    glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
    glEnableVertexAttribArray(textCoor);

这里的关键点glGetAttribLocation(self.mProgram, "position") 和 glGetAttribLocation(self.mProgram, "textCoordinate") 正是对应的着色器代码中的position和textCoordinate输入变量

接下来绑定纹理

CGImageRef image = [UIImage imageNamed:imageName].CGImage;
    if (!image) {
        NSLog(@"load Image texture failed");
        return;
    }
    size_t width = CGImageGetWidth(image);
    size_t height = CGImageGetHeight(image);
    GLubyte * imageData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    CGContextRef imageContext = CGBitmapContextCreate(imageData, width, height, 8, width*4,
                                                      CGImageGetColorSpace(image), kCGImageAlphaPremultipliedLast);
    CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
    CGContextRelease(imageContext);
    glBindTexture(GL_TEXTURE_2D, 0);
    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);
    float fw = width, fh = height;
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
    glBindTexture(GL_TEXTURE_2D, 0);
    free(imageData);

我们还有个旋转矩阵变量需要设置输入到着色器中

GLuint rotate = glGetUniformLocation(self.mProgram, "rotateMatrix");
    float radians = 180 * 3.14159f / 180.0f; //旋转180度让图片正向
    float s = sin(radians);
    float c = cos(radians);
    
    //z轴旋转矩阵
    GLfloat zRotation[16] = {
        c,-s,0,0,
        s,c,0,0,
        0,0,1,0,
        0,0,0,1
    };
    
    //设置旋转矩阵
    glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);

最后一步绘制

 glDrawArrays(GL_TRIANGLES, 0, 6);
 [self.mContext presentRenderbuffer:GL_RENDERBUFFER];

这样我们最简单的图片绘制就完成了


总结:

着色器语言GLSL是一种基于C的编程语言 可以理解为GPU编程 方便我们操作顶点和片元相关的渲染管线 实现我们自定义的绘制效果。和普通的编程一样。需要编写代码 编译 运行 不过shader的加载编译运行是在app运行是动态解释执行的。理解了着色器的运行流程。再去编写GLSL。会有更加深入的理解。

Demo代码地址:LearnOpenGLESDemo

参考文章:
GLSL基础语法介绍

你可能感兴趣的:(iOS-OpenGL ES入门教程(五)初识GLSL)