美文网首页OpenGL
使用GLSL编写自定义着色器加载图片

使用GLSL编写自定义着色器加载图片

作者: iOSer_jia | 来源:发表于2020-08-04 14:59 被阅读0次

概述

GLSL(OpenGL Shading Language)是一种用于OpenGL的高级过程着色语言,开发者可以使用GLSL自定义编写顶点着色器和片元着色器实现图形效果。本文将使用GLSL自定义着色器着iPhone设备上渲染一张图片。

相关环境配置

开发工具
Xcode 11.4.1

系统环境
macOS Catalina 10.15.6 / iOS 13.4.1

使用语言
OC/C/GLSL

着色器编写

为了方便查看和维护,我们一般把着色器单独编写在一个独立的文件,我们新建一个shaderv.vsh文件编写顶点着色器源码,shaderf.fsh文件编写片元着色器源码。

Xcode无法编译着色器源码,所以“vsh“和”fsh“只是作为标示区分顶点着色器和片元着色器,并无其他意义。
着色器源码中不要出现中文,避免无法预知的错误

shderv.vsh中:

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

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

attribute vec4 position; // 声明一个名为position、类型为4分量浮点向量的attribute通道,用来传递顶点坐标数据
attribute vec2 textCoordinate; // 声明一个名为textCoordinate、类型为2分量浮点向量的attribute通道,用来传递纹理坐标数据,由于片元着色器没有attribute通道,所以需要从顶点着色器中传入
varying lowp vec2 varyTextCoord; // 声明一个名为varyTextCoord、 低精度(因为纹理坐标范围是0~1)、类型为2分量浮点向量的变量,这个变量作用是将纹理坐标传递到片元着色器中
varyTextCoord = textCoordinate; // 将纹理坐标传递到片元着色器
gl_Position = position; // 传递顶点坐标给OpenGL ES,gl_Position是OpenGL ES的内建变量,用来传递顶点数据。

shderf.fsh中:

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

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

precision highp float; 指定float为高精度
varying vec2 varyTextCoord;纹理坐标,从顶点着色器中传入,参数名称和类型一定要和顶点着色器中的纹理坐标参数一模一样
uniform sampler2D colorMap;纹理,通过uniform通道传入
lowp vec4 temp = texture2D(colorMap, varyTextCoord);获取纹理对应坐标下的纹素,及纹理坐标对象像素点的颜色值,texture2D()是OpenGL ES的一个内建函数,用来获取纹素,他需要传入两个参数,第一个传递纹理,第二个传递纹理坐标,返回对应坐标的颜色值。
gl_FragColor = temp;gl_Position一样,gl_FragColor时OpenGL ES的一个内建变量,用来放回颜色值。

顶点着色器的执行次数和顶点数量有关,比如10个顶点那么顶点着色器会执行10次,而片元着色器的执行次数和像素点数量有关,绘制一个100*100的图片片元着色器会志晖行100000次,顶点着色器和片元着色器的执行都是在GPU中执行,所以执行效率会很高,但是模拟器中并没有着色器,执行的时候时CPU在模拟GPU的执行。

Client代码

客户端代码的大体执行流程为:

  1. 创建CAEAGLLayer
  2. 创建上下文
  3. 清空缓冲区
  4. 设置RenderBuffer
  5. 设置FrameBuffer
  6. 开始绘制

首先创建一个ESView,并将它的layer指定CAEAGLLayer来展示绘制的结果

ESView初始化

ESView.m 中

// 导入入相关头文件
#import "ESView.h"
#import <OpenGLES/ES3/gl.h>

// 声明相关属性
@interface ESView ()

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

@end

指定layer为CAEAGLLayer

+ (Class)layerClass {
    return CAEAGLLayer.class;
}

在layoutSubviews中开始绘制流程

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

创建CAEAGLLayer、创建上下文、清空缓冲区

前三步比较简单,直接上代码

    // 获取layer
    self.myEagLayer = (CAEAGLLayer *)self.layer;
    
    // 设置scale
    CGFloat scale = UIScreen.mainScreen.scale;
    [self setContentScaleFactor:scale];
    
    // 设置参数
    self.myEagLayer.drawableProperties = 
    @{kEAGLDrawablePropertyRetainedBacking: @false, 
    kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};

