第3.2章:着色器材质系统

材质系统是连接着色器和渲染对象的桥梁,理解材质系统的工作原理对于高效开发着色器至关重要。本章将深入讲解Cocos Creator中的材质系统架构、使用方法和最佳实践。

🎯 学习目标

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

  • 材质系统的整体架构和工作原理
  • Material和MaterialInstance的区别和使用
  • 程序化创建和管理材质
  • 材质参数的动态修改和优化
  • 材质资源的管理和复用策略
  • 多Pass渲染和材质变体系统

🏗️ 材质系统架构

系统组成概览

1
2
3
4
5
6
7
8
9
10
11
12
13
graph TD
A[Effect文件] --> B[Material资产]
B --> C[MaterialInstance实例]
C --> D[MeshRenderer组件]
C --> E[Sprite组件]

F[Technique技术] --> G[Pass通道]
G --> H[Properties属性]
G --> I[Shader程序]

A --> F
B --> J[参数配置]
C --> K[运行时修改]

核心组件说明

组件作用生命周期
Effect着色器定义文件编译时固定
Material材质资产模板设计时创建
MaterialInstance材质实例运行时动态
Technique渲染技术编译时选择
Pass渲染通道每帧执行

📄 Material vs MaterialInstance

Material(材质模板)

Material是静态的材质配置模板,在编辑器中创建和配置:

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
// Material特点:
// 1. 静态配置,多个对象可以共享
// 2. 修改会影响所有使用该材质的对象
// 3. 性能优化:减少状态切换
// 4. 资源共享:节省内存

@ccclass('MaterialSharing')
export class MaterialSharing extends Component {
@property(Material)
sharedMaterial: Material = null!;

@property([MeshRenderer])
renderers: MeshRenderer[] = [];

start() {
// 多个对象共享同一材质
this.renderers.forEach(renderer => {
renderer.setMaterial(this.sharedMaterial, 0);
});
}

// 修改shared material会影响所有对象
changeSharedColor(color: Color) {
this.sharedMaterial.setProperty('mainColor', color);
// 所有使用此材质的对象都会改变颜色
}
}

MaterialInstance(材质实例)

MaterialInstance是动态的材质实例,允许独立修改参数:

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
@ccclass('MaterialInstancing')
export class MaterialInstancing extends Component {
@property(Material)
baseMaterial: Material = null!;

@property(MeshRenderer)
meshRenderer: MeshRenderer = null!;

private materialInstance: MaterialInstance | null = null;

start() {
// 创建材质实例
this.materialInstance = new MaterialInstance({
parent: this.baseMaterial
});

// 应用到渲染器
this.meshRenderer.setMaterialInstance(this.materialInstance, 0);
}

// 修改实例参数不会影响其他对象
changeInstanceColor(color: Color) {
if (this.materialInstance) {
this.materialInstance.setProperty('mainColor', color);
// 只有当前对象的颜色改变
}
}

onDestroy() {
// 清理材质实例
if (this.materialInstance) {
this.materialInstance.destroy();
}
}
}

选择原则

1
2
3
4
5
6
7
8
9
10
11
// 使用Material的场景:
// • 静态环境对象(建筑、地形)
// • 相同外观的批量对象
// • UI元素(按钮、图标)
// • 性能敏感的场景

// 使用MaterialInstance的场景:
// • 角色换装系统
// • 特效动画
// • 动态颜色变化
// • 独立参数控制的对象

🔧 程序化材质创建

