佳木斯网站建设哪家好网络推广员
一、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 不生效,这个上面刚提到过,毕竟这两种方法本质思路都是开辟一段新的内存用于数据的读取,很明显,在数据唯一的这一铁定条件下,它不可能存在于两块空间中,因此这两套方案可以说是平行/不相容