Cocos Creator Shader 入门 ⑹ —— 灰阶、反色等滤镜的实现

💡 本系列文章收录于个人专栏 ShaderMyHead,欢迎订阅。

在片元着色器中对像素颜色进行有规律的改动,可以实现各种有趣的滤镜效果。

本文会介绍部分常规着色器滤镜(效果见下图),它们大多是用固定范式来实现的。

一、灰阶滤镜

通过 GLSL 内置的 dot 函数,计算获取像素 RGB 和 vec3(0.299, 0.587, 0.114)点积,来作为着色器的 RGB 输出。

js 复制代码
vec4 grayscale(vec4 color) {
    float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
    return vec4(vec3(gray), color.a);
}

// 使用
color = grayscale(color);

💡 dot 函数语法可查阅《附录 ------ 二、GLSL 内置方法》

💡 输入向量 vec3(0.299, 0.587, 0.114) 中的三个数值,是最常用的 NTSC 标准数值,是基于人眼视网膜的生物学特性确定的(人眼对绿色光最敏感,红色次之,蓝色最不敏感)。

完整的 Effect 文件代码如下(后续其它滤镜不赘述):

js 复制代码
CCEffect %{
common-pass-config: &common-pass-config
  blendState:
    targets:
      - blend: true
        blendSrc: src_alpha
        blendDst: one_minus_src_alpha
  depthStencilState:      
    depthTest: false  
    depthWrite: false
  rasterizerState: 
    cullMode: none  # 兼容 Spine,剔除背景

techniques:
  - name: gray-scale # 灰阶
    passes:
      - vert: vs:vert
        frag: gray-scale-fs:frag
        <<: *common-pass-config
}%

CCProgram vs %{
  #include "../../resources/chunk/normal-vert.chunk"
}%

// 灰阶滤镜
CCProgram gray-scale-fs %{
  precision highp float;
  #include <sprite-texture> 

  in vec2 uv;

  #if USE_LOCAL
    in vec4 v_color;
  #endif

  // 声明灰阶函数
  vec4 grayscale(vec4 color) {
    float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
    return vec4(vec3(gray), color.a);
  }

  vec4 frag() {
    vec4 color = texture(cc_spriteTexture, uv); 

    #if USE_LOCAL
      // 若为 Spine 文件,先混合顶点色值
      color *= v_color;
    #endif

    return grayscale(color);  // 使用灰阶函数
  }
}%

滤镜效果:

如果需要实现一个逐渐置灰的动效,可以自定义一个 grayPercent 属性作为灰阶颜色要应用的百分比:

js 复制代码
  vec4 frag() {
    vec4 color = texture(cc_spriteTexture, uv); 
    
    #if USE_LOCAL
      color *= v_color; 
    #endif

    vec4 grayColor = grayscale(color);
    
    return color + (grayColor - color) * grayPercent;
  }

再通过外部的脚本修改它的值即可(从 0.0 逐渐增加到 1.0)。

二、颜色反转

对像素色值进行「取反」------ 使用 1.0 减去 RGB 各通道的值,来作为着色器的输出:

js 复制代码
vec4 invert(vec4 color) {
    return vec4(1.0 - color.rgb, color.a);
}

// 使用
color = invert(color);

滤镜效果:

三、像素化

在着色器中实现马赛克风格的像素滤镜很简单,原理是压缩材质的纹理坐标,按区间来采样。例如让 UV 坐标 [0.0, 0.0][0.1, 0.1] 的像素都统一取样 [0.0, 0.0] 的纹理色值:

js 复制代码
  float pixelSize = 30.0;  // 把图片被划分成 pixelSize 乘以 pixelSize 个像素块


  vec4 pixelate(vec2 uv) {
      vec2 pixelatedUV = floor(uv * pixelSize) / pixelSize;
      return texture(cc_spriteTexture, pixelatedUV);
  }

  vec4 frag() {
    vec4 color = pixelate(uv); 

    return color;
  }

关键步骤解析:

  • uv * pixelSize

    • 将 UV 坐标映射到更大的坐标空间,划分成 pixelSize × pixelSize 个格子。
    • 例如 pixelSize = 10,就相当于把纹理划分为 10×10 网格,UV 从 [0.0, 1.0] 被扩展成 [0.0, 10.0]
  • floor(uv * pixelSize)

    • 把每个坐标取整,相当于「压缩」到每个像素块左上角的 UV 坐标。
    • 所有在这个格子里的像素都会被映射到同一个整数坐标。
  • 再除以 pixelSize

    • 把 UV 再还原回 [0.0, 1.0] 范围。
    • 所以,这个 pixelatedUV 实际上是「跳格子式的 UV」,同一个像素块里的像素全部采样相同的纹理点。

