Unity Term Book
Graphics & Rendering

Mesh Renderer & Filter

Mesh Filter plus Mesh Renderer display 3D geometry — the Filter supplies mesh data, the Renderer draws it with the assigned Material, shadows, and lighting.

Imagine...

A Mesh Filter is a mold — it holds the 3D shape (vertices, edges, faces). A Mesh Renderer is the painter — it takes that mold, applies a Material (paint, texture), and sends it to the GPU. They work as a pair: without a Mesh Filter the Renderer has nothing to draw; without a Renderer the mesh is invisible. At build time, Unity batches thousands of Mesh Renderers into draw call batches so the GPU stays efficient.

The concept in detail

MeshFilter holds a reference to a Mesh asset — geometry data: vertices, triangles (index triplets), normals (for lighting), UVs (texture mapping), and optionally tangents and colors. The mesh can be an imported asset (.fbx/.obj) or built procedurally from code.

MeshRenderer holds a Materials array (one per sub-mesh), Shadow Casting/Receiving, Lightmap baking, Probe Anchor, and most importantly Layer and Rendering Layer Mask (URP lighting). The bounds (bounding box) drive frustum culling — the object is skipped when its bounds sit outside the camera frustum.

Unity batches with several techniques: Static Batching (mark objects “Static”), Dynamic Batching (moving objects under 300 vertices), GPU Instancing (same mesh + same material, one draw call for many instances). Enable GPU Instancing on the Material.

From Unity 2022+, Mesh API 2.0 lets you create and edit meshes procedurally without boxing: Mesh.SetVertexBufferData, Mesh.SetIndexBufferData — write straight into GPU buffers, skipping managed arrays.

From Mesh to screen

MeshFilter

Mesh Asset
vertices, triangles
normals, UVs

+

MeshRenderer

Materials[ ]
Shadow settings
Lightmap/Probe

Batching / Culling

Static/Dynamic Batch
Frustum Culling
Occlusion Culling

GPU Draw Calls

Vertex Shader
Fragment Shader
→ Pixels

Sub-mesh & Materials

Mesh (2 sub-mesh)

Materials[ ]

Materials[0] = Body_Mat
Materials[1] = Window_Mat

Hands-on steps

1

Import a model — MeshFilter is added for you

Drop a .fbx/.obj into the Project, then into the Scene — Unity creates MeshFilter + MeshRenderer with Material slots.

2

Configure Shadow Casting

MeshRenderer → Cast Shadows: On/Off/Two Sided. Receive Shadows: on for floors that catch other objects’ shadows.

3

Enable Static Batching for static environment

Inspector → Static dropdown → Batching Static. Many static objects sharing a Material collapse into one draw call.

4

Build a mesh procedurally

new Mesh(), set vertices[], triangles[], normals[], uv[] → assign to GetComponent().mesh.

5

Count draw calls with Frame Debugger

Window → Analysis → Frame Debugger: inspect each draw call and see which objects cost the most.

Interactive simulator

Build a mesh procedurally — pick a topology and inspect wireframe / normals.

Vertices: 24Triangles: 12Draw Calls: 1

Code example

Basic

Build a triangle mesh procedurally and show it with MeshFilter + MeshRenderer.

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class ProceduralTriangle : MonoBehaviour
{
  void Start()
  {
      Mesh mesh = new Mesh();
      mesh.name = "Procedural Triangle";

      // 3 vertices of the triangle
      mesh.vertices = new Vector3[] {
          new Vector3(0f, 0f, 0f),
          new Vector3(1f, 0f, 0f),
          new Vector3(0.5f, 1f, 0f)
      };

      // Clockwise indices (viewed from the front)
      mesh.triangles = new int[] { 0, 1, 2 };

      // UV mapping: one texture coord per vertex
      mesh.uv = new Vector2[] {
          new Vector2(0f, 0f),
          new Vector2(1f, 0f),
          new Vector2(0.5f, 1f)
      };

      mesh.RecalculateNormals(); // Compute normals automatically

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

Code example

Advanced

Swap Materials at runtime and use a MaterialPropertyBlock to tint instances without breaking batching.

using UnityEngine;

public class MeshRendererUtils : MonoBehaviour
{
  MeshRenderer rend;
  MaterialPropertyBlock mpb;

  void Awake()
  {
      rend = GetComponent<MeshRenderer>();
      mpb = new MaterialPropertyBlock();
  }

  // Read mesh bounds in world space
  void LogMeshInfo()
  {
      Mesh mesh = GetComponent<MeshFilter>().sharedMesh;
      Debug.Log($"Vertices: {mesh.vertexCount}");
      Debug.Log($"Triangles: {mesh.triangles.Length / 3}");
      Debug.Log($"Bounds: {rend.bounds.size}");
  }

  // Swap the Material on a specific sub-mesh (index 1 = window glass)
  public void SwapSubMesh(int index, Material mat)
  {
      Material[] mats = rend.materials; // Get a copy
      mats[index] = mat;
      rend.materials = mats;            // Assign back
  }

  // Per-instance color via PropertyBlock — keeps GPU Instancing
  public void SetInstanceColor(Color c)
  {
      rend.GetPropertyBlock(mpb);
      mpb.SetColor("_BaseColor", c);
      rend.SetPropertyBlock(mpb);
  }
}

📌 Quick recap

  • MeshFilter = shape, MeshRenderer = how it is drawn
  • sharedMesh does not clone; .mesh creates a private copy
  • Static + same Material → Static Batching
  • triangles[] must be clockwise (CW) from the front
  • RecalculateNormals() after changing vertices

⚠️ Common mistakes

  • ❌ The mesh is inside-out (face culled)

    Triangle indices are counter-clockwise → Unity culls that face

    ✅ Flip the index order: {0,1,2} → {0,2,1}, or use a Double Sided shader

  • ❌ Assigning meshFilter.mesh many times in Play Mode

    Each assignment creates a new Mesh → memory leak

    ✅ Create the Mesh once in Awake(), only update vertices in Update()