一、问题背景
在 Unity 项目中,经常会遇到这样的情况:
多个物体使用同一个材质球,例如多个离心管、试剂瓶、液体、操作提示高亮区域都引用了同一个 Material。
如果在运行时直接修改这个材质:
csharp
renderer.sharedMaterial.color = Color.red;
可能会发现一个问题:
text
只想改当前物体的颜色,结果所有使用同一个材质球的物体都一起变色了。
这就是典型的共享材质修改问题。
二、为什么会出现共享材质问题
Unity 中的材质资源本质上是一个 Material 对象。
当多个 Renderer 引用同一个材质资源时,它们看到的是同一份材质数据:
text
Material_A
├── Tube_01 Renderer
├── Tube_02 Renderer
├── Tube_03 Renderer
└── Bottle_01 Renderer
如果直接修改 Material_A 的颜色、透明度、贴图、浮点参数,那么所有引用它的物体都会受到影响。
这并不是 Unity 出错,而是共享资源本来就是这样工作的。
三、material 和 sharedMaterial 的区别
Unity 中常见的两个属性:
csharp
renderer.material
renderer.sharedMaterial
它们看起来都能拿到材质,但含义不同。
1. sharedMaterial
sharedMaterial 指向项目资源中的共享材质。
修改它会影响所有引用这个材质的对象:
csharp
// 修改共享材质,会影响所有使用该材质的 Renderer。
renderer.sharedMaterial.color = Color.red;
适合用在:
text
编辑器工具统一调整材质
全局换肤
确实希望所有对象同步变化的场景
不适合用在:
text
单个物体临时高亮
单个液体变色
单个器材透明度变化
运行时按对象区分显示状态
2. material
material 会在运行时为当前 Renderer 创建一份材质实例。
csharp
// Unity 会为当前 Renderer 创建材质实例,避免直接修改共享材质。
renderer.material.color = Color.red;
这样可以避免影响其他物体,但也有代价:
text
每个被修改的 Renderer 都可能生成一个新的 Material 实例。
实例多了以后,会增加内存占用。
材质实例变多,也可能影响渲染合批。
所以 renderer.material 虽然能解决"互相影响"的问题,但如果大量对象都这样改,就容易造成材质实例膨胀。
四、MaterialPropertyBlock 是什么
MaterialPropertyBlock 通常简称 MPB。
它的作用是:
text
不复制材质本身,只给某一个 Renderer 提供一份单独的材质参数覆盖值。
可以理解成:
text
共享材质:负责 Shader、贴图、基础配置
MPB:负责当前 Renderer 自己的颜色、透明度、数值参数
结构可以理解为:
text
Material_A
├── Tube_01 Renderer + MPB(color = red)
├── Tube_02 Renderer + MPB(color = blue)
├── Tube_03 Renderer + MPB(color = green)
└── Bottle_01 Renderer + 无 MPB,使用材质默认值
材质仍然是同一个材质,但每个 Renderer 可以有自己的显示参数。
五、MPB 的基本原理
渲染时 Unity 会把材质参数传给 GPU。
普通材质修改是这样:
text
修改 Material 数据
↓
所有引用这个 Material 的 Renderer 都使用新数据
MPB 是这样:
text
Material 保持不变
↓
Renderer 上保存一份额外的属性覆盖表
↓
渲染这个 Renderer 时,Unity 把 MPB 中的属性覆盖到材质参数上
↓
只影响当前 Renderer 的最终显示
所以 MPB 解决问题的关键是:
text
它改的是 Renderer 的渲染参数覆盖值,不是 Material 资源本身。
这也是它适合运行时单对象变色、透明、高亮的原因。
六、MPB 能解决什么问题
MPB 适合解决:
text
同一个材质,不同物体显示不同颜色
同一个材质,不同物体透明度不同
选中某个物体时只高亮当前物体
液体材质共享,但每个容器中的液体颜色不同
大量对象需要按状态显示不同参数,但不想创建大量 Material 实例
MPB 不适合解决:
text
不同对象需要完全不同的 Shader
不同对象需要不同的渲染队列 Render Queue
不同对象需要开关不同的 Shader Keyword
不同对象需要切换材质的混合模式、剔除模式、深度写入等渲染状态
这些内容属于材质或 Shader 变体级别,MPB 不能完全替代材质实例。
七、基础示例:用 MPB 修改单个物体颜色
下面示例用于修改单个 Renderer 的颜色,不影响其他共享材质对象。
csharp
using UnityEngine;
/// <summary>
/// 使用 MaterialPropertyBlock 修改当前物体颜色。
/// 适用于多个物体共享同一个材质,但运行时需要单独控制当前物体颜色的情况。
/// </summary>
public class RendererColorByMPB : MonoBehaviour
{
/// <summary>
/// 当前物体用于显示的 Renderer。
/// 如果不手动绑定,会在 Awake 中从当前节点自动获取。
/// </summary>
[SerializeField] private Renderer targetRenderer;
/// <summary>
/// 当前 Renderer 独立使用的材质属性块。
/// 缓存下来可以避免每次修改颜色时重复 new 对象。
/// </summary>
private MaterialPropertyBlock propertyBlock;
/// <summary>
/// Shader 中颜色属性的 ID。
/// 使用 PropertyToID 可以避免每次通过字符串查找属性。
/// Built-in 标准材质常用 "_Color",URP Lit 常用 "_BaseColor"。
/// </summary>
private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
/// <summary>
/// 初始化 Renderer 和 MaterialPropertyBlock。
/// </summary>
private void Awake()
{
// 未在 Inspector 绑定时,默认使用当前物体上的 Renderer。
if (targetRenderer == null)
{
targetRenderer = GetComponent<Renderer>();
}
// 每个控制脚本持有一份 MPB,后续重复使用。
propertyBlock = new MaterialPropertyBlock();
}
/// <summary>
/// 设置当前 Renderer 的显示颜色。
/// 这里只修改当前 Renderer 的属性覆盖值,不修改共享材质资源。
/// </summary>
/// <param name="color">当前物体需要显示的颜色。</param>
public void SetColor(Color color)
{
// 先读取当前 Renderer 已有的属性块,避免覆盖其他地方已经写入的 MPB 参数。
targetRenderer.GetPropertyBlock(propertyBlock);
// 写入颜色覆盖值,属性名必须和 Shader 中的颜色属性一致。
propertyBlock.SetColor(BaseColorId, color);
// 把修改后的属性块重新设置回 Renderer,当前 Renderer 下一次渲染时会使用这个覆盖值。
targetRenderer.SetPropertyBlock(propertyBlock);
}
}
注意:
text
如果项目使用 Built-in Standard Shader,颜色属性可能是 "_Color"。
如果项目使用 URP Lit,颜色属性通常是 "_BaseColor"。
具体属性名要看 Shader。
八、同时兼容 _BaseColor 和 _Color 的写法
如果项目中材质来源不统一,可以同时写两个常见属性。
csharp
using UnityEngine;
/// <summary>
/// 同时写入 URP 和 Built-in 常见颜色属性的 MPB 示例。
/// 适合项目中材质 Shader 不完全统一的情况。
/// </summary>
public class RendererColorCompatMPB : MonoBehaviour
{
/// <summary>
/// 需要单独控制颜色的 Renderer。
/// </summary>
[SerializeField] private Renderer targetRenderer;
/// <summary>
/// 当前 Renderer 的属性覆盖数据。
/// </summary>
private MaterialPropertyBlock propertyBlock;
/// <summary>
/// URP Lit 常见基础颜色属性。
/// </summary>
private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
/// <summary>
/// Built-in Standard 常见颜色属性。
/// </summary>
private static readonly int ColorId = Shader.PropertyToID("_Color");
/// <summary>
/// 初始化组件引用和 MPB。
/// </summary>
private void Awake()
{
if (targetRenderer == null)
{
targetRenderer = GetComponent<Renderer>();
}
propertyBlock = new MaterialPropertyBlock();
}
/// <summary>
/// 设置当前 Renderer 的颜色。
/// 同时写入两个常见颜色属性,减少 Shader 差异导致颜色不生效的概率。
/// </summary>
/// <param name="color">当前 Renderer 的目标颜色。</param>
public void SetColor(Color color)
{
targetRenderer.GetPropertyBlock(propertyBlock);
// URP Lit 常用属性。
propertyBlock.SetColor(BaseColorId, color);
// Built-in Standard 常用属性。
propertyBlock.SetColor(ColorId, color);
targetRenderer.SetPropertyBlock(propertyBlock);
}
}
这个写法比较稳,但最终还是建议明确项目使用的 Shader 属性名。
九、修改透明度示例
如果液体或高亮对象需要单独改变透明度,可以通过 MPB 修改颜色的 Alpha。
csharp
using UnityEngine;
/// <summary>
/// 使用 MPB 控制当前 Renderer 的透明度。
/// 前提是材质本身的 Shader 和材质渲染模式支持透明显示。
/// </summary>
public class RendererAlphaByMPB : MonoBehaviour
{
/// <summary>
/// 需要控制透明度的 Renderer。
/// </summary>
[SerializeField] private Renderer targetRenderer;
/// <summary>
/// 当前 Renderer 的属性覆盖数据。
/// </summary>
private MaterialPropertyBlock propertyBlock;
/// <summary>
/// 当前颜色缓存。
/// 改透明度时保留 RGB,只改变 Alpha。
/// </summary>
private Color currentColor = Color.white;
/// <summary>
/// URP Lit 常见颜色属性。
/// </summary>
private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
/// <summary>
/// 初始化 Renderer 和 MPB。
/// </summary>
private void Awake()
{
if (targetRenderer == null)
{
targetRenderer = GetComponent<Renderer>();
}
propertyBlock = new MaterialPropertyBlock();
}
/// <summary>
/// 设置当前 Renderer 的透明度。
/// 这里只改颜色 Alpha,不改变材质的透明渲染模式。
/// </summary>
/// <param name="alpha">透明度,0 表示完全透明,1 表示完全不透明。</param>
public void SetAlpha(float alpha)
{
// 限制 Alpha 范围,避免传入异常值导致显示结果不可控。
currentColor.a = Mathf.Clamp01(alpha);
targetRenderer.GetPropertyBlock(propertyBlock);
propertyBlock.SetColor(BaseColorId, currentColor);
targetRenderer.SetPropertyBlock(propertyBlock);
}
}
注意:
text
MPB 可以改 Alpha 数值,但不能把一个不透明材质自动变成透明材质。
如果材质本身没有开启透明混合,Alpha 改了也可能看不出透明效果。
透明显示通常还需要材质本身已经使用透明 Shader 或透明渲染模式。
十、多材质 Renderer 的 MPB 用法
一个 Renderer 可能有多个材质槽。
如果只想改第 0 个材质槽,可以使用带 materialIndex 的重载:
csharp
/// <summary>
/// 修改 Renderer 指定材质槽的颜色。
/// 适用于一个模型有多个材质,只希望其中一个材质槽单独变色的情况。
/// </summary>
/// <param name="targetRenderer">目标 Renderer。</param>
/// <param name="materialIndex">材质槽索引,从 0 开始。</param>
/// <param name="color">目标颜色。</param>
private void SetSlotColor(Renderer targetRenderer, int materialIndex, Color color)
{
// 为本次设置准备属性块。
var block = new MaterialPropertyBlock();
// 读取指定材质槽已有的属性覆盖值,避免覆盖其他参数。
targetRenderer.GetPropertyBlock(block, materialIndex);
// 写入指定材质槽的颜色覆盖。
block.SetColor(Shader.PropertyToID("_BaseColor"), color);
// 只把属性块应用到指定材质槽,不影响其他材质槽。
targetRenderer.SetPropertyBlock(block, materialIndex);
}
如果不传 materialIndex,则属性块会应用到整个 Renderer。
十一、清除 MPB
如果想让对象恢复材质默认显示,可以清空属性块:
csharp
/// <summary>
/// 清除当前 Renderer 上的 MPB 覆盖值。
/// 清除后,Renderer 会重新使用共享材质中的默认参数。
/// </summary>
/// <param name="targetRenderer">需要恢复默认材质显示的 Renderer。</param>
private void ClearPropertyBlock(Renderer targetRenderer)
{
// 传入 null 表示移除 Renderer 上的属性覆盖。
targetRenderer.SetPropertyBlock(null);
}
也可以只清空并重新设置:
csharp
/// <summary>
/// 清空已有属性块内容,但保留 MPB 对象本身继续复用。
/// </summary>
private void ClearAndReuseBlock(Renderer targetRenderer, MaterialPropertyBlock propertyBlock)
{
// 清除属性块内已经记录的所有覆盖参数。
propertyBlock.Clear();
// 将空属性块设置回 Renderer,使当前 Renderer 不再使用之前的覆盖值。
targetRenderer.SetPropertyBlock(propertyBlock);
}
十二、MPB 和材质实例的对比
| 方案 | 是否影响共享材质 | 是否创建材质实例 | 适用场景 |
|---|---|---|---|
sharedMaterial |
会 | 不创建 | 全局修改材质 |
material |
不会 | 会 | 少量对象临时独立材质 |
MaterialPropertyBlock |
不会 | 不创建材质实例 | 大量对象单独改颜色、透明度、数值参数 |
简单判断:
text
想全局改:sharedMaterial
想少量对象完全独立材质:material
想大量对象只改 Shader 参数:MaterialPropertyBlock
十三、项目中的推荐使用方式
如果项目中是"液体材质共享,但每个容器液体颜色可能不同",推荐:
text
材质球保持共享
颜色、透明度、液面高度等按对象变化的参数用 MPB 设置
不要在运行时频繁 renderer.material
不要直接改 renderer.sharedMaterial
推荐封装成一个小组件,例如:
csharp
using UnityEngine;
/// <summary>
/// 液体 Renderer 的显示参数控制器。
/// 使用 MPB 控制单个液体对象的颜色,避免修改共享液体材质导致所有液体一起变化。
/// </summary>
public class LiquidRenderPropertyController : MonoBehaviour
{
/// <summary>
/// 液体模型使用的 Renderer。
/// </summary>
[SerializeField] private Renderer liquidRenderer;
/// <summary>
/// 当前液体 Renderer 的属性覆盖块。
/// </summary>
private MaterialPropertyBlock propertyBlock;
/// <summary>
/// 液体颜色属性 ID。
/// 具体名称需要和液体 Shader 中的颜色属性保持一致。
/// </summary>
private static readonly int LiquidColorId = Shader.PropertyToID("_BaseColor");
/// <summary>
/// 初始化 Renderer 和 MPB。
/// </summary>
private void Awake()
{
if (liquidRenderer == null)
{
liquidRenderer = GetComponent<Renderer>();
}
propertyBlock = new MaterialPropertyBlock();
}
/// <summary>
/// 设置当前液体对象的显示颜色。
/// 只影响当前 Renderer,不影响其他使用同一液体材质的对象。
/// </summary>
/// <param name="color">当前液体需要显示的颜色。</param>
public void SetLiquidColor(Color color)
{
liquidRenderer.GetPropertyBlock(propertyBlock);
propertyBlock.SetColor(LiquidColorId, color);
liquidRenderer.SetPropertyBlock(propertyBlock);
}
}
后续如果液体还有液面高度、浊度、发光强度等参数,也可以继续放到同一个 MPB 里:
csharp
// 液面高度属性,用于按当前容器液体体积改变显示高度。
private static readonly int LiquidHeightId = Shader.PropertyToID("_LiquidHeight");
/// <summary>
/// 设置当前液体的液面高度参数。
/// 该参数只覆盖当前 Renderer,不修改共享材质。
/// </summary>
/// <param name="height">液面高度,具体取值范围由 Shader 约定。</param>
public void SetLiquidHeight(float height)
{
liquidRenderer.GetPropertyBlock(propertyBlock);
propertyBlock.SetFloat(LiquidHeightId, height);
liquidRenderer.SetPropertyBlock(propertyBlock);
}
十四、使用 MPB 的注意事项
1. 属性名必须和 Shader 对上
MPB 设置的是 Shader 属性。
如果 Shader 中没有 _BaseColor,那么:
csharp
propertyBlock.SetColor("_BaseColor", color);
不会产生预期效果。
需要确认 Shader 属性名。
2. SetPropertyBlock 之后才会生效
只调用 SetColor 不会自动应用到 Renderer。
必须调用:
csharp
targetRenderer.SetPropertyBlock(propertyBlock);
3. 多处写 MPB 时要先 GetPropertyBlock
如果多个逻辑都在写同一个 Renderer 的 MPB,建议先读取已有属性块:
csharp
targetRenderer.GetPropertyBlock(propertyBlock);
然后再追加修改。
否则可能把其他逻辑写入的参数覆盖掉。
4. 不要每帧频繁 new MaterialPropertyBlock
推荐缓存:
csharp
private MaterialPropertyBlock propertyBlock;
在 Awake 中创建一次,后续反复使用。
5. MPB 不是材质状态切换工具
MPB 适合设置:
text
Color
Float
Vector
Texture
Matrix
但不适合切换:
text
Shader
Render Queue
Blend Mode
Cull Mode
ZWrite
Shader Keyword
这些仍然应该通过不同材质或材质实例处理。
十五、总结
共享材质修改问题的本质是:
text
多个 Renderer 引用了同一个 Material,对 Material 的修改会被所有引用者看到。
解决方式有三种:
text
sharedMaterial:改共享材质,所有对象一起变。
material:复制材质实例,当前对象单独变,但会增加材质实例。
MaterialPropertyBlock:不复制材质,只覆盖当前 Renderer 的 Shader 参数。
在运行时需要单独控制某个物体颜色、透明度、液体高度、高亮状态时,优先考虑 MPB。
一句话总结:
text
MPB 不是在改材质,而是在给当前 Renderer 增加一份渲染参数覆盖表。
它既避免了共享材质互相影响,也避免了大量创建材质实例。