OpenGL ES 2.0 ——一張膠片的紋理渲染之旅

前面的博客沒有過多的涉及代碼,這篇博客就聊聊渲染紋理,和OpenGL ES2.0 API的使用 和 步驟。編程

什麼是紋理? 紋理能夠是一張圖片也能夠是顏色,就好像咱們穿着的那一層衣服,他就是紋理;你在赤裸的身體上用一種或多種顏料畫滿了畫,這層畫也是紋理,他就是最外層的包裝。swift

什麼是紋理渲染?,就是在將那層紋理轉換成famebuffer內帶顏色的像素點,顯示在屏幕上,錯落不一樣的顏色像素點,構成了咱們看到的畫面。因此說,紋理就是以不一樣顏色的形式呈如今屏幕上。api

紋理圖片
相機:哈蘇500cm
膠片:Kodak Ektar100
環境
操做系統:macOS 10.15.5
IDE:Xcode 11.5
運行設備:Simulator iPhone SE 2nd
執行結果 數組

接下來按照這個流程來進行代碼編寫,從 開始設置圖層CAEAGLLayer->完成FrameBuffer 看成是渲染的準備工做,最後一步渲染會繼續拆分緩存

引入必要的庫——OpenGL ES2.0

#import <OpenGLES/ES2/gl.h>markdown

定義須要用到的變量屬性

@interface DrawView()

/// 在iOS和tvOS上繪製OpenGL ES內容的圖層,CAEAGLLayer繼承於CALayer
@property(nonatomic,strong)CAEAGLLayer *myEagLayer;

/// 建立的上下文
@property(nonatomic,strong)EAGLContext *myContext;

/// 渲染緩存區 (實際上是該緩存區的ID,經過ID能夠訪問該緩存區,類比指針)
@property(nonatomic,assign)GLuint myColorRenderBuffer;
/// 幀緩存區 (同上)
@property(nonatomic,assign)GLuint myColorFrameBuffer;

/// 自定義的可編程管線,編譯並附着了頂點着色器和片元着色器,能夠將CPU內的頂點等其餘數據傳入自定義着色器
@property(nonatomic,assign)GLuint myPrograme;

@end
複製代碼

1、設置渲染的圖層CAEAGLLayer

重寫layerClass 將self.layer從CALayer變成->CAEAGLLayer框架

// 代碼
+ (Class)layerClass
{
    return [CAEAGLLayer class];
}
複製代碼

開始設置圖層ide

// 代碼
- (void)setupLayer
{
    //1.建立特殊圖層
    self.myEagLayer = (CAEAGLLayer *)self.layer;
    
    //2.設置scale
    [self setContentScaleFactor:[[UIScreen mainScreen]scale]];

    //3.設置描述屬性,這裏設置不維持渲染內容以及顏色格式爲RGBA8
    NSDictionary *drawableProperties = @{
        kEAGLDrawablePropertyRetainedBacking: @false,
        kEAGLDrawablePropertyColorFormat    : kEAGLColorFormatRGBA8
    };
    
    self.myEagLayer.drawableProperties = drawableProperties;
  
}
複製代碼

CAEAGLLayer是Apple在QuartzCore裏爲咱們提供的繪製圖層,咱們繪製的紋理等能夠繪製在這個圖層,他繼承於CALayer,而且繼承了協議EAGLDrawable 函數

drawablePropertieskEAGLDrawablePropertyRetainedBacking表示繪圖表面顯示後,是否保留其內容
drawableProperties中的kEAGLDrawablePropertyColorFormat表示繪製表面的內部顏色緩存區格式 下面這是Apple給出的定義ui

