特效中的着色器代码详解

前言

图像和视频渲染离不开OpenGLES,在可编程的OpenGLES渲染管线中,顶点着色器和片段着色器使得OpenGLES更加灵活。

通过以下几种常见的特效,来认识和了解如何编写一个片段着色器。

分屏效果

片段着色器代码来源于这篇文章。代码如下:

// 分六屏特效
precision highp float;
uniform sampler2D inputTexture;
varying highp vec2 textureCoordinate;

void main() {
    highp vec2 uv = textureCoordinate;
    // 左右分三屏
    if (uv.x <= 1.0 / 3.0) {
        uv.x = uv.x + 1.0 / 3.0;
    } else if (uv.x >= 2.0 / 3.0) {
        uv.x = uv.x - 1.0 / 3.0;
    }
    // 上下分两屏,保留 0.25 ~ 0.75部分
    if (uv.y <= 0.5) {
        uv.y = uv.y + 0.25;
    } else {
        uv.y = uv.y - 0.25;
    }
    gl_FragColor = texture2D(inputTexture, uv);
}

这是一个典型的片段着色器代码。
第一行代码用精度修饰符声明了精度类型。在OpenGLES中,有三种精度类型,高(highp)、中(mediump)、低(lowp),默认是高精度也就是highp的。
第二行代码声明了一个采样器,用于访问着色器中的纹理图像。
第三行代码声明了一个高精度的2D纹理坐标变量textureCoordinate
其中precision 代表精度修饰符;uniform是变量类型限定符,代表统一变量,统一变量存储应用程序通过OpenGLESAPI传入着色器的只读值,对于保存着色器所需的所有数据类型(如变换矩阵、照明参数和颜色)都很有用。统一变量的命名空间在顶点和片段着色器中是共享的,也就是说,如果顶点和片段着色器一起连接到一个程序对象,他们就会共享同一组统一变量;varying变量是顶点着色器中传递给片段着色器的变量值,它修饰了一个vec2类型的变量,是一个(x, y)标识的点。

main函数是程序对象开始调用片段着色器的入口,在该函数中,声明一个高精度的临时坐标变量uv,用于接收顶点着色器传入的纹理点的坐标。
接下来是具体分屏的逻辑代码,根据需要填充的原图像的区域,来修改当前的填充区域。
根据当前纹理坐标点的x坐标,确定该点是否在整个纹理的三分之一以内以及是否超过了纹理坐标的三分之二。它分别代表了将纹理的x坐标[0, 1]的纹理区间分为了3个部分。纹理读取的结果和从顶点着色器传递的输入值textureCoordinate用来确定的填充纹理的区域uv,生成新的纹理。这个裁剪坐标是可以自定义取原图像的部分区域。

上面是一个需要裁剪的示例,下面是一个不需要裁剪的示例
// 四分屏
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
    if(uv.x <= 0.5){
        uv.x = uv.x * 2.0;
    }else{
        uv.x = (uv.x - 0.5) * 2.0;
    }
    
    if (uv.y<= 0.5) {
        uv.y = uv.y * 2.0;
    }else{
        uv.y = (uv.y - 0.5) * 2.0;
    }
    
    gl_FragColor = texture2D(Texture, uv);
}

如果原图像是正方形,那么,4分屏既2x2在不需要对原图像进行裁剪的情况下,将填充范围修改为当前值得二倍。

图像灰度

这是图像灰度的片段着色器代码

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);

void main(){
    // 获取对应纹理坐标系下色颜色值
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    // 将颜色mask与变换因子相乘得到灰度值
    float luminance = dot(mask.rgb, W);
    // 将灰度值转换为(luminance,luminance,luminance,mask.a)并填充到像素中
    gl_FragColor = vec4(vec3(luminance), 1.0);
}

在不同平台有多重方法可以实现灰度效果,如GPUImage库,以及iOS的CoreImage。但在着色器上可以选择权值法、平均值法、取绿色值法中,权值方法是公认的效果最好的,如上,参考这里

浮点算法:Gray = R*0.3 + G*0.59 + B*0.11 (RGB的权重总和为1)
整数方法:Gray = (R*30 + G*59 + B*11)/100(RGB的权重总和为100)
移位方法:Gray = (R*76 + G*151 + B*28)>>8

第一行代码声明了高精度的浮点类型。
第二行代码声明了一个采样器,用于读取纹理。
第三行代码是顶点着色器中传入的纹理的坐标变量。
第四行代码是一个高精度的向量的常量值,该值和目标点的RGBA相乘会得到一个灰度值。
main函数中,第一行代码使用texture2D函数从采样器中读取给定的坐标点的像素的颜色值RGBA(是vec4类型);第二行代码是将读取到的纹理的颜色值与上述常量进行点乘,调用dot函数即可。
最后一行代码就是将计算好的新的像素值返回给需要填充的像素,gl_FragColor也是片段着色器的内置函数,主要用来设置片元像素的颜色。

