Can we please finally get custom mesh data?

If I want to make any solid setup with materials the only way to make a change right now is through vertex colors or maybe UVs. This is extremely limiting.

In Unreal or other engines you can just have custom object data and change a float and cycle through colors, remove unwanted elements, change the gloss, change the tiling, remove triplanar etc without changing the material, switch palettes and so on.

We have custom vertex streams for particle effects and its tremendously useful.

Right now we have to do insanely dirty hacks like change my transform X scale to X.XX1 or X.XX2 to enable certain effects as there are just no means to properly do things in Unity. Making a new material to adjust a small float is an extreme waste.

Not having custom mesh data can mean you need 10-100x more materials to get the same results.

With custom vertex streams in particles we were able to cut down our VFX materials from 1000 to around 15.

Here is the unreal equivalent:
Storing Custom Data in Unreal Engine Materials Per-Primitive | Unreal Engine 5.4 Documentation | Epic Developer Community (epicgames.com)

Please look into this topic, it is very overdue.

Edit: after the long discussion about varied options, none of the options are really optimal and being able to set an additional vector 4 similar to transform rotation would be far superior and create zero issues with batching or material creation and deletion.

Wouldn’t you use mesh.SetVertexBufferParams and mesh.SetVertexBufferData?

1 Like

This has always been possible in Unity?

Using the default vertex format, you can pass entirely custom mesh data in each UV channel (which is a float4 per-vertex), and you have 8 of these. That’s 32 floats per vertex ready to be used any way you like. I’ve used them for changing color palettes, passing custom SH coefficients for lighting, barycentric coordinates for wireframe rendering, and tons other stuff.

If you don’t want to stick to the default vertex format, you have another API (see Trindenberg’s answer) that allows you to define your own entirely custom vertex formats and have multiple vertex streams. I’ve used it in the past for creating a format that has octahedral encoded normals in a single float, RGBA colors packed in 2 halfs, and quantized vertices in a separate stream with great success.

If you want to go even further you can pass entirely custom vertex structs to shaders (using ComputeBuffers) and fill these from a compute buffer without ever setting foot on the CPU.

1 Like

By the way the Unreal link you shared is about custom primitive data, that is custom object data, not custom mesh data. I think you’re mixing up vertex streams, vertex formats, and per-object/instance data. They all allow you to pass custom data per object or even per vertex, but they’re completely different stuff.

Unity has also had custom per object data since 2015 (v5.2 if I recall correctly), called MaterialPropertyBlocks. They allow you to have many objects sharing the exact same material, but pass custom data (floats, vectors, matrices, textures, you name it) to each one. So if you have 300 objects with the same material and only their colors and specular change, you don’t need to have 300 individual materials which would be very really wasteful and a chore to set up.

Hey, thanks for the replies

I am aware of UVs holding vector 4 which is usable but how do you define the UV information? The entire point is to be able to do it in editor and see the results in real time by changing a float. If you have to encode UV data into the mesh or have to do it offline in a 3D software this defeats the entire purpose. In a shader or unreal object data you can change a float and see changes update in editor instantly. Also adding an additional UV set just for setting a single float value is very inefficient

mesh.SetVertexBufferParams is also offline and baked into the mesh as far as I see

Materialpropertyblocks seems like its very close but then its not supported with the SRP batcher, ill investigate on that, thanks! Odd how I never found it on search, maybe the name is too far fetched

How would it be possible to define UV data in the editor? that requires having a full-fledged modeling tool inside Unity, to be able to select vertices and set their data or be able to paint them. It’s just easier to do this in a proper modeling tool.

If you mean defining per-mesh attributes, just access the renderer’s material (not sharedMaterial, as that would modify the original material shared by all objects using it): any changes you make to it will apply to that specific renderer only. If you want to expose this in the editor, it’s a one-liner:

public MyCustomPerObjectColor: MonoBehaviour
{
public Color myColor;

  Update()
  {
      GetComponent<Renderer>().material.color = myColor;
  }
}