/************************************************************************/
/* Values for kEAGLDrawablePropertyColorFormat key                      */
/************************************************************************/
EAGL_EXTERN NSString * const kEAGLColorFormatRGBA8;	// 32位RGBA的顏色,4*8=32位
EAGL_EXTERN NSString * const kEAGLColorFormatRGB565;// 16位RGB的顏色
EAGL_EXTERN NSString * const kEAGLColorFormatSRGBA8 // sRGB表明了標準的紅、綠、藍,即CRT顯示器、LCD顯示器、投影機、打印機以及其餘設備中色彩再現所使用的三個基本色素。sRGB的色彩空間基於獨立的色彩座標,可使色彩在不一樣的設備使用傳輸中對應於同一個色彩座標體系,而不受這些設備各自具備的不一樣色彩座標的影響。NS_AVAILABLE_IOS(7_0);
複製代碼

2、建立上下文EAGLContext

-(void)setupContext
{
    //1.指定OpenGL ES 渲染API版本,咱們使用2.0
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
    
    //2.建立圖形上下文
    EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
    
    //3.判斷是否建立成功
    if (!context) {
        NSLog(@"Create context failed!");
        return;
    }
    
    //4.設置圖形上下文
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"setCurrentContext failed!");
        return;
    }
    
    //5.將局部context,變成全局的
    self.myContext = context;  
}
複製代碼

3、清空緩存區

-(void)deleteRenderAndFrameBuffer
{
    /*
     buffer分爲frame buffer 和 render buffer2個大類。
     其中frame buffer 至關於render buffer的管理者。
     frame buffer object即稱FBO。
     render buffer則又可分爲3類。colorBuffer、depthBuffer、stencilBuffer。
     */
    
    glDeleteBuffers(1, &_myColorRenderBuffer);
    self.myColorRenderBuffer = 0;
    
    glDeleteBuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;
}
複製代碼

在這裏,出現了兩個緩存區:framebuffer 、 renderbuffer

framebuffer 和 renderbuffer

framebuffer在離屏渲染的時候曾提到過,這是一個最終繪製完成的緩存區,當屏幕刷新控制器刷新(60Hz)時,從framebuffer交給屏幕顯示。 一個framebuffer object 對象又被稱爲FBO,下面均使用FBO

renderbuffer是什麼呢?下圖是蘋果官方提供的framebuffer和renderbuffer的關係圖。renderbuffer有三種:colorbuffer(顏色)、depthbuffer(深度)、texture(紋理)。這三種分別用來存放color、depth、texture。FBO則提供了三個附着點 GL_COLOR_ATTACHMENT0 、 GL_DEPTH_ATTACHMENT 、GL_STENCIL_ATTACHMENT 分別用來掛載(綁定) colorbuffer object 、depthbuffer object 、 texture

圖中只畫出了GL_COLOR_ATTACHMENT0和GL_DEPTH_ATTACHMENT

4、設置緩存區RenderBuffer

