agentleFS
IceMasterTIceMasterT/Unity-6.3_SKILLS/unity-procedural-mesh/SKILL.md

unity-procedural-mesh

Skill0 starsChanged 3 months agoMITView on GitHub

Use when generating or modifying mesh geometry from code — the Mesh class, vertex/triangle/UV buffers, or the Mesh.MeshData advanced mesh API. Grounds answers in the local Unity 6.3 docs over pretrained knowledge.

The file

Files
SKILL.md

Unity Procedural Mesh Generation

Retrieval Sources

SourcePathUse for
Meshes landing & terminologyManual/mesh.html, Manual/get-started-with-meshes.html, Manual/mesh-introduction.htmlEntry points into the whole mesh manual tree; defines mesh/vertex/index/topology vocabulary used everywhere else
Select a mesh assetManual/mesh-select-mesh-asset.htmlHow to open the Mesh Inspector on a GameObject's mesh asset to eyeball generated data
Mesh data landing pageManual/AnatomyofaMesh.htmlIndex of the four mesh-data sub-pages (vertex/topology/index/deformable) below
Mesh vertex dataManual/mesh-vertex-data.htmlPer-vertex attributes (position, normal, tangent, color, up to 8 UV channels, bone weights) and which Mesh.Get*/Set* pair backs each one
Mesh topology dataManual/mesh-topology-data.htmlMeshTopology enum (Triangle/Quad/Lines/LineStrip/Points) and how topology determines index-group size
Mesh index dataManual/mesh-index-data.htmlIndex buffer semantics, shared vs. duplicated vertices, and Unity's clockwise winding-order rule for front-facing triangles
Mesh data for deformable meshesManual/mesh-data-deformable-meshes.htmlBlend shape vertex deltas and bind poses — the two deformable-mesh-only data types
View mesh data visualizationsManual/view-mesh-data-visualizations.htmlMesh Preview window modes (Wireframe, UV Checker, Normals, Tangents, Blendshapes) for debugging generated geometry visually
Creating/accessing meshes via script (landing)Manual/creating-meshes.html, Manual/create-mesh.htmlOverview of the three ways to get a mesh into a project (import, in-Editor tools, code) and links into the scripting sub-pages
Access meshes via the Mesh APIManual/UsingtheMeshClass.htmlCore scripting-workflow page: MeshFilter access, vertex/triangle array ordering rules, pointer to the advanced buffer API
Create a quad mesh via script (tutorial)Manual/Example-CreatingaBillboardPlane.htmlWorked step-by-step example building a simple quad mesh from script
Combine meshes manuallyManual/combining-meshes.htmlWhen/why to use Mesh.CombineMeshes as a draw-call optimization vs. static batching
Mesh asset Inspector referenceManual/class-Mesh.htmlMesh Inspector fields (Vertices/Indices/Skin/Other sections) — useful for interpreting a generated mesh's stats
Mesh Filter component referenceManual/class-MeshFilter.htmlMeshFilter.mesh vs MeshFilter.sharedMesh and how a mesh asset reaches the renderer
Mesh Renderer component referenceManual/class-MeshRenderer.htmlCompanion renderer component that draws whatever MeshFilter references
Skinned Mesh Renderer referenceManual/class-SkinnedMeshRenderer.html, Manual/troubleshooting-skinned-mesh-renderer-visibility.htmlRenderer used for deformable/skinned procedural meshes instead of MeshFilter+MeshRenderer
Mesh compressionManual/compressing-mesh-data-optimization.html, Manual/types-of-mesh-data-compression.html, Manual/configure-mesh-compression.htmlVertex Compression (FP32→FP16 per-channel) vs. Mesh Compression (on-disk, decompressed at load) — both apply to imported/authored mesh assets, including ones you save from generated data
Mesh Collider & cookingManual/mesh-colliders.html, Manual/mesh-colliders-introduction.html, Manual/prepare-mesh-for-mesh-collider.html, Manual/physics-optimization-cpu-mesh-cooking-options.html, Manual/class-MeshCollider.htmlCost of cooking a procedurally generated mesh for physics use — full collider/physics detail lives in unity-physics, cited here only for the mesh-generation-time cost tradeoff
Mesh components reference (index)Manual/mesh-components-reference.htmlManual index page linking MeshFilter/MeshRenderer/MeshCollider/SkinnedMeshRenderer references
Mesh class scripting referenceScriptReference/Mesh.htmlThe full member list, the "simple vs advanced API" distinction, and three canonical usage patterns (build-from-scratch, per-frame vertex animation, clear-and-rebuild) with worked examples
Mesh constructor & ClearScriptReference/Mesh-ctor.html, ScriptReference/Mesh.Clear.htmlCreating an empty Mesh instance; wiping vertex/index data before a full rebuild to avoid stale out-of-bounds indices
Vertices (simple API)ScriptReference/Mesh-vertices.html, ScriptReference/Mesh.SetVertices.html, ScriptReference/Mesh.GetVertices.htmlLegacy array property vs. the List<Vector3>-friendly Set/GetVertices overloads
Triangles / indices (simple API)ScriptReference/Mesh-triangles.html, ScriptReference/Mesh.SetTriangles.html, ScriptReference/Mesh.SetIndices.htmlPer-submesh triangle assignment, baseVertex for exceeding 65535 vertices under 16-bit indices, the SetIndices(topology, submesh) overload for non-triangle topologies
Normals (simple API)ScriptReference/Mesh-normals.html, ScriptReference/Mesh.SetNormals.html, ScriptReference/Mesh.RecalculateNormals.htmlManually supplying normals vs. auto-computing them from triangle/vertex data; smoothing behavior across shared vertices
Tangents (simple API)ScriptReference/Mesh-tangents.html, ScriptReference/Mesh.SetTangents.html, ScriptReference/Mesh.RecalculateTangents.html4-component tangent format (xyz + w-sign for binormal), dependency on normals/UV0 being present first
UVs (simple API)ScriptReference/Mesh-uv.html, ScriptReference/Mesh.SetUVs.html, ScriptReference/Mesh.GetUVs.htmlUp to 8 UV channels (uv..uv8), Vector2/Vector3/Vector4 UV data, TEXCOORD0-7 shader semantic mapping
Vertex colors (simple API)ScriptReference/Mesh-colors.html, ScriptReference/Mesh-colors32.html, ScriptReference/Mesh.SetColors.htmlFloat Color vs. byte-packed Color32 vertex color storage
BoundsScriptReference/Mesh-bounds.html, ScriptReference/Mesh.RecalculateBounds.htmlBounding volume used for culling; must be recalculated manually after buffer-API vertex writes
Submeshes (simple API)ScriptReference/Mesh-subMeshCount.html, ScriptReference/Mesh.SetSubMeshes.html, ScriptReference/Mesh.GetSubMesh.html, ScriptReference/Rendering.SubMeshDescriptor.htmlOne submesh per material; SubMeshDescriptor fields (indexStart/indexCount/topology/baseVertex/bounds)
MeshTopology enumScriptReference/MeshTopology.htmlTriangles/Quads/Lines/LineStrip/Points values, with a two-submesh mixed-topology code example
isReadable / CPU-side mesh dataScriptReference/Mesh-isReadable.htmlWhen a generated mesh must stay CPU-readable (combining at runtime, NavMesh baking, Mesh Collider with negative-scale/skew) vs. when to free the CPU copy
MarkDynamic / UploadMeshData / MarkModifiedScriptReference/Mesh.MarkDynamic.html, ScriptReference/Mesh.UploadMeshData.html, ScriptReference/Mesh.MarkModified.htmlGPU buffer strategy hint for frequently-updated meshes; forcing an immediate upload; notifying renderers of an out-of-band change
Index formatScriptReference/Mesh-indexFormat.html, ScriptReference/Rendering.IndexFormat.html16-bit (65535 vertex cap) vs. 32-bit index buffers, and the platform-support caveat for 32-bit on some GPUs
Mesh.AllocateWritableMeshDataScriptReference/Mesh.AllocateWritableMeshData.htmlEntry point to the advanced MeshData API: allocate one or more writable MeshData structs for job/Burst-friendly mesh construction, with a full tetrahedron-mesh worked example
Mesh.ApplyAndDisposeWritableMeshDataScriptReference/Mesh.ApplyAndDisposeWritableMeshData.htmlCommit populated MeshData structs to real Mesh objects and dispose the array; validation caveats around submesh index ranges
Mesh.AcquireReadOnlyMeshData / MeshUtility variantScriptReference/Mesh.AcquireReadOnlyMeshData.html, ScriptReference/MeshUtility.AcquireReadOnlyMeshData.htmlJob-safe read-only snapshot of existing mesh data; MeshUtility variant skips the isReadable check for Editor-only tooling
Mesh.MeshDataArray / Mesh.MeshData structsScriptReference/Mesh.MeshDataArray.html, ScriptReference/Mesh.MeshData.htmlContainer and per-mesh struct types underlying the advanced API; disposal rules
MeshData buffer setupScriptReference/Mesh.MeshData.SetVertexBufferParams.html, ScriptReference/Mesh.MeshData.GetVertexData.html, ScriptReference/Mesh.MeshData.SetIndexBufferParams.html, ScriptReference/Mesh.MeshData.GetIndexData.html, ScriptReference/Mesh.MeshData.SetSubMesh.htmlThe MeshData-struct equivalents of the Mesh-level advanced methods, callable from worker threads/Jobs
Mesh advanced buffer APIScriptReference/Mesh.SetVertexBufferParams.html, ScriptReference/Mesh.SetVertexBufferData.html, ScriptReference/Mesh.SetIndexBufferParams.html, ScriptReference/Mesh.SetIndexBufferData.html, ScriptReference/Mesh.SetSubMesh.htmlDirect raw-buffer writes on the main-thread Mesh object (no Jobs required); minimal validation, maximum performance
VertexAttributeDescriptor / attribute enumsScriptReference/Rendering.VertexAttributeDescriptor.html, ScriptReference/Rendering.VertexAttribute.html, ScriptReference/Rendering.VertexAttributeFormat.htmlDeclaring a custom vertex layout (which attributes, what data type/dimension, which stream) for the advanced API
MeshUpdateFlagsScriptReference/Rendering.MeshUpdateFlags.htmlPer-call flags to skip index validation, bounds recalculation, bone-bounds reset, or renderer notification when using the advanced API
Query a mesh's vertex attribute layoutScriptReference/Mesh.GetVertexAttribute.html, ScriptReference/Mesh.GetVertexAttributes.html, ScriptReference/Mesh.HasVertexAttribute.htmlInspecting what attributes/format/stream an existing (possibly imported) mesh actually uses before writing compatible code against it
Combining meshes (scripting)ScriptReference/Mesh.CombineMeshes.html, ScriptReference/CombineInstance.htmlmergeSubMeshes/useMatrices/hasLightmapData parameters and a full worked "combine all children" example
Blend shapes (scripting)ScriptReference/Mesh.AddBlendShapeFrame.html, ScriptReference/Mesh.ClearBlendShapes.html, ScriptReference/Mesh.GetBlendShapeFrameCount.html, ScriptReference/Mesh.GetBlendShapeFrameWeight.html, ScriptReference/Mesh.GetBlendShapeIndex.html, ScriptReference/Mesh-blendShapeCount.htmlAdding/querying morph-target frames from code; delta-vertex/normal/tangent semantics
Skinning data (scripting)ScriptReference/Mesh-bindposes.html, ScriptReference/Mesh-boneWeights.html, ScriptReference/Mesh.SetBoneWeights.html, ScriptReference/Mesh.GetAllBoneWeights.html, ScriptReference/Mesh.GetBonesPerVertex.htmlBind pose matrices, legacy 4-bones-per-vertex BoneWeight vs. modern up-to-255-bones BoneWeight1/SetBoneWeights
MeshFilter scripting referenceScriptReference/MeshFilter.html, ScriptReference/MeshFilter-sharedMesh.html.mesh (auto-instances a per-object copy) vs. .sharedMesh (edits the shared asset directly) — the split most procedural-mesh bugs trip over

