Tool Development: Resolve, Fusion, Custom Code #1: Exposure

Welcome to this blog series on developing custom colour grading tools using DaVinci Resolve, Fusion, and DCTL coding.
In this series, you’ll learn how to build a range of tools across three platforms, each offering a unique level of control and flexibility.

TL;DR

In this tutorial, we build a professional exposure adjustment tool from scratch—starting with Resolve’s Colour page, refining it in Fusion, and finalising it in DCTL code. Along the way, you'll learn:

  • How to model exposure using linear gain

  • How to use Fusion's Custom Tool to visualise and control image maths

  • How to write a custom DCTL that mimics Resolve's CSTs and Gain wheel

  • How to build a slider that reads and responds in stops—just like a cinematographer expects

By the end, you'll have a reusable, real-world exposure tool that works, reads, and feels just like a camera—and you'll understand every step behind it.

We’ll begin in the Colour page, using native nodes and operators. This approach is the most accessible—especially if you're already familiar with Resolve's interface—and provides an intuitive way to prototype and understand tool behaviour. However, these setups can quickly become complex and cumbersome in practical workflows.

Next, we’ll move into the Fusion page, where you’ll start working with the underlying maths that power these tools. Fusion offers much greater control, helping you to understand exactly how adjustments affect your image on a technical level. While this step introduces a steeper learning curve, it unlocks significant creative and technical power.

Finally, we’ll step into the world of custom coding using DCTL, writing our tools from scratch in a text editor like Sublime Text. This stage involves translating your mathematical understanding into actual code, allowing you to create fully custom tools that run natively in the Colour page—just like Resolve’s built-in effects.

By the end of the series, you’ll not only have rebuilt some of Resolve’s native tools but also have the skills and insight to design your own. Each stage will introduce new concepts in tool development, giving you a powerful foundation to expand your creative and technical toolkit as a colourist.

The first tool we’ll be developing is one of the most essential and powerful operators in a colourist’s toolkit: Exposure.
Exposure gives us the ability to adjust an image in the same way a cinematographer would on set—by opening or closing the iris to control how much light hits the sensor. As colourists, it’s crucial that some of our tools mirror this real-world behaviour. Doing so allows us to fine-tune shots in a way that aligns with the creative intent captured during production.

Even more importantly, we want to think and work in the same language as a cinematographer. In the case of exposure, that means using stops—a universal unit that describes doubling or halving of light. Our goal is to build a tool that doesn’t just feel like an exposure adjustment but one that responds and reads out in stops, just as a cinematographer would expect. This creates a seamless bridge between set and post, ensuring consistency and clarity throughout the image-mastering process.

Before we start building the tool, let’s first understand what exposure adjustment really means. At its core, an exposure adjustment is simply a gain operation applied within a linear domain. If that sounds technical—don’t worry. We’ll break this down step by step, and by the end, it’ll all make sense.


#1: The Colour Page

We’ll begin by building this concept directly in the Colour page of DaVinci Resolve using a simple three-node setup:

  1. Node 1 – This is a Colour Space Transform (CST) that converts your working colour space to a linear domain. In my case, I’m going from DaVinci Wide Gamut / DaVinci Intermediate to DaVinci Wide Gamut / Linear.

  2. Node 2 – This is where the actual exposure adjustment happens. We’ll use the Gain control here to produce the exposure adjustment.

  3. Node 3 – Another CST that brings us back to our working colour space, converting from DaVinci Wide Gamut / Linear back to DaVinci Wide Gamut / DaVinci Intermediate.

An exposure pipeline in the colour page

What we’ve created here is essentially a linear sandwich—with the gain operation right in the middle. This setup models exactly what we defined earlier: a gain operation that takes place inside a linear domain.


#2: The Fusion Page

Now that we’ve built our exposure tool using Resolve’s native colour page, let’s take it a step further by rebuilding it inside the Fusion page.

This not only gives us more control, but also helps us understand the maths behind exposure adjustments more clearly. Along the way, we’ll also take a quick detour into Desmos—a free online graphing calculator that’s incredibly useful for visualising what a mathematical operation does to a curve. And really, that’s what tool development is all about: manipulating curves.

In Fusion, the beginning and end of the pipeline remain the same. We’ll still use Colour Space Transform nodes to convert from DaVinci Wide Gamut / DaVinci Intermediate to DaVinci Wide Gamut / Linear, and then back again at the end. But this time, instead of using Resolve’s native gain wheel, we’ll write the math ourselves.

