OpenGL ES 入門 — 3.使用GLSL繪制及變換圖片

第一節中我們采樣GLKBaseEffect來繪制圖片,這次我們使用編譯鏈接自定義的著色器(shader),用簡單的GLSL語言來實現頂點、片元著色器,并對圖形進行簡單的變換。

預先說下思路,大概有以下幾步:

  1. 設置圖層
  2. 設置上下文
  3. 清空緩存區
  4. 設置RenderBuffer
  5. 設置FrameBuffer
  6. 開始繪制

好,那我們一步步按照這6步來完成最終的渲染!

前期準備

我們可以自定義一個view,在故事板中將viewController的view的類型改為此view

自定義SFView

在自定義SFView.m中:
導入<OpenGLES/ES3/gl.h>框架

#import <OpenGLES/ES3/gl.h>

定義以下屬性:

//在iOS和tvOS上繪制OpenGL ES內容要用CAEAGLLayer圖層,它繼承于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;

重寫layoutSubviews,以上6步就在此方法中完成,當然你也可以寫到初始化方法中。

-(void)layoutSubviews
{
    //1.設置圖層
    [self setupLayer];
    
    //2.設置圖形上下文
    [self setupContext];
    
    //3.清空緩存區
    [self deleteRenderAndFrameBuffer];
    
    //4.設置RenderBuffer
    [self setupRenderBuffer];
    
    //5.設置FrameBuffer
    [self setupFrameBuffer];
    
    //6.開始繪制
    [self renderLayer];
    
}

1. 設置圖層

-(void)setupLayer
{
    //將SFView的圖層從CALayer替換成CAEAGLLayer
    self.myEagLayer = (CAEAGLLayer *)self.layer;
    
    //設置放大倍數
    [self setContentScaleFactor:[[UIScreen mainScreen]scale]];
    
    //CALayer 默認是透明的,必須將它設為不透明才能將其可見。
    self.myEagLayer.opaque = YES;
    
    //設置描述屬性,這里設置不維持渲染內容以及顏色格式為RGBA8
    self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:false],kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];
   
}

Note:將SFView的圖層從CALayer替換成CAEAGLLayer,還需要重寫layerClass方法。

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

其中設置描述屬性要說明一下,CAEAGLLayer 圖層的drawableProperties 屬性要用字典設置,key為kEAGLDrawablePropertyRetainedBacking表示繪圖表面顯示后,是否保留其內容。這個key對應的value,是一個通過NSNumber包裝的bool值。如果是false,則不保留,顯示內容后不能依賴于相同的內容,如果是ture,則保留,表示顯示后內容不變。一般只有在需要內容保存不變的情況下,才建議設置使用,但因為會導致性能降低、內存使用量增減,一般設置為flase。

key為kEAGLDrawablePropertyColorFormat表示可繪制表面的內部顏色緩存區格式,這個key對應的value是一個NSString指定特定顏色緩存區對象,有以下幾種:

  • kEAGLColorFormatRGBA8:32位RGBA的顏色,4*8=32位,默認。
  • kEAGLColorFormatRGB565:16位RGB的顏色,
  • kEAGLColorFormatSRGBA8:sRGB代表了標準的紅、綠、藍,即CRT顯示器、LCD顯示器、投影機、打印機以及其他設備中色彩再現所使用的三個基本色素。sRGB的色彩空間基于獨立的色彩坐標,可以使色彩在不同的設備使用傳輸中對應于同一個色彩坐標體系,而不受這些設備各自具有的不同色彩坐標的影響,不常用。

2. 設置上下文

-(void)setupContext
{
    //指定API版本
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES3;

    //創建圖形上下文
    EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];

    //判斷是否創建成功
    if (!context) {
        NSLog(@"上下文創建失敗");
        return;
    }
    
    //設置當前圖形上下文
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"設置當前圖形上下文失敗");
        return;
    }
    
    self.myContext = context;
}

3. 清空緩存區

-(void)deleteRenderAndFrameBuffer
{
    //刪除顏色渲染緩存區,幀緩存區
    glDeleteBuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
    
    //刪除幀緩存區
    glDeleteBuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
    
}

你可能注意到了,上面的RenderBufferFrameBuffer是什么鬼?

buffer分為RenderBufferFrameBuffer2個大類,其中FrameBuffer是幀緩存區,RenderBuffer是渲染緩存區,RenderBuffer又可分為3類,分別為colorBuffer、depthBuffer、stencilBuffer。

RenderBuffer和FrameBuffer

