前言
图像和视频渲染离不开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
也是片段着色器的内置函数,主要用来设置片元像素的颜色。
漩涡效果
片段着色器代码如下
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;
}
灵魂出窍效果
示例源地址
效果图
由图中可以发现此效果有多个图层,最下面的图层不动,上面的图层随着时间的变化变大,并且透明度变低直至透明。此效果是由多个图层构成,那么就需要颜色混合,所以此效果需要在片元着色器中实现,顶点着色器不变。
片段着色器代码如下
// 声明为高精度
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;
}
抖动效果
抖动效果是抖音的经典图标和效果,其效果如下(示例代码源同灵魂出窍)。
过程:图层变大,并且颜色发生了偏移,然后所以的再变回原来的效果。着色器代码如下:
// 声明片段着色器中为高精度浮点型
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);
}
反相(也就是反色效果)
// 声明片段着色器中为高精度浮点型
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)
}
高斯模糊
效果如下:
参考链接
模糊过滤的基本原理是对附近像素进行加权和来混合当前像素颜色。通常使用的权重随距离减小(二维屏幕空间距离),距离当前像素较远的像素贡献较小。
顶点着色器
// 声明一个统一变量的 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);
}