查看原文
其他

[Cocos Creator] 一个全能的挖孔 Shader

文弱书生陈皮皮 菜鸟小栈 2022-06-10

本文由“壹伴编辑器”提供技术支

前言
 来了来了,今天给大家分享的绝对是好东西!!!
 相信很多人都遇到需要在图片上挖孔(镂空)的需求,最常见的例子就是新手引导中的镂空遮罩。虽然可以用 Mask 实现,但是效果太勉强,也不好控制,而且很不优雅,更好的解决方案就是用 Shader 来实现。
 所以今天给大家带来的是可以满足几乎所有挖孔需求的 Shader 和炒鸡方便的配套组件
 矩形、圆形、圆角、边缘虚化,位置可控,统统打包带走,而且可通过代码轻松控制!
 什么?想要三角形和五角星?不,你不想要!

本文由“壹伴编辑器”提供技术支

效果展示
镂空 Shader  HollowOut 组件搭配使用效果顶呱呱~
是矩形还是圆形呢

圆形

大小位置变化丝毫不影响

多姿多彩

★ 下图是我配合 TouchBlocker 组件实现的新手引导功能。
 TouchBlocker 是用来限制可点击的节点的独立组件,完整文件在 eazax-ccc/component 目录下。
eazax-ccc 是我目前维护的一个开源游戏开发脚手架,包含各种实用的组件,目前也在不断更新中,有需要的童鞋在公众号发送“开源”即可获取链接,不要忘记 Star 哦~ 
让你点啥就点啥

☆ 实现上面的新手引导需要的核心代码还不到 15 行,嗐!苏福~
// 以下为新手引导实现核心代码,多简单啊

protected onLoad() {
    this.startBtn.on('touchend', this.onStartBtnClick, this);
    this.oneBtn.on('touchend', this.onOneBtnClick, this);
    this.twoBtn.on('touchend', this.onTwoBtnClick, this);
}

protected start() {
    this.hollowOut.nodeSize(); // 将遮罩镂空设为节点大小
    this.touchBlocker.setTarget(this.startBtn); // 设置可点击节点
}

private async onStartBtnClick() {
    this.touchBlocker.blockAll(); // 屏蔽所有点击
    await this.hollowOut.rectTo(1, this.oneBtn.getPosition(), this.oneBtn.width + 10, this.oneBtn.height + 10, 5, 5);
    this.touchBlocker.setTarget(this.oneBtn); // 设置可点击节点
}

private async onOneBtnClick() {
    this.hollowOut.nodeSize(); // 将遮罩镂空设为节点大小
    this.touchBlocker.blockAll(); // 屏蔽所有点击
    await this.hollowOut.rectTo(1, this.twoBtn.getPosition(), this.twoBtn.width + 10, this.twoBtn.height + 10, 5, 5);
    this.touchBlocker.setTarget(this.twoBtn); // 设置可点击节点
}

private onTwoBtnClick() {
    this.hollowOut.nodeSize(); // 将遮罩镂空设为节点大小
    this.touchBlocker.passAll(); // 放行所有点击
}


本文由“壹伴编辑器”提供技术支

正文

整体思路

1. 镂空的具体实现思路无非就是渲染时判断每个点的位置,是否符合我们的要求,符合的设为透明或者直接放弃渲染,否则正常渲染即可。
2. 由于 Shader 在渲染时使用的是标准屏幕坐标系(左上角为原点),与我们平时在 Creator 中使用的笛卡尔坐标系(左下角为原点)和本地坐标系(中间为原点)不同,使用时需要经过坐标转换。
3. 同时 Shader 中的点的坐标使用的不是相对于坐标系的位置,而是点处于节点宽高的百分比值,比如在屏幕中间的位置为(0, 0),在 Shader 中就为 (0.5, 0.5),这也是需要我们自己去计算的地方。
4. 由于我接触 Shader 的时间还不是很长,很多地方都不熟悉,一路跌跌撞撞边学边写花了几个晚上才把这个 Shader 和配套组件做完,而且我觉得还有优化的空间。
5. 以后我也会持续学习并深入理解 Shader 的编写,自己学习的同时也不忘记把知识分享给大家。后面我会写一系列入门文章给同样想要学习 Shader 的童鞋参考,感兴趣的童鞋可以关注下哦~

代码实现

注:本 Shader 基于 Cocos Creator 2.3.3 开发
 重要提醒:使用自定义 Shader 需要禁用动态合图功能,否则在运行的时候会出现渲染单色图片 Shader 失效的情况(编辑器中正常显示)!
// 禁用动态合图
cc.dynamicAtlasManager.enabled = false;

1. 由于完整 Shader 代码过于冗长,这里只贴出来比较关键的片段着色器部分,至于完整的代码以及文件可以点击文章底部的阅读原文查看并下载
另外由于我对 Shader 编写还不是很熟悉,主函数中使用了很多 if else 判断,我也在尝试优化中,如果有大佬知道如何优化,还请多多指教!
// 以下为镂空 Shader 中的片段着色器部分