Key Guidelines

Basic Mesh Construction (vertices/triangles/UVs)

A Mesh stores vertex data as several parallel arrays of equal length — position, normal, tangent, color, and up to 8 UV channels — where index i in every array describes the same logical vertex (Manual/mesh-vertex-data.html). Positions are required; everything else is optional and can be left unassigned if a shader doesn't need it. Triangles are a flat int[] of indices into the vertex arrays, taken three at a time; Unity culls back-facing triangles by default using a clockwise winding rule, so the indices for each face must run clockwise as seen from the visible side or the triangle renders invisible from the front and visible from the back (Manual/mesh-index-data.html). The Mesh class exposes two parallel scripting surfaces: a "simple" API (SetVertices, SetTriangles, SetNormals, SetUVs, SetColors, SetIndices, SetBoneWeights, plus the older bare array properties like .vertices/.triangles/.uv) that validates the data you hand it and is the right default for nearly all procedural-mesh code, and an "advanced" buffer API (covered below) for cases where that validation overhead actually shows up in a profile (ScriptReference/Mesh.html). When building a mesh from scratch, always assign vertices before triangles — Unity validates triangle indices against the current vertex count at assignment time, so assigning triangles first (or against a stale, smaller vertex array) throws an out-of-bounds exception. When rebuilding a mesh that already has data (e.g. regenerating a procedural terrain chunk in place), call Mesh.Clear() first so the old, larger index buffer can't reference vertices that no longer exist once the new (possibly smaller) vertex array is assigned.

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class ProceduralGrid : MonoBehaviour
{
    [SerializeField] private int width = 10;
    [SerializeField] private int depth = 10;
    [SerializeField] private float cellSize = 1f;

    void Start()
    {
        Mesh mesh = new Mesh { name = "ProceduralGrid" };

        int vertsPerRow = width + 1;
        int vertsPerCol = depth + 1;
        var vertices = new Vector3[vertsPerRow * vertsPerCol];
        var uvs = new Vector2[vertices.Length];

        for (int z = 0; z < vertsPerCol; z++)
        {
            for (int x = 0; x < vertsPerRow; x++)
            {
                int i = z * vertsPerRow + x;
                vertices[i] = new Vector3(x * cellSize, 0f, z * cellSize);
                uvs[i] = new Vector2((float)x / width, (float)z / depth);
            }
        }

        var triangles = new int[width * depth * 6];
        int t = 0;
        for (int z = 0; z < depth; z++)
        {
            for (int x = 0; x < width; x++)
            {
                int a = z * vertsPerRow + x;
                int b = a + vertsPerRow;
                // Clockwise winding as seen from above (+Y) so the top face is front-facing.
                triangles[t++] = a;
                triangles[t++] = b + 1;
                triangles[t++] = b;
                triangles[t++] = a;
                triangles[t++] = a + 1;
                triangles[t++] = b + 1;
            }
        }

        // Order matters: vertices/UVs first, triangles last, so index validation always succeeds.
        mesh.vertices = vertices;
        mesh.uv = uvs;
        mesh.triangles = triangles;
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        GetComponent<MeshFilter>().mesh = mesh; // .mesh instances a per-object copy, safe to own and later mutate
    }
}

