CocosCreator Shader圆形裁剪:从UV坐标到圆角矩形与环形进度条

发布时间:2026/10/14 3:35:47

CocosCreator Shader圆形裁剪:从UV坐标到圆角矩形与环形进度条 图片圆形裁剪这个需求做游戏UI的应该都不陌生。头像、小地图、技能冷却图标、活动入口的圆角按钮……几乎每个项目都会碰到“同一个贴图这边要用圆形的那边要用方形的还得带一圈描边”这种破事。真正逼你上Shader的是动态裁剪的情况角色头像随时更换图片但始终要显示成圆形圆形血条要跟着进度从某个角度开始扫活动图标要加一圈周期性的描边光效。美术直接出一张圆形PNG顶多解决静态场景一张图要应对多种形状、半径、透明度最后基本都只能跑Shader。说回CocosCreator的Shader学习系列前面几篇聊了基础概念和入门写法这篇聚焦图片的圆形裁剪。目标很简单给你一份能直接用的Effect文件同时把背后UV坐标、距离计算、宽高比还原理清楚免得下次换个形状、换个头像大小又不会了。这篇更适合已经在CocosCreator 3.x里创建过Effect、配过材质但对片元着色器怎么处理Alpha通道还没完全吃透的同学。看完这篇文章你能拿到的不是一段CtrlC的代码而是一套“想做圆形裁剪都能随手改出来”的通用思路。1. 为什么做圆形裁剪我先选了Shader而不是Mask1.1 裁剪的本质是透明度的局部改写很多人第一次面对圆形裁剪下意识就会去拖一个Mask组件。Mask也能裁剪成圆形项目还能正常跑所以大家觉得没必要专门写Shader。但其实认识这个需求的本质对后面所有扩展都有帮助。所谓裁剪并不是真的把图片边缘剪掉而是在片元着色器里把不应该显示区域的Alpha改成0。图片最终显示到屏幕上的时候GPU是把每个像素的颜色和已有的帧缓冲内容做混合。Alpha为0的像素混合完还是原来背景的样子于是看起来就像被剪掉了。GPU并不会动你原来纹理的数据只是戴着“滤镜”看它。想清楚这一点之后Shader方案的优势就明显了。圆形裁剪Shader做的事情就是“计算每个像素离圆心的远近远的把Alpha干掉”而中心点、半径、边缘羽化宽度全都是可以随时改的变量。这意味着同一个贴图同一个Shader在A处显示成小圆在B处显示成大圆在C处还有一个正在播放“从小到大”的展开动画全都不会互相干扰。1.2 Mask和Shader的真实成本对比Mask组件本质上是在模板缓冲Stencil Buffer里写一个标记后续的像素只有通过模板测试才会被绘制。直接导致三件事一是多一个DrawCall二是渲染批次要跟Mask内容保持相同队列三是Mask的圆形边缘只能做锯齿比较严重的硬边想做羽化得再叠一层纹理或者搞额外的东西。Shader方案完全在GPU里完成不给CPU增加任何额外状态设置也不依赖模板缓冲。而且裁剪形状完全由函数决定圆形、圆角矩形、环形、任意多个洞都能在一套Effect里通过数学表达。对比项Mask组件圆形裁剪Shader额外DrawCall有每个Mask至少增加一次无裁剪形状方形/圆形不好扩展圆形/圆角/环形/描边取决于数学函数边缘抗锯齿弱基本靠图片自带透明smoothstep可自由控制羽化动态参数旋转、缩放是组件级不够灵活center、radius、feather都能逐帧改性能开销模板操作额外DC片元着色器计算增加透明区域依然会被执行可能你会问那Mask是不是完全没用了也不是。如果只是简单的小圆头像且不追求任何动态效果用Mask也没问题。但做项目会有越来越多“这个也想动一下、那个也想换一下”的需求Shader方案的边际成本越往后越低代码写好一次到处改参数。这就是我最终在项目里选择Shader的原因。2. UV坐标和圆的计算看懂这两行代码才能改得动2.1 UV是0到1的地图坐标距离公式是唯一钥匙在Shader里我们拿到的纹理采样坐标叫UV。它跟贴图分辨率其实没什么关系不管贴图是128x128还是1024x1024UV范围基本都在0到1之间。左下角是(0,0)右上角是(1,1)。每个片元着色器执行的时候都不知道自己对应的像素在屏幕哪个位置但知道自己在贴图上的UV位置。圆形裁剪的第一步就是算“当前这个片元离圆心有多远”。设圆心在UV空间的center那么float dist distance(v_uv0, center);这句代码是整个圆形裁剪的核心。dist小于半径就在圆内大于半径就在圆外。你说这像不像地理课上的“以圆心为原点量每个点到圆心的直线距离”其实就是一个勾股定理文化点叫欧几里得距离。distance(a, b) length(b - a) sqrt((a.x-b.x)^2 (a.y-b.y)^2)在只有这几行代码的情况下你得到的圆圆心在UV空间里的位置就是固定的。如果贴图是正方形一切都完美如果贴图是长方形问题马上就来了。2.2 宽高比陷阱为什么非正方形贴图会剪出椭圆这是新手最常见的坑。你拿一张512x512的图用上面的代码剪出来的圆非常正。但换成一张512x256的图剪出来就是一个被垂直压扁的椭圆。原因在于UV的0到1在x和y两个方向对应的像素数量不一样。在x方向1.0单位等于贴图宽度W个像素在y方向1.0单位等于贴图高度H个像素。distance函数计算的是两个坐标轴上的差值平方和再开根号但两个轴的缩放比例不同。换成物理像素来看在x方向上移动0.1个UV等于W/10个像素在y方向上移动0.1个UV等于H/10个像素。当W不等于H时同样的UV距离对应的物理距离在x和y方向不一致。所以UV空间里的“圆”映射到纹理像素上就成了椭圆。修正办法很简单把UV差值按比例拉伸一下让x和y方向物理尺寸一致。以宽度为基准将y方向的差值乘以“高度/宽度”。也可以反过来以高度为基准乘“宽度/高度”效果一样。vec2 diff v_uv0 - center; diff.y * uTexSize.y / uTexSize.x; // 用纹理实际宽高比修正 float dist length(diff);这里的uTexSize需要单独传进Shader不会自动有。可以在CPU侧获取SpriteFrame的纹理尺寸然后通过材质属性传进去。一定要在脚本里动态设置尤其是同一张Effect可能用于不同尺寸贴图的时候。2.3 边缘用step还是smoothstep差别很大圆内显示、圆外隐藏最直观的做法是用step函数float alpha 1.0 - step(radius, dist); color.a * alpha;step在dist小于半径时返回0大于半径时返回1。这就像一刀切边界处没有过渡。GPU光栅化一张图时每个像素可能部分覆盖在圆内、部分在圆外一刀切会导致覆盖率信息被丢弃圆边缘出现严重的锯齿甚至因为相机移动产生闪烁。解决方案是smoothstep。它可以在两个边界值之间做平滑过渡float aa 1.0 - smoothstep(radius - feather, radius feather, dist); color.a * aa;feather是羽化宽度。在radius-feather到radiusfeather这个区间内alpha从1渐变到0。这个过渡模拟了边缘的覆盖率视觉上就是抗锯齿的圆边。比如想让边缘只有2像素的过渡feather就取纹理宽度下的2像素对应的UV长度feather 2.0 / textureWidth。如果feather给太大边缘会变得非常柔和像毛玻璃一样。理解了UV和距离这层关系Shader代码后面的各种变体其实都是在这个基础上加加减减。3. 在CocosCreator里落地完整Effect与材质配置流程3.1 一个能直接用的圆形裁剪Effect文件直接给一份我目前在CocosCreator 3.x里用的模板你可以根据自己项目版本微调CCEffect %{ techniques: - passes: - vert: vs frag: fs blendState: targets: - blend: true properties: texture: { value: white } center: { value: [0.5, 0.5] } radius: { value: 0.5 } feather: { value: 0.02 } uTexSize: { value: [1.0, 1.0] } }% CCProgram vs %{ precision highp float; #include cc-global #include cc-local in vec3 a_position; in vec2 a_uv0; out vec2 v_uv0; void main () { vec4 pos vec4(a_position, 1); v_uv0 a_uv0; gl_Position cc_matViewProj * pos; } }% CCProgram fs %{ precision highp float; #include cc-global #include cc-local in vec2 v_uv0; uniform sampler2D texture; uniform MyCircle { vec2 center; float radius; float feather; }; uniform MyTexSize { vec2 uTexSize; }; void main () { vec4 color CCSampleWithAlphaSeparated(texture, v_uv0); vec2 diff v_uv0 - center; diff.y * uTexSize.y / uTexSize.x; float dist length(diff); float aa 1.0 - smoothstep(radius - feather, radius feather, dist); color.a * aa; gl_FragColor color; } }%几个细节需要说明一下。首先是CCSampleWithAlphaSeparated这个宏CocosCreator内置纹理采样会用Alpha分离的格式直接采样会导致透明纹理的边缘发黑这个宏就是用来规避的照用即可。其次是uniform块CocosCreator的Effect里可以用uniform块来组合参数也可以直接声明散装uniform两种写法都行。最后是properties里面的texture不用自己声明GLSL变量引擎会帮你绑定到fs里的sampler2D texture上。如果你的项目版本在include部分有差异就去看看自带内置sprite的effect头部是怎么写的照着改就行。不同3.x版本的include路径经常有调整这点要注意。3.2 创建材质、指定Effect的完整操作链在编辑器里跑起来其实很简单。先在资源管理器里新建一个Material然后在Inspector里把effect字段拖上刚才创建的Effect资源。选中场景里的Sprite节点把它的CustomMaterial属性指到这个Material。默认情况下图片会直接应用这个材质但你还是会看到完整的方形图片因为radius默认0.5而center是(0.5,0.5)这个圆恰好覆盖了贴图中心边长刚好和半径相等四个角被裁剪掉了。把radius调到0.5center保持(0.5,0.5)如果贴图是正方形你会看到一个内切圆四角变透明。如果贴图是长方形你会看到椭圆这就是前面说的问题。调到这个状态后把uTexSize填对椭圆马上变成正确的圆。uTexSize在材质Inspector里是手动填的这样其实有点麻烦。每次都填纹理尺寸容易出错我建议在脚本里自动用SpriteFrame拿const spriteFrame this.sprite.spriteFrame; const tex spriteFrame.texture; const mat this.sprite.customMaterial; mat.setProperty(uTexSize, new Vec2(tex.width, tex.height));注意如果Sprite当前用的图集纹理那么tex.width和tex.height是图集整张图的大小不是子图大小。合图情况放在后面常见问题重点讲。3.3 运行时动态控制半径和中心固定一个圆形裁剪效果只是入门更常见的玩法是在运行中修改参数。比如头像从小圆变大圆或者圆形进度展开。在CocosCreator 3.x里Material实例化之后可以直接setPropertythis.sprite.customMaterial.setProperty(radius, 0.6); this.sprite.customMaterial.setProperty(center, new Vec2(0.4, 0.5));有个容易踩的坑是如果多个Sprite共用了同一个Material资产setProperty会同时改变所有Sprite的外观。正确做法是每个需要独立参数的Sprite各自复制一份Material实例const mat new Material(); mat.initialize({ effectName: my-circle-clip }); mat.setProperty(radius, 0.3); this.sprite.customMaterial mat;用代码new出来的Material不是资产文件但运行时完全可用项目打包也不会影响。每个头像一个实例参数随便改互不干扰。4. 圆角矩形、环形、描边把圆形裁剪扩展成各种UI花样4.1 圆角矩形的通用公式圆形裁剪学会之后圆角矩形就是同一个体系里的下一站。圆角矩形可以理解为“把矩形四个角替换成四分之一圆”。Shader里判断一个点距离圆角矩形边缘的距离有个非常经典的SDF写法vec2 p abs(v_uv0 - center) - halfSize; float maxDist length(max(p, 0.0)) min(max(p.x, p.y), 0.0); float dist maxDist - cornerRadius;看着复杂道理不复杂。先把UV坐标相对于矩形中心取绝对值再减去半个矩形的尺寸得到的就是“这个点相对于矩形的偏移量”。如果点还在矩形内部p的一部分会是负数用max(p,0)保证只计算伸到外部凸出的距离然后用length得到到边的距离。min项是为了处理点在矩形内部时返回的是到角点的距离而不是负值指数增长。拿到dist之后用法和圆形完全一样float aa 1.0 - smoothstep(cornerRadius - feather, cornerRadius feather, dist); color.a * aa;这个公式可以扩展到任意panel尺寸半径也能改成动画。比如聊天框的圆角从4像素拉到16像素配合一个tween就能实现圆角呼吸效果。4.2 用环形裁剪做圆形进度条圆形进度条本质上是在圆形内部再切掉一个内圆形成圆环然后按角度只显示一部分弧。环形部分可以这样算float ringAlpha smoothstep(inner - feather, inner feather, dist) * (1.0 - smoothstep(outer - feather, outer feather, dist));内圆半径到外圆半径之间保留其余Alpha为0。这样就有一个圆环了。如果还想按进度只显示弧线的一部分需要借助角度。把UV坐标换算成角度vec2 dir v_uv0 - center; float angle atan(dir.y, dir.x); // 结果范围 [-PI, PI] float normalized angle / (2.0 * 3.14159265359) 0.5; // 归一到 [0,1] float segment step(normalized, progress); // 小于进度的部分保留把环形的Alpha再乘以segment就是圆形进度条。要注意atan的方向和UV坐标系的原点位置有关CocosCreator 2D的UV左下角是(0,0)角度将以center为原点逆时针方向给出。如果发现进度方向反了把dir.y取反或者用1.0 - normalized调一下即可。4.3 用两个圆做交集、并集和描边Shader对Alpha的处理非常自由。两个圆的Alpha取最小值代表交集取最大值代表并集。比如头像的圆形描边逻辑就是“一个比头像半径更大的圆Alpha减去头像半径以内的Alpha”剩下的一圈就是描边float bigCircleAlpha 1.0 - smoothstep(bigRadius - feather, bigRadius feather, dist); float innerCircleAlpha 1.0 - smoothstep(innerRadius - feather, innerRadius feather, dist); float borderAlpha bigCircleAlpha - innerCircleAlpha; color.a * borderAlpha;这种做法比美术单独做一张描边图有优势半径变化时描边粗细不会变而且描边颜色可以直接通过uniform传vec4换色不用改图。我项目里活动入口的红色描边光效就是靠这种方式在Radius动画驱动下描边厚度始终稳定。5. 实战排查椭圆、锯齿、合图偏移和性能问题5.1 剪出来是椭圆前面原理部分已经讲过但真到了现场排查还是建议按顺序检查三项第一uTexSize是不是填对了尤其是换过图片之后有没有更新第二center是否在期望位置第三radius相对uTexSize是不是合理。我最常遇到的问题其实是换图的时候图片尺寸变了但脚本里没有重新setProperty。比如场景里原本挂的是一张512x512的头像后来改成1024x1024的高清图uTexSize还是512x512算出来的宽高比就是1:1于是高清图被剪成椭圆。修复方法是在设置SpriteFrame之后强制刷新一下const tex this.sprite.spriteFrame.texture; mat.setProperty(uTexSize, new Vec2(tex.width, tex.height));这问题在编辑器里不容易发现因为默认材质都是手动填的很多人填了第一次就不再理它。5.2 边缘锯齿和白边怎么处理锯齿主要来自羽毛宽度不够。feather建议至少为2物理像素换算成UV单位就是2除以贴图宽度的max值。如果项目是像素风可以再小一点但千万别用0否则手机屏幕上会明显发虚。白边这个问题更隐蔽。当贴图本身就带半透明边缘时仔细观察圆边缘会看到一圈淡淡的白边。这本质上是一个预乘Alpha问题很多美术导出图片时边缘像素的RGB并没有跟着Alpha一起乘以透明度半透明区域残留了原始颜色。解决思路有两个一是让美术导出“裁掉多余透明度”的图二是在Shader采样后用颜色校正。最省事的办法是在Effect里用Cocos内置的Tint颜色把边缘拉向背景色比如UI背景深色边缘颜色乘以0.5视觉上白边就消失了。但这不是根治只是掩盖。5.3 合图UV偏移圆心对不上这是移动端项目最容易翻车的地方。当图片被打进图集Atlas后v_uv0不再是0到1而是子图在整张大图纹理里的局部UV范围。比如子图的UV范围是(0.1,0.1)到(0.3,0.3)你直接用center(0.5,0.5)圆心就会跑出子图剪出来的圆完全对不上。解决办法有三条路可以选。第一条图片不参与自动合图强制走独立纹理UI头像这种个数不多但每个都要动态裁剪的图走这条路最简单。第二条在脚本里拿SpriteFrame的uvRect把center和radius换算到图集空间const uvRect spriteFrame.uvRect; const u uvRect.x 0.5 * uvRect.width; const v uvRect.y 0.5 * uvRect.height; mat.setProperty(center, new Vec2(u, v)); const scale Math.min(uvRect.width, uvRect.height); mat.setProperty(radius, uvRadius * scale);这里uvRect是子图在图集纹理中的矩形坐标按0到1归一化所以u和v就是子图的中心。uvRadius是你想要的半径UV空间比如0.5表示半径占子图宽度的一半。乘以scale之后半径就变成了以图集空间的纹理坐标表示的半径。第三条路更狠一点在Shader里直接用顶点着色器把a_uv0归一化到自己定义的UV空间把图集UV还原成0到1代码也没多几行但要注意顶点着色器里必须拿到子图范围Cocos内置的sprite Effect里有uv变换相关的宏可以顺着引擎源码看一下。我的建议是项目里凡是需要圆形裁剪的图优先独立打包。数量不多没必要为了那一点合图收益去背UV换算的维护成本。注意合图空间换算一定不能只用uvRect.width要结合纹理尺寸。在不同图集、不同纹理格式下uTicks和vTicks的单位是统一的只要子图范围是归一化的换算就成立。5.4 圆形裁剪Shader真的免费吗Shader方案也不是没有性能代价。圆形外的像素虽然Alpha是0但GPU依然执行了片元着色器距离计算、采样、混合全都干了。这意味着裁掉越多的区域无效计算越多。平时做头像、图标这类小尺寸UI完全没问题但如果你打算做全屏圆形渐显转场Shader方案会造成很大的性能浪费。碰到这种情况我的建议是用Mask组件来挡或者更干脆直接美术出一张遮罩序列帧。圆形裁剪Shader适合的是“形状动态变化但区域相对较小”的场景。头像列表一屏几十个头像每个用Shader也扛得住因为头像尺寸不大但如果你把整个UI面板都裁剪成圆形帧率一定会掉除非你在Shader里加一个discard路径让圆外的片元直接丢弃但要注意discard在透明物体上会打断一些GPU优化实际提速也不一定明显。5.5 半透明交叉时的渲染顺序问题还有一类问题经常被误解成Shader bug圆形裁剪后裁剪边缘和后面的UI元素交错出现“看到不该看的半透明”。这其实是透明物体排序问题。CocosCreator 2D渲染透明物体时按节点树深度排序当两个半透明Sprite交叉时绘制顺序会决定混合结果。圆形裁剪不会改变这个规则所以如果你的Shader裁剪边缘没有按预期遮蔽后面的元素先检查一下节点层级和SortingOrder。我项目里遇到过一次两个技能图标在同一个位置做切换半透明度在切换过程中出现叠化。把需要优先显示的节点的SortingOrder调高问题直接消失。这个真不是Shader能解决的。写在最后这套圆形裁剪Shader我从头到尾跑通之后最大的体会是一旦理解了UV坐标和距离计算之间的关系Shader就再也不是封装在黑盒里的玄学。你可以在一个周末内就把它改成圆角矩形、环形进度条、头像描边甚至组合出各种花哨的形状。后来我在项目里把center和radius两个参数接进了动画系统一个技能图标的解锁动画直接靠半径从0拉到0.5完成省掉了一整张美术序列帧。最后再分享一个小技巧如果你在编辑器里调试时总是分不清到底是中心算歪了还是半径算小了可以先给Shader加一个临时调试开关把圆内区域填充成纯红色这样一来哪里对哪里错一目了然。调通了再切回正常采样排查效率能快不少。
延伸阅读

