题记:本章会开始写一些滤镜了,LearnOpenGL CN中都以3D效果为主,此系列只处理2D图片效果,涉及视频、相机等。音视频学习Demo有OpenGL在相机、视频方面的应用,文章或代码若有错误,也希望大佬不吝赐教。
一、着色器
在OpenGL中,可编程管线允许开发者控制图形渲染流程中的特定阶段。这些自定义的控制程序称为着色器(Shader)。着色器运行在GPU上,使用特定的着色器语言(GLSL)编写,开发者可以根据需求实现各种复杂的算法和处理逻辑。
OpenGL ES对细分着色器和几何着色器支持有限,本系列只讨论顶点着色器和片元着色器。
- 顶点着色器(Vertex Shader)
- 处理顶点的位置、法线、纹理坐标等属性。
- 常用于顶点位移、骨骼动画等。
- 片段着色器(Fragment Shader)
- 处理像素(片段)的颜色、透明度等属性。
- 纹理采样、光照计算、颜色混合等。
关于着色器的处理流程,可以参考前面的文章,本章讲述语言的语法细节。
1.1 GLSL(OpenGL Shading Language)
1.1.1 数据类型
GLSL是一种类C语言,除了void
、bool
、int
、float
外,也支持一些对向量和矩阵的类型。
-
特有类型
- 向量:
vec2
,vec3
,vec4
- 支持
float
、int
、bool
类型 vec2
是float
类型2维向量;ivec3
是int
类型3维向量;bvec4
是bool
类型4维向量;
- 支持
- 矩阵:
mat3
,mat4
- 仅支持浮点类型,
mat4
表示4x4的浮点矩阵
- 仅支持浮点类型,
- 纹理:
sampler2D
,samplerCube
- 向量:
-
精度限定符
highp
(高)、mediump
(中)、lowp
(低),修饰int
、float
类型等lowp
的float类型范围是[-2, 2],在数据转换中需要特别注意- 顶点着色器 有默认类型
float
默认为highp
,int
默认为mediump
- 片元着色器
float
无默认类型,int
类型默认mediump
所以一般看Shader时会发现同一对象的定义,片元需要显式说明
arduino// 顶点着色器中定义 varying vec2 textureCoordinate; // 同一对象在片元着色器定义 varying highp vec2 textureCoordinate;
或者在片元着色器开始添加定义
arduino// 片段着色器必须声明默认浮点精度 precision mediump float;
-
变量限定符有两个版本
- 旧版(OpenGL ES 2.0)
uniform
:从CPU传递的全局常量。attribute
(只用于顶点着色器):逐顶点的输入数据。varying
:顶点着色器向片段着色器传递插值数据
- 新版(OpenGL ES 3.0+,兼容旧版)
uniform
:不变in
:输入变量,替代旧版attribute
和varying
out
:输出变量,替代旧版varying
inout
:函数参数修饰符,表示参数既是输入也是输出,仅用于函数参数
OpenGL ES 3.0一般写成
csharp/// 顶点着色器 #version 300 es in vec3 aPosition; // 输入顶点位置(替代attribute) in vec2 aTexCoord; // 输入纹理坐标 out vec2 vTexCoord; // 输出到片段着色器(替代varying) void main() { gl_Position = vec4(aPosition, 1.0); vTexCoord = aTexCoord; } /// 片元着色器 #version 300 es precision mediump float; in vec2 vTexCoord; // 输入来自顶点着色器 uniform sampler2D uTexture; out vec4 FragColor; // 输出颜色(替代gl_FragColor) void main() { FragColor = texture(uTexture, vTexCoord); }
- 旧版(OpenGL ES 2.0)
1.1.2 内置函数与常量
-
数学函数
函数 描述 示例 典型应用 abs(x)
绝对值 abs(-5.0) → 5.0
距离计算、法线方向处理 floor(x)
/ceil(x)
向下/向上取整 floor(3.7) → 3.0
像素对齐、离散化操作 round(x)
四舍五入 round(2.3) → 2.0
纹理坐标量化 mod(x, y)
取模运算 mod(5.2, 3.0) → 2.2
周期性效果(如平铺纹理) clamp(x, min, max)
将值限制在 [min, max]
范围内clamp(1.5, 0.0, 1.0) → 1.0
避免数值溢出 mix(a, b, t)
线性插值: a*(1-t) + b*t
mix(0.0, 10.0, 0.3) → 3.0
颜色渐变、动画过渡 step(edge, x)
阶跃函数: x >= edge ? 1.0 : 0.0
step(0.5, 0.7) → 1.0
条件分支的无分支替代,(OpenGL避免用if) smoothstep(a, b, x)
平滑过渡的插值(S 形曲线) smoothstep(0.0, 1.0, 0.5) → 0.5
抗锯齿、柔和边缘效果 length(v)
向量长度 length(vec2(3.0, 4.0)) → 5.0
距离计算、归一化 distance(a, b)
两点间距离 distance(vec3(0.0), vec3(1.0)) → 1.732
碰撞检测、光照衰减 dot(a, b)
点积 dot(vec3(1,0,0), vec3(0,1,0)) → 0.0
光照强度、投影计算 cross(a, b)
叉积(仅适用于三维向量) cross(vec3(1,0,0), vec3(0,1,0)) → (0,0,1)
法线计算、旋转轴确定 normalize(v)
向量归一化 normalize(vec3(2,0,0)) → (1,0,0)
方向向量处理 reflect(I, N)
反射向量: I - 2*dot(N,I)*N
reflect(lightDir, normal)
镜面反射、环境映射 refract(I, N, eta)
折射向量 refract(lightDir, normal, 1.5)
透明材质(水、玻璃) sin(x)
/cos(x)
/tan(x)
基本三角函数(输入为弧度) sin(radians(90.0)) → 1.0
波形动画、旋转矩阵计算 asin(x)
/acos(x)
反三角函数 acos(0.5) → 1.047
(≈60°)反射向量计算 pow(x, y)
幂运算: x^y
pow(2.0, 3.0) → 8.0
光照衰减、非线性颜色空间转换 exp(x)
/log(x)
指数函数和自然对数 exp(1.0) → e ≈ 2.718
复杂数学模型(如体积渲染) sqrt(x)
平方根 sqrt(4.0) → 2.0
向量长度归一化 -
内置常量
常量 值 说明 gl_MaxVertexAttribs
≥16 顶点属性最大数量(如位置、法线、纹理坐标等)。 gl_MaxTextureUnits
≥16 支持的纹理单元数量。 gl_MaxVertexOutput
≥16 顶点着色器可输出的向量数量。
1.2 顶点着色器
顶点着色器最重要的是构建裁剪空间的坐标点gl_Position
,绘制点时也可以设置gl_PointSize
。具体可参考OpenGL基础一坐标系,处理坐标系变换:
ini
// vec4类型
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vertexPosition;
在顶点着色器中还有两个只读的变量。
内置变量 | 数据类型 | 作用说明 |
---|---|---|
gl_VertexID |
int |
顶点索引,表示正在处理的是第几个顶点 |
gl_InstanceID |
int |
实例索引,多次绘制时很有用 |
1.3 片元着色器
片元着色器最重要的是构建gl_FragColor
(OpenGL 3以后out vec4 FragColor
替代),可以直接赋值颜色,也可以通过采样纹理获得。
ini
/// 颜色,赋值为红色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
/// 纹理采样,inputImageTexture为纹理单元,textureCoordinate为纹理坐标
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
这里需要说明的是,片元着色器会对每个像素点执行一次,在顶点没有指向的地方是通过插值完成的(无论是直接赋值颜色还是纹理采样) 。例子中分别对3个顶点赋值了不同的颜色,可以看到下列类似iOS CAGradientLayer
图层的效果。
css
float vertices[] = {
// positions // colors
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top
};
- 内置变量
内置变量 | 数据类型 | 作用说明 |
---|---|---|
gl_FragCoord |
vec4 |
窗口坐标 |
gl_FrontFacing |
bool |
判断当前片段是否属于正面 |
gl_PointCoord |
vec2 |
点的纹理坐标,仅当渲染点时有效 |
gl_FragDepth |
float |
手动设置片段深度值(默认使用 gl_FragCoord.z ) |
- discard用法
此外,绘制纹理时,特别是透明通道时,使用discard
会很有用(混合也可以),可参考learnOpenGL。
ini
void main()
{
vec4 texColor = texture(texture1, TexCoords);
// 如果纹理的透明度小于某个阈值,则丢弃该片元
if(texColor.a < 0.1)
discard;
FragColor = texColor;
}
二、着色器示例
经过上面的介绍,可以动手做点着色器了,下面给过一些例子和解释。顶点着色器和片元着色器都是配合使用的,但有不同效果处理重点在不同的着色器上。
2.1 顶点示例
顶点着色器的基本用法就是构建图形,如经典的金字塔图形顶点着色器基本就是模型变换输出。
ini
// 顶点着色
attribute vec4 position;
attribute vec4 color;
varying vec4 fragColor;
uniform mat4 modelViewProjectionMatrix;
void main() {
gl_Position = modelViewProjectionMatrix * position;
fragColor = color;
}
// 片元着色
precision mediump float;
varying vec4 fragColor;
void main() {
gl_FragColor = fragColor;
}\
-
构建顶点
- 构建面顶点
arduinoconst GLfloat pyramidVertices[] = { // 正面 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 顶点 -1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, // 左下 1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, // 右下 // 右面 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, // 后面 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, // 左面 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 0.0f ... };
- 复制到GPU VBO
scssglGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidVertices), pyramidVertices, GL_STATIC_DRAW);
-
纹理赋值
- uniform类型,获取layout,使用
glUniformxxx
进行赋值
ini// 设置MVP矩阵,设置shader中的uniform modelViewProjectionMatrix xxx GLint modelViewProjectionMatrixUniform = glGetUniformLocation(_program, "modelViewProjectionMatrix"); glUniformMatrix4fv(modelViewProjectionMatrixUniform, 1, 0, modelViewProjectionMatrix.m);
- attribute类型,获取layout,打开
glEnableVertexAttribArray
,使用glVertexAttribPointer
赋值
scss// 设置顶点属性,获取attribute的layout GLuint positionAttribute = glGetAttribLocation(_program, "position"); GLuint colorAttribute = glGetAttribLocation(_program, "color"); // 默认属性关闭,这里必须打开 glEnableVertexAttribArray(positionAttribute); glEnableVertexAttribArray(colorAttribute); // attribute赋值 glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, 0); glVertexAttribPointer(colorAttribute, 4, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (void*)(sizeof(GLfloat) * 3));
- uniform类型,获取layout,使用
2.2 片元示例
片元着色器基本用法绘制片段颜色,例如在图片处理中,顶点着色器几乎不处理,片元着色器处理变换。(美颜的形变可以在片元处理,也可以在顶点处理
)。这里举出灰度图和马赛克作为练手。
2.2.1 灰度图
把图片显示效果变成灰度图是滤镜中常用到的效果。
RGB转灰度公式如下:
ini
// BT.709
Gray=0.2125×R+0.7154×G+0.0721×B
所以,可以把灰度图frag shader写成如下,vec4(gray, gray, gray, a)
ini
precision highp float;
varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);
void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
float luminance = dot(textureColor.rgb, W);
gl_FragColor = vec4(vec3(luminance), textureColor.a);
}
2.2.2 马赛克效果
再来看下马赛克效果:
马赛克效果简单理解就是一小块区域取一个颜色(中间或者左下都可以),根据需要分成不同粒度,图中是宽高都等分成50份,所以下列就是一个floor(TextureCoordsVarying.x / unit)
取整操作(去掉小数位)。
ini
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
void main (void) {
vec2 maskXY = TextureCoordsVarying;
float unit = 1.0 / 50.0;
maskXY.x = float(floor(TextureCoordsVarying.x / unit)) * unit;
maskXY.y = float(floor(TextureCoordsVarying.y / unit)) * unit;
vec4 mask = texture2D(Texture, maskXY);
gl_FragColor = vec4(mask.rgb, 1.0);
}