效果
聚光灯效果,主要用于突出游戏中的角色或特殊物体,并在黑暗环境中营造沉浸式的氛围,效果如下:
思路
效果的核心:
- 控制光圈的形状和大小
- 边缘虚化
- 控制移动
前两点需要着色程序来控制,第三点通过外部脚本控制光圈的移动。
光圈实现
- 圆形光圈:通过计算像素与圆心的距离,判断其是否在圆内。在圆内的像素保持原样,圆外的像素则设置透明度为0,从而实现一个圆形光圈。
- 圆心坐标 :圆心默认在纹理的中间,坐标设置为
vec2(0.5,0.5)
。 - 透明度控制 :使用
step
函数来控制透明度。如果像素距离圆心的距离大于半径,则透明度为0,否则为1
代码如下:
typescript
void main () {
vec4 color = vec4(1, 1, 1, 1);
color *= texture(texture, v_uv0);
color *= v_color;
color.a = step(length(v_uv0 - vec2(0.5,0.5)), 0.5);
gl_FragColor = color;
}
其中step
为阶梯函数,它接受两个参数,一个边界值edge
,一个输入变量value
, 当value >= edge
时,返回1,否则返回0.
typescript
function step<T>(edge: T, value: T): T {
return value >= edge ? 1.0 : 0.0;
}
通过step(length(v_uv0 - vec2(0.5,0.5)), 0.5);
得出纹理坐标v_uv0
到中心点vec2(0.5, 0.5)
的距离小于半径0.5,透明度为1,可以展示,大于半径0.5, 透明度为0,不展示。
宽高比修正
仔细看后,发现光圈是椭圆的,不是圆的,因为在shader中,不管纹理的真实宽高是多少,纹理坐标 v_uv0
的 x 和 y 分量都是在 [0, 1] 范围内,而不考虑实际的宽高比,所以直接使用 v_uv0
来计算距离会导致椭圆形状的光圈,特别是在非正方形的纹理上。
为了确保圆形保持其形状,你需要调整 x 或 y 分量,使其在计算距离时考虑到纹理的实际宽高比。以下是相应的修改:
typescript
vec4 color = vec4(1, 1, 1, 1);
color *= texture(texture, v_uv0);
color *= v_color;
float aspectRatio = ratio; // 纹理的宽高比(宽度除以高度),需要外部脚本计算好传进来
vec2 adjustedUV = vec2(v_uv0.x * aspectRatio, v_uv0.y);
float radius = 0.3; // 光圈半径,可以根据需要调整
color.a = step(length(adjustedUV - vec2(0.5 * aspectRatio, 0.5)), radius);
gl_FragColor = color;
在这段代码中,aspectRatio
是纹理的宽高比。通过将 v_uv0.x
乘以这个比例,我们调整了 x 坐标,使得圆形在宽高不同的纹理上依然保持圆形。同样,圆心坐标也进行了相应的调整。
注意,aspectRatio
的值需要从你的应用或游戏引擎中获取,通常是纹理的宽度除以高度。如果纹理是正方形的,那么这个值应该是 1。如果是宽度大于高度的长方形,这个值将大于 1;反之,如果高度大于宽度,这个值将小于 1。
外部计算背景图的宽高比,传入到shader
typescript
this._material.setProperty('ratio', this.bg.width / this.bg.height);
通过这种方式,你可以确保在不同宽高比的纹理上都能绘制出一个标准的圆形光圈。并且这里将光圈的半径设置为0.3,减小光圈的大小。
边缘虚化
上面的图中光圈的边缘有明显的锯齿,为了使光圈的边缘更加平滑,要在着色器中实现边缘虚化处理,可以使用了 smoothstep
函数。这个函数可以在指定的范围内生成平滑的过渡,从而实现边缘虚化的效果。
typescript
...
float blurWidth = blur; // 边缘虚化的宽度,通过外部传入
// 计算像素到圆心的距离
float dist = length(adjustedUV - vec2(0.5 * aspectRatio, 0.5));
// 使用 smoothstep 实现边缘虚化
color.a = 1.0 - smoothstep(radius - blurWidth, radius, dist);
...
blurWidth
是边缘虚化的宽度,这个值决定了虚化效果的强度和范围。smoothstep
的前两个参数radius - blurWidth
和radius
定义了虚化过渡的区间。dist
是当前处理像素与圆心的距离。当dist
在这个区间内时,smoothstep
会返回一个在 0 到 1 之间平滑变化的值,从而实现虚化效果。
调整 blurWidth
的值可以控制边缘的虚化程度。较大的 blurWidth
值会产生更宽、更平滑的边缘,而较小的值则会产生较窄、较尖锐的边缘。
效果如下:
控制移动
片段着色器中定义一个 uniform
变量来代表圆心坐标,然后在外部脚本中更新这个坐标。以下是修改后的着色器代码,其中包含了一个 uniform vec2 center;
声明,以及如何在外部脚本中更新这个变量的说明:
typescript
uniform vec2 center; // 外部脚本控制的圆心坐标
...
vec2 adjustedCenter = vec2(center.x * aspectRatio, center.y);
// 计算像素到圆心的距离
float dist = length(adjustedUV - adjustedCenter);
...
在外部脚本中控制圆心坐标:
typescript
_onTouchMove(event: cc.Event.EventTouch) {
this.center[0] += event.getDeltaX() / this.bg.width;
this.center[1] -= event.getDeltaY() / this.bg.height;
this._material.setProperty('center', this.center);
}
完整代码
typescript
// FollowSpot.ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class FollowSpot extends cc.Component {
/** 背景 */
@property(cc.Node)
bg: cc.Node = null;
/** 材质 */
private _material: cc.Material = null;
/** 光圈起点 */
center: number[] = [0.1, 0.5];
onLoad() {
this._material = this.bg.getComponent(cc.Sprite).getMaterial(0);
this._material.setProperty('ratio', this.bg.width / this.bg.height);
this._material.setProperty('center', this.center);
this.bg.on(cc.Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
}
_onTouchMove(event: cc.Event.EventTouch) {
this.center[0] += event.getDeltaX() / this.bg.width;
this.center[1] -= event.getDeltaY() / this.bg.height;
this._material.setProperty('center', this.center);
}
}
// FollowSpot.effect
CCEffect %{
techniques:
- passes:
- vert: vs
frag: fs
blendState:
targets:
- blend: true
rasterizerState:
cullMode: none
properties:
texture: { value: white }
ratio: {
value: 1.5,
editor: {
tooltip: "宽高比"
}
}
blur: {
value: 0.35,
editor: {
tooltip: "光圈模糊程度"
}
}
radius: {
value: 0.5,
editor: {
tooltip: "光圈半径"
}
}
center: {
value: [0.5, 0.5],
editor: {
tooltip: "光圈起点"
}
}
}%
CCProgram vs %{
precision highp float;
#include <cc-global>
#include <cc-local>
in vec3 a_position;
in vec4 a_color;
out vec4 v_color;
#if USE_TEXTURE
in vec2 a_uv0;
out vec2 v_uv0;
#endif
void main () {
vec4 pos = vec4(a_position, 1);
#if CC_USE_MODEL
pos = cc_matViewProj * cc_matWorld * pos;
#else
pos = cc_matViewProj * pos;
#endif
#if USE_TEXTURE
v_uv0 = a_uv0;
#endif
v_color = a_color;
gl_Position = pos;
}
}%
CCProgram fs %{
precision highp float;
#include <alpha-test>
in vec4 v_color;
#if USE_TEXTURE
in vec2 v_uv0;
uniform sampler2D texture;
#endif
uniform ARGS{
float radius;
float blur;
vec2 center;
float ratio;
};
void main () {
vec4 color = vec4(1, 1, 1, 1);
color *= texture(texture, v_uv0);
color *= v_color;
float aspectRatio = ratio; // 纹理的宽高比(宽度除以高度)
vec2 adjustedUV = vec2(v_uv0.x * aspectRatio, v_uv0.y);
vec2 adjustedCenter = vec2(center.x * aspectRatio, center.y);
float radius = 0.3; // 光圈半径,可以根据需要调整
float blurWidth = blur; // 边缘虚化的宽度
// 计算像素到圆心的距离
float dist = length(adjustedUV - adjustedCenter);
// 使用 smoothstep 实现边缘虚化
color.a = 1.0 - smoothstep(radius - blurWidth, radius, dist);
gl_FragColor = color;
}
}%
到此就实现了开头的效果。