FrameBuffer包含三個附著點,分別是:color Attachment(顏色附著點),depth Attachment(深度附著點),stencil Attachment(模板附著點)。

它本身不保存顏色值、深度值、 模型,而是它內部的三個附著點對應指向(類似于指針)RenderBuffer的三個buffer:color/Texture mip(顏色紋理貼圖),depth buffer(深度緩沖區),stencil buffer(模板緩沖區),它們真正存放著顏色紋理值、深度值、 模型。所以,FrameBuffer相當于render buffer的管理者。

4. 設置RenderBuffer

-(void)setupRenderBuffer
{
    //定義一個緩存區
    GLuint buffer;
    
    //申請一個緩存區標識符
    glGenRenderbuffers(1, &buffer);
    self.myColorRenderBuffer = buffer;
    
    //將標識符綁定到GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    
    //分配存儲空間
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
    
}

5. 設置FrameBuffer

-(void)setupFrameBuffer
{
    //定義一個緩存區
    GLuint buffer;
    
    //申請一個緩存區標志符
    glGenFramebuffers(1, &buffer);
    self.myColorFrameBuffer = buffer;
    
    //將標識符綁定到GL_FRAMEBUFFER
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    
    //將renderbuffer跟framebuffer進行綁定
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
    
}

Note:frame buffer僅僅是管理者,不需要分配空間,生成空間之后,則需要將 renderbuffer跟framebuffer進行綁定,調用glFramebufferRenderbuffer函數進行綁定,后面的繪制才能起作用。

6. 開始繪制

這部分就要用GLSL編寫著色器程序了,先創建兩個empty文件,片元著色文件shaderf.fsh,頂點著色文件shaderv.vsh文件,文件后綴不重要,隨便取的。

創建empty文件

因為程序先執行頂點著色器文件shaderv.vsh,所以我們先編寫shaderv.vsh文件。
先定義如下屬性:

attribute vec4 position;//頂點位置
attribute vec2 textCoordinate;//紋理坐標
uniform mat4 rotateMatrix;//旋轉矩陣
varying vec2 varyTextCoord;//需要傳到片元著色器的紋理坐標數據

在main函數中作如下操作:

void main()
{
   //給需要傳到片元著色器的紋理數據賦值
    varyTextCoord = textCoordinate;
    
   //對頂點進行旋轉變換
    vec4 vPos = position;
    vPos = vPos * rotateMatrix;

    //給內建變量賦值
    gl_Position = vPos;
}

Note:gl_Position為內建變量,它在頂點著色器程序中必須要賦值,頂點著色器程序會計算出新的頂點,交給gl_Position。

再來編寫片元著色器文件shaderf.fsh。
先定義屬性:

varying vec2 varyTextCoord;

uniform sampler2D colorMap;//貼圖

Note:varyTextCoord是頂點著色器傳過來的,所以變量名包括修飾符都要跟頂點著色器一致。

在main函數中作如下操作:

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

Note:gl_FragColor屬于片元著色器中的內建函數,它也必須要賦值。

再回到SFView.m中:

-(void)renderLayer
{
    
    //設置清屏顏色
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);

    //清除屏幕
    glClear(GL_COLOR_BUFFER_BIT);
    
    //1.設置視口大小
    CGFloat scale = [[UIScreen mainScreen]scale];
    glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);
    
    //2.讀取頂點著色程序、片元著色程序
    NSString *vertFile = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fragFile = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];
    
    NSLog(@"vertFile:%@",vertFile);
    NSLog(@"fragFile:%@",fragFile);
    
    //3.加載shader
    self.myPrograme = [self loadShaders:vertFile Withfrag:fragFile];
    
    //4.鏈接
    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 *messageString = [NSString stringWithUTF8String:message];
        NSLog(@"Program Link Error:%@",messageString);
        return;
    } else {
        NSLog(@"Program Link Success!");
    }
    
    //5.使用program
    glUseProgram(self.myPrograme);
    
    //設置頂點、紋理坐標,前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, 0.0f,        1.0f, 1.0f, //右下
     -0.5f, 0.5f, 0.0f,        0.0f, 0.0f, // 左上
     -0.5f, -0.5f, 0.0f,       0.0f, 1.0f, // 左下
     0.5f, 0.5f, 0.0f,         1.0f, 0.0f, // 右上
     -0.5f, 0.5f, 0.0f,        0.0f, 0.0f, // 左上
     0.5f, -0.5f, 0.0f,        1.0f, 1.0f, // 右下
     };
     */
    
    /頂點緩存區
    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);

    //將頂點數據通過myPrograme中的傳遞到頂點著色程序的position,注意:第二參數字符串必須和shaderv.vsh中的輸入變量:position保持一致
    GLuint position = glGetAttribLocation(self.myPrograme, "position");
    
    //2.設置合適的格式從buffer里面讀取數據
    glEnableVertexAttribArray(position);
    
    //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(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
    
    //----處理紋理數據-------
    //1.glGetAttribLocation,用來獲取vertex attribute的入口的.
    //注意:第二參數字符串必須和shaderv.vsh中的輸入變量:textCoordinate保持一致
    GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
    
    //2.設置合適的格式從buffer里面讀取數據
    glEnableVertexAttribArray(textCoor);
    
    //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(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);
    
    //7. 加載紋理
    [self setupTexture:@"SF-1"];
    
    //rotate取的是shaderv.vsh中的uniform屬性,rotateMatrix,注意,想要獲取shader里面的變量,這里記得要。在glLinkProgram后面
    GLuint rotate = glGetUniformLocation(self.myPrograme, "rotateMatrix");
    
    //獲取弧度
    float radians = 10 * 3.14159f / 180.0f;

    //弧度對于的sin\cos值
    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, 0, 0, 1.0
    };
    
    //設置旋轉矩陣
    glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);
    
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    //將渲染緩存區中的數據渲染到上下文
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];
       
}

