ODIX Tool Suite · 001
DCTL · DaVinci Resolve

Skin
Anchor

Locks skin tone hue to a consistent target angle across shots. Eliminates the drift that makes multicam grades look inconsistent — without touching anything outside the skin hue range.

Version
v1.0
Type
Hue Control
Format
.dctl
Price
Free

Hue anchoring for skin tones

Skin tones occupy a narrow hue band — roughly 15–35° on the vectorscope. Across shots, camera differences, lighting changes, and white balance inconsistencies cause that hue to drift. The drift is subtle enough that you don't notice it per shot, but obvious when you see them cut together.

Skin Anchor defines where skin should sit (Target Hue), where it currently is (Source Hue Center), and rotates only the pixels within that hue range to close the gap. Everything outside that range is untouched.

Parameter Range / Default Description
Target Hue 0–60° · Default 24 The hue angle you want skin tones anchored to. 24° is a common neutral. Adjust per project.
Source Hue Center 0–60° · Default 24 The current skin hue in your footage. Read from the vectorscope. Match this to your source shot.
Hue Range 5–90° · Default 28 Width of the selection around Source Hue Center. Wider catches more tones but risks color bleed.
Strength 0–1 · Default 1.0 Master blend. 1.0 = full correction. Reduce if the shift feels too strong for a particular shot.
Saturation Floor 0–0.4 · Default 0.06 Minimum saturation for a pixel to be affected. Excludes near-neutral grays and whites from correction.
Falloff Softness 0.05–1.0 · Default 0.35 Controls how gradually the selection fades at the hue range boundary. Higher = softer edges.
ODIX_SkinAnchor_v1.dctl — Full Source
↓ Download .dctl
// ═══════════════════════════════════════════════════════════════
// ODIX Skin Anchor v1.0
// Locks skin tone hue to a consistent target angle
//
// USAGE:
//   Place in a serial or parallel node after primary correction.
//   Set Source Hue Center to match your footage's current skin hue
//   (read it from the vectorscope — skin tone line is ~25°).
//   Set Target Hue to where you want it anchored.
//   Increase Range if the tool isn't catching all skin tones.
//   Combine with a Qualifier for surgical precision.
//
// COLOR SPACE:
//   Works in any color space. Best in a balanced log or
//   normalized scene-linear node before output LUT.
//
// by Odbat Batsaikhan — odixbat.com — @odixbat
// ═══════════════════════════════════════════════════════════════

DEFINE_UI_PARAMS(p_TargetHue,   Target Hue,           DCTLUI_SLIDER_FLOAT, 24.0,  0.0,  60.0, 0.5)
DEFINE_UI_PARAMS(p_SkinCenter,  Source Hue Center,     DCTLUI_SLIDER_FLOAT, 24.0,  0.0,  60.0, 0.5)
DEFINE_UI_PARAMS(p_Range,       Hue Range,             DCTLUI_SLIDER_FLOAT, 28.0,  5.0,  90.0, 0.5)
DEFINE_UI_PARAMS(p_Strength,    Strength,              DCTLUI_SLIDER_FLOAT,  1.0,  0.0,   1.0, 0.01)
DEFINE_UI_PARAMS(p_SatMin,      Saturation Floor,      DCTLUI_SLIDER_FLOAT,  0.06, 0.0,   0.4, 0.005)
DEFINE_UI_PARAMS(p_FalloffSoft, Falloff Softness,      DCTLUI_SLIDER_FLOAT,  0.35, 0.05,  1.0, 0.01)

__DEVICE__ float3 RGBtoHSL(float r, float g, float b) {
    float maxC  = _fmaxf(r, _fmaxf(g, b));
    float minC  = _fminf(r, _fminf(g, b));
    float delta = maxC - minC;
    float l     = (maxC + minC) * 0.5f;
    float s     = 0.0f;
    float h     = 0.0f;

    if (delta > 0.00001f) {
        float denom = 1.0f - _fabs(2.0f * l - 1.0f);
        s = (denom > 0.00001f) ? delta / denom : 0.0f;

        if      (maxC == r) h = 60.0f * _fmod((g - b) / delta, 6.0f);
        else if (maxC == g) h = 60.0f * ((b - r) / delta + 2.0f);
        else                h = 60.0f * ((r - g) / delta + 4.0f);

        if (h < 0.0f) h += 360.0f;
    }
    return make_float3(h, s, l);
}

__DEVICE__ float3 HSLtoRGB(float h, float s, float l) {
    float c  = (1.0f - _fabs(2.0f * l - 1.0f)) * s;
    float x  = c * (1.0f - _fabs(_fmod(h / 60.0f, 2.0f) - 1.0f));
    float m  = l - c * 0.5f;
    float r  = 0.0f, g = 0.0f, b = 0.0f;

    if      (h < 60.0f)  { r = c; g = x; b = 0.0f; }
    else if (h < 120.0f) { r = x; g = c; b = 0.0f; }
    else if (h < 180.0f) { r = 0.0f; g = c; b = x;  }
    else if (h < 240.0f) { r = 0.0f; g = x; b = c;  }
    else if (h < 300.0f) { r = x; g = 0.0f; b = c;  }
    else                  { r = c; g = 0.0f; b = x;  }

    return make_float3(r + m, g + m, b + m);
}

__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 hsl = RGBtoHSL(p_R, p_G, p_B);
    float  h   = hsl.x, s = hsl.y, l = hsl.z;

    float diff = _fabs(h - p_SkinCenter);
    if (diff > 180.0f) diff = 360.0f - diff;

    float hardEdge   = p_Range * 0.5f;
    float softRegion = hardEdge * p_FalloffSoft;
    float rangeMask  = 1.0f - _clampf((diff - (hardEdge - softRegion)) / (softRegion + 0.001f), 0.0f, 1.0f);
    float satMask    = _clampf((s - p_SatMin) / (_fmaxf(p_SatMin, 0.02f)), 0.0f, 1.0f);
    float mask       = rangeMask * satMask * p_Strength;

    float hueShift = p_TargetHue - p_SkinCenter;
    float newH     = h + hueShift * mask;
    if (newH <    0.0f) newH += 360.0f;
    if (newH >= 360.0f) newH -= 360.0f;

    return HSLtoRGB(newH, s, l);
}