资讯动态

用Shader让静态图片实现3D旋转:UV极坐标与伪3D圆环效果

发布时间:2026/9/6 8:06:05 来源:尧图企业网站定制
有一次做活动页美术丢给我一张 PNG画面中央是个带金属质感的 3D 圆环高光、倒角、阴影全都在但它是死的。需求很离谱——让它自己在页面里转起来最好还有空间感。用序列帧的话得导几十张图用视频的话加载慢透明通道还难处理。后来我花了一个小时用一个不到一百行的 Shader 把问题解决了。这个方法说白了就是不生成任何新的美术资源不引入模型和骨骼只靠 Fragment Shader 操作 UV 坐标就能让一张静态圆环贴图看上去像在 3D 空间里自转。游戏 UI 里的技能充能环、活动页里的发光涡轮、网页 Banner 里的旋转齿轮都是这么干的。不管你是做前端、游戏客户端、TA还是单纯想给 H5 页面加点“活气”这套思路都值得收藏。1. 效果拆解用户眼里的 3D其实是 UV 在骗人1.1 一张静态图怎么让大脑觉得它在转 3D先说说最终视觉上发生了什么。你把一个圆环贴图放到页面上它本身是一个平面的圆形图片但素材里往往带着高光、倒角、阴影、渐变。比如左上角亮、右下角暗边缘还有一条厚度过渡带。这些东西单独看是死的一旦它们绕中心旋转起来大脑就会自动脑补出一个立体的环在转。所以核心不是真的建立三维几何体而是让贴图的“采样位置”动起来。每个像素原本读的是图片上的某个点这个点的坐标叫 UV。原来 UV 是死的画面上就是原图如果给 UV 加一个随时间变化的角度偏移画面上的高光、纹理、颗粒就会沿着圆环走旋转效果就出来了。这一步做好之后再微微调整半径和明暗让高光在转到不同角度时有大有小、有亮有暗3D 感就出来了。1.2 为什么不用序列帧、视频或者直接上 3D 模型我把几个方案对比过一遍最后才选 Shader。方案性能包体/素材量易用性缺点序列帧中大几十张图简单但不可控资源量爆炸循环不自然视频/GIF低中大简单透明通道难处理播放控制弱真实 3D 模型看平台模型贴图复杂需要建模、烘焙、场景搭建Fragment Shader高几乎为零改动一个 shader 即生效需要一点数学基础如果你只是要让“图片里的圆环转起来”真实 3D 模型反而是最重的方案。做个圆环面模型、加材质、打灯光、烘焙阴影这套链路在移动端上未必划算。Shader 的好处是素材不变GPU 每帧帮你重新计算采样位置零额外包体还天然支持透明通道。2. 核心数学从像素坐标到旋转角度的秘密2.1 先把 UV 挪到原点再变成极坐标在 Fragment Shader 里每个像素都会执行一次函数。我们拿到的uv是当前像素在图片上的位置范围通常是 0 到 1。圆环的圆心不一定在(0,0)所以要先把坐标平移到以圆心为中心。假设圆心在(0.5, 0.5)当前像素相对圆心的偏移量就是vec2 p uv - vec2(0.5, 0.5);拿到这个偏移量之后用两个值描述它一个是到圆心的距离r一个是角度a。这就是极坐标float r length(p); // 半径 float a atan(p.y, p.x); // 角度范围是 -PI 到 PI为什么要转成极坐标因为圆环自转本质上是“角度变了半径不变”。如果直接对 UV 的 x、y 做平移或缩放根本得不到绕中心的旋转效果圆环会像摊大饼一样被拉变形。极坐标天然贴合“绕圆环走”这件事。2.2 绕中心旋转给角度加上时间接下来最简单的一步让角度随时间增加a u_time * speed;然后把这个新角度转回直角坐标用来从原图里采样vec2 sampleUV vec2(0.5, 0.5) vec2(cos(a), sin(a)) * r; vec4 color texture2D(u_texture, sampleUV);这样原图上某个角度的内容就被“搬”到了新的角度。只要原图上有高光、花纹、明暗渐变它们就会沿着圆环一圈圈跑起来。这就是 2D 圆环自转的基本原理。2.3 加一点伪 3D让半径和明暗随角度波动如果只是让高光绕圈转很多人会觉得“这不就是旋转嘛也没多 3D”。要让视觉上更像一个立体圆环得再做两件事。第一件让半径微微波动。真实世界里的圆环一旦带一点倾角离你近的一侧和远的一侧在屏幕上的宽度和位置会有细微差别。我们可以让半径乘一个带角度关系的正弦波r * 1.0 0.06 * sin(a * 2.0 u_time * 1.5);这个公式的意思是不同角度上的圆环宽度不一样而且这个“不一样”还在随时间流动。看起来就像圆环在翻转、倾斜而不是一个平面圆盘死板地转。第二件让明暗跟着角度变化。圆环上方的材质高光在转到不同角度时亮度应该有差异。我习惯在采样后再乘一个亮度系数float light 0.85 0.15 * cos(a u_time * 2.0); color.rgb * light;这样会有一种“光源固定环在转”的错觉3D 感一下就上来了。做这个效果时我踩过的坑是亮度系数不能调太大不然会闪成霓虹灯一般控制在 0.75 到 1.15 之间最自然。3. 可直接抄的 Shader 实现3.1 通用 GLSL 版本WebGL / three.js 都能用下面这段是脱离引擎的 Fragment Shader 写法只要你项目里能传u_texture和u_time两个 uniform基本可以直接用。precision highp float; varying vec2 v_uv; uniform sampler2D u_texture; uniform float u_time; void main() { vec2 uv v_uv; vec2 center vec2(0.5, 0.5); vec2 p uv - center; float r length(p); float a atan(p.y, p.x); // 自转速度 a u_time * 1.2; // 伪3D半径波动 r * 1.0 0.06 * sin(a * 2.0 u_time * 1.5); // 转回直角坐标去采样原图 vec2 sampleUV center vec2(cos(a), sin(a)) * r; // 采样范围保护防止跑到贴图外面 if (sampleUV.x 0.0 || sampleUV.x 1.0 || sampleUV.y 0.0 || sampleUV.y 1.0) { gl_FragColor vec4(0.0, 0.0, 0.0, 0.0); return; } vec4 color texture2D(u_texture, sampleUV); // 模拟光源扫过 float light 0.85 0.15 * cos(a u_time * 2.0); color.rgb * light; gl_FragColor color; }在 three.js 里接这段代码时注意 ShaderMaterial 的 uniformsconst material new THREE.ShaderMaterial({ uniforms: { u_texture: { value: texture }, u_time: { value: 0 } }, vertexShader: vertexShader, fragmentShader: fragmentShader });每帧更新u_time.value clock.getElapsedTime();就行。3.2 Unity ShaderLab 版本UI 上用最方便Unity 里做 UI 特效时我更常用带属性的写法。这样能直接在 Inspector 面板上调速度、调伪 3D 强度不用改代码。Shader Custom/RotatingRing2D { Properties { _MainTex (Texture, 2D) white {} _Speed (Speed, Range(-3.0, 3.0)) 1.0 _Tilt (Tilt, Range(0.0, 0.2)) 0.08 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; float _Speed; float _Tilt; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { float2 uv i.uv; float2 center float2(0.5, 0.5); float2 delta uv - center; float r length(delta); float a atan2(delta.y, delta.x); a _Time.y * _Speed; r * 1.0 _Tilt * sin(a * 2.0 _Time.y * 2.0); float2 sampleUV center float2(cos(a), sin(a)) * r; if (sampleUV.x 0.0 || sampleUV.x 1.0 || sampleUV.y 0.0 || sampleUV.y 1.0) discard; fixed4 col tex2D(_MainTex, sampleUV); return col; } ENDCG } } }用的时候把_MainTex拖进静态圆环图材质放到 UI 的 Image 上即可。要注意把 Image 的材质改成这个 Shader并且图片类型设置为 Sprite否则 UI 系统可能不会按预期渲染。3.3 Cocos Creator / Shader Graph 里怎么改Cocos Creator 的写法类似把_Time.y替换成cc_time.x即可。Cocos 里自定义 Shader 时我会把噪声、渐变、UV 旋转分开写在不同的函数里方便和现有的 label 渐变 Shader、粒子材质叠加。这个“极坐标 时间”的思路和 Cocos 里做 label 渐变 Shader 时操作 UV 的套路是同一个底层逻辑。如果你用 Unity Shader Graph不用写代码也能实现先把 UV 节点 Split分别接Length和Atan2得到极坐标Time 节点乘速度后加到角度上再用Cosine、Sine和乘法重组 UV最后接 Sample Texture 2D。只是节点相对繁琐调参体验不如代码版本直接。4. 踩坑与常见问题排查4.1 为什么圆环越转越偏明显不是绕中心转八成是中心点算错了。检查一下 UV 中心到底是不是(0.5, 0.5)。如果你的贴图在图集里带 padding或者 Sprite 的 pivot 不在中心实际中心可能不是 0.5。Unity 里做图集后会重排 UV直接用i.uv的话中心会偏。解决办法是在 Shader 里从_MainTex_TexelSize或图集的 rect 算出真正的中心或者干脆把素材单独拖成独立贴图让 UV 范围固定为 0 到 1。另一个原因是宽高比。如果 UI 显示区域不是正方形圆形会变成椭圆旋转中心看起来也会歪。如果你要严格正圆在算r之前把 UV 按宽高比矫正vec2 p (uv - center) * vec2(aspect, 1.0);但是这样采样回去时要注意映射关系。一般来说UI 控件如果强制保持 1:1就不需要这一步。4.2 透明区域出现黑边、灰边这个坑困扰了我很久。圆环素材大多是透明背景但 PNG 在边缘往往会残留一圈偏暗的颜色。旋转之后采样到了这些边缘像素结果画面上出现脏边。我常用的两种解法第一种采样之后把 RGB 乘以 Alpha让半透明区域的 RGB 不参与显示col.rgb * col.a;第二种在 alpha 非常低时直接 discardif (col.a 0.05) discard;具体用哪种取决于素材。如果你用的是 Premultiplied Alpha第一种更合适如果是 Straight Alpha第二种更稳妥。实在不行就让美术重新导一份带透明通道的 PNG比在 Shader 里硬纠正好得多。4.3 转起来像“滚筒洗衣机”不像圆环自转多半是那个伪 3D 半径波动幅度太大了。_Tilt一旦超过 0.1整个圆环会像被风吹皱的塑料膜变形感很重。我一般会把_Tilt控制在 0.02 到 0.08 之间。如果你要的是严谨的 3D 透视翻转光靠 Fragment Shader 改 UV 是不够的。那种情况建议把圆环放到一个实际旋转的四边形 mesh 上或者直接用模型渲染。还有一种是“转起来像平面旋转完全没有立体感”。这种时候可以加大明暗扫光的幅度让高光更明显。人的视觉对亮度变化的敏感度远高于几何细微的变形所以light系数的效果比_Tilt更直接。4.4 移动端低端机发热、掉帧Shader 本身花不了多少 GPU但 Fragment Shader 里的atan2、sin、cos在低端 GPU 上还是有点贵。尤其是全屏大尺寸 UI 时每一个像素都做一次极坐标运算压力不小。我实际测试下来只要控制 Shader 的作用区域不要铺满全屏通常没问题。如果必须全屏建议把渲染目标切成低分辨率 RenderTexture再从后处理阶段放大显示看起来没什么差别帧率却能稳不少。也可以在 Shader 里用half代替float减少精度开销。但有一点要注意移动端不同 GPU 对half的支持不完全一样极端情况下会出现精度不够导致的闪烁上线前最好真机测一圈。4.5 常见问题速查表现象原因解决方案圆环偏心转动UV 中心算错 / 图集 padding固定 UV 范围用纹理中心点 0.5,0.5旋转出现脏边PNG 透明边缘有色col.rgb * col.a或 alpha 阈值 discard变形太夸张伪 3D 波动幅度太大_Tilt调到 0.02~0.08减小幅度移动端发烫大量 sin/cos 操作缩小作用区域用低分辨率 RT转起来像滚筒没有同时做周期明暗增加light系数让高光扫过三个引擎里行为不一致uniform 变量名冲突分开封装避免直接依赖全局时间变量最后再分享一个小技巧。我在做这类 UI 特效时习惯把时间u_time先乘以一个全局速度因子而不是直接写死在 Shader 里。这样暂停按钮、点击加速、倒放都能复用同材质。你只需要在外部把时间传给 Shader改起来就很灵活。这个习惯帮我省了很多返工因为美术和策划永远会在最后一天告诉你“环可以再转快一点。”

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

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

免费获取报价