更多相关文章

2026/10/14 3:35:47

虚拟电厂多时间尺度调度与储能衰减建模:Matlab复现要点

1. 为什么我说这篇论文值得复现做电力系统优化调度的人,这两年应该都有一个共同的感受:可再生能源渗透率一上来,传统的“机组组合经济调度”框架越来越捉襟见肘。不是模型不对,而是问题本身变了——电源侧的不确定性从“可忽略的扰…

2026/10/14 4:50:50

Pandas生态贯通:NumPy、Matplotlib与Scikit-learn数据流程实战

很多人学数据分析,喜欢一个库一个库地啃:先学NumPy,再学Pandas,又学Matplotlib,最后还报了个课学Scikit-learn。单看每个库的教程都能看懂,可真到项目里,一份CSV文件摆在面前,要清洗…

2026/10/14 4:50:50

本地跑27B大模型实测:159 tok/s如何实现与AI编程体验

最近我在折腾本地大模型,把 Qwen3.8-27B 跑到了 159 tok/s。先说结论:这个速度已经接近甚至超过了部分云端 API 的响应体验,但“能不能替代云端”这个问题,答案没这么简单。这篇帖子就围绕这个标题,聊聊我实测的硬件配…

2026/10/14 4:50:50

NetApp FAS8300企业级存储安装与ONTAP初始化实战指南

