Cocos Creator Shader 入门 ⑼ —— 溶解动画

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

溶解在游戏领域属于经常使用的动画特效,例如在角色死亡时,角色的形象逐步被侵蚀瓦解:

然而溶解虽然效果酷炫,但它其实是着色器中比较简单的实现,很多入门课程都会拿它来作为第一个演示案例。

本文将介绍溶解动画的原理和实现方式。

一、溶解原理

噪声图是溶解动画必备的素材:

此类图片布满了随机、不规则的明暗纹理,可通过原材质 UV 映射来采样噪声图上对应位置的像素,通过判断该像素的明度是否小于指定阈值,来决定是否抛弃原材质像素:

通过动态修改阈值(例如从原本的 0.0 逐步加大到 1.0),原材质就会有越来越多的像素被抛弃,最终使得所有像素从画面中消失。

二、着色器实现

2.1 CCEffect

根据上文,我们需要自定义 noiseTexturedissolveThreshold 两个着色器参数:

yaml 复制代码
CCEffect %{
  techniques:
  - name: dissolve
    passes:
    - vert: vs:vert
      frag: fs:frag
      blendState:
        targets:
        - blend: true
          blendSrc: src_alpha
          blendDst: one_minus_src_alpha
      depthStencilState:      
          depthTest: false  
          depthWrite: false
      properties:
        noiseTexture: { value: grey, editor: { tooltip: '噪声贴图' } }
        dissolveThreshold: { value: 0.0, editor: { range:[0.0, 1.0, 0.01], slide: true, tooltip: '溶解阈值' } }
}%

noiseTexture 为噪声图材质(需要在材质属性检查器中将噪声图绑定到该参数),dissolveThreshold 为溶解阈值,后续在噪声图上采样的像素明度,会与该阈值进行对比。

💡 噪声图上采样的像素明度,会简单的使用该像素 RGB 某通道的分量值来表示,故其取值范围为 [0.0, 1.0],进而 dissolveThreshold 的取值范围也同样是 [0.0, 1.0]

2.2 片元着色器

根据溶解原理,我们进一步书写片元着色器:

js 复制代码
CCProgram fs %{
  precision highp float;
  #include <sprite-texture>

  in vec2 uv;

  uniform Dissolve{
    float dissolveThreshold;
  };

  uniform sampler2D noiseTexture;

  vec4 frag () {
    vec4 noiseTextureColor = texture(noiseTexture, uv);  // 对噪声材质采样
    float noiseRedValue = noiseTextureColor.r;  // 采样到的噪声像素 R 通道的分量值(任意选一个通道即可),用于表示明度

    if (noiseRedValue < dissolveThreshold) {
      discard;  // 将小于阈值的片段丢弃,形成溶解
    }

    vec4 color = texture(cc_spriteTexture, uv);  // 原纹理采样;
    return color;
  }
}%

此时随着 dissolveThreshold 的增大,原材质会丢弃越来越多的像素:

可以看到当 dissolveThreshold0.0 时原材质所有像素都被保留,当 dissolveThreshold1.0 时,原材质的所有像素都被丢弃。

为了让溶解效果更加美观,可以给溶解区域加一圈橘色的描边:

js 复制代码
    vec4 color = texture(cc_spriteTexture, uv);  // 原纹理采样;

    if (noiseRedValue < dissolveThreshold + 0.05) {
      color = vec4(0.9, 0.6, 0.3, color.a);  // 溶解的边缘设置一个橘色的边缘过渡色
    }

    return color;

2.3 组件脚本动态修改阈值

要让溶解动画自己动起来,需要通过脚本来修改材质 dissolveThreshold 参数的值:

js 复制代码
@ccclass('DissolveComp')
export class DissolveComp extends Component {
    private dissolveThreshold = 0;
    private spriteComp: Sprite;

    start() {
        this.spriteComp = this.getComponent(Sprite);
    }

    update(deltaTime: number) {
        if (this.dissolveThreshold === 1) return;

        this.dissolveThreshold += (deltaTime / 5);    // 逐步加大阈值
        if (this.dissolveThreshold > 1) {
            this.dissolveThreshold = 1;
        }

        this.spriteComp.material.setProperty('dissolveThreshold', this.dissolveThreshold);
    }
}

将该组件脚本绑定到 Sprite 节点上,游戏运行时就会直接播放溶解动画。

三、兼容 Spine 动画

上述实现的代码还无法兼容 Spine 动画,需要做进一步改造。

我们在《Cocos Creator Shader 入门 ⑷ ------ 纹理采样与受击闪白的实现》中已做过类似的兼容操作。

3.1 Effect 文件修改

首先要在 CCEffect 中新增 rasterizerState 配置来关闭背景剔除:

yaml 复制代码
CCEffect %{
  techniques:
      # 略...
      depthStencilState:      
          depthTest: false  
          depthWrite: false
      rasterizerState:
        cullMode: none   #兼容 Spine
      properties:
        # 略...
}%

接着修改片元着色器,加入顶点颜色混合的操作:

js 复制代码
CCProgram fs %{
  precision highp float;
  #include <sprite-texture>

  in vec2 uv;
  
  #if USE_LOCAL
    in vec4 v_color;   // 兼容 Spine
  #endif

  uniform Dissolve{
    float dissolveThreshold;
  };

  uniform sampler2D noiseTexture;

  vec4 frag () {
    vec4 noiseTextureColor = texture(noiseTexture, uv);
    float noiseRedValue = noiseTextureColor.r;

    if (noiseRedValue < dissolveThreshold) {
      discard;
    }

    vec4 color = texture(cc_spriteTexture, uv); 

    #if USE_LOCAL
      color *= v_color;  // 兼容 Spine,混合顶点颜色
    #endif

    if (noiseRedValue < dissolveThreshold + 0.05) {
      color = vec4(0.9, 0.6, 0.3, color.a);
    }

    return color;
  }
}%

将着色器材质应用到 Spine 组件上,手动修改 dissolveThreshold 便可看到 Spine 已能被溶解:

然而这里存在一个问题 --------- 溶解区域的橘色边缘,在原本 Spine 透明的区域也上色了,这导致 Spine 的溶解像是在多个矩形区域里执行的,显得很不自然:

这是因为 Spine 插槽(Slot)或 Attachment 可能会设置一些图层混合效果,从而导致原本透明区域的 RGB 色值在后期被强行混合进来了。

解决该问题的方案很简单 ------ 在设置边缘过渡色时判断原材质的 Alpha 是否大于 0.0 即可:

js 复制代码
    if (noiseRedValue < dissolveThreshold + 0.05 && color.a > 0.0) {  // 新增 color.a > 0.0 判断
      color = vec4(0.9, 0.6, 0.3, color.a); 
    }

此时 Spine 可按预期一样只在实心像素区域进行溶解:

3.2 组件脚本修改

原先的组件脚本仅支持 Sprite 组件,我们再加上对 Spine 组件的处理即可:

js 复制代码
@ccclass('DissolveComp')
export class DissolveComp extends Component {
    private dissolveThreshold = 0;
    private spriteComp: Sprite;
    private spineComp: sp.Skeleton;

    start() {
        this.spriteComp = this.getComponent(Sprite);
        this.spineComp = this.getComponent(sp.Skeleton);
    }

    update(deltaTime: number) {
        if (this.dissolveThreshold === 1) return;

        this.dissolveThreshold += (deltaTime / 5);
        if (this.dissolveThreshold > 1) {
            this.dissolveThreshold = 1;
        }

        if (this.spriteComp?.material) {  // Sprite 组件处理
            this.spriteComp.material.setProperty('dissolveThreshold', this.dissolveThreshold);
        } else {  // Spine 组件处理
            // 更新 Spine 所有插槽的材质实例
            const spineMatCaches = this.spineComp['_materialCache'];
            for (let k in spineMatCaches) {
                spineMatCaches[k].setProperty('dissolveThreshold', this.dissolveThreshold);
            }
        }
    }
}

执行后即可实现文章开头的动画效果:

💡 着色器会在 Spine 的每个部件上独立执行,因此会看到 Spine 的溶解是在每个独立的部件上进行的。如果你希望将整个 Spine 当做一个独立的材质来进行溶解,可以参考上一章的 Render Texture 方案,本章不再赘述。

相关推荐
VaJoy8 小时前
Cocos Creator Shader 入门 ⒀ —— UBO 内存布局策略
cocos creator
成长ing121389 天前
cocos creator 3.x shader 流光
前端·cocos creator
VaJoy10 天前
Cocos Creator Shader 入门 ⑾ —— 光照跟随
cocos creator
成长ing1213811 天前
闪白效果
前端·cocos creator
冷水金枪鱼12 天前
Light2D光照系统(基于CocosCreater引擎3.x/2.x)
cocos creator
VaJoy14 天前
Cocos Creator Shader 入门 ⑽ —— 拖尾效果的实现
cocos creator
VaJoy2 个月前
Cocos Creator Shader 入门 ⑺ —— 图层混合样式的实现与 Render Texture
cocos creator
VaJoy2 个月前
Cocos Creator Shader 入门 ⑹ —— 灰阶、反色等滤镜的实现
cocos creator
VaJoy2 个月前
Cocos Creator Shader 入门 ⑸ —— 代码复用与绿幕抠图技术
cocos creator