108 lines
4.2 KiB
Swift
108 lines
4.2 KiB
Swift
|
|
import XCTest
|
||
|
|
import ForgeColor
|
||
|
|
@testable import ForgeGrade
|
||
|
|
|
||
|
|
final class GradeStackTests: XCTestCase {
|
||
|
|
|
||
|
|
// Empty stack = identity.
|
||
|
|
func testEmptyStackIdentity() {
|
||
|
|
let stack = GradeStack(nodes: [])
|
||
|
|
let px = SIMD3<Float>(0.3, 0.5, 0.7)
|
||
|
|
XCTAssertEqual(stack.evaluate(px), px)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Single CDL node applies CDL math.
|
||
|
|
func testSingleCDLNode() {
|
||
|
|
let cdl = CDL(slope: SIMD3(2, 2, 2), offset: .zero, power: .one, saturation: 1)
|
||
|
|
let stack = GradeStack(nodes: [GradeNode(kind: .cdl(cdl))])
|
||
|
|
let out = stack.evaluate(SIMD3(0.25, 0.25, 0.25))
|
||
|
|
XCTAssertEqual(out.x, 0.5, accuracy: 1e-6)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ordering matters: CDL(x2) then LUT(half) != LUT(half) then CDL(x2) on clamping LUT.
|
||
|
|
func testOrderingMatters() {
|
||
|
|
let double = CDL(slope: SIMD3(2, 2, 2), offset: .zero, power: .one, saturation: 1)
|
||
|
|
// LUT clamps at 1: input 0.8 doubled -> 1.6 -> LUT sample clamps to 1 -> 0.5
|
||
|
|
let halfLut = Lut3D.build(size: 17) { $0 * 0.5 }
|
||
|
|
|
||
|
|
let cdlFirst = GradeStack(nodes: [
|
||
|
|
GradeNode(kind: .cdl(double)),
|
||
|
|
GradeNode(kind: .lut3d(halfLut)),
|
||
|
|
])
|
||
|
|
let lutFirst = GradeStack(nodes: [
|
||
|
|
GradeNode(kind: .lut3d(halfLut)),
|
||
|
|
GradeNode(kind: .cdl(double)),
|
||
|
|
])
|
||
|
|
let px = SIMD3<Float>(0.8, 0.8, 0.8)
|
||
|
|
let a = cdlFirst.evaluate(px) // 0.8*2=1.6 clamp-> lut(1.0)=0.5
|
||
|
|
let b = lutFirst.evaluate(px) // lut(0.8)=0.4 -> *2 = 0.8
|
||
|
|
XCTAssertEqual(a.x, 0.5, accuracy: 1e-4)
|
||
|
|
XCTAssertEqual(b.x, 0.8, accuracy: 1e-4)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bypassed node skipped.
|
||
|
|
func testBypassSkipsNode() {
|
||
|
|
let double = CDL(slope: SIMD3(2, 2, 2), offset: .zero, power: .one, saturation: 1)
|
||
|
|
var node = GradeNode(kind: .cdl(double))
|
||
|
|
node.isEnabled = false
|
||
|
|
let stack = GradeStack(nodes: [node])
|
||
|
|
let px = SIMD3<Float>(0.25, 0.25, 0.25)
|
||
|
|
XCTAssertEqual(stack.evaluate(px), px)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Saturation node.
|
||
|
|
func testSaturationNode() {
|
||
|
|
let stack = GradeStack(nodes: [GradeNode(kind: .saturation(0))])
|
||
|
|
let out = stack.evaluate(SIMD3(1, 0, 0))
|
||
|
|
XCTAssertEqual(out.x, 0.2126, accuracy: 1e-5)
|
||
|
|
XCTAssertEqual(out.y, 0.2126, accuracy: 1e-5)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Curves node.
|
||
|
|
func testCurvesNode() {
|
||
|
|
var set = CurveSet.identity
|
||
|
|
set.master = Curve(points: [.init(x: 0, y: 0), .init(x: 1, y: 0.5)])
|
||
|
|
let stack = GradeStack(nodes: [GradeNode(kind: .curves(set))])
|
||
|
|
let out = stack.evaluate(SIMD3(1, 1, 1))
|
||
|
|
XCTAssertEqual(out.x, 0.5, accuracy: 1e-5)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Input transform node decodes camera log to linear Rec709.
|
||
|
|
func testInputTransformNode() {
|
||
|
|
let cs = ColorSpace.arriLogC4AWG4
|
||
|
|
let encodedGray = SIMD3<Float>(repeating: cs.transfer.encode(0.18))
|
||
|
|
let stack = GradeStack(nodes: [GradeNode(kind: .inputTransform(.arriLogC4AWG4))])
|
||
|
|
let out = stack.evaluate(encodedGray)
|
||
|
|
XCTAssertEqual(out.x, 0.18, accuracy: 0.002)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Output transform node: linear -> Rec709 display (2.4 gamma).
|
||
|
|
func testOutputTransformNode() {
|
||
|
|
let stack = GradeStack(nodes: [GradeNode(kind: .outputTransform(.rec709Display))])
|
||
|
|
let out = stack.evaluate(SIMD3(0.18, 0.18, 0.18))
|
||
|
|
XCTAssertEqual(out.x, pow(0.18, 1 / 2.4), accuracy: 1e-4)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Full stack on known vector: input LogC4 -> CDL gain -> output 2.4 gamma.
|
||
|
|
func testFullStackKnownVector() {
|
||
|
|
let cs = ColorSpace.arriLogC4AWG4
|
||
|
|
let gain = CDL(slope: SIMD3(2, 2, 2), offset: .zero, power: .one, saturation: 1)
|
||
|
|
let stack = GradeStack(nodes: [
|
||
|
|
GradeNode(kind: .inputTransform(.arriLogC4AWG4)),
|
||
|
|
GradeNode(kind: .cdl(gain)),
|
||
|
|
GradeNode(kind: .outputTransform(.rec709Display)),
|
||
|
|
])
|
||
|
|
let encodedGray = SIMD3<Float>(repeating: cs.transfer.encode(0.18))
|
||
|
|
let out = stack.evaluate(encodedGray)
|
||
|
|
let expected = pow(0.36, 1 / 2.4) // 0.18 * 2 -> display gamma
|
||
|
|
XCTAssertEqual(out.x, Float(expected), accuracy: 0.003)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Node IDs unique + stable.
|
||
|
|
func testNodeIDsUnique() {
|
||
|
|
let a = GradeNode(kind: .saturation(1))
|
||
|
|
let b = GradeNode(kind: .saturation(1))
|
||
|
|
XCTAssertNotEqual(a.id, b.id)
|
||
|
|
}
|
||
|
|
}
|