Materials and Shaders
Most sprites never need a material — the engine draws them correctly out of the box. Materials are how you get the other looks: a glow, a dissolve, an outline, a screen-wide underwater wobble, an enemy flashing white when hit.
Two files, and which one you edit
This trips people up once and then never again:
| File | What it is | Where you edit it |
|---|---|---|
.mat | The shader — the actual program that runs per pixel | The Shader Editor panel |
.jmaterial | A material — one shader plus a set of values for its parameters | The Properties panel |
One .mat called Glow, and three .jmaterial files using it: a blue glow, a red glow, a faint white glow. Change the shader once and all three follow.
The .jmaterial is what you assign to a renderer. Sprite Renderer, Tilemap and Mesh Renderer each have a material slot in the Properties panel; drag a .jmaterial onto it.
Writing a shader
Double-click a .mat in the Content Browser to open the Shader Editor. Its toolbar has everything you need:
| Button | What it does |
|---|---|
| Save (Ctrl+S) | Saves and compiles — errors appear in the gutter next to the offending line and in the Log panel |
| Compile | Compiles without saving |
| Open in Joystick Code | Hands the file to the full editor, for anything longer than a quick tweak |
| View Generated | Shows what your material actually compiled down to |
| Cheat Sheet | The built-in reference for what you can write |
There's a Templates dropdown with commented starter shaders — start from one of those rather than a blank file. Below the source is a live preview you can point at a quad, a sphere, a cube or the entity you have selected, with a few lighting environments and an animation toggle for time-based effects.
Compilation happens in-process on a worker thread and typically takes tens of milliseconds, so the loop is genuinely edit-save-look.
Sprites need a sprite-compatible shader
A shader written for meshes won't work on a Sprite Renderer. The engine checks this both when the material compiles and at the moment you drop it onto a renderer, so you get told rather than getting an invisible sprite.
Parameters
Anything your shader exposes as a parameter shows up as a field in the material's inspector — the engine reads the shader and builds the UI, so you never maintain a separate list.
You can annotate a parameter in the shader source to change how it's presented: a colour swatch instead of three number boxes, a slider with a sensible range, a checkbox, a tooltip. Parameters can be numbers, vectors, colours, booleans or textures; a texture parameter also carries its own filtering and wrap settings.
A material can optionally override a small amount of render state — double-sided, depth writing, cull mode.
Changing materials from C#
public class Enemy : GameEntity
{
private Material m_Flash;
protected override void OnCreate()
{
// Shared: every entity using this .jmaterial sees the change.
Material shared = Material.LoadShared("Materials/HitFlash.jmaterial");
// A clone only this entity uses.
m_Flash = shared.InstantiateMaterial();
}
public void OnHit()
{
m_Flash.SetFloat("flashAmount", 1.0f);
}
protected override void OnDestroy()
{
m_Flash.Dispose();
}
}| Method | Sets |
|---|---|
SetFloat / GetFloat | A single number |
SetVector2 / SetVector3 / SetVector4 | Vectors |
SetColor / GetColor | A colour, as a Vector4 of r, g, b, a |
SetBool / GetBool | A toggle |
SetTexture | A texture, by asset path |
HasParameter | Whether the shader actually declares that name |
InstantiateMaterial() gives you something to dispose
LoadShared hands back the shared material — changing a parameter on it changes it for every entity using that file. InstantiateMaterial() gives you a private copy, and you own its lifetime: dispose it in OnDestroy. This is deliberately a verb rather than something that happens by accident, because leaked clones are one of the easiest performance bugs to create and one of the hardest to find.
Setting a parameter the shader doesn't declare doesn't throw — it logs a warning naming both the parameter and the material. Check the Log panel if a change appears to do nothing.
Tinting without a material
For a plain colour tint you don't need a material at all:
GetComponent<SpriteRendererComponent>().Color = new Vector4(1, 0.4f, 0.4f, 1);
GetComponent<UIImage>().Color = new Vector4(1, 1, 1, 0.5f); // fade a UI imageAnd to swap the image itself:
GetComponent<SpriteRendererComponent>().TexturePath = "Textures/PlayerHurt.png";
GetComponent<UIImage>().Source = "Textures/AlertPanel.png";Both take effect on the next frame's draw.
Shader graphs
If you'd rather build an effect out of connected nodes than write shader source, a .jshadergraph compiles down to a .mat — you get a real material out the other end, usable everywhere a hand-written one is.
The graph canvas isn't drawable yet in 0.1.0. Opening a .jshadergraph shows a summary of the graph rather than the diagram, and the visual editing surface is still being built. See Node graphs for exactly what works today across all of Joystick's node-based editors.
Keeping it fast
- Use the default material where you can. No custom shader means no custom shader cost.
- Sprites sharing a material and a texture batch into one draw call. Every distinct material is a batch break, so a per-enemy clone of the same effect costs more than one shared material with the same values.
- The shader runs per pixel. A full-screen effect runs it millions of times a frame. Test on the weakest device you intend to ship to, not on your desktop.
- Watch for per-frame material changes — swapping materials every frame defeats batching.
Performance covers how to measure any of this.
See also
- Node graphs — what the shader graph and visual script editors can and can't do today
- Particle effects — blend modes and additive rendering
- Performance — draw calls, batching, and profiling