Cocos Creator 圆角矩形Shader

在Cocos Creator中图片元素都是以矩形的形式来呈现的,并没有直接进行圆角化处理的方式。写过前端的朋友应该都用过 border-radius 来将区块圆角化,输入圆角半径即可呈现效果,布局微调的时候非常方便。为了更舒坦的进行界面微调,XianYu根据border-radius的使用习惯制作了圆角矩形Shader。

效果展示

圆角顺序与border-radius的顺序相同,输入大于等于1的数值,会以px为单位进行圆角化,输入小于1的数值时,会以宽高百分比的方式进行圆角化。

可能有的童鞋会好奇,为什么每个角对应两个值,半径一个值不就够了吗?有此疑问的童鞋请移步至MDN查看border-radius说明,border-radius属性可以单独设置水平轴和垂直轴的长度,此时边角会呈现椭圆形。

与前端的border-radius联合写法相比,此种配置方案显得繁琐。有简单方案怎么能不安排呢? :partying_face:

受限于微信平台对动图的要求,仅仅演示了直接设置px的方式,百分比配置方式在CocosEditor模式下以小于1的小数方式实现,当然为了能够实现1px圆角,输入1的时候并不代表100%。在Web模式下可以自由的输入字符串,但请遵循前端规范。

  • 不写数字、“px”、“PX”、"%"、空格以外的其他字符。
  • 百分比不以小数形式表示,用%的形式表示。
  • 在画椭圆边角时,"/"两边记得带上空格。
  • 末尾";"可有可无

正文

1. 实现思路

开始表演前先来MDN学习下border-radius,毕竟我的前端也就是半吊子水平。下图为MDN对border-radius给出的解释。


从上图可以看出,border-radius的实现原理就是在边角画内切椭圆(圆只是椭圆的特殊形态),而椭圆的原点可以通过设置的圆角半径计算出来,下边只需要像素是否在椭圆内部即可,将边角区域椭圆外部的像素的Alpha通道的值设为零就会呈现出圆角效果。

如何确定在椭圆外呢?一步一步来,先来画个椭圆吧!下图是长半轴为4,短半轴为2的椭圆。


椭圆有了,那么椭圆上的点该如何用代数式来表示呢?根据椭圆标准方程书写规则,上图的椭圆可以表示为:
202104241716
椭圆上的点的坐标满足上述表达式,将 = 换成 > 后表达式就变成了:
202104241717
上述表达式对应的区域为:

很明显,这就是我们要找的区域,在该区域的点均满足上述不等式。接下来的任务就是在Shader中找到对应的点将Alpha通道的值设为0即可。

2. 代码实现

在Shader中不管是uv坐标还是颜色值,取值范围均被压缩至 [0, 1] ,而长方形的宽高是不相等的,为了使圆角能够达到满意的效果,统一的将uv坐标的取值范围映射到精灵节点的宽高,传入的圆角半径也以该范围为基准,百分比半径在传入前通过组件脚本进行计算转换后传入Shader。

第一次接触Shader,不知道这种操作方式是否会有什么负面影响,至少目前在我编写的过程中它是可以运行的。如有不妥之处还望大佬多多指教。

CCEffect %{
  techniques:
  - passes:
    - vert: vs
      frag: fs
      blendState:
        targets:
        - blend: true
      rasterizerState:
        cullMode: none
      properties:
        texture: { value: white }
        size: { value: [0, 0], editor: { tooltip: "图片尺寸" } }
        leftTop: { value: [0, 0], editor: { tooltip: "左上角,第一个数为水平方向" } }
        rightTop: { value: [0, 0], editor: { tooltip: "右上角,第一个数为水平方向" } }
        rightBottom: { value: [0, 0], editor: { tooltip: "右下角,第一个数为水平方向" } }
        leftBottom: { value: [0, 0], editor: { tooltip: "左下角,第一个数为水平方向" } }
}%

参数区代码