漩涡效果

image.png

片段着色器代码如下

precision mediump float;
// 计算圆周需要用到的π
const float PI = 3.14159265;
// 采样器
uniform sampler2D image;
// 旋转的角度,Radius是旋转的半径
const float uD = 80.0; 
// 设置为0.5,其实就是为了取旋涡半径用到的
const float uR = 0.5;
// 顶点着色器传入的纹理坐标
varying vec2 vTexcoord;
// 主函数
void main() {
    // 声明一个整形二维向量,x = 512, y = 512,其实就是一个512宽高的正方形
    ivec2 ires = ivec2(512, 512);
    // 取出当前正方形的边长也就是被旋转区域的圆的直径
    float res = float(ires.s);
    // 当前纹理坐标
    vec2 st = vTexcoord;
    // 旋转半径由正方形的边长和Ur相乘得到
    float radius = Res * uR;
    // 通过直径获取纹理坐标对应的物体坐标,st是当前的纹理坐标。
    vec2 xy = Res * st;
    // 取出纹理坐标减去半径之后的具体物体坐标,向量相减
    vec2 dxy = xy - vec2(res/2., res/2.);  
    // 当前半径
    float r = length(dxy);
    // atan函数获取当前纹理坐标的正切值,与需要旋转的值相加得到新的旋转角度
    float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * (-(r/radius)*(r/radius) + 1.0);//(1.0 - r/radius);
    vec2 xy1 = xy;
    // 当前半径小于旋转半径时,将该点坐标旋转成新的目标角度
    if(r <= radius)  {
        xy1 = res/2.0 + r*vec2(cos(beta), sin(beta));
    }
    // 计算新的纹理坐标
    st = xy1/res;
    // 通过采样器设置新的纹理坐标,并返回给内置函数glFragColor,设置新的片元。
    vec3 irgb = texture2D(image, st).rgb;
    gl_FragColor = vec4( irgb, 1.0 );
}

前几行代码和之前唯一不同的是多了一个常量π的声明,实现图像的漩涡效果的原理是在某个半径范围里,把当前采样点旋转一定角度,旋转以后当前点的颜色就被旋转后的点的颜色代替,因此整个半径范围里会有旋转的效果。如果旋转的时候旋转角度随着当前点离半径的距离递减,整个图像就会出现漩涡效果。这里使用的了抛物线递减因子:(1.0-(r/Radius)*(r/Radius) ),参考这里这里
main函数中,不同变量的意义如下

PI:我们的计算中的π,取值3.14159265
uR:设置为0.5,其实就是为了取旋涡半径用到的
ivec2:整形的二维向量,这里的ires其实就是一个512宽高的正方形
Res:取出当前正方形的边长
uD:旋转的角度,Radius是我们旋转的半径
xy:通过直径获得纹理坐标对应的物体坐标
dxy:取出纹理坐标减去半径之后的具体物体坐标
r:当前半径

atan(dxy.y, dxy.x):获取的当前的夹角,如果不设置其他的值,那么当前图片没有任何旋转效果。
radians(uD) * 2.0:在原来夹角的基础上加上我们设置的旋转角度80x2 = 160
(-(r/Radius)(r/Radius) + 1.0):抛物线衰减因子,通过距离圆心的距离计算我们旋转衰减的增益值

缩放效果

缩放效果图及源码在这里
缩放效果也是常见的视频特效的效果,图片有一个放大的过程,然后再回弹。它可以通过修改顶点坐标和纹理坐标的对应关系来实现。
修改顶点坐标和纹理坐标,既可以在顶点着色器实现,也可以在片元着色器上实现,下面是一个顶点着色器的示例:

// 声明顶点坐标属性
attribute vec4 Position; 
// 声明纹理坐标属性(attribute修饰符只在顶点着色器中使用)
attribute vec2 TextureCoords; 
// 声明纹理坐标将修改后的纹理坐标传递给片段着色器
varying vec2 TextureCoordsVarying; 
// 统一变量时间戳
uniform float Time;
// PI 
const float PI = 3.1415926; 
// 顶点着色器调用入口
void main (void) {
    // ⼀次缩放效果时⻓ = 0.6ms 
    float duration = 0.6; 
    // 最⼤缩放幅度 
    float maxAmplitude = 0.3; 
    // 表示传⼊的时间周期.即time的范围被控制在[0.0~0.6];  mod(a,b),求模运算. 即a%b
    float time = mod(Time, duration); 
    // amplitude 表示振幅,引⼊ PI 的⽬的是为了使⽤ sin 函数,将 amplitude 的范围控制在 1.0 ~ 1.3 之间,并随着时间变化 
    float amplitude = 1.0 + maxAmplitude * abs(sin(time * (PI / duration))); 
    // 将顶点坐标的 x 和 y 分别乘上⼀个放⼤系数,在纹理坐标不变的情况下,就达到了拉伸的 效果。x,y 放⼤; z和w保存不变 
    gl_Position = vec4(Position.x * amplitude, Position.y * amplitude, Position.zw);
    // 纹理坐标传递给TextureCoordsVarying,该坐标可以在片段着色器中进行使用
    TextureCoordsVarying = TextureCoords;
}

