第4.4章:卡通着色器实现

卡通着色器(Toon Shader)是非真实感渲染(NPR)的重要技术,通过特殊的光照模型和着色技术实现动画风格的渲染效果。本章将详细讲解如何在Cocos Creator中实现卡通风格的着色器。

🎯 学习目标

通过本章学习,你将掌握:

  • 卡通渲染的基本原理和特�?- 卡通风格光照模型的实现
  • 分层阴影和色带技�?- 描边效果的实现方�?- 卡通材质参数的调节技�?

🎨 卡通渲染基本原�?

卡通渲染特�?

卡通渲染与真实感渲染的主要区别�?

  1. *离散化光�?: 光照被分成几个明确的层次
  2. *硬边缘阴�?: 阴影边界清晰,没有渐�?3. *简化高�?: 高光通常是纯白色的圆形或椭圆�?4. 描边效果: 物体边缘有明显的轮廓�?5. 色彩饱和: 颜色鲜艳,对比度�?

核心技术要�?

1
2
3
4
5
6
7
8
9
10
11
// 卡通渲染的核心函数
float toonLighting(float NdotL, int levels) {
// 将连续的光照值离散化
float lightLevel = floor(NdotL * float(levels)) / float(levels);
return lightLevel;
}

float toonSpecular(float NdotH, float shininess) {
// 硬边缘高�? float spec = pow(max(NdotH, 0.0), shininess);
return step(0.5, spec);
}

🌟 基础卡通着色器实现

简单卡通着色器

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
71
72
73
74
75
76
CCEffect %{
techniques:
- name: toon-basic
passes:
- vert: vs:vert
frag: fs:frag
properties:
mainTexture: { value: white }
mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
shadowColor: { value: [0.5, 0.5, 0.8, 1], editor: { type: color } }
lightLevels: { value: 3, editor: { range: [2, 8], step: 1 } }
outlineWidth: { value: 0.1, editor: { range: [0, 0.5] } }
outlineColor: { value: [0, 0, 0, 1], editor: { type: color } }
}%

CCProgram vs %{
precision highp float;
#include <builtin/uniforms/cc-global>
#include <builtin/uniforms/cc-local>

in vec3 a_position;
in vec3 a_normal;
in vec2 a_texCoord;

out vec2 v_uv;
out vec3 v_worldNormal;
out vec3 v_worldPos;

vec4 vert () {
vec4 pos = vec4(a_position, 1);
vec4 worldPos = cc_matWorld * pos;

v_worldPos = worldPos.xyz;
v_worldNormal = normalize((cc_matWorldIT * vec4(a_normal, 0)).xyz);
v_uv = a_texCoord;

return cc_matViewProj * worldPos;
}
}%

CCProgram fs %{
precision mediump float;
#include <builtin/uniforms/cc-global>

in vec2 v_uv;
in vec3 v_worldNormal;
in vec3 v_worldPos;

uniform sampler2D mainTexture;
uniform ToonParams {
vec4 mainColor;
vec4 shadowColor;
float lightLevels;
float outlineWidth;
vec4 outlineColor;
};

vec4 frag () {
vec4 texColor = texture(mainTexture, v_uv);

// 计算光照方向
vec3 lightDir = normalize(cc_mainLitDir.xyz);
vec3 normal = normalize(v_worldNormal);

// 计算Lambert光照
float NdotL = dot(normal, lightDir);

// 卡通化光照
float toonFactor = floor((NdotL * 0.5 + 0.5) * lightLevels) / lightLevels;

// 颜色混合
vec4 lightColor = mix(shadowColor, mainColor, toonFactor);

return texColor * lightColor;
}
}%