Normals, Tangents, and Bounds Recalculation

Normals, tangents, and bounds are all derived data that Unity does not keep in sync automatically once you hand-edit vertex positions — every one of them requires an explicit recalculation call, and each has a different dependency chain. Mesh.RecalculateNormals() derives per-vertex normals from the triangle/vertex data; because normals are averaged across every triangle sharing a vertex, vertices that were deliberately duplicated at a UV seam or hard edge (so each triangle can have visually distinct normals there) get correctly sharp results, while unintentionally duplicated vertices produce faceted shading where smooth was expected (ScriptReference/Mesh.RecalculateNormals.html). Mesh.RecalculateTangents() must run after normals and UV0 are both present, since it derives the 4-component tangent (xyz direction + w sign for the binormal cross product) from vertex positions, normals, and texture coordinates together — skipping this on a mesh whose shader samples a normal map produces broken or flat-looking normal mapping even though normals themselves look correct (ScriptReference/Mesh.RecalculateTangents.html, Manual/mesh-vertex-data.html). Mesh.RecalculateBounds() recomputes the bounding volume used for frustum culling; assigning through the simple .triangles property triggers this automatically, but the advanced buffer API and Mesh.MeshData.SetSubMesh do not always recalculate mesh-level bounds, so an explicit call is required after buffer-API writes or the mesh may vanish prematurely under culling despite rendering correctly up close. Both RecalculateNormals and RecalculateTangents silently convert the mesh's vertex position (and normal/UV0, respectively) data to VertexAttributeFormat.Float32 if a different format was in use, which is a hidden cost worth knowing about if a mesh was deliberately built with a compressed (FP16) position or normal format via the advanced API.

