Skip to Content
DocumentationHow-To GuidesScripting Materials

Scripting Materials

Genesis materials can be fully scripted from C# — every property, keyword, and feature toggle is exposed via the standard Unity material API. The catch is that the wrong API call instantiates the material and silently kills batching. This recipe shows the safe patterns.

What you’ll build

A scene where many enemies share one Genesis material and each flashes red on hit via MaterialPropertyBlock, while the SRP batcher keeps them in a single draw call.

Requirements

  • Unity + Genesis.
  • Basic familiarity with MonoBehaviour and coroutines.

The two APIs

Wrong: renderer.material

// Instantiates a UNIQUE material per renderer. Breaks batching. renderer.material.SetColor("_BaseColor", Color.red);

A scene of 100 enemies hit this way becomes 100 unique materials and 100 draw calls.

Right: MaterialPropertyBlock

var mpb = new MaterialPropertyBlock(); renderer.GetPropertyBlock(mpb); mpb.SetColor("_BaseColor", Color.red); renderer.SetPropertyBlock(mpb);

The MPB path keeps the shared material intact — the same 100 enemies stay one batched draw call. The difference is invisible until you open the Frame Debugger.

Steps

1. Cache property IDs

Property name strings hit a hashmap every time. Cache the IDs once:

static readonly int BaseColor = Shader.PropertyToID("_BaseColor"); static readonly int CelMode = Shader.PropertyToID("_CelMode"); static readonly int OutlineWidth = Shader.PropertyToID("_OutlineWidth"); static readonly int DissolveAmount = Shader.PropertyToID("_DissolveThreshold");

2. Hit-flash on damage

public class HitFlash : MonoBehaviour { [SerializeField] Renderer rend; static readonly int FlashColor = Shader.PropertyToID("_HitEffectColor"); static readonly int FlashAmt = Shader.PropertyToID("_HitEffectAmount"); MaterialPropertyBlock mpb; void Awake() => mpb = new MaterialPropertyBlock(); public void Flash(float duration = 0.15f) => StartCoroutine(Run(duration)); System.Collections.IEnumerator Run(float duration) { rend.GetPropertyBlock(mpb); mpb.SetColor(FlashColor, Color.white * 4f); for (float t = 0; t < duration; t += Time.deltaTime) { mpb.SetFloat(FlashAmt, 1f - t / duration); rend.SetPropertyBlock(mpb); yield return null; } mpb.SetFloat(FlashAmt, 0f); rend.SetPropertyBlock(mpb); } }

3. Toggle keywords at runtime

Keywords are per-material, not per-renderer. You must instantiate to change them — accept the batching break, or pre-create two material variants and swap.

// Approach A: live keyword toggle (breaks batching) renderer.material.EnableKeyword("_DISSOLVE_ON"); // Approach B: pre-created variants (preserves batching) renderer.sharedMaterial = dissolveOnMaterial;

Variant B is faster if you have only a handful of keyword combinations. Variant A is fine for one-shot effects on a few hero objects.

4. Animate from Timeline

Use Material Property Track. Bind to the renderer. Animate any Genesis float / color property. Timeline uses MPB internally, so batching survives.

5. Bulk updates with the Genesis Material API

For one-shot setup, Genesis offers a thin wrapper that batches multiple property sets and validates keyword state:

using GenesisShader; GenesisMaterial.For(renderer) .SetCelMode(CelMode.Stepped, steps: 3) .EnableOutline(width: 1.5f, color: Color.black) .Apply();

See Genesis Material API for the full surface.

Always profile material script changes with the Frame Debugger. If your draw call count jumps when a script runs, you are probably hitting renderer.material somewhere.

Variations

  • Pool of effects: pre-create MPBs in a stack, recycle on enable / disable.
  • Per-instance random: assign a random _VertexAnimOffset per enemy at spawn for variation.
  • Editor live-tuning: drive properties from a [Range] field in a [ExecuteAlways] MonoBehaviour.
Last updated on