rainbow-dragon/Sources/ForgeColor/CDL.swift

46 lines
1.6 KiB
Swift

import Foundation
/// ASC CDL primary correction: out = clamp0(in * slope + offset)^power, then saturation.
/// Saturation uses Rec709 luma weights per ASC CDL 1.2.
public struct CDL: Equatable, Sendable {
public var slope: SIMD3<Float>
public var offset: SIMD3<Float>
public var power: SIMD3<Float>
public var saturation: Float
public init(slope: SIMD3<Float>, offset: SIMD3<Float>, power: SIMD3<Float>, saturation: Float) {
self.slope = slope
self.offset = offset
self.power = power
self.saturation = saturation
}
public static let identity = CDL(slope: .one, offset: .zero, power: .one, saturation: 1)
public var isIdentity: Bool { self == .identity }
/// Rec709 luma weights (ASC CDL saturation spec).
public static let lumaWeights = SIMD3<Float>(0.2126, 0.7152, 0.0722)
public func apply(_ rgb: SIMD3<Float>) -> SIMD3<Float> {
// SOP, clamped to >=0 before power for determinism (ASC pre-power clamp).
let sop = rgb * slope + offset
let zero = SIMD3<Float>.zero
let clamped = sop.replacing(with: zero, where: sop .< zero)
var out = SIMD3<Float>(
pow(clamped.x, power.x),
pow(clamped.y, power.y),
pow(clamped.z, power.z))
// Saturation about Rec709 luma.
if saturation != 1 {
let luma = (out * Self.lumaWeights).sum()
out = SIMD3(repeating: luma) + (out - SIMD3(repeating: luma)) * saturation
}
return out
}
}
extension SIMD3 where Scalar == Float {
public static var one: SIMD3<Float> { SIMD3(1, 1, 1) }
}