void RebuildWithNormals(Mesh mesh, Vector3[] vertices, int[] triangles, Vector2[] uvs)
{
    mesh.Clear();
    mesh.vertices = vertices;
    mesh.uv = uvs;
    mesh.triangles = triangles;

    mesh.RecalculateNormals();   // from triangles + vertices
    mesh.RecalculateTangents();  // needs normals + UV0 already set
    mesh.RecalculateBounds();    // redundant here since .triangles already did it, but required after buffer-API writes
}

Submeshes & Multi-Material Meshes

A submesh is a slice of the shared index buffer rendered with one material; a Renderer with N materials needs exactly N submeshes on its mesh, matched by array order to the Renderer.materials array. Mesh.subMeshCount sets how many slices exist, and each slice's index range/topology is described by a SubMeshDescriptor (indexStart, indexCount, topology, baseVertex, plus an auto-computed bounds/firstVertex/vertexCount unless MeshUpdateFlags.DontRecalculateBounds is passed) set via Mesh.SetSubMesh (ScriptReference/Rendering.SubMeshDescriptor.html, ScriptReference/Mesh.SetSubMesh.html). The simple API's SetTriangles(triangles, submeshIndex) overload is the easiest path for a small, fixed number of materials — call it once per submesh index after the shared vertex array is assigned. Submesh index ranges must not overlap one another; the advanced API does not check this for you and an overlap produces undefined rendering rather than an exception.

void BuildTwoMaterialMesh(Mesh mesh, Vector3[] vertices, int[] groundTriangles, int[] roadTriangles)
{
    mesh.Clear();
    mesh.vertices = vertices;
    mesh.subMeshCount = 2;
    mesh.SetTriangles(groundTriangles, 0); // rendered with materials[0]
    mesh.SetTriangles(roadTriangles, 1);   // rendered with materials[1]
    mesh.RecalculateNormals();
    mesh.RecalculateBounds();
}

Advanced MeshData API for Performance

