Hi I have a script that adds a material to all mesh renderers in an object:
foreach (var renderer in renderers)
{
// Append outline shaders
var materials = renderer.sharedMaterials.ToList();
materials.Add(outlineFillMaterial);
renderer.materials = materials.ToArray();
}
This works if each gameobject has only one material on its mesh renderer. However if there are submeshes with different material slots it will only overlay on the last material slot/submesh. Is there any way to add this material and assign it to every submesh on the renderer (keeping their current materials too)? I saw some old threads that seemed to make the case that you would need to basically separate the mesh into individual gameobjects but I don’t that would be very efficient resource-wise and I’m unsure of how to do that to begin with.
Nope. It’s a long time bug / limitation that Unity hasn’t bothered to fix for several years now.
If you need the materials to properly respect the material queue and sorting, the only option is to use a second mesh.
If you just need to make sure it renders before the frame is done, I use commandBuffer.DrawRenderer().
Just know that for objects with multiple materials you will have to add a DrawRenderer() call for each sub mesh. Each material will be a different submesh, though an object may have multiple submeshes for other reasons as well, such as a high triangle count, or high bone count, both of which Unity has limits to how many a single “mesh” can have, and will internally break up a single imported and presented mesh into multiple submeshes if those limits are met. It also will return the wrong number of submeshes for any mesh that’s batched, as it’ll show the number of submeshes for the batched mesh instead of the original mesh. You can work around that problem by storing the submesh count in the component at edit time.
The code I use looks something like this:
// loop over List<Renderer> renderers
int[] subMeshCount = new int[renderers.Count];
for (int i=0; i<renderers.Count; i++)
{
Mesh mesh = null;
if (r is SkinnedMeshRenderer)
mesh = (r as SkinnedMeshRenderer).sharedMesh;
else if (r is MeshRenderer)
mesh = r.GetComponent<MeshFilter>().sharedMesh;
Debug.Assert(mesh != null, "MeshHighlight's renderer [" + i + "] is missing a valid mesh.", gameObject);
if (mesh != null)
{
if (renderers[i].isPartOfStaticBatch)
subMeshCount[i] = 1; // hack hack hack
else
subMeshCount[i] = mesh.subMeshCount;
}
}
It could support other renderer components as well with a bit more work (though most other renderers don’t support more than one submesh, or don’t support accessing how many submeshes there are), but I don’t need that so I don’t.
Do I need to DrawRenderer() every frame that I want the effect active on? I tried setting something up where it’s only called once and it doesn’t seem to show up. Also which event timing should I use if this is just rendering on top of the other materials for the object (and is semi-transparent). BeforeForwardAlpha? (that’s what I’m using rn)
Technically, yes, DrawRenderer() needs to be called every frame. However that doesn’t mean you need to call the function every frame. That’s the nice thing with command buffers is you can set them up once, assign them to the camera or light, and mostly forget about them as they’ll get automatically called during the event you attached it to until you remove it.
And CameraEvent.BeforeForwardAlpha is a perfectly acceptable event to use. That’s guaranteed to run regardless of the (built in) rendering path you’ve chosen. If nothing is showing up, you might try the frame debugger to make sure it’s actually issuing a draw call when it’s supposed to. But if it was working before when it was being added as an additional material, there shouldn’t be anything else you need to do to get it to work, assuming you’re doing everything correctly from c#.
if (renderer is SkinnedMeshRenderer skin)
{
var mesh = skin.sharedMesh;
var meshSubMeshCount = mesh.subMeshCount;
if (meshSubMeshCount > 1)
{
var descArray = new SubMeshDescriptor[meshSubMeshCount + 1];
for (int i = 0; i < meshSubMeshCount; i++)
{
descArray[i] = mesh.GetSubMesh(i);
}
var lastMesh = descArray[meshSubMeshCount - 1];
descArray[meshSubMeshCount] =
new SubMeshDescriptor(0, lastMesh.indexStart + lastMesh.indexCount);
mesh.SetSubMeshes(descArray);
}
}
this code add a submesh(which contains all vertex), so your material can append on all mesh