- (void)setupRenderBuffer
{
    //1.定義一個緩存區ID
    GLuint buffer;
    
    //2.申請一個緩存區標誌
    glGenRenderbuffers(1, &buffer);
    
    //3.
    self.myColorRenderBuffer = buffer;
    
    //4.將標識符綁定到GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    
    //5.將可繪製對象drawable object's  CAEAGLLayer的存儲綁定到OpenGL ES renderBuffer對象
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
複製代碼

蘋果官方文檔也講的很清楚:申請、綁定、 存儲到self.myEagLayer

5、設置緩存區FrameBuffer

- (void)setupFrameBuffer
{
    //1.定義一個緩存區ID
    GLuint buffer;
    
    //2.申請一個緩存區標誌
    glGenBuffers(1, &buffer);
    
    //3. 
    self.myColorFrameBuffer = buffer;
    
    //4. 將申請的緩存區標誌和緩存區綁定,ID即該緩存區,經過使用ID就是在使用緩存區
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    
    /*生成幀緩存區以後,則須要將renderbuffer跟framebuffer進行綁定,
     調用glFramebufferRenderbuffer函數進行綁定到對應的附着點上,後面的繪製才能起做用
     */
    
    //5.將渲染緩存區myColorRenderBuffer 經過glFramebufferRenderbuffer函數綁定到 GL_COLOR_ATTACHMENT0上。
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
    
}
複製代碼

步驟和前面的renderbuffer差很少

  • 使用函數:glGenBuffers 申請緩存區標誌
  • 使用函數:glBindFramebuffer 綁定緩存區標誌和真實的緩存區,後面能夠直接使用此標誌,類別指針來理解。
  • 可是接下來不一樣於renderbuffer,framebuffer是將renderbuffer綁定到framebuffer本身的GL_COLOR_ATTACHMENT0位置,GL_COLOR_ATTACHMENT0是一個位置。

6、渲染

這一步算是比較核心,須要準備繪製圖形的編譯着色器->使用program->座標——頂點數據->圖片解碼->加載紋理->提交framebuffer顯示。

這一步的方法,咱們命名爲- (void)renderDraw {}, 下面的代碼塊和用到完整方法均是在此方法內執行或調用

- (void)renderDraw
{
	// 常規基礎操做:清除背景色、設置視口
    
    // 讀取頂點着色器、片元着色器
    
    // 加載編譯後的頂點、片元着色器
    
    // 連接、使用附着了兩大着色器的program
    
    // 設置頂點座標和紋理座標
    
    // 處理頂點:申請頂點緩存區、
    
    // 加載紋理:(圖片解碼)
    
    // 繪製、提交顯示
}
複製代碼

一、常規基礎操做:清除背景色、設置視口

// renderDraw的代碼
//設置清屏顏色
glClearColor(0.3f, 0.45f, 0.5f, 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);
複製代碼

二、讀取頂點着色器、片元着色器

咱們熟悉的美顏相機的各類濾鏡,其實就是用GLSL語言自定義頂點或者片元着色器,在GPUImage的代碼裏,能看到不一樣濾鏡的着色器代碼。可是怎樣用GLSL去寫一個自定義濾鏡,後面開專題案例,也能夠參考GPUImage,它裏面有很是豐富的GLSL濾鏡代碼。

// renderDraw的代碼
// 讀取自定義的兩個着色器文件
NSString *vertFile = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fragFile = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];
複製代碼

這裏須要說明一下,由於Xcode不支持編譯GLSL,因此頂點着色器和片元着色器就是兩段字符串文本,我只是將他們單獨寫在兩個文件裏,再讀取。事實上這兩段文本也能夠以其餘的形態存在,在GPUImage中,這兩個着色器就是以C字符串的形式同時定義在.m文件裏,只要拿到的是字符串,能知足OpenGL指定的方法去編譯便可

三、加載編譯後的頂點、片元着色器

上面也講了,Xcode不支持編譯着色器,咱們須要OpenGL本身去編譯,而後附着到program上,點到爲止,剩下的事情,由program繼續完成。因此這一步,咱們的任務是:將着色器字符串編譯->附着到program。

咱們先封裝一個着色器編譯方法compileShader:type:file:

// 着色器編譯
//shader:編譯完存儲的底層地址
//type:編譯的類型,GL_VERTEX_SHADER(頂點)、GL_FRAGMENT_SHADER(片元)
//file:文件路徑
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
    
    //1.讀取文件路徑字符串
    NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    const GLchar* source = (GLchar *)[content UTF8String];
    
    //2.建立一個shader(根據type類型)
    *shader = glCreateShader(type);
    
    //3.將着色器源碼附加到着色器對象上。
    //參數1:shader,要編譯的着色器對象 *shader
    //參數2:numOfStrings,傳遞的源碼字符串數量 1個
    //參數3:strings,着色器程序的源碼(真正的着色器程序源碼)
    //參數4:lenOfStrings,長度,具備每一個字符串長度的數組,或NULL,這意味着字符串是NULL終止的
    glShaderSource(*shader, 1, &source,NULL);
    
    //4.把着色器源代碼編譯成目標代碼
    glCompileShader(*shader);
}
複製代碼

