Shader与Material:实例化渲染的艺术

文章摘要

Material(材质)是Shader的一个实例,这种设计在图形学和游戏引擎中至关重要。Shader是定义渲染算法的GPU程序,而Material则是绑定该Shader并填充具体参数的对象,类似菜谱和具体菜品的关系。例如,同一PBR Shader可以生成不同外观的材质(如金属、粗糙度等参数不同)。这种设计提高了效率(代码复用)、灵活性(参数调整)和可维护性(统一升级)。在Unity或Unreal中,一个Shader模板可实例化出无数独特材质,渲染时材质将参数传递给Shader实现最终效果。简言之,Material是Shader的具体应用实例。

Material(材质)是Shader的一个实例,这句话在图形学和游戏引擎中非常重要。下面我用通俗和专业的方式详细解释这个概念。


1. 类比理解

可以把ShaderMaterial的关系类比为:

  • Shader:像“菜谱”——规定了做菜的步骤和方法(比如先炒后煮、加什么调料)。
  • Material:像“这道菜的具体做法”——用具体的食材、调料、火候,按照菜谱做出来的成品。

同一个菜谱(Shader),可以做出无数道味道不同的菜(Material),只要你用的食材(参数)不同。


2. 专业解释

2.1 Shader是什么?

  • Shader是一段GPU程序,定义了如何根据输入(如顶点、纹理、光照等)计算最终的像素颜色。
  • 它规定了“算法”和“流程”,但不包含具体的数据。

2.2 Material是什么?

  • Material是绑定了某个Shader,并为其所有参数赋了具体值的对象
  • 这些参数包括:颜色、贴图、数值、开关等。
  • Material本身不包含算法,只是“填空”——把Shader需要的所有输入都准备好。

2.3 “实例”的含义

  • “实例”就是“具体的一份”。
  • Shader是“模板”或“类”,Material是“对象”或“实例”。
  • 你可以用同一个Shader创建无数个Material,每个Material参数不同,表现也不同。

3. 例子

3.1 Unity中的例子

假设有一个PBR Shader,定义了如下参数:

  • _BaseColor
  • _Metallic
  • _Roughness
  • _MainTex

你可以创建多个Material:

  • Material A:红色、金属度0.8、粗糙度0.2、贴图A
  • Material B:蓝色、金属度0.1、粗糙度0.7、贴图B

它们都用同一个Shader,但参数不同,视觉效果完全不同。

3.2 Unreal中的例子

  • 一个Master Material(Shader),可以实例化出无数Material Instance,每个Instance参数不同。

4. 为什么要这样设计?

  • 高效:同一个Shader代码可以服务于成千上万个Material,节省内存和开发成本。
  • 灵活:只需改参数,不用改Shader代码,就能做出丰富多样的视觉效果。
  • 易维护:Shader升级后,所有Material自动受益,无需逐个修改。

5. 视觉流程

  1. 你在场景中给物体赋予Material。
  2. Material内部记录了用哪个Shader,以及所有参数的具体值。
  3. 渲染时,Material把参数传给Shader,Shader用这些参数计算最终效果。

6. 总结

Material是Shader的一个实例,意思是:Material是基于某个Shader模板,填入具体参数后生成的“具体材质对象”。同一个Shader可以有无数个Material实例,每个实例表现不同,但都遵循同一套渲染算法。


Material是Shader的一个实例
在Unity底层是如何实现的?
它们在内存中是如何组织和关联的?

下面我会结合Unity的源码(基于公开的C++/C#部分和官方文档),详细剖析Shader和Material的底层结构与关系。


1. Unity中的Shader和Material对象

1.1 Shader

  • Unity的Shader对象本质上是对GPU着色器程序的封装。
  • 在C#层,Shader是一个UnityEngine.Object的子类,主要负责管理着色器代码、编译状态、关键字变体等。
  • 在C++引擎层,Shader对象会持有所有编译好的shader变体(Variant),以及参数描述表(Property Sheet)。

1.2 Material

  • Material同样是UnityEngine.Object的子类。
  • Material内部有:
    • 一个指向Shader的引用(指明用哪个Shader)
    • 一组参数表(PropertyBlock),存储所有参数的具体值(如颜色、贴图、数值等)
    • 关键字开关(Shader Keywords),决定启用Shader的哪些分支
    • RenderQueue、Pass等渲染相关设置

2. 内存结构简化图

[Material]
   |
   |---> [Shader] (只读引用)
   |
   |---> [PropertyBlock] (参数表)
   |         |-- _Color: (1,0,0,1)
   |         |-- _MainTex: Texture2D实例
   |         |-- _Metallic: 0.5
   |
   |---> [Keywords] (如 _EMISSION_ON)
   |
   |---> [RenderQueue/Pass/Tag等]
  • 多个Material可以引用同一个Shader,但PropertyBlock内容各自独立。

3. Unity源码实现(C#层)

3.1 Material的核心字段(C#伪代码)

public class Material : Object
{
    // 指向Shader对象
    public Shader shader { get; set; }

    // 参数表,存储所有属性的值
    private Dictionary<string, object> m_Properties;

    // 关键字
    private List<string> m_Keywords;

    // 其他渲染设置
    private int m_RenderQueue;
    // ...
}

3.2 Shader的核心字段(C#伪代码)

public class Shader : Object
{
    // 着色器代码和变体
    private byte[] m_ShaderCode;
    private List<ShaderVariant> m_Variants;

    // 属性描述表
    private List<ShaderProperty> m_Properties;
    // ...
}

3.3 Material和Shader的关系

  • Material持有Shader的引用(只读,不会复制Shader内容)。
  • Material的参数表(m_Properties)会根据Shader的属性描述表(m_Properties)初始化。
  • Material的参数值可以和Shader的默认值不同。

4. Unity底层C++实现(简要)

  • Unity底层是C++实现,C#层是托管包装。
  • C++层的Material对象(Material::m_Shader)持有Shader的指针。
  • Material的参数表(Material::m_SavedProperties)是一个序列化的结构,存储所有参数的实际值。
  • Shader的属性表(Shader::m_Properties)是只读的,描述了有哪些参数、类型、默认值等。

5. 运行时数据流

  1. 加载Shader:Shader代码被编译,属性表被解析。
  2. 创建Material:Material引用Shader,并初始化参数表(可用默认值或自定义值)。
  3. 渲染时:Material将参数表内容打包成Uniform Buffer,传递给GPU,Shader用这些参数进行渲染。

6. 关键源码片段(Unity C#)

6.1 Material构造函数

public Material(Shader shader)
{
    this.shader = shader;
    // 初始化参数表
    // ...
}

6.2 设置参数

public void SetColor(string name, Color value)
{
    // 更新参数表
    m_Properties[name] = value;
    // 通知底层同步
}

6.3 关键字控制

public void EnableKeyword(string keyword)
{
    m_Keywords.Add(keyword);
    // 触发Shader变体切换
}

7. 多Material共享Shader的好处

  • 节省内存:Shader只加载一份,Material只存参数。
  • 高效渲染:同一Shader的Material可以批量渲染(SRP Batcher)。
  • 灵活扩展:只需改参数,不用复制Shader代码。

8. 参考资料


9. 总结

  • Material是Shader的实例,本质上是“引用Shader+存储参数”。
  • Material和Shader在内存中分离,Material只持有Shader引用和参数表。
  • 多个Material可共享同一Shader,参数互不影响。
  • Unity底层通过C++对象和C#托管包装实现高效的Shader/Material协作。

下面我将以Unity引擎底层C++源码为主线,深入分析Material和Shader的内存结构、参数管理、实例化与渲染流程。由于Unity引擎的完整C++源码并不开源,但通过Unity官方泄露的部分源码、Mono/C#绑定、以及Unity技术文档和社区逆向分析,我们可以还原出较为准确的底层实现原理。


1. Unity底层对象关系

1.1 主要类结构(伪C++)

// Shader.h
class Shader : public Object
{
public:
    std::vector<ShaderProperty> m_Properties; // 属性描述表
    std::vector<ShaderVariant> m_Variants;    // 变体
    // ... 其他成员
};

// Material.h
class Material : public Object
{
public:
    Shader* m_Shader; // 指向Shader对象
    SavedProperties m_SavedProperties; // 材质参数表
    std::vector<std::string> m_Keywords; // 关键字
    int m_RenderQueue;
    // ... 其他成员
};

1.2 SavedProperties结构

struct SavedProperties
{
    std::map<std::string, Color> m_Colors;
    std::map<std::string, float> m_Floats;
    std::map<std::string, Texture*> m_TexEnvs;
    // ... 其他类型
};

2. Shader与Material的实例化流程

2.1 Shader加载

  • Unity解析ShaderLab文件,编译成平台相关的着色器代码(如HLSL/GLSL/Metal)。
  • 解析所有Properties,生成ShaderProperty表,记录参数名、类型、默认值、UI描述等。
  • 编译所有变体(根据关键字组合)。

2.2 Material创建

  • Material实例化时,持有Shader指针。
  • Material的m_SavedProperties根据Shader的m_Properties初始化,参数值可用默认值或自定义值。
  • Material的参数表和Shader的属性表一一对应,但只存储实际值。

3. 参数同步与渲染流程

3.1 参数设置

  • C#层调用Material.SetFloat等API,底层通过ICall(Internal Call)进入C++,更新m_SavedProperties
  • 关键字(Keywords)同理,更新m_Keywords,影响Shader变体选择。

3.2 渲染时数据流

  1. Renderer持有Material指针。
  2. 渲染前,Material将m_SavedProperties打包成Uniform Buffer(CBUFFER),传递给GPU。
  3. Shader使用这些参数进行像素/顶点计算。
  4. 如果Material关键字变化,Shader变体切换,重新绑定对应的GPU程序。

4. 关键源码片段(伪C++)

4.1 Material参数设置

void Material::SetFloat(const std::string& name, float value)
{
    m_SavedProperties.m_Floats[name] = value;
    // 标记脏位,通知渲染线程同步
}

4.2 Material与Shader的绑定

void Material::SetShader(Shader* shader)
{
    m_Shader = shader;
    // 重新初始化参数表
    // ...
}

4.3 Uniform Buffer打包

void Material::UploadPropertiesToGPU()
{
    for (auto& prop : m_Shader->m_Properties)
    {
        // 查找Material参数表
        // 按类型写入Uniform Buffer
    }
    // 提交到GPU
}

4.4 关键字与变体切换

void Material::SetKeyword(const std::string& keyword, bool enabled)
{
    if (enabled)
        m_Keywords.push_back(keyword);
    else
        m_Keywords.remove(keyword);

    // 重新选择Shader变体
    m_Shader->SelectVariant(m_Keywords);
}

5. 内存与性能优化

  • Shader只读共享:所有Material只持有Shader指针,Shader代码和属性表只加载一份。
  • Material参数独立:每个Material的参数表独立,互不影响。
  • SRP Batcher:同一Shader的Material参数可批量上传,极大提升渲染效率。
  • 变体管理:Shader变体在内存中按需加载,避免爆炸性增长。

6. 相关源码/文档参考


7. 总结

  • Unity底层C++中,Material和Shader是分离的对象,Material持有Shader指针和独立参数表。
  • 参数表是map结构,按类型分组,渲染时打包成Uniform Buffer上传GPU。
  • 关键字控制Shader变体,Material参数和Shader属性一一对应。
  • 这种设计保证了高效、灵活和可扩展性,是现代游戏引擎通用做法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值