We’ll do this using Fusion’s Custom Tool node—a powerful tool that lets us input custom mathematical expressions and hook them up to sliders, just like any standard grading tool.

#2.1: The Maths Behind Exposure (Gain)

Thankfully, the core idea behind exposure is simple: it’s just multiplication. When you adjust gain, you’re multiplying every pixel value by a certain number—nothing more. We can visualise this in Desmos using the formula: x * n. Here, x represents all values —and n is a variable we can adjust to dynamically modify the curve.

The gain formula demonstrated via Desmos

So inside the Custom Tool, head to the Channels tab. You’ll see separate inputs for Red, Green, and Blue—the building blocks of every pixel.

For each channel, we’ll apply the exact same formula:

Red: r1 * n1
Green: g1 * n1
Blue: b1 * n1

This means: take the original red, green, and blue values, and multiply each one by n1, which is the value that corresponds to the first slider in the Custom Tool.

Now, if we set the n1 slider to 2, we’re multiplying every channel of every pixel by 2. Just like that, we’ve created a custom gain operation that works identically to Resolve’s native Gain control—but now we wrote the maths behind it.

However, one of our original goals wasn’t just to build an exposure tool that acts like exposure—but one that also reads like it. Right now, our tool’s default “no effect” state is when the multiplier is set to 1, because multiplying anything by 1 leaves it unchanged.

But here's the problem: if we want each slider step to represent a full stop of exposure (like a cinematographer would think in), we’d need to double the value for every stop increase. That means:

  • 1 stop brighter = n1 = 2

  • 2 stops brighter = n1 = 4

  • 3 stops brighter = n1 = 8
    And so on.

For decreases, we’d need to halve the value:

  • 1 stop darker = n1 = 0.5

  • 2 stops darker = n1 = 0.25

  • 3 stops darker = n1 = 0.125

This works, but it’s not intuitive—because our slider doesn’t reflect the stop-based language we’re aiming for.

So let’s fix that. We can keep the exact same gain operation, but remap the maths so that:

  • The do-nothing state is at n = 0

  • Each step of +1 equals a one-stop increase in exposure

  • Each step of -1 equals a one-stop decrease

To do this, we simply change the formula from x * n to:

x * 2^n

In words: x multiplied by 2 to the power of n. This expression allows us to work directly in stops. At n = 0, the result is just x * 1, so nothing changes. At n = 1, we double the value. At n = -1, we halve it. It’s the same concept as before, just now mapped to stop-based values.

You can visualise this in Desmos by plotting y = x * 2^n and observing how the curve shifts depending on the value of n.

So now we update our Custom Tool formulas to:

Red: r1 * 2^n1
Green: g1 * 2^n1
Blue: b1 * 2^n1

The gain formula applied to each individual colour channel

Now, when we adjust the n1 slider, we’re working in proper photographic stops—with a natural zero point and a meaningful, readable scale. We’ve just built an exposure tool that not only works like the real thing but reads like it too.


#3: Custom Code

The third—and slightly more complex—step is implementing our exposure tool as custom code. For many, this will feel like the biggest leap, as we’re moving away from the familiar environment of Resolve and stepping into the blank canvas of a text editor. But the key difference now is: we already understand the maths behind the tool.
So all we need to do is translate that logic into the correct syntax. Think of it like learning the basics of a new language—you already know what you want to say, you just need to learn how to say it in DCTL.

#3.1 Getting Started with DCTL

If this is the first blog post you’ve come across, I’d recommend checking out an earlier one in the series: “Tool Development: Resolve, Fusion, Custom Code #0 – Introduction.” It dives deeper into the logic and structure of DCTL code and is especially useful if you’re new to this kind of scripting.

That said, here’s a quick recap of the absolute basics:

To write DCTL code, you’ll need a plain text editor. I like Sublime Text—it’s lightweight, easy to use, and offers helpful syntax highlighting that makes your code much easier to read.

Two important things are required for your DCTL to work in Resolve:

  1. The file must have a .dctl extension .dctl extension.

  2. It must include this specific block of starter code below.

__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B){
float3 inRGB = make_float3(p_R, p_G, p_B);
float3 out = inRGB;


return out; 
}

#3.2 Creating the Exposure Slider

To give the user control, we’ll define a slider using the following structure:

DEFINE_UI_PARAMS(variable name, label, DCTLUI_SLIDER_FLOAT, default, min, max, step)

For our tool, we’ll insert our own inputs relevant to our exposure tool. So the full line becomes:

DEFINE_UI_PARAMS(exposure, Exposure, DCTLUI_SLIDER_FLOAT, 0, -10, 10, 0.001)

