资讯动态

Unity Shader实现摆钟双图切换:UV角度计算与边缘柔化

发布时间:2026/10/18 9:06:40 来源:尧图企业网站定制
1. 摆钟效果的视觉拆解一张Shader搞定的“高级感”先说我第一次看到这个效果时的判断短视频里那种摆钟指针扫过、两张图片跟着切换的玩法第一眼以为要上序列帧或者做一大串遮罩动画。实际拆开看核心就是个“按角度切纹理”的Shader。指针扫过去左边还是旧图右边已经切成新图中间有一条清晰的边界线配合指针来回摆动视觉上就像钟摆在扫动画面。这个效果的底层逻辑不复杂以图片中心为原点把每个像素的UV坐标换算成相对于中心点的角度然后拿这个角度和当前指针的旋转角度做比较。指针角度左边或者右边的像素显示图A另一侧的像素显示图B。指针摆动分界线跟着转两张图就被“扫”着切换了。我之前还真考虑过另一个方案用Mask组件加旋转的RectTransform做分层遮罩。试过之后发现两个问题一是图A和图B要裁成严格的半圆再分别拼接边缘对不齐时会有细缝二是摆钟往往不是正半圆扫动可能是120度、240度的扇形扫动Mask的旋转和子物体裁切关系调起来很麻烦性能也不占优。Shader方案只需要一个材质、两张贴图、一个角度参数指针旋转角度由外部C#控制即可。哪张图盖在左边哪张图盖在右边完全是Shader内部一次比较运算的事简单直接。适合用这个方案的场景我归纳了几类短视频编辑类App里“时钟切换照片”特效指针扫一圈切图互动H5、营销页面的双图对比展示扇形滑动露出底部图片Unity UI里需要做“扫光擦除”“扇形展开”的转场效果做一些仪表盘、雷达扇区的渲染本质上也是同一套角度算法。不管做哪一种只要看完这篇的Shader部分再结合工程里的参数驱动基本都能自己改出来。2. 从像素到角度Shader核心代码逐步拆解2.1 UV坐标与角度计算的数学基础先明确一个概念UI上显示的图片每个像素的UV坐标是0到1。图片中心点的UV是(0.5, 0.5)。我第一步是把UV平移到中心点float2 uv i.uv - float2(0.5, 0.5);这样图片中心就成了(0,0)像素坐标有正有负比如图片右上角是(0.5, 0.5)左下角是(-0.5, -0.5)。接着用反正切函数算出这个点相对于中心的角度float angle atan2(uv.y, uv.x);在CG里atan2(y, x)返回的是弧度值范围是-PI到PI逆时针方向0弧度对应的是正右方向。这一步是整个Shader的灵魂把“像素在图片上的位置”问题转化成“像素相对于中心的方位角度”问题后续的所有判断都建立在这个角度值上。换算成更直观的0到360度float degree (angle / UNITY_PI) * 180.0; degree degree 0 ? degree 360.0 : degree;换算逻辑是angle从-PI到PI映射到-180到180度负值加360得到0到360的顺时针角度值。注意一点中心点本身的方向角是不定的x和y都接近0但中心点只有一两个像素对显示效果没有影响不用特别处理。2.2 角度比较与纹理切换拿到每个像素的degree之后要再拿到当前指针的旋转角度。这个值我建议从外部传入而不是写死在Shader里这样动画、交互都可以随时改变。float curAngle _PointerAngle; // 0-360外部C#控制接下来最直觉的写法是拿两个角度做差差值落在某个范围内就显示图A否则显示图B。但这里有一个新手一定会踩的坑角度差的“越界”问题。如果我的curAngle是350度某个像素的角度是10度直接相减得到340度实际它们只差20度。无论是切图边界的计算还是扇形范围的判断这个340都会导致完全错误的结果。我见过有人在Shader里写死“只能摆180度范围”就是为了避开这个坑但这样效果就局限了。正确的做法是计算“环形角度距离”float diff abs(degree - curAngle); diff min(diff, 360.0 - diff);这个公式的含义是取两个角度之间的短弧距离。350度和10度最后得到20度不管指针转到哪里边界判断都正确。这一步必须要加不加的话指针一旦从359度跳到1度分界线就会瞬间抽动视觉上直接穿帮。拿到diff之后我定义摆钟的有效扫射范围。假设这个摆钟能摆180度也就是指针从0度扫到180度那分界线的判定就是float halfSweep _SweepRange * 0.5; // 比如90度 float isA step(diff, halfSweep);step(edge, x)在CG里是经典的阶跃函数x edge返回0否则返回1。我用step(diff, halfSweep)得到的结果是像素角度距离指针角度在halfSweep范围内的返回1显示图A范围外的返回0即图A权重为1时显示图A权重为0时不显示图A。纹理混合方向看个人喜好我习惯让“指针范围内显示图A范围外显示图B”fixed4 colA tex2D(_MainTexA, i.uv); fixed4 colB tex2D(_MainTexB, i.uv); fixed4 col lerp(colB, colA, isA);到这里一个静态的双图摆钟Shader就完成了。2.3 边缘抗锯齿不能用step硬切上面代码的问题很明显step是硬边界图片A和图片B会有一条锯齿感很强的硬边。尤其在图片尺寸大、像素密度高的屏幕上斜线边缘的锯齿会被放大。我要的效果是短视频里那种“指针扫过留下一道柔和光带”所以边界处要做柔化。CG里做边缘渐变最常用的函数是smoothstep我实际项目中是这样写的float edgeWidth 2.0; // 边缘过渡宽度单位是角度 float t smoothstep(halfSweep - edgeWidth, halfSweep edgeWidth, diff); fixed4 col lerp(colB, colA, t);这段的意思是当diff小于halfSweep - edgeWidth时t为0显示图B当diff大于halfSweep edgeWidth时t为1显示图A处于两者之间时按距离平滑过渡形成一个渐变带。这个渐变带就是那道看上去很自然的扫光边缘。关于edgeWidth的取值我看网上有人推荐按像素数量换算但Shader里换算比较啰嗦。实际经验是UI尺寸在1920x1080左右时edgeWidth取1.5到3之间效果比较理想如果是小图、Icon级别的应用256x256这个值要降到1以下否则边界会显得“糊开一大片”。到这里Shader的核心部分已经完整了。我给一个完整的代码方便直接复制到工程里测试Shader Custom/PendulumImageSwitch { Properties { _MainTexA (Image A, 2D) white {} _MainTexB (Image B, 2D) white {} _PointerAngle (Pointer Angle, Range(0, 360)) 0 _SweepRange (Sweep Range, Range(0, 360)) 180 _EdgeWidth (Edge Width, Range(0.1, 10)) 2.0 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTexA; sampler2D _MainTexB; float _PointerAngle; float _SweepRange; float _EdgeWidth; v2f vert(appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag(v2f i) : SV_Target { float2 uv i.uv - float2(0.5, 0.5); float angle atan2(uv.y, uv.x); float degree (angle / UNITY_PI) * 180.0; degree degree 0 ? degree 360.0 : degree; float diff abs(degree - _PointerAngle); diff min(diff, 360.0 - diff); float halfSweep _SweepRange * 0.5; float t smoothstep(halfSweep - _EdgeWidth, halfSweep _EdgeWidth, diff); fixed4 colA tex2D(_MainTexA, i.uv); fixed4 colB tex2D(_MainTexB, i.uv); fixed4 col lerp(colB, colA, t); return col; } ENDCG } } FallBack UI/Default }需要说明的是这个Shader为了演示精简了Alpha相关逻辑。如果图片本身是带透明通道的PNG还想保留透明区域需要额外处理fixed4 col lerp(colB, colA, t); col.a max(colA.a, colB.a);否则两张图各自透明区域叠加后会出现半透明黑边。这个细节很坑后面踩坑部分再展开讲。3. 从静态到动态C#驱动指针让摆钟摆起来Shader写好之后要让它动起来。我实际项目里没有在Shader内部做时间动画而是用C#脚本更新_PointerAngle参数。这样做的好处是灵活既可以让指针自动摆动也可以让用户手指控制指针转到哪个角度对应不同的图片切换点。最简单直观的自动摆动逻辑是用正弦函数让角度在0到180度之间来回using UnityEngine; public class PendulumController : MonoBehaviour { public Material targetMaterial; public float swingAngle 180f; public float duration 2f; private void Update() { float t Mathf.PingPong(Time.time, duration) / duration; float angle Mathf.Lerp(0f, swingAngle, t); targetMaterial.SetFloat(_PointerAngle, angle); } }这里用Mathf.PingPong(Time.time, duration)获得一个0到duration之间来回变化的量再归一化成0到1最后Lerp到目标角度。效果就是指针缓慢摆动图片A和图片B在指针扫过的位置平滑切换。如果想让摆动带一点钟摆的力学感——两端慢、中间快就不能用上面的线性Lerp得改缓动曲线float pingpong Mathf.PingPong(Time.time, duration) / duration; float eased Mathf.SmoothStep(0f, 1f, pingpong); float angle Mathf.Lerp(0f, swingAngle, eased);Mathf.SmoothStep会做一个三次缓动0和1两端平缓、中间陡峭视觉上比较像是在克服重力摆动的钟摆。如果后续要支持手势操控不在Update里更新角度而是在拖拽事件里计算手指位置相对于图片中心的角度再转成0到360映射给材质。这里有一个绑定方式上的建议不要直接改targetMaterial这个引用指向的材质除非你有意让它影响同一材质的所有物体。Unity引擎里Renderer.material访问到的其实是该渲染器的一个实例化材质直接修改是安全的不会影响其他物体而挂在脚本上的public Material引用如果指向Asset资源修改会直接改掉磁盘里的材质资产。所以正确的是拿到目标渲染器后调renderer.material或者干脆把材质变量直接拖到Inspector但要注意这个坑。驱动参数的时候还有一个容易被忽略的问题CPU向GPU传Shader参数是有开销的频繁每帧SetFloat其实没问题OpenGL/Vulkan的uniform更新开销很小但要注意别在Update里无脑对着一个不会变化的参数反复Set。一般来说摆钟动画每帧更新一次角度性能完全可接受。如果是移动平台建议只在角度发生变化时才Set避免无谓的CPU到GPU数据传输。4. UI工程接入与渲染细节让Shader在UI上正确显示4.1 Unity UI的Shader改造要点上面的Shader理论上可以挂到任何Renderer上但在UGUI里使用需要多处理一层。Canvas默认是使用UI/Default这类UI Shader的如果直接给Image材质挂一个非UI类的Shader常见问题是UI合批被打断DrawCall升高不过单个Image无所谓多个UI时就要注意Shader缺少_MainTex与UI顶点色的处理Image的Color染色、透明度调整都可能失效没有处理裁剪Mask组件对非UI Shader可能不生效。解决方案有两个方向一是直接基于上面的代码写一个UI版本。UI版本的Shader顶点函数一样片元里多乘一个顶点色col * i.color并且要声明_MainTex的颜色属性这样Image组件的Color参数才能正常染色。我贴一段UI版核心片元代码改造示例fixed4 frag(v2f i) : SV_Target { // ...角度计算、混合逻辑同前... fixed4 col lerp(colB, colA, t); col * i.color; // 支持UI顶点色 return col; }还有一件事UGUI的Image所使用的纹理在Shader采样时要注意纹理的Stretch和Tiled模式确保_MainTexA和_MainTexB的Wrap Mode都是Clamp否则边缘容易出现奇怪的延伸。4.2 两图尺寸与锚点对齐做这个效果的时候一个几乎所有人都会忽略的问题是图A和图B的尺寸和位置必须完全对得上。如果没有对好指针扫过之后新图会“跳”一下——明明是同一个人物或同一个场景因为尺寸不一致切换瞬间会有回缩或位移感。我的做法是在导入贴图时先统一规格两图像素尺寸完全一致比如都是1024x1024导入设置的Pixels Per Unit一致图片的Sprite Mode都设为SingleMesh Type设为Full Rect在RawImage或Image上显示时保持两个贴图所在的UI节点位置、缩放完全一致。如果你用的是RawImage直接挂同一个节点、分别换材质或换贴图即可锚点天然一致。如果用的是Image组件加Sprite要注意Sprite的Border和Pivot设置必须一致否则也会出现偏移。另外一个工程上的小细节两个贴图在Shader里的采样坐标用的是同一个i.uv所以只要两个贴图的网格uv布局一致采样结果就能对齐。也就是说即使场景里缩放不同只要两个贴图本身像素规格一致逻辑上就不会错位。4.3 RenderQueue与Blend设置这个Shader的Tags里我写了QueueTransparentBlend SrcAlpha OneMinusSrcAlpha。如果不加Blend非透明Shader会把背景覆盖掉UI上就会像贴了一块不透明的板子。由于图片A和图片B各自可能带Alpha通道因此走透明混合是合理的选择。在UI里还有一个渲染顺序问题Canvas内部把所有UI元素按层级与顺序渲染材质排序会影响同层级下多个UI元素的前后遮挡。如果两个摆钟效果重叠在一起要注意RenderQueue一致否则可能导致被遮挡方先绘制、遮挡方后绘制出现闪烁。一般情况下Unity会自动处理Canvas内元素的排序但如果把Shader挂到3D物体上RenderQueue就必须仔细设置。5. 效果细节打磨扫光、曲线与边界过渡的实际调优5.1 让扫过边界更耐看从硬边到扫光感短视频里的摆钟效果边界通常不是纯硬边而是带着淡淡的扫光渐变扫过的地方还有细微的亮度提升。这个效果我建议不要放到图片切换Shader里强行做那样容易把图片颜色搞脏。可以叠加一个单独的扫光Shader或者在上面的Shader里再额外增加一个光亮带。我实际操作时是这样做的在已有角度差diff的基础上以边界为中心halfSweep位置生成一个高亮值float highlight exp(-pow((diff - halfSweep) / _GlowWidth, 2.0)); col.rgb highlight * _GlowColor * _GlowStrength;这个高斯函数曲线在距离边界越近时越亮越远则指数级衰减呈现一道柔和的光带。如果只是简单地在边缘用step或smoothstep做线性的亮度提升扫光会显得很生硬。高斯衰减在这里效果最好而且参数很好控制。不喜欢数学公式的话也可以退而求其次用float highlight 1.0 - saturate(abs(diff - halfSweep) / _GlowWidth);也就是三角形衰减。视觉上会带一点硬棱角胜在直观。5.2 多图扩展按区间切图如果想把两张图扩展到四张、六张让指针摆动时依次切换逻辑上很自然把0到360度划分成若干区间每个区间对应一张图。比如四张图每个区间90度指针在0到90度显示图190到180度显示图2以此类推。实现上可以不把多张图都通过参数传进去那样需要声明很多纹理属性而是做一个“索引映射”先根据当前像素角度算出一个索引值然后动态决定采样哪张贴图。但Shader里动态采样不同纹理得写多个采样分支或者用Texture2DArray。用Texture2DArray更优雅不过低端移动端兼容性会差一点。我的建议是如果场景是固定的几张图直接声明四个纹理属性分别采样然后用分支或lerp组合输出即可。Unity的GPU分支在顶点片元Shader里效率尚可只要分支条件是统一的不依赖屏幕坐标而是依赖固定的角度区间性能损耗可以接受。关于分支要提醒一件事GPU分支和CPU分支不同if语句在GPU里不一定高效尤其是依赖像素坐标的分支会导致warp分叉。但纹理切换时同一时钟内像素是连续分布在几个区间的GPU分支的分叉概率并不大不必过度优化。用lerp写法反而代码可读性更差。这点实测下来可以放心写if。5.3 边缘过渡的“物理正确”问题做摆钟效果时图片A和图片B在边界处不是简单的Alpha交叉渐变因为这两张图的颜色完全不同交叉渐变会出现重影像两张图叠印在一起。我看到很多人直接写lerp(colA, colB, t)结果边界处颜色发灰、发虚。这是最常见的画质问题。两种修正方式方式一把渐变带压缩窄一点让两张图的过渡非常锐利几乎只有1到2个像素的渐变。适合“硬切换加轻微抗锯齿”的定位。_EdgeWidth取0.5左右。这种方式视觉上接近指针扫过就立即切图没有中间调。短视频里大部分是这种。方式二给渐变带加一个“溶解”处理用噪声纹理打散过渡区域。适合想要一点风化、撕裂效果的需求。核心是引入噪声纹理在过渡带内对图片A和图片B做抖动选择float noiseVal tex2D(_NoiseTex, i.uv * _NoiseScale).r; float threshold t; float isA step(threshold, noiseVal);像边缘燃烧的纸一样图片A被一点点侵蚀成图片B。这个是额外的玩法如果不是做这个方向可以忽略。5.4 性能优化与移动端适配这个Shader在移动端的开销主要集中在两处atan2运算和双纹理采样。atan2本身在GPU里是硬件指令级支持或者编译器会优化成几条ALU指令不算太贵。真正贵的其实是两张1024x1024纹理的采样如果是UI叠加在复杂场景上片段着色器每像素要采两次纹理会增加带宽压力。如果是低端机且画面上同时有多个摆钟可以考虑预合并纹理或者把图A和图B合成一张双通道纹理比如把A放在RGB通道、B放在Alpha通道只用一次采样然后内部解包。不过这样会牺牲反锯齿和颜色精度能不用尽量不用。关于低端机的精度问题还有一个挺隐蔽的坑移动端部分GPU对atan2返回值的精度不高特别是half精度模式下OpenGL ES 2.0的mediump。如果发现边界在移动端出现抖动或轻微偏移就检查一下Shader里有没有把相关的float变量用half声明。我建议在高风险Shader里全部用float声明角度相关变量只在最终输出的颜色值上用fixed4这样能避免不少玄学精度坑。6. 实测中容易踩的坑与排查方法6.1 摆钟指针从359度跳到1度时画面闪动这个问题我在第2节已经提过原因角度差没有取短弧距离。但实际项目中除了diff min(diff, 360.0 - diff)这个修正还牵涉到_PointerAngle本身在跨越0度时不要突然跳变。比如C#脚本里如果直接用Mathf.Lerp(350f, 10f, t)它会从350开始升到10实际绕过360到370再被Shader取模Unity的Mathf.Lerp对角度插值不友好应该用Mathf.LerpAngle或者Mathf.MoveTowardsAngle。这是工程层和Shader层都要注意的双保险问题。我实际项目里最后是这么处理的C#脚本里维护一个连续角度变量不做0/360取模只在设置给Shader时才取模360。这样指针从350走到10C#端连续递增到370Shader端收到370后取模得到10整个过程中边界完全不会跳变。这是我认为最稳的方案。6.2 图片切换后有半透明黑边这个坑出现在带Alpha通道的PNG上。现象是两张图交界处隐约有一条黑色的半透明细线或者图片边缘出现黑晕。原因是两张图的Alpha通道各自独立在图A区域里图B的Alpha是0在图B区域里图A的Alpha是0lerp结果的Alpha如果直接取混合后的值边缘处的Alpha就接近0.5而颜色值等于两个颜色的平均底色越深越容易产生黑灰边。我在第2节的代码里给过一个简单方案col.a max(colA.a, colB.a);。但更稳妥的方式是在进入混合之前就避免出现半透明边缘。具体做法是先用step明确把两个像素的Alpha区分开colA.a * isA; colB.a * (1.0 - isA); fixe4 col colA colB;这个写法的思路是每个像素要么完全归图A要么完全归图B而不是两个半透明像素叠加。虽然边缘的抗锯齿会丢失一点但只要把smoothstep的过渡宽度保持到2到3个像素抗锯齿效果依然在。这个方案对带Alpha的圆形、异形图片素材特别重要。6.3 UI缩放后边界模糊在Canvas的缩放模式是Scale With Screen Size时Image本身的像素尺寸可能和实际屏幕像素不是1:1。如果Shader里的_EdgeWidth是固定的绝对角度值那么在像素密度不同的屏幕上边界过渡的实际像素宽度会不同在小屏上过渡窄、显得锐利在大屏上过渡宽、显得模糊。想彻底解决得知道屏幕的实际尺寸和UI元素的尺寸把_EdgeWidth从角度值换算成像素值。这一步比较麻烦。实际项目里如果只是追求短视频那种效果我反而建议不要换算——因为不同手机上看本身就是不同像素密度的模糊一点反而更接近真实光效不需要过分纠结。但如果你的UI要同时适配手机和Pad且希望效果完全一致那就得在C#侧动态计算EdgeWidthfloat pixelScale (float)Screen.height / referenceScreenHeight; material.SetFloat(_EdgeWidth, baseEdgeWidth * pixelScale);这样大屏的EdgeWidth变大过渡的像素宽度相对一致。工程上不复杂就是别忘了在OnGUI或分辨率变化事件里更新。6.4 Material实例化引发的“改动所有物体”事故我第3节提过Unity的材质实例化机制这里展开细说。很多人在C#里想动态改变某个UI Image显示的图片会这样写image.material.SetFloat(_PointerAngle, angle);image.material这个属性本身返回的就是一个实例化材质所以这样写没有问题修改不会影响其他Image。但是如果项目里几个摆钟复用同一个材质资产你直接在Inspector里拖拽那个材质资产到脚本引用然后targetMaterial.SetFloat就会把材质资产给改了。后面其他摆钟引用这个资产的全部跟着变。正确做法是在Awake里先做一个renderer.material renderer.material的显式实例化或者用MaterialPropertyBlock。MaterialPropertyBlock其实更适合这种“同一材质、不同实例参数”的场景。它的好处是不会产生材质实例也不打断合批性能更好var props new MaterialPropertyBlock(); renderer.GetPropertyBlock(props); props.SetFloat(_PointerAngle, angle); renderer.SetPropertyBlock(props);但MaterialPropertyBlock对UGUI的Image支持不太好UI合批和材质属性块在某些版本有bug如果是3D物体强烈推荐用PropertyBlock如果是UGUI就老老实实实例化材质然后自己在销毁时记得释放。7. 进一步扩展的几种玩法Shader方案的优势就在于视觉效果的基础逻辑打通后扩展空间很大。最后分享几个我实测过比较有意思的方向。玩法一摆钟 光追式扫光。给摆钟扫过的区域额外叠一层高光渐变模拟镜头光晕扫过画面的感觉只需要在片段Shader里把高光颜色加给col.rgb。这个适合照片展示、相册翻页类应用能让切换瞬间有一种“被光线拂过”的质感。玩法二双图切换变成三图衔接循环。把角度区间切成三段指针摆过去切一次摆回来再切一次两张图片在摆钟的两侧各占半区。加上多个Region的过渡带处理视觉上可以做一个完整的“来回扫动切换”效果。比如初始是图A全覆盖指针从0扫到120度扫过区域变成图B继续到240度变成图C再摆回0度又恢复图A。我记得之前做展示类Demo时这种方案被客户夸过“像高端发布会的感觉”。玩法三将角度算法迁移到3D模型。不只在UI平面上用把贴花ShaderDecal Shader跟角度切换结合在3D物体的表面做局部擦拭显示。做产品展示时可以在物体表面模拟“刷子扫过新外观出现”的效果。实际上只需要把UV采样坐标换掉即可核心的角度判断逻辑完全复用。有一个玩法我没有试成功过就是让摆钟边界跟着图片的明暗区域走而不是严格的直线。理论上可以用明度图对角度差做扰动让边界变成曲线比如float disturbance tex2D(_NoiseTex, i.uv).r * _DisturbanceStrength; float effectiveDiff diff disturbance;但视觉上很难控制曲线形状容易变成“烂边”。如果哪位读者做成功了欢迎分享经验我倒是挺想看看的。8. 结尾再啰嗦两句这个效果做下来花在Shader上的时间其实很少大部分时间花在调UI、调参数、对素材对齐上了。短视频里的摆钟切换很多人以为用了视频蒙版或人像分割实际上就是一张Shader材质的事情。核心知识点无非三点一是把UV坐标换成以中心为原点的角度坐标二是用环形角度距离处理0/360跨越三是用smoothstep控制边界柔和程度。掌握这三板斧之后不只摆钟切换扇形菜单、轮盘选中高亮、雷达扫描图、双图滑动对比都能顺藤摸瓜地做出来。最后分享一个我个人的调试技巧在Shader Preview面板或者运行时先用纯色替代图片A和图片B纯红和纯蓝这样能非常直观地看到角度边界是否正确、扫光范围是否合适而不会被图片内容干扰视觉判断。确认逻辑没问题后再替换回真实图片。一个简单的习惯能省下不少调参的冤枉时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