~6 min read

Rendering, Materials and Shaders

URP, Material vs Shader, Shader Graph, and why "multicolored cubes" are expensive.

What actually draws a frame

Unity submits the scene to the GPU through a render pipeline — a sequence of passes: shadows, opaque, transparent, postprocess. The exact steps depend on the chosen pipeline (see the chapter on the stack):

  • Built-in RP — old, monolithic. Forward or Deferred rendering, your choice.
  • URP — Forward+ by default, cross-platform. This is the recommended choice now.
  • HDRP — Deferred + Forward, physically correct, for high-end PCs and consoles.
Web

A WebGL/WebGPU draw call is a call that draws a vertex buffer with a shader. The browser does almost nothing for you: you assemble batches yourself and send uniforms.

Unity

Unity assembles draw calls from the scene for you, sorts them, merges them (batching), and invokes shaders. You see the list of calls in the Frame Debugger and can optimize it.

Mesh, Material, Shader — the three pillars

  • Mesh — geometry: arrays of vertices, normals, UV coordinates, indices. Imported from FBX/OBJ/glTF.
  • Shader — a GPU program describing how pixels are produced from vertices and textures.
  • Material — an instance of a Shader with concrete parameters (textures, colors, numbers).

One Mesh + one Material = one draw call (simplified). If you have 100 objects with the same material, Unity can merge them into one draw call (Static / Dynamic / GPU Instancing). If each has its own Material — 100 draw calls.

An atlas instead of 50 textures

A standard optimization technique: combine textures into a single atlas so that all objects use one material. This enables automatic batching and reduces overhead.

The Material Inspector — what you tweak

Open a material and you see the parameters of the active shader. For URP/Lit (the standard):

  • Surface Type — Opaque or Transparent. Transparent is more expensive and requires sorting.
  • Workflow — Metallic or Specular (physically correct PBR).
  • Base Map — the main color texture (albedo).
  • Metallic Map + Smoothness — how metallic and smooth the object is.
  • Normal Map — a normal map to simulate fine surface detail without adding polygons.
  • Emission — self-illuminated areas.
  • Tiling / Offset — UV repetition and offset.
// Change a material's color from code (Built-in RP / Standard shader: the _Color property)
var renderer = GetComponent<Renderer>();
renderer.material.color = Color.red;          // creates an instance!

// For URP/Lit the main property is called _BaseColor — .color may not work as expected:
renderer.material.SetColor("_BaseColor", Color.red);

// Via a property block — without creating a new material, without losing batching
var block = new MaterialPropertyBlock();
renderer.GetPropertyBlock(block);
block.SetColor("_BaseColor", Color.red);
renderer.SetPropertyBlock(block);
renderer.material vs renderer.sharedMaterial

Accessing renderer.material creates a unique copy of the material for this object. This breaks batching and leaks memory if done often. If you change it frequently — use a MaterialPropertyBlock (as in the example above) — it does not create a new material.

Shader Graph — a visual shader

If HLSL is still uncomfortable for you, URP/HDRP have Shader Graph — a visual shader editor. In the Project: Create → Shader Graph → URP → Lit Shader Graph. A graph opens, where you connect nodes: textures, operators, the output to the Surface.

Under the hood Shader Graph generates HLSL and is suitable for most gameplay effects: a pulsating glow, hologram, dissolve, water surface. Complex post-processing can also be assembled.

Render Graph (URP 17, Unity 6)

In Unity 6 / URP 17 the URP renderer works through Render Graph by default — a declarative API where each pass describes its inputs/outputs (textures, buffers), and the engine itself builds a dependency graph and optimizes execution (merge passes, reuse render targets, parallel where possible).

This affects you if you:

  • Write custom Renderer Features in URP — the old API (ScriptableRenderPass.Execute) is deprecated; you need to move to the new RecordRenderGraph().
  • Use built-in passes — you do nothing, Render Graph is under the hood.
  • Work with HDRP — there Render Graph has been around since 2021, but in Unity 6 the model is unified.

The old non-Render-Graph path remains as “Compatibility Mode” in the URP Asset, but it is deprecated.

Batching and draw call optimization

To reduce the number of draw calls, Unity offers several mechanisms in order of historical age:

  1. Static Batching — for non-moving objects. Their meshes are merged into one large mesh when the scene loads. Enabled with the “Static” checkbox in the Inspector.
  2. Dynamic Batching — Unity merges small meshes (up to ~300 vertices) with the same material on the fly. An expensive CPU pass, disabled by default in URP.
  3. GPU Instancing — N instances of a single mesh with different matrices are drawn with one draw call. Enabled in the material with the “Enable GPU Instancing” checkbox.
  4. SRP Batcher — for URP/HDRP. Merges the rendering of objects with shaders that are compatible with the SRP Batcher (most URP shaders are). Enabled by default.
  5. GPU Resident Drawer (Unity 6, the headline feature) — the next step after the SRP Batcher. Built on BatchRendererGroup, it automatically batches thousands of repeating meshes (props, rocks, foliage) without manually configuring a MultiMesh. It significantly reduces CPU load in scenes with a large number of objects.

How to enable the GPU Resident Drawer

The requirements are strict — skipping any item leads to a silent fail:

  1. URP Asset → Rendering → Rendering Path = Forward+ (required; Forward / Deferred are not supported).
  2. URP Asset → Rendering → GPU Resident Drawer = Instanced Drawing.
  3. SRP Batcher enabled in the URP Renderer (enabled by default).
  4. Project Settings → Graphics → Shader Stripping → BatchRendererGroup Variants = Keep All. Otherwise the shaders are stripped in the build, and the GRD silently does not work.
  5. The objects’ shaders must be DOTS Instancing-compatible: #pragma target 4.5 + the declared DOTS_INSTANCING_ON keyword. The standard URP/Lit/Unlit satisfy this.

BatchRendererGroup is the low-level Unity API on which the GRD is built. The GRD transparently works both with static geometry (the Static checkbox) and with dynamic MeshRenderer instances — the key thing is that the shader supports DOTS Instancing.

GPU Resident Drawer + GPU Occlusion Culling = the main Unity 6 buff

Together with GPU Occlusion Culling (also Unity 6) it gives a huge boost in open-world scenes: rocks beyond the horizon do not go into draw calls at all, and the visible ones are batched into dozens of calls instead of thousands.

Frame Debugger

Window → Analysis → Frame Debugger. It freezes the frame and shows all draw calls step by step. Understanding what’s bottlenecking there is half the battle in render optimization.

Post-processing (Volume Framework)

In URP/HDRP, instead of a separate component, a Volume is used. It is a GameObject with a Volume component and an attached Volume Profile — an asset with effect settings.

The effects:

  • Bloom — a glow from bright pixels.
  • Tonemapping — converting HDR to LDR (ACES, Neutral, …).
  • Color Adjustments — exposure, contrast.
  • Vignette — darkening at the edges.
  • Depth of Field — blurring out of focus.
  • Motion Blur — blur during fast movement.
  • Chromatic Aberration — separation of color channels at the edges.

A Volume can be Global (applied everywhere) or Local (with a trigger zone — the effect only in that area). Convenient: entering a cave — a Local Volume with a different tonemapping and vignette.

Don't overuse post-processing

Bloom + Motion Blur + Vignette + Depth of Field — the typical kit of an indie developer who “wants it to look like a movie”. On mobile this easily eats half the frame budget. Enable only what works for your visual language.

In the next chapter — lighting, without which even perfect materials look flat.