This example uses a color, but you can do the same with floats, matrices, textures, vectors, whatever.

I still think you’re confusing per-vertex/vertex formats and per-mesh/instance data. The link you shared about Unreal is doing this on a per-object/primitive basis, which as pointed above is trivial to do in Unity.

It’s whatever you make of it, I’ve made custom tools to edit/create/pass custom per vertex data to procedurally generated meshes with it. The whole point of this API is to set this data at runtime, if it was offline it would be pretty useless.

Not supported with SRP batcher because the SRP batcher already batches together similar materials (or rather, makes changing material attributes while using the same shader much more performant), making it a non-issue and rendering MaterialPropertyBlock obsolete: they’re different solutions to the same problem.

To add to this, you also have Material variants, which allow you to make modifications that are “layered” on top of a base material. So if you ever change the base material, all its variants change too. You can use this to make for instance weathered versions of a metal material.

To wrap up, you have (from more fine-grained to less fine-grained):

  • Custom vertex formats.
  • Custom per-vertex data.
  • Custom per-material data.
  • Material variants

You can use any of the above (or combine them) depending on what your use case is.

Yes thats what I meant. You cannot use UVs as flexible data container like this as it has to be done offline (or runtime) and as I said its inefficient to use per vertex data when you only need per object data.

Material variants are slightly better than creating a copy but in the end you still have 50 materials instead of 1 material.

Are you sure about .material? IIRC we had to always create instances in runtime for this to work and that also wouldnt work in editor right? For art assets its important that you see the change in editor but its interesting that there is this distinction.

Ill give the property blocks a try, but it is paramount that you see the change in editor, not just in runtime. Having your assets look incorrect in the development stage is really not ideal, so it cannot be offline nor runtime only.

Yes I’m 100% sure: having per-renderer instances of a material is its intended use case. It’s a basic Unity thing, the same pattern exists in many places throughout the engine: for example meshes (renderer.mesh and renderer.sharedMesh), collision materials (collider.material and collider.sharedMaterial) or collider meshes. These allow to access data shared by many objects, or a instance unique to a specific object.

Well yes, .material internally creates an instance for you. That’s what instances are for: you can’t have multiple independent copies of some data if you don’t store them somewhere.

First call to .material, Unity creates an instance of the sharedMaterial in case none existed for that renderer yet. Subsequent calls return the already existing instance. This is a convenience property to save you some typing, as internally it simply does:

if (material == null)
    material = Instantiate(sharedMaterial);
return material;

Keep in mind that having many instances of a material typically has a performance impact in the built-in render pipeline. This is what MaterialPropertyBlock in built-in and later the SRP batcher were designed to solve.