多级光照卡通着色器

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
// 更复杂的多级光照实现
CCProgram toon-advanced-fs %{
precision mediump float;
#include <builtin/uniforms/cc-global>

in vec2 v_uv;
in vec3 v_worldNormal;
in vec3 v_worldPos;

uniform sampler2D mainTexture;
uniform sampler2D rampTexture; // 光照渐变纹理
uniform AdvancedToonParams {
vec4 mainColor;
vec4 shadowColor;
vec4 highlightColor;
float lightLevels;
float shadowThreshold;
float highlightThreshold;
float softness;
};

vec4 frag () {
vec4 texColor = texture(mainTexture, v_uv);
vec3 normal = normalize(v_worldNormal);
vec3 lightDir = normalize(cc_mainLitDir.xyz);
vec3 viewDir = normalize(cc_cameraPos.xyz - v_worldPos);

// 基础光照计算
float NdotL = dot(normal, lightDir) * 0.5 + 0.5;

// 使用渐变纹理进行光照查找
vec4 rampColor = texture(rampTexture, vec2(NdotL, 0.5));

// 分层光照
vec4 finalColor;
if (NdotL < shadowThreshold) {
finalColor = shadowColor;
} else if (NdotL > highlightThreshold) {
finalColor = highlightColor;
} else {
float t = (NdotL - shadowThreshold) / (highlightThreshold - shadowThreshold);
finalColor = mix(shadowColor, mainColor, smoothstep(0.0, 1.0, t));
}

// 应用软化
finalColor = mix(rampColor, finalColor, softness);

return texColor * finalColor;
}
}%

💡 分层阴影和色带技�?

