Cocos Creator Shader 入门 ⑺ —— 图层混合样式的实现与 Render Texture

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

在 Photoshop 中,我们可以对顶层的图层选用指定的「图层混合模式」,来让它与底层图像实现不同的叠加效果:

在前端 CSS 中存在可实现同样叠加效果的 mix-blend-mode 属性,可以实现如下十多种图层叠加样式:

此类图层叠加样式在平面设计、页面或游戏设计领域都是常会用到的视觉效果,但在 Cocos Creator 中并没有内置这类功能,需要开发自行创建对应的着色器,本文将介绍这块的实现。

一、multiply 和 screen 的实现

multiply(正片叠底)screen(滤色) 的实现相对简单,我们指定 CCEffect 中的 blendSrcblendDst 的配置,交由 GPU 在硬件混合阶段按规则去混合前景色与后景色即可:

yaml 复制代码
CCEffect %{
common-pass-config: &common-pass-config
  depthStencilState:      
    depthTest: false  
    depthWrite: false

techniques:
  - name: multiply  # 正片叠底
    passes:
      - vert: vs:vert
        frag: fs:frag
        <<: *common-pass-config 
        blendState:
          targets:
          - blend: true
            blendSrc: dst_color   # 关键:使用目标颜色作为源因子
            blendDst: zero        # 关键:不使用目标混合因子

  - name: screen    # 滤色混合
    passes:
      - vert: vs:vert
        frag: fs:frag
        <<: *common-pass-config 
        blendState:
          targets:
          - blend: true
            blendSrc: one                  # 关键:完全使用源颜色
            blendDst: one_minus_src_color  # 关键:使用(1-源颜色)作为目标因子
}%

着色器片段:

js 复制代码
CCProgram vs %{
  #include "../../resources/chunk/normal-vert.chunk"
}%

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

  in vec2 uv;

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

      return color; 
  }
}%

其中 multiply 使用目标颜色作为源因子且不使用目标混合因子,其输出颜色的混合计算为:

js 复制代码
color.rgb * dst_color + dst_color.rgb * 0 = color.rgb * dst_color

即等于源颜色与目标颜色相乘 ("multiply" 本身也是 "相乘" 的意思),这意味着:

  • 无论是源颜色还是目标颜色,只要一方存在黑色的像素,混合结果会是黑色(黑色的 RGB 为 (0.0, 0.0, 0.0), 任何色值乘以它结果都是 (0.0, 0.0, 0.0));
  • 无论是源颜色还是目标颜色,其中一方若存在的白色像素,混合后会变成对方的色值(白色的 RGB 为 (1.0, 1.0, 1.0), 相乘后等于对方的色值)。

screen 则使用 100% 的源颜色,使用(1-源颜色)作为目标因子,其输出颜色的混合计算为:

js 复制代码
color.rgb + dst_color.rgb * (1 - color.rgb)

即在源颜色的基础上,加上了目标颜色与"源颜色反转色值"的混合色,这意味着:

  • 混合后的像素,通常会比源颜色更亮(除非源颜色是白色,再怎么混合也是白色);
  • 源颜色如果是黑色,会直接变成目标颜色,因为混合计算等于 (0.0, 0.0, 0.0) + dst_color.rgb * 1

multiplyscreen 的应用效果如下:

二、Render Texture 的应用

blendSrcblendDst 是 GPU 硬件混合阶段的参数,虽然它们的实现非常高效,但只支持非常有限的线性(一次函数)混合计算:

css 复制代码
outColor = A * srcColor + B * dstColor

multiplyscreen 之外的图层叠加式(例如 overlaysoft-lightcolor-dodgehue 等)属于复杂的非线性混合逻辑,则无法简单的使用 blendSrcblendDst 来实现了。

这意味我们需要自行想办法,在 Cocos Creator 着色器中获取背景色(目标颜色),来按指定范式跟前景色进行计算。

我们可以通过官方提供的渲染纹理资源(Render Texture),把摄像头拍摄到的画面作为材质纹理,并在着色器中使用该材质(来作为目标颜色)。

2.1 创建和绑定 Render Texture

在资源管理器中可以直接创建 Render Texture 文件(.rt 格式):

该资源将存储指定摄像机捕获的帧缓冲数据作为纹理使用。点击该文件并在属性检查器中设置其尺寸,鉴于我们需要获取的是整个画布大小的底图内容,故材质宽高与画布尺寸保持一致(本案例中的画布尺寸为 1280 * 720):

