50 lines
1.8 KiB
Swift
50 lines
1.8 KiB
Swift
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)
|
|
}
|
|
}
|