The OnMeshAndMaterialsUpdated callback is indeed a combined callback for both mesh and materials, and there isn't a separate callback specifically for just the mesh update in the current API.
However, there's another approach you can try. Instead of using OnMeshAndMaterialsUpdated, you can use the UpdateComplete callback and then access the mesh via the MeshFilter component. The key difference is that UpdateComplete is raised after world transforms are calculated, but you'd need to hook in at the right point.
Since you mentioned that multithreading may be causing the empty mesh issue, the most reliable approach would be to use OnMeshAndMaterialsUpdated but access the mesh through the SkeletonRenderer's mesh directly rather than through the MeshFilter:
void OnMeshUpdated(SkeletonRenderer renderer) {
if (!needsRecook) return;
needsRecook = false;
// Access the mesh directly from the SkeletonRenderer
Mesh mesh = renderer.GetComponent<MeshFilter>().sharedMesh;
if (mesh == null || mesh.vertexCount == 0) return; // guard against empty mesh
meshCollider.sharedMesh = null;
meshCollider.sharedMesh = mesh;
meshCollider.convex = meshCollider.convex;
}
The null/empty vertex count guard is important when multithreading is enabled, as the mesh may not be fully populated yet in some frames.
Regarding separating material and mesh updates — one workaround is to track whether materials changed independently by caching the current materials and comparing them each frame, so you can skip the re-cook when only materials changed:
Material[] cachedMaterials;
void OnMeshUpdated(SkeletonRenderer renderer) {
MeshRenderer meshRenderer = renderer.GetComponent<MeshRenderer>();
Material[] currentMaterials = meshRenderer.sharedMaterials;
bool onlyMaterialChanged = cachedMaterials != null &&
currentMaterials.Length == cachedMaterials.Length;
if (onlyMaterialChanged) {
for (int i = 0; i < currentMaterials.Length; i++) {
if (currentMaterials[i] != cachedMaterials[i]) {
onlyMaterialChanged = false;
break;
}
}
}
cachedMaterials = currentMaterials;
if (onlyMaterialChanged) return; // skip re-cook, only materials changed
Mesh mesh = renderer.GetComponent<MeshFilter>().sharedMesh;
if (mesh == null || mesh.vertexCount == 0) return;
meshCollider.sharedMesh = null;
meshCollider.sharedMesh = mesh;
meshCollider.convex = meshCollider.convex;
}
This gives you an approximation of separating the two concerns without requiring changes to the runtime itself.