Mesh.MeshData/Mesh.MeshDataArray (in UnityEngine, backed by Unity.Collections.NativeArray) is a lower-level API purpose-built for writing mesh data from worker threads inside C# Jobs, with Burst compilation, instead of the managed-array simple API which only the main thread can touch. Mesh.AllocateWritableMeshData(meshCount) returns a MeshDataArray of writable MeshData structs; populate each one by calling SetVertexBufferParams with a VertexAttributeDescriptor[] layout (which attributes, their VertexAttributeFormat/dimension, and which of up to 4 interleaved streams each lives in), writing into the NativeArray<T> returned by GetVertexData<T>(), then SetIndexBufferParams + writing into GetIndexData<T>(), then setting subMeshCount and calling SetSubMesh per submesh (ScriptReference/Mesh.AllocateWritableMeshData.html). Finally, Mesh.ApplyAndDisposeWritableMeshData(dataArray, mesh) commits the data to real Mesh object(s) and disposes the array in one call — after this the MeshDataArray is invalid and must not be touched again. The mirror-image read path, Mesh.AcquireReadOnlyMeshData(mesh), snapshots an existing mesh's data for job-safe reading (zero-copy as long as the source mesh isn't modified while the snapshot is alive, and it must be disposed, ideally via a using block); it throws if Mesh.isReadable is false, whereas the Editor-only MeshUtility.AcquireReadOnlyMeshData skips that check. A parallel, main-thread-only "advanced" surface exists directly on Mesh (SetVertexBufferParams/SetVertexBufferData/SetIndexBufferParams/SetIndexBufferData/SetSubMesh) for cases that want raw-buffer performance without going through Jobs at all — same validation tradeoffs, no MeshData/allocation ceremony (ScriptReference/Mesh.SetVertexBufferParams.html). Every advanced-API call performs minimal validation by default; Rendering.MeshUpdateFlags (Default, DontValidateIndices, DontResetBoneBounds, DontNotifyMeshUsers, DontRecalculateBounds, DontValidateLodRanges) lets you selectively re-enable or suppress specific checks per call.

using UnityEngine;
using UnityEngine.Rendering;

[RequireComponent(typeof(MeshFilter))]
public class TetrahedronMeshData : MonoBehaviour
{
    void Start()
    {
        // Allocate mesh data for one mesh.
        var dataArray = Mesh.AllocateWritableMeshData(1);
        var data = dataArray[0];

        // 4 faces, 3 unique vertices per face (12 total) since normals differ per face.
        data.SetVertexBufferParams(12,
            new VertexAttributeDescriptor(VertexAttribute.Position),
            new VertexAttributeDescriptor(VertexAttribute.Normal, stream: 1));

        var sqrt075 = Mathf.Sqrt(0.75f);
        var p0 = new Vector3(0, 0, 0);
        var p1 = new Vector3(1, 0, 0);
        var p2 = new Vector3(0.5f, 0, sqrt075);
        var p3 = new Vector3(0.5f, sqrt075, sqrt075 / 3);

        var pos = data.GetVertexData<Vector3>();
        pos[0] = p0; pos[1] = p1; pos[2] = p2;
        pos[3] = p0; pos[4] = p2; pos[5] = p3;
        pos[6] = p2; pos[7] = p1; pos[8] = p3;
        pos[9] = p0; pos[10] = p3; pos[11] = p1;
        // Normals are left unset here and filled in later by RecalculateNormals.

        data.SetIndexBufferParams(12, IndexFormat.UInt16);
        var ib = data.GetIndexData<ushort>();
        for (ushort i = 0; i < ib.Length; ++i) ib[i] = i;

        data.subMeshCount = 1;
        data.SetSubMesh(0, new SubMeshDescriptor(0, ib.Length));

        var mesh = new Mesh { name = "Tetrahedron" };
        Mesh.ApplyAndDisposeWritableMeshData(dataArray, mesh); // dataArray is invalid after this call
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        GetComponent<MeshFilter>().mesh = mesh;
    }
}

(Worked example per ScriptReference/Mesh.AllocateWritableMeshData.html.) The same struct family also underlies reading: wrap Mesh.AcquireReadOnlyMeshData in a using block, index into the returned MeshDataArray, and call data.GetVertices(destinationNativeArray) or the equivalent Get* accessor to pull data out for a Job without allocating a managed array copy.

Combining Meshes (CombineInstance)

