OpenGL ES

第十节—初探GLSL

2020-09-21  本文已影响0人  L_Ares

本文为L_Ares个人写作,包括图片皆为个人亲自操作,如需转载请表明原文出处。

之前的渲染一直都是用GLKit来帮助我们完成,那么如果不借助GLKit框架的话,想要实现渲染效果,我们就需要自己来进行渲染代码的编写。

想要使用着色器进行渲染的话,前提条件就是一定要有2个基本的对象:着色器对象和程序对象,程序对象,关于这一个步骤,在链接中的文章已经说明了原因和他们的作用。

在创建了两个基本对象,并获取链接之后,着色器对象就需要开始它的工作,这个工作一般包含了大概如下的步骤:

  1. 创建顶点着色器对象,创建片元着色器对象。

  2. 将源代码链接到每个着色器对象。

  3. 编译着色器对象。

  4. 创建程序对象。

  5. 将已经编译过的着色器对象和程序对象链接。

  6. 链接程序对象。

在没有完成这些步骤之前,我们是很难直接将着色器里面的内容和我们Client中的内容进行交互的。

一、渲染缓冲区

渲染缓冲区英文名:RenderBuffer

RenderBuffer是一个通过应用分配的2D缓冲区。RenderBuffer可以用来分配和存储颜色、深度、模版,也可以用过一个framebuffer的颜色、深度、模板的附件。RenderBuffer就类似于窗口系统提供的一个可绘制的表面。

但是,RenderBuffer不能被拿来当作一个GL的纹理直接使用。

RenderBuffer里面包含了深度缓冲区(DepthBuffer)、模版缓冲区
(StencilBuffer)、纹理(Texture)。

在GLKit的相关介绍中说过,显示在你屏幕上的图形,是在帧缓冲区(FrameBuffer)中被呈现上去的,只不过GLKit框架帮我们创建过了FrameBuffer。FrameBuffer在OpenGl ES是非常重要的组件,GLKit本身也是苹果基于OpenGL ES来进行的封装,所以在不使用GLKit之后,图形依然也是在FrameBuffer中完成设置后呈现到屏幕上的,但是这里的framebuffer因为不用GLKit框架了,没有GLKView了, 就要我们自己来创建。

在OpenGL ES中,常称FrameBuffer对象为FBO。

那么framebuffer又和这里要说的RenderBuffer是什么关系呢?

直接点的说,FameBuffer是来管理RenderBuffer的。真正用来存储颜色、深度、模版值的是RenderBuffer,而FrameBuffer是他们的一个附着点。关系图如下1.1所示:

1.1.png

图中颜色本身就是可以当作纹理使用的,比如纯色纹理。深度则是在OpenGL中就说过,深度的大小会影响颜色缓冲区存储的颜色值,近存远删。

二、简单的GLSL实现图片渲染的案例

先说明一下.vsh.fsh文件的创建,其实就是empty文件的创建,大家应该都使用过了,我就直接贴图2.1了,记得把后缀名加上就行。

2.1.png

然后说一下这着色器文件中,最简单的,也是最必需要写的东西,因为最好不要在这两个文件中写注释,所以就单独拿出来解释一下。

.vsh(顶点着色器代码)


//顶点坐标
attribute vec4 position;
//纹理坐标
attribute vec2 textCoordinate;
//varying是一个标记,声明了这个变量是用来在vsh和fsh文件之间传递的变量
//lowp是指这个二维向量的单位:GLFloat,它的精度
//这个参数是存储纹理坐标的
varying lowp vec2 varyTextCoord;

void main ()
{
    //把纹理坐标值赋值给传递变量,由传递变量将纹理坐标传输到片元着色器
    varyTextCoord = textCoordinate;
    //gl_Position是GLSL的内建变量,也就是GLSL已经创建好了的,用来保存顶点坐标的变量
    gl_Position = position;
}

.fsh(顶点着色器代码)

