Quick answer: Mesh.CombineMeshes preserves UVs only when every source has matching UV channels. A single source with an empty uv array produces a combined mesh with null/empty UVs. Validate source meshes before combining; populate missing channels with default coordinates if needed.

Here is how to fix Unity CombineMeshes producing a single combined mesh that renders solid white or with wildly wrong textures. The combine call succeeds. Triangle and vertex counts look right. UVs are absent or scrambled. The cause is that the combine API takes the union of input meshes, but UV channels need to be present on every input or the entire output channel is dropped.

The Symptom

You combine 50 prop meshes into one to reduce draw calls. The combined mesh renders, but every triangle samples the wrong texture coordinates. Either the surface is completely uniform color (UVs all zero) or textures slide and warp across the surface.

What Causes This

One source missing UVs. Procedurally generated meshes often have only positions and triangles. CombineMeshes drops the UV channel for the entire output if any input is missing it.

Different UV channels in use. If some inputs have only uv0 and others have uv0+uv2, the output may keep uv0 but lose uv2 (or vice versa).

Different atlas regions. Each input mesh was authored against a specific atlas region. After combine, all inputs share the same UV space and reference whatever atlas the renderer’s material points at — sampling the wrong region.

mergeSubMeshes mismatch. If you combine with mergeSubMeshes = true but inputs have different materials, the merge collapses them into one submesh. Material assignment is lost.

The Fix

Step 1: Validate source UVs before combining.

using System.Linq;
using UnityEngine;

public class MeshCombiner : MonoBehaviour
{
    [SerializeField] private MeshFilter[] sources;

    public void Combine()
    {
        foreach (var mf in sources)
        {
            var m = mf.sharedMesh;
            if (m.uv == null || m.uv.Length != m.vertexCount)
            {
                // Fill with zeros so combine preserves the channel
                m.uv = new Vector2[m.vertexCount];
            }
        }

        var combines = sources.Select(mf => new CombineInstance {
            mesh = mf.sharedMesh,
            transform = mf.transform.localToWorldMatrix
        }).ToArray();

        var combined = new Mesh();
        combined.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        combined.CombineMeshes(combines, mergeSubMeshes: true, useMatrices: true);

        GetComponent<MeshFilter>().sharedMesh = combined;
    }
}

Step 2: Preserve light map UVs. Lightmap UVs are in mesh.uv2. Validate them too:

if (m.uv2 == null || m.uv2.Length != m.vertexCount)
    m.uv2 = new Vector2[m.vertexCount];

For correct lightmapping, generate proper secondary UVs at import time via Mesh Import Settings → Generate Lightmap UVs rather than zero-filling.

Step 3: Remap UVs to atlas regions. If your combined mesh uses an atlas, you need to remap each source’s UVs to the correct atlas tile before combine:

void RemapToAtlas(Mesh m, Rect atlasRect)
{
    Vector2[] uv = m.uv;
    for (int i = 0; i < uv.Length; i++)
    {
        uv[i] = new Vector2(
            atlasRect.x + uv[i].x * atlasRect.width,
            atlasRect.y + uv[i].y * atlasRect.height);
    }
    m.uv = uv;
}

Step 4: Use mergeSubMeshes wisely. If sources have different materials, set mergeSubMeshes = false to preserve submeshes (one per material):

combined.CombineMeshes(combines, mergeSubMeshes: false);
// Then assign matching materials in order
GetComponent<MeshRenderer>().sharedMaterials = sources.Select(s => s.GetComponent<MeshRenderer>().sharedMaterial).ToArray();

Step 5: Set UInt32 index format for large combined meshes. Default UInt16 index format caps at 65,535 vertices. Combining beyond that silently truncates. Set combined.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32.

Diagnostic Workflow

After combine, inspect the result with the Unity Mesh inspector or a debug log:

Debug.Log($"Combined: vertices={combined.vertexCount}, uvs={combined.uv?.Length ?? 0}, uv2={combined.uv2?.Length ?? 0}");

If uvs is 0 or much less than vertexCount, your sources are missing UVs. If lightmapping does not work, uv2 is the culprit.

“CombineMeshes is union of channels. Missing on one source = missing on output. Validate before combining.”

Related Issues

For ProBuilder UV stretching, see ProBuilder UV Stretching. For sprite atlas issues, see Sprite Atlas Variant Resolution.

Validate UVs on every source. Atlas remap before combine. UInt32 indices for large merges.