97 lines
2.6 KiB
Swift
97 lines
2.6 KiB
Swift
|
|
import Foundation
|
||
|
|
import ForgeColor
|
||
|
|
|
||
|
|
/// Named input transform (camera log -> linear Rec709 working space).
|
||
|
|
public enum InputTransform: String, Sendable, Codable, CaseIterable {
|
||
|
|
case arriLogC4AWG4
|
||
|
|
case redLog3G10RWG
|
||
|
|
case sonySLog3SGamut3Cine
|
||
|
|
case none
|
||
|
|
|
||
|
|
public var colorSpace: ColorSpace {
|
||
|
|
switch self {
|
||
|
|
case .arriLogC4AWG4: return .arriLogC4AWG4
|
||
|
|
case .redLog3G10RWG: return .redLog3G10RWG
|
||
|
|
case .sonySLog3SGamut3Cine: return .sonySLog3SGamut3Cine
|
||
|
|
case .none: return .rec709
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Named output transform (linear working space -> display).
|
||
|
|
public enum OutputTransform: String, Sendable, Codable, CaseIterable {
|
||
|
|
case rec709Display // 2.4 gamma
|
||
|
|
case none
|
||
|
|
|
||
|
|
public func apply(_ rgb: SIMD3<Float>) -> SIMD3<Float> {
|
||
|
|
switch self {
|
||
|
|
case .none:
|
||
|
|
return rgb
|
||
|
|
case .rec709Display:
|
||
|
|
let g: Float = 1 / 2.4
|
||
|
|
return SIMD3(
|
||
|
|
pow(max(rgb.x, 0), g),
|
||
|
|
pow(max(rgb.y, 0), g),
|
||
|
|
pow(max(rgb.z, 0), g))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// One node in a grade stack.
|
||
|
|
public struct GradeNode: Identifiable, Sendable {
|
||
|
|
public enum Kind: Sendable {
|
||
|
|
case inputTransform(InputTransform)
|
||
|
|
case cdl(CDL)
|
||
|
|
case saturation(Float)
|
||
|
|
case curves(CurveSet)
|
||
|
|
case lut3d(Lut3D)
|
||
|
|
case outputTransform(OutputTransform)
|
||
|
|
}
|
||
|
|
|
||
|
|
public let id: UUID
|
||
|
|
public var kind: Kind
|
||
|
|
public var isEnabled: Bool
|
||
|
|
|
||
|
|
public init(id: UUID = UUID(), kind: Kind, isEnabled: Bool = true) {
|
||
|
|
self.id = id
|
||
|
|
self.kind = kind
|
||
|
|
self.isEnabled = isEnabled
|
||
|
|
}
|
||
|
|
|
||
|
|
public func apply(_ rgb: SIMD3<Float>) -> SIMD3<Float> {
|
||
|
|
guard isEnabled else { return rgb }
|
||
|
|
switch kind {
|
||
|
|
case .inputTransform(let t):
|
||
|
|
return t.colorSpace.toLinearRec709(rgb)
|
||
|
|
case .cdl(let cdl):
|
||
|
|
return cdl.apply(rgb)
|
||
|
|
case .saturation(let sat):
|
||
|
|
let luma = (rgb * CDL.lumaWeights).sum()
|
||
|
|
return SIMD3(repeating: luma) + (rgb - SIMD3(repeating: luma)) * sat
|
||
|
|
case .curves(let set):
|
||
|
|
return set.apply(rgb)
|
||
|
|
case .lut3d(let lut):
|
||
|
|
return lut.sample(rgb)
|
||
|
|
case .outputTransform(let t):
|
||
|
|
return t.apply(rgb)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ordered node stack. Evaluation composes nodes top to bottom.
|
||
|
|
public struct GradeStack: Sendable {
|
||
|
|
public var nodes: [GradeNode]
|
||
|
|
|
||
|
|
public init(nodes: [GradeNode] = []) {
|
||
|
|
self.nodes = nodes
|
||
|
|
}
|
||
|
|
|
||
|
|
public func evaluate(_ rgb: SIMD3<Float>) -> SIMD3<Float> {
|
||
|
|
var v = rgb
|
||
|
|
for node in nodes {
|
||
|
|
v = node.apply(v)
|
||
|
|
}
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
}
|