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
MonoBehaviourand 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
_VertexAnimOffsetper enemy at spawn for variation. - Editor live-tuning: drive properties from a
[Range]field in a[ExecuteAlways]MonoBehaviour.