Most of Unity’s API works both in the actual game and in-editor. Any script you write can be made to also work in-editor by adding the [ExecuteAlways] attribute to it, this is also basic stuff that gets used very often. You can also write editor-only scripts, or even conditionally compile stuff when in editor (#if UNITY_EDITOR).

When applying this to the .material property only thing you need to be mindful about is managing the lifetime of the material instance yourself (to avoid leaking memory) as the manual explains:

Annoyingly enough even if you do this, the editor will still log a warning to the console telling you that you should consider using .sharedMaterial in the editor instead to avoid leaking memory. It is safe to ignore this, but if you want to get rid of it you can simply create the material instance yourself.

Here’s an example of a script that allows you to have per-object colors using a single material both in the game and in-editor, I’m creating the material instance myself instead of using .material so that the console stays clutter-free. Disabling the component destroys the material instance and reverts the object to its original material. Same script can be used for things other than the main material color, using SetFloat, SetVector, SetTexture, etc.

Note this should only be used with SRPs as the SRP batcher will take care of performance automatically. For the built-in pipeline it would be wiser to use MaterialPropertyBlocks.

using UnityEngine;

[ExecuteAlways]
[RequireComponent(typeof(Renderer))]
public class PerObjectMaterialInstance : MonoBehaviour
{
    public Color myColor = Color.white;

    private Renderer rend;
    private Material sharedMaterial;

    void OnEnable()
    {
        // get a reference to this object's renderer:
        rend = GetComponent<Renderer>();

        // store a reference to its original material and create an instance of it:
        sharedMaterial = rend.sharedMaterial;
        rend.sharedMaterial = Instantiate(sharedMaterial);
    }

    void OnDisable()
    {
        // destroy the instanced material and revert to the original one:
        DestroyImmediate(rend.sharedMaterial);
        rend.sharedMaterial = sharedMaterial;
    }

    private void Update()
    {
        // change whatever unique material properties we want:
        if (rend.sharedMaterial != null)
            rend.sharedMaterial.color = myColor;
    }
}
1 Like

Thanks a lot for the explainations and the example. Ill definitely give it a try.

However in the end, we are back at the start and that there is no ideal solution that compares to just being able to set primitive data in a simple way which can be read out easily through shaders similar to how you can do it in unreal or in a 3D software.

If you just had even one or two extra vector 4s similar to the object position or rotation, there would be zero issues with draw calls, material creation, batching and everything else. In unreal, the object data is used to reduce draw calls and materials, here we would increase it (although save on project bloat at least)

Still don’t understand the problem. You can store any per-object data in a component and pass it to a material instance unique to that object - as just did in my previous post, then read it in shaders by name. This works both in editor and at runtime, it is trivial to do and performant.

In what way is Unreal’s solution better than this? As far as I can tell it is the exact same thing, except that they have a built-in UI that lets you add properties and set their type/name without scripting (UI that you can replicate in Unity using your own inspector if you want/need).

Imho there’s no obvious performance issues with the above solution: all objects using the same shader variant will be drawn with minimal state changes in between them, since the SRP batcher caches material properties in the GPU using constant buffers. There’s also no extra material assets created -since instances are only stored in memory- so you only author one material, then set per-object properties when needed.

Quoting Unity on SRP batcher vs MaterialPropertyBlock performance:

Also not sure why you mentioned in your original post you needed to

Object position/rotation/scale are not different in any way to any custom property you create, they are still passed to the shader as a matrix uniform (no GPU instancing by default) so it’s not like you’re solving a performance problem this way. What problem are you trying to solve then? is it meant to work around a functionality limitation or usability problem? because it certainly sounds like an extremely awkward and inconvenient workflow…

Im probably not fully following but as far as I understood does the new instance create a new draw call and swap
as I said, I will try it, it sounds fine and usable for my case but creating instance on demand is technically not as nice as just a changing an innate parameter of the object just like the transform.

Also as far as I researched, it is not possible to import custom data with the FBX format, so external saved data has to be vertex based (UV or vertex color) and custom object data cannot be imported in unity

Maybe we are talking along each other.
But yes I will definitely try your approach and create a material instance and pass it the parameters.

I am of course not on your level on these topics and not a graphics programmer but Im a very experienced artist, can programm and know quite a lot about low level graphics topics but even for me this is all quite obscure. This should be a very straight forward solution that does not require days of discussion to find a solid solution in general. I might get to handle this with a little bit of editor scripting but I will be the 0.1%, while in unreal many people use this as a typical workflow.

Unity in general is not setting many good precedents for solid workflows, be it structure, coding, UI, level creating or most other things. So I guess In reality I am asking for a solid precedent for a good official workflow from unity.

Yes of course they create a new drawcall. It is simply impossible for a GPU to render different meshes as part of a single drawcall if that means switching to different vertex buffers. So the only solution is to statically batch meshes (merge them into a single uber-mesh), and in that case you’re paying extra memory to store large vertex buffers for the merged mesh.

If all objects were to have the exact same mesh and material (despite each one having its own shader parameters), then the most efficient approach is to use GPU instancing to draw them with per-instance data in a single draw call. This is what you’d typically use for drawing say, an asteroid field or terrain grass.

However there’s drawcalls, and there’s drawcalls**.** What I mean by this is that the cost of a draw call is largely determined by the amount of data that needs to be sent from the CPU to the GPU prior to actually drawing stuff.
If you just create one material instance per object, not using MaterialPropertyBlocks and not using SRP batcher, then every time you switch to a new material all material parameters have to be sent to the GPU. What the SRP batcher does isn’t reducing the amount of draw calls, instead it makes each drawcall significantly cheaper as it caches material data on the GPU and only copies it from the CPU when it is modified, as opposed to sending it every single time the material is about to be used.

Even if you want to get fancy and create your own instance in-editor like I did, it’s like 2 lines of code to create the instance, and another 2 to revert to the original material. Shouldn’t be much of a problem.

Having this as built-in values in the transform’s UI would still require to create a material instance internally, so the only thing missing here is the UI and you can easily write one to mimic Unreal’s if that’s what you’re used to.

The typical workflow in Unity is to just call .material and set any per-object attributes you need to. If you look for “unity change object color” in Google this is the solution you’ll find pretty much everywhere, everyone has been using it for ages and it generalizes well beyond just material colors, so I highly doubt you’d be the 0.1%.

Went ahead and extended the above component with a custom UI that is similar to Unreal’s. Also added support for integer, float, vector, matrix, color and texture per-object properties, and optional support for MaterialPropertyBlocks instead of material instances.

To use this together with ShaderGraph, just declare a property in the blackboard which has the same name as in the ObjectMaterialProperties component (eg. if you named the property “_MyCustomProp”, make sure the name is the same in both ShaderGraph’s blackboard and the object component).

Hope you find this useful!

using UnityEngine;
using System.Collections.Generic;

#if UNITY_EDITOR
using UnityEditor;

[CustomPropertyDrawer(typeof(ObjectMaterialProperties.Property))]
public class MaterialPropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        position.height = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
        property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, label);
        if (property.isExpanded)
        {
            position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            EditorGUI.PropertyField(position, property.FindPropertyRelative("name"));

            var type = property.FindPropertyRelative("type");
            position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            EditorGUI.PropertyField(position, type);

            position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            switch ((ObjectMaterialProperties.Property.PType)type.enumValueIndex)
            {
                case ObjectMaterialProperties.Property.PType.Integer:
                    EditorGUI.PropertyField(position, property.FindPropertyRelative("intValue")); break;
                case ObjectMaterialProperties.Property.PType.Float:
                    EditorGUI.PropertyField(position, property.FindPropertyRelative("floatValue")); break;
                case ObjectMaterialProperties.Property.PType.Vector:
                    EditorGUI.PropertyField(position, property.FindPropertyRelative("vectorValue")); break;
                case ObjectMaterialProperties.Property.PType.Color:
                    EditorGUI.PropertyField(position, property.FindPropertyRelative("colorValue")); break;
                case ObjectMaterialProperties.Property.PType.Matrix:
                    EditorGUI.PropertyField(position, property.FindPropertyRelative("matrixValue")); break;
                case ObjectMaterialProperties.Property.PType.Texture2D:
                    EditorGUI.PropertyField(position, property.FindPropertyRelative("textureValue")); break;
            }
        }
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        if (property.isExpanded)
        {
            return base.GetPropertyHeight(property, label) + (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing)*3;
        }
        else
        {
            return base.GetPropertyHeight(property, label);
        }
    }
}
#endif

