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 方案,本章不再赘述。

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