Mesh.CombineMeshes merges several source meshes into one destination Mesh, which is a legitimate draw-call optimization for meshes that sit close together and never move relative to each other (a static cabinet made of many drawer meshes, for example) — each CombineInstance in the input array pairs a source mesh with a transform matrix, and one CombineInstance is required per submesh if a source mesh has more than one (Manual/combining-meshes.html, ScriptReference/CombineInstance.html). mergeSubMeshes = true flattens every instance into a single submesh (only valid when every instance shares one material and topology); mergeSubMeshes = false keeps each instance as its own submesh in the result, for combining meshes that still need distinct materials. useMatrices = true applies each instance's transform to the vertices during the combine so everything lands in a common space; source meshes must be CPU-readable (Mesh.isReadable == true) or Unity logs a warning and skips that instance entirely, silently producing an incomplete combined mesh. Note the explicit tradeoff versus the technique this replaces: Unity cannot cull parts of a combined mesh individually — if any part of the combined result is onscreen, all of it draws — so for scenes that need per-object culling, static batching is the better default and manual combining is reserved for cases where the combine itself removes enough draw calls to be worth that loss.

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class CombineChildren : MonoBehaviour
{
    void Start()
    {
        MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
        var instances = new CombineInstance[meshFilters.Length];

        for (int i = 0; i < meshFilters.Length; i++)
        {
            instances[i] = new CombineInstance
            {
                mesh = meshFilters[i].sharedMesh,
                transform = meshFilters[i].transform.localToWorldMatrix,
            };
            meshFilters[i].gameObject.SetActive(false);
        }

        var combinedMesh = new Mesh();
        combinedMesh.CombineMeshes(instances); // mergeSubMeshes defaults true, useMatrices defaults true
        GetComponent<MeshFilter>().sharedMesh = combinedMesh;
    }
}

Skinned & Blend-Shape Meshes

Deformable meshes carry two additional, optional categories of data beyond the base vertex/index buffers: bind poses (for skinned meshes) and blend shapes (for morph-target animation) (Manual/mesh-data-deformable-meshes.html). A skinned mesh's bindposes array stores, per bone, the inverse of that bone's transform at rest — one entry per bone, indexed to match SkinnedMeshRenderer.bones (ScriptReference/Mesh-bindposes.html). Per-vertex bone influence has two parallel APIs: the legacy Mesh.boneWeights property stores a fixed-size BoneWeight struct (exactly 4 bone/weight pairs) per vertex, while the modern BoneWeight1-based Mesh.SetBoneWeights/Mesh.GetAllBoneWeights/Mesh.GetBonesPerVertex trio supports a variable, larger count (up to 255) of weighted bones per vertex and is the preferred path for anything new — weights passed to SetBoneWeights must be sorted most-significant-first, and Unity stores them at reduced (currently ≥16-bit normalized) precision, so don't expect bit-exact round trips (ScriptReference/Mesh.SetBoneWeights.html). Blend shapes are added with Mesh.AddBlendShapeFrame(name, weight, deltaVertices, deltaNormals, deltaTangents): the delta arrays must be Mesh.vertexCount long (deltas, not absolute positions — subtract the base mesh's vertices/normals/tangents to compute them), frames for a multi-frame blend shape must be added in increasing weight order, and frames can only be appended to a brand-new shape or to the last shape already on the mesh, never inserted mid-sequence (ScriptReference/Mesh.AddBlendShapeFrame.html). Procedurally generating a fully rigged skinned mesh from scratch is rare; the far more common pattern is generating or modifying the base geometry (vertices/normals/tangents) procedurally while leaving bind poses and bone weights authored by an importer/DCC tool untouched, or driving existing blend shape weights at runtime via SkinnedMeshRenderer.SetBlendShapeWeight rather than authoring new frames in code.

Common Mistakes