接着再额外创建一个材质摄像头用于来捕获背景:

留意 Otho Height 要设置为画布的高度的一半(让摄像机的位置刚好能捕获画布尺寸的画面),Visibility 选择背景节点对应的层级(上图项目中,背景节点层级归属于 DEFAULT,前景节点层级归属于 UI_2D)。

我们再把前面创建的 Render Texture 文件拖动到该摄像机的 Target Texture 配置框:

便完成了摄像机与 Render Texture 文件的绑定 ------ 后续该摄像机照射的内容会通过离屏framebuffer 绘制到 Render Texture 文件对应的纹理上。

💡 设置了 Target Texture 的摄像机,所捕获的画面会离屏,相当于该摄像机节点被隐藏了。

2.2 创建着色器和材质

创建 .effect 文件并初始化着色器模板:

js 复制代码
CCEffect %{
common-pass-config: &common-pass-config
  depthStencilState:      
    depthTest: false  
    depthWrite: false
  properties: 
    bgTexture: { value: grey }   # 对应 RenderTexture

techniques:
  - name: mix-blend-mode
    passes:
      - vert: vs:vert
        frag: fs:frag
        <<: *common-pass-config
}%

CCProgram vs %{
    precision highp float;
    #include <cc-global>
    #include <common/common-define>  // 引入 CC_HANDLE_RT_SAMPLE_FLIP 内置函数
    in vec3 a_position;
    in vec2 a_texCoord;   
    out vec2 uv;

    vec4 vert() {
    vec4 pos = vec4(a_position, 1);

    pos = cc_matViewProj * pos;
    uv = a_texCoord;
    
    // 解决不同平台和渲染管线中 RenderTexture 的坐标系差异,可处理 RenderTexture 采样时的 UV 坐标翻转问题
    uv = CC_HANDLE_RT_SAMPLE_FLIP(uv);
    
    return  pos;
  }
}%

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

  in vec2 uv;

  uniform sampler2D bgTexture;  // 材质变量无需走 UBO 形式定义

  vec4 frag() {
      // 前景色(暂时没用到)
      vec4 color = texture(cc_spriteTexture, uv);

      // 背景色(目标色)
      vec4 baseColor = texture(bgTexture, uv);

      return baseColor;  // 先不混合,直接返回背景色看下效果
  }
}%

其中第 7 行我们定义了一个材质类型的 bgTexture 变量,它会在材质的属性检查器界面被赋予前面创建的 Render Texture 文件(见后文),故我们可以在片元着色器中引入该变量作为目标材质来使用:

js 复制代码
      uniform sampler2D bgTexture;  // 材质变量无需走 UBO 形式定义
      
      ...

      // 背景色(目标色)
      vec4 baseColor = texture(bgTexture, uv);

需要留意的是,当我们在着色器中使用 Render Texture 时,必须在顶点着色器里引入 Cocos Creator 内置的 CC_HANDLE_RT_SAMPLE_FLIP 函数对 UV 坐标进行跨平台的兼容性处理(第 20 和第 32 行),否则可能出现目标纹理翻转的问题。


回到 Cocos Creator 编辑器,创建材质文件后,拖动 Render Texture 文件到材质属性检查器的 bgTexture 变量配置框进行绑定:

此时应用了该材质的 Sprite 节点效果如下:

可以看到该节点成功绘制出了 Render Texture 材质,但是尺寸被压缩了(从画布大小的尺寸,压缩到该节点尺寸)。

2.3 计算背景像素 UV

上述背景材质(Render Texture)尺寸被压缩的问题,是因为我们直接拿前景色的 UV 来作为背景材质的 UV 了,然而二者的材质尺寸是不相同的,UV 等比例映射后导致纹理采样范围错误。

例如前景中心的像素,在前景节点(下图绿框区域)中的 UV 坐标为 (0.5, 0.5)

但该像素在背景材质中的 UV 位置应是 (BG_X / 1280, BG_Y / 720)

我们需要通过计算来获得 BG_XBG_Y

