第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 @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 ); }); } 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 (); } } }
选择原则 🔧 程序化材质创建 动态创建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 { const material = new Material (); material.initialize ({ effectAsset : this .effectAsset , technique : 0 }); 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 } }); 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 , 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 ( ) { const material = new Material (); material.initialize ({ effectAsset : this .multiPassMaterial .effectAsset !, technique : 0 , defines : { MULTI_PASS : true , PASS_COUNT : 3 } }); material.setProperty ('pass1_color' , Color .RED ); material.setProperty ('pass1_blend' , BlendState .NORMAL ); material.setProperty ('pass2_color' , Color .YELLOW ); material.setProperty ('pass2_blend' , BlendState .ADDITIVE ); material.setProperty ('rimPower' , 2.0 ); 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 !; @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 ( ) { 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); } }); } 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 ; 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 ); 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 { private material : Material = Material .createWithBuiltin ('builtin-standard' , 0 ); 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; } }
📚 下一步学习 完成材质系统学习后,建议继续学习:
第3.3章:着色器调试与开发工具 - 学习调试工具使用第5.1章:Surface Shader详解 - 探索Surface Shader第4.3章:PBR着色器基础原理 - 深入PBR渲染第11.3章:着色器优化技术详解 - 着色器优化技术💡 总结 通过本章学习,你现在应该掌握了: - 完整的材质系统架构 - Material和MaterialInstance的正确使用 - 程序化材质创建和管理 - 多Pass渲染系统 - 性能优化和最佳实践 材质系统是着色器开发的重要基础,正确使用材质系统不仅能提高开发效率,还能显著优化应用性能。记住始终考虑内存管理和性能影响。 重要提醒 :在实际项目中,合理规划材质的使用策略。静态对象使用共享Material,动态对象使用MaterialInstance。及时清理不再使用的材质实例,避免内存泄漏。