//这个就是刚才从顶点着色器传过来的纹理坐标,注意这里最好直接复制过来,因为一个字母都不许差
varying lowp vec2 varyTextCoord;
//uniform属性 sampler2D代表的是声明纹理属性,就是说声明这个变量是纹理,他是以类似标识符的方式存储的
//也就是说不是把你真的纹理放进来了,而是给纹理声明了一个身份ID,由ID去索引相应的纹理
uniform sampler2D colorMap;

void main ()
{
    //内建变量gl_FragColor(纹理采样器,纹理坐标)
    //参数1 : 纹理的身份ID
    //参数2 : 纹理坐标。
    //内建函数会返回一个vec4类型的rgba值
    //它的作用就是读取纹素
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}


这里的代码要用直接复制走的话,用的时候记得把中文注释都删除掉,尽量避免出现错误的情况。

下面直接上代码,但是这次绘制的图片是翻转过来的,原因很简单,这次的代码没有做之前GLKit里面设置的OriginBottomLeft,所以纹理原点没有在左下角,而是和view的一样,在左上角。

另外,这只是自定义View里面的内容,所以要显示出来记得要在viewcontroller里面把view加上去。

//
//  JDView.m
//  04GLSL渲染图片
//
//  Created by EasonLi on 2020/9/20.
//  Copyright © 2020 EasonLi. All rights reserved.
//

#import "JDView.h"
#import <OpenGLES/ES2/gl.h>

#define MY_ORIGIN self.frame.origin
#define MY_SIZE self.frame.size

@interface JDView ()

//继承于CALayer。是在iOS上用于绘制OpenGL ES的图层类
@property (nonatomic, strong) CAEAGLLayer *eaglLayer;

//上下文
@property (nonatomic,strong) EAGLContext *nContext;

//渲染缓冲区
@property (nonatomic,assign) GLuint nRenderBuffer;

//帧缓冲区
@property (nonatomic,assign) GLuint nFrameBuffer;

//Program
@property (nonatomic,assign) GLuint nProgram;

@end



@implementation JDView

#pragma mark - 重绘View
- (void)layoutSubviews
{
    
    //创建图层
    [self createLayer];
    
    //创建图形的上下文
    [self createContext];
    
    //清空缓存区
    [self cleanUpBuffers];
    
/********************************************/
    
    //这里要注意,必须是先有渲染缓存区,再有帧缓存区,因为renderbuffer才是真的缓存颜色,模版,深度的地方
    //frameBuffer是附着点!!!相当于只是管理着renderbuffer
    
    //设置渲染缓存区
    [self setUpRenderBuffer];
    
    //设置帧缓存区
    [self setUpFrameBuffer];
/********************************************/
    
    //渲染并呈现
    [self rendLayer];
    
}

#pragma mark - 创建图层
- (void)createLayer
{
    
    //要重写layerClass,把JDView的图层强转成CAEAGLLayer类型,并赋值给eaglLayer
    self.eaglLayer = (CAEAGLLayer *)self.layer;
    
    //配置一下分辨率的缩放因子
    [self setContentScaleFactor:[[UIScreen mainScreen] scale]];
    
    //设置layer绘制的描述属性
    //描述属性接收字典类型,这里设置了绘图表面显示之后,不保留其内容。(一般默认都是不保留,就是说下一次重新绘制)
    //以及颜色格式是RGBA8888
    self.eaglLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@false,kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
    
}

//重写一下返回图层类的方法,宿主图层换成CALayer子类CAEAGLLayer
+ (Class)layerClass
{
    return [CAEAGLLayer class];
}

#pragma mark - 设置图层上下文
- (void)createContext
{
    
    //初始化上下文,设置OpenGL ES的版本,因为需求不大,OpenGL ES2.0足够,可以用3.0
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
    //判断这个上下文是否创建成功
    if (!context) {
        NSLog(@"上下文创建失败");
        return;
    }
    
    //设置当前上下文
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"设置当前上下文失败");
        return;
    }
    
    //将局部变量的context赋值给我们的属性
    self.nContext = context;
    
}