MistakeWhy it happens / fix
Forgetting RecalculateNormals/RecalculateBounds after assigning vertices via the buffer APIUnlike the simple .triangles property (which auto-recalculates bounds), advanced buffer writes don't recompute normals or mesh-level bounds automatically — call both explicitly, and RecalculateTangents too if the shader samples a normal map
Backface culling from wrong winding orderUnity culls back-facing triangles using clockwise winding as seen from the visible side; a counter-clockwise index order makes an otherwise-correct triangle invisible from the front and visible from behind — verify winding, not just index correctness
Exceeding 65535 vertices with the default 16-bit index formatMesh.indexFormat defaults to IndexFormat.UInt16 (max index 65535); a mesh needing more vertices must set indexFormat = IndexFormat.UInt32 before assigning vertices/indices, or split into multiple submeshes using baseVertex to stay within per-submesh 65535-vertex windows
Assigning triangles before vertices (or before enough vertices)Unity validates triangle/index values against the current vertex array size at assignment time; assigning triangles first, or against a stale smaller array, throws an out-of-bounds exception — always set vertices (and other per-vertex arrays) first
Mutating a shared Mesh asset via MeshFilter.sharedMesh instead of an owned instancesharedMesh edits the actual project asset (or an already-shared runtime mesh) in place, silently affecting every other object referencing it; use MeshFilter.mesh (which auto-instances a per-object copy) or explicitly Instantiate/new Mesh() a private copy before mutating
Rebuilding a mesh's data without calling Mesh.Clear() firstAssigning a new, smaller vertex array while the old, larger triangle/index array is still attached can produce out-of-bounds indices Unity rejects, or leftover submesh ranges that no longer make sense — clear before reassigning when vertex count can shrink
Reading from a mesh with Mesh.isReadable == falseMeshes imported without "Read/Write Enabled," or explicitly marked non-readable via Mesh.UploadMeshData(true), throw on any script access to vertex/index arrays at runtime (Editor-only access outside Play mode still works); check/set isReadable before any runtime Get* call, or before passing to Mesh.CombineMeshes/Mesh.AcquireReadOnlyMeshData
Calling MarkDynamic() on a mesh that rarely changesMarkDynamic hints the graphics API to allocate GPU buffers optimized for frequent CPU updates, which can slightly cost rendering-read performance on meshes that don't actually change often — reserve it for meshes updated every frame or near it
Writing raw buffer data with the advanced API and expecting the same validation as the simple APISetVertexBufferParams/SetIndexBufferData/SetSubMesh and friends perform minimal validation by design (for performance); supplying out-of-range indices or overlapping submesh ranges produces undefined behavior instead of a clear exception — get the logic right with the simple API first if unsure, then port to the advanced API
Forgetting ApplyAndDisposeWritableMeshData invalidates the MeshDataArrayAfter applying, the array (and all MeshData structs taken from it) can no longer be read or written; holding a reference and touching it afterward is a use-after-dispose bug
Setting only UV0 but expecting RecalculateTangents to work with no UVs at allRecalculateTangents needs vertex positions, normals, and UV0 together; if UVs are absent entirely, Unity fills tangents with a fallback (1,0,0,1) rather than throwing, which can silently produce wrong-looking normal mapping that's easy to miss in review
Combining meshes that aren't CPU-readableMesh.CombineMeshes silently skips (with a console warning) any CombineInstance whose source mesh has isReadable == false, producing a combined mesh that's missing geometry rather than failing loudly
Assuming submesh index ranges can overlap safelyBoth the simple submesh API and SetSubMesh require non-overlapping indexStart/indexCount ranges per submesh; nothing enforces this at the API boundary, and overlaps produce visually wrong rendering that's hard to trace back to the cause
Using Mesh.boneWeights (max 4 bones/vertex) when more influence is neededThe legacy BoneWeight-based property hard-caps at 4 bones per vertex; if a rig needs more, use the BoneWeight1-based SetBoneWeights/GetAllBoneWeights/GetBonesPerVertex API instead, which supports up to 255
Expecting Mesh Collider cooking to be free for a procedurally regenerated meshRe-cooking collision data on every regeneration of a procedural mesh (e.g. a deforming terrain patch) is a real per-update CPU cost, separate from the render-mesh update itself — see unity-physics for collider/cooking-option tuning once geometry generation itself is confirmed correct

Quick Reference

ItemPurpose
new Mesh()Creates an empty mesh with submesh count and LOD count both starting at 1
Mesh.Clear()Wipes all vertex data and triangle/index data; call before reassigning a smaller vertex array
Mesh.vertices / SetVertices / GetVerticesVertex position array (object space); required attribute for every mesh
Mesh.normals / SetNormals / GetNormals / RecalculateNormalsPer-vertex surface direction; optional but needed for correct lighting
Mesh.tangents / SetTangents / GetTangents / RecalculateTangents4-component tangent (xyz + w binormal sign); needed for normal-mapped shaders
Mesh.uv..Mesh.uv8 / SetUVs / GetUVsUp to 8 texture coordinate channels (TEXCOORD0-7)
Mesh.colors / Mesh.colors32 / SetColorsPer-vertex float or byte-packed color
Mesh.triangles / SetTriangles / SetIndicesIndex buffer; grouped per MeshTopology (3 per triangle, etc.)
MeshTopologyTriangles / Quads / Lines / LineStrip / Points — set per submesh
Mesh.subMeshCount / SetSubMesh / SetSubMeshes / GetSubMeshNumber of material-slot slices and their index ranges/topology
SubMeshDescriptorindexStart, indexCount, topology, baseVertex, bounds, firstVertex, vertexCount for one submesh
Mesh.bounds / RecalculateBoundsCulling bounding volume; auto-updated by .triangles, must be called manually after buffer-API writes
Mesh.indexFormat (IndexFormat.UInt16/UInt32)16-bit (65535 vertex cap, default) vs. 32-bit index buffer
Mesh.isReadableWhether CPU-side mesh data is retained after GPU upload; required for runtime Get* calls, combining, NavMesh baking
Mesh.MarkDynamic()Hints the graphics API to use update-optimized GPU buffers for a frequently-modified mesh
Mesh.UploadMeshData(markNoLongerReadable)Forces an immediate GPU upload of pending changes; can also free the CPU-side copy
Mesh.MarkModified()Notifies Renderer components of an out-of-band mesh change
Mesh.SetVertexBufferParams / SetVertexBufferDataAdvanced API: define and write raw vertex buffer layout/data on the main thread
Mesh.SetIndexBufferParams / SetIndexBufferDataAdvanced API: define and write raw index buffer size/format/data
VertexAttributeDescriptorDeclares one vertex attribute's type/format/dimension/stream for the advanced API
VertexAttribute / VertexAttributeFormatEnum of attribute kinds (Position, Normal, Tangent, Color, TexCoord0-7, BlendWeight, BlendIndices) and their possible data types
Rendering.MeshUpdateFlagsPer-call flags to skip index validation, bounds recalculation, bone-bounds reset, or renderer notification
Mesh.AllocateWritableMeshData(count)Advanced API: allocate one or more writable MeshData structs for Job/Burst-friendly mesh construction
Mesh.ApplyAndDisposeWritableMeshData(data, mesh)Commits populated MeshData to real Mesh object(s) and disposes the MeshDataArray
Mesh.AcquireReadOnlyMeshData(mesh) / MeshUtility.AcquireReadOnlyMeshDataJob-safe read-only snapshot of existing mesh data; Editor-only variant skips the isReadable check
Mesh.MeshDataArray / Mesh.MeshDataContainer/per-mesh struct types for the advanced Jobs-friendly API
Mesh.CombineMeshes(instances, mergeSubMeshes, useMatrices, hasLightmapData)Merges several source meshes/submeshes into this mesh as a draw-call optimization
CombineInstancePairs one source mesh (+ optional submesh index) with a transform matrix for CombineMeshes
Mesh.AddBlendShapeFrame / ClearBlendShapes / GetBlendShapeFrameCount/Weight/IndexAuthor and query morph-target blend shape data
Mesh.bindposesPer-bone inverse rest-pose matrices for a skinned mesh, indexed to match SkinnedMeshRenderer.bones
Mesh.boneWeights (legacy) vs. SetBoneWeights/GetAllBoneWeights/GetBonesPerVertex (modern)4-bones-per-vertex BoneWeight struct vs. up-to-255-bones BoneWeight1-based API
MeshFilter.mesh vs. MeshFilter.sharedMesh.mesh auto-instances a private per-object copy (safe to mutate); .sharedMesh edits the actual shared asset in place
Mesh.HasVertexAttribute / GetVertexAttribute(s)Inspect what attributes/format/stream an existing mesh actually stores

