最近在项目里做夜景灯光氛围,一直在抠一个细节:远处的路灯、车灯、橱窗高光,如果只是亮一个点,画面会很“平”,缺少那种镜头感。后来在UE5里用材质节点做了个十字光斑效果,才把这种感觉找回来。
先说结论:这个东西不是贴图,是纯数学算出来的。在材质编辑器里用UV坐标、幂函数、旋转矩阵几个节点组合,就能生成一个可控的十字星芒,而且不依赖任何外部贴图。这篇文章就把完整的搭建过程、背后的数学逻辑、常见坑都讲一遍,适合正在做灯光特效、粒子、UI表现或者单纯想折腾材质节点的朋友参考。
1. 起步:为什么光斑要用材质节点算,而不是贴图
1.1 光斑的本质:不是绘制出来,是计算出来
十字光斑在现实里的来源,是镜头内部结构对强光源产生的衍射现象。你可以把它理解为:光线经过镜头叶片边缘时,被“掰”出了几道指向性的光芒,看起来就是一条十字或者六角星芒。摄影圈经常会用缩小光圈的办法拍出这种效果,夜景里路灯瞬间变成小星星,就是同一个原理。
在实时渲染里,我们没必要真的模拟光线衍射,只需要“算”出一个亮斑分布在屏幕上就够了。这个亮斑有几个关键特征:
- 中心最亮,向四周快速衰减;
- 沿水平方向有一条细长的亮线;
- 沿垂直方向也有一条细长的亮线;
- 两条线交叉的中心点亮度远高于单条线。
这其实就是一个二维函数 f(x, y) 在屏幕上的分布。用材质节点去计算它,本质上是在写这个函数。这也是材质编辑器最擅长的事——每个像素都执行一遍同样的数学公式。
1.2 材质节点的优势在哪
贴图方案不是不行,很多项目前期为了快速出效果,直接拿一张光晕贴图放在粒子或贴花上,也能看。但用下来你会发现几个痛点:
- 贴图有分辨率限制,靠近了看会有锯齿和发虚;
- 光斑方向是固定的,想让它跟随镜头旋转,要么做多张序列帧,要么在UV层面做奇怪处理;
- 想调亮度、拉长度、改颜色,往往要重新做图或者叠加多层混合,效率低;
- UV拉伸到一个极端比例时,贴图会产生明显的形变。
用材质节点算出来的十字光斑,全部是参数驱动,强度和长度直接暴露成Material Instance的数值,随时改随时看效果。而且没有贴图采样,纯数学节点不占纹理内存,性能开销微乎其微。更关键的是,分辨率无关——不管这个面片离相机多近,光斑边缘始终是干净的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心公式:用一张UV图玩出整片星空
2.1 UV坐标系与坐标镜像是所有效果的地基
要理解这个光斑怎么生成,先得搞明白UV坐标在材质里扮演的角色。一个平面网格的UV范围正常是左上角(0, 0)到右下角(1, 1),材质系统会对这个范围内的每个点执行一次节点计算。UV坐标可以理解成一张“地图坐标”,告诉材质系统“你现在处理的是模型上的哪个位置”。
但光斑是以某个点为中心向外扩散的,如果直接把UV 0到1的区间拿去算,中心点就落在(0.5, 0.5),看起来没问题,可一旦想挪动中心位置就会很别扭。所以通常第一步都是把UV平移到以0为中心:
- 对UV执行
Subtract,减去(0.5, 0.5); - 这样坐标范围变成-0.5到0.5,中心点自然落到(0, 0)上。
有了这个“以0为中心的坐标”,就能做很多事了。比如 Abs 节点把负数变成正数,效果相当于把坐标沿中心镜像折叠。想象一下:-0.5和0.5经过绝对值之后都变成0.5,那么整个坐标就变成了两侧向中间对称的形态。
这个镜像操作是十字光斑的关键第一步。
2.2 从圆形光斑到十字亮线:一个乘法的力量
先从一个简单问题入手:怎么在材质里画一个圆形光斑。通常做法是计算像素到中心的距离,再用SmoothStep判断这个距离是否够近:
- Distance节点计算坐标点与中心点的欧几里得距离;
- SmoothStep在半径边缘做平滑过渡,避免硬边;
- 1 - SmoothStep的结果就是中心亮、边缘暗的圆形光斑。
但如果只是圆形,还远远不够——我们需要的是“十字”。观察一下十字的形状特征:水平亮线意味着垂直方向的坐标接近0,垂直亮线意味着水平方向的坐标接近0。
这句话反过来说更实用:如果我把坐标的Y分量挑出来,取绝对值,然后让这个值越小越亮,就得到一条水平亮线;同理,把X分量挑出来取绝对值,让值越小越亮,就得到一条垂直亮线。这两条线叠加在一起,就是十字。
具体的数学表达可以这样写:
- 水平亮线亮度 =
1 - abs(U)(U为偏移后的横坐标); - 垂直亮线亮度 =
1 - abs(V)(V为偏移后的纵坐标); - 十字亮度 = 两者相加或者相乘。
如果直接相加,中心区域会非常亮,因为两条线在中心处亮度都是1,加起来约等于2,之后可以把结果通过Saturate或Clamp限制到合理范围。如果相乘,则只有两条线同时存在的地方才有值,中心会收缩成一个菱形亮斑,四周的臂会相对细一些。两种方案各有味道,我一般用相加的方式,因为中心更能压住亮度,看起来很扎眼。
但到这里还只是一个“粗十字”,边缘过渡太硬,看起来像四把刀子插在一起,不够柔和。于是需要引入幂函数 Power,用指数控制衰减速度。指数越大,亮度衰减得越快,光斑就越“瘦长”;指数越小,尾巴越长,整体更弥漫。通过几个Power节点,就能把十字光斑的形状控制到非常舒服的状态。
3. 完整实操:在UE5材质编辑器里搭建十字光斑
3.1 从零到一:最基础的十字光斑节点网络
打开材质编辑器,新建一个Material,把Blend Mode改成AlphaComposite或者Translucent,因为光斑要叠加到场景上,半透明混合更合适。Shading Model可以保持Default Lit,也可以改成Unlit,让材质自己发光,不参与光照计算。
节点网络的顺序大概是这样的:
TextureCoordinate节点,输出UV;- 加一个
Subtract节点,把UV减去(0.5, 0.5),得到中心在0的坐标; - 分别用
ComponentMask或者BreakFloat2取出U和V分量; - 对U分量的绝对值做
1 - abs(U),得到初始的垂直亮线; - 对V分量的绝对值做
1 - abs(V),得到初始的水平亮线; - 两个结果用
Add相加,得到基础十字; - 接一个
Power节点,设置指数比如2或者3,控制亮部衰减; - 最终输出到Emissive Color和Opacity。
注意每一步的数值范围。默认UV从0到1,减去0.5之后是-0.5到0.5,直接取绝对值再被1减,最亮处是1,最暗处是0.5,底边不够黑。所以更稳妥的办法是把坐标再乘2,让范围变成-1到1,这样绝对值取反后,远端正好是0。
实际操作时,我习惯把节点结构做成这样:
- UV → Subtract 0.5 → Multiply 2 → ComponentMask
- U通道:Abs → OneMinus
- V通道:Abs → OneMinus
- 两者Add → Power
这里的OneMinus就是“1减X”,材质编辑器里直接搜就有。为什么不用Subtract节点手动算?因为OneMinus输入比较直观,省得再连一个常量节点。
搭完这一串,你看到的效果应该是一个标准的十字亮线。如果形状觉得太短或太长,调整Power的指数就行。比如指数调到4,亮线会更集中在中心,两边收得更快。
3.2 参数化与旋转:直接复用的节点网络
基础版能出效果,但离“可用”还有距离。实际项目里光斑不是死板固定在屏幕中央的,它要跟着光源位置走、跟着镜头方向转,强度色彩也要能调整。所以下一步把这些值全部参数化。
在材质编辑器里添加以下参数:
ScalarParameter:Intensity,控制整体亮度,默认1.0;ScalarParameter:Length,控制光斑长度,本质上是Power的指数,默认2.0;ScalarParameter:Tilt,控制旋转角度,单位用弧度,默认0.0;VectorParameter:TintColor,控制光斑颜色,默认白色;ScalarParameter:OpacityStrength,控制透明通道,默认1.0。
旋转怎么加?最稳的是旋转整张UV坐标。记住这个二维旋转公式:
code复制U' = cos(A) * U - sin(A) * V
V' = sin(A) * U + cos(A) * V
在材质节点里做这个公式,就是几组Multiply和Add的组合。A是旋转角度。需要注意,UE5材质里三角函数默认使用弧度制,如果你习惯用角度,需要乘一个0.0174533(即π/180)转成弧度。
旋转节点布局可以这样:
- 先用
Multiply把U乘以cos(A),V乘以sin(A); - 拿U乘
-sin(A)和V乘cos(A)做加减; - 得到的新U和新V替换原来的分量。
这一步做完,光斑就会整体绕中心旋转。很多人在写旋转的时候会把公式方向搞反,导致顺时针逆时针混乱。解决方法是:先试一个固定角度,比如0.785(45度),看旋转方向是否符合预期。如果方向反了,把Add改成Subtract,或者把两个三角函数位置调换,就能修正过来。
参数连好之后,记得把材质里所有的参数设成Public,这样在内容浏览器里创建Material Instance时,这些值都可以直接在实例里调整。Lighting Mode我一般选Surface ForwardShading或者Unlit,看具体使用场景。
这里还有一个很实用的技巧:参数数值别全设成1。比如Intensity给到2到5,配合HDR颜色,在Bloom开启的场景里会有非常漂亮的发光扩散效果。如果你发现光斑亮得没有层次,多半是Intensity不够或者Bloom阈值太高,优先调这两个值。
4. 进阶雕琢:四角星与八芒星效果
4.1 增加斜45度光芒,变成八芒星
基础十字已经能解决大部分灯光点缀的需求,但有些场景需要更复杂的效果,比如八芒星。做法也简单:再做一套十字,但把UV旋转45度,然后和原来的十字叠加在一起。
为什么是45度?因为八芒星就是在原有水平/垂直十字基础上,再多加一套对角方向的光芒。四十五度正好是中间方向,可以和原来的十字形成均匀的角度分布。
操作上,我通常不直接改前面做好的节点,而是把十字计算的部分封装成一个子函数,或者用Ctrl+C复制一份节点网络,旋转之后再Add回主结果。在UE5里复制一大段节点会显得乱,所以更推荐用Material Function做封装。先把十字的骨架做进一个材质函数,留出UV、长度、强度三个输入,然后用两个不同的旋转角度调用两次,最后合并输出。
旋转角度可以做成参数,这样就能同时控制两套光芒的夹角。除了45度,你也可以试试30度、60度,能做出好几种风格的星芒。游戏里那种“米字形”光晕,很多就是靠三层十字叠加出来的。
但是注意:每加一层十字,中心亮度会成倍增加。两层相加中心大约2倍亮度,三层就是3倍。如果不处理,中心会白成一团。我的做法是加完所有层之后,统一用一个Divide或Multiply把总量压回一个合理区间,或者干脆加一个Clamp把输出限制在0到1之间。这样中心依然是亮的,但不会爆到失去层次。
4.2 亮度、长度与柔边控制,顺手做强
想把十字光斑雕琢到真正可用,关键在三个细节:边缘柔度、长度渐变、亮度压实。
边缘柔度的问题,本质是抗锯齿。直接用 1 - abs(V) 这种线性函数,边缘是硬切,在屏幕上会看到明显的斜向锯齿。解决办法是用 SmoothStep 替代线性计算。做法是先计算距离中心的距离,然后对距离做一个平滑阈值判断:
- 光照宽度 =
SmoothStep(0.2, 0.0, distance); - 意思是距离大于0.2的像素完全透明,距离小于0.0的完全亮起,中间过渡平滑。
SmoothStep的两个边界值控制柔边范围,数值靠得越近边缘越锐利,离得越远边缘越柔和。为了保持性能,不建议每个分支都用SmoothStep,只在最外层用一次即可。
长度渐变主要靠Power指数控制。指数小,光斑臂长且亮;指数大,光斑短且集中。调试时可以先锁定一个值反复看效果,找到合适感觉再参数化。另外提一个技巧:长度指数可以分开控制。比如横向长度指数设成3,纵向设成2,这样光斑就不是对称十字,而是一个有方向倾向的星芒,用在车辆前灯扫射效果时挺好用。
亮度的“压实”是很多人忽略的。直接输出的亮度是一个高动态范围数值,如果不做处理,在不同环境光照下结果可能差很多。我的习惯是最后接一个Exponential2或者直接乘一个常数,让最亮值稳定在某个范围。具体数值取决于项目使用的后期曝光方式,不能一概而论,但有一点通用:如果你发现关了Bloom光斑就看不见,开了Bloom就白成一片,那大概率是输出范围没校准,拿Clamp压一下就能解决。
5. 常见问题与排查实录
5.1 最常见的问题和它们的排查步骤
做材质节点效果,最花时间的不是搭建,而是排错。分享几个我几乎每次都踩的坑,以及排查思路,整理成速查表方便大家对照。
| 问题现象 | 可能原因 | 排查/解决思路 |
|---|---|---|
| 光斑不是居中,而是出现在角落 | UV坐标没有减去中心点偏移 | 检查是否执行了Subtract 0.5,并Multiply 2 |
| 光斑全是锯齿,边缘很脏 | 没有做平滑过渡,直接用了线性阈值 | 用SmoothStep替代硬阈值,留出过渡带 |
| 旋转后形状变成椭圆或畸变 | 旋转矩阵U/V两个分量没有同时计算 | 检查旋转公式,确保U'和V'用的是同一套三角函数结果 |
| 中心亮得刺眼,周围却什么都没有 | Power指数设得过大 | 把Power指数降到1.5到3之间再微调 |
| 材质在移动端完全不显示 | 精度问题或混合模式不支持 | 把Shader精度改成Full Precision,检查混合模式是否兼容 |
| 光斑是黑色或灰灰的 | Emissive Color没有赋颜色,或Alpha通道为0 | 确认TintColor不是黑色,Opacity有正值 |
| 场景里Bloom开不起来 | 输出亮度值过低,达不到Bloom阈值 | 把颜色通道值调高,比如乘以3到5,或用HDR数值 |
这些坑里,最让我头疼的是“旋转后畸变”这个。原因是只旋转了坐标分量中的一个,比如只改了V通道,没改U通道,导致旋转矩阵不完整。检查的时候最容易忽略,因为表面上看起来“有旋转”,实际上形状已经被撕裂了。每次改完旋转,都建议用一个大一点的光斑、固定一个45度角,先目测检查形状是否正确,再继续下一步。
5.2 性能优化与适配注意点
十字光斑材质全部使用数学节点,没有纹理采样,理论上性能开销极低。但“没有纹理采样”不等于“可以为所欲为”。以下几点在项目里非常重要:
第一,避免大量节点串联。材质编辑器里几百个节点连成一串,虽然功能上没问题,但Shader编译时间和维护成本会几何级上升。我在项目里就见过一张画面特效材质,节点数量超过500个,每次改一个参数都要等十几秒编译,后来拆成多个材质函数才好很多。
第二,光斑如果用在粒子上,尽量不要每帧更新大量实例参数。粒子系统里几百个粒子同时驱动同一个材质参数,会带来明显的CPU开销。更好的方式是让材质根据粒子数据(比如颜色、大小)自行计算,而不是在蓝图里逐个Set Scalar Parameter。
第三,注意移动端和PC端的差异。移动端GPU对高精度数学运算支持的尺度不一样,一些节点在Mobile上会退化成低精度,导致光斑边缘出现条纹。遇到这种情况,可以在材质细节面板里把Quality Switch配置成高低端两套节点结构,移动端用简化版,比如减少一层十字叠加。
第四,如果是放在UI上使用的十字光斑,记得把材质设置成User Interface的Blend Mode,否则Canvas里不会正确渲染。这个点小的容易忽略,但一旦出错,排查起来很浪费时间。
6. 落地扩展:应用到实际场景中的几个方向
6.1 让特效跟随并提取场景亮部
十字光斑最有意思的用法是把它接到灯光和镜头上,模拟真实的摄影效果。一个常见的做法是:用后处理材质或贴花,读取场景的亮度信息,把高亮区域提取出来,只在这些地方叠加十字光斑。
在材质里可以用SceneTexture节点,选SceneColor,然后计算各个通道的亮度。亮度公式一般用经典的Rec.709系数:
code复制Luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
把亮度值减去一个阈值,比如0.6,小于0的截断为0,剩下的部分作为光斑的强度遮罩。这样一来,只有场景里足够亮的光源才会产生十字闪耀,暗部区域不受影响,看起来非常自然,像是摄像机真实拍出来的一样。
这种做法的好处是自动适配场景里的所有亮点,不需要人工摆放粒子。坏处是阈值不好调,阈值太高只有太阳有星芒,阈值太低整个屏幕都闪闪发光。我的经验是先从0.7开始,慢慢下调,调到0.5到0.6之间基本能覆盖大部分场景。
6.2 封装成材质函数,给整个团队复用
如果一个项目里多个地方需要十字光斑,最好的实践是封装成Material Function。打开内容浏览器,右键材质,创建材质函数,把前面做好的节点网络粘贴进去,把需要外部控制的值全部暴露为Function Input。这样其他人使用的时候,只需要拖一个材质函数节点,连上旋转角、颜色、强度,五分钟就能出效果。
封装时我把Input设计成五个:
- UV:输入要处理的UV坐标,默认使用TextureCoordinate;
- Center:光斑中心位置,默认(0.5, 0.5);
- Angle:旋转角度,单位弧度;
- LengthPower:长度指数,控制星芒长度;
- Color:颜色和强度的组合,常用HDR值传入。
这五个参数覆盖了90%的使用场景。到了项目后期,我把材质函数放到团队的公共内容包里,美术同学拿到手就能直接用,研发也不用反复解释原理,效率提升很明显。
另外一个细节是:材质函数里尽量别用全局参数引用,否则每次调用都要额外维护一套参数集合,很容易冲突。所有参数走Function Input是最干净的。
最后再分享一个小经验。我在做这个效果的时候,最开始用的是严格的数学公式,一个节点一个节点地抠,结果调试了很久都达不到理想效果。后来换了个思路——先把十字形状用最粗暴的方式叠出来,哪怕是锯齿明显的版本,确认方向、位置、旋转都对了,再一点点加SmoothStep、Power这些细腻节点。这两步分开做,比一步到位要快得多。
十字光斑这个效果本身不难,难的是理解UV坐标系里那套“先平移、再镜像、再旋转、再衰减”的思维逻辑。这套逻辑一旦建立起来,你会发现不只是十字光斑,很多看似复杂的材质效果,比如极光扫过、火焰光晕、粒子拖尾,底层都是同一堆数学节点在排列组合。