#pragma mark - 清空缓存区
- (void)cleanUpBuffers
{
    
    // Buffer(缓存区)分为renderBuffer(渲染缓存区)和frameBuffer(帧缓存区)两种。都要清空
    
    //清空renderBuffer
    glDeleteBuffers(1, &_nRenderBuffer);
    self.nRenderBuffer = 0;
    
    //清空frameBuffer
    glDeleteBuffers(1, &_nFrameBuffer);
    self.nFrameBuffer = 0;
     
}

#pragma mark - 申请并设置渲染缓冲区
- (void)setUpRenderBuffer
{
    
    //定义一个存储缓存区的ID的变量
    GLuint renderBufferID;
    
    //申请缓存区,并将其身份ID赋值
    glGenRenderbuffers(1, &renderBufferID);
    
    //将渲染缓存区的身份ID赋值给属性来保存
    self.nRenderBuffer = renderBufferID;
    
    //根据缓存区ID绑定缓存区的类型
    glBindRenderbuffer(GL_RENDERBUFFER, self.nRenderBuffer);
    
    //将刻绘制对象,也即是我们的CAEAGLLayer图层对象,绑定到RenderBuffer对象
    BOOL result = [self.nContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eaglLayer];
    
    if (!result) {
        NSLog(@"绘制图层和渲染缓存区绑定失败");
    }
    
}

#pragma mark - 申请并设置帧缓存区
- (void)setUpFrameBuffer
{
    
    //定义保存帧缓存区ID的对象
    GLuint frameBufferID;
    
    //申请帧缓存区并将身份ID赋值
    glGenFramebuffers(1, &frameBufferID);
    
    //将得到的frameBufferID赋值给属性
    self.nFrameBuffer = frameBufferID;
    
    //根据缓存区ID,把它的绑定到对应的缓存区类型
    glBindFramebuffer(GL_FRAMEBUFFER, self.nFrameBuffer);
    
    //把framebuffer和renderbuffer绑定在一起
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.nRenderBuffer);
    
}

#pragma mark - 渲染并呈现
- (void)rendLayer
{
    
    //绘制前一样要设置好清屏颜色,和OpenGL是一样的
    glClearColor(0.3f, 0.3f, 0.3f, 1.f);
    
    //绘制前一定要清空缓冲区
    glClear(GL_COLOR_BUFFER_BIT);
    
    //设置视口大小
    //先拿到mainScreen主屏的缩放因子
    CGFloat scale = [UIScreen mainScreen].scale;
    //设置视口
    glViewport(MY_ORIGIN.x * scale, MY_ORIGIN.y * scale, MY_SIZE.width * scale, MY_SIZE.height * scale);
    
    //加载着色器,链接program,使用program
    [self loadShaderAndLinkUseProgram];
    
    //设置顶点
    [self makeVertex];
    
    //处理纹理信息
    [self makeTextureInfo];
    
    //绘图
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    //将渲染缓冲区(RenderBuffer)上的内容渲染到屏幕上
    [self.nContext presentRenderbuffer:GL_RENDERBUFFER];
    
}

#pragma mark - 加载着色器,链接并使用程序program
- (void)loadShaderAndLinkUseProgram
{
    
    //读取顶点着色器和片元着色器的程序文件
    //拿到顶点着色器和片元着色器的程序路径
    NSString *vshFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fshFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
    
    //加载着色器文件,并创建最终的程序
    self.nProgram = [self loadVertex:vshFile Fragment:fshFile];
    
    //链接程序
    glLinkProgram(self.nProgram);
    
    //获取链接的状态
    GLint linkStatus;
    glGetProgramiv(self.nProgram, GL_LINK_STATUS, &linkStatus);
    //判断程序是否链接成功
    if (linkStatus == GL_FALSE) {
        //失败的话要拿取错误信息,存储在数组里面
        //定义错误信息数组GLChar类型数组,直接分配内存空间
        GLchar message[512];
        //参数:(1)程序 (2)错误信息的内存大小 (3)从哪里开始放 (4)错误信息放在哪里,直接写message一样,数组首地址
        glGetProgramInfoLog(self.nProgram, sizeof(message), 0, &message[0]);
        NSLog(@"程序链接失败,失败信息 : %@",[NSString stringWithUTF8String:message]);
        return;
    }
    
    //使用Program
    glUseProgram(self.nProgram);
    
}