- (void)setupContext {

    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    
    if (!context) {
        NSLog(@"context create fail");
        return;
    }
    
    // 设置当前上下文为全局上下文
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"set current context fail");
        return;
    }
    
    self.myContext = context;
}
- (void)deleteRenderAndFrameBuffer {
    glDeleteRenderbuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
    
    glDeleteFramebuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
}

设置RenderBuffer和FrameBuffer

关于renderbuffer和framebuffer的关系,可以将FrameBuffer看作RenderBuffer的管理者,一个renderbuffer对象是通过应用分配的一个2D图像缓存区,它能够用来分配和存储颜色、深度或者模版值,也能够在一个framebuffer里作为颜色、深度、模版的附件,一个renderbuffer类似于窗口系统提供的可绘制的表面,比如pBuffer。而一个framebuffer对象(FBO)提供了许多附着点,他虽然有着缓冲区的字眼,但他更像一个容器,他可以保存可以进行渲染的对象,比如渲染缓冲区,采用这种方式,FBO能够在保存OpenGL
管线的输出时将需要的状态和表面绑定在一起。

设置渲染缓冲区

- (void)setupRenderBuffer {
    
    // 申请一个缓冲区标示
    GLuint buffer;
    glGenRenderbuffers(1, &buffer);
    
    // 绑定buffer到GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, buffer);
    
    self.myColorRenderBuffer = buffer;
    
    // 将layer的存储绑定到renderbuffer
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
    
}

设置帧缓冲区

- (void)setupFrameBuffer {
    
    // 申请一个缓冲区标示
    GLuint buffer;
    glGenFramebuffers(1, &buffer);

    // 绑定
    glBindFramebuffer(GL_FRAMEBUFFER, buffer);

    self.myColorFrameBuffer = buffer;

    //将渲染缓冲区绑定到frameBuffer的GL_COLOR_ATTACHMENT0上
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorFrameBuffer);
    
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status == GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"framebuffer complete");
    } else {
        NSLog(@"not complete");
    }
}

开始绘制

绘制主要流程如下:

  1. 设置清屏颜色
  2. 清空缓冲区
  3. 设置视口
  4. 加载顶点着色器和片元着色器
  5. 设置顶点坐标和纹理坐标
  6. 将顶点坐标从内存保存到顶点缓冲区
  7. 将顶点坐标和纹理坐标传递到着色器
  8. 加载纹理
  9. 将纹理数据传递到着色器
  10. 开始绘制(指定图元连接方式)
  11. 提交渲染

具体代码如下

