学习OpenGL ES之什么是Shader?

本系列所有文章目录

获取示例代码


上一篇文章中我们有说到OpenGL的渲染流程。


这其中Vertex Shader和Fragment Shader两步是可编程的。简而言之,Vertex Shader负责将顶点数据进一步处理,Fragment Shader将像素数据进一步处理。所以Vertex Shader中的代码针对每个点都会调用一次,Fragment Shader中的代码针对每个像素都会调用一次。接下来我就分三个部分讲解Shader的相关知识。

如何使用Shader

要使用Shader首先要编译Shader代码。

bool compileShader(GLuint *shader, GLenum type, const GLchar *source) {
    GLint status;
    
    if (!source) {
        printf("Failed to load vertex shader");
        return false;
    }
    
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    glCompileShader(*shader);
    
    GLint logLength;
    glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
    
#if Debug
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetShaderInfoLog(*shader, logLength, &logLength, log);
        printf("Shader compile log:\n%s", log);
        printf("Shader: \n %s\n", source);
        free(log);
    }
#endif
    
    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
    if (status == 0) {
        glDeleteShader(*shader);
        return false;
    }
    
    return true;
}

然后把编译好的Shader附加到Program上,Program可以理解为一个跑在GPU上的小程序。

// Attach vertex shader to program.
glAttachShader(program, vertShader);
    
// Attach fragment shader to program.
glAttachShader(program, fragShader);

然后链接Program

    if (!linkProgram(program)) {
        printf("Failed to link program: %d", program);
        
        if (vertShader) {
            glDeleteShader(vertShader);
            vertShader = 0;
        }
        if (fragShader) {
            glDeleteShader(fragShader);
            fragShader = 0;
        }
        if (program) {
            glDeleteProgram(program);
            program = 0;
        }
        return false;
    }
bool linkProgram(GLuint prog) {
    GLint status;
    glLinkProgram(prog);
    
#if Debug
    GLint logLength;
    glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetProgramInfoLog(prog, logLength, &logLength, log);
        printf("Program link log:\n%s", log);
        free(log);
    }
#endif
    
    glGetProgramiv(prog, GL_LINK_STATUS, &status);
    if (status == 0) {
        return false;
    }
    
    return true;
}

链接完后就可以使用了。所有和GPU交互的代码都会用到program的值。激活Vertex Shader属性的代码就用到了program。

GLuint positionAttribLocation = glGetAttribLocation(self.shaderProgram, "position");
glEnableVertexAttribArray(positionAttribLocation);
GLuint colorAttribLocation = glGetAttribLocation(self.shaderProgram, "color");
glEnableVertexAttribArray(colorAttribLocation);

Vertex Shader的语法

为了介绍Shader中的uniform变量,我特地在上一篇文章的基础上修改了Vertex Shader,如下。

attribute vec4 position;
attribute vec4 color;

uniform float elapsedTime;

varying vec4 fragColor;

void main(void) {
    fragColor = color;
    float angle = elapsedTime * 1.0;
    float xPos = position.x * cos(angle) - position.y * sin(angle);
    float yPos = position.x * sin(angle) + position.y * cos(angle);
    gl_Position = vec4(xPos, yPos, position.z, 1.0);
}

Shader的变量声明格式为:变量类型 变量数据类型 变量名;
变量类型有三种:

  1. attribute:就是顶点数据(Vertex Data)包含的属性,位置,颜色或是其他,顶点数据包含多少属性,这里就可以写多少,通过glEnableVertexAttribArrayglVertexAttribPointer激活和传值。
  2. varying: 传递给Fragment Shader的变量,Fragment Shader是无法直接接受顶点数据的,因为它处理的是像素级别的数据。传递给Fragment Shader的值是根据像素位置插值计算之后的值。
  3. uniform: 可以理解为全局变量,所有顶点处理程序共享这个变量。