這段代碼就幹了三件事:

  • 建立shader——————————————glCreateShader
  • 附着shader的GLSL源碼————-glShaderSource
  • 編譯shader——————————————glCompileShader

回到renderDraw方法,這是咱們整個渲染模塊的主函數。

// renderDraw的代碼
// 一、定義兩個着色器 
GLuint verShader, fragShader;

// 二、編譯頂點和片元着色器
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];

// 三、建立program
self.myPrograme = glCreateProgram();

// 四、將編譯的着色器附着到program
glAttachShader(program, verShader);
glAttachShader(program, fragShader);

// 5.釋放不須要的shader
glDeleteShader(verShader);
glDeleteShader(fragShader); 
複製代碼

此刻咱們就已經拿到了附着了編譯好着色器的program——self.myPrograme。 在拿到編譯好的shader後,這段代碼其實就幹了兩件事,目的是獲取program:

  • 建立program
  • 附着shader到program

四、連接、使用附着了兩大着色器的program

上面已經提到Program,Program幹什麼,打個比方:她就像是擁有了兩項已經練成(編譯附着)的絕世武功(着色器),而咱們又不會,那咱們只有經過告訴她作什麼,間接地使用了這兩項絕世武功(着色器)。因此program更像是一個OC/swift和GLSL通訊的橋樑。 就像是JSCoreBridge的做用!!!

// renderDraw的代碼
// 一、連接program
glLinkProgram(self.myPrograme);
GLint linkStatus;
// 2.獲取連接狀態
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
    // 連接異常處理
    return;
}
// 3.使用program
glUseProgram(self.myPrograme);
複製代碼

值得注意的是———— 到這一步爲止,咱們已經完成開始使用program,目的是爲了將頂點數據等其餘傳入頂點和片元着色器,因此咱們也能夠先作接下來的 設置頂點座標和紋理座標 和 設置頂點數據 ,只要在經過program將數據傳入着色器以前完成使用program就能夠。

五、設置頂點座標和紋理座標

// renderDraw的代碼
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,
    };
複製代碼

attAr存放了紅色虛線分割的兩個三角形,共計六個頂點和紋理座標。 前3個是頂點座標,後邊2個是映射的紋理座標

這裏的座標系是物體座標系,即物體中心點是座標中心點,以下圖所示:左邊是按照數組映射了紋理的頂點座標系,右邊是紋理座標系。

補充: 咱們在貼圖的時候,紋理的座標系以左下角爲原點。長寬均爲1,和紋理圖片的實際寬高無關。咱們要作的就是將紋理對應的映射到頂點上,完成貼圖。

六、處理頂點數據

這一步,咱們的目的很簡單:就是申請一塊頂點緩存區存放剛剛的頂點數據,再將緩存區內的頂點經過program傳給頂點着色器。

(1)首先:申請頂點緩存區,存入數據

又是緩存三步曲:申請glGenBuffers、綁定glBindBuffer、放數據glBufferData

// renderDraw的代碼
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);
複製代碼

(2)將頂點數據經過myPrograme中的傳遞到頂點着色程序的position

  • glGetAttribLocation/glGetUniformLocation 獲取着色器中定義的變量
// renderDraw的代碼
// 「position」是着色器中定義的變量名,先經過glGetAttribLocation獲取着色器中這個變量標誌,若是position是用uniform定義,則使用glGetUniformLocation
GLuint position = glGetAttribLocation(self.myPrograme, "position");
// 設置合適的格式從buffer裏面讀取數據
glEnableVertexAttribArray(position);
// 設置讀取方式
glVertexAttribPointer(position, 3, GL_FLOAT,GL_FALSE,sizeof(GLfloat) * 5, NULL);
複製代碼

這也是OC和着色器的數據傳遞的固定格式。其中的glVertexAttribPointer

//參數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 (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
複製代碼