#pragma mark - 处理顶点数据
- (void)makeVertex
{
    
    //设置顶点坐标数组
    GLfloat vertexArr[] = {
    
        0.5f,-0.5f,0.f,   1.f,0.f,
        -0.5f,0.5f,0.f,   0.f,1.f,
        -0.5f,-0.5f,0.f,  0.f,0.f,
        
        0.5f,0.5f,0.f,    1.f,1.f,
        -0.5f,0.5f,0.f,   0.f,1.f,
        0.5f,-0.5f,0.f,   1.f,0.f
    
    };
    
    //处理顶点信息
    //定义变量存储顶点缓存区ID
    GLuint vertexID;
    
    //申请顶点缓存区,并将ID赋值
    glGenBuffers(1, &vertexID);
    
    //绑定缓存区ID和对应的缓存区类型
    glBindBuffer(GL_ARRAY_BUFFER, vertexID);
    
    //将顶点数据从CPU拷贝到GPU中,也就是内存数据放入显存
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexArr), vertexArr, GL_DYNAMIC_DRAW);
    
    //将顶点数据通过Program,传入到顶点着色器的position中,并返回一个属性变量的位置
    //第二个参数必须和顶点着色器中的顶点坐标属性字母完全一致
    GLuint position = glGetAttribLocation(self.nProgram, "position");
    
    //打开属性通道,并且以合适的格式传输从buffer中读取顶点数据
    glEnableVertexAttribArray(position);
    
    //设置顶点坐标读取方式
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
    
}

#pragma mark - 设置纹理信息
- (void)makeTextureInfo
{
    
    //将纹理坐标通过Program传入到顶点和片元着色器,同样的,字母名称必须和着色器中定义的变量完全一致
    GLuint textCoord = glGetAttribLocation(self.nProgram, "textCoordinate");
    
    //打开属性通道,传输纹理坐标
    glEnableVertexAttribArray(textCoord);
    
    //设置纹理坐标的读取方式
    glVertexAttribPointer(textCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
    
    //加载纹理
    [self loadTexture:@"image1"];
    
    //设置纹理采样器
    //参数:
    //(1). 第一个是得到纹理的ID索引的位置,因为纹理是不经常改变的,所以用Uniform通道
    //(2). 第几个纹理
    glUniform1i(glGetUniformLocation(self.nProgram, "colorMap"), 0);
    
}

#pragma mark - 加载着色器shader,并返回Program信息
- (GLuint)loadVertex:(NSString *)vertexFile Fragment:(NSString *)fragmentFile
{
    
    //定义两个临时的着色器变量
    GLuint vertextShader, fragmentShader;
    
    //创建程序
    GLuint program = glCreateProgram();
    
    //编译顶点着色器和片元着色器程序
    //参数:
    //(1). 编译完成后的着色器的内存地址
    //(2). 编译的是哪个着色器,也就是着色器的类型。
    //(3). 着色器文件的项目路径
    //编译顶点着色器
    [self compileShader:&vertextShader type:GL_VERTEX_SHADER file:vertexFile];
    //编译片元着色器
    [self compileShader:&fragmentShader type:GL_FRAGMENT_SHADER file:fragmentFile];
    
    //把着色器都附着或者说链接上程序
    //附着顶点着色器
    glAttachShader(program, vertextShader);
    //附着片元着色器
    glAttachShader(program, fragmentShader);
    
    //用完了这两个临时的着色器变量,也就是附着到程序上面了,就可以删除掉了
    //删除顶点着色器
    glDeleteShader(vertextShader);
    //删除片元着色器
    glDeleteShader(fragmentShader);
    
    return program;
    
}

//编译着色器
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
    
    //读取shader文件的路径
    NSString *shaderFile = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    //因为glShaderSouce这个函数需要的是字符串类型的指针,所以这里转成C语言的字符串
    const GLchar *source = (GLchar *)[shaderFile UTF8String];
    
    //创建一个shader,并直接将创建的shader放入参数传过来的着色器内容(这里的*不是指的地址,是指的临时着色器的内容)
    *shader = glCreateShader(type);
    
    //将着色器源码附着到着色器对象上
    //参数:
    //(1). shader,要编译的着色器对象(*shader)
    //(2). 着色器源码字符串的数量,就是用了几个字符串写的或者说承载的着色器源码
    //(3). 真正的着色器程序的源码,也就是vsh和fsh里面的。(这就是第二个参数说的那一个字符串的地址)
    //(4). 着色器源码字符串的长度,如果不知道或者说不确定,写NULL,NULL代表字符串的终止位
    glShaderSource(*shader, 1, &source, NULL);
    
    //将着色器源码编译成目标代码
    glCompileShader(*shader);
    
}

