第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; } 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; 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; 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); 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
💡 实践练习
- 创建一个能量护盾的边缘光效果
- 实现角色轮廓高亮系统
- 开发UI元素的边缘光特效
系列导航