动态创建Material

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
@ccclass('DynamicMaterialCreator')
export class DynamicMaterialCreator extends Component {
@property(EffectAsset)
effectAsset: EffectAsset = null!;

@property(Texture2D)
defaultTexture: Texture2D = null!;

createBasicMaterial(): Material {
// 创建新的Material
const material = new Material();

// 设置Effect
material.initialize({
effectAsset: this.effectAsset,
technique: 0 // 使用第一个technique
});

// 设置基础属性
material.setProperty('mainTexture', this.defaultTexture);
material.setProperty('mainColor', Color.WHITE);

return material;
}

createPBRMaterial(albedo: Texture2D, normal: Texture2D, metallic: number, roughness: number): Material {
const material = new Material();

material.initialize({
effectAsset: this.effectAsset,
technique: 0,
defines: {
USE_ALBEDO_MAP: true,
USE_NORMAL_MAP: true,
USE_PBR: true
}
});

// 设置PBR参数
material.setProperty('albedoMap', albedo);
material.setProperty('normalMap', normal);
material.setProperty('metallic', metallic);
material.setProperty('roughness', roughness);

return material;
}
}

材质变体系统

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
@ccclass('MaterialVariantSystem')
export class MaterialVariantSystem extends Component {
@property(Material)
baseMaterial: Material = null!;

// 预定义的材质变体
private variants: Map<string, Material> = new Map();

start() {
this.createVariants();
}

private createVariants() {
// 创建不同的材质变体
this.variants.set('red', this.createColorVariant(Color.RED));
this.variants.set('blue', this.createColorVariant(Color.BLUE));
this.variants.set('green', this.createColorVariant(Color.GREEN));

// 创建特殊效果变体
this.variants.set('glow', this.createGlowVariant());
this.variants.set('transparent', this.createTransparentVariant());
}

private createColorVariant(color: Color): Material {
const variant = Material.createWithBuiltin('builtin-unlit', 0);
variant.setProperty('mainColor', color);
return variant;
}

private createGlowVariant(): Material {
const variant = new Material();
variant.initialize({
effectAsset: this.baseMaterial.effectAsset!,
technique: 0,
defines: {
USE_GLOW: true,
USE_EMISSION: true
}
});

variant.setProperty('emissionColor', new Color(255, 255, 0, 255));
variant.setProperty('glowIntensity', 2.0);

return variant;
}

private createTransparentVariant(): Material {
const variant = new Material();
variant.initialize({
effectAsset: this.baseMaterial.effectAsset!,
technique: 1, // 使用transparent technique
defines: {
USE_ALPHA_TEST: false
}
});

variant.setProperty('mainColor', new Color(255, 255, 255, 128));

return variant;
}

// 切换材质变体
switchVariant(variantName: string, renderer: MeshRenderer) {
const variant = this.variants.get(variantName);
if (variant) {
renderer.setMaterial(variant, 0);
}
}
}

🎨 多Pass渲染系统

多Pass材质配置

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
@ccclass('MultiPassRenderer')
export class MultiPassRenderer extends Component {
@property(Material)
multiPassMaterial: Material = null!;

@property(MeshRenderer)
meshRenderer: MeshRenderer = null!;

start() {
this.setupMultiPassMaterial();
}

private setupMultiPassMaterial() {
// 配置多Pass材质
const material = new Material();

material.initialize({
effectAsset: this.multiPassMaterial.effectAsset!,
technique: 0, // 多Pass technique
defines: {
MULTI_PASS: true,
PASS_COUNT: 3
}
});

// Pass 1: 基础渲染
material.setProperty('pass1_color', Color.RED);
material.setProperty('pass1_blend', BlendState.NORMAL);

// Pass 2: 边缘光效
material.setProperty('pass2_color', Color.YELLOW);
material.setProperty('pass2_blend', BlendState.ADDITIVE);
material.setProperty('rimPower', 2.0);

// Pass 3: 细节纹理
material.setProperty('pass3_texture', this.detailTexture);
material.setProperty('pass3_blend', BlendState.MULTIPLY);

this.meshRenderer.setMaterial(material, 0);
}
}