滤镜效果:

四、边缘检测(描边)

边缘检测的原理是,通过读取像素周围的颜色值,计算图像在水平和垂直方向的亮度变化强度,从而检测出边缘。

首先获取当前像素周围 8 个方向(左上、正上、右上、正左、正右、左下、正下、右下)的像素颜色:

js 复制代码
    // // 计算单个像素在 UV 空间中的尺寸,这样就能通过 uv ± pixelSize 精确采样到周围单个像素的位置
    vec2 pixelSize = 1.0 / textureSize;
    
    // 获取周围像素
    vec4 topLeft = texture(cc_spriteTexture, uv + vec2(-pixelSize.x, pixelSize.y));
    vec4 top = texture(cc_spriteTexture, uv + vec2(0.0, pixelSize.y));
    vec4 topRight = texture(cc_spriteTexture, uv + vec2(pixelSize.x, pixelSize.y));
    vec4 left = texture(cc_spriteTexture, uv + vec2(-pixelSize.x, 0.0));
    vec4 right = texture(cc_spriteTexture, uv + vec2(pixelSize.x, 0.0));
    vec4 bottomLeft = texture(cc_spriteTexture, uv + vec2(-pixelSize.x, -pixelSize.y));
    vec4 bottom = texture(cc_spriteTexture, uv + vec2(0.0, -pixelSize.y));
    vec4 bottomRight = texture(cc_spriteTexture, uv + vec2(pixelSize.x, -pixelSize.y));

接着我们通过Sobel 算子来计算水平(Gx)与垂直(Gy)方向的梯度:

js 复制代码
    // 计算梯度
    vec4 gx = -topLeft - 2.0 * left - bottomLeft + topRight + 2.0 * right + bottomRight;
    vec4 gy = -topLeft - 2.0 * top - topRight + bottomLeft + 2.0 * bottom + bottomRight;

gx 是图像在 水平方向(x轴) 上的亮度变化趋势,gy 则是图像在 垂直方向(y轴) 上的亮度变化趋势:

  • 它们不是图像本身的亮度,而是亮度的 "变化速度""斜率" ,也可以说是 "图像的一阶导数"
  • gx 为正,表示图像从左向右变亮;若 gx 为负,表示图像从左向右变暗;若 gx = 0,表示横向亮度变化不大。同理,gy 表示纵向变化。
  • gx.rgbagy.rgba 各通道(例如 gx.r)的理论取值范围是 [-4.0, 4.0]。不过在实际图像中,亮度不会跳变那么剧烈,因此一般会远小于这个范围,通常落在 [-1.5, +1.5] 区间内。

虽然我们计算出的亮度梯度结果包含 RGBA 四个通道,但常规而言边缘检测仅提取其中 RGB 一个通道来判断边缘即可(简化计算)。

在本例中,我们只提取 .r 通道来判断边缘强度:

js 复制代码
// 使用红色通道计算边缘强度
float edge = length(vec2(gx.r, gy.r));  // 等同于 edge = sqrt(gx² + gy²)

这里通过 GLSL 内置的 length 方法对 gx.rgy.r 进行取模,使用它们的模长来表示红色通道的亮度变化强度。

此时 edge = 0 时表示无边缘,edge 越大表示边缘强。通过对它进行取反即可绘制出描边:

js 复制代码
return vec4(vec3(1.0 - edge), 1.0);

可以看到虽然检测出了边缘,但鉴于原图杂色较多,导致描边效果不够理想。我们可以进一步使用 smoothstep 函数来平滑边缘,减少冗余描边:

js 复制代码
edge = smoothstep(0.1, 0.3, edge);

我们也可以保留原本材质的底色,在原纹理上进行描边:

js 复制代码
    // return vec4(vec3(1.0 - min(edge * 5.0, 1.0)), 1.0);

    // 保留原本颜色
    vec3 orig = texture(cc_spriteTexture, uv).rgb;
    return vec4(orig.rgb * (1.0 - edge), 1.0);

💡 扩展

如果希望使用多通道处理(而不是只处理 R 通道),可以使用亮度计算:

js 复制代码
// 使用亮度计算
float luminance(vec4 c) {
    return 0.299*c.r + 0.587*c.g + 0.114*c.b;
}

float edge = length(vec2(luminance(gx), luminance(gy)));

五、创意彩色滤镜

5.1 电影胶卷滤镜

胶片滤镜具备几大核心要素:胶片颗粒生成、暗角效果、对比度曲线、色彩分级、褪色处理,我们封装为对应的 GLSL 函数来处理。

5.1.1 胶片颗粒生成

在材质上生成随机分布的噪点颗粒:

js 复制代码
  float filmGrain(vec2 uv, float time) {
    // 使用简单噪声算法
    return fract(sin(dot(uv * time, vec2(12.9898, 78.233))) * 43758.5453);
  }

这是一个经典的 伪随机噪声算法,使用 UV 和时间因子作为输入,生成一个 0~1 之间的随机值来模拟颗粒:

  • 通过 dot(uv * time, vec2(...)) 变化种子。
    • dot 是点积,它在这里的作用是:把 2D 坐标哈希成一个数
    • 常数 12.989878.233 是精心挑选的"魔数",可以最大程度避免周期性、重复性。
  • sin 是一个周期函数,但因为 dot 的结果很大或随机,sin 的输入是非常"乱"的,乘上另一个大数 43758.5453,是为了让 sin 的输出值在 [-1,1] 区间拉伸到一个更大的范围,以提高 hash 的离散性。
  • fract(...) 是为了把大而乱的数值压缩到 [0.0, 1.0) 区间,成为最终的随机噪声值

💡 本文使用了 dotfractdistancesmoothstep 等 GLSL 内置函数,读者可自行在《附录 ------ 二、GLSL 内置方法》 中查阅。

5.1.2 暗角效果

离材质中心越远则色值越暗:

js 复制代码
  float vignetteIntensity = 0.8; // 暗角强度

  float vignette(vec2 uv) {
    // 计算到中心的距离
    vec2 center = vec2(0.5, 0.5);
    float dist = distance(uv, center);
    
    // 通过 vignetteIntensity 来控制暗角扩散程度,使用 smoothstep 来平滑边缘暗角
    return 1.0 - smoothstep(0.3, 0.8, dist * vignetteIntensity);
  }

这里使用了 GLSL 内置的 distance 函数来算出当前像素距离材质正中心的距离 (离中心越近,dist 的值越小,最远的距离则约等于 0.707):

此时如果通过 smoothstep(0.3, 0.8, dist) 可以在 UV 坐标 [0.3, 0.8] 区间返回一个平滑插值 (返回值在 [0.0, 1.0] 之间),通过 1.0 减去该返回值则可以取反:

最后加入 vignetteIntensity 来控制暗角扩散程度(缩放 dist 的值):

js 复制代码
1.0 - smoothstep(0.3, 0.8, dist * vignetteIntensity)

5.1.3 对比度曲线

本质上是用一个sigmoid(S型)函数来增强图像中间亮度的对比度,而对黑和白的区域影响较小:

js 复制代码
vec3 filmCurve(vec3 color) {
  vec3 x = color * 1.2;
  vec3 curve = x * (1.0 / (1.0 + exp(-5.0*(x-0.5))));
  return mix(color, curve, 0.7);
}
  • 通过 color * 1.2 将颜色向更亮方向拉伸,增强整个图像的亮度感(相当于增强输入值,使中间区域更容易被强调)。

  • 应用 sigmoid 对比度曲线,第 3 行相当于 x * sigmoid(5.0 * (x - 0.5))

    • GLSL 里没有内置的 sigmoid 函数 ,不过其等效于:sigmoid(t) = 1 / (1 + exp(-t));
    • x 在中间值(如 0.5)附近变化得更快(增强对比度),即低值区域(暗部)和高值区域(亮部)变化缓慢(保留细节):
    lua 复制代码
         ↑
        1|                             
         |                 ________
         |               /
         |            __/
         |          /
        0+------------------------→ x
             0    0.5      1
  • 使用 mix 与原色混合,保留部分原始感。

5.1.4 色彩分级

一般电影都使用的橙青风格来进行调色,其着色器实现为:

js 复制代码
vec3 filmColorGrade(vec3 color) {
  vec3 shadows = vec3(0.1, 0.25, 0.3); // 青
  vec3 midtones = vec3(0.7, 0.5, 0.3); // 橙

  float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));  // 亮度
  vec3 graded = mix(shadows, midtones, smoothstep(0.2, 0.8, luminance));
  return mix(color, graded * color, 0.4);
}
  • 在第 5 行通过 dot 计算像素亮度 luminance

    • 这行代码是标准的人眼感知亮度计算,三个通道的权重来源于 ITU-R BT.709 标准(红 21%,绿 71%,蓝 7%);
    • 作用是将输入颜色转换成单通道的亮度值(范围大致在 [0, 1])。
  • 通过 smootstep 函数,用亮度在阴影(青)和中间调(橙)之间平滑插值。

  • 乘以原色并混合回来,形成经典的橙青风格

5.1.5 投入着色器

我们把封装好的函数整合到片元着色器中:

js 复制代码
CCEffect %{
略...

techniques:   
  - name: lut # 创意色彩滤镜
    passes:
      - vert: vs:vert
        frag: lut-fs:frag
        <<: *common-pass-config
        properties:
          timeFactor: { value: 0.0 }               # 时间因子,用于控制颗粒变化速度
}%


// 创意色彩滤镜(片元着色器代码)
CCProgram lut-fs %{
  precision highp float;
  #include <sprite-texture> 

  in vec2 uv;

  #if USE_LOCAL
    in vec4 v_color;
  #endif

  uniform LutArgs {
    vec2 textureSize;
    float timeFactor;
  };

  /** 电影胶卷质感 **/
  float grainIntensity = 3.25;               // 颗粒感强度
  float vignetteIntensity = 0.8;             // 暗角强度
  vec3 fadeColor = vec3(0.15, 0.15, 0.05);   // 褪色颜色
  
  // 胶片颗粒生成函数
  float filmGrain(vec2 coord, float time) {
    // 使用简单噪声算法
    return fract(sin(dot(coord * time, vec2(12.9898, 78.233))) * 43758.5453);
  }
  
  // 暗角效果
  float vignette(vec2 uv) {
    // 计算到中心的距离
    vec2 center = vec2(0.5, 0.5);
    float dist = distance(uv, center);
    
    // 平滑边缘暗角
    return 1.0 - smoothstep(0.3, 0.8, dist * vignetteIntensity);
  }
  
  // 胶片曲线(对比度调整)
  vec3 filmCurve(vec3 color) {
    // S形曲线增强对比度
    vec3 x = color * 1.2;
    vec3 curve = x * (1.0 / (1.0 + exp(-5.0*(x-0.5))));
    return mix(color, curve, 0.7);
  }

  // 色彩分级(橙青色调)
  vec3 filmColorGrade(vec3 color) {
    // 分离色调:高光偏橙,阴影偏青
    vec3 shadows = vec3(0.1, 0.25, 0.3); // 青色调
    vec3 midtones = vec3(0.7, 0.5, 0.3); // 橙色调
    
    // 计算亮度
    float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
    
    // 混合色调
    vec3 graded = mix(shadows, midtones, smoothstep(0.2, 0.8, luminance));
    
    // 混合原始颜色
    return mix(color, graded * color, 0.4);
  }
  
  // 褪色效果
  vec3 filmFade(vec3 color) {
    return mix(color, fadeColor, 0.15);
  }

  vec4 film(vec4 color, vec2 uv) {
  
    // 应用胶片曲线
    color.rgb = filmCurve(color.rgb);
    
    // 应用色彩分级
    color.rgb = filmColorGrade(color.rgb);
    
    // 添加褪色效果
    color.rgb = filmFade(color.rgb);
    
    // 应用暗角
    color.rgb *= vignette(uv);
    
    // 添加胶片颗粒(带时间变化)
    float grain = filmGrain(uv, timeFactor) * grainIntensity;
    color.rgb += (grain - 0.5) * 0.1;
    
    return color;
  }


  vec4 frag() {
    vec4 color = texture(cc_spriteTexture, uv); 

    #if USE_LOCAL
      color *= v_color;
    #endif

    return film(color, uv);
  }
}%

将材质应用到节点组件后就能得到电影胶卷的风格化效果,还可以通过修改材质的 timeFactor 变量来实现噪点动画:

5.2 更多滤镜

通过其它固定范式对纹理颜色进行修改,可以获得「老照片」、「故障效果」等有趣的滤镜,效果和片元着色器参考代码如下:

js 复制代码
CCEffect %{
略...

techniques:   
  - name: lut # 创意色彩滤镜
    passes:
      - vert: vs:vert
        frag: lut-fs:frag
        <<: *common-pass-config
        properties:
          textureSize: { value: [192, 192] }       # 纹理尺寸
          timeFactor: { value: 0.0 }               # 时间因子,用于控制颗粒变化速度
}%

CCProgram lut-fs %{
  precision highp float;
  #include <sprite-texture> 

  in vec2 uv;

  #if USE_LOCAL
    in vec4 v_color;
  #endif

  uniform LutArgs {
    vec2 textureSize;
    float timeFactor;
  };

  // 老照片滤镜(棕褐色调)
  vec4 sepia(vec4 color) {
    vec3 newColor;
    newColor.r = dot(color.rgb, vec3(0.393, 0.769, 0.189));
    newColor.g = dot(color.rgb, vec3(0.349, 0.686, 0.168));
    newColor.b = dot(color.rgb, vec3(0.272, 0.534, 0.131));
    return vec4(min(newColor, vec3(1.0)), color.a);  // min 是 GLSL 内置的方法,在参数里选取最小的值
  }

  // 赛博朋克滤镜
  vec4 cyberpunk(vec4 color) {
    // 增强对比度
    color.rgb = (color.rgb - 0.5) * 2.0 + 0.5;
    
    // 调整色调
    float r = color.r * 1.2;
    float g = color.g * 0.9;
    float b = color.b * 1.5;
    
    // 添加紫色偏移
    r = min(r + color.b * 0.2, 1.0);
    b = min(b + color.r * 0.3, 1.0);
    
    return vec4(r, g, b, color.a);
  }

  // 双色调
  vec4 duotone(vec4 color) {
    vec3 darkColor = vec3(0.0, 0.0, 0.5);  // 深色调
    vec3 lightColor = vec3(1.0, 0.8, 0.2); // 浅色调

    float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114));
    return vec4(mix(darkColor, lightColor, luminance), color.a);
  }

  // 故障效果
  vec4 glitchEffect(vec4 color, vec2 uv) {
    float time; // 时间变量
    float intensity = 0.02; // 位移强度

    // 红色通道位移
    vec2 redUV = uv + vec2(cos(time * 10.0) * intensity, sin(time * 8.0) * intensity);
    float r = texture(cc_spriteTexture, redUV).r;
    
    // 绿色通道位移
    vec2 greenUV = uv + vec2(sin(time * 12.0) * intensity, cos(time * 9.0) * intensity);
    float g = texture(cc_spriteTexture, greenUV).g;
    
    // 蓝色通道位移
    vec2 blueUV = uv + vec2(cos(time * 7.0) * intensity, sin(time * 11.0) * intensity);
    float b = texture(cc_spriteTexture, blueUV).b;
    
    return vec4(r, g, b, color.a);
  }

  // 电影胶卷质感
  // 略...


  vec4 frag() {
    vec4 color = texture(cc_spriteTexture, uv); 

    #if USE_LOCAL
      color *= v_color;
    #endif

    // 通过自定义宏选择要使用的 LUT 滤镜
    #if USE_SEPIA
      color = sepia(color);
    #elif USE_CYBERPUNK
      color = cyberpunk(color);
    #elif USE_DUOTONE
      color = duotone(color);
    #elif USE_GLITCH
      color = glitchEffect(color, uv);
    #elif USE_FILM
      color = film(color, uv);
    #endif

    return color;
  }
}%
相关推荐
VaJoy2 天前
Cocos Creator Shader 入门 ⑸ —— 代码复用与绿幕抠图技术
cocos creator
VaJoy4 天前
Cocos Creator Shader 入门 ⑷ —— 纹理采样与受击闪白的实现
cocos creator
VaJoy5 天前
Cocos Creator Shader 入门 ⑶ —— 给节点设置透明度
cocos creator
VaJoy7 天前
Cocos Creator Shader 入门 (2) —— 给节点染色
cocos creator
VaJoy8 天前
Cocos Creator Shader —— 附录
cocos creator
成长ing121389 天前
多层背景视差滚动Parallax Scrolling
cocos creator
似水流年wxk1 个月前
cocos creator使用jenkins打包微信小游戏,自动上传资源到cdn,windows版运行jenkins
运维·jenkins·cocos creator
成长ing121382 个月前
点击音效系统
前端·cocos creator
blakeyi2 个月前
vscode保存自动刷新cocos creator编辑器
ide·vscode·cocos creator·热更新