#pragma mark - 从图片中加载纹理
- (void)loadTexture:(NSString *)textureFile
{
    
    //将UIImage类型的图片转换成CGImageRef,因为纹理最终需要的是像素位图,也就是要解压图片
    CGImageRef spriteImage = [UIImage imageNamed:textureFile].CGImage;
    
    //可以判断一下是否获得到了像素位图
    if (!spriteImage) {
        NSLog(@"解压缩图片失败 : %@",textureFile);
        //非正常运行程序导致程序退出。exit(0)是正常运行程序导致退出
        exit(1);
    }
    
    //成功拿到位图了,获取图片的宽高的大小
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    //获取图片字节数是多少  也就是图片面积 * 颜色通道数量(RGBA就是4个)
    //也可以用malloc,malloc(width * height * 4 * sizeof(GLubyte));
    //稍提一嘴,calloc就是在内存的动态存储区上,分配第一个参数个数量的,每个单位长度为第二个参数的大小的连续空间
    //返回值是指向分配起始地址的指针,分配失败的话,返回值是NULL
    //calloc会清空分配的内存,而malloc不会。所以自行选择
    GLubyte *spriteByte = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
    
    //创建上下文
    //参数:
    //(1). 指向要渲染的绘制图像的地址
    //(2). bitmap(位图)的宽,单位是像素
    //(3). bitmap(位图)的高,单位是像素
    //(4). bitsPerComponent是指内存中,像素的每个组件的位数,比如32位的RGBA,那么每一个颜色位都是8
    //(5). bytesPerRow指的是bitmap每一行内存需要多少bit(位)内存
    //(6). space指的是bitmap使用的颜色空间,可以通过CGImageGetColorSpace()获取
    //(7). bitmapInfo是枚举类型,CGImageAlpahInfo
    CGContextRef spriteContext = CGBitmapContextCreate(spriteByte, width, height, 8, width * 4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
    //在上下文上把图片绘制出来
    //定义变量,存储位图的尺寸CGRect
    CGRect rect = CGRectMake(0, 0, width, height);
    
    //使用默认的方法绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
    
    //绘制完成后就可以释放上下文了
    CGContextRelease(spriteContext);
    
    //绑定纹理到默认的纹理ID,因为glUniform里面也设置的0
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //设置纹理属性,这里就不多说了,可以参考OpenGL的文章,里面有纹理的属性设置
    //参数:
    //(1). 纹理维度
    //(2). 要设置的纹理属性的名字
    //(3). 要设置的纹理属性的参数
    //这里要设置纹理过滤方式和环绕方式
    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);
    
    //要转一下图片宽高的类型,不然会提示,毕竟一个是unsigned的size_t,但是载入纹理要的是int_32
    float tWidth = width,tHeight = height;
    
    //载入2D纹理
    //https://www.jianshu.com/p/4e2bb76e31c3  这里有解释
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tWidth, tHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteByte);
    
    //图片数据也用完了,可以释放了
    free(spriteByte);
    
}


/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

@end

效果图如下2.2所示:

2.2.png
上一篇 下一篇

猜你喜欢

热点阅读