简介:本资源是NetApp FAS8300企业级存储系统的官方级安装与配置实践指南,面向存储运维工程师、系统集成实施人员及中高级SAN/NAS架构学习者,聚焦真实交付场景中的初始化部署、高可用集群构建与多协议统一管理等核心任务。压缩包为单文件PDF文…

2026/10/14 4:50:50

Qt表格与Excel互转实战:QXlsx方案选型、导出导入与避坑指南

1. 从一次数据交付返工说起:为什么表格与Excel的互转值得认真对待做过桌面端数据管理工具的人,大概都遇到过这样的场景:用户用你的软件录了一整天的数据,临走前问一句“能导出成Excel吗,我要发给同事汇总”。如果你当时…

2026/10/14 4:45:50

LLM Agent可观测性实战:Tracing、Evals与成本追踪一体化落地

1. 这不是“又一个AI监控工具”,而是LLM应用落地的生存指南你有没有遇到过这样的情况:模型在测试环境里回答得头头是道,一上线就频繁“胡言乱语”;用户投诉说“昨天还能正常用,今天突然卡住半天没反应”;运…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/14 2:32:55

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/14 0:00:33

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:33

macOS/iOS高精度计时:mach_absolute_time原理与实战

1. 项目概述:为什么mach_absolute_time是 macOS/iOS 时间测量的“黄金标准”在 macOS 和 iOS 开发中,我见过太多人用CFAbsoluteTimeGetCurrent()或NSDate.timeIntervalSinceReferenceDate做性能打点,结果在高精度场景下被系统时钟跳变、NTP 同…

2026/10/14 0:00:33

SCORM课件演示与测试全解析:从API通信到自动化验证

简介:这是一套遵循SCORM标准的交互式课件演示与测试资源,面向E-Learning课程开发者、LMS平台测试人员及在线教学从业者,用于验证课件在不同学习管理系统中的识别、传递与跟踪能力。压缩包共28个文件,约12.07MB,以swf动…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