其中,第3步加載shader方法如下:

-(GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
    //定義2個零時著色器對象
    GLuint verShader, fragShader;
    //創建program
    GLint program = glCreateProgram();
    
    //編譯頂點著色程序、片元著色器程序
    //參數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];
    
    //創建最終的程序
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    //釋放不需要的shader
    glDeleteShader(verShader);
    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(根據type類型)
    *shader = glCreateShader(type);
    
    //將頂點著色器源碼附加到著色器對象上。
    //參數1:shader,要編譯的著色器對象 *shader
    //參數2:numOfStrings,傳遞的源碼字符串數量 1個
    //參數3:strings,著色器程序的源碼(真正的著色器程序源碼)
    //參數4:lenOfStrings,長度,字符串數組的長度
    glShaderSource(*shader, 1, &source,NULL);
    
    //把著色器源代碼編譯成目標代碼
    glCompileShader(*shader);
}

其中,第7步加載紋理方法如下:

- (GLuint)setupTexture:(NSString *)fileName {
    //1、獲取圖片的CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    //判斷圖片是否獲取成功
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }
    
    //2、讀取圖片的大小,寬和高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    //3.獲取圖片字節數 寬*高*4(RGBA)
    GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    
    //4.創建上下文
    /*
     參數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);
    
    
    
    //5、在CGContextRef上繪圖
    /*
     CGContextDrawImage 使用的是Core Graphics框架,坐標系與UIKit 不一樣。UIKit框架的原點在屏幕的左上角,Core Graphics框架的原點在屏幕的左下角。
     CGContextDrawImage 
     參數1:繪圖上下文
     參數2:rect坐標
     參數3:繪制的圖片
     */
    CGRect rect = CGRectMake(0, 0, width, height);
    //繪制,發現圖片是倒的。
    CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
    /*
     解決圖片倒置的方法:
     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);
     */
   
    //6、畫圖完畢就釋放上下文
    CGContextRelease(spriteContext);
    
    //5、綁定紋理到默認的紋理ID(這里只有一張圖片,故而相當于默認于片元著色器里面的colorMap,如果有多張圖不可以這么做)
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //設置紋理屬性
    /*
     參數1:紋理維度
     參數2:線性過濾、為s,t坐標設置模式
     參數3: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);
    
    float fw = width, fh = height;
    //載入紋理2D數據
    /*
     參數1:紋理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
     參數2:加載的層次,一般設置為0
     參數3:紋理的顏色值GL_RGBA
     參數4:寬
     參數5:高
     參數6:border,邊界寬度
     參數7:format
     參數8:type
     參數9:紋理數據
     */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    //綁定紋理
    /*
     參數1:紋理維度
     參數2:紋理ID,因為只有一個紋理,給0就可以了。
     */
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //釋放spriteData
    free(spriteData);
    
    return 0;
}

至此,自定義的view算是完成了,最后在viewController中調用一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.myView = (SFView *)self.view;
}

最終效果

最終效果圖

可以發現,圖片是倒置的,解決這個問題的方案也在代碼中有所提及,大家可以嘗試一下。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容