Unity 共享材质修改问题与 MaterialPropertyBlock 解决方案

一、问题背景

在 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 增加一份渲染参数覆盖表。
它既避免了共享材质互相影响,也避免了大量创建材质实例。
相关推荐
丁小未2 小时前
Unity 极致高效的IM设计方案教程
unity·性能优化·游戏引擎·im
unityのkiven3 小时前
Unity UGUI 中 EventTrigger 导致 ScrollRect 鼠标滚轮失效的问题分析与解决
unity·计算机外设·游戏引擎
丁小未7 小时前
Unity动态图集方案
unity·游戏引擎
元气少女小圆丶9 小时前
停止单机,开始学一点后台通信吧
unity
丁小未11 小时前
Unity高效的DOTS动画方案Rukhanka深度剖析
unity·游戏引擎·rukhanka·dots动画·gpu动画·gpuecsanimation
unityのkiven13 小时前
Unity 中的 Mesh 和 Renderer 详解:从模型数据到画面显示
unity·游戏引擎
玖玥拾1 天前
Unity 3D 基础(一)Unity基础架构、物理系统、视觉渲染、空间变换与对象查询
笔记·游戏·3d·unity·游戏引擎