拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Cocos Creator 3.x中高性能2D圆角矩形实现:Shader遮罩方案详解

1. 项目概述与核心价值最近在做一个2D卡牌对战项目美术同学给过来的卡牌设计稿边缘都带着精致的圆角。这看似一个简单的UI需求但在Cocos Creator 3.x的3D渲染管线下实现一个性能优秀、效果稳定且支持动态变化的2D圆角矩形还真不是拖个Sprite就能搞定的事。网上搜了一圈要么是2.4.x时代的老方案要么是过于复杂的Shader魔改对于只想快速实现一个卡牌底图的开发者来说信息太零散了。这个项目要解决的就是在Cocos Creator 3.8.x中从零开始实现一个高质量的2D圆角矩形效果并最终应用到卡牌上。它不仅仅是画个圆角更要考虑如何在UI合批中保持高性能、如何灵活控制圆角半径、颜色甚至描边以及如何与卡牌的其他元素如文字、图标完美结合。踩过几个坑之后我总结出了一套从思路到代码的完整流程你会发现用内置的Mask组件和一点点材质技巧就能优雅地解决这个问题完全不需要引入沉重的第三方库或者写复杂的自定义渲染组件。2. 方案选型与思路拆解2.1 为什么不用简单的图片Sprite最直观的想法是让美术直接出一张带圆角的卡牌底图不就好了这个方法在原型阶段确实最快但它有几个致命缺点缺乏灵活性圆角半径、卡牌尺寸、颜色一旦定死任何调整都需要重新出图沟通和迭代成本高。资源冗余不同颜色、不同尺寸的卡牌都需要单独的图片增大包体。动态效果受限难以实现圆角半径、颜色渐变动画等动态效果。因此我们的目标是程序化生成圆角矩形。2.2 Cocos Creator 3.x 下的2D渲染考量Cocos Creator 3.x 虽然主打3D但其UI系统ui-transform,Renderer2D仍然是构建2D游戏的主力。在3.8.x版本中2D渲染已相当稳定。实现图形主要有几种路径Graphics组件用于矢量绘图可以画圆角矩形。但它在UI合批方面不占优且每帧重绘如果内容变化有性能开销适合动态变化的简单图形但对于静态卡牌底图不是最优选。自定义Mesh Shader最灵活、性能潜力最高的方案但门槛也最高需要处理网格生成、材质管理、合批等问题。Sprite 遮罩Mask这是本次选择的方案。利用一个矩形的Sprite作为基底配合一个圆角形状的遮罩来“裁剪”出圆角效果。这是性能、灵活性和实现复杂度三者平衡的最佳选择。2.3 最终方案Sprite 圆角遮罩Mask组合核心思路分两步创建圆角遮罩我们需要一个圆角矩形的图形作为遮罩模板。这里巧妙地使用一个同样大小的Sprite但其材质使用一个特殊的着色器Shader这个着色器将圆角矩形区域输出为不透明Alpha1其余角落区域输出为完全透明Alpha0。应用遮罩将上一步生成的圆角图形节点添加Mask组件并设置为RECT模式或GRAPHICS模式但RECT性能更好。然后将我们的卡牌底图Sprite可以是一个纯色矩形也可以是一张纹理作为这个Mask节点的子节点。根据Mask的工作原理子节点只有在遮罩不透明的区域才会被显示出来从而实现了圆角裁剪。这个方案的优点在于性能好Sprite和Mask都是引擎高度优化的内置组件能很好地参与UI合批。灵活通过修改Shader的参数可以实时调整圆角半径、抗锯齿等。卡牌底图可以随意更换颜色或纹理。易用无需深度修改引擎所有操作在编辑器和脚本中即可完成。3. 核心环节圆角遮罩Shader的实现这是整个流程的技术核心。我们需要编写一个片段着色器Fragment Shader来定义每个像素是否属于圆角矩形内部。3.1 Shader代码解析我们创建一个名为rounded-rect-mask.effect的着色器文件。// 着色器头部定义属性和渲染状态 CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true blendSrc: src_alpha blendDst: one_minus_src_alpha properties: radius: { value: 0.1, editor: { tooltip: 圆角半径归一化值 (0-0.5) } } smoothness: { value: 0.01, editor: { tooltip: 抗锯齿平滑度 } } }% // 顶点着色器标准2D Sprite顶点处理 CCProgram vs %{ precision highp float; #include cc-global #include cc-local in vec3 a_position; in vec2 a_texCoord; out vec2 v_uv; void main () { vec4 pos vec4(a_position, 1.0); #if CC_USE_MODEL pos cc_matViewProj * cc_matWorld * pos; #else pos cc_matViewProj * pos; #endif gl_Position pos; v_uv a_texCoord; } }% // 核心片段着色器 CCProgram fs %{ precision highp float; in vec2 v_uv; uniform RoundedRect { float radius; float smoothness; }; // 定义一个函数计算从点p到矩形rect左下角原点宽高size的符号距离场SDF float sdRoundedRect( in vec2 p, in vec2 size, in float radius ) { // 将坐标中心移到矩形中心并取绝对值这样只需处理第一象限 vec2 d abs(p) - size radius; // 计算到内部矩形或圆角的距离 return min(max(d.x, d.y), 0.0) length(max(d, 0.0)) - radius; } void main () { // 将UV从[0,1]映射到[-0.5, 0.5]的坐标系中心为(0,0) vec2 centerUV v_uv - vec2(0.5, 0.5); // 矩形半宽高因为UV范围是1所以半宽高是0.5 vec2 halfSize vec2(0.5, 0.5); // 计算当前像素到圆角矩形的有符号距离SDF // 距离为负在图形内部距离为正在图形外部距离为零在边界上 float distance sdRoundedRect(centerUV, halfSize, radius); // 利用smoothstep函数实现平滑的边缘过渡抗锯齿 // 当distance从 -smoothness 变化到 smoothness 时alpha值从1平滑过渡到0 float alpha 1.0 - smoothstep(-smoothness, smoothness, distance); // 输出颜色。RGB值不重要我们只关心Alpha通道作为遮罩。 // 可以将RGB设为白色方便在编辑器里预览。 gl_FragColor vec4(1.0, 1.0, 1.0, alpha); } }%关键点解析SDF有符号距离场函数sdRoundedRect是核心算法。它计算任意一点到圆角矩形轮廓的最短距离内部为负外部为正。这是图形学中生成平滑形状的常用高效方法。坐标变换v_uv是纹理坐标(0,0)到(1,1)。我们将其减去0.5变换为以中心为原点的坐标系(-0.5, -0.5)到(0.5, 0.5)方便距离计算。抗锯齿Smoothness直接使用step函数会产生锯齿状边缘。我们使用smoothstep在边界附近创建一个平滑的透明度过渡区通常1-2像素宽这样渲染出的圆角边缘非常光滑。输出我们只关心gl_FragColor的Alpha值。遮罩节点本身的颜色不会被显示但它的Alpha通道决定了子节点的可见性。3.2 在编辑器中创建与配置材质创建材质在资源管理器右键 - 创建 - 材质命名为RoundedRectMask。选择Effect在材质面板的Effect属性中选择我们刚才编写的rounded-rect-mask.effect。调整参数你会在材质面板看到暴露的radius和smoothness属性。radius归一化的圆角半径。因为我们的矩形半宽高是0.5所以radius的最大有效值约为0.5此时将变成圆形。通常设置为0.1-0.15效果较好。smoothness抗锯齿平滑度一般0.005-0.02即可值太大会使圆角“变糊”。注意这里的radius是相对于矩形半宽高的归一化值。如果你的Sprite节点尺寸不是正方形最终的圆角视觉上是椭圆形的这是符合预期的。如果你需要四个角是标准的圆形弧线即半径的像素值固定则需要更复杂的计算将像素尺寸传入Shader。对于卡牌这种固定比例的UI使用归一化值更方便。4. 完整场景搭建与脚本控制4.1 场景节点结构搭建让我们在场景编辑器中一步步构建创建卡牌根节点创建一个空节点命名为CardRoot添加UITransform组件设置卡牌大小例如 200x280。创建遮罩节点在CardRoot下创建子节点命名为MaskNode。为MaskNode添加Sprite组件。SpriteFrame可以留空或者给一个纯白小图仅用于在编辑器内占位和定义网格。关键一步将之前创建的RoundedRectMask材质拖拽到Sprite组件的CustomMaterial属性上。此时你应该能看到这个节点显示为一个圆角矩形白色。为MaskNode添加Mask组件。将Type属性设置为RECT。确保Inverted未勾选。创建卡牌底图在MaskNode下创建子节点命名为CardBackground。为其添加Sprite组件。这里可以设置你想要的卡牌底色若要纯色SpriteFrame使用一个纯色的纹理或引擎内置的纯色Sprite然后在Color属性中选择颜色。若要使用纹理直接指定SpriteFrame即可。调整CardBackground节点的UITransform大小使其与父节点MaskNode大小一致或略大以确保覆盖。至此一个具有圆角效果的卡牌底图就完成了。你可以通过修改MaskNode上Sprite组件所使用的材质实例参数实时调整圆角大小。4.2 编写控制脚本为了让设计人员和策划能更方便地调整属性我们创建一个脚本CardRoundedMask.ts。import { _decorator, Component, Sprite, Material } from cc; const { ccclass, property, executeInEditMode } _decorator; ccclass(CardRoundedMask) executeInEditMode // 允许在编辑器模式下实时更新 export class CardRoundedMask extends Component { property({ type: Sprite, tooltip: 应用了圆角遮罩材质的Sprite节点 }) public maskSprite: Sprite | null null; property({ tooltip: 圆角半径 (归一化 0-0.5), range: [0, 0.5, 0.01] }) public radius: number 0.1; property({ tooltip: 边缘平滑度, range: [0, 0.05, 0.001] }) public smoothness: number 0.01; // 用于保存材质实例避免每帧获取 private _materialInstance: Material | null null; start() { this.updateMaterialProperties(); } // 在属性检查器中修改数值时编辑器会调用此函数 onLoad() { this.updateMaterialProperties(); } update(deltaTime: number) { // 如果在编辑模式下并且属性被动画或其它方式修改可以在这里更新 // 对于静态卡牌可以移除update以节省性能 // this.updateMaterialProperties(); } public updateMaterialProperties() { if (!this.maskSprite) { console.warn(CardRoundedMask: maskSprite is not assigned.); return; } // 获取或创建材质实例 let mat this.maskSprite.getMaterialInstance(0); if (!mat) { // 如果还没有材质实例则使用共享材质创建一个实例 const sharedMat this.maskSprite.sharedMaterial; if (sharedMat) { mat new Material(); mat.copy(sharedMat); this.maskSprite.setMaterialInstance(mat, 0); } else { console.error(CardRoundedMask: No shared material found on the sprite.); return; } } this._materialInstance mat; // 设置着色器属性 this._materialInstance.setProperty(radius, this.radius); this._materialInstance.setProperty(smoothness, this.smoothness); // 通知材质更新 this._materialInstance.overridePipelineStates(); } // 提供外部调用的方法用于动态改变圆角例如动画 public setRadius(value: number) { this.radius Math.max(0, Math.min(0.5, value)); // 钳制范围 this.updateMaterialProperties(); } public setSmoothness(value: number) { this.smoothness Math.max(0, value); this.updateMaterialProperties(); } }脚本使用步骤将脚本挂载到CardRoot节点上。在脚本组件的maskSprite属性中拖拽场景中的MaskNode带有Sprite组件的那个节点进行关联。现在你可以在CardRoot节点上直接调整radius和smoothness属性圆角效果会实时变化。实操心得务必使用getMaterialInstance和setMaterialInstance来操作材质。直接修改sharedMaterial会影响所有使用同一材质的节点。为每个动态控制的Sprite创建独立的材质实例Material Instance是标准做法。executeInEditMode装饰器让你在场景编辑器里调整参数时能即时看到反馈非常方便。5. 性能优化与高级技巧5.1 性能考量与合批我们的方案性能开销很小主要在于一次额外的遮罩渲染Mask组件需要额外渲染一次遮罩模板。但RECT模式的Mask是经过高度优化的。一个自定义Shader这是一个非常简单的片段着色器每像素计算一次SDF对现代GPU来说几乎无感。为了最大化性能静态卡牌如果卡牌创建后圆角不再变化确保脚本的update函数为空或移除update。在start或onLoad中初始化一次即可。合批Batching多个使用完全相同材质参数即同一个材质实例的圆角遮罩Sprite引擎仍然有可能对它们进行合批。但通常每张卡牌我们都会单独创建材质实例以便独立控制这会打断合批。对于大量相同样式的静态卡牌可以考虑使用同一个材质实例牺牲独立控制换取性能。需要根据项目实际情况权衡。减少Overdraw确保遮罩节点和底图节点的大小精确匹配不要有不必要的重叠或过大区域。5.2 实现圆角描边效果描边能极大提升卡牌的质感。我们可以在同一个Shader中增加描边功能。修改rounded-rect-mask.effect的properties和fs函数// 在properties块中增加 outlineWidth: { value: 0.02, editor: { tooltip: 描边宽度归一化值 } } outlineColor: { value: [1.0, 0.8, 0.2, 1.0], editor: { type: color } } // 在CCProgram fs的uniform块中增加 uniform RoundedRect { float radius; float smoothness; float outlineWidth; // 新增 vec4 outlineColor; // 新增 }; // 修改fs的main函数 void main () { vec2 centerUV v_uv - vec2(0.5, 0.5); vec2 halfSize vec2(0.5, 0.5); float distance sdRoundedRect(centerUV, halfSize, radius); // 计算内部填充的Alpha float fillAlpha 1.0 - smoothstep(-smoothness, smoothness, distance); // 计算描边的Alpha在边界附近根据outlineWidth生成第二个平滑区域 float outlineAlpha smoothstep(-smoothness, smoothness, distance - outlineWidth) - smoothstep(-smoothness, smoothness, distance); // 混合填充色白色和描边色 vec3 finalColor mix(vec3(1.0), outlineColor.rgb, outlineAlpha); // 总Alpha是填充和描边的Alpha之和但不超过1 float finalAlpha min(fillAlpha outlineAlpha, 1.0); gl_FragColor vec4(finalColor, finalAlpha); }然后在控制脚本中增加对应的outlineWidth和outlineColor属性及设置逻辑即可。这样遮罩本身就有了颜色描边卡牌底图作为其子节点显示在描边之内。5.3 与卡牌其他元素的整合卡牌通常还包括卡面图案、文字、攻击力/防御力图标等。我们的圆角遮罩节点MaskNode是一个完美的容器所有视觉子元素将卡面图案、文字、图标等节点都作为MaskNode的子节点。这样它们自然都会被圆角裁剪并且渲染顺序正确底图在最下层。交互区域如果需要为卡牌添加按钮事件如点击、拖动应将Button或EventTarget组件添加到CardRoot节点而不是MaskNode。因为Mask组件可能会影响子节点的事件接收。CardRoot节点的UITransform定义了卡牌的逻辑大小和点击区域。6. 常见问题与排查实录问题1圆角效果不显示卡牌仍然是直角矩形。检查点1确认MaskNode上的Mask组件是否启用且Type设置为RECT。检查点2确认MaskNode的Sprite组件是否正确应用了RoundedRectMask材质。在编辑器场景中MaskNode本身应该显示为圆角白色图形。检查点3确认卡牌底图节点是MaskNode的直接子节点。检查点4检查Shader编译是否有错误。查看浏览器开发者工具F12的Console面板或Cocos Creator的日志。问题2圆角边缘有锯齿Aliasing。解决方案适当增加材质参数中的smoothness值例如从0.01调到0.015。但注意不要太大否则边缘会变模糊。根本原因抗锯齿平滑度不足。我们的smoothstep函数就是在模拟抗锯齿。问题3修改脚本的radius属性圆角没有实时变化。检查点1脚本是否添加了executeInEditMode装饰器如果没有编辑器模式下修改不会触发更新。检查点2updateMaterialProperties方法中是否成功获取到了maskSprite的材质实例检查Console是否有警告或错误。检查点3确保你修改的是挂载了脚本的节点如CardRoot上的属性而不是材质资源本身的属性。问题4在真机上特别是移动端效果异常或性能差。检查点1确保使用的Shader语法是GLSL ES 1.0Cocos Creator默认兼容移动设备。我们的代码是标准的没问题。检查点2如果卡牌数量非常多如上百张考虑性能。可以尝试对不需要动态变化圆角的卡牌禁用其控制脚本的update。如果多张卡牌样式完全相同尝试让它们共享同一个材质实例但这样就不能单独控制圆角了。使用更简单的方案比如对静态卡牌烘焙成带圆角的纹理图集Atlas但这又回到了灵活性的问题。需要项目特定权衡。问题5我想让圆角的弧度是绝对的像素值而不是相对比例。解决方案需要将像素尺寸传入Shader。修改Shader增加size属性卡牌像素尺寸的一半并修改距离计算函数。同时在脚本中需要将像素半径转换为归一化值像素半径 / size.x或取min(size.x, size.y)。这会更复杂一些但能保证在不同尺寸的卡牌上圆角的像素半径一致。对于大多数UI设计使用相对比例归一化更便于维护视觉一致性。这套方案经过项目实战检验在Cocos Creator 3.8.x上运行稳定效果出色。它成功地在3D引擎里实现了高质量的2D圆角UI平衡了效果、性能和开发效率。你可以根据这个基础进一步扩展出阴影、渐变、不规则遮罩等更丰富的卡牌效果。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门