Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
PyTorch系列文章目录
Python系列文章目录
C#系列文章目录
01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
18-C#游戏开发【第18天】 | 深入理解队列(Queue)与栈(Stack):从基础到任务队列实战
19-【C# 进阶】深入理解枚举 Flags 属性:游戏开发中多状态组合的利器
20-C#结构体(Struct)深度解析:轻量数据容器与游戏开发应用 (Day 20)
21-Unity数据持久化进阶:告别硬编码,用ScriptableObject优雅管理游戏配置!(Day 21)
22-Unity C# 健壮性编程:告别崩溃!掌握异常处理与调试的 4 大核心技巧 (Day 22)
23-C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)
24-Unity脚本通信终极指南:从0到1精通UnityEvent与事件解耦(Day 24)
25-精通C# Lambda与LINQ:Unity数据处理效率提升10倍的秘诀! (Day 25)
26-# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)
27-Unity协程从入门到精通:告别卡顿,用Coroutine优雅处理异步与时序任务 (Day 27)
28-搞定玩家控制!Unity输入系统、物理引擎、碰撞检测实战指南 (Day 28)
29-# Unity动画控制核心:Animator状态机与C#脚本实战指南 (Day 29)
30-Unity UI 从零到精通 (第30天): Canvas、布局与C#交互实战 (Day 30)
31-Unity性能优化利器:彻底搞懂对象池技术(附C#实现与源码解析)
32-Unity C#进阶:用状态模式与FSM优雅管理复杂敌人AI,告别Spaghetti Code!(Day32)
33-Unity游戏开发实战:从PlayerPrefs到JSON,精通游戏存档与加载机制(Day 33)
34-Unity C# 实战:从零开始为游戏添加背景音乐与音效 (AudioSource/AudioClip/AudioMixer 详解)(Day 34)
35-Unity 场景管理核心教程:从 LoadScene 到 Loading Screen 实战 (Day 35)
36-Unity设计模式实战:用单例和观察者模式优化你的游戏架构 (Day 36)
37-Unity性能优化实战:用Profiler揪出卡顿元凶 (CPU/GPU/内存/GC全面解析) (Day 37)
38-Unity C# 与 Shader 交互入门:脚本动态控制材质与视觉效果 (含 MaterialPropertyBlock 详解)(Day 38)
文章目录
- Langchain系列文章目录
- PyTorch系列文章目录
- Python系列文章目录
- C#系列文章目录
- 前言
- [一、Shader 基础概念回顾](#一、Shader 基础概念回顾)
-
- [1.1 什么是 Shader?](#1.1 什么是 Shader?)
- [1.2 顶点着色器 (Vertex Shader)](#1.2 顶点着色器 (Vertex Shader))
- [1.3 片元着色器 (Fragment/Pixel Shader)](#1.3 片元着色器 (Fragment/Pixel Shader))
- [1.4 Shader 代码一瞥 (ShaderLab 示例)](#1.4 Shader 代码一瞥 (ShaderLab 示例))
- [二、材质 (Material) 与 Shader 的关系](#二、材质 (Material) 与 Shader 的关系)
-
- [2.1 Material:Shader 的实例与参数配置](#2.1 Material:Shader 的实例与参数配置)
- [2.2 如何在 Unity 中查看和设置](#2.2 如何在 Unity 中查看和设置)
- [三、通过 C# 脚本控制材质属性](# 脚本控制材质属性)
-
- [3.1 获取 Material 组件](#3.1 获取 Material 组件)
- [3.2 修改常见属性](#3.2 修改常见属性)
-
- [3.2.1 修改浮点数 (Float)](#3.2.1 修改浮点数 (Float))
- [3.2.2 修改颜色 (Color)](#3.2.2 修改颜色 (Color))
- [3.2.3 修改纹理 (Texture)](#3.2.3 修改纹理 (Texture))
- [3.3 查找 Shader 中的属性名](#3.3 查找 Shader 中的属性名)
- [四、进阶控制:Shader.Find 与 MaterialPropertyBlock](#四、进阶控制:Shader.Find 与 MaterialPropertyBlock)
-
- [4.1 动态加载 Shader (Shader.Find)](#4.1 动态加载 Shader (Shader.Find))
- [4.2 高效修改:MaterialPropertyBlock (MPB)](#4.2 高效修改:MaterialPropertyBlock (MPB))
-
- [4.2.1 为什么需要 MPB?](#4.2.1 为什么需要 MPB?)
- [4.2.2 如何使用 MPB?](#4.2.2 如何使用 MPB?)
- [4.2.3 MPB 的适用场景](#4.2.3 MPB 的适用场景)
- 五、实践案例:角色受伤闪红效果
-
- [5.1 准备工作](#5.1 准备工作)
- [5.2 Shader 简易实现 (示意)](#5.2 Shader 简易实现 (示意))
- [5.3 C# 脚本实现](# 脚本实现)
- [5.4 效果演示与扩展思路](#5.4 效果演示与扩展思路)
- 六、常见问题与注意事项
- 七、总结
前言
欢迎来到 C# for Unity 学习之旅的第 39 天!在之前的学习中,我们已经掌握了 C# 的基础与进阶知识,并熟悉了 Unity 的核心机制。今天,我们将探索一个让游戏视觉效果更加生动有趣的主题:C# 与 Shader 的交互。
想象一下,游戏角色受伤时全身闪烁红光,或者环境根据一天中的时间变换色彩,又或者物体的特定区域根据玩家的交互而发光------这些酷炫的视觉效果都离不开 C# 脚本对 Shader 的动态控制。
本篇文章将带你入门 C# 与 Shader 交互的核心知识,从理解 Shader 和 Material 的基本概念,到学会如何使用 C# 脚本修改材质属性,再到掌握更高效的 MaterialPropertyBlock
技术。最终,我们将通过一个实战案例------实现角色受伤闪红效果,来巩固所学知识。无论你是刚接触 Shader 的新手,还是希望深化 C# 应用的进阶开发者,相信都能从中获益。
一、Shader 基础概念回顾
在我们深入探讨如何用 C# 控制视觉效果之前,有必要先快速回顾一下 Shader 的基本概念。
1.1 什么是 Shader?
简单来说,Shader(着色器) 是一段运行在 GPU(图形处理单元)上的小程序。它的核心任务是告诉计算机如何绘制(渲染)游戏世界中的物体。你可以把它想象成一个详细的"绘画指南"或者"渲染配方",它定义了物体表面的每一个像素应该是什么颜色、如何响应光照、是否透明等等。
在 Unity 中,Shader 通常使用一种叫做 ShaderLab 的语言来组织结构,并在其中嵌入 HLSL (High-Level Shading Language) 或类似的着色器语言代码来执行具体的计算。
1.2 顶点着色器 (Vertex Shader)
顶点着色器 是渲染管线中的早期阶段,它负责处理模型的每个顶点(Vertex)。
- 主要工作 :
- 坐标变换:将顶点的 3D 坐标从模型空间转换到屏幕空间。这是决定物体最终出现在屏幕哪个位置的关键步骤。
- 顶点属性传递:计算并传递顶点相关信息(如法线、纹理坐标)给后续阶段(主要是片元着色器)。
- 顶点动画/变形:可以通过修改顶点位置来实现如风吹草动、角色变形等效果。
你可以将顶点着色器理解为负责确定物体"形状"和"位置"的程序。
1.3 片元着色器 (Fragment/Pixel Shader)
片元着色器 (也常被称为像素着色器)在顶点着色器之后运行,它负责计算屏幕上每个像素(Pixel)的最终颜色。
- 主要工作 :
- 颜色计算:根据光照、材质颜色、纹理采样等信息,确定像素的最终 RGB 颜色值。
- 纹理应用:读取纹理贴图(Texture)并在模型表面"贴"上图案。
- 光照计算:模拟光线与物体表面的交互,产生明暗、高光等效果。
- 透明与特效:处理透明度、雾效、溶解等各种视觉特效。
片元着色器是决定物体最终"看起来怎么样"(颜色、质感、光影)的核心。
1.4 Shader 代码一瞥 (ShaderLab 示例)
为了让大家对 Shader 有个更直观的认识,我们来看一个极简的 ShaderLab 结构,特别是 Properties
部分,这与我们后续 C# 交互息息相关:
shaderlab
Shader "Custom/SimpleColorShader" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1) // 定义一个颜色属性,可在 Inspector 中编辑,也可被 C# 访问
_MainTex ("Albedo (RGB)", 2D) = "white" {} // 定义一个纹理属性
_Glossiness ("Smoothness", Range(0,1)) = 0.5 // 定义一个范围滑块属性 (浮点数)
_Metallic ("Metallic", Range(0,1)) = 0.0 // 定义另一个范围滑块属性 (浮点数)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM // HLSL/CG 代码块开始
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex; // 对应 Properties 中的 _MainTex
fixed4 _Color; // 对应 Properties 中的 _Color
half _Glossiness; // 对应 Properties 中的 _Glossiness
half _Metallic; // 对应 Properties 中的 _Metallic
struct Input {
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; // 采样纹理并乘以颜色
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG // HLSL/CG 代码块结束
}
FallBack "Diffuse" // 如果以上 SubShader 不支持,则回退到内置 Diffuse Shader
}
注意 Properties
代码块,这里定义的变量(如 _Color
, _MainTex
, _Glossiness
)就是我们稍后要用 C# 脚本来控制的目标。它们的名字 (字符串,如 "_Color"
)至关重要。
二、材质 (Material) 与 Shader 的关系
理解了 Shader 是"渲染配方"后,材质 (Material) 又是什么呢?
2.1 Material:Shader 的实例与参数配置
如果说 Shader 是一个通用的"食谱"(比如"红烧肉怎么做"),那么 Material 就是根据这个食谱具体做出来的一道"菜"(比如"这盘加了更多酱油和糖的红烧肉")。
- Material 是 Shader 的一个实例:每个 Material 都必须指定一个 Shader。它使用这个 Shader 来定义渲染方式。
- Material 存储 Shader 的具体参数值 :Shader 在
Properties
中定义了可配置的参数(如颜色、纹理、光滑度等),而 Material 则存储了这些参数的具体数值。例如,一个"金属球"Material 和一个"橡胶球"Material 可能使用同一个标准 Shader,但它们的颜色、金属度、光滑度等属性值会完全不同。 - 一个 Shader 可以创建多个 Material:你可以基于同一个 Shader 创建无数个 Material,每个 Material 都有自己独特的属性设置,从而实现丰富多彩的视觉表现。
在 Unity 中,我们将 Material 赋给场景中游戏对象的 Renderer
组件(如 MeshRenderer
),这样 Unity 就知道该如何渲染这个对象了。
2.2 如何在 Unity 中查看和设置
- 在 Unity 编辑器的 Project 窗口中,右键 -> Create -> Material,即可创建一个新的 Material 文件。
- 选中新创建的 Material,在 Inspector 窗口顶部可以看到它当前使用的 Shader。你可以点击下拉菜单更换 Shader。
- Shader 下方会列出该 Shader 在
Properties
中定义的所有可配置属性。你可以直接在 Inspector 中调整这些值(如颜色拾取器、拖入纹理、滑动条等),并实时在 Scene 或 Game 视图中看到应用了该 Material 的对象外观发生变化。
这种直观的编辑方式非常适合美术师和设计师调整静态效果。但要实现动态变化,就需要 C# 脚本的介入了。
三、通过 C# 脚本控制材质属性
现在,激动人心的部分来了!我们将学习如何用 C# 代码在运行时动态修改 Material 的属性,从而改变游戏对象的视觉表现。
3.1 获取 Material 组件
要修改一个对象的材质属性,首先需要获取到它的 Material
实例。通常,这是通过对象的 Renderer
组件(如 MeshRenderer
, SkinnedMeshRenderer
)来实现的。主要有两种方式:
-
renderer.material
:- 当你访问
renderer.material
时,Unity 会自动创建一个该材质的实例(副本) 。后续你对这个material
实例的修改只会影响当前的这个 Renderer,不会影响其他使用相同原始 Material 资产的对象。 - 优点:安全地修改单个对象的材质,不影响其他对象。
- 缺点:每次访问都会创建一个新的 Material 实例,如果频繁对大量对象执行此操作,可能会导致内存占用增加和 Draw Call 数量上升(因为每个实例化的 Material 可能被视为不同的渲染批次),影响性能。
- 适用场景:需要对某个特定对象进行独一无二的、永久性的材质改变时(注意这里的"永久性"是相对于该对象实例的生命周期)。
- 当你访问
-
renderer.sharedMaterial
:- 当你访问
renderer.sharedMaterial
时,你获取的是所有使用该 Material 资产的对象所共享的那个原始 Material。 - 优点 :修改
sharedMaterial
会立即影响所有使用该 Material 资产的对象,且不会创建新的实例,性能开销较小。 - 缺点 :危险操作! 如果你无意中修改了
sharedMaterial
,可能会改变项目中所有使用该材质的对象的的外观,甚至包括 Project 视图中的原始 Material 资产。这通常不是我们想要的结果,尤其是在运行时。 - 适用场景:编辑器脚本中需要修改 Material 资产本身时;或者你确实需要一次性改变所有使用该材质的对象的外观(需谨慎)。
- 当你访问
重要提示 :在运行时动态修改单个对象的材质属性时,优先考虑使用 MaterialPropertyBlock
(将在第四部分详述),它是性能最优的方式。如果确实需要独立的 Material 实例(比如效果需要持续存在,即使禁用了脚本),则可以使用 renderer.material
,但要注意潜在的性能影响。尽量避免在运行时修改 sharedMaterial
。
csharp
using UnityEngine;
public class MaterialController : MonoBehaviour
{
private Renderer objectRenderer;
private Material uniqueMaterialInstance; // 用于存储通过 .material 获取的实例
void Start()
{
objectRenderer = GetComponent<Renderer>();
if (objectRenderer == null)
{
Debug.LogError("Renderer component not found on this GameObject.");
return;
}
// 方式一:获取独立的材质实例 (会创建新实例)
// uniqueMaterialInstance = objectRenderer.material;
// Debug.Log("Accessed .material, a new instance might be created.");
// 方式二:获取共享的材质资源 (谨慎使用)
// Material sharedMat = objectRenderer.sharedMaterial;
// Debug.Log("Accessed .sharedMaterial. Modifying this affects all users.");
// 推荐方式将在第四部分介绍 MaterialPropertyBlock
}
// 后续将在此脚本中添加修改属性的方法
}
3.2 修改常见属性
获取到 Material 引用(无论是 material
还是 sharedMaterial
,或是通过 MaterialPropertyBlock
操作)后,就可以使用一系列 Set
方法来修改 Shader 中定义的属性了。关键在于提供正确的属性名(字符串)和对应类型的值。
3.2.1 修改浮点数 (Float)
用于修改 Shader 中定义的 Float
或 Range
类型的属性。
-
语法 :
material.SetFloat("_PropertyName", floatValue);
-
示例 :假设 Shader 有一个
_Transparency
属性控制透明度(0=不透明, 1=完全透明)。csharp// 假设 uniqueMaterialInstance 是通过 renderer.material 获取的 // 或者后续会看到,通过 MaterialPropertyBlock 设置 float currentTransparency = 0.5f; // 注意:属性名前面的下划线 "_" 是常见约定,但不绝对,要看 Shader 定义 material.SetFloat("_Transparency", currentTransparency);
-
常见应用:控制透明度、溶解效果的阈值、发光强度、平滑度/金属度等。
3.2.2 修改颜色 (Color)
用于修改 Shader 中定义的 Color
类型的属性。
-
语法 :
material.SetColor("_ColorPropertyName", colorValue);
-
示例 :修改名为
_BaseColor
的主颜色属性。csharpColor newColor = Color.red; // 使用 Unity 内置的红色 // 或者自定义颜色: Color newColor = new Color(1.0f, 0.5f, 0.0f, 1.0f); // 橙色 material.SetColor("_BaseColor", newColor);
-
常见应用 :改变物体基础色、高光颜色、自发光颜色 (
_EmissionColor
) 等。
3.2.3 修改纹理 (Texture)
用于修改 Shader 中定义的 2D
, Cube
, 3D
等纹理类型的属性。
-
语法 :
material.SetTexture("_TexturePropertyName", textureObject);
-
示例 :将名为
_MainTex
的主纹理替换为另一张纹理。csharppublic Texture2D alternativeTexture; // 在 Inspector 中拖入一张纹理 void ChangeTexture() { if (alternativeTexture != null) { material.SetTexture("_MainTex", alternativeTexture); } }
-
常见应用 :动态切换角色皮肤、改变地面材质、应用法线贴图 (
_BumpMap
)、遮罩贴图等。
3.3 查找 Shader 中的属性名
关键点 :SetFloat
, SetColor
, SetTexture
等方法接受的第一个参数是属性名字符串 。这个字符串必须 与你的 Shader 文件中 Properties
代码块里定义的属性名完全一致(包括大小写和下划线)。
- 如何找到属性名?
- 在 Unity Project 窗口找到你的 Shader 文件(
.shader
)。 - 双击打开它(通常会在 Visual Studio 或其他代码编辑器中打开)。
- 找到
Properties { ... }
代码块。 - 块内的每一行定义了一个属性,格式通常是
_PropertyName ("Inspector Label", Type) = DefaultValue
。你需要的就是那个带下划线的_PropertyName
字符串。
- 在 Unity Project 窗口找到你的 Shader 文件(
例如,在之前的 Shader 示例中:
_Color ("Main Color", Color) = (1,1,1,1)
-> 属性名是 "_Color"
_Glossiness ("Smoothness", Range(0,1)) = 0.5
-> 属性名是 "_Glossiness"
_MainTex ("Albedo (RGB)", 2D) = "white" {}
-> 属性名是 "_MainTex"
如果你使用了错误的属性名,或者属性名大小写不对,Set
方法不会报错,但也不会产生任何效果,这是调试时常见的一个坑点。
四、进阶控制:Shader.Find 与 MaterialPropertyBlock
除了直接修改现有材质,我们有时还需要更灵活或更高效的控制方式。
4.1 动态加载 Shader (Shader.Find)
如果你需要在运行时为一个对象动态更换整个 Shader(而不仅仅是修改当前 Shader 的属性),可以使用 Shader.Find
方法。
-
语法 :
Shader shader = Shader.Find("ShaderNameInProject");
-
示例 :将一个物体的材质使用的 Shader 更换为内置的 "Standard" Shader。
csharpRenderer rend = GetComponent<Renderer>(); Shader standardShader = Shader.Find("Standard"); if (rend != null && standardShader != null) { // 注意:这里访问 .material 会创建实例 rend.material.shader = standardShader; // 如果你想修改共享材质的 Shader (危险) // rend.sharedMaterial.shader = standardShader; } else { if(standardShader == null) Debug.LogError("Shader 'Standard' not found. Is it included in the build?"); }
-
注意 :
Shader.Find
使用的是 Shader 的名称 (在 Shader 文件第一行Shader "..."
定义的那个)。- 这个方法效率不高 ,不应在
Update
等频繁调用的地方使用。 - 更重要的是,
Shader.Find
只能找到已包含在项目构建设置中 的 Shader。如果一个 Shader 没有被任何场景中的材质使用,或者没有被放入Resources
文件夹或添加到 "Always Included Shaders" 列表中(Edit -> Project Settings -> Graphics),那么在打包后的游戏中Shader.Find
会返回null
。
4.2 高效修改:MaterialPropertyBlock (MPB)
这是在运行时为单个对象(或少量对象)设置独立材质属性的最推荐、性能最好的方式。
4.2.1 为什么需要 MPB?
如前所述,访问 renderer.material
会创建 Material 实例,增加 Draw Call 和内存。而修改 renderer.sharedMaterial
会影响所有对象。MaterialPropertyBlock
(MPB) 解决了这个问题。
MPB 允许你为单个 Renderer 覆盖部分 材质属性,而无需创建新的 Material 实例。GPU 仍然可以将使用相同基础 Material 和相同 MPB 覆盖(或无覆盖)的对象进行批处理渲染,从而保持较低的 Draw Call 数量。
4.2.2 如何使用 MPB?
使用 MPB 通常涉及以下步骤:
- 创建 MPB 实例 :
MaterialPropertyBlock props = new MaterialPropertyBlock();
- (可选但推荐)获取当前属性 :
renderer.GetPropertyBlock(props);
这一步会将 Renderer 当前的 MPB 覆盖(如果有的话)读取到你的props
对象中。如果你只想修改一两个属性,这样做可以保留其他可能已通过 MPB 设置的属性。如果你确定要完全重置 MPB,可以跳过此步。 - 设置属性 :使用与 Material 类似的
Set
方法,但这次是在props
对象上调用:props.SetFloat("_PropertyName", value);
,props.SetColor(...)
,props.SetTexture(...)
等。 - 应用 MPB :
renderer.SetPropertyBlock(props);
将你配置好的属性块应用到指定的 Renderer 上。
csharp
using UnityEngine;
public class MPBController : MonoBehaviour
{
private Renderer objectRenderer;
private MaterialPropertyBlock propertyBlock; // 缓存 MPB 实例
// 缓存 Shader 属性 ID,比每次用字符串查找更高效
private static readonly int ColorPropID = Shader.PropertyToID("_Color");
private static readonly int EmissionColorPropID = Shader.PropertyToID("_EmissionColor");
void Awake() // 使用 Awake 确保在 Start 前初始化
{
objectRenderer = GetComponent<Renderer>();
propertyBlock = new MaterialPropertyBlock(); // 创建实例
if (objectRenderer == null)
{
Debug.LogError("Renderer component not found.");
enabled = false; // 禁用脚本
}
}
// 示例:根据生命值百分比改变颜色
public void UpdateColorBasedOnHealth(float healthPercentage)
{
if (objectRenderer == null) return;
// 1. (可选) 获取当前 MPB 状态,如果需要保留其他 MPB 设置
// objectRenderer.GetPropertyBlock(propertyBlock);
// 2. 计算新颜色 (例如从绿到红)
Color healthColor = Color.Lerp(Color.red, Color.green, healthPercentage);
// 3. 设置属性到 MPB (使用缓存的 ID)
propertyBlock.SetColor(ColorPropID, healthColor);
// 也可以设置其他属性,比如发光
// propertyBlock.SetColor(EmissionColorPropID, healthColor * 0.5f); // 根据生命值发微光
// 4. 应用 MPB 到 Renderer
objectRenderer.SetPropertyBlock(propertyBlock);
}
// 如果想清除 MPB 效果,恢复到原始材质状态,可以传递 null
public void ResetMaterialProperties()
{
if (objectRenderer == null) return;
objectRenderer.SetPropertyBlock(null); // 传入 null 会清除 MPB
}
}
性能提示 :Shader.PropertyToID("_PropertyName")
可以将属性名字符串转换为一个整数 ID。在 Update
等频繁调用的地方使用这个 ID 来调用 Set
方法(如 propertyBlock.SetColor(ColorPropID, ...)
)比每次都使用字符串 "_Color"
要快得多 ,因为它避免了内部的字符串哈希查找。建议在 Awake
或 Start
中获取并缓存这些 ID。
4.2.3 MPB 的适用场景
- 需要为大量使用相同 Material 的对象设置不同的属性值时。例如:一群士兵,每个士兵的衣服根据其生命值有不同的脏污程度(通过修改某个 float 属性实现)。
- 需要频繁更新单个对象的某个材质属性时。例如:角色受到攻击时短暂闪烁特定颜色。
- 任何你原本想用
renderer.material
但又担心性能开销的场景。
五、实践案例:角色受伤闪红效果
现在,让我们运用所学的知识,特别是 MaterialPropertyBlock
,来实现一个经典的游戏效果:角色受到伤害时短暂地闪烁红色。
5.1 准备工作
- 创建一个简单的 Shader :这个 Shader 需要有一个基础颜色/纹理属性,以及一个用于控制"闪红"效果的属性。我们可以添加一个
_FlashColor
(Color) 和一个_FlashAmount
(Float, Range 0-1) 属性。当_FlashAmount
为 0 时显示正常颜色,为 1 时显示_FlashColor
,中间值则进行混合。 - 创建一个 Material:使用上面创建的 Shader,并将这个 Material 赋给你想要应用效果的游戏对象(比如一个立方体或一个角色模型)的 Renderer 组件。
- 创建一个 C# 脚本 :命名为
DamageFlashEffect
,并将其附加到同一个游戏对象上。
5.2 Shader 简易实现 (示意)
这里提供一个非常基础的表面着色器 (Surface Shader) 的 Properties
和 surf
函数部分,用于演示:
shaderlab
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
// --- 闪烁效果属性 ---
_FlashColor ("Flash Color", Color) = (1,0,0,1) // 默认为红色
_FlashAmount ("Flash Amount", Range(0, 1)) = 0 // 0 = 无闪烁, 1 = 完全闪烁色
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
fixed4 _Color;
half _Glossiness;
half _Metallic;
// --- 闪烁效果变量 ---
fixed4 _FlashColor;
half _FlashAmount;
struct Input {
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 baseColor = tex2D (_MainTex, IN.uv_MainTex) * _Color; // 获取基础颜色
// 使用 lerp (线性插值) 函数混合基础色和闪烁色
// 当 _FlashAmount = 0 时,结果是 baseColor
// 当 _FlashAmount = 1 时,结果是 _FlashColor
fixed4 finalColor = lerp(baseColor, _FlashColor, _FlashAmount);
o.Albedo = finalColor.rgb; // 应用最终颜色
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = finalColor.a;
}
ENDCG
}
FallBack "Diffuse"
关键在于 surf
函数中的 lerp
(线性插值) 操作 :lerp(a, b, t)
会根据 t
(范围通常是 0 到 1) 在 a
和 b
之间插值。
5.3 C# 脚本实现
现在编写 DamageFlashEffect.cs
脚本:
csharp
using UnityEngine;
using System.Collections; // 需要引入这个命名空间来使用 Coroutine
public class DamageFlashEffect : MonoBehaviour
{
[SerializeField] private float flashDuration = 0.2f; // 闪烁总时长
[SerializeField] private Color flashColor = Color.red; // 也可以在 Inspector 配置闪烁颜色
private Renderer objectRenderer;
private MaterialPropertyBlock propertyBlock;
// 缓存 Shader 属性 ID
private static readonly int FlashColorPropID = Shader.PropertyToID("_FlashColor");
private static readonly int FlashAmountPropID = Shader.PropertyToID("_FlashAmount");
private Coroutine flashCoroutine; // 用于存储当前运行的协程,防止重复启动
void Awake()
{
objectRenderer = GetComponent<Renderer>();
propertyBlock = new MaterialPropertyBlock();
if (objectRenderer == null)
{
Debug.LogError("Renderer not found for DamageFlashEffect.");
enabled = false;
}
// 可以在这里预设 MPB 的 FlashColor,如果 Inspector 可配置的话
// objectRenderer.GetPropertyBlock(propertyBlock); // 获取现有 MPB (如果需要)
// propertyBlock.SetColor(FlashColorPropID, flashColor);
// objectRenderer.SetPropertyBlock(propertyBlock);
}
// 公开方法,供其他脚本调用(例如 PlayerHealth 脚本)
public void TriggerFlash()
{
// 如果上一个闪烁效果还在进行中,先停止它
if (flashCoroutine != null)
{
StopCoroutine(flashCoroutine);
}
// 启动新的闪烁协程
flashCoroutine = StartCoroutine(FlashSequence());
}
private IEnumerator FlashSequence()
{
// 获取当前的 MPB 状态 (如果需要保留其他 MPB 设置)
objectRenderer.GetPropertyBlock(propertyBlock);
// 设置闪烁颜色 (如果需要每次都更新)
propertyBlock.SetColor(FlashColorPropID, flashColor);
float elapsedTime = 0f;
float halfDuration = flashDuration / 2f;
// 阶段一:从 0 到 1 (变红)
while (elapsedTime < halfDuration)
{
elapsedTime += Time.deltaTime;
float flashAmount = Mathf.Clamp01(elapsedTime / halfDuration); // 计算当前闪烁强度 (0-1)
propertyBlock.SetFloat(FlashAmountPropID, flashAmount); // 设置 MPB 中的 _FlashAmount
objectRenderer.SetPropertyBlock(propertyBlock); // 应用 MPB
yield return null; // 等待下一帧
}
// 阶段二:从 1 到 0 (恢复)
elapsedTime = 0f; // 重置计时器
while (elapsedTime < halfDuration)
{
elapsedTime += Time.deltaTime;
float flashAmount = Mathf.Clamp01(1f - (elapsedTime / halfDuration)); // 计算当前闪烁强度 (1-0)
propertyBlock.SetFloat(FlashAmountPropID, flashAmount); // 设置 MPB 中的 _FlashAmount
objectRenderer.SetPropertyBlock(propertyBlock); // 应用 MPB
yield return null; // 等待下一帧
}
// 确保最终恢复到 0
propertyBlock.SetFloat(FlashAmountPropID, 0f);
objectRenderer.SetPropertyBlock(propertyBlock);
flashCoroutine = null; // 标记协程已结束
}
// (可选) 在对象禁用或销毁时,确保重置效果
void OnDisable()
{
if (objectRenderer != null)
{
// 可以选择停止协程并尝试重置
if (flashCoroutine != null)
{
StopCoroutine(flashCoroutine);
flashCoroutine = null;
}
// 尝试清除 MPB (如果确定离开时应该恢复原状)
// objectRenderer.GetPropertyBlock(propertyBlock);
// propertyBlock.SetFloat(FlashAmountPropID, 0f);
// objectRenderer.SetPropertyBlock(propertyBlock);
// 或者更彻底地清除整个 MPB
// objectRenderer.SetPropertyBlock(null);
}
}
}
如何使用:
-
确保你的游戏对象有 Renderer 组件、使用了支持
_FlashColor
和_FlashAmount
的 Shader 的 Material,并且挂载了DamageFlashEffect
脚本。 -
在你的角色受到伤害的逻辑处(比如
PlayerHealth
脚本的TakeDamage
方法里),获取DamageFlashEffect
组件的引用,并调用TriggerFlash()
方法。csharp// 在 PlayerHealth.cs (或其他地方) DamageFlashEffect flashEffect = GetComponent<DamageFlashEffect>(); public void TakeDamage(float amount) { // ... 处理伤害逻辑 ... health -= amount; // 触发闪烁效果 if (flashEffect != null) { flashEffect.TriggerFlash(); } // ... 其他逻辑 (如播放音效, 检查死亡) ... }
5.4 效果演示与扩展思路
运行游戏,当调用 TriggerFlash()
时,你应该能看到附加了脚本的对象在 flashDuration
的时间内快速地从原始颜色变为 flashColor
指定的颜色,然后再变回原始颜色。
扩展思路:
- 基于生命值改变颜色 :修改 C# 脚本,在
Update
中根据当前生命值百分比持续设置一个_HealthColorBlend
属性(0=红, 1=绿),Shader 中使用lerp
来混合。 - 使用纹理遮罩 :在 Shader 中添加一个
_FlashMask
纹理属性,只在遮罩纹理的白色区域显示闪烁效果。 - 改变自发光 (Emission) :不改变基础色,而是修改材质的自发光颜色和强度 (
_EmissionColor
) 来实现发光闪烁。 - 更多参数控制:暴露闪烁的峰值强度、渐入渐出曲线等参数到 Inspector。
六、常见问题与注意事项
- 属性名不匹配 (
_PropertyName
) :- 问题 : 调用
SetFloat/Color/Texture
或props.Set...
没有任何效果。 - 排查 : 仔细核对 C# 脚本中使用的属性名字符串与 Shader 文件
Properties
部分定义的名称是否完全一致(包括大小写和下划线)。确认 Shader 已正确编译且没有错误。
- 问题 : 调用
material
vssharedMaterial
vsMaterialPropertyBlock
的选择 :- 回顾 :
material
创建实例(影响性能),sharedMaterial
修改资源(危险),MaterialPropertyBlock
高效覆盖单个实例属性(推荐)。 - 建议 : 运行时动态修改,优先使用
MaterialPropertyBlock
。仅在确实需要独立且持久的材质实例时考虑material
。极少情况下才应在运行时修改sharedMaterial
。
- 回顾 :
- Shader 未包含在构建中 (
Shader.Find
失败) :- 问题 :
Shader.Find("MyShaderName")
在编辑器中工作正常,但在构建后的游戏中返回null
。 - 原因 : 该 Shader 没有被任何场景中的材质直接引用,也没有放在
Resources
文件夹,也未添加到 Graphics Settings 的 "Always Included Shaders" 列表。 - 解决 : 确保 Unity 能在构建时知道需要包含这个 Shader。最简单的方式是创建一个材质使用该 Shader 并放置在某个会被打包的场景中,或者将其放入
Resources
文件夹。
- 问题 :
- 性能考量 :
- 频繁的
renderer.material
调用: 会生成大量 Material 实例,增加 GC 负担和 Draw Call。 - 字符串属性查找 : 在
Update
中反复使用"PropertyName"
字符串调用Set
方法比使用缓存的Shader.PropertyToID
效率低。 - MPB 的开销 : 虽然 MPB 很高效,但频繁地创建新的
MaterialPropertyBlock
实例(如在Update
里new MaterialPropertyBlock()
) 也是有开销的。应缓存 MPB 实例。频繁调用SetPropertyBlock
本身也有一定开销,仅在属性确实改变时调用。
- 频繁的
七、总结
恭喜你完成了 C# 与 Shader 交互的入门学习!通过今天的探索,我们掌握了动态控制游戏视觉效果的关键技术:
- 理解了 Shader 的本质:它是 GPU 执行的渲染指令,分为顶点和片元阶段,决定物体如何被绘制。
- 明确了 Material 与 Shader 的关系:Material 是 Shader 的实例,存储着 Shader 定义属性的具体值。
- 学会了用 C# 修改 Material 属性 :可以通过
renderer.material
(创建实例)或renderer.sharedMaterial
(修改共享资源,需谨慎)访问材质,并使用SetFloat
,SetColor
,SetTexture
等方法修改其属性,关键是属性名要匹配。 - 掌握了高效的
MaterialPropertyBlock
:这是在运行时为单个对象(或多个对象独立地)修改材质属性的最佳实践,它避免了创建新材质实例的开销,有利于性能优化。我们还学习了如何使用它,并推荐缓存属性 ID (Shader.PropertyToID
) 以提高效率。 - 实践了受伤闪红效果 :通过结合自定义 Shader 属性 (
_FlashColor
,_FlashAmount
)、C# 协程以及MaterialPropertyBlock
,我们成功实现了一个常见的动态视觉反馈。
掌握 C# 与 Shader 的交互能力,将为你打开创造更生动、更具表现力游戏世界的大门。从简单的颜色变化到复杂的程序化效果,都建立在今天学习的基础之上。鼓励你多多尝试,将这些技术应用到自己的项目中去!
接下来,我们将继续深入 Unity 开发的其他高级主题,敬请期待!