> For the complete documentation index, see [llms.txt](https://postica.gitbook.io/binding-system-3/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://postica.gitbook.io/binding-system-3/tutorials/first-modifier.md).

# Writing Your First Modifier

Write a Gamma modifier in about twenty lines, inverse included.

A linear volume slider does not sound linear. Half way along is far louder than half as loud, because hearing is roughly logarithmic and the slider is not. The usual fix is a `Mathf.Pow` buried in whatever reads the slider.

That fix belongs in the connection, not in either end of it. This tutorial writes it as a modifier: about twenty lines, usable on any `float` binding in the project, with a parameter that can itself be bound and a write direction that works.

## What you will build

A **Gamma** modifier that appears in the Add Modifier menu next to the built-in ones, draws on one line, and reads correctly in both directions.

<figure><img src="https://3048705056-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLUfBXR02sJ5i0MxwYbaV%2Fuploads%2FAtilxVptPT6kw0CtwzGh%2FScreenshot%202026-09-26%20at%2023.13.03.png?alt=media&amp;token=94907fa4-2024-418e-8467-30d79e816e6e" alt=""><figcaption><p>The finished Gamma modifier on a binding, drawn on one line</p></figcaption></figure>

**About 15 minutes.**

## What you need

A project with the Binding System, and a `float` binding to try it on. The brightness setting from [the settings panel tutorial](/binding-system-3/tutorials/settings-panel.md) is ideal, but any bound float will do.

## 1. Check the catalogue first

Before writing a modifier, look for one that already does the job. There are around fifty, and **Evaluate Curve** in particular can shape a response by hand with an `AnimationCurve`.

A curve is the right tool when the shape is artistic. A gamma exponent is the right tool when the shape is a formula you want to state once and reuse, and when you need an exact inverse, which a hand-drawn curve does not give you. That is the case here.

## 2. Write it

Create `GammaModifier.cs` anywhere in `Assets`:

{% code title="GammaModifier.cs" %}

```csharp
using System;
using Postica.BindingSystem;
using Postica.Common;
using UnityEngine;

[Serializable]
[OneLineModifier]
[TypeDescription("Bends a 0 to 1 value through a gamma curve, so a linear control feels linear.")]
public class GammaModifier : BaseModifier<float>
{
    [Tooltip("The exponent. Above 1 bends the curve down, below 1 bends it up.")]
    public ReadOnlyBind<float> gamma = 2.2f.Bind();

    public override string Id => "Gamma";

    public override string ShortDataDescription => $"^ {gamma.ToString("gamma")}";

    protected override float Modify(float value)
        => Mathf.Pow(Mathf.Clamp01(value), SafeGamma);

    protected override float InverseModify(float output)
        => Mathf.Pow(Mathf.Clamp01(output), 1f / SafeGamma);

    private float SafeGamma => Mathf.Max(0.0001f, gamma);
}
```

{% endcode %}

Save and let Unity compile. That is the whole modifier.

## 3. Find it in the menu

Open any binding on a `float` field, click the path button, and look under **Add Modifier**.

**Gamma** is there, between the built-in entries, with your `[TypeDescription]` text as its tooltip.

<figure><img src="https://3048705056-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLUfBXR02sJ5i0MxwYbaV%2Fuploads%2F4romgoOQ6sx0ZS4IxZH2%2FScreenshot%202026-09-26%20at%2023.10.33.png?alt=media&amp;token=9ba9603f-05ac-4208-933a-9e08ba948b81" alt="" width="375"><figcaption><p>Gamma in the Add Modifier menu, with its description</p></figcaption></figure>

You registered nothing. The package scans for types implementing `IModifier` on import and registers what it finds.

## 4. Add it and read the row

Add it to the binding. The row is a single line with the exponent field on it, and to its right a summary reading `^ 2.2`.

Change the exponent to `0.5`. The summary becomes `^ 0.5` as you type.

<figure><img src="https://3048705056-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLUfBXR02sJ5i0MxwYbaV%2Fuploads%2FQLOwcIjB5VqceU4Wu3SL%2FScreenshot%202026-09-26%20at%2023.11.45.png?alt=media&amp;token=278ac56c-6b39-4b51-ab87-a9b11c49d078" alt="" width="563"><figcaption><p>The collapsed row, with the exponent and its summary</p></figcaption></figure>

That summary is `ShortDataDescription`, and it is what makes a stack of five modifiers readable without expanding any of them. Put the *shape* of what the modifier does in it, not its name: the name is already on the row.

## 5. Bind the parameter

Click the small hexagon toggle beside the exponent field and point it at any float in the scene.

The summary changes from `^ 2.2` to `^ gamma`, in the accent colour.

That is what `gamma.ToString("gamma")` does: it prints the *name* when the parameter is bound and the *value* when it is not. Printing a value that is really coming from somewhere else would be worse than useless, so every built-in modifier uses this helper for its parameters, and yours should too.

You did not write any code for this. Declaring the parameter as `ReadOnlyBind<float>` rather than `float` is the entire mechanism, and it is why nearly every parameter in the catalogue is bindable.

## 6. Check the inverse

Set the binding's mode to **RW** by clicking the mode icon on the row.

Now the write direction runs `InverseModify`. Write `0.25` into the target and the source receives `0.5`, because `0.5 ^ 2 = 0.25`. Round-trip a value through both directions and you get back what you started with, to floating point precision.

Try deleting `InverseModify` and doing the same thing. Writes now pass straight through unchanged, so the source gets the curved value rather than the raw one, and the binding is quietly asymmetric. Put it back.

{% hint style="info" %}
`InverseModify` defaults to returning the value untouched, which is right for a modifier that logs or fans out, and wrong for one that transforms. If your modifier changes the value and you can express the reverse, write it. If you cannot express the reverse honestly, say so in the description rather than leaving a plausible-looking one that lies.
{% endhint %}

## What just happened

**`BaseModifier<T>` is doing the plumbing.** It implements `IReadWriteModifier<T>`, holds the serialized `ModifyMode`, and routes the two passes for you: on a read it calls `Modify`, on a write it calls `InverseModify`, and it swaps them when the mode is write-only so a one-directional modifier still behaves. You wrote the two transforms and nothing else.

**It was registered by being compiled.** Auto registration picks up every non-abstract, non-generic type implementing `IModifier` that is not marked `[HideMember]`, which is why the built-ins carry that attribute: they register themselves explicitly instead. The toggle lives in [Project Settings ▸ Binding System](/binding-system-3/reference/settings.md). Explicit registration is `ModifiersFactory.Register<GammaModifier>()`.

**`Id` names the menu entry. The class name is the identity.** The `Id` is the label in the Add Modifier menu and the key the registry uses, and it has to be unique across the project. It is not written into saved data.

What *is* written into saved data is the class. Modifiers are stored as `[SerializeReference]`, which persists the assembly, the namespace and the class name, so **renaming or moving the class orphans every instance already saved in a scene or a prefab**. [Refactoring](/binding-system-3/project-tools/diagnostics/refactoring.md) cannot repair that: it tracks serialized member paths, not modifier types. Pick the class name and its namespace before the modifier ships, and change the `Id` freely afterwards.

**`[OneLineModifier]` works because there is exactly one visible field.** The row is built from the modifier's visible serialized fields. One of them plus this attribute puts the editor into the row header. Add a second visible field and the row silently becomes a foldout, which is a good reason to keep a one-line modifier to one parameter.

**Nothing here is dynamic.** `BaseModifier<T>` deliberately does not declare `IDynamicComponent`, because its output depends only on its input. A modifier whose output can change *without* the input changing, anything driven by time or randomness, has to declare that itself. See [Custom Modifiers](/binding-system-3/reference/extending/modifiers.md) and [Making a Value Follow Smoothly](/binding-system-3/tutorials/smooth-values.md).

## Try changing this

**Make it work for every numeric type.** Derive from `NumericModifier` instead and implement `Modify(long)` and `Modify(double)`. One registration then covers `int`, `long`, `float`, `double` and the rest, which is how the built-in **Absolute Value** covers them all in six lines.

**Give it a second parameter.** Add a bindable floor so the curve does not reach zero. The row becomes a foldout, so drop `[OneLineModifier]`, and extend `ShortDataDescription` to mention both.

**Test it without Unity.** A modifier is a plain class, so it needs no scene:

```csharp
[Test]
public void Gamma_Inverts()
{
    var m = new GammaModifier { gamma = 2f.Bind() };
    var read = ((IReadModifier<float>)m).ModifyRead(0.5f);
    Assert.AreEqual(0.25f, read, 1e-5f);
}
```

**Rename the `Id` and nothing breaks.** Saved instances keep working, because their `[SerializeReference]` points at the class, not at the label. Now rename the *class* in a throwaway scene and watch the saved instances turn into empty rows. Worth doing once, deliberately, so you never do it by accident in a real project.

## Related pages

* [Custom Modifiers](/binding-system-3/reference/extending/modifiers.md): every interface, the drawing attributes, and registration.
* [Modifiers](/binding-system-3/pipeline/modifiers.md): where a modifier sits in the pipeline.
* [Modifier Catalogue](/binding-system-3/pipeline/modifier-catalogue.md): check here before writing another one.
* [Writing Your First Converter](/binding-system-3/tutorials/first-converter.md): the same exercise, across a type boundary.
* [Standard Modifiers for Your Types](/binding-system-3/pipeline/standard-modifiers.md): generic modifiers closed over your own types.
