第8.2章:边缘光Surface Shader

边缘光(Rim Light)是游戏中最常用的视觉效果之一,能够突出物体轮廓,增强视觉层次感。本章将详细介绍如何在Surface Shader中实现各种边缘光效果。

🎯 学习目标

  • 理解边缘光的数学原理和视觉效果
  • 掌握基础边缘光的Surface Shader实现
  • 学会创建高级边缘光变体效果
  • 理解边缘光的性能优化技巧

💡 边缘光原理

菲涅尔反射原理

边缘光基于菲涅尔反射原理,当视线与表面法向量夹角越大时,反射越强。

1
2
// 基础菲涅尔计算
float fresnel = 1.0 - dot(viewDirection, normal);

视觉效果分析

1
2
3
4
5
物体中心 → 弱边缘光

物体边缘 → 强边缘光 → 视觉突出

背景分离 → 轮廓清晰

🔧 基础边缘光实现

1. 简单边缘光

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
46
47
48
49
50
CCEffect %{
techniques:
- name: basic-rim
passes:
- vert: rim-vs:vert
frag: rim-fs:frag
properties: &props
mainTexture: { value: white }
baseColor: { value: [1, 1, 1, 1], editor: { type: color } }
rimColor: { value: [0, 1, 1, 1], editor: { type: color } }
rimPower: { value: 2.0, range: [0.1, 10] }
rimIntensity: { value: 1.0, range: [0, 5] }
}%

CCProgram rim-vs %{
#include <surface-vertex>
}%

CCProgram rim-fs %{
#include <surface-fragment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;
uniform vec4 rimColor;
uniform float rimPower;
uniform float rimIntensity;

void surf(in SurfaceIn In, inout SurfaceOut Out) {
// 基础颜色
vec4 albedo = texture(mainTexture, In.uv) * baseColor;

// 计算视线方向
vec3 viewDir = normalize(cc_cameraPos.xyz - In.worldPos);
vec3 normal = normalize(In.worldNormal);

// 边缘光计算
float rim = 1.0 - dot(viewDir, normal);
rim = pow(rim, rimPower) * rimIntensity;

// 边缘光颜色混合
vec3 rimEffect = rim * rimColor.rgb;

Out.albedo = albedo;
Out.normal = normal;
Out.metallic = 0.0;
Out.roughness = 0.5;
Out.emissive = rimEffect; // 边缘光作为自发光
Out.ao = 1.0;
}
}%

2. 可控制边缘光

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
CCEffect %{
techniques:
- name: controllable-rim
passes:
- vert: ctrl-rim-vs:vert
frag: ctrl-rim-fs:frag
properties: &props
mainTexture: { value: white }
baseColor: { value: [1, 1, 1, 1], editor: { type: color } }
rimColor: { value: [0, 1, 1, 1], editor: { type: color } }
rimPower: { value: 2.0, range: [0.1, 10] }
rimIntensity: { value: 1.0, range: [0, 5] }
rimWidth: { value: 1.0, range: [0.1, 3] }
rimSoftness: { value: 0.1, range: [0.01, 1] }
rimOffset: { value: 0.0, range: [-1, 1] }
}%

CCProgram ctrl-rim-vs %{
#include <surface-vertex>
}%

CCProgram ctrl-rim-fs %{
#include <surface-fragment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;
uniform vec4 rimColor;
uniform float rimPower;
uniform float rimIntensity;
uniform float rimWidth;
uniform float rimSoftness;
uniform float rimOffset;

void surf(in SurfaceIn In, inout SurfaceOut Out) {
vec4 albedo = texture(mainTexture, In.uv) * baseColor;

vec3 viewDir = normalize(cc_cameraPos.xyz - In.worldPos);
vec3 normal = normalize(In.worldNormal);

// 改进的边缘光计算
float fresnel = 1.0 - dot(viewDir, normal);

// 添加偏移控制
fresnel = clamp(fresnel + rimOffset, 0.0, 1.0);

// 宽度和软度控制
float rimMask = smoothstep(1.0 - rimWidth, 1.0 - rimWidth + rimSoftness, fresnel);

// 强度曲线
float rim = pow(fresnel, rimPower) * rimMask * rimIntensity;

vec3 rimEffect = rim * rimColor.rgb;

Out.albedo = albedo;
Out.normal = normal;
Out.metallic = 0.0;
Out.roughness = 0.5;
Out.emissive = rimEffect;
Out.ao = 1.0;
}
}%