js 复制代码
      /** CCEffect 处新增前景色节点的 X 和 Y 偏移变量 **/
      ...
      properties: 
        bgTexture: { value: grey } 
        offsetX: { value: 0.0 }      # 前景节点的 X 偏移
        offsetY: { value: 0.0 }      # 前景节点的 Y 偏移


      /** 顶点着色器 **/
      // 计算背景色的 UV 坐标
      float bgX = (uv.x * 192.0 + offsetX) / 1280.0;  // 192 是前景节点的宽和高
      float bgY = (uv.y * 192.0 + offsetY) / 720.0;
      bgUv = vec2(bgX, bgY);

      bgUv = CC_HANDLE_RT_SAMPLE_FLIP(bgUv);
      
      ...
      
      /** 片元着色器 **/
      ...

      in vec2 uv;
      in vec2 bgUv;

      uniform sampler2D bgTexture;

      vec4 frag() {
          // 前景色(暂时没用到)
          vec4 color = texture(cc_spriteTexture, uv);
          // 背景色(目标色)
          vec4 baseColor = texture(bgTexture, bgUv);

          return baseColor; 
      }

💡 留意需要在顶点着色器的 CC_HANDLE_RT_SAMPLE_FLIP 函数执行前去计算,否则 UV 可能因为被翻转了而导致计算不准确。

其中 192 是前景节点的宽和高(这里为了方便直接写死,如果你的节点尺寸是变化的,可以通过参数传入),offsetXoffsetY 是前景色节点的偏移:

我们可以额外创建一个组件脚本,通过世界坐标获取这两个偏移值,然后赋值到材质中的 offsetXoffsetY

js 复制代码
// BlendComp.ts
import { _decorator, Component, Sprite } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('BlendComp')
export class BlendComp extends Component {
    start() {
        const halfSize = 192 / 2;
        const material = this.node.getComponent(Sprite).material;
        const box = this.node.worldPosition;
        material.setProperty('offsetX', box.x - halfSize);
        material.setProperty('offsetY', 720 - box.y - halfSize);
    }
}

此时前景节点所截取的 RenderTexture 纹理,已和背景节点严丝合缝融合在一起了:

三、实现其它图层混合样式

3.1 darken 和 lighten 的实现

darkenlighten 分别对应 Photoshop 里的「变暗」和「变亮」图层混合效果。

「变亮」表示只保留明亮的部分,「变暗」反之,因此它们的着色器函数非常简单:

js 复制代码
  vec3 blendDarken(vec3 base, vec3 blend) {
    return min(base, blend);
  }

  vec3 blendLighten(vec3 base, vec3 blend) {
    return max(base, blend);
  }

其中 base 表示背景色,blend 表示前景色,我们在片元着色器中使用这两个函数:

js 复制代码
  vec4 frag() {
    // 前景色
    vec4 blendColor = texture(cc_spriteTexture, uv);
    // 背景色(目标色)
    vec4 baseColor = texture(bgTexture, bgUv);

    // 混合后的颜色
    vec3 color = vec3(1.0);

      // 通过自定义宏选择要使用的图层混合样式.
    #if USE_DARKEN
      color = blendDarken(baseColor.rgb, blendColor.rgb);
    #elif USE_LIGHTEN
      color = blendLighten(baseColor.rgb, blendColor.rgb);
    #endif

    return vec4(color, blendColor.a);
  }

对应效果如下:

3.2 其它图层混合效果

根据其它混合样式所需计算的固定范式,我们进一步完善着色器:

js 复制代码
/** 其它图层混合样式函数 **/

  const float EPSILON = 0.00001;  // EPSILON 是为了防止 `1.0 - blend` 为 `0.0`,导致紧接着的除零错误(`NaN` 或 `Infinity`)。

  // 获取亮度值
  float luminance(vec3 c) {
    return dot(c, vec3(0.2126, 0.7152, 0.0722));
  }

  // 设置指定颜色的亮度值
  vec3 setLum(vec3 c, float l) {
    float d = l - luminance(c);
    return c + vec3(d);
  }

  // 饱和度调整
  vec3 setSat(vec3 c, float s) {
    float l = luminance(c);
    vec3 grey = vec3(l);
    return mix(grey, c, s);
  }

  // 计算颜色的近似饱和度(saturation)
  float sat(vec3 c) {
    return max(max(c.r, c.g), c.b) - min(min(c.r, c.g), c.b);
  }

  vec3 blendOverlay(vec3 base, vec3 blend) {
    return mix(2.0 * base * blend, 1.0 - 2.0 * (1.0 - base) * (1.0 - blend), step(0.5, base));
  }

  vec3 blendColorDodge(vec3 base, vec3 blend) {
    return base / max(vec3(EPSILON), 1.0 - blend);
  }

  vec3 blendColorBurn(vec3 base, vec3 blend) {
    return 1.0 - (1.0 - base) / max(vec3(EPSILON), blend);
  }

  vec3 blendHardLight(vec3 base, vec3 blend) {
    return blendOverlay(blend, base);
  }

  vec3 blendSoftLight(vec3 base, vec3 blend) {
    return mix(
      sqrt(base) * blend * 2.0,
      1.0 - 2.0 * (1.0 - base) * (1.0 - blend),
      blend
    );
  }

  vec3 blendDifference(vec3 base, vec3 blend) {
    return abs(base - blend);
  }

  vec3 blendExclusion(vec3 base, vec3 blend) {
    return base + blend - 2.0 * base * blend;
  }

  vec3 blendHue(vec3 base, vec3 blend) {
    float s = sat(base);
    float l = luminance(base);
    return setLum(setSat(blend, s), l);
  }

  vec3 blendSaturation(vec3 base, vec3 blend) {
    float s = sat(blend);
    float l = luminance(base);
    return setLum(setSat(base, s), l);
  }

  vec3 blendColor(vec3 base, vec3 blend) {
    return setLum(blend, luminance(base));
  }

  vec3 blendLuminosity(vec3 base, vec3 blend) {
    return setLum(base, luminance(blend));
  }

在片元着色器入口函数中使用:

js 复制代码
  vec4 frag() {
    // 前景色
    vec4 blendColor = texture(cc_spriteTexture, uv);
    // 背景色(目标色)
    vec4 baseColor = texture(bgTexture, bgUv);

    // 混合后的颜色
    vec3 color = vec3(1.0);

      // 通过自定义宏选择要使用的图层混合样式.
    #if USE_DARKEN
        color = blendDarken(baseColor.rgb, blendColor.rgb);
    #elif USE_LIGHTEN
        color = blendLighten(baseColor.rgb, blendColor.rgb);
    #elif USE_OVERLAY
        color = blendOverlay(baseColor.rgb, blendColor.rgb);
    #elif USE_COLOR_DODGE
        color = blendColorDodge(baseColor.rgb, blendColor.rgb);
    #elif USE_COLOR_BURN
        color = blendColorBurn(baseColor.rgb, blendColor.rgb);
    #elif USE_HARD_LIGHT
        color = blendHardLight(baseColor.rgb, blendColor.rgb);
    #elif USE_SOFT_LIGHT
        color = blendSoftLight(baseColor.rgb, blendColor.rgb);
    #elif USE_DIFFERENCE
        color = blendDifference(baseColor.rgb, blendColor.rgb);
    #elif USE_EXCLUSION
        color = blendExclusion(baseColor.rgb, blendColor.rgb);
    #elif USE_HUE
        color = blendHue(baseColor.rgb, blendColor.rgb);
    #elif USE_SATURATION
        color = blendSaturation(baseColor.rgb, blendColor.rgb);
    #elif USE_COLOR
        color = blendColor(baseColor.rgb, blendColor.rgb);
    #elif USE_LUMINOSITY
        color = blendLuminosity(baseColor.rgb, blendColor.rgb);
    #endif

    return vec4(color, blendColor.a);
  }

此时我们可以在材质的属性检查器面板,选择前景节点需要叠加的混合效果:

例如这里选择了 hue 图层混合样式,执行效果如下:

相关推荐
VaJoy2 天前
Cocos Creator Shader 入门 ⑹ —— 灰阶、反色等滤镜的实现
cocos creator
VaJoy4 天前
Cocos Creator Shader 入门 ⑸ —— 代码复用与绿幕抠图技术
cocos creator
VaJoy6 天前
Cocos Creator Shader 入门 ⑷ —— 纹理采样与受击闪白的实现
cocos creator
VaJoy7 天前
Cocos Creator Shader 入门 ⑶ —— 给节点设置透明度
cocos creator
VaJoy9 天前
Cocos Creator Shader 入门 (2) —— 给节点染色
cocos creator
VaJoy10 天前
Cocos Creator Shader —— 附录
cocos creator
成长ing1213811 天前
多层背景视差滚动Parallax Scrolling
cocos creator
似水流年wxk1 个月前
cocos creator使用jenkins打包微信小游戏,自动上传资源到cdn,windows版运行jenkins
运维·jenkins·cocos creator
成长ing121382 个月前
点击音效系统
前端·cocos creator