Advanced Notes

MeshData vs. the classic managed-array API. The classic simple API (SetVertices, .triangles, etc.) operates on ordinary managed C# arrays/lists, which means it can only be called from the main thread and always pays a managed-allocation and per-call validation cost. Mesh.MeshData/MeshDataArray wraps Unity.Collections.NativeArray<T> buffers instead, which are safe to read and write from worker threads inside IJob/IJobParallelFor implementations and are Burst-compilable — this is the intended path for generating large amounts of procedural geometry (terrain chunks, voxel meshes, particle-driven geometry) every frame without spiking the main thread or triggering GC. The tradeoff is ceremony and reduced safety: you must get vertex layout, index format, and submesh ranges correct yourself, since the advanced API performs minimal validation by default (ScriptReference/Mesh.html, "Simple vs Advanced Mesh API"). A reasonable rule of thumb: build correctness with the simple API first, and only move hot, per-frame, high-volume mesh generation to MeshData + Jobs after profiling shows the managed-array path is the actual bottleneck.

MarkDynamic for frequently-updated meshes. Calling Mesh.MarkDynamic() once, ideally before the first vertex/index upload, tells the underlying graphics API (Direct3D/Vulkan/Metal) to allocate the mesh's GPU buffers using a strategy suited for frequent CPU-side writes rather than the default strategy optimized for static, rarely-changing geometry. This meaningfully speeds up repeated SetVertices/SetVertexBufferData/UploadMeshData calls on the same mesh (e.g. a wave-animated water plane, cloth-like deformation, or a mesh driven every Update), at a small potential cost to raw rendering-read throughput — so it should be reserved for meshes that are actually updated every frame or close to it, not applied blanket to every procedurally-generated mesh regardless of how often it changes (ScriptReference/Mesh.MarkDynamic.html).

Mesh Collider cooking cost for procedural geometry. Attaching a MeshCollider to a procedurally generated or frequently regenerated mesh means Unity must "cook" that geometry into a physics-engine-friendly collision representation every time the source mesh changes meaningfully — this cooking step is a distinct, often non-trivial CPU cost on top of the mesh-generation work itself, and its cost is tunable via the Mesh Collider's Cooking Options (Manual/physics-optimization-cpu-mesh-cooking-options.html, Manual/prepare-mesh-for-mesh-collider.html). If a procedural mesh needs to double as a collider and is regenerated often (deforming terrain, runtime-carved geometry), budget for re-cooking cost separately from render-mesh generation cost, and consider whether a simpler proxy collider (primitive shapes, a lower-resolution convex hull) can stand in instead of cooking the full-detail render mesh every update. Full Rigidbody/Collider/cooking-option tuning guidance lives in unity-physics — this skill only flags the cost as something to account for once the mesh-generation code itself is working.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.