iOS-OpenGL ES入門教程(五)初識(shí)GLSL

前言

前面的基礎(chǔ)文章列表

  1. iOS-零基礎(chǔ)學(xué)習(xí)OpenGL ES入門教程(一)

  2. iOS-OpenGL ES入門教程(二)最簡(jiǎn)單的紋理Demo

  3. iOS-OpenGL ES入門教程(三)紋理取樣,混合,多重紋理

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

寫在前面:OpenGLES系列是自己在做音視頻時(shí)候邊學(xué)邊記錄的文章 距離最初記錄已經(jīng)三年有余,現(xiàn)在回頭看補(bǔ)齊這個(gè)當(dāng)初的學(xué)習(xí)系列也算是一種回溯吧,學(xué)習(xí)總是讓人開心的,由陌生到熟練再到更進(jìn)一步精通的過(guò)程充滿著樂(lè)趣。希望自己抽出時(shí)間盡快將這個(gè)基礎(chǔ)系列補(bǔ)齊。如今雖然OpenGL在iOS上已經(jīng)被蘋果廢棄了,不過(guò)圖形框架的基礎(chǔ)原理和思路還是很相像的,熟悉opengl再熟悉metal會(huì)更快上手

初識(shí)GLSL

之前的章節(jié)提到過(guò) 我們使用GLKit框架所以避開了寫繁瑣的GLSL

GLSL是一個(gè)以C語(yǔ)言為基礎(chǔ)的高階著色語(yǔ)言 通過(guò)編寫glsl 提供開發(fā)者對(duì)繪圖管線更多的直接控制,而無(wú)需使用匯編語(yǔ)言或硬件規(guī)格語(yǔ)言

我們來(lái)看下怎么用GLSL
同樣是一個(gè)最簡(jiǎn)單的繪制一張圖片應(yīng)用

準(zhǔn)備繪制工作

不同于GLkit 我們直接使用GLkitView 我們先自定義一個(gè)UIView的子類 作為opengl繪制的載體View

@interface GLSLDemoView : UIView

@end

重載layerClass 指定為CAEAGLLayer iOS下只有CAEAGLLayer才支持opengl的繪制

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

初始化opengl上下文和配置好CAEAGLLayer的相關(guān)屬性

//設(shè)置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];
    
    
    //設(shè)置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);

常規(guī)的opengl渲染管線的準(zhǔn)備工作 接下來(lái) 我們就要開始加載我們自己的shader(著色器),
之前我們提過(guò)opengl的渲染管線中

Vetex shader 頂點(diǎn)著色器
Fragment shader 片元著色器

加載著色器

著色器自然是一種編程語(yǔ)言 我們首先要做的是編寫一個(gè)最簡(jiǎn)單的著色器 了解下著色器語(yǔ)法

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

這里介紹下最簡(jiǎn)單的修飾符

const:常量值必須在聲明是初始化。它是只讀的不可修改的。

attribute:表示只讀的頂點(diǎn)數(shù)據(jù),只用在頂點(diǎn)著色器中。數(shù)據(jù)來(lái)自當(dāng)前的頂點(diǎn)狀態(tài)或者頂點(diǎn)數(shù)組。它必須是全局范圍聲明的,不能在函數(shù)內(nèi)部。一個(gè)attribute可以是浮點(diǎn)數(shù)類型的標(biāo)量,向量,或者矩陣。不可以是數(shù)組或者結(jié)構(gòu)體

uniform:一致變量。在著色器執(zhí)行期間一致變量的值是不變的。與const常量不同的是,這個(gè)值在編譯時(shí)期是未知的是由著色器外部初始化的。一致變量在頂點(diǎn)著色器和片段著色器之間是共享的。它也只能在全局范圍進(jìn)行聲明。

varying:頂點(diǎn)著色器的輸出。例如顏色或者紋理坐標(biāo),(插值后的數(shù)據(jù))作為片段著色器的只讀輸入數(shù)據(jù)。必須是全局范圍聲明的全局變量??梢允歉↑c(diǎn)數(shù)類型的標(biāo)量,向量,矩陣。不能是數(shù)組或者結(jié)構(gòu)體。