3. 彩虹边缘光

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
CCEffect %{
techniques:
- name: rainbow-rim
passes:
- vert: rainbow-vs:vert
frag: rainbow-fs:frag
properties: &props
mainTexture: { value: white }
baseColor: { value: [1, 1, 1, 1], editor: { type: color } }
rimPower: { value: 2.0, range: [0.1, 10] }
rimIntensity: { value: 1.0, range: [0, 5] }
rainbowSpeed: { value: 1.0, range: [0, 5] }
rainbowScale: { value: 1.0, range: [0.1, 5] }
saturation: { value: 1.0, range: [0, 2] }
}%

CCProgram rainbow-vs %{
#include <surface-vertex>
}%

CCProgram rainbow-fs %{
#include <surface-fragment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;
uniform float rimPower;
uniform float rimIntensity;
uniform float rainbowSpeed;
uniform float rainbowScale;
uniform float saturation;

void surf(in SurfaceIn In, inout SurfaceOut Out) {
vec4 albedo = texture(mainTexture, In.uv) * baseColor;

vec3 viewDir = normalize(cc_cameraPos.xyz - In.worldPos);
vec3 normal = normalize(In.worldNormal);

float fresnel = 1.0 - dot(viewDir, normal);
float rim = pow(fresnel, rimPower) * rimIntensity;

// 彩虹色计算
float hue = fract(cc_time.x * rainbowSpeed + In.worldPos.y * rainbowScale);
vec3 rainbowColor = hsv2rgb(vec3(hue, saturation, 1.0));

vec3 rimEffect = rim * rainbowColor;

Out.albedo = albedo;
Out.normal = normal;
Out.metallic = 0.0;
Out.roughness = 0.5;
Out.emissive = rimEffect;
Out.ao = 1.0;
}

// HSV到RGB转换
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
}%

🎨 高级边缘光效果

1. 能量脉冲边缘光

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
CCEffect %{
techniques:
- name: pulse-rim
passes:
- vert: pulse-vs:vert
frag: pulse-fs:frag
properties: &props
mainTexture: { value: white }
baseColor: { value: [1, 1, 1, 1], editor: { type: color } }
rimColor: { value: [0, 1, 1, 1], editor: { type: color } }
pulseSpeed: { value: 2.0, range: [0.1, 10] }
pulseAmplitude: { value: 0.5, range: [0, 1] }
baseIntensity: { value: 0.3, range: [0, 1] }
maxIntensity: { value: 2.0, range: [1, 5] }
rimPower: { value: 2.0, range: [0.1, 10] }
}%

CCProgram pulse-vs %{
#include <surface-vertex>
}%

CCProgram pulse-fs %{
#include <surface-fragment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;
uniform vec4 rimColor;
uniform float pulseSpeed;
uniform float pulseAmplitude;
uniform float baseIntensity;
uniform float maxIntensity;
uniform float rimPower;

void surf(in SurfaceIn In, inout SurfaceOut Out) {
vec4 albedo = texture(mainTexture, In.uv) * baseColor;

vec3 viewDir = normalize(cc_cameraPos.xyz - In.worldPos);
vec3 normal = normalize(In.worldNormal);

float fresnel = 1.0 - dot(viewDir, normal);
float rim = pow(fresnel, rimPower);

// 脉冲动画
float pulse = sin(cc_time.x * pulseSpeed) * pulseAmplitude + (1.0 - pulseAmplitude);
float intensity = baseIntensity + (maxIntensity - baseIntensity) * pulse;

// 添加次级脉冲
float secondaryPulse = sin(cc_time.x * pulseSpeed * 2.5 + 1.57) * 0.2 + 0.8;
intensity *= secondaryPulse;

vec3 rimEffect = rim * intensity * rimColor.rgb;

Out.albedo = albedo;
Out.normal = normal;
Out.metallic = 0.0;
Out.roughness = 0.5;
Out.emissive = rimEffect;
Out.ao = 1.0;
}
}%

2. 扫描线边缘光

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
CCEffect %{
techniques:
- name: scan-rim
passes:
- vert: scan-vs:vert
frag: scan-fs:frag
properties: &props
mainTexture: { value: white }
baseColor: { value: [1, 1, 1, 1], editor: { type: color } }
rimColor: { value: [0, 1, 1, 1], editor: { type: color } }
scanColor: { value: [1, 1, 0, 1], editor: { type: color } }
scanSpeed: { value: 1.0, range: [0.1, 5] }
scanWidth: { value: 0.2, range: [0.01, 1] }
scanIntensity: { value: 3.0, range: [1, 10] }
rimPower: { value: 2.0, range: [0.1, 10] }
rimIntensity: { value: 1.0, range: [0, 3] }
}%

