rainbow-dragon/Sources/ForgeGrade/Flattener.swift

51 lines
1.8 KiB
Swift
Raw Normal View History

import Foundation
import ForgeColor
/// Flattened look ready for camera push: optional native CDL + baked 3D LUT.
/// When cdl is present, camera applies CDL first, then LUT (split mode).
public struct FlattenedLook: Sendable {
public var cdl: CDL?
public var lut: Lut3D?
public var latticeSize: Int
public init(cdl: CDL?, lut: Lut3D?, latticeSize: Int) {
self.cdl = cdl
self.lut = lut
self.latticeSize = latticeSize
}
}
public enum Flattener {
/// Bake a grade stack to a single 3D LUT over the given lattice.
/// splitLeadingCDL: if stack's first enabled-relevant node is a CDL, keep it
/// native (fast camera trim) and bake only the remainder. Requires the camera
/// to apply CDL before LUT. LUT input domain matches whatever the stack input
/// domain is (camera log signal in production use).
public static func flatten(stack: GradeStack, latticeSize: Int, splitLeadingCDL: Bool) -> FlattenedLook {
var nodes = stack.nodes
var splitCDL: CDL?
if splitLeadingCDL,
let first = nodes.first,
first.isEnabled,
case .cdl(let cdl) = first.kind {
splitCDL = cdl
nodes.removeFirst()
}
let remainder = GradeStack(nodes: nodes)
var lut: Lut3D
if let cdl = splitCDL {
// LUT domain = post-CDL signal. Camera applies CDL then LUT, so bake
// remainder over the *raw* lattice but pre-invert nothing: lattice point p
// represents CDL output. remainder(p) is exactly what LUT must produce.
lut = Lut3D.build(size: latticeSize) { remainder.evaluate($0) }
_ = cdl
} else {
lut = Lut3D.build(size: latticeSize) { remainder.evaluate($0) }
}
return FlattenedLook(cdl: splitCDL, lut: lut, latticeSize: latticeSize)
}
}