最常用的內(nèi)置變量

gl_Position:vec4類型 輸出屬性-變換后的頂點(diǎn)的位置,用于后面的固定的裁剪等操作。所有的頂點(diǎn)著色器都必須寫這個(gè)值。

gl_FragColor:vec4類型 片元著色器的輸出顏色 可用于后續(xù)使用

知道了這些最簡(jiǎn)單的修飾符和內(nèi)置變量 我們就可以編寫起自己的shader啦

首先先編寫頂點(diǎn)著色器:(一般頂點(diǎn)著色器文件以.vsh結(jié)尾)

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對(duì)應(yīng)的旋轉(zhuǎn)矩陣 可以用來(lái)向量運(yùn)算 旋轉(zhuǎn)矩陣

varing 定義了 varyTextCoord 用來(lái)傳遞紋理坐標(biāo)給片元著色器

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

gl_Position輸出屬性-變換后的頂點(diǎn)的位置 這里做了個(gè)最簡(jiǎn)單的矩陣變換 vPos * rotateMatrix

接下來(lái)編寫片元著色器

varying lowp vec2 varyTextCoord;

uniform sampler2D colorMap;


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

sampler2D 二維紋理的句柄也就是當(dāng)前綁定的紋理 這里我們根據(jù)頂點(diǎn)著色器傳入的紋理坐標(biāo) 僅僅做個(gè)texture2D 取色 返回紋理對(duì)應(yīng)坐標(biāo)的顏色

這樣我們就編寫好了頂點(diǎn)和片元shader 接下來(lái)我們來(lái)代碼加載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 其實(shí)這正是對(duì)應(yīng)JIT類型的語(yǔ)言的編譯 鏈接 運(yùn)行

+ (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寫的有誤 這里會(huì)報(bào)錯(cuò) 不過(guò)相比于xcode這些編譯器的報(bào)錯(cuò) 給到的debug信息比較弱
可以看到 加載shader是比較常規(guī)的固定代碼流程 適合封裝到工具方法中 方便復(fù)用
接下來(lái)就是創(chuàng)建VBO 也就是頂點(diǎn) 并且綁定到我們的shader上 相當(dāng)于將數(shù)據(jù)從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);

這里的關(guān)鍵點(diǎn)glGetAttribLocation(self.mProgram, "position") 和 glGetAttribLocation(self.mProgram, "textCoordinate") 正是對(duì)應(yīng)的著色器代碼中的position和textCoordinate輸入變量

接下來(lái)綁定紋理

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

我們還有個(gè)旋轉(zhuǎn)矩陣變量需要設(shè)置輸入到著色器中

GLuint rotate = glGetUniformLocation(self.mProgram, "rotateMatrix");
    float radians = 180 * 3.14159f / 180.0f; //旋轉(zhuǎn)180度讓圖片正向
    float s = sin(radians);
    float c = cos(radians);
    
    //z軸旋轉(zhuǎn)矩陣
    GLfloat zRotation[16] = {
        c,-s,0,0,
        s,c,0,0,
        0,0,1,0,
        0,0,0,1
    };
    
    //設(shè)置旋轉(zhuǎn)矩陣
    glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);

最后一步繪制

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

這樣我們最簡(jiǎn)單的圖片繪制就完成了


總結(jié):

著色器語(yǔ)言GLSL是一種基于C的編程語(yǔ)言 可以理解為GPU編程 方便我們操作頂點(diǎn)和片元相關(guān)的渲染管線 實(shí)現(xiàn)我們自定義的繪制效果。和普通的編程一樣。需要編寫代碼 編譯 運(yùn)行 不過(guò)shader的加載編譯運(yùn)行是在app運(yùn)行是動(dòng)態(tài)解釋執(zhí)行的。理解了著色器的運(yùn)行流程。再去編寫GLSL。會(huì)有更加深入的理解。

Demo代碼地址:LearnOpenGLESDemo

參考文章:
GLSL基礎(chǔ)語(yǔ)法介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容