音视频学习笔记十三——渲染与滤镜之着色器基础

题记:本章会开始写一些滤镜了,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语言,除了voidboolintfloat外,也支持一些对向量和矩阵的类型。

  • 特有类型

    • 向量:vec2, vec3, vec4
      • 支持floatintbool类型
      • vec2float类型2维向量;ivec3int类型3维向量;bvec4bool类型4维向量;
    • 矩阵:mat3, mat4
      • 仅支持浮点类型,mat4表示4x4的浮点矩阵
    • 纹理:sampler2D, samplerCube
  • 精度限定符

    • highp(高)、mediump(中)、lowp(低),修饰intfloat类型等
    • lowp的float类型范围是[-2, 2],在数据转换中需要特别注意
    • 顶点着色器 有默认类型 float 默认为 highpint 默认为 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:输入变量,替代旧版 attributevarying
      • 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);
    }

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;
}\
  • 构建顶点

    • 构建面顶点
    arduino 复制代码
    const 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
    scss 复制代码
    glGenBuffers(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));

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);
}
相关推荐
是阿鸽呀1 天前
【音视频开发】7. 使用 FFmpeg7 提取 MP4 中的 H264 视频并封装成 Annex-B 流
音视频开发
程序员_Rya2 天前
RTC、直播、点播技术对比|腾讯云/即构/声网如何 选型 2025 版
音视频开发·直播·技术选型·音视频sdk·音视频对比
AJi3 天前
FFmpeg学习(五):音视频数据转换
ffmpeg·音视频开发·视频编码
音视频牛哥4 天前
Android平台GB28181执法记录仪技术方案与实现
音视频开发·视频编码·直播
音视频牛哥5 天前
Python下的毫秒级延迟RTSP|RTMP播放器技术探究和AI视觉算法对接
音视频开发·视频编码·直播
jaywangep9 天前
纯前端:提取视频某一帧显示在页面上
前端·音视频开发
音视频牛哥9 天前
Android平台GB28181接入模块(SmartGBD)技术接入说明
音视频开发·视频编码·直播
音视频牛哥15 天前
DaniuSDK:Pioneering the Future of Live Streaming with Cutting-edge SDK Solutions
音视频开发·视频编码·直播