Shader实现聚光灯效果(Follow-spot)

效果

聚光灯效果,主要用于突出游戏中的角色或特殊物体,并在黑暗环境中营造沉浸式的氛围,效果如下:

思路

效果的核心:

  • 控制光圈的形状和大小
  • 边缘虚化
  • 控制移动

前两点需要着色程序来控制,第三点通过外部脚本控制光圈的移动。

光圈实现

  • 圆形光圈:通过计算像素与圆心的距离,判断其是否在圆内。在圆内的像素保持原样,圆外的像素则设置透明度为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 - blurWidthradius 定义了虚化过渡的区间。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;

  }
}%

到此就实现了开头的效果。

相关推荐
布鲁克零三四四2 个月前
Cocos Creator导出obj文件用于后端寻路
cocos creator
烧仙草奶茶2 个月前
【cocos creator】输入框滑动条联动小组建
cocos creator·cocos-creator
烧仙草奶茶4 个月前
【cocos creator】2.x里,使用3D射线碰撞检测
3d·cocos creator·cocos-creator·2.x
仅此而已7294 个月前
Cocos Creator倒计时
游戏引擎·cocos creator
仅此而已7294 个月前
cocosUI多分辨率适配
游戏引擎·cocos creator·多分辩率适配
SilenceJude5 个月前
cocos creator 3学习记录01——如何替换图片
前端·cocos creator
GrimRaider6 个月前
[Cocos Creator] v3.8开发知识点记录(持续更新)
开发·cocos creator
S_clifftop6 个月前
cocos creator如何使用cryptojs加解密(及引入方法)
cocos creator·加密解密·cryptojs
平淡风云6 个月前
cocosCreator获取手机剪切板内容
java·智能手机·typescript·android studio·cocos creator
zhenmu6 个月前
【cocos creator 3.x】 修改builtin-unlit 加了一个类似流光显示的mask参数
cocos creator·shader·effect