变量数据类型有:

  1. vecX: vec开头的有vec2,vec3,vec4,分别代表二维,三维,四维向量。初始化方式分别为,vec2(x,y),vec3(x,y,z),vec4(x,y,z,w)
  2. float: 浮点数,记住,shader中int是不会自动转换为float的,所有需要使用float的地方必须写成浮点数格式,比如1要写成1.0,0要写成0.0。
  3. int: 整型
  4. matX: vec开头的有mat2,mat3,mat4,分别代表二维,三维,四维矩阵。主要用来传递变换矩阵,后面使用到时会介绍。

上面的代码的uniform变量elapsedTime表示的是程序运行经过时间的秒数。

float angle = elapsedTime * 1.0;//修改1.0为其他值可以调整转速
float xPos = position.x * cos(angle) - position.y * sin(angle);
float yPos = position.x * sin(angle) + position.y * cos(angle);
gl_Position = vec4(xPos, yPos, position.z, 1.0);

将position围绕(0,0,0)点旋转角度angle,然后将旋转后的点赋给gl_Position,就是交给OpenGL进行后续处理。angle会根据elapsedTime变化,所以点的位置也会根据elapsedTime变化。

我增加了两行代码来为uniform elapsedTime赋值。

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    // 清空之前的绘制
    glClearColor(1, 0.2, 0.2, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 使用fragment.glsl 和 vertex.glsl中的shader
    glUseProgram(self.shaderProgram);
    // 设置shader中的 uniform elapsedTime 的值
    GLuint elapsedTimeUniformLocation = glGetUniformLocation(self.shaderProgram, "elapsedTime");
    glUniform1f(elapsedTimeUniformLocation, (GLfloat)self.elapsedTime);
    
    [self drawTriangle];
}

先获取uniform elapsedTime的位置,因为elapsedTimefloat类型,所以使用glUniform1f进行赋值。如果是其他类型的uniform,就需要使用其他的glUniformXXX方法赋值了。后面用到的时候再详细介绍。

这个Shader具体的效果如下:

shader.gif

每个顶点都会旋转,所以最后整个三角形都在旋转。如果你乐意的话,还可以在Vertex Shader中对顶点位置进行缩放或者移动。试试看会有什么样的效果。

Fragment Shader的语法

varying lowp vec4 fragColor;
uniform highp float elapsedTime;

void main(void) {
    highp float processedElapsedTime = elapsedTime;
    highp float intensity = (sin(processedElapsedTime) + 1.0) / 2.0;
    gl_FragColor = fragColor * intensity;
}

Fragment Shader的变量只能是uniformvarying,这里的varying是从Vertex Shader传过来的值。与Vertex Shader不同的是,这里的变量都要声明精度,比如highp float processedElapsedTime = elapsedTime;中的highp代表高精度。精度包括lowp highp mediump,低精度,高精度,中等精度。如果你不想为每一个变量都指定精度可以在第一行写上precision highp float;,当然这只是为float指定默认精度highp。要为其他类型指定精度的话继续加就好了。比如再加上precision highp vec2;
上面的Fragment Shader中我把传递过来的颜色根据当前的elapsedTime进行了计算,颜色的强度会随着(sin(processedElapsedTime) + 1.0) / 2.0;的曲线变化。这里我做了vec4 * float的运算,结果仍然是vec4vec4(a,b,c,d) * f的运算结果是vec4(a*f,b*f,c*f,d*f)
使用了这个Fragment Shader后效果如下。

本文主要通过Shader的两个小动画介绍了Vertex Shader和Fragment Shader的基本语法和功能。如果你想要了解更多Shader中的运算规则和内置函数请参见:
OpenGL-ES-2_0-Reference-card
我还发现了一个比较有意思的网站,可以直接在线编辑预览Fragment Shader。
http://haxiomic.github.io/webgl-workshop/editor//index.html

time是一个默认的uniform,会随着时间改变。uv是纹理坐标,有两个值s和t,值从0到1。有兴趣的可以去玩玩。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容