CCProgram fs %{
  precision highp float;

  in vec2 v_uv0;
  in vec4 v_color;

  uniform sampler2D texture;

  uniform BaseParams {
    vec2 center;
    float ratio;
  };

  uniform RectParams {
    float width;
    float height;
    float round;
    float feather;
  };

  void main () {
    vec4 color = v_color;
    color *= texture(texture, v_uv0);
    // 边缘
    float minX = center.x - (width / 2.0);
    float maxX = center.x + (width / 2.0);
    float minY = center.y - (height * ratio / 2.0);
    float maxY = center.y + (height * ratio / 2.0);
    if (v_uv0.x >= minX && v_uv0.x <= maxX && v_uv0.y >= minY && v_uv0.y <= maxY) {
      if (round == 0.0) discard; // 没有圆角则直接丢弃
      // 圆角处理
      float roundY = round * ratio;
      vec2 vertex;
      if (v_uv0.x <= minX + round) {
        if (v_uv0.y <= minY + roundY) {
          vertex = vec2(minX + round, (minY + roundY) / ratio); // 左上角
        } else if (v_uv0.y >= maxY - roundY) {
          vertex = vec2(minX + round, (maxY - roundY) / ratio); // 左下角
        } else {
          vertex = vec2(minX + round, v_uv0.y / ratio); // 左中
        }
      } else if (v_uv0.x >= maxX - round) {
        if (v_uv0.y <= minY + roundY){
          vertex = vec2(maxX - round, (minY + roundY) / ratio); // 右上角
        } else if (v_uv0.y >= maxY - roundY) {
          vertex = vec2(maxX - round, (maxY - roundY) / ratio); // 右下角
        } else {
          vertex = vec2(maxX - round, v_uv0.y / ratio); // 右中
        }
      } else if (v_uv0.y <= minY + roundY) {
        vertex = vec2(v_uv0.x, (minY + roundY) / ratio); // 上中
      } else if (v_uv0.y >= maxY - roundY) {
        vertex = vec2(v_uv0.x, (maxY - roundY) / ratio); // 下中
      } else {
        discard; // 中间
      }
      float dis = distance(vec2(v_uv0.x, v_uv0.y / ratio), vertex);
      color.a = smoothstep(round - feather, round, dis);
    } else {
      color.a = 1.0;
    }
    
    color.a *= v_color.a;
    gl_FragColor = color;
  }
}%

2. 然后是配套使用的 HollowOut 组件,开箱即用~组件中已经实现了坐标以及距离的转换,使用非常的方便快捷。组件的完整文件在 exzax-ccc/component 目录下(公众号发送“开源”获取链接)。
这个组件的代码也比较多,这里只贴出较为关键的代码,大多数的情况处理我都已经封装好了,通过下面的代码大家可以轻易得知我是如何转换参数的,所以你也可以参照实现自己需要的特效或功能~
/**
 * 渲染
 * @param keepUpdating 是否每帧自动更新
 */
private render(keepUpdating: boolean) {
    if (!this.material) this.getMaterial();
    switch (this.shape) {
        case Shape.Rect:
            this.rect(this.center, this.width, this.height, this.round, this.feather, keepUpdating);
            break;
        case Shape.Circle:
            this.circle(this.center, this.radius, this.feather, keepUpdating);
            break;
    }
}

/**
 * 矩形镂空
 * @param center 中心坐标
 * @param width 宽
 * @param height 高
 * @param round 圆角半径
 * @param feather 边缘虚化宽度
 * @param keepUpdating 是否每帧自动更新
 */
public rect(center?: cc.Vec2, width?: number, height?: number, round?: number, feather?: number, keepUpdating: boolean = false) {
    this.shape = Shape.Rect;
    if (center !== null) this.center = center;
    if (width !== null) this.width = width;
    if (height !== null) this.height = height;
    if (round !== null) {
        this.round = round >= 0 ? round : 0;
        let min = Math.min(this.width / 2, this.height / 2);
        this.round = this.round <= min ? this.round : min;
    }
    if (feather !== null) {
        this.feather = feather >= 0 ? feather : 0;
        this.feather = this.feather <= this.round ? this.feather : this.round;
    }
    this.material.setProperty('ratio', this.getRatio());
    this.material.setProperty('center', this.getCenter(this.center));
    this.material.setProperty('width', this.getWidth(this.width));
    this.material.setProperty('height', this.getHeight(this.height));
    this.material.setProperty('round', this.getRound(this.round));
    this.material.setProperty('feather', this.getFeather(this.feather));
    this.keepUpdating = keepUpdating;
}

/**
 * 圆形镂空
 * @param center 中心坐标
 * @param radius 半径
 * @param feather 边缘虚化宽度
 * @param keepUpdating 是否每帧自动更新
 */
public circle(center?: cc.Vec2, radius?: number, feather?: number, keepUpdating: boolean = false) {
    this.shape = Shape.Circle;
    if (center !== null) this.center = center;
    if (radius !== null) this.radius = radius;
    if (feather !== null) this.feather = feather >= 0 ? feather : 0;
    this.material.setProperty('ratio', this.getRatio());
    this.material.setProperty('center', this.getCenter(this.center));
    this.material.setProperty('width', this.getWidth(this.radius * 2));
    this.material.setProperty('height', this.getHeight(this.radius * 2));
    this.material.setProperty('round', this.getRound(this.radius));
    this.material.setProperty('feather', this.getFeather(this.feather));
    this.keepUpdating = keepUpdating;
}

/**
 * 缓动镂空(矩形)
 * @param time 时间
 * @param center 中心坐标
 * @param width 宽
 * @param height 高
 * @param round 圆角半径
 * @param feather 边缘虚化宽度
 */
public rectTo(time: number, center: cc.Vec2, width: number, height: number, round: number = 0, feather: number = 0): Promise<void> {
    return new Promise(res => {
        cc.Tween.stopAllByTarget(this);
        this.tweenRes && this.tweenRes();
        this.tweenRes = res;
        if (round > width / 2) round = width / 2;
        if (round > height / 2) round = height / 2;
        if (feather > round) feather = round;
        this.shape = Shape.Rect;
        cc.tween<HollowOut>(this)
            .call(() => this.keepUpdating = true)
            .to(time, {
                center: center,
                width: width,
                height: height,
                round: round,
                feather: feather
            })
            .call(() => {
                this.scheduleOnce(() => {
                    this.keepUpdating = false;
                    this.tweenRes();
                    this.tweenRes = null;
                });
            })
            .start();
    });
}

/**
 * 缓动镂空(圆形)
 * @param time 时间
 * @param center 中心坐标
 * @param radius 半径
 * @param feather 边缘虚化宽度
 */
public circleTo(time: number, center: cc.Vec2, radius: number, feather: number = 0): Promise<void> {
    return new Promise(res => {
        cc.Tween.stopAllByTarget(this);
        this.tweenRes && this.tweenRes();
        this.tweenRes = res;
        this.shape = Shape.Circle;
        
        cc.tween<HollowOut>(this)
            .call(() => this.keepUpdating = true)
            .to(time, {
                center: center,
                radius: radius,
                feather: feather
            })
            .call(() => {
                this.scheduleOnce(() => {
                    this.keepUpdating = false;
                    this.tweenRes();
                    this.tweenRes = null;
                });
            })
            .start();
    });
}

/**
 * 取消所有挖孔
 */
public reset() {
    this.rect(cc.v2(), 0, 0, 0, 0);
}

/**
 * 挖孔设为节点大小(就整个都挖没了)
 */
public nodeSize() {
    this.rect(this.node.getPosition(), this.node.width, this.node.height, 0, 0);
}

/**
 * 获取中心点
 * @param center
 */
private getCenter(center: cc.Vec2) {
    let x = (center.x + (this.node.width / 2)) / this.node.width;
    let y = (-center.y + (this.node.height / 2)) / this.node.height;
    return cc.v2(x, y);
}

/**
 * 获取节点宽高比
 */
private getRatio() {
    return this.node.width / this.node.height;
}

/**
 * 获取挖孔宽度
 * @param width
 */
private getWidth(width: number) {
    return width / this.node.width;
}

/**
 * 获取挖孔高度
 * @param height
 */
private getHeight(height: number) {
    return height / this.node.width;
}

/**
 * 获取圆角半径
 * @param round
 */
private getRound(round: number) {
    return round / this.node.width;
}

/**
 * 获取边缘虚化宽度
 * @param feather
 */
private getFeather(feather: number) {
    return feather / this.node.width;
}

3. 另外我还提供了矩形和圆形的独立版本 Shader ,独立版本需要自行设置 Material 才能使用,同时不适用于 HollowOut 组件,当然可以自行实现。原文件在同级目录下,需要的话也是可以点击文章底部阅读原文找到~
独立版本

使用方法

1. 在带有 Sprite 组件的节点上添加 HollowOut 组件。
2. 将镂空 Shader 文件 eazax-hollowout.effect 拖到 HollowOut 组件的 Effect 属性上即可。
3. 在编辑器上调整需要的属性,或者使用代码获取 HollowOut 组件来设置属性。
How to use?

本文由“壹伴编辑器”提供技术支

结束语

以上皆为陈皮皮的个人观点,小生不才,文采不足,如果写得不好还请各位多多包涵。如果有哪些地方说的不对,还请各位指出,希望与大家共同进步。
接下来我会持续分享自己所学的知识与见解,欢迎各位关注本公众号。
我们,下次见!

本文由“壹伴编辑器”提供技术支

扫描二维码

获取更多精彩

文弱书生陈皮皮


点击 阅读原文 获取完整文件

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存