CCProgram fs %{
  precision highp float;

  in vec2 v_uv0;
  in vec4 v_color;

  #if USE_TEXTURE
    uniform sampler2D texture;
  #endif

  uniform Properties {
    vec2 size;
    vec2 leftTop;
    vec2 rightTop;
    vec2 rightBottom;
    vec2 leftBottom;
  };

  float inEllipse(vec2 origin, float radius_a, float radius_b, vec2 coordinate) {
    // 转换坐标
    coordinate.x -= origin.x;
    coordinate.y -= origin.y;
    // 圆锥曲线
    return pow(coordinate.x, 2.0) / pow(radius_a, 2.0) + pow(coordinate.y, 2.0) / pow(radius_b, 2.0);
  }

  void main () {
    vec4 color = v_color;

    color *= texture(texture, v_uv0);

    // 转换坐标原点到左下角
    vec2 uv = vec2(v_uv0.x, 1.0 - v_uv0.y);

    // 转换坐标到实际尺寸大小
    uv.xy *= size.xy;

    // 处理左上角
    if(uv.x < leftTop.x && uv.y > size.y - leftTop.y) {
      if(inEllipse(vec2(leftTop.x, size.y - leftTop.y), leftTop.x, leftTop.y, uv) > 1.0) {
        color.a = 0.0;
      };
    }

    // 右上角
    if(uv.x > size.x - rightTop.x && uv.y > size.y - rightTop.y) {
      if(inEllipse(vec2(size.x - rightTop.x, size.y - rightTop.y), rightTop.x, rightTop.y, uv) > 1.0) {
        color.a = 0.0;
      };
    }

    // 右下角
    if(uv.x > size.x - rightBottom.x && uv.y < rightBottom.y) {
      if(inEllipse(vec2(size.x - rightBottom.x, rightBottom.y), rightBottom.x, rightBottom.y, uv) > 1.0) {
        color.a = 0.0;
      };
    }

    // 左下角
    if(uv.x < leftBottom.x && uv.y < leftBottom.y) {
      if(inEllipse(vec2(leftBottom.x, leftBottom.y), leftBottom.x, leftBottom.y, uv) > 1.0) {
        color.a = 0.0;
      };
    }

    color.a *= v_color.a;

    gl_FragColor = color;
  }
}%

片段着色器代码

顶点着色器未进行实质性的修改,就不贴代码了。

Shader写完后只是实现了绘制圆角的功能,修改一次圆角创建一个Material还不如直接让美工给带有圆角的图片。因此配合使用的SpriteRadius脚本就登场了,考虑到篇幅问题就不再贴代码了,上传送门

3. 使用方法

从gitee获取代码文件啥的就不说了,获取过程中点Star还是要提一提的 :crazy_face:

  1. 将下载过的资源文件放到工程中,具体放置路径看你项目的资源架构了。

  2. 找到需要圆角的精灵节点,将SpriteRadius脚本添加到节点上。

  3. 将Effect资源拖入到SpriteRadius脚本组件中

  4. 剩下的就是配置圆角半径了,X为水平方向圆角半径,Y为垂直方向圆角半径,Web方式用前端的CSS字符串即可使用。

4. 参考资料

站在皮皮的肩膀上编程,香 :crazy_face:

文章排版有点乱,公众号相对凑和点

源码获取:传送门
开发环境:Cocos Creator 2.4.5其他版本未测试

202104241727

12赞

赞 

斯国一 斯国一

会有毛边吗?

个人感觉有轻微锯齿,在Cocos logo上测试感觉不太明显,但在纯白色精灵图上感觉有点大,可以先弄个demo看下效果

牛皮 plus! :crazy_face:

斯故一斯故一

都说 shader 代码里有 if 语句,会影响性能,实际上具体能影响多少呢?

厉害厉害呀

抗锯齿可以参考下iq的版本
https://www.shadertoy.com/view/4llXD7

1赞

既然你都提出问题了,我不回答怪不好的,那我就勉为其难的说句不知道吧 :expressionless:毕竟我也只是个刚入门的菜鸡

emmm,你可以试下在长方形上的效果,应该会出现椭圆形 :crazy_face:

感谢大佬指点,我去学习下 :partying_face:

有没有遇见过,shader 圆角后,替换 spriteFrame (微信头像),图片就飞了的问题?
image

没遇见过,我在摸索的过程中看他们都说关闭合图,不知道是否受这个影响,可以试下this.sprite.spriteFrame.getTexture().packable = false;
圆角头像Shader在皮皮的脚手架中也有,可以去翻下https://gitee.com/ifaswind/eazax-ccc

尺寸大小写了吗

这个确实是正解啊,多谢。我图片给了 false 了,没想到 new 的时候默认又给回 true 了 :rofl:
我现在的写法是这样,给大家参考:

   cc.assetManager.loadRemote(avatar, { ext: ".png" }, (err, texture: cc.Texture2D) => {
      texture.packable = false; // 主要是这句
      this.playerHeadImg.spriteFrame = new cc.SpriteFrame(texture);
    });
1赞

替换材质会打断合批吗?

shader 不同,应该就会打断

一个ps就搞定了,搞这么复杂?