灵魂出窍效果

示例源地址
效果图

20200816130455528.gif

由图中可以发现此效果有多个图层,最下面的图层不动,上面的图层随着时间的变化变大,并且透明度变低直至透明。此效果是由多个图层构成,那么就需要颜色混合,所以此效果需要在片元着色器中实现,顶点着色器不变。
片段着色器代码如下

// 声明为高精度
recision highp float;
// 声明全局变量 采样器
uniform sampler2D Texture;
// 顶点着色器传入的纹理坐标变量
varying vec2 TextureCoordsVarying;
// 统一变量时间
uniform float Time;

void main (void) {
    // 动画效果时长
    float duration = 0.7;
    // 最大透明度
    float maxAlpha = 0.4;
    // 放大最大的倍数
    float maxScale = 1.8;
    
    // 当前时间的进度 0-1,mod函数求模,当前时间%动画时长
    float progress = mod(Time, duration) / duration; 
    // 当前透明度的进度 0.4 - 0
    float alpha = maxAlpha * (1.0 - progress);
    // 放大倍数的进度 1 - 1.8
    float scale = 1.0 + (maxScale - 1.0) * progress;
    
    // 放大后的x值 0.5是中心点,中心点是不变的
    float weakX = 0.5 + (TextureCoordsVarying.x - 0.5) / scale;
    // 放大后的x值 0.5是中心点,中心点不变
    float weakY = 0.5 + (TextureCoordsVarying.y - 0.5) / scale;
    // 放大后的纹理坐标
    vec2 weakTextureCoords = vec2(weakX, weakY);
    // 放大后的纹素图层
    vec4 weakMask = texture2D(Texture, weakTextureCoords);
    // 正常的纹素图层
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    // 通过矩阵和透明度的乘积再相加实现颜色混合模式
    gl_FragColor = mask * (1.0 - alpha) + weakMask * alpha;
}
颜色混合模式

在上述灵魂出窍示例中,用到了颜色混合模式。什么是混合模式?这里是一篇不错的参考。混合模式是图像处理技术中的一个技术名词,主要功效是可以用不同的方法将对象颜色与底层对象的颜色混合。将一种混合模式应用于某一对象时,在此对象的图层或组下方的任何对象上都可看到混合模式的效果。通过上面索引的文章,我们已经能够了解到,颜色混合其实是对RGBA矩阵执行加、减、乘,以及其他mix操作。

正片叠底是一种常见的混合方法,它的片段着色器代码如下:

// 纹理坐标
varying vec2 V_Texcoord;
 // 声明统一变量基础纹理
uniform sampler2D U_BaseTexture;
// 声明统一变量混合纹理
uniform sampler2D U_BlendTexture;

void main() {
    // 从混合纹理采样器中根据当前纹理坐标获取RGBA
    vec4 blendColor=texture2D(U_BlendTexture,V_Texcoord);
    // 从混合纹理采样器中根据当前纹理坐标获取RGBA
    vec4 baseColor=texture2D(U_BaseTexture,V_Texcoord);
    // 重新赋予新的正片叠底后的纹理RGBA,正片叠底是颜色矩阵的相乘。
    gl_FragColor=blendColor*baseColor;
}

抖动效果

抖动效果是抖音的经典图标和效果,其效果如下(示例代码源同灵魂出窍)。


20200816130843536.gif

过程:图层变大,并且颜色发生了偏移,然后所以的再变回原来的效果。着色器代码如下:

// 声明片段着色器中为高精度浮点型
precision highp float;
// 声明统一变量纹理采样器
uniform sampler2D Texture;
// 顶点着色器传入的纹理坐标
varying vec2 TextureCoordsVarying;
// 统一变量时间
uniform float Time;

