Nanfeng

Notes on software development, code, and curious ideas

Creating a Shine Sweep Effect with a Cocos Creator Shader

Overview

  • Feature: a moving shine across an image
  • Engine: Cocos Creator 3.7.2
  • Language: TypeScript

Result

Shine sweep effect

Shader design

Create a custom sprite effect based on Cocos Creator’s built-in sprite shader. Expose these material properties:

1
2
3
4
5
6
7
8
9
properties:
alphaThreshold: { value: 0.5 }
lightColor: { value: [1.0, 1.0, 0.0, 1.0], editor: { type: color } }
lightCenterPoint: { value: [0.2, 0.2] }
lightAngle: { value: 36.0 }
lightWidth: { value: 0.2 }
enableGradient: { value: 1.0 }
cropAlpha: { value: 1.0 }
enableFog: { value: 0.0 }

In the fragment program, calculate the perpendicular distance from the current UV coordinate to the light band’s center line. Pixels outside half of lightWidth are discarded from the effect; pixels inside it receive lightColor. The optional gradient decreases the added light as the distance approaches the band’s edge. Multiplying by the source alpha prevents the light from appearing over transparent pixels.

The central fragment logic is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
float angleInRadians = radians(lightAngle);
float dis = 0.0;

if (mod(lightAngle, 180.0) != 0.0) {
float lightOffsetX = lightCenterPoint.x
- ((1.0 - lightCenterPoint.y) / tan(angleInRadians));
float dx = lightOffsetX + (1.0 - uv0.y) / tan(angleInRadians);
dis = sin(angleInRadians) * abs(uv0.x - dx);
} else {
dis = abs(uv0.y - lightCenterPoint.y);
}

float a = 1.0;
if (bool(cropAlpha)) {
a *= step(0.01, textureColor.a);
}
if (!bool(enableFog)) {
a *= step(dis, lightWidth * 0.5);
}
if (bool(enableGradient)) {
a *= 1.0 - dis / (lightWidth * 0.5);
}

vec4 finalLightColor = textureColor + lightColor * a;
finalLightColor.a = textureColor.a;
return finalLightColor;

Set up the material

  1. Create a material manually.
  2. Bind the custom effect to the material.
  3. Assign the material to the sprite.
  4. Move lightCenterPoint from a TypeScript component.

TypeScript animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { _decorator, CCFloat, Component, Sprite, Material, Vec2 } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('NewComponent')
export class NewComponent extends Component {
@property(Sprite)
sprite!: Sprite;

@property({ type: CCFloat, tooltip: 'Light width' })
lightWidth = 0.03;

@property({ type: CCFloat, tooltip: 'Sweep duration' })
loopTime = 1.0;

@property({ type: CCFloat, tooltip: 'Pause between sweeps' })
timeInterval = 2.0;

private material: Material = null!;
private position = 0;
private elapsed = 0;
private startPosition = 0;
private speed = 0;

start() {
this.material = this.sprite.getMaterial(0);
this.startPosition = -this.lightWidth / 2;
const moveLength = this.lightWidth + 1;
this.speed = moveLength / this.loopTime / 2;
this.position = this.startPosition;
}

update(dt: number) {
this.position += dt * this.speed;
this.elapsed += dt;
this.material.setProperty(
'lightCenterPoint',
new Vec2(this.position, this.position)
);

if (this.elapsed > this.loopTime + this.timeInterval) {
this.position = this.startPosition;
this.elapsed = 0;
}
}
}

The component continuously moves the material’s light center, then resets it after the sweep and pause have elapsed.

Complete demo

Download the demo project

+