This gives the user a clear, responsive exposure slider - which is connected to our gain operation via the “exposure” variable. The tool will start at zero so there’s no effect applied on application - and the user has 10 stops of latitude in either direction, with a precise 0.001 step-size.

#3.3 Writing the Core Transform Logic

Now that we’ve set up the basic structure of our DCTL, it’s time to write the three key operations that make up our exposure tool. These go in the space between the start of our main transform function and the final '“return out;”.

Remember, the core of an exposure tool involves:

  1. Converting from DaVinci Intermediate (DI) to Linear

  2. Applying an exposure adjustment (a gain operation in stops)

  3. Converting from Linear back to DI

Let’s walk through each step.

Step 1 – Convert from DI to Linear
This is the code equivalent of our first Colour Space Transform.

We write:

 out = DItoLinear(out);

This tells our tool: take the current image (out) and pass it through the DItoLinear function. It’s exactly like dropping a CST node into your Resolve node tree. At this point, the code doesn’t yet know what DItoLinear means—we’ll define that function shortly.

Step 2 – Apply Gain in Stops
This is the exposure adjustment itself. We write:

out = out * _powf(2, exposure);

This takes the image in its linear state and multiplies every pixel value by 2 to the power of the exposure value. It’s identical in concept to our earlier Fusion formula x * 2^n1, just written in DCTL syntax. _powf(2, exposure) means "2 raised to the power of exposure".

Step 3 – Convert from Linear back to DI
This brings us full circle—like the final CST node in Resolve. We write:

 out = LinearToDI(out);

This will convert our image from linear back into DaVinci Intermediate, so it's ready to be viewed or used in your regular grading pipeline.

So the full block of code now looks like this:

DEFINE_UI_PARAMS(exposure, Exposure, DCTLUI_SLIDER_FLOAT, 0, -10, 10, 0.001)

__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B){
float3 inRGB = make_float3(p_R, p_G, p_B);
float3 out = inRGB;

out = DItoLinear(out);
out = out * _powf(2, exposure);
out = LinearToDI(out);

return out; 
}

Now you’ve replicated your Resolve and Fusion exposure setup in code—a clean three-step process that models photometric exposure accurately and intuitively. A gain operation sandwiched inside a linear domain.

All that’s left is to define two things: The functions for DItoLinear and LinearToDI.

#3.4 Replacing CSTs with Code: Adding Transform Functions

In the Colour page and Fusion, we used Colour Space Transforms (CSTs) to convert to and from DaVinci Intermediate and Linear. In DCTL, we have to manually write those conversions using the official maths from Blackmagic’s white papers. To keep things simple, I’ve provided you with functions that handle this transformation for you.

Here’s the function that converts DaVinci Intermediate to Linear:

__DEVICE__ float3 DItoLinear(float3 in) { 

float DI_A = 0.0075; 
float DI_B = 7.0; 
float DI_C = 0.07329248; 
float DI_M = 10.44426855; 
float DI_LOG_CUT = 0.02740668; 

in.x = in.x > DI_LOG_CUT ? _powf(2, (in.x / DI_C) - DI_B) - DI_A : in.x / DI_M; 
in.y = in.y > DI_LOG_CUT ? _powf(2, (in.y / DI_C) - DI_B) - DI_A : in.y / DI_M; 
in.z = in.z > DI_LOG_CUT ? _powf(2, (in.z / DI_C) - DI_B) - DI_A : in.z / DI_M; 

return in; 

}

In our introductory blogpost - we covered the general syntax for creating functions so I won’t recap this here, however, you can check this link if you would like go over the general syntax of building DCTL functions.

However, what is unique to this code block are the five float values (DI_A, DI_B, etc.), these are constants specific to the DaVinci Intermediate transfer function, taken directly from Blackmagic’s documentation. You don’t need to understand why these numbers work—just know they’re necessary for accurate conversion from DI to Linear. The reason we must define them as floats is because they are decimal point numbers.

Understanding the Formula:

This line:

in.x = in.x > DI_LOG_CUT ? _powf(2, (in.x / DI_C) - DI_B) - DI_A : in.x / DI_M;

...uses a conditional expression. Let’s unpack a few key elements of the syntax that might look unfamiliar at first:

  • ? means “if true, do this…”

  • : means “…otherwise, do this instead.” Together, they form a conditional expression (also called a ternary operator).

So this line of code means:

  • If in.x (a placeholder for our red channel) is greater than DI LOG CUT, we apply formula: 2 ^ (in.x / DI C) - DI B) - DI A).

  • if in.x is not greater, we use simple division: in.x / DI M.