void main (void) {
    // 抖动时长
    float duration = 0.7;
    // 放大上限
    float maxScale = 1.1;
    // 颜色偏移步长
    float offset = 0.02;
    // 当前时间的进度 0-1, mod是求模函数,即当前时间%抖动时长
    float progress = mod(Time, duration) / duration; // 0~1
    // 颜色偏移的进度
    vec2 offsetCoords = vec2(offset, offset) * progress;
    // 缩放的进度
    float scale = 1.0 + (maxScale - 1.0) * progress;
    
    // 放大后的纹理坐标,中心点的纹理坐标+当前坐标减去中心点的坐标的差除以缩放进度,得到放大后的纹理坐标
    vec2 ScaleTextureCoords = vec2(0.5, 0.5) + (TextureCoordsVarying - vec2(0.5, 0.5)) / scale;
    // R偏移的纹素,涉及到纹素的变化都需要从采样器中读取
    vec4 maskR = texture2D(Texture, ScaleTextureCoords + offsetCoords);
     // B偏移的纹素
    vec4 maskB = texture2D(Texture, ScaleTextureCoords - offsetCoords);
     // 放大后的纹素
    vec4 mask = texture2D(Texture, ScaleTextureCoords);
    
    gl_FragColor = vec4(maskR.r, mask.g, maskB.b, mask.a);
}

反相(也就是反色效果)

image.png
// 声明片段着色器中为高精度浮点型
precision highp float;
// 声明统一变量纹理采样器
uniform sampler2D Texture;
// 顶点着色器传入的纹理坐标
varying vec2 TextureCoordsVarying;

void main (void) {
    // 根据纹理坐标点获取当前纹理的纹素
    vec4 textureColor = texture2D(Texture,TextureCoordsVarying);
    // 将当前纹素的RGB值取反,即可得到图像的反相。
    gl_FragColor = vec4(1.0 - textureColor.r,1.0 -textureColor.g,1.0 -textureColor.b,1)
}

高斯模糊

效果如下:

image.png

参考链接
模糊过滤的基本原理是对附近像素进行加权和来混合当前像素颜色。通常使用的权重随距离减小(二维屏幕空间距离),距离当前像素较远的像素贡献较小。

顶点着色器

// 声明一个统一变量的 4x4矩阵
uniform mat4 uMVPMatrix;
// 纹理坐标 给
attribute vec4 aPosition;
// 纹理坐标
attribute vec4 aTextureCoord;

// 高斯算子大小(3 x 3)
const int GAUSSIAN_SAMPLES = 9;
// 统一变量 横向偏移
uniform float texelWidthOffset;
// 统一变量 纵向偏移
uniform float texelHeightOffset;
// 计算后传给片段着色器的纹理坐标
varying vec2 textureCoordinate;
// 传给片段着色器的模糊坐标向量集
varying vec2 blurCoordinates[GAUSSIAN_SAMPLES];

void main() {
    // 通过矩阵变换 获取新的位置
    gl_Position = uMVPMatrix * aPosition;
    // 纹理坐标(x, y)
    textureCoordinate = aTextureCoord.xy;
    // 用于计算模糊步长
    int multiplier = 0;
    // 模糊步长
    vec2 blurStep;
    // 单个纹理的xy步长偏移量
    vec2 singleStepOffset = vec2(texelHeightOffset, texelWidthOffset);
    // 计算3x3矩阵中的模糊步长与当前纹理坐标的和,并保存到坐标集合中。
    for (int i = 0; i < GAUSSIAN_SAMPLES; i++) {
        multiplier = (i - ((GAUSSIAN_SAMPLES - 1) / 2));
        blurStep = float(multiplier) * singleStepOffset;
        blurCoordinates[i] = aTextureCoord.xy + blurStep;
    }
}

片段着色器

// 声明中等精度
precision mediump float;
// 
varying highp vec2 textureCoordinate;
uniform sampler2D inputTexture;
const lowp int GAUSSIAN_SAMPLES = 9;
varying highp vec2 blurCoordinates[GAUSSIAN_SAMPLES];

void main()
{
    lowp vec3 sum = vec3(0.0);
   lowp vec4 fragColor=texture2D(inputTexture,textureCoordinate);

    sum += texture2D(inputTexture, blurCoordinates[0]).rgb * 0.05;
    sum += texture2D(inputTexture, blurCoordinates[1]).rgb * 0.09;
    sum += texture2D(inputTexture, blurCoordinates[2]).rgb * 0.12;
    sum += texture2D(inputTexture, blurCoordinates[3]).rgb * 0.15;
    sum += texture2D(inputTexture, blurCoordinates[4]).rgb * 0.18;
    sum += texture2D(inputTexture, blurCoordinates[5]).rgb * 0.15;
    sum += texture2D(inputTexture, blurCoordinates[6]).rgb * 0.12;
    sum += texture2D(inputTexture, blurCoordinates[7]).rgb * 0.09;
    sum += texture2D(inputTexture, blurCoordinates[8]).rgb * 0.05;

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

推荐阅读更多精彩内容