[ExecuteAlways]
[RequireComponent(typeof(Renderer))]
public class ObjectMaterialProperties : MonoBehaviour
{
    [System.Serializable]
    public class Property
    {
        public enum PType
        {
            Integer,
            Float,
            Vector,
            Color,
            Matrix,
            Texture2D
        };

        public string name;
        public PType type;

        public int intValue;
        public float floatValue;
        public Vector4 vectorValue;
        public Color colorValue;
        public Matrix4x4 matrixValue;
        public Texture2D textureValue;
    }

    [SerializeField] private bool _useMaterialPropertyBlock;
    public bool useMaterialPropertyBlock
    {
        get { return _useMaterialPropertyBlock; }
        set {
            if (_useMaterialPropertyBlock != value)
            {
                _useMaterialPropertyBlock = value;
                UpdateMaterialInstance();
            }
        }
    }

    public List<Property> properties = new List<Property>();

    private Renderer rend;
    private Material sharedMaterial;
    private MaterialPropertyBlock mpb;

    void OnEnable()
    {
        // get a reference to this object's renderer:
        rend = GetComponent<Renderer>();

        // create a material property block:
        mpb = new MaterialPropertyBlock();

        if (!useMaterialPropertyBlock)
            SwitchToMaterialInstance();
    }

    void OnDisable()
    {
        RevertToSharedMaterial();
    }