(3)將紋理數據經過myPrograme中的傳遞到頂點着色程序的textCoordinate

// renderDraw的代碼

GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);
複製代碼

七、加載紋理

咱們封裝一個方法setupTexture從圖片中獲取紋理

// renderDraw的代碼

[self setupTexture:@"bali"];
複製代碼

setupTexture方法展開:

- (GLuint)setupTexture:(NSString *)fileName {
    
    //一、將 UIImage 轉換爲 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    //判斷圖片是否獲取成功
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }
    
    //二、讀取圖片的大小,寬和高
    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);
    

    //五、在CGContextRef上--> 將圖片繪製出來
    /*
     CGContextDrawImage 使用的是Core Graphics框架,座標系與UIKit 不同。UIKit框架的原點在屏幕的左上角,Core Graphics框架的原點在屏幕的左下角。
     CGContextDrawImage 
     參數1:繪圖上下文
     參數2:rect座標
     參數3:繪製的圖片
     */
    CGRect rect = CGRectMake(0, 0, width, height);
   
    //6.使用默認方式繪製
    CGContextDrawImage(spriteContext, rect, spriteImage);
   
    //七、畫圖完畢就釋放上下文
    CGContextRelease(spriteContext);
    
    //八、綁定紋理到默認的紋理ID(
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //9.設置紋理屬性
    /*
     參數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;
    
    //10.載入紋理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);
    
    //11.釋放spriteData
    free(spriteData);   
    return 0;
}
複製代碼

八、繪製、提交顯示

設置紋理採樣器 sampler2D

glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);

繪圖

glDrawArrays(GL_TRIANGLES, 0, 6);

從渲染緩存區顯示到屏幕上

[self.myContext presentRenderbuffer:GL_RENDERBUFFER];

總結

代碼的核心在於文章的 渲染 一大段的分析內。

第一個點:緩存區的使用

// 一、申請標誌
glGenBuffers (GLsizei n, GLuint* buffers);
// 二、綁定標誌和緩存區
glBindBuffer (GLenum target, GLuint buffer);// 普通buffer綁定
glBindFramebuffer (GLenum target, GLuint framebuffer);//Framebuffer綁定
glBindRenderbuffer (GLenum target, GLuint renderbuffer);//Renderbuffer的綁定
複製代碼

第二個點:着色器的編譯和編寫

// 一、建立
glCreateShader (GLenum type); 
// 二、附着GLSL源碼字符串
glShaderSource (GLuint shader, GLsizei count, const GLchar* const *string, const GLint* length); 
// 三、編譯
glCompileShader (GLuint shader); 
複製代碼

第三個點:program的建立和使用

// 一、建立
glCreateProgram (void) ; 
// 二、附着着色器
glAttachShader (GLuint program, GLuint shader); // 附着頂點着色器
glAttachShader (GLuint program, GLuint shader); // 附着片元着色器
// 三、連接
glLinkProgram (GLuint program); 
// 四、使用
glUseProgram (GLuint program);
複製代碼

第四個點:CPU與GPU的數據傳遞,經過program,將數據傳給着色器

// 一、從program中得到着色器的輸入變量
glGetAttribLocation; // 得到attribute定義的輸入變量
glGetUniformLocation;// 得到uniform定義的輸入變量
// 二、設置合適的格式從buffer裏面讀取數據
glEnableVertexAttribArray
// 三、設置讀取方式
glVertexAttribPointer;
複製代碼

第五個點:圖片的解碼 就是setupTexture方法

這個案例只是簡單以一張靜態圖片爲例。可是遺留了一個小問題:咱們在構造頂點數據時,Y軸正向是向上的,而通過一系列座標系轉換後,獲得iOS手機屏幕座標系截然相反,Y軸是向下的,最終呈現了圖片倒置的狀況。就須要對圖片作反轉處理,這個能夠在頂點着色器內完成,也能夠在OC代碼裏完成。

相關文章
相關標籤/搜索