一、光照跟随介绍
光照跟随是一种模拟动态光源聚焦的技术,其核心表现为:画面四周呈现深色暗角,将玩家视线自然引导至中心区域------通常是主角或关键目标。

随着角色在场景中移动,这个被照亮的中心区域也随之移动,如同聚光灯始终追随主角,营造出强烈的舞台感和叙事沉浸感。
假设我们在 Cocos Creator 中有这么一个 Demo:

即一个奔跑的外星人 Spine 动画对象可以在地图中随意移动,本文将介绍如何使用着色器来实现光照跟随该外星人的效果。
二、色调调整
和以往的案例一样,我们可以通过一个额外的摄像头捕获整体画面生成 Render Texture,从而在片元着色器中对整幅画面进行像素级处理:
js
vec4 frag () {
vec4 color = texture(rawRT, uv); // rawRT 为整体画面的渲染纹理
// 对纹理采样后的像素进行加工
}
应用光照跟随的场景一般处于弱光环境(例如夜间态),我们可以先修改整体画面色调,让其呈现出一种昏暗、被橙红色微光覆盖的视觉:
js
vec3 tintHandler(vec4 color) {
// 傍晚色调参数 (橙红色调)
vec3 eveningTint = vec3(1.0, 0.5, 0.3);
// 原色与傍晚色调混合(20%)
vec3 blended = mix(color.rgb, eveningTint, 0.2);
// 整体降低 20% 亮度
vec3 darkenColor = blended * 0.8;
// 获取灰阶分量
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
// 叠加灰阶来提升饱和度
return mix(vec3(gray), darkenColor, 1.2);
}
vec4 frag () {
vec4 color = texture(rawRT, uv);
vec3 handledRGB = tintHandler(color);
return vec4(handledRGB, color.a);
}
tintHandler
函数对原画面的 RGB 分量进行了逐步调整处理:

其中第 12 行通过点积来获取灰阶的方式,在之前的《灰阶、反色等滤镜的实现 --------- 一、灰阶滤镜》中便已使用过了:
js
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
接着通过 mix
函数叠加上灰阶色值,可修改画面的饱和度:
js
mix(vec3(gray), darkenColor, 1.2); // 等价于 vec3(gray) * (1.0 - 1.2) + darkenColor * 1.2
下图是通过修改 mix
的第三个参数值,来实现不同饱和度的示例:

三、添加暗角
添加暗角的方法,与《灰阶、反色等滤镜的实现 --------- 5.1.2 暗角效果》的实现一致,我们同样新增一个 vignette
函数来处理暗角:
js
float vignette() {
float vignetteIntensity = 1.0; // 暗角强度使用 1.0
// 计算到中心的距离
vec2 center = vec2(0.5, 0.5);
float dist = distance(uv, center);
// 通过 vignetteIntensity 来控制暗角扩散程度,使用 smoothstep 来平滑边缘暗角
// `0.3` 是光照最亮区域半径,`0.8` 是最暗区域的扩散边缘
return 1.0 - smoothstep(0.3, 0.8, dist * vignetteIntensity);
}
vec4 frag () {
vec4 color = texture(rawRT, uv);
if (showSpot == 0) {
return color;
}
vec3 handledRGB = tintHandler(color);
return vec4(handledRGB * vignette(), color.a);
}
💡 之前的文章已介绍过暗角代码原理,故本文不再赘述。
执行效果如下:

此时光照区域偏大,我们可以通过调整 smoothstep
函数的两个阈值参数,来缩小聚光的覆盖范围:
js
float vignette() {
float vignetteIntensity = 1.0;
vec2 center = vec2(0.5, 0.5);
float dist = distance(uv, center);
// 缩小光照区间到 [0.05, 0.5]
return 1.0 - smoothstep(0.05, 0.5, dist * vignetteIntensity);
}
修改后的效果:

另外由于着色器使用的是正方形的 UV 坐标系空间,而最终渲染出来的界面尺寸为 1280x720(即宽高比约为 1.78
),导致圆形的光照区域被水平拉伸为椭圆形。
我们可以在 vignette
中修正这个问题:
js
float vignette() {
float vignetteIntensity = 1.0; // 暗角强度
// 计算画布宽高比
float aspectRatio = 1280.0 / 720.0;
// 校正 UV 坐标,让聚光区域为正圆形
vec2 aspectCorrectedUV = uv;
aspectCorrectedUV.x = (uv.x - 0.5) * aspectRatio + 0.5;
vec2 center = vec2(0.5, 0.5);
float dist = distance(aspectCorrectedUV, center); // 应用 aspectCorrectedUV
return 1.0 - smoothstep(0.05, 0.5, dist * vignetteIntensity);
}
其中第 9 行为修改的关键,它将原本 X 轴上的像素,从 1280 像素压缩到 720 像素等效范围:
js
aspectCorrectedUV.x = (uv.x - 0.5) * aspectRatio + 0.5;
此行代码可分解为:
- 通过
uv.x - 0.5
将 UV 坐标的 X 轴范围修改为[-0.5, 0.5]
,其中心点 X 坐标变为0.0
,且左边为负,右边为正。这意味此时乘以任何数值,水平方位的像素都将向画面正中央方向去做伸缩(即确保了画面拉伸的原点处于正中央); - 乘以
aspectRatio
,按照屏幕的宽高比对 X 轴做一个伸缩变换,将原本的矩形画面压缩为正方形; - 把坐标平移回标准的 UV 坐标空间(即
[0.0, 1.0]
)。

执行效果:

四、光照跟随角色
目前光照位置都是固定在画面中心,无法跟随运动中的外星人角色:

对此我们可以通过外部组件脚本,实时往着色器传入角色的 UV 坐标。
4.1 着色器修改光照中心
首先在片元着色器中新增 vec2
类型的 uniform
参数 spotCenter
,它是由外部脚本传入的角色 UV 坐标:
js
uniform UBO {
vec2 spotCenter; // 新增光照中心参数
int showSpot;
};
float vignette() {
float vignetteIntensity = 1.0;
float aspectRatio = 1280.0 / 720.0;
vec2 aspectCorrectedUV = uv;
aspectCorrectedUV.x = (uv.x - 0.5) * aspectRatio + 0.5;
// 对中心点也进行宽高比校正
vec2 correctedCenter = spotCenter;
correctedCenter.x = (spotCenter.x - 0.5) * aspectRatio + 0.5;
// 应用校正后的中心 UV 坐标
float dist = distance(aspectCorrectedUV, correctedCenter);
return 1.0 - smoothstep(0.05, 0.5, dist * vignetteIntensity);
}
留意第 16 行的处理:传入的 spotCenter
坐标也需要进行和 aspectCorrectedUV
相同的 X 轴宽高比校正,确保两者处于同一坐标体系下。
4.2 组件脚本更新材质参数
新增 SpotComp
组件脚本,在 update
生命周期中实时获取移动中的角色坐标,转化为 UV 坐标形式后传递给着色器:
js
@ccclass('SpotComp')
export class SpotComp extends Component {
spineNode: Node = null;
spriteMaterial: Material = null;
start() {
this.spineNode = this.node.getChildByName('Spine');
this.spriteMaterial = this.node.getChildByName('Sprite').getComponent(Sprite).material;
}
update() {
// 1. 获取角色在屏幕空间的位置
const worldPos = this.spineNode.worldPosition;
// 2. 转换为归一化 UV 坐标 (0-1 范围)
const uvX = worldPos.x / 1280; // 1280 x 720 是画布的宽高
const uvY = worldPos.y / 720;
// 3. 传递给着色器
this.spriteMaterial.setProperty('spotCenter', v2(uvX, uvY));
}
}
此时光照中心便能跟随角色实时移动:
