ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cocos Creator 3.x 光球流光 Shader 手写教程:从原理拆解到工程落地

Cocos Creator 3.x 光球流光 Shader 手写教程:从原理拆解到工程落地 光球流光这种效果在卡牌大招、技能爆发、抽卡转场里几乎天天见。很多项目第一反应是做序列帧但换一套配色就要重新出图改个速度还要反复调动画。我自己的习惯是直接上 Shader一张白模、一个材质颜色、流速、条纹密度全部实时调出效果的时间能压缩到序列帧的十分之一。这篇就用 Cocos Creator 3.x 手写一个光球流光 Shader从数学拆解到完整 Effect 文件再到调参和避坑一步步看完就能直接搬到项目里。这篇文章适合两类人一类是刚接触 Shader想知道流光效果到底由哪几层合成的新手另一类是已经在项目里用序列帧做这类特效想换 Shader 方案但又怕踩坑的实操派。我会把最终代码放在第二部分但强烈建议先把第一部分的原理看完不然遇到效果不对你连该调哪一行都不知道。1. 效果拆解一个看似复杂的光球其实只有四层逻辑1.1 先想清楚为什么流光必须用 Shader先解决一个根本问题光球流光到底能不能用序列帧做能做但代价很高。假设你需要 10 帧循环每个技能 3 种颜色变体那就是 30 张图。再遇到不同屏幕分辨率特效图还得单独处理。而 Shader 方案只需要一张纯白图甚至直接用 Sprite 的原始 UV所有变化都在运行时计算。更关键的是Shader 能跟游戏逻辑联动。比如角色释放技能时瞬爆流光速度从 0.5 加速到 3.0序列帧很难做到这种连续响应但 Shader 里就是一个 uniform 的事。框架层接入Material.setProperty即可实时改。这也解释了为什么现在中等以上品质的项目这类光效几乎全走 Shader。1.2 第一层用距离场画一个软边圆任何特效的第一步都是确定形状。光球得是一个圆形而且边缘要柔和不能是硬邦邦的圆盘。这就用到 UV 平面上的距离场概念。假设纹理的 UV 范围是 0 到 1球心放在center vec2(0.5, 0.5)。像素到球心的距离就是float d distance(uv, center);d 最小是 0球心最大能到约 0.707顶点。接下来用smoothstep把距离变成遮罩Mask让圆内是 1圆外是 0中间过渡带是 0 到 1float ballMask 1.0 - smoothstep(radius - softness, radius softness * 0.5, d);radius控制球的大小softness控制边缘过渡宽度。softness越大边缘越虚光球看起来越像气态调小了就是一颗硬边实心球。这一步多用生活化类比理解就像拿软橡皮擦铅笔画的圆擦得越用力边缘越虚。1.3 第二层径向渐变让球鼓起来光球不是均匀的色块它应该中心亮、边缘暗这样才有立体感。径向渐变的公式我习惯写成float radial pow(clamp(1.0 - d / radius, 0.0, 1.0), 2.0);这个式子好懂d 等于 0 时1 - d/radius是 1亮度最高d 接近 radius 时数值趋向 0。再做一个平方运算是为了让亮度衰减得更快中心更集中。指数可以继续调大比如pow(..., 3.0)球心会变成一个很亮的光核边缘迅速暗下来。这个光核再往下叠加做出光球受光的感觉非常够用。很多新手以为光球要有 3D 光照模型其实 2D 特效里用这种径向渐变就能骗过眼睛。1.4 第三层流光条纹数学上就是一个移动的波浪流光是最有意思的一层。它的本质是沿某个方向把一个周期函数切出来再让相位随时间移动。先把 UV 坐标投影到流光的运动方向dir上vec2 dir vec2(cos(angle), sin(angle)); float proj dot(uv - center, dir);这个proj可以理解成每个像素在流光方向上的位置。如果dir是水平方向(1, 0)proj就是uv.x - center.x。斜向流光就是把坐标系旋转了一下。然后引入正弦函数生成条纹float phase (proj time) * density; float stripeRaw 0.5 0.5 * sin(phase * 6.2831853); float stripe pow(stripeRaw, width);相位随时间整体平移条纹就会朝dir方向滚动。density控制条纹条数width是指数控制条纹粗细。这个操作的本质是先把连续的正弦波叠出来再用指数把波峰附近以外的部分压暗形成一条亮带的视觉。指数越大亮带越窄越锐利。别忘了最关键的一步流光必须被限制在球内。所以最终要乘以ballMask否则条纹会延伸到矩形纹理外面非常难看。1.5 第四层边缘辉光让光球溢出边界球本身有了还差点氛围感。光球应该像能量源一样在边缘往外渗光。这个效果我用一段独立的边缘光公式float edge pow(1.0 - smoothstep(radius * 0.7, radius softness, d), 3.0);它在圆半径附近形成一个窄窄的亮环向外逐渐衰减。配合加色混合会让光球边缘有一圈泛光。这一圈辉光处理得好不好直接决定了特效是干净的光球还是廉价贴图。2. 手写一个可用的 Cocos Creator 3.x Effect 文件2.1 Effect 文件的结构YAML 声明 两段 GLSLCocos Creator 3.x 的 Shader 文件沿用.effect格式核心是三部分CCEffect 声明段YAML、顶点着色器 CCProgram vs、片段着色器 CCProgram fs。YAML 段里最重要的是properties。它定义了材质面板上能调整的参数并且要求每个属性名必须在 fs 的 uniform 块里能找到对应成员。如果把stripeParams写进 YAML但 GLSL 的 uniform 块里没有这个名字材质编译会直接报错。我习惯把所有参数打包成vec4因为引擎对 vec4 的传输和缓存效率最好这也是项目里常见的做法。2.2 完整代码直接复制即可运行下面给出我验证过可用的版本。新建一个.effect文件把内容整个贴进去再创建材质拖到 Sprite 上就能看到效果。// LightBallFlow.effect CCEffect %{ techniques: - passes: - vert: vs frag: fs blend: src: one dst: one rasterizerState: cullMode: none properties: center: { value: [0.5, 0.5, 0.0, 0.0], editor: { type: vec4, tooltip: 球心位置(UV坐标) } } radiusSoft: { value: [0.42, 0.06, 0.0, 0.0], editor: { type: vec4, tooltip: x半径 y边缘柔化 } } coreColor: { value: [1.0, 0.85, 0.6, 1.0], editor: { type: vec4, tooltip: 球心高光色 } } glowColor: { value: [0.15, 0.45, 1.0, 1.0], editor: { type: vec4, tooltip: 边缘辉光色 } } stripeColor: { value: [1.0, 1.0, 1.0, 1.0], editor: { type: vec4, tooltip: 流光主色 } } stripeParams: { value: [0.6, 5.0, 6.0, 0.6], editor: { type: vec4, tooltip: x速度 y条纹密度 z条纹宽度指数 w流光方向角度 } } }% CCProgram vs %{ precision highp float; #include cc-global in vec3 a_position; in vec2 a_uv0; out vec2 v_uv0; vec4 vert () { vec4 pos vec4(a_position, 1.0); v_uv0 a_uv0; return cc_matViewProj * pos; } }% CCProgram fs %{ precision highp float; #include cc-global in vec2 v_uv0; uniform LightBall { vec4 center; vec4 radiusSoft; vec4 coreColor; vec4 glowColor; vec4 stripeColor; vec4 stripeParams; }; vec4 frag () { vec2 uv v_uv0; vec2 c center.xy; float radius radiusSoft.x; float soft radiusSoft.y; // 距离场与球体遮罩 float d distance(uv, c); float ballMask 1.0 - smoothstep(radius - soft, radius soft * 0.5, d); // 径向渐变球体高光 float radial pow(clamp(1.0 - d / radius, 0.0, 1.0), 2.0); // 流光条纹 float t cc_time.x * stripeParams.x; vec2 dir vec2(cos(stripeParams.w), sin(stripeParams.w)); float proj dot(uv - c, dir); float phase (proj t) * stripeParams.y; float stripeRaw 0.5 0.5 * sin(phase * 6.2831853); float stripe pow(stripeRaw, stripeParams.z); // 边缘辉光 float edge pow(1.0 - smoothstep(radius * 0.7, radius soft, d), 3.0); // 颜色叠加 vec3 col vec3(0.0); col glowColor.rgb * edge * 1.2; col coreColor.rgb * radial * ballMask; col stripeColor.rgb * stripe * ballMask * 1.8; return vec4(col, 1.0); } }%cc_time.x是引擎内置的时间变量单位秒无需自己传但必须在 fs 里#include cc-global才能访问。这里我用加色混合src: one, dst: one核心原因是流光和辉光属于亮色叠加加色混合可以让多层光效相加暗部不会发灰发脏。如果你希望光球有半透明边缘可以改用src: src_alpha, dst: one但注意此时返回的 alpha 必须正确否则边缘会发黑。2.3 从文件到画面创建材质和挂载 Sprite代码写完还不能立刻看到效果需要走一遍资产流程步骤其实很快在资源管理器里新建 Effect把上面的代码粘贴进去。新建 Material在 Effect 属性里选择 LightBallFlow。场景里新建一个 Sprite把 SpriteFrame 设成任意纯白图没有的话可以用内置默认 SpriteFrame。选择 Sprite 节点在 Sprite 组件的 Custom Material 中挂上刚才创建的材质。建议场景背景放一张深色图黑色或深蓝色都行。因为加色混合在浅色背景上会泛白看不清层次。如果手头没有纯白图可以先用一个 1x1 的白色 PNG效果完全一样。3. 关键参数与调优让效果真正适配你的项目3.1 每个参数的调节思路和三档手感我把材质 Inspector 面板里的参数全部列成一张速查表这是我在项目里调特效时最常参照的东西。别小看这张表很多新手拿到代码乱调一通最后效果不对其实是参数之间互相影响。参数范围建议手感说明center.x / center.y0.35 ~ 0.65球心位置配合图集裁切时使用radiusSoft.x0.35 ~ 0.48半径越大球越大超过 0.5 会超出纹理边界radiusSoft.y0.02 ~ 0.12柔化值越小边越硬越大越像气态光团stripeParams.x0.3 ~ 1.0流光滚动速度攻击演出前段建议偏快stripeParams.y3 ~ 8条纹密度数字越大条数越多stripeParams.z4 ~ 12条纹粗细数字越大条纹越细越锐利stripeParams.w0 ~ 3.14流光方向0 是水平方向1.57 是垂直方向我最常用的三档预设是这样大招蓄力半径 0.45、柔化 0.10、速度 0.4、密度 4、宽度 6、角度 0.6。技能爆发的瞬间半径 0.48、柔化 0.08、速度 2.0、密度 6、宽度 8、角度 1.2。抽卡转场装饰半径 0.38、柔化 0.12、速度 0.8、密度 3、宽度 4、角度 0.3。注意条纹密度和宽度指数是关联的。密度调大以后条纹数量变多如果宽度指数没有跟着提高整个球会糊成一片。反过来密度小的时候宽度指数调太大球面可能几乎看不到条纹。所以调参时先把密度固定再动宽度指数最后回头微调速度。3.2 混合模式的选择加色还是半透明Effect 里的blend块决定最终颜色怎么画到屏幕上。我用的是blend: src: one dst: one这是最简单的加色混合最终颜色等于源颜色加上目标颜色。它的好处是亮色叠加后更亮暗部几乎不残留特别适合深色背景上的能量系特效。如果游戏界面是浅色调比如某些日系卡牌的白色抽卡界面加色混合会把整个区域冲得很白。这种场景建议改成blend: src: src_alpha dst: one这样颜色输出前乘以 alpha透明度能够控制叠加强度避免过曝。代码里return vec4(col, 1.0)的 alpha 也要跟着调或者直接给 col 的亮度乘以一个强度系数。我项目里习惯再暴露一个intensity参数调 0.8 到 1.2 之间比临时改混合模式方便得多。3.3 配色的底层逻辑别把同一颜色全堆进去光球好不好看七成靠配色。最常见也最稳的结构是中心高光用暖色边缘辉光用冷色流光用介于两者之间的过渡色。举个例子中心coreColor给乳白色偏金(1.0, 0.9, 0.7)边缘glowColor给能量蓝(0.2, 0.5, 1.0)流光stripeColor给淡蓝白(0.9, 0.95, 1.0)。这样球心是亮核边缘透出冷光视觉层次立刻拉开。如果三个颜色都是冷的或者都是暖的效果会平。这是做材质配色时一个非常实用的经验。4. 从光球到完整技能特效三个常用扩展4.1 让流光自动旋转模拟蓄力能量很多技能里的光球条纹是围绕球心旋转的。现在的代码里角度是固定 uniform改起来有两种方式一种是在 Shader 内部写stripeParams.w cc_time.x * rotateSpeed这样流光方向会自动转但这样做会破坏原方向参数的可控性不太推荐更好的方式是每帧在脚本里更新材质属性。import { _decorator, Component, Material, Vec4 } from cc; const { ccclass, property } _decorator; ccclass(LightBallCtrl) export class LightBallCtrl extends Component { property(Material) mat: Material null; angle 0; update(dt: number) { this.angle dt * 0.8; const cur this.mat.getProperty(stripeParams) as Vec4; this.mat.setProperty(stripeParams, new Vec4(cur.x, cur.y, cur.z, this.angle)); } }每次把新的Vec4写回去会有一点开销但光球数量一般不多性能可以接受。如果同屏光球非常多比如几十个建议每个球用一个独立的 Material 实例避免共享材质导致全部一起被改。4.2 用噪声扰动条纹边缘去掉塑料感正弦条纹做出来的流光边缘非常光滑看久了会觉得有点塑料。想要更自然的能量流动感可以在投影计算里加入一点点噪声扰动。先在 fs 里加一个简单的哈希噪声函数float hash21(vec2 p) { p fract(p * vec2(123.34, 456.21)); p dot(p, p 45.32); return fract(p.x * p.y); } float noise(vec2 p) { vec2 i floor(p); vec2 f fract(p); vec2 u f * f * (3.0 - 2.0 * f); float a hash21(i); float b hash21(i vec2(1.0, 0.0)); float c hash21(i vec2(0.0, 1.0)); float d hash21(i vec2(1.0, 1.0)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); }然后修改条纹相位计算float projNoise proj (noise(uv * 4.0 vec2(t * 0.5, t * 0.3)) - 0.5) * 0.08; float phase (projNoise t) * stripeParams.y;uv * 4.0控制噪声的密度数字越大扰动越碎乘以 0.08 控制扰动幅度幅度太大会让条纹变形到认不出来。这个技巧同样适用于粒子特效里的扭曲扰动核心思路在这里都是通用的。4.3 叠加粒子与多重流光做出爆发感光球 Shader 本身解决了球的形态和光流的动态但要撑起一个完整的技能演出还需要外部组合。我实际项目里的做法分三层第一层用这个光球 Shader 做核心能量体放在技能中心。第二层叠加一个粒子系统粒子用同色系但更亮的纹理从球心向外喷射用来补充动态爆发感。第三层在球体上方叠加一个环形光圈用另一个带圆环的 Shader或者直接用拉伸过的 Sprite制造能量环锁定的感觉。这三层都用同一个主色调但要拉开透明度。核心球和粒子是主角光圈做点缀层次就出来了。千万不要三层都一样亮那是新手最容易犯的错误。5. 常见问题与排查实录5.1 问题速查表现象原因解决办法物体渲染全黑fs 里变量未初始化或 uniform 块成员名与 properties 不一致检查uniform LightBall内的变量名是否和 YAML properties 完全一致完全看不到球材质没挂上、SpriteFrame 是空的、混合模式不对确认自定义材质已赋给 Sprite并给一个有效的 SpriteFrame流光不动没有在 fs 里 include cc-global或cc_time.x用错确保 fs 顶部有#include cc-global条纹细到闪烁条纹密度过高在低分辨率真机上采样不足降低 density或改用 smoothstep 宽度控制必要时提高纹理分辨率白色背景上整个区域发白加色混合导致过曝换src_alpha, one或把亮度统一压低真机颜色跟编辑器差很多部分 GPU 对 highp 精度运算的取舍不同关键计算保持 highp纹理采样时额外做一次 clamp5.2 最值得记住的两个坑第一个坑是 uniform 命名不一致。properties里的键要和uniform LightBall {}块里的成员名对得上否则 Cocos Creator 编译材质时会报错或者输出错误结果。我见过很多情况是手滑把stripeParams写成stripeParam结果编辑器里改参数完全没反应。第二个坑是光球尺寸很容易超过纹理边界。radius如果大于 0.5球会被 Sprite 的矩形裁掉边缘出现一个生硬的切面。解决方法是把半径压在 0.48 以内或者给球心位置预留边界。还有一种办法是改 Sprite 的sizeMode放大节点但节点大小和 Shader 里的 UV 是两套体系别混了。5.3 Cocos Creator 2.x 项目怎么办这篇文章的代码基于 Cocos Creator 3.x 语法。如果你的项目还在用 2.4.xeffect 文件的框架结构基本一样CCEffect、CCProgram、vert、frag 都还在但有三点差异需要注意一是内置变量和 include 路径在不同版本有个别差异建议以你本地安装的引擎官方示例为准二是代码里in/out关键字在 2.4 的某些版本支持良好但个别老项目需要改成varying风格三是 Material 的 setProperty 传参格式在 2.4 中是通过setProperty(name, value)访问参数类型和 3.x 也有细节区别。遇到问题时最可靠的方法是打开引擎自带的光照示例 effect 文件对标一下。6. 最后聊两句调试习惯做 Shader 调试我最推荐的方法是分层可视化。拿到一个新 Shader先把最终颜色输出拆成几段临时返回值一段只看遮罩一段只看条纹一段只看辉光确认各自正常后再合成。比如我把 frag 里return vec4(col, 1.0)临时改成return vec4(vec3(ballMask), 1.0)就能直接看到球体遮罩长什么样。这一步能节省大量排查时间。还有一个藏在细节里的习惯Shder 调亮度过高导致看不清层次时别急着改参数先把整体颜色除以一个常量比如col * 0.25看清楚结构后再恢复。一旦习惯了这种分段验证的方式你会发现 Shader 调试比想象中容易得多光球流光这种级别的问题几乎都能在半小时内解决。
返回列表