一、materials 与 sharedMaterials

1.1 使用上的区别与差异

Unity 开发时,在 C# 中通过 Renderer 取材质操作是非常常见的操作,Renderer 有两种常规获取材质的方式:

  1. sharedMaterials:可以理解这个就是原始材质,所有使用了同一个材质资源的模型 renderer, sharedMaterial 相同,修改了 sharedMaterials 相当于就是修改了资源

  2. 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;}
}

从当中可以看出:

  1. Material 的 m_Owner 是否指向是 Renderer,决定此 Material 是否是 Renderer 的实例化材质(InstantiatedMaterial)
  2. 如果传入的 material 是①所述的实例化材质直接返回,否则就克隆一份 material 并都将归属 m_Owner 指向 renderer
  3. 克隆出来的实例材质名字规则是在原有 material 的名字后边追加字符串(Instance)

1.3 得出结论

  1. Renderer 仅有一份 material 数组,模型初始化后,其内容就为 sharedMaterial(以 mesh 类为例,可以理解 MeshRenderer 创建后,material 数组中的内容就是 prefab 上的材质资源列表
  2. 执行 renderer.material,无论左值还是右值都会触发 Renderer 的创建材质实例的函数,此时如果 material 数组中的材质已经是本 renderer 创造的材质实例,则直接返回,否则创建返回,它会覆盖 shareMaterial
  3. 在步骤②之后,如果再次调用 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 的注意事项

  1. 没有必要在 shader 属性前面声明 [PerRendererData] 前缀:有教程将 [PerRendererData] 和 MaterialPropertyBlock 捆绑在了一起,其实它们没有直接的逻辑关系,[PerRendererData] 只影响 Editor 的显示行为,即当你通过 MaterialPropertyBlock 改变了某个 Material 的属性之后,只有加上了这个,才能在预览对应的 Material 面板上看到对应属性值的变更,很明显,这没有太大的意义
  2. MaterialPropertyBlock 会使得 SRP Batcher 不生效,这个上面刚提到过,毕竟这两种方法本质思路都是开辟一段新的内存用于数据的读取,很明显,在数据唯一的这一铁定条件下,它不可能存在于两块空间中,因此这两套方案可以说是平行/不相容

Renderer 使用材质分析:materials、sharedMaterials 及 MaterialPropertyBlock相关推荐

  1. [我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之材质了解Materials...

    [我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之材质了解Materials 既上一篇分享了中文字幕的灯光介绍Lights后,本篇分享一下第3个已完工 ...

  2. 3D打印中常见的7中材质分析

    3D打印中常见的7中材质分析 3D打印中使用过的材质是有很多种的,想要选择合适的3D打印材料,就必须了解3D打印材料的特性,每种3D打印材料都有自己的特性,应该根据自己产品的需求以及打印材料特性,有针 ...

  3. UE4 ZenGarden Rake 材质分析

    ZenGarden Rake 材质分析 今天要讨论的问题就是这个随着鼠标的拖动,能够形成痕迹的效果是怎么实现的 逻辑控制 主要的逻辑控制实在这个actor里面,和之前的讨论一样,如果它被点击的花,会触 ...

  4. 物理材质(Physics Materials)

    在Unity3d(5.5)中已经配置好了7种常用的物理材质,Bouncy,Ice,Metal,Rubber,Wood,MaxFriction,ZeroFriction在菜单中依次选择Assets - ...

  5. 怎么做应力应变曲线_做冲压材质分析很重要,材料性能分析汇总~

    1.关于拉伸力-伸长曲线和应力-应变曲线的问题 低碳钢的应力-应变曲线 a.拉伸过程的变形: 弹性变形,屈服变形,加工硬化(均匀塑性变形),不均匀集中塑性变形. b.相关公式: 工程应力 σ=F/A0 ...

  6. Unity MeshRender更换材质球方法

    https://blog.csdn.net/ystistheking/article/details/70207792 转载自CSDN布莱克汉: 干活的时候遇到了这样一个问题,当要用代码给这个模型换材 ...

  7. Cesium 源码分析 Material

    Cesium关于实例的创建都是封装在类的静态函数中,这个习惯很好,方便创建和管理,对外只提供创建的方法,正如Material中的Material.fromType()函数一样,传递参数创建材质. Ma ...

  8. unity 一个物体赋予多个材质球

    修改Materials size 大小 添加不同材质 注意在添加覆盖花纹类型的材质是修改 Rendering Mode 渲染模式总共有四种: 渲染模式 意思 适用对象举例 说明 Opaque 不透明 ...

  9. Unity Trail Renderer(拖尾渲染器)

    @[TOC](Unity Trail Renderer(拖尾渲染器)) 介绍 Trail Renderer(拖尾渲染器)是Unity源生自带的组件,直接添加就好. 使用拖尾渲染器 (trail ren ...

最新文章

  1. Atitit.文件搜索工具 attilax 总结
  2. 这 100 个心理学知识你必须了解
  3. Android:四种启动模式分析
  4. 2019年安徽省模块七满分多少_艺考资讯 | 2021年美术统考考多少分才能通过?过了合格线有什么意义?美术生一定要重视!...
  5. vb.net 设置打印纸张与页边距_文字办公—Word文档如何设置装订线
  6. MATLAB-基本语法
  7. nginx php实例,多个mysql,nginx,php实例环境安装zabbix(完全自定义)
  8. 网络访问保护(NAP)技术之详解
  9. Win32 Thread Information Block
  10. 扩展方法/对象与集合初始化器
  11. 偶师傅说过的很有意思的话
  12. 好好学习努力工作,要工作也要生活—2016总结,2017规划
  13. IDEA 配置 google翻译插件(Translate)
  14. win10本地计算机策略进不去,win10系统gpedit.msc打不开怎么处理 win10本地安全策略打不开...
  15. 详解第一范式、第二范式、第三范式、BCNF范式
  16. Muzli – 所有你需要的设计灵感都在这
  17. 一年增加 1.2w 星,Dapr 能否引领云原生中间件的未来?
  18. Go:go程序报错Cannot run program
  19. linux添加菜单栏,Gnome desktop主菜单中添加自己的菜单栏
  20. web网页调用本地cs客户端程序exe

热门文章

  1. Acrobat Pro 集成升级包的方法
  2. mac电脑的环境变量怎样配置?
  3. 人工智能、机器学习和深度学习有哪些区别?
  4. 最简单C/C++数据可视化函数库MathGL配置方法
  5. 深度篇——目标检测史(二) 细说 R-CNN 目标检测
  6. win10下执行Hadoop命令报错:系统找不到指定的路径。Error: JAVA_HOME is incorrectly set. Please update D:\
  7. 反调试(设置主线程为隐藏调试破坏调试通道调试器的检测)
  8. front-matter使用详解
  9. java手游+刺客_自走棋手游:刺客流阵容很弱?掌握了精髓玩法,轻松上皇后
  10. Linux基于qt的开题报告,基于qt图像的开题报告