- (void)renderLayer {
    
    // 设置清屏颜色
    glClearColor(1, 0, 0, 1.0);
    // 清空
    glClear(GL_COLOR_BUFFER_BIT);
    
    
    // 设置视口大小
    CGFloat scale = UIScreen.mainScreen.scale;
    CGRect rect = self.frame;
    glViewport(rect.origin.x*scale, rect.origin.y*scale, rect.size.width*scale, rect.size.height*scale);
    
    // 读取顶点着色器、片元着色器
    NSString *vertFile = [NSBundle.mainBundle pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fragFile = [NSBundle.mainBundle pathForResource:@"shaderf" ofType:@"fsh"];
    
    // 加载着色器生成program
    self.myPrograme = [self loadShaders:vertFile withFrag:fragFile];
    
    // 链接program
    glLinkProgram(self.myPrograme);
    
    // 获取链接状态
    GLint linkStatus;
    glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
    
    if (GL_FALSE == linkStatus) {
        // 获取错误信息
        GLchar message[512];
        glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
        NSString *msgStr = [NSString stringWithUTF8String:message];
        NSLog(@"link program failure: %@", msgStr);
        return;
    }
    
    // 使用program
    glUseProgram(self.myPrograme);
    
     //设置顶点
    GLfloat attArr[] = {
        -1.0, -0.5, 0.0,  0.0, 0.0,
        1.0, -0.5, 0.0,  1.0, 0.0,
        1.0, 0.5, 0.0,  1.0, 1.0,
        -1.0, 0.5, 0.0,  0.0, 1.0,
    };
    
    // 将顶点数据从内存保存到显存中
    // 申请一个缓冲区标示
    GLuint attBuffer;
    glGenBuffers(1, &attBuffer);
    // 绑定
    glBindBuffer(GL_ARRAY_BUFFER, attBuffer);
    // 传递到显存
    glBufferData(GL_ARRAY_BUFFER, sizeof(attArr), attArr, GL_DYNAMIC_DRAW);
    
    // 将数据传递到着色器
    
    // 获取position通道
    GLuint position = glGetAttribLocation(self.myPrograme, "position");
    //苹果默认关闭attribute通道,需要打开通道
    glEnableVertexAttribArray(position);
    // 将顶点坐标传递到顶点着色器
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
    
    // 传递纹理坐标到着色器
    GLuint textCoordinate = glGetAttribLocation(self.myPrograme, "textCoordinate");
    glEnableVertexAttribArray(textCoordinate);
    glVertexAttribPointer(textCoordinate, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL + 3);
    
    // 加载纹理
    [self setupTexture:@"benzema"];
    
    //获取颜色uniform通道
    GLuint colorMap = glGetUniformLocation(self.myPrograme, "colorMap");
    //设置纹理采样器
    // 0 标示默认
    glUniform1f(colorMap, 0);
    
    // 绘图
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    
    // 从渲染缓冲区显示到屏幕上
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}

加载着色器

加载着色器的过程如下:

  1. 获取着色器源码
  2. 将着色器源码附着到着色器对象
  3. 编译着色器
  4. 将着色器连接到program
  5. 链接program
  6. 使用program

具体代码如下:

// 加载顶点着色器和片元着色器,返回program
- (GLuint)loadShaders:(NSString *)vert withFrag:(NSString *)frag {
    
    // 定义两个临时着色器对象
    GLuint vertShader, fragShader;
    // 创建program
    GLuint program = glCreateProgram();
    
    // 编译着色器
    [self compileShader:&vertShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    
    // 将着色器连接到program
    glAttachShader(program, vertShader);
    glAttachShader(program, fragShader);
    
    // 释放不需要的shader
    glDeleteShader(vertShader);
    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
    *shader = glCreateShader(type);
    
    //将着色器源码附着到shader
    //参数1:shader,要编译的着色器对象 *shader
    //参数2:numOfStrings,传递的源码字符串数量 1个
    //参数3:strings,着色器程序的源码(真正的着色器程序源码)
    //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
    glShaderSource(*shader, 1, &source, NULL);
    
    //编译着色器源码
    glCompileShader(*shader);
}

加载纹理

加载纹理的过程为:

  1. 获取图片数据
  2. 创建一个纹理id
  3. 绑定纹理id
  4. 设置纹理属性
  5. 载入纹理2D数据到纹理缓冲区
  6. 释放图片数据

而通过我所能拿到的是UIImage对象,我们可以使用CoreGraphics框架获得纹理数据,具体流程为

  1. 通过UIImage获得CGImageRef对象
  2. 创建上下文
  3. 创建一个大小为CGImageRef大小的内存空间
  4. 使用CGContextDrawImage重绘获得纹理数据
  5. 释放上下文

具体代码如下:

- (GLuint)setupTexture:(NSString *)fileName {
    
    // 将uiimage转化为cgimagref
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    if(!spriteImage) {
        NSLog(@"failed to load image %@", fileName);
        exit(1);
    }
    
    // 使用CoreGraphics重绘获取位图数据
    // 读取图片的大小
    size_t width = CGImageGetWidth(spriteImage);
    size_t heith = CGImageGetHeight(spriteImage);
    
    // 创建空间存放数据
    GLubyte *spriteData = calloc(width*heith*4, sizeof(GLubyte));
    
    // 创建上下文
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, heith, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
    // 绘制图片
    CGRect rect = CGRectMake(0, 0, width, heith);
    CGContextDrawImage(spriteContext, rect, spriteImage);
    
    // 释放上下文
    CGContextRelease(spriteContext);
    
    // 绑定纹理到默认id
    glBindBuffer(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);
    
    // 载入纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (float)width, (float)heith, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    // 释放纹理
    free(spriteData);
    
    return 0;
    
}

执行

最终显示的结果为


15962994997321.jpg

可以看到图片虽然显示成功,但是是上下颠倒的,具体原因将在下一篇文章作出解释和解决。

相关文章

网友评论

    本文标题:使用GLSL编写自定义着色器加载图片

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