This same calculation is then repeated for the green and blue channels.

Once written, this function acts just like a CST node—you can feed your image through it to get it into the Linear domain from DaVinci Intermediate.

Next Step: Linear to DI Conversion

To complete the roundtrip, we’ll also need a LineartoDI function that does the reverse transformation. It uses a different set of constants and equations, but follows the same structure.

__DEVICE__ float3 LineartoDI(float3 in){

    float DI_A = 0.0075f;
    float DI_B = 7.0f;
    float DI_C = 0.07329248f;
    float DI_M = 10.44426855f;
    float DI_LIN_CUT = 0.00262409f;

    in.x = in.x > DI_LIN_CUT ? (_log2f(in.x + DI_A) + DI_B) * DI_C : in.x * DI_M;
    in.y = in.y > DI_LIN_CUT ? (_log2f(in.y + DI_A) + DI_B) * DI_C : in.y * DI_M;
    in.z = in.z > DI_LIN_CUT ? (_log2f(in.z + DI_A) + DI_B) * DI_C : in.z * DI_M;

    return in;
}

And again—don’t stress about the maths behind the constants. Blackmagic’s engineers have already done the hard work. You just need to reuse the function whenever you need to convert between these spaces.

And with our code-based CSTs included, that’s now our custom Exposure Tool complete. The entire code block should look like this:

__DEVICE__ float3 DItoLinear(float3 in) {

    float DI_A = 0.0075;
    float DI_B = 7.0;
    float DI_C = 0.07329248;
    float DI_M = 10.44426855;
    float DI_LOG_CUT = 0.02740668;

    in = in.x > DI_LOG_CUT ? _powf(2, (in.x / DI_C) - DI_B) - DI_A : in.x / DI_M; 
    in = in.y > DI_LOG_CUT ? _powf(2, (in.y / DI_C) - DI_B) - DI_A : in.y / DI_M; 
    in = in.z > DI_LOG_CUT ? _powf(2, (in.z / DI_C) - DI_B) - DI_A : in.z / DI_M;

    return in; 
}

__DEVICE__ float3 LineartoDI(float3 in){

    float DI_A = 0.0075f;
    float DI_B = 7.0f;
    float DI_C = 0.07329248f;
    float DI_M = 10.44426855f;
    float DI_LIN_CUT = 0.00262409f;

    in.x = in.x > DI_LIN_CUT ? (_log2f(in.x + DI_A) + DI_B) * DI_C : in.x * DI_M;
    in.y = in.y > DI_LIN_CUT ? (_log2f(in.y + DI_A) + DI_B) * DI_C : in.y * DI_M;
    in.z = in.z > DI_LIN_CUT ? (_log2f(in.z + DI_A) + DI_B) * DI_C : in.z * DI_M;

    return in;
}

DEFINE_UI_PARAMS(exposure_factor, Exposure, DCTLUI_SLIDER_FLOAT, 0, -10, 10, 0.001)

__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B) {
    float3 inRGB = make_float3(p_R, p_G, p_B);
    float3 out = inRGB;

    out = DItoLinear(out);
    out = out * _powf(2, exposure_factor);
    out = LineartoDI(out);
    
    return out;
}

Now all that’s left to do is save your file and install the DCTL into Resolve. If you’re unfamiliar with the installation process, I’ve included a link to the manual for my tools here, which walks you through everything you need to get up and running.

Once installed, you’ll have a custom-built photometric exposure tool ready to use—one that not only works like real-world exposure, but also reads like it too. A tool you built from the ground up.

Wrapping Up: The Full Exposure Tool in Three Forms

So now, we’ve completed our three-step journey to building a fully functioning exposure adjustment tool:

  1. In the Colour page using CSTs and the gain wheel.

  2. In the Fusion page using a Custom Tool and some basic maths.

  3. In DCTL code, writing everything from scratch to produce a reusable, professional-grade tool.

At this point, you’ve not only created a working exposure control, but you’ve also taken your first real steps into custom tool development—a powerful and rewarding part of colour grading that’s often hidden beneath the surface.

Hopefully, this tutorial has helped demystify the fundamentals of exposure and made the initial learning curve a little less steep. In our next blog post, we’ll take things a step further by building a custom RGB mixer tool. This will introduce slightly more complex operations as we create crosstalk between colour channels—but with that added complexity comes powerful, creative control over how we shape and stylise our images.

Next
Next

Tool Development: Resolve, Fusion, Custom Code #0: Introduction