CCProgram scan-vs %{
#include <surface-vertex>
}%

CCProgram scan-fs %{
#include <surface-fragment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;
uniform vec4 rimColor;
uniform vec4 scanColor;
uniform float scanSpeed;
uniform float scanWidth;
uniform float scanIntensity;
uniform float rimPower;
uniform float rimIntensity;

void surf(in SurfaceIn In, inout SurfaceOut Out) {
vec4 albedo = texture(mainTexture, In.uv) * baseColor;

vec3 viewDir = normalize(cc_cameraPos.xyz - In.worldPos);
vec3 normal = normalize(In.worldNormal);

float fresnel = 1.0 - dot(viewDir, normal);
float rim = pow(fresnel, rimPower) * rimIntensity;

// 扫描线计算
float scanPosition = fract(cc_time.x * scanSpeed);
float worldY = In.worldPos.y;

// 标准化世界Y坐标
float normalizedY = fract(worldY * 0.1); // 调整扫描密度

// 扫描线遮罩
float scanMask = 1.0 - smoothstep(scanPosition - scanWidth * 0.5,
scanPosition + scanWidth * 0.5,
normalizedY);

// 扫描强度
float scanEffect = scanMask * scanIntensity;

// 合并边缘光和扫描效果
vec3 finalRim = rim * rimColor.rgb + scanEffect * rim * scanColor.rgb;

Out.albedo = albedo;
Out.normal = normal;
Out.metallic = 0.0;
Out.roughness = 0.5;
Out.emissive = finalRim;
Out.ao = 1.0;
}
}%

3. 基于法线的多色边缘光

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
CCEffect %{
techniques:
- name: normal-based-rim
passes:
- vert: normal-rim-vs:vert
frag: normal-rim-fs:frag
properties: &props
mainTexture: { value: white }
baseColor: { value: [1, 1, 1, 1], editor: { type: color } }
topRimColor: { value: [1, 0, 0, 1], editor: { type: color } }
sideRimColor: { value: [0, 1, 0, 1], editor: { type: color } }
bottomRimColor: { value: [0, 0, 1, 1], editor: { type: color } }
rimPower: { value: 2.0, range: [0.1, 10] }
rimIntensity: { value: 1.0, range: [0, 5] }
colorMixRange: { value: 0.3, range: [0.1, 1] }
}%

CCProgram normal-rim-vs %{
#include <surface-vertex>
}%

CCProgram normal-rim-fs %{
#include <surface-fragment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;
uniform vec4 topRimColor;
uniform vec4 sideRimColor;
uniform vec4 bottomRimColor;
uniform float rimPower;
uniform float rimIntensity;
uniform float colorMixRange;

void surf(in SurfaceIn In, inout SurfaceOut Out) {
vec4 albedo = texture(mainTexture, In.uv) * baseColor;

vec3 viewDir = normalize(cc_cameraPos.xyz - In.worldPos);
vec3 normal = normalize(In.worldNormal);

float fresnel = 1.0 - dot(viewDir, normal);
float rim = pow(fresnel, rimPower) * rimIntensity;

// 基于法线方向的颜色混合
float upWeight = smoothstep(-colorMixRange, colorMixRange, normal.y);
float downWeight = smoothstep(-colorMixRange, colorMixRange, -normal.y);
float sideWeight = 1.0 - upWeight - downWeight;

// 确保权重归一化
float totalWeight = upWeight + downWeight + sideWeight;
upWeight /= totalWeight;
downWeight /= totalWeight;
sideWeight /= totalWeight;

// 颜色混合
vec3 finalRimColor = upWeight * topRimColor.rgb +
downWeight * bottomRimColor.rgb +
sideWeight * sideRimColor.rgb;

vec3 rimEffect = rim * finalRimColor;

Out.albedo = albedo;
Out.normal = normal;
Out.metallic = 0.0;
Out.roughness = 0.5;
Out.emissive = rimEffect;
Out.ao = 1.0;
}
}%

📱 移动端优化

