文章摘要
Material(材质)是Shader的一个实例,这种设计在图形学和游戏引擎中至关重要。Shader是定义渲染算法的GPU程序,而Material则是绑定该Shader并填充具体参数的对象,类似菜谱和具体菜品的关系。例如,同一PBR Shader可以生成不同外观的材质(如金属、粗糙度等参数不同)。这种设计提高了效率(代码复用)、灵活性(参数调整)和可维护性(统一升级)。在Unity或Unreal中,一个Shader模板可实例化出无数独特材质,渲染时材质将参数传递给Shader实现最终效果。简言之,Material是Shader的具体应用实例。
Material(材质)是Shader的一个实例,这句话在图形学和游戏引擎中非常重要。下面我用通俗和专业的方式详细解释这个概念。
1. 类比理解
可以把Shader和Material的关系类比为:
- 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. 视觉流程
- 你在场景中给物体赋予Material。
- Material内部记录了用哪个Shader,以及所有参数的具体值。
- 渲染时,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. 运行时数据流
- 加载Shader:Shader代码被编译,属性表被解析。
- 创建Material:Material引用Shader,并初始化参数表(可用默认值或自定义值)。
- 渲染时: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 渲染时数据流
- Renderer持有Material指针。
- 渲染前,Material将
m_SavedProperties
打包成Uniform Buffer(CBUFFER),传递给GPU。 - Shader使用这些参数进行像素/顶点计算。
- 如果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. 相关源码/文档参考
- UnityCsReference/Material.bindings.cs
- UnityCsReference/Shader.bindings.cs
- Unity SRP Batcher官方文档
- Unity官方论坛/逆向分析
7. 总结
- Unity底层C++中,Material和Shader是分离的对象,Material持有Shader指针和独立参数表。
- 参数表是map结构,按类型分组,渲染时打包成Uniform Buffer上传GPU。
- 关键字控制Shader变体,Material参数和Shader属性一一对应。
- 这种设计保证了高效、灵活和可扩展性,是现代游戏引擎通用做法。