    private void OnValidate()
    {
        UpdateMaterialInstance();
    }

    private void UpdateMaterialInstance()
    {
        if (!_useMaterialPropertyBlock)
            SwitchToMaterialInstance();
        else
            RevertToSharedMaterial();
    }

    private void SwitchToMaterialInstance()
    {
        // store a reference to its original material and create an instance of it:
        if (sharedMaterial == null)
        {
            sharedMaterial = rend.sharedMaterial;
            rend.sharedMaterial = Instantiate(sharedMaterial);
        }
    }

    private void RevertToSharedMaterial()
    {
        // destroy the instanced material and set the original one:
        if (sharedMaterial != null && rend.sharedMaterial != null)
        {
            DestroyImmediate(rend.sharedMaterial);
            rend.sharedMaterial = sharedMaterial;
            sharedMaterial = null;
        }
    }

    private void Update()
    {
        // change whatever unique material properties we want:
        if (useMaterialPropertyBlock)
        {
            foreach (var prop in properties)
            {
                if (prop.name != string.Empty)
                {
                    switch (prop.type)
                    {
                        case Property.PType.Integer: mpb.SetInteger(prop.name, prop.intValue); break;
                        case Property.PType.Float: mpb.SetFloat(prop.name, prop.floatValue); break;
                        case Property.PType.Vector: mpb.SetVector(prop.name, prop.vectorValue); break;
                        case Property.PType.Color: mpb.SetColor(prop.name, prop.colorValue); break;
                        case Property.PType.Matrix: mpb.SetMatrix(prop.name, prop.matrixValue); break;
                        case Property.PType.Texture2D: mpb.SetTexture(prop.name, prop.textureValue); break;
                    }
                }
            }
            rend.SetPropertyBlock(mpb);
        }
        else if (rend.sharedMaterial != null)
        {
            foreach (var prop in properties)
            {
                if (prop.name != string.Empty)
                {
                    switch (prop.type)
                    {
                        case Property.PType.Integer: rend.sharedMaterial.SetInteger(prop.name, prop.intValue); break;
                        case Property.PType.Float: rend.sharedMaterial.SetFloat(prop.name, prop.floatValue); break;
                        case Property.PType.Vector: rend.sharedMaterial.SetVector(prop.name, prop.vectorValue); break;
                        case Property.PType.Color: rend.sharedMaterial.SetColor(prop.name, prop.colorValue); break;
                        case Property.PType.Matrix: rend.sharedMaterial.SetMatrix(prop.name, prop.matrixValue); break;
                        case Property.PType.Texture2D: rend.sharedMaterial.SetTexture(prop.name, prop.textureValue); break;
                    }
                }
            }
        }
    }
}
5 Likes

Hey, Thanks!
Havent looked in detail yet but that looks exactly like the thing. Will check it out soon!

Unreal suffers a litte from having no default properties so you have to add them from fresh every time IIRC,
in the script I think I can add that easily.

To the previous comment, yes Ive been using material set float etc quite extensively in runtime code but creating a editor instance didnt occur to me as possible or good idea but I guess it is

9859740--1420248--d2bbcf91e04b82159114ae7d4059ead016b248e7.gif

(This here does however work by referencing materials into a palette script, so a different use case)