性能优化版边缘光

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
CCEffect %{
techniques:
- name: optimized-rim
passes:
- vert: opt-rim-vs:vert
frag: opt-rim-fs:frag
properties: &props
mainTexture: { value: white }
baseColor: { value: [1, 1, 1, 1], editor: { type: color } }
rimColor: { value: [0, 1, 1, 1], editor: { type: color } }
rimParams: { value: [2.0, 1.0, 0.0, 0.0] } # power, intensity, unused, unused
}%

CCProgram opt-rim-vs %{
#include <surface-vertex>

out vec3 v_viewDir;

void vert() {
SurfaceIn surfaceIn;
VertexInput(surfaceIn);

// 在顶点着色器中预计算视线方向
vec3 worldPos = (cc_matWorld * vec4(surfaceIn.position.xyz, 1.0)).xyz;
v_viewDir = normalize(cc_cameraPos.xyz - worldPos);

SurfaceVertex(surfaceIn);
}
}%

CCProgram opt-rim-fs %{
#include <surface-fragment>

// 移动端精度优化
#ifdef CC_PLATFORM_MOBILE
precision mediump float;
#endif

uniform sampler2D mainTexture;
uniform vec4 baseColor;
uniform vec4 rimColor;
uniform vec4 rimParams; // x: power, y: intensity

in vec3 v_viewDir;

void surf(in SurfaceIn In, inout SurfaceOut Out) {
vec4 albedo = texture(mainTexture, In.uv) * baseColor;

vec3 normal = normalize(In.worldNormal);

// 简化的边缘光计算
float fresnel = 1.0 - max(dot(v_viewDir, normal), 0.0);

// 使用预设参数减少uniform访问
float rim = pow(fresnel, rimParams.x) * rimParams.y;

vec3 rimEffect = rim * rimColor.rgb;

Out.albedo = albedo;
Out.normal = normal;
Out.metallic = 0.0;
Out.roughness = 0.5;
Out.emissive = rimEffect;
Out.ao = 1.0;
}
}%

🔧 TypeScript集成

边缘光控制器

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { Component, Material, Vec4, _decorator } from 'cc';

const { ccclass, property, menu } = _decorator;

@ccclass('RimLightController')
@menu('Custom/RimLightController')
export class RimLightController extends Component {

@property({ type: Vec4, displayName: '边缘光颜色' })
public rimColor: Vec4 = new Vec4(0, 1, 1, 1);

@property({ range: [0.1, 10], displayName: '边缘光强度曲线' })
public rimPower: number = 2.0;

@property({ range: [0, 5], displayName: '边缘光强度' })
public rimIntensity: number = 1.0;

@property({ range: [0.1, 5], displayName: '脉冲速度' })
public pulseSpeed: number = 2.0;

@property({ displayName: '启用脉冲' })
public enablePulse: boolean = false;

private _material: Material | null = null;
private _time: number = 0;

onLoad() {
const renderer = this.getComponent('cc.MeshRenderer');
if (renderer && renderer.material) {
this._material = renderer.material;
}
}

update(deltaTime: number) {
if (!this._material) return;

// 更新基础属性
this._material.setProperty('rimColor', this.rimColor);
this._material.setProperty('rimPower', this.rimPower);

// 脉冲效果
if (this.enablePulse) {
this._time += deltaTime;
const pulse = Math.sin(this._time * this.pulseSpeed) * 0.5 + 0.5;
const dynamicIntensity = this.rimIntensity * (0.5 + pulse);
this._material.setProperty('rimIntensity', dynamicIntensity);
} else {
this._material.setProperty('rimIntensity', this.rimIntensity);
}
}

// 动态切换边缘光类型
switchToRainbow() {
if (this._material) {
// 这里可以切换到彩虹边缘光材质
console.log('切换到彩虹边缘光');
}
}

switchToScan() {
if (this._material) {
// 这里可以切换到扫描线边缘光材质
console.log('切换到扫描线边缘光');
}
}
}

📖 本章总结

通过本章学习,我们掌握了:

  • ✅ 边缘光的菲涅尔反射原理
  • ✅ 基础和高级边缘光效果实现
  • ✅ 特殊边缘光变体(彩虹、脉冲、扫描线)
  • ✅ 移动端性能优化技巧
  • ✅ TypeScript集成和动态控制

🚀 下一步学习

掌握了边缘光Surface Shader后,建议继续学习:
👉 第8.3章:溶解特效Surface Shader

💡 实践练习

  1. 创建一个能量护盾的边缘光效果
  2. 实现角色轮廓高亮系统
  3. 开发UI元素的边缘光特效

系列导航