离散化光照函�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 多种离散化方�?float discretizeLighting(float lightValue, float levels, int method) {
if (method == 0) {
// 硬分�? return floor(lightValue * levels) / levels;
} else if (method == 1) {
// 软分�? float step = 1.0 / levels;
float level = floor(lightValue / step);
float t = (lightValue - level * step) / step;
return (level + smoothstep(0.4, 0.6, t)) / levels;
} else {
// 基于阈值的分割
float thresholds[4] = float[](0.0, 0.3, 0.6, 1.0);
for (int i = 0; i < 3; i++) {
if (lightValue <= thresholds[i + 1]) {
return thresholds[i];
}
}
return 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
// 使用1D渐变纹理控制光照
CCProgram ramp-lighting-fs %{
precision mediump float;

in vec2 v_uv;
in vec3 v_worldNormal;

uniform sampler2D mainTexture;
uniform sampler2D lightRamp; // 1D渐变纹理
uniform sampler2D shadowRamp; // 阴影渐变纹理
uniform RampParams {
vec4 mainColor;
float rampOffset;
float rampScale;
float shadowIntensity;
};

vec4 frag () {
vec4 texColor = texture(mainTexture, v_uv);
vec3 normal = normalize(v_worldNormal);
vec3 lightDir = normalize(cc_mainLitDir.xyz);

// 计算光照强度
float NdotL = dot(normal, lightDir) * 0.5 + 0.5;

// 应用偏移和缩�? float rampU = NdotL * rampScale + rampOffset;
rampU = clamp(rampU, 0.0, 1.0);

// 从渐变纹理采�? vec4 lightColor = texture(lightRamp, vec2(rampU, 0.5));
vec4 shadowColor = texture(shadowRamp, vec2(rampU, 0.5));

// 混合光照和阴�? vec4 finalColor = mix(shadowColor, lightColor, lightColor.a);
finalColor.rgb *= mainColor.rgb;

return texColor * finalColor;
}
}%

多层次阴�?

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
// 三层阴影系统
CCProgram multi-shadow-fs %{
precision mediump float;
#include <builtin/uniforms/cc-global>

in vec2 v_uv;
in vec3 v_worldNormal;
in vec3 v_worldPos;

uniform sampler2D mainTexture;
uniform MultiShadowParams {
vec4 lightColor;
vec4 midtoneColor;
vec4 shadowColor;
vec4 darkShadowColor;
float lightThreshold;
float shadowThreshold;
float darkThreshold;
float feather;
};

vec4 frag () {
vec4 texColor = texture(mainTexture, v_uv);
vec3 normal = normalize(v_worldNormal);
vec3 lightDir = normalize(cc_mainLitDir.xyz);

float NdotL = dot(normal, lightDir) * 0.5 + 0.5;

vec4 finalColor;

// 三层阴影判断
if (NdotL >= lightThreshold) {
// 亮部
float t = smoothstep(lightThreshold - feather, lightThreshold + feather, NdotL);
finalColor = mix(midtoneColor, lightColor, t);
} else if (NdotL >= shadowThreshold) {
// 中间�? float t = smoothstep(shadowThreshold - feather, shadowThreshold + feather, NdotL);
finalColor = mix(shadowColor, midtoneColor, t);
} else if (NdotL >= darkThreshold) {
// 阴影
float t = smoothstep(darkThreshold - feather, darkThreshold + feather, NdotL);
finalColor = mix(darkShadowColor, shadowColor, t);
} else {
// 深阴�? finalColor = darkShadowColor;
}

return texColor * finalColor;
}
}%

�?卡通高光实�?

硬边缘高�?

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
// 卡通风格的高光计算
CCProgram toon-specular-fs %{
precision mediump float;
#include <builtin/uniforms/cc-global>

in vec2 v_uv;
in vec3 v_worldNormal;
in vec3 v_worldPos;

uniform sampler2D mainTexture;
uniform SpecularParams {
vec4 mainColor;
vec4 specularColor;
float shininess;
float specularThreshold;
float specularSoftness;
float specularSize;
};

vec4 frag () {
vec4 texColor = texture(mainTexture, v_uv);
vec3 normal = normalize(v_worldNormal);
vec3 lightDir = normalize(cc_mainLitDir.xyz);
vec3 viewDir = normalize(cc_cameraPos.xyz - v_worldPos);
vec3 halfDir = normalize(lightDir + viewDir);

// 基础光照
float NdotL = max(dot(normal, lightDir), 0.0);

// 高光计算
float NdotH = max(dot(normal, halfDir), 0.0);
float specular = pow(NdotH, shininess);

// 卡通化高光
float toonSpecular = smoothstep(
specularThreshold - specularSoftness,
specularThreshold + specularSoftness,
specular
);

// 高光形状控制
float specularMask = 1.0 - length(v_uv - 0.5) * specularSize;
toonSpecular *= step(0.0, specularMask);

// 最终颜�? vec4 diffuse = texColor * mainColor * NdotL;
vec4 spec = specularColor * toonSpecular;

return diffuse + spec;
}
}%

各向异性高�?

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
// 卡通风格各向异性高光(如头发)
CCProgram anisotropic-toon-fs %{
precision mediump float;
#include <builtin/uniforms/cc-global>

in vec2 v_uv;
in vec3 v_worldNormal;
in vec3 v_worldPos;
in vec3 v_worldTangent;

uniform sampler2D mainTexture;
uniform sampler2D noiseTexture;
uniform AnisotropicParams {
vec4 mainColor;
vec4 specColor1;
vec4 specColor2;
float roughnessU;
float roughnessV;
float specularShift;
float noiseScale;
};

float anisotropicSpecular(vec3 H, vec3 T, float roughness) {
float TdotH = dot(T, H);
return exp(-2.0 * (TdotH * TdotH) / (roughness * roughness));
}

vec4 frag () {
vec4 texColor = texture(mainTexture, v_uv);
vec3 normal = normalize(v_worldNormal);
vec3 tangent = normalize(v_worldTangent);
vec3 lightDir = normalize(cc_mainLitDir.xyz);
vec3 viewDir = normalize(cc_cameraPos.xyz - v_worldPos);
vec3 halfDir = normalize(lightDir + viewDir);

// 噪声偏移切线
float noise = texture(noiseTexture, v_uv * noiseScale).r;
vec3 shiftedTangent = tangent + normal * (noise - 0.5) * specularShift;

// 两层高光
float spec1 = anisotropicSpecular(halfDir, shiftedTangent, roughnessU);
float spec2 = anisotropicSpecular(halfDir, tangent, roughnessV);

// 卡通化处理
spec1 = step(0.5, spec1);
spec2 = step(0.3, spec2);

vec4 specular = spec1 * specColor1 + spec2 * specColor2;

return texColor * mainColor + specular;
}
}%

🖌�?描边效果实现

基于法线的描�?

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
# 双Pass描边着色器
CCEffect %{
techniques:
- name: toon-outline
passes:
# 第一个Pass:绘制描�? - vert: outline-vs:vert
frag: outline-fs:frag
rasterizerState:
cullMode: front
depthStencilState:
depthTest: true
depthWrite: true
properties:
outlineWidth: { value: 0.02, editor: { range: [0, 0.1] } }
outlineColor: { value: [0, 0, 0, 1], editor: { type: color } }
# 第二个Pass:正常渲�? - vert: main-vs:vert
frag: main-fs:frag
rasterizerState:
cullMode: back
depthStencilState:
depthTest: true
depthWrite: true
properties:
mainTexture: { value: white }
mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
}%

CCProgram outline-vs %{
precision highp float;
#include <builtin/uniforms/cc-global>
#include <builtin/uniforms/cc-local>

in vec3 a_position;
in vec3 a_normal;

uniform OutlineParams {
float outlineWidth;
};

vec4 vert () {
vec3 pos = a_position;
vec3 normal = normalize(a_normal);

// 沿法线方向膨胀顶点
pos += normal * outlineWidth;

vec4 worldPos = cc_matWorld * vec4(pos, 1);
return cc_matViewProj * worldPos;
}
}%

CCProgram outline-fs %{
precision mediump float;

uniform OutlineParams {
vec4 outlineColor;
};

vec4 frag () {
return outlineColor;
}
}%

基于屏幕空间的描�?

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
// 后处理描边效�?CCProgram screen-outline-fs %{
precision mediump float;

in vec2 v_uv;

uniform sampler2D sceneTexture;
uniform sampler2D depthTexture;
uniform sampler2D normalTexture;
uniform OutlineParams {
float outlineThickness;
float depthThreshold;
float normalThreshold;
vec4 outlineColor;
vec2 screenSize;
};

vec4 frag () {
vec2 texelSize = 1.0 / screenSize;

// 采样周围像素的深度和法线
float depth = texture(depthTexture, v_uv).r;
vec3 normal = texture(normalTexture, v_uv).rgb;

// Sobel边缘检�? float depthDiff = 0.0;
float normalDiff = 0.0;

for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
if (x == 0 && y == 0) continue;

vec2 offset = vec2(float(x), float(y)) * texelSize * outlineThickness;
vec2 sampleUV = v_uv + offset;

float sampleDepth = texture(depthTexture, sampleUV).r;
vec3 sampleNormal = texture(normalTexture, sampleUV).rgb;

depthDiff += abs(depth - sampleDepth);
normalDiff += length(normal - sampleNormal);
}
}

// 边缘检�? float edgeDepth = step(depthThreshold, depthDiff);
float edgeNormal = step(normalThreshold, normalDiff);
float edge = max(edgeDepth, edgeNormal);

// 混合原图和描�? vec4 sceneColor = texture(sceneTexture, v_uv);
return mix(sceneColor, outlineColor, edge);
}
}%

智能描边算法

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
// 基于距离场的描边
CCProgram sdf-outline-fs %{
precision mediump float;

in vec2 v_uv;

uniform sampler2D mainTexture;
uniform sampler2D distanceField;
uniform SDFOutlineParams {
vec4 mainColor;
vec4 outlineColor;
float outlineWidth;
float smoothness;
float threshold;
};

vec4 frag () {
vec4 texColor = texture(mainTexture, v_uv);
float dist = texture(distanceField, v_uv).r;

// 距离场描�? float edge = smoothstep(threshold - smoothness, threshold + smoothness, dist);
float outline = smoothstep(
threshold - outlineWidth - smoothness,
threshold - outlineWidth + smoothness,
dist
);

// 组合颜色
vec4 finalColor = mix(outlineColor, texColor * mainColor, edge);
finalColor.a = outline;

return finalColor;
}
}%

🎭 完整卡通着色器实例

综合卡通着色器

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
CCEffect %{
techniques:
- name: complete-toon
passes:
# 描边Pass
- vert: outline-vs:vert
frag: outline-fs:frag
rasterizerState:
cullMode: front
depthStencilState:
depthTest: true
depthWrite: true
properties: &outline-props
outlineWidth: { value: 0.02, editor: { range: [0, 0.1] } }
outlineColor: { value: [0, 0, 0, 1], editor: { type: color } }
# 主渲染Pass
- vert: toon-vs:vert
frag: toon-fs:frag
rasterizerState:
cullMode: back
depthStencilState:
depthTest: true
depthWrite: true
properties: &toon-props
mainTexture: { value: white }
rampTexture: { value: white }
mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
shadowColor: { value: [0.5, 0.5, 0.8, 1], editor: { type: color } }
specularColor: { value: [1, 1, 1, 1], editor: { type: color } }
lightLevels: { value: 3, editor: { range: [2, 8], step: 1 } }
specularThreshold: { value: 0.9, editor: { range: [0, 1] } }
specularSoftness: { value: 0.1, editor: { range: [0, 0.5] } }
rimLightColor: { value: [1, 1, 1, 1], editor: { type: color } }
rimLightPower: { value: 2.0, editor: { range: [0, 10] } }
}%

实际应用案例

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
// 卡通材质控制脚�?@ccclass('ToonMaterialController')
export class ToonMaterialController extends Component {
@property(MeshRenderer)
meshRenderer: MeshRenderer = null!;

@property
animateColors: boolean = false;

@property({ type: Color })
dayColor: Color = Color.WHITE;

@property({ type: Color })
nightColor: Color = Color.BLUE;

@property
timeOfDay: number = 0.5;

private material: Material | null = null;

start() {
this.material = this.meshRenderer.getMaterial(1); // 主渲染材�? }

update(dt: number) {
if (!this.material) return;

if (this.animateColors) {
// 时间循环
this.timeOfDay += dt * 0.1;
if (this.timeOfDay > 1.0) this.timeOfDay = 0.0;

// 颜色插�? const currentColor = Color.lerp(new Color(), this.dayColor, this.nightColor,
Math.sin(this.timeOfDay * Math.PI * 2) * 0.5 + 0.5);

this.material.setProperty('mainColor', currentColor);

// 调整光照层次
const levels = Math.floor(3 + Math.sin(this.timeOfDay * Math.PI) * 2);
this.material.setProperty('lightLevels', levels);
}
}

// 切换到不同的卡通风�? public switchToonStyle(style: string) {
if (!this.material) return;

switch (style) {
case 'classic':
this.material.setProperty('lightLevels', 3);
this.material.setProperty('specularThreshold', 0.9);
break;
case 'soft':
this.material.setProperty('lightLevels', 5);
this.material.setProperty('specularThreshold', 0.7);
break;
case 'hard':
this.material.setProperty('lightLevels', 2);
this.material.setProperty('specularThreshold', 0.95);
break;
}
}
}

💡 优化建议

性能优化

  1. 减少Pass数量: 考虑在单个Pass中实现描�?2. *简化计�?: 避免复杂的数学函�?3. 合理使用精度: 在移动设备上使用mediump
  2. 纹理优化: 使用小尺寸的渐变纹理

视觉优化

  1. 颜色选择: 使用饱和度高的颜�?2. *对比度控�?: 确保光暗部分有明显区�?3. 描边宽度: 根据观察距离调整描边粗细
  2. 细节平衡: 保持适度的细节,不要过于复杂

📚 总结

卡通着色器的核心在于:

技术要�?- *离散化光�?: 将连续光照转换为分层效果

  • *硬边缘处�?: 创建清晰的明暗边�?- *描边技�?: 增强物体轮廓的视觉效�?- 色彩控制: 使用鲜艳饱和的颜�?

应用场景

  • 动画风格游戏
  • 儿童向应�?- 艺术表现项目
  • 性能受限的场�?
    掌握卡通着色器技术能够创造出独特的艺术风格,为项目增添丰富的视觉表现力�?

*下一步学�?