条件Pass系统

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
77
78
79
80
81
82
83
84
85
86
87
88
89
@ccclass('ConditionalPassSystem')
export class ConditionalPassSystem extends Component {
@property(Material)
baseMaterial: Material = null!;

// Pass控制标志
@property
enableOutline: boolean = false;

@property
enableGlow: boolean = false;

@property
enableShadow: boolean = true;

private currentMaterial: Material | null = null;

start() {
this.updateMaterial();
}

onEnable() {
this.updateMaterial();
}

private updateMaterial() {
// 根据条件动态构建defines
const defines: Record<string, any> = {};

if (this.enableOutline) {
defines.USE_OUTLINE = true;
defines.OUTLINE_WIDTH = 0.01;
}

if (this.enableGlow) {
defines.USE_GLOW = true;
defines.GLOW_INTENSITY = 1.5;
}

if (this.enableShadow) {
defines.RECEIVE_SHADOW = true;
}

// 创建新材质实例
const material = new Material();
material.initialize({
effectAsset: this.baseMaterial.effectAsset!,
technique: 0,
defines: defines
});

// 复制基础属性
this.copyBaseMaterialProperties(material);

// 应用材质
const renderer = this.getComponent(MeshRenderer);
if (renderer) {
renderer.setMaterial(material, 0);
}

// 清理旧材质
if (this.currentMaterial) {
this.currentMaterial.destroy();
}
this.currentMaterial = material;
}

private copyBaseMaterialProperties(target: Material) {
// 复制基础材质的所有属性
const sourceProps = this.baseMaterial.passes[0].properties;
Object.keys(sourceProps).forEach(key => {
const value = this.baseMaterial.getProperty(key);
if (value !== undefined) {
target.setProperty(key, value);
}
});
}

// 运行时切换Pass
toggleOutline() {
this.enableOutline = !this.enableOutline;
this.updateMaterial();
}

toggleGlow() {
this.enableGlow = !this.enableGlow;
this.updateMaterial();
}
}

🔄 材质资源管理

材质池管理

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
@ccclass('MaterialPool')
export class MaterialPool {
private static instance: MaterialPool | null = null;
private pools: Map<string, Material[]> = new Map();
private inUse: Set<Material> = new Set();

static getInstance(): MaterialPool {
if (!this.instance) {
this.instance = new MaterialPool();
}
return this.instance;
}

// 获取材质(从池中或创建新的)
getMaterial(effectName: string, config?: any): Material {
const pool = this.pools.get(effectName);

if (pool && pool.length > 0) {
const material = pool.pop()!;
this.inUse.add(material);
this.configureMaterial(material, config);
return material;
}

// 创建新材质
const material = this.createMaterial(effectName, config);
this.inUse.add(material);
return material;
}

// 归还材质到池中
returnMaterial(material: Material, effectName: string) {
if (this.inUse.has(material)) {
this.inUse.delete(material);

// 重置材质状态
this.resetMaterial(material);

// 放回池中
if (!this.pools.has(effectName)) {
this.pools.set(effectName, []);
}
this.pools.get(effectName)!.push(material);
}
}

private createMaterial(effectName: string, config?: any): Material {
const material = Material.createWithBuiltin(effectName, 0);
this.configureMaterial(material, config);
return material;
}

private configureMaterial(material: Material, config?: any) {
if (config) {
Object.keys(config).forEach(key => {
material.setProperty(key, config[key]);
});
}
}

private resetMaterial(material: Material) {
// 重置到默认状态
material.setProperty('mainColor', Color.WHITE);
material.setProperty('metallic', 0.0);
material.setProperty('roughness', 1.0);
// ... 其他默认属性
}

// 清理未使用的材质
cleanup() {
this.pools.forEach((pool, effectName) => {
pool.forEach(material => {
if (material.isValid) {
material.destroy();
}
});
pool.length = 0;
});
}
}

// 使用示例
@ccclass('MaterialUser')
export class MaterialUser extends Component {
private myMaterial: Material | null = null;

start() {
// 从池中获取材质
this.myMaterial = MaterialPool.getInstance().getMaterial('builtin-standard', {
mainColor: Color.RED,
metallic: 0.5
});

const renderer = this.getComponent(MeshRenderer);
if (renderer && this.myMaterial) {
renderer.setMaterial(this.myMaterial, 0);
}
}

onDestroy() {
// 归还材质到池中
if (this.myMaterial) {
MaterialPool.getInstance().returnMaterial(this.myMaterial, 'builtin-standard');
this.myMaterial = null;
}
}
}

材质预加载系统

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
@ccclass('MaterialPreloader')
export class MaterialPreloader extends Component {
@property([EffectAsset])
effectAssets: EffectAsset[] = [];

@property([Texture2D])
commonTextures: Texture2D[] = [];

private preloadedMaterials: Map<string, Material> = new Map();

async onLoad() {
await this.preloadMaterials();
}

private async preloadMaterials() {
const loadPromises: Promise<void>[] = [];

this.effectAssets.forEach(effect => {
const promise = this.preloadEffect(effect);
loadPromises.push(promise);
});

await Promise.all(loadPromises);
console.log('All materials preloaded');
}

private async preloadEffect(effect: EffectAsset): Promise<void> {
return new Promise((resolve) => {
// 创建基础材质
const material = new Material();
material.initialize({
effectAsset: effect,
technique: 0
});

// 预设纹理
if (this.commonTextures.length > 0) {
material.setProperty('mainTexture', this.commonTextures[0]);
}

// 存储预加载的材质
this.preloadedMaterials.set(effect.name, material);

resolve();
});
}

// 获取预加载的材质
getPreloadedMaterial(effectName: string): Material | null {
return this.preloadedMaterials.get(effectName) || null;
}

// 克隆预加载的材质
clonePreloadedMaterial(effectName: string): Material | null {
const baseMaterial = this.preloadedMaterials.get(effectName);
if (!baseMaterial) return null;

const cloned = new Material();
cloned.copy(baseMaterial);
return cloned;
}
}

📊 性能优化策略

材质状态排序

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
@ccclass('MaterialSorter')
export class MaterialSorter extends Component {
@property([MeshRenderer])
renderers: MeshRenderer[] = [];

start() {
this.sortByMaterial();
}

private sortByMaterial() {
// 按材质类型分组
const materialGroups = new Map<Material, MeshRenderer[]>();

this.renderers.forEach(renderer => {
const material = renderer.getMaterial(0);
if (material) {
if (!materialGroups.has(material)) {
materialGroups.set(material, []);
}
materialGroups.get(material)!.push(renderer);
}
});

// 重新排序渲染顺序
let sortOrder = 0;
materialGroups.forEach((renderers, material) => {
renderers.forEach(renderer => {
// 设置渲染优先级
renderer.priority = sortOrder;
});
sortOrder += 10; // 给同组材质留出空间
});

console.log(`Sorted ${this.renderers.length} renderers into ${materialGroups.size} material groups`);
}
}

批处理优化

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
@ccclass('MaterialBatcher')
export class MaterialBatcher extends Component {
@property(Material)
sharedMaterial: Material = null!;

@property([Node])
objectsToMerge: Node[] = [];

start() {
this.createBatch();
}

private createBatch() {
// 合并相同材质的对象
const mergedVertices: number[] = [];
const mergedIndices: number[] = [];
let vertexOffset = 0;

this.objectsToMerge.forEach(obj => {
const meshRenderer = obj.getComponent(MeshRenderer);
if (meshRenderer && meshRenderer.getMaterial(0) === this.sharedMaterial) {
const mesh = meshRenderer.mesh;
if (mesh) {
// 提取顶点数据
const vertices = this.extractVertices(mesh, obj.worldMatrix);
const indices = this.extractIndices(mesh, vertexOffset);

mergedVertices.push(...vertices);
mergedIndices.push(...indices);

vertexOffset += vertices.length / 3; // 假设每个顶点3个分量

// 禁用原始对象
obj.active = false;
}
}
});

// 创建合并后的网格
this.createMergedMesh(mergedVertices, mergedIndices);
}

private extractVertices(mesh: Mesh, worldMatrix: Mat4): number[] {
// 提取并变换顶点数据
const vertices: number[] = [];
// ... 实现顶点提取逻辑
return vertices;
}

private extractIndices(mesh: Mesh, offset: number): number[] {
// 提取并调整索引
const indices: number[] = [];
// ... 实现索引提取逻辑
return indices;
}

private createMergedMesh(vertices: number[], indices: number[]) {
// 创建合并后的网格对象
const mergedNode = new Node('MergedBatch');
const meshRenderer = mergedNode.addComponent(MeshRenderer);

// 创建网格
const mesh = utils.createMesh({
positions: vertices,
indices: indices
});

meshRenderer.mesh = mesh;
meshRenderer.setMaterial(this.sharedMaterial, 0);

this.node.addChild(mergedNode);
}
}

📚 最佳实践

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
// 好的做法
class GoodMaterialPractice {
// 共享静态材质
static readonly STANDARD_MATERIAL = Material.createWithBuiltin('builtin-standard', 0);

// 延迟创建MaterialInstance
private _materialInstance: MaterialInstance | null = null;

get materialInstance(): MaterialInstance {
if (!this._materialInstance) {
this._materialInstance = new MaterialInstance({
parent: GoodMaterialPractice.STANDARD_MATERIAL
});
}
return this._materialInstance;
}

// 批量属性更新
updateMaterialBatch(properties: Record<string, any>) {
Object.entries(properties).forEach(([key, value]) => {
this.materialInstance.setProperty(key, value);
});
}
}

// 避免的做法
class BadMaterialPractice {
// 不要为每个对象创建独立的Material
private material: Material = Material.createWithBuiltin('builtin-standard', 0);

// 不要频繁创建和销毁MaterialInstance
updateColor(color: Color) {
const instance = new MaterialInstance({ parent: this.material });
instance.setProperty('color', color);
// 忘记销毁 - 内存泄漏
}
}

2. 内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ccclass('MaterialMemoryManager')
export class MaterialMemoryManager extends Component {
private materialInstances: MaterialInstance[] = [];

createMaterialInstance(baseMaterial: Material): MaterialInstance {
const instance = new MaterialInstance({ parent: baseMaterial });
this.materialInstances.push(instance);
return instance;
}

onDestroy() {
// 清理所有材质实例
this.materialInstances.forEach(instance => {
if (instance.isValid) {
instance.destroy();
}
});
this.materialInstances.length = 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
@ccclass('MaterialDebugger')
export class MaterialDebugger extends Component {
@property(MeshRenderer)
targetRenderer: MeshRenderer = null!;

@property
debugMode: boolean = false;

update() {
if (this.debugMode) {
this.debugMaterialState();
}
}

private debugMaterialState() {
const material = this.targetRenderer.getMaterial(0);
if (material) {
console.log('Material Debug Info:', {
effectName: material.effectAsset?.name,
technique: material.technique,
passes: material.passes.length,
defines: material.defines,
properties: this.extractProperties(material)
});
}
}

private extractProperties(material: Material): Record<string, any> {
const properties: Record<string, any> = {};
const pass = material.passes[0];

if (pass && pass.properties) {
Object.keys(pass.properties).forEach(key => {
properties[key] = material.getProperty(key);
});
}

return properties;
}
}

📚 下一步学习

完成材质系统学习后,建议继续学习:

  1. 第3.3章:着色器调试与开发工具 - 学习调试工具使用
  2. 第5.1章:Surface Shader详解 - 探索Surface Shader
  3. 第4.3章:PBR着色器基础原理 - 深入PBR渲染
  4. 第11.3章:着色器优化技术详解 - 着色器优化技术

💡 总结

通过本章学习,你现在应该掌握了:
- 完整的材质系统架构
- Material和MaterialInstance的正确使用
- 程序化材质创建和管理
- 多Pass渲染系统
- 性能优化和最佳实践
材质系统是着色器开发的重要基础,正确使用材质系统不仅能提高开发效率,还能显著优化应用性能。记住始终考虑内存管理和性能影响。

重要提醒:在实际项目中,合理规划材质的使用策略。静态对象使用共享Material,动态对象使用MaterialInstance。及时清理不再使用的材质实例,避免内存泄漏。