Renderer 使用材质分析:materials、sharedMaterials 及 MaterialPropertyBlock
一、materials 与 sharedMaterials
1.1 使用上的区别与差异
Unity 开发时,在 C# 中通过 Renderer 取材质操作是非常常见的操作,Renderer 有两种常规获取材质的方式:
sharedMaterials:可以理解这个就是原始材质,所有使用了同一个材质资源的模型 renderer, sharedMaterial 相同,修改了 sharedMaterials 相当于就是修改了资源
materials:material 这个相当于 material Instance,比如同一个箱子模型实例化两个 renderer, sharedMaterial 相同,这时候你想让其中一个箱子是红色的,另一个箱子是绿色的,这时候就可以使用 material,clone 不同的材质实例来做表现的差异化
当然,除了 clone material instance 来做表现差异化,Unity 更希望你用 MaterialPropertyBlock,这个才是做材质表现差异化的正确路子
先不提 MaterialPropertyBlock,我们拉回到 sharedMaterial,material,有带 s 的和不带 s 的,这个对应美术同学制作过程中的多维子材质的概念:也就是一个完整几何体是可以有多个材质,虽然渲染次数还是多次,但完整的几何体天然的解决缝合问题,并且如果相邻渲染,几何体可以不用重复传入,性能好一些,所以不管 sharedMaterials 还是 materials,都是数组的形式存储,对于没有 s 的,其实就是取数组的第一个元素,因为大多数情况数组里还是仅仅有一个元素
综上所述,我们脑海中的 Renderer 内部是什么样子的,一定有个 sharedMaterial Array 和 material Array,当 material Array 存在的时候,就用其来渲染,否则用 sharedMaterial Array 来渲染,这样我使用了 material,后边的逻辑想舍弃它继续用 sharedMaterial,只需置空 material Array 即可,真实的 Unity 是这样的规则吗?直接上 Unity 源码
1.2 Unity 源码分析
[NativeHeader("Runtime/Graphics/Renderer.h")]
public partial class Renderer : Component
{[FreeFunction(Name = "RendererScripting::GetMaterial", HasExplicitThis = true)] extern private Material GetMaterial();[FreeFunction(Name = "RendererScripting::GetSharedMaterial", HasExplicitThis = true)] extern private Material GetSharedMaterial();[FreeFunction(Name = "RendererScripting::SetMaterial", HasExplicitThis = true)] extern private void SetMaterial(Material m);[FreeFunction(Name = "RendererScripting::GetMaterialArray", HasExplicitThis = true)] extern private Material[] GetMaterialArray();[FreeFunction(Name = "RendererScripting::GetMaterialArray", HasExplicitThis = true)] extern private void CopyMaterialArray([Out] Material[] m);[FreeFunction(Name = "RendererScripting::GetSharedMaterialArray", HasExplicitThis = true)] extern private void CopySharedMaterialArray([Out] Material[] m);[FreeFunction(Name = "RendererScripting::SetMaterialArray", HasExplicitThis = true)] extern private void SetMaterialArray([NotNull] Material[] m);public Material[] materials{get{#if UNITY_EDITORif (IsPersistent()){Debug.LogError("Not allowed to access Renderer.materials on prefab object. Use Renderer.sharedMaterials instead", this);return null;}#endifreturn GetMaterialArray();}set { SetMaterialArray(value); }}public Material material{get{#if UNITY_EDITORif (IsPersistent()){Debug.LogError("Not allowed to access Renderer.material on prefab object. Use Renderer.sharedMaterial instead", this);return null;}#endifreturn GetMaterial();}set { SetMaterial(value); }}public Material sharedMaterial { get { return GetSharedMaterial(); } set { SetMaterial(value); } }public Material[] sharedMaterials { get { return GetSharedMaterialArray(); } set { SetMaterialArray(value); } }}
namespace RendererScripting
{Material* GetMaterial(Renderer* r);Material* GetSharedMaterial(Renderer* r);void SetMaterial(Renderer* r, Material* m);dynamic_array<Material*> GetMaterialArray(Renderer* r);void GetMaterialArray(Renderer* r, dynamic_array<Material*>& mat);void GetSharedMaterialArray(Renderer* r, dynamic_array<PPtr<Material> >& mat);void SetMaterialArray(Renderer* r, const dynamic_array<Material*>& ma);}
Material* RendererScripting::GetMaterial(Renderer* r)
{
#if UNITY_EDITORDebugAssert(!r->IsPersistent());
#endifreturn r->GetAndAssignInstantiatedMaterial(0, false);
}Material* RendererScripting::GetSharedMaterial(Renderer* r)
{return r->GetMaterialCount() ? r->GetMaterial(0) : 0;
}void RendererScripting::SetMaterial(Renderer* r, Material* m)
{r->SetMaterialCount(std::max(1, r->GetMaterialCount()));r->SetMaterial(m, 0);
}void RendererScripting::GetMaterialArray(Renderer* r, dynamic_array<Material*>& mat)
{
#if UNITY_EDITORDebugAssert(!r->IsPersistent());
#endifDebugAssert(r->GetMaterialCount() <= mat.size());for (int i = 0, in = r->GetMaterialCount(); i < in; ++i)mat[i] = r->GetAndAssignInstantiatedMaterial(i, false);
}void RendererScripting::GetSharedMaterialArray(Renderer* r, dynamic_array<PPtr<Material> >& mat)
{DebugAssert(r->GetMaterialCount() <= mat.size());for (int i = 0, in = r->GetMaterialCount(); i < in; ++i)mat[i] = r->GetMaterialArray()[i];
}dynamic_array<Material*> RendererScripting::GetMaterialArray(Renderer* r)
{dynamic_array<Material*> ret(r->GetMaterialCount(), kMemDynamicArray);RendererScripting::GetMaterialArray(r, ret);return ret;
}void RendererScripting::SetMaterialArray(Renderer* r, const dynamic_array<Material*>& ma)
{r->SetMaterialCount(ma.size());for (int i = 0, in = ma.size(); i < in; ++i)r->SetMaterial(ma[i], i);
}
上述代码仅仅截取了 Renderer 中 sharedMaterial 和 material 的源代码调用部分,其他部分暂时省去,先从源代码的 get 部分分析,可以发现:
material 的 get 最终调用来自于 Renderer::GetAndAssignInstantiatedMaterial 函数
sharedMaterial 的 get 最终调用来自于 Renderer::GetMaterial 函数
virtual PPtr<Material> Renderer::GetMaterial(int i) const override
{ return m_Materials[i];
}
void Renderer::SetMaterial(PPtr<Material> material, int index)
{Assert(index < (int)m_Materials.size());m_Materials[index] = material;/*#if !DEPLOY_OPTIMIZEDMaterial* materialPtr = material;if (materialPtr && materialPtr->GetOwner ().GetInstanceID () != 0 && materialPtr->GetOwner() != PPtr<Object> (this)){ErrorString("Assigning an instantiated material is not a good idea. Since the material is owned by another game object, it will be destroyed when the game object is destroyed.\nYou probably want to explicitly instantiate the material.");}#endif*/SetDirty();
}
Material* Renderer::GetAndAssignInstantiatedMaterial(int i, bool allowFromEditMode)
{// Grab shared materialMaterial* material = NULL;if (GetMaterialCount() > i)material = GetMaterial(i);// instantiate material if necessaryMaterial* instantiated = &Material::GetInstantiatedMaterial(material, *this, allowFromEditMode);// Assign materialif (material != instantiated){SetMaterialCount(std::max(GetMaterialCount(), i + 1));SetMaterial(instantiated, i);}return instantiated;
}
从 Renderer 的源码分析,内部仅仅有一个 material 数组,而不是两个,这个和我们上述脑海中浮现的数据结构已经不一样了,很有意思,倒是要看看他到底怎么做的:因此到这继续深度分析,抛开诸多假象来看实际的本质
class EXPORT_COREMODULE Renderer : public Unity::Component, public BaseRenderer
{ ...........typedef dynamic_array<PPtr<Material> > MaterialArray;MaterialArray m_Materials; ///< List of materials to use when rendering.........
}
看上边 Renderer::GetAndAssignInstantiatedMaterial 的实现,发现其主要调用了 Material::GetInstantiatedMaterial 来实现的材质克隆,再粘一下代码
Material& Material::GetInstantiatedMaterial(Material* material, Object& renderer, bool allowInEditMode)
{if (material == NULL)material = GetDefaultMaterial();if (material->m_Owner == PPtr<Object>(&renderer))return *material;else{if (!allowInEditMode && !IsWorldPlaying())ErrorStringObject("Instantiating material due to calling renderer.material during edit mode. This will leak materials into the scene. You most likely want to use renderer.sharedMaterial instead.", &renderer);// Make sure the properties are initialized before we're cloning, otherwise we'll end up using the properties of the default materialmaterial->EnsurePropertiesExist();Material* instance;instance = CreateObjectFromCode<Material>();instance->SetNameCpp(Append(material->GetName(), " (Instance)"));instance->m_Shader = material->m_Shader;instance->m_Owner = &renderer;// Creating the material above already creates the shared material data, so release and create a new one using copy constructor.// Would be nice to avoid this extra work (but then, the default "create material" already does a bunch of extra other work// that we are discarding here; an optimization for some future day).SAFE_RELEASE(instance->m_SharedMaterialData);instance->m_SharedMaterialData = UNITY_NEW(SharedMaterialData, kMemMaterial)(*material->m_SharedMaterialData);instance->m_SharedMaterialData->smallMaterialIndex = instance->GetInstanceID();instance->CopySettingsFromOther(*material);instance->m_SavedProperties = material->m_SavedProperties;return *instance;}
}
从当中可以看出:
- Material 的 m_Owner 是否指向是 Renderer,决定此 Material 是否是 Renderer 的实例化材质(InstantiatedMaterial)
- 如果传入的 material 是①所述的实例化材质直接返回,否则就克隆一份 material 并都将归属 m_Owner 指向 renderer
- 克隆出来的实例材质名字规则是在原有 material 的名字后边追加字符串(Instance)
1.3 得出结论
- Renderer 仅有一份 material 数组,模型初始化后,其内容就为 sharedMaterial(以 mesh 类为例,可以理解 MeshRenderer 创建后,material 数组中的内容就是 prefab 上的材质资源列表
- 执行 renderer.material,无论左值还是右值都会触发 Renderer 的创建材质实例的函数,此时如果 material 数组中的材质已经是本 renderer 创造的材质实例,则直接返回,否则创建返回,它会覆盖 shareMaterial
- 在步骤②之后,如果再次调用 renderer.shareMaterial,右值则直接返回当前 material 数组中的材质(当然它已经不再是最早的那个 material 资产了,也就是说此 renderer.shareMaterial 非比原 renderer.shareMaterial),左值则会再次实例化创建新的 material 并赋值,material 数组中的材质会再次被覆盖
因此,不存在最早我们分析的:sharedMaterial 和 material 是泾渭分明的两套数组存储,他们最终 cache 在同一个数组里,就是说你调用了 renderer.material 后,renderer.sharedMaterial 也就变成了你最新克隆的 renderer.material,如果你需要找回初始的 renderer.sharedMaterial,就只能自己提前 cache
源码中 m_Owner 就是材质克隆归属的依据:如果不满足克隆规则,Unity 会重新克隆,这样很多时候都会和我们预想的事与愿违
觉得绕可以直接看下面的例子:
Renderer r = go.GetComponent<Renderer>();
Material ma = r.material;
r.sharedMaterial = ms1;
Material mb = r.material;
上述代码,假设 r.sharedMaterial 为 ms。
- 执行完第2句,r.material 和 r.sharedMaterial 皆为 ms(Instance),ms 在此处的引用已经丢失
- 执行完第3句,r.sharedMaterial 为 ms1
- 执行完第4句,r.material 和 r.sharedMaterial 皆为 ms1(Instance),ms1 在此处的引用已经丢失
所以,替换 sharedMaterial 要谨慎,如果你直接修改了它(r.sharedMaterial),可能就会动到资源文件,但如果你在实例化了 material 之后再访问 renderer.shareMaterial,难以避免的会再次实例化一个新的材质,很明显这个时候就会出现资源的浪费(一个 GO 实例化了两个 material),因此尽量还是采用 MaterialPropertyBlock 来做材质个性化的事
二、MaterialPropertyBlock
https://www.jianshu.com/p/eff18c57fa42
使用MaterialPropertyBlock来替换Material属性操作 - UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技
接上文,其实从应用层考虑的话,其实我们只是想实现一个简单的需求:那就是修改当前 GameObject(Renderer) 的材质属性
前面提到过,如果你通过 renderer.material 修改材质属性,那么其实底层相当于是给你实例化了一个新的 material,并且这个 material 专属于当前的 GO,其实这样也没有问题,只要你能管理好这个实例化后的 material 也不是不行
当然还有一个更快更省的方法,就是使用 MaterialPropertyBlock
//一个使用 MaterialPropertyBlock 及 Renderer.SetPropertyBlock 修改材质的例子
private void onFxCircleLoaded(GameObject obj, int fxId, object userData)
{if (!obj || userData == null){return;}Vector4 vec = (Vector4)userData;var mr = obj.GetComponentInChildren<MeshRenderer>();if (mr){var mpb = MCommonObjectPool<MaterialPropertyBlock>.Get();mr.GetPropertyBlock(mpb);mpb.SetVector("_Params", vec);mr.SetPropertyBlock(mpb);mpb.Clear();MCommonObjectPool<MaterialPropertyBlock>.Release(mpb);}
}
网上很多文章都会将 MaterialPropertyBlock 和 GPU Instancing 绑定讲解,但其实 MaterialPropertyBlock 本质上只是一种优化的手段:其还可以被用于 Graphics.DrawMesh 和 Renderer.SetPropertyBlock 两个 API,当我们想要绘制许多相同材质但不同属性的对象时都可以使用它(无论是否 GPU Instancing)
它和直接赋值 renderer.material 不同,完全不会产生额外的材质实例,使用 MaterialPropertyBlock 会直接覆盖某个渲染器上对应的属性,开辟一片新的存储空间存储当前变量而并非在原先的 cbuffer (此 cbuffer 非比 DX 里的 constant buffer,更准确的说法应该是指 Unity 材质属性区)里面,后面也不再从 cbuffer 中拿数据了,也因此它会打破 SRP Batcher
2.1 使用 MaterialPropertyBlock 的注意事项
- 没有必要在 shader 属性前面声明 [PerRendererData] 前缀:有教程将 [PerRendererData] 和 MaterialPropertyBlock 捆绑在了一起,其实它们没有直接的逻辑关系,[PerRendererData] 只影响 Editor 的显示行为,即当你通过 MaterialPropertyBlock 改变了某个 Material 的属性之后,只有加上了这个,才能在预览对应的 Material 面板上看到对应属性值的变更,很明显,这没有太大的意义
- MaterialPropertyBlock 会使得 SRP Batcher 不生效,这个上面刚提到过,毕竟这两种方法本质思路都是开辟一段新的内存用于数据的读取,很明显,在数据唯一的这一铁定条件下,它不可能存在于两块空间中,因此这两套方案可以说是平行/不相容
Renderer 使用材质分析:materials、sharedMaterials 及 MaterialPropertyBlock相关推荐
- [我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之材质了解Materials...
[我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之材质了解Materials 既上一篇分享了中文字幕的灯光介绍Lights后,本篇分享一下第3个已完工 ...
- 3D打印中常见的7中材质分析
3D打印中常见的7中材质分析 3D打印中使用过的材质是有很多种的,想要选择合适的3D打印材料,就必须了解3D打印材料的特性,每种3D打印材料都有自己的特性,应该根据自己产品的需求以及打印材料特性,有针 ...
- UE4 ZenGarden Rake 材质分析
ZenGarden Rake 材质分析 今天要讨论的问题就是这个随着鼠标的拖动,能够形成痕迹的效果是怎么实现的 逻辑控制 主要的逻辑控制实在这个actor里面,和之前的讨论一样,如果它被点击的花,会触 ...
- 物理材质(Physics Materials)
在Unity3d(5.5)中已经配置好了7种常用的物理材质,Bouncy,Ice,Metal,Rubber,Wood,MaxFriction,ZeroFriction在菜单中依次选择Assets - ...
- 怎么做应力应变曲线_做冲压材质分析很重要,材料性能分析汇总~
1.关于拉伸力-伸长曲线和应力-应变曲线的问题 低碳钢的应力-应变曲线 a.拉伸过程的变形: 弹性变形,屈服变形,加工硬化(均匀塑性变形),不均匀集中塑性变形. b.相关公式: 工程应力 σ=F/A0 ...
- Unity MeshRender更换材质球方法
https://blog.csdn.net/ystistheking/article/details/70207792 转载自CSDN布莱克汉: 干活的时候遇到了这样一个问题,当要用代码给这个模型换材 ...
- Cesium 源码分析 Material
Cesium关于实例的创建都是封装在类的静态函数中,这个习惯很好,方便创建和管理,对外只提供创建的方法,正如Material中的Material.fromType()函数一样,传递参数创建材质. Ma ...
- unity 一个物体赋予多个材质球
修改Materials size 大小 添加不同材质 注意在添加覆盖花纹类型的材质是修改 Rendering Mode 渲染模式总共有四种: 渲染模式 意思 适用对象举例 说明 Opaque 不透明 ...
- Unity Trail Renderer(拖尾渲染器)
@[TOC](Unity Trail Renderer(拖尾渲染器)) 介绍 Trail Renderer(拖尾渲染器)是Unity源生自带的组件,直接添加就好. 使用拖尾渲染器 (trail ren ...
最新文章
- Atitit.文件搜索工具 attilax 总结
- 这 100 个心理学知识你必须了解
- Android:四种启动模式分析
- 2019年安徽省模块七满分多少_艺考资讯 | 2021年美术统考考多少分才能通过?过了合格线有什么意义?美术生一定要重视!...
- vb.net 设置打印纸张与页边距_文字办公—Word文档如何设置装订线
- MATLAB-基本语法
- nginx php实例,多个mysql,nginx,php实例环境安装zabbix(完全自定义)
- 网络访问保护(NAP)技术之详解
- Win32 Thread Information Block
- 扩展方法/对象与集合初始化器
- 偶师傅说过的很有意思的话
- 好好学习努力工作,要工作也要生活—2016总结,2017规划
- IDEA 配置 google翻译插件(Translate)
- win10本地计算机策略进不去,win10系统gpedit.msc打不开怎么处理 win10本地安全策略打不开...
- 详解第一范式、第二范式、第三范式、BCNF范式
- Muzli – 所有你需要的设计灵感都在这
- 一年增加 1.2w 星,Dapr 能否引领云原生中间件的未来?
- Go:go程序报错Cannot run program
- linux添加菜单栏,Gnome desktop主菜单中添加自己的菜单栏
- web网页调用本地cs客户端程序exe
热门文章
- Acrobat Pro 集成升级包的方法
- mac电脑的环境变量怎样配置?
- 人工智能、机器学习和深度学习有哪些区别?
- 最简单C/C++数据可视化函数库MathGL配置方法
- 深度篇——目标检测史(二) 细说 R-CNN 目标检测
- win10下执行Hadoop命令报错:系统找不到指定的路径。Error: JAVA_HOME is incorrectly set. Please update D:\
- 反调试(设置主线程为隐藏调试破坏调试通道调试器的检测)
- front-matter使用详解
- java手游+刺客_自走棋手游:刺客流阵容很弱?掌握了精髓玩法,轻松上皇后
- Linux基于qt的开题报告,基于qt图像的开题报告