• RuntimesUnity
  • 关于材质MaterialPropertyBlock 的问题

我在runtime下使用了MaterialPropertyBlock

这导致材质一直在被更新 情况与下帖基本一致

https://zh.esotericsoftware.com/forum/d/17888-%E6%9D%90%E8%B4%A8%E5%9C%A8%E8%BF%90%E8%A1%8C%E6%97%B6%E4%BC%9A%E4%B8%80%E7%9B%B4%E6%9B%BF%E6%8D%A2%E9%80%A0%E6%88%90%E9%97%AA%E7%83%81%E7%8E%B0%E8%B1%A1
代码部分:


这就导致了
SkeletonRenderer这部分代码一直在刷新
if (updateTriangles) { // Check if the triangles should also be updated.
meshGenerator.FillTriangles(currentMesh);
meshRenderer.sharedMaterials = rendererBuffers.GetUpdatedSharedMaterialsArray();

我看spineruntime的文档是可以使用MaterialPropertyBlock 的 我想了解这部分内容 如果可以使用该如何避免上面的问题 如果不能使用该如何处理呢?

Related Discussions
...

在运行时使用 MaterialPropertyBlock 是一种有效的方法来更改材质属性,而不需要创建新的材质实例。根据 spine-unity 文档MaterialPropertyBlock 可以用于在不创建新材质实例的情况下更改单个 MeshRenderer 的材质属性。

然而,使用 MaterialPropertyBlock 时需要注意以下几点,以避免材质不断更新的问题:

  1. 缓存 MaterialPropertyBlock 实例:不要在每次需要更改属性时创建新的 MaterialPropertyBlock 实例。相反,应该在类中缓存一个 MaterialPropertyBlock 实例,并在需要时更新其属性。

  2. 使用 Shader.PropertyToID:为了提高性能,使用 Shader.PropertyToID(string) 来缓存属性的 int ID,而不是每次使用字符串。

  3. 避免频繁调用 SetPropertyBlock:只有在属性值实际发生变化时才调用 SetPropertyBlock。频繁调用会导致性能问题。

  4. 监听 SkeletonRenderer.OnMeshAndMaterialsUpdated 回调:在材质被替换和分配后,使用 SkeletonRenderer.OnMeshAndMaterialsUpdated 回调来设置材质属性。这样可以确保在正确的时机更新材质属性。

通过这些方法,可以有效地使用 MaterialPropertyBlock 来避免材质不断更新的问题。


这是通过renderer组件设置的 ,我稍微修改了代码 监听 SkeletonRenderer.OnMeshAndMaterialsUpdated 回调去SetPropertyBlock 还是不行 会一直被skeletonrenderer中的代码覆盖 renderer的材质

  • Harald ответили на это сообщение.


    如上视频所示

    当我手动设置MaterialPropertyBlock render组件就会变成这样 究其原因是因为
    SkeletonRenderer.cs 475行 每帧都在重新设置sharedMaterials
    我利用MaterialPropertyBlock在ShaderGUI上制作了一个可以修改运行时参数的功能
    这导致我的shaderGUI一直在刷新 我无法使用我的功能了= =

    • Misaki и Harald ответили на это сообщение.

      tofuTAT Unfortunately, Harald, our Unity master, is currently on leave, so it may take some time for a more detailed answer. By the way, we would appreciate it if you could post your code in text rather than screenshots if at all possible. That way we can test your code as soon as possible.

      I can only give general advice, but as mentioned in the documentation, there is an exmaple scene available that demonstrates the use of MaterialPropertyBlock, so if you haven't already done so, please check it out in detail: Spine Examples/Other Examples/Per Instance Material Properties

      • tofuTAT ответили на это сообщение.
        • Изменено

        Misaki
        demo是正常的 不好意思 我找到了原因 不过大概和SkeletonRenderer有一点点关系 urpInstance开启后 runtime下在shaderGUI上修改参数的时候 unity会自动给一个 material(instance) 这个时候修改material不会直接应用到资产上, 但是spine的renderer会一直去覆盖那个 材质 , 我专门做了一个shaderGui 方便runtime下进行调试

            public class ShaderCustomerEditor:ShaderGUI
            {
                private const string DevelopInstanceKey = "ShaderCustomerEditor_DevelopInstance";
        
                public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
                {
                    // Editor 模式使用基类
                    if (!Application.isPlaying)
                    {  
                        // 在Editor直接使用基类的默认渲染
                        base.OnGUI(materialEditor, properties);
                    }
                    else
                    {
                        // 渲染开发模式开关
                        base.OnGUI(materialEditor, properties);
                        // 获取当前选中的对象
                        Object selectedObject = Selection.activeObject;
                        // 检查选中的对象是否是 GameObject
                        if (selectedObject is GameObject gameObject)
                        {
                            // 获取该 GameObject 的 Renderer
                            Renderer renderer = gameObject.GetComponent<Renderer>();
                            if (renderer==null)
                            {
                                return;
                            }
                            IRenderInstanceHelper instanceHelper = renderer.gameObject.GetComponent<IRenderInstanceHelper>();
                            if (instanceHelper == null)
                            {
                                return;
                            }
                            // 从 EditorPrefs 读取开发模式状态
                            bool developInstance = EditorPrefs.GetBool(DevelopInstanceKey, false);
                            developInstance = EditorGUILayout.Toggle("开发模式", developInstance);
        
                            // 保存状态到 EditorPrefs
                            EditorPrefs.SetBool(DevelopInstanceKey, developInstance);
        
                            if (developInstance)
                            {
                                foreach (var item in properties)
                                {
                                    switch (item.type)
                                    {
                                        case MaterialProperty.PropType.Color:
                                            // 获取颜色属性值
                                            Color colorValue = instanceHelper.GetColor(item.name);
                                            // 在 UI 上显示并允许编辑
                                            Color newColorValue = EditorGUILayout.ColorField(item.displayName, colorValue);
                                            // 只有在值变化时才更新
                                            if (newColorValue != colorValue)
                                            {
                                                instanceHelper.SetColor(item.name, newColorValue);
                                            }
        
                                            break;
                                        case MaterialProperty.PropType.Vector:
                                            // 获取向量属性值
                                            Vector4 vectorValue = instanceHelper.GetVector(item.name);
                                            // 在 UI 上显示并允许编辑
                                            Vector4 newVectorValue =
                                                EditorGUILayout.Vector4Field(item.displayName, vectorValue);
                                            // 只有在值变化时才更新
                                            if (newVectorValue != vectorValue)
                                            {
                                                instanceHelper.SetVector(item.name, newVectorValue);
                                            }
        
                                            break;
        
                                        case MaterialProperty.PropType.Float:
                                            // 获取浮点数属性值
                                            float floatValue = instanceHelper.GetFloat(item.name);
                                            // 在 UI 上显示并允许编辑
                                            float newFloatValue = EditorGUILayout.FloatField(item.displayName, floatValue);
                                            // 只有在值变化时才更新
                                            if (!Mathf.Approximately(newFloatValue, floatValue))
                                            {
                                                instanceHelper.SetFloat(item.name, newFloatValue);
                                            }
                                            break;
                                        case MaterialProperty.PropType.Range:
                                            // 获取范围属性值
                                            float rangeValue = instanceHelper.GetFloat(item.name); // 假设范围也是浮点数
                                            float minRange = item.rangeLimits.x; // 假设这些值已定义
                                            float maxRange = item.rangeLimits.y;
                                            // 在 UI 上显示并允许编辑
                                            float newRangeValue = EditorGUILayout.Slider(item.displayName, rangeValue,
                                                minRange, maxRange);
                                            // 只有在值变化时才更新
                                            if (!Mathf.Approximately(newRangeValue, rangeValue))
                                            {
                                                instanceHelper.SetFloat(item.name, newRangeValue);
                                            }
                                            break;
        
                                        case MaterialProperty.PropType.Texture:
                                            // 获取纹理属性值
                                            Texture textureValue = instanceHelper.GetTexture(item.name);
                                            // 在 UI 上显示并允许编辑
                                            Texture newTextureValue = (Texture)EditorGUILayout.ObjectField(item.displayName,
                                                textureValue, typeof(Texture), false);
                                            // 只有在值变化时才更新
                                            if (newTextureValue != textureValue)
                                            {
                                                instanceHelper.SetTexture(item.name, newTextureValue);
                                            }
                                            break;
                                        default:
                                            EditorGUILayout.LabelField(item.displayName, "未实现的属性类型");
                                            break;
                                    }
                                }
                            }
                        }
                    }
                }
        
            }

        ### 关于MaterialPropertyBlock部分

          public class DefaultRenderInstanceHelper : MonoBehaviour, IRenderInstanceHelper
            {
                private Renderer _renderer;
                private MaterialPropertyBlock[] _propertyBlocks;
        
                private void Awake()
                {
                    _renderer = GetComponent<Renderer>();
                    if (_renderer == null)
                    {
                        Debug.LogError("DefaultRenderInstanceHelper requires a Renderer component.");
                    }
        
                    // 初始化 MaterialPropertyBlock 数组,根据材质数量来决定大小
                    _propertyBlocks = new MaterialPropertyBlock[MaterialCount];
                }
        
                public int MaterialCount => _renderer != null ? _renderer.materials.Length : 0;
        
                private MaterialPropertyBlock GetPropertyBlock(int materialIndex)
                {
                    if (materialIndex < 0 || materialIndex >= MaterialCount)
                    {
                        throw new ArgumentOutOfRangeException(nameof(materialIndex), "Material index is out of range.");
                    }
        
                    // 如果尚未初始化该索引的 MaterialPropertyBlock,则创建一个新的
                    if (_propertyBlocks[materialIndex] == null)
                    {
                        _propertyBlocks[materialIndex] = new MaterialPropertyBlock();
                    }
        
                    // 从 Renderer 中加载现有属性
                    _renderer.GetPropertyBlock(_propertyBlocks[materialIndex], materialIndex);
                    return _propertyBlocks[materialIndex];
                }
        
                /// <summary>
                /// 设置材质的属性块,并应用到 Renderer。
                /// </summary>
                private void ApplyPropertyBlock(int materialIndex)
                {
                    _renderer.SetPropertyBlock(_propertyBlocks[materialIndex], materialIndex);
                }
        
                public void SetColor(string propertyName, Color color, int materialIndex = 0)
                {
                    GetPropertyBlock(materialIndex).SetColor(propertyName, color);
                    ApplyPropertyBlock(materialIndex);
                }
        
                public void SetFloat(string propertyName, float value, int materialIndex = 0)
                {
                    GetPropertyBlock(materialIndex).SetFloat(propertyName, value);
                    ApplyPropertyBlock(materialIndex);
                }
        
                public void SetVector(string propertyName, Vector4 vector, int materialIndex = 0)
                {
                    GetPropertyBlock(materialIndex).SetVector(propertyName, vector);
                    ApplyPropertyBlock(materialIndex);
                }
        
                public void SetTexture(string propertyName, Texture texture, int materialIndex = 0)
                {
                    GetPropertyBlock(materialIndex).SetTexture(propertyName, texture);
                    ApplyPropertyBlock(materialIndex);
                }
        
                public void SetMatrix(string propertyName, Matrix4x4 matrix, int materialIndex = 0)
                {
                    GetPropertyBlock(materialIndex).SetMatrix(propertyName, matrix);
                    ApplyPropertyBlock(materialIndex);
                }
        
                public Color GetColor(string propertyName, int materialIndex = 0) =>
                    GetPropertyBlock(materialIndex).GetColor(propertyName);
        
                public float GetFloat(string propertyName, int materialIndex = 0) =>
                    GetPropertyBlock(materialIndex).GetFloat(propertyName);
        
                public Vector4 GetVector(string propertyName, int materialIndex = 0) =>
                    GetPropertyBlock(materialIndex).GetVector(propertyName);
        
                public Texture GetTexture(string propertyName, int materialIndex = 0) =>
                    GetPropertyBlock(materialIndex).GetTexture(propertyName);
        
                public Matrix4x4 GetMatrix(string propertyName, int materialIndex = 0) =>
                    GetPropertyBlock(materialIndex).GetMatrix(propertyName);
            }

        然后我发现了原因

        gui在获取数据的时候 GetPropertyBlock(materialIndex).GetColor(propertyName);调用了renderer的 renderer.GetPropertyBlock(propertyBlocks[materialIndex], materialIndex);

        这行代码导致 让renderer生成了一个新的材质 material(instance) skeletonRender中又一直覆盖shaderMaterial 无限递归就导致了上面视频的现象

        • Harald ответили на это сообщение.

          Sorry for the late reply.

          tofuTAT 当我手动设置MaterialPropertyBlock render组件就会变成这样 究其原因是因为
          SkeletonRenderer.cs 475行 每帧都在重新设置sharedMaterials

          Please note that SkeletonRenderer.cs line 475 is empty in both spine-unity 4.2 and 4.1. Please always make sure to use the latest spine-unity package when encountering and reporting any problems. Sharing line numbers otherwise does not help.

          tofuTAT 这是通过renderer组件设置的 ,我稍微修改了代码 监听 SkeletonRenderer.OnMeshAndMaterialsUpdated 回调去SetPropertyBlock 还是不行 会一直被skeletonrenderer中的代码覆盖 renderer的材质
          This is set through the renderer component. I slightly modified the code to listen to SkeletonRenderer.OnMeshAndMaterialsUpdated and call back to SetPropertyBlock. It still doesn’t work. The renderer’s material will always be overwritten by the code in skeletonrenderer.

          See the documentation here on why materials are automatically assigned according to visible attachments each frame:
          https://zh.esotericsoftware.com/spine-unity-rendering
          https://esotericsoftware.com/spine-unity-rendering#Rendering

          tofuTAT demo是正常的 不好意思 我找到了原因 不过大概和SkeletonRenderer有一点点关系 urpInstance开启后 runtime下在shaderGUI上修改参数的时候 unity会自动给一个 material(instance) 这个时候修改material不会直接应用到资产上, 但是spine的renderer会一直去覆盖那个 材质 , 我专门做了一个shaderGui 方便runtime下进行调试
          The demo is normal. Sorry, I found the reason, but it probably has something to do with SkeletonRenderer. After urpInstance is turned on, when modifying parameters on the shaderGUI at runtime, unity will automatically give a material (instance). At this time, modifying the material will not be directly applied to the asset.

          Machine translation might be wrong here, unfortunately I don't completely understand the above paragraph. Do you mean that you have found the issue in your code, and that your custom shader GUI is creating the unnecessary instances?

          tofuTAT 然后我发现了原因

          gui在获取数据的时候 GetPropertyBlock(materialIndex).GetColor(propertyName);调用了renderer的 renderer.GetPropertyBlock(propertyBlocks[materialIndex], materialIndex);

          这行代码导致 让renderer生成了一个新的材质 material(instance) skeletonRender中又一直覆盖shaderMaterial 无限递归就导致了上面视频的现象
          Then I found out the reason

          When the gui gets the data, GetPropertyBlock(materialIndex).GetColor(propertyName); calls the renderer's renderer.GetPropertyBlock(propertyBlocks[materialIndex], materialIndex);

          This line of code causes the renderer to generate a new material, material(instance), and skeletonRender always overwrites shaderMaterial. Infinite recursion leads to the phenomenon in the video above.

          Renderer.GetPropertyBlock should never create material instances. Something else must be wrong with your code if you see material instances created. You can experiment with the Spine Examples/Other Examples/Per Instance Material Properties example scene and add the respective calls there. You will see that MaterialPropertyBlock operations don't lead to material instance copies of the material as shown in your video.

          It might be incorrect translation, but I also don't understand the last line regarding "infinite recursion": The video shows instances being created and being overwritten again the next frame by the SkeletonRenderer, see the Renderer documentation section mentioned above why that happens. I can see no infinite recursion in the video, which would lead to stack overflow.

          • tofuTAT ответили на это сообщение.
            18 дней спустя

            Harald thank u
            Apologies for the delayed response as the issue has been resolved. Yes, I am using an older version of Spine (3.8). In the video, the material keeps switching between material and material (instance). This happens because I used GetPropertyBlock in the Shader GUI, which causes the renderer to automatically create a material (instance). Then, the older version of Spine resets the material in LateUpdate, resulting in the behavior seen in the video. (Translation provided by ChatGPT)