diff --git a/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift b/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift index 234cdfd7..3dc471f2 100755 --- a/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift +++ b/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift @@ -190,7 +190,16 @@ let filterOperations: Array = [ }, filterOperationType:.singleInput ), -// TODO : Tone curve + FilterOperation( + filter:{ToneCurveFilter()}, + listName:"Tone curve", + titleName:"Tone curve", + sliderConfiguration:.enabled(minimumValue:0.0, maximumValue:1.00, initialValue:0.5), + sliderUpdateCallback: {(filter, sliderValue) in + filter.blueControlPoints = [Position(0, 0), Position(0.5, sliderValue), Position(1, 1)] + }, + filterOperationType:.singleInput + ), FilterOperation( filter:{HighlightsAndShadows()}, listName:"Highlights and shadows", diff --git a/framework/GPUImage.xcodeproj/project.pbxproj b/framework/GPUImage.xcodeproj/project.pbxproj index 9ff93151..b14cf58c 100755 --- a/framework/GPUImage.xcodeproj/project.pbxproj +++ b/framework/GPUImage.xcodeproj/project.pbxproj @@ -350,6 +350,10 @@ BCFF46FC1CBAF85000A0C521 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFF46FB1CBAF85000A0C521 /* TransformOperation.swift */; }; BCFF46FE1CBB0C1F00A0C521 /* AverageColorExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFF46FD1CBB0C1F00A0C521 /* AverageColorExtractor.swift */; }; BCFF47081CBB443B00A0C521 /* CameraConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFF47071CBB443B00A0C521 /* CameraConversion.swift */; }; + E9E37505215920AF00CA9F00 /* ToneCurveFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E37504215920AF00CA9F00 /* ToneCurveFilter.swift */; }; + E9E37506215920AF00CA9F00 /* ToneCurveFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E37504215920AF00CA9F00 /* ToneCurveFilter.swift */; }; + E9E37517215923D200CA9F00 /* ACVFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E37516215923D200CA9F00 /* ACVFile.swift */; }; + E9E37518215923D200CA9F00 /* ACVFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E37516215923D200CA9F00 /* ACVFile.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -686,6 +690,9 @@ BCFF46FF1CBB0D8900A0C521 /* AverageColor_GL.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; name = AverageColor_GL.fsh; path = Source/Operations/Shaders/AverageColor_GL.fsh; sourceTree = ""; }; BCFF47001CBB0D8900A0C521 /* AverageColor.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; name = AverageColor.vsh; path = Source/Operations/Shaders/AverageColor.vsh; sourceTree = ""; }; BCFF47071CBB443B00A0C521 /* CameraConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CameraConversion.swift; path = Source/CameraConversion.swift; sourceTree = ""; }; + E9E37504215920AF00CA9F00 /* ToneCurveFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ToneCurveFilter.swift; path = Source/Operations/ToneCurveFilter.swift; sourceTree = ""; }; + E9E37507215920E000CA9F00 /* ToneCurve_GL.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; name = ToneCurve_GL.fsh; path = Source/Operations/Shaders/ToneCurve_GL.fsh; sourceTree = ""; }; + E9E37516215923D200CA9F00 /* ACVFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ACVFile.swift; path = Source/Operations/ACVFile.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -783,6 +790,9 @@ BC4C85F31C9F114600FD95D8 /* Color processing */ = { isa = PBXGroup; children = ( + E9E37516215923D200CA9F00 /* ACVFile.swift */, + E9E37504215920AF00CA9F00 /* ToneCurveFilter.swift */, + E9E37507215920E000CA9F00 /* ToneCurve_GL.fsh */, BC4C85F11C9F05EB00FD95D8 /* Luminance.swift */, BC4C85F01C9F054100FD95D8 /* Luminance_GL.fsh */, BC7FD0851CA62E1100037949 /* BrightnessAdjustment.swift */, @@ -1404,6 +1414,7 @@ BCFF46E21CBADB3E00A0C521 /* SingleComponentGaussianBlur.swift in Sources */, BC7FD0F71CB0620E00037949 /* ChromaKeyBlend.swift in Sources */, BC7FD0861CA62E1100037949 /* BrightnessAdjustment.swift in Sources */, + E9E37517215923D200CA9F00 /* ACVFile.swift in Sources */, BC7FD1631CB17C8D00037949 /* ImageOrientation.swift in Sources */, BC9673411C8B897100FB64C2 /* FramebufferCache.swift in Sources */, BC7FD0BB1CA7799B00037949 /* Halftone.swift in Sources */, @@ -1488,6 +1499,7 @@ BC7FD0F31CB0612700037949 /* AddBlend.swift in Sources */, BC7FD1931CB1D38500037949 /* Size.swift in Sources */, BC4EE1741CB3711600AD8A65 /* GaussianBlur.swift in Sources */, + E9E37505215920AF00CA9F00 /* ToneCurveFilter.swift in Sources */, BC7FD0A21CA6316600037949 /* Haze.swift in Sources */, BCB279EA1C8D0E800013E213 /* Camera.swift in Sources */, BC4C85FA1C9F169D00FD95D8 /* SaturationAdjustment.swift in Sources */, @@ -1587,6 +1599,7 @@ BC9E35871E52571D00B8604F /* PrewittEdgeDetection.swift in Sources */, BC9E35551E52522200B8604F /* ConvertedShaders_GLES.swift in Sources */, BC9E35D21E52580700B8604F /* SoftLightBlend.swift in Sources */, + E9E37518215923D200CA9F00 /* ACVFile.swift in Sources */, BC9E353C1E524D8400B8604F /* Color.swift in Sources */, BC9E357B1E5256F500B8604F /* AverageColorExtractor.swift in Sources */, BC9E35701E5256D200B8604F /* LuminanceRangeReduction.swift in Sources */, @@ -1671,6 +1684,7 @@ BC9E35621E5256A500B8604F /* OperationGroup.swift in Sources */, BC9E35381E524D7E00B8604F /* OpenGLRendering.swift in Sources */, BC9E35B41E5257A900B8604F /* ToonFilter.swift in Sources */, + E9E37506215920AF00CA9F00 /* ToneCurveFilter.swift in Sources */, BC9E354F1E52508A00B8604F /* RawDataInput.swift in Sources */, BC9E35681E5256BD00B8604F /* GammaAdjustment.swift in Sources */, BC9E35A81E52578400B8604F /* Vignette.swift in Sources */, diff --git a/framework/Source/Operations/ACVFile.swift b/framework/Source/Operations/ACVFile.swift new file mode 100644 index 00000000..dcd057fa --- /dev/null +++ b/framework/Source/Operations/ACVFile.swift @@ -0,0 +1,69 @@ +import Foundation + +public class ACVFile { + fileprivate var version: UInt16 + fileprivate var curvesCount: UInt16 + + public var redControlPoints: [Position] + public var greenControlPoints: [Position] + public var blueControlPoints: [Position] + public var rgbCompositeControlPoints: [Position] + + public convenience init?(fileName: String) throws { + guard let url = Bundle.main.url(forResource: fileName, withExtension: "acv") else { + return nil + } + let data = try Data(contentsOf: url) + self.init(data: data) + } + + public init?(data: Data) { + guard data.count > 0 else { return nil } + let stepSize = 2 + + var offset = 0 + self.version = data.scanValue(start: offset, length: stepSize) + offset += stepSize + + self.curvesCount = data.scanValue(start: offset, length: stepSize) + offset += stepSize + + let pointRate: Float = 1.0 / 255 + + var curves: [[Position]] = [] + + for _ in 0 ..< curvesCount { + let pointCount = data.scanValue(start: offset, length: stepSize) + offset += stepSize + + var points = [Position]() + + for _ in 0 ..< pointCount { + let y = data.scanValue(start: offset, length: stepSize) + offset += stepSize + let x = data.scanValue(start: offset, length: stepSize) + offset += stepSize + + points.append(Position(Float(x) * pointRate, Float(y) * pointRate)) + } + curves.append(points) + } + self.rgbCompositeControlPoints = curves[0] + self.redControlPoints = curves[1] + self.greenControlPoints = curves[2] + self.blueControlPoints = curves[3] + } +} + +extension Data { + fileprivate func scanValue(start: Int, length: Int) -> UInt16 { + var bytes = [UInt8](repeating: 0, count: count) + copyBytes(to: &bytes, count: count) + + let slice = Array(bytes[start ..< start + length]) + let u16raw = UnsafePointer(slice).withMemoryRebound(to: UInt16.self, capacity: length) { $0.pointee } + let u16 = CFSwapInt16BigToHost(u16raw) + + return u16 + } +} diff --git a/framework/Source/Operations/Shaders/ConvertedShaders_GL.swift b/framework/Source/Operations/Shaders/ConvertedShaders_GL.swift index 7eb18a5e..a1b29832 100644 --- a/framework/Source/Operations/Shaders/ConvertedShaders_GL.swift +++ b/framework/Source/Operations/Shaders/ConvertedShaders_GL.swift @@ -129,6 +129,7 @@ public let ThresholdEdgeDetectionFragmentShader = "varying vec2 textureCoordinat public let ThresholdSketchFragmentShader = "varying vec2 textureCoordinate;\n varying vec2 leftTextureCoordinate;\n varying vec2 rightTextureCoordinate;\n \n varying vec2 topTextureCoordinate;\n varying vec2 topLeftTextureCoordinate;\n varying vec2 topRightTextureCoordinate;\n \n varying vec2 bottomTextureCoordinate;\n varying vec2 bottomLeftTextureCoordinate;\n varying vec2 bottomRightTextureCoordinate;\n \n uniform sampler2D inputImageTexture;\n uniform float threshold;\n \n uniform float edgeStrength;\n \n void main()\n {\n float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;\n float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;\n float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;\n float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;\n float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;\n float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;\n float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;\n float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;\n float h = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;\n h = max(0.0, h);\n float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;\n v = max(0.0, v);\n \n float mag = length(vec2(h, v)) * edgeStrength;\n mag = 1.0 - step(threshold, mag);\n \n gl_FragColor = vec4(vec3(mag), 1.0);\n }\n " public let ThresholdedNonMaximumSuppressionFragmentShader = "uniform sampler2D inputImageTexture;\n \n varying vec2 textureCoordinate;\n varying vec2 leftTextureCoordinate;\n varying vec2 rightTextureCoordinate;\n \n varying vec2 topTextureCoordinate;\n varying vec2 topLeftTextureCoordinate;\n varying vec2 topRightTextureCoordinate;\n \n varying vec2 bottomTextureCoordinate;\n varying vec2 bottomLeftTextureCoordinate;\n varying vec2 bottomRightTextureCoordinate;\n \n uniform float threshold;\n \n void main()\n {\n float bottomColor = texture2D(inputImageTexture, bottomTextureCoordinate).r;\n float bottomLeftColor = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;\n float bottomRightColor = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;\n vec4 centerColor = texture2D(inputImageTexture, textureCoordinate);\n float leftColor = texture2D(inputImageTexture, leftTextureCoordinate).r;\n float rightColor = texture2D(inputImageTexture, rightTextureCoordinate).r;\n float topColor = texture2D(inputImageTexture, topTextureCoordinate).r;\n float topRightColor = texture2D(inputImageTexture, topRightTextureCoordinate).r;\n float topLeftColor = texture2D(inputImageTexture, topLeftTextureCoordinate).r;\n \n // Use a tiebreaker for pixels to the left and immediately above this one\n float multiplier = 1.0 - step(centerColor.r, topColor);\n multiplier = multiplier * (1.0 - step(centerColor.r, topLeftColor));\n multiplier = multiplier * (1.0 - step(centerColor.r, leftColor));\n multiplier = multiplier * (1.0 - step(centerColor.r, bottomLeftColor));\n \n float maxValue = max(centerColor.r, bottomColor);\n maxValue = max(maxValue, bottomRightColor);\n maxValue = max(maxValue, rightColor);\n maxValue = max(maxValue, topRightColor);\n \n float finalValue = centerColor.r * step(maxValue, centerColor.r) * multiplier;\n finalValue = step(threshold, finalValue);\n \n gl_FragColor = vec4(finalValue, finalValue, finalValue, 1.0);\n //\n // gl_FragColor = vec4((centerColor.rgb * step(maxValue, step(threshold, centerColor.r)) * multiplier), 1.0);\n }\n " public let TiltShiftFragmentShader = "varying vec2 textureCoordinate;\n varying vec2 textureCoordinate2;\n \n uniform sampler2D inputImageTexture;\n uniform sampler2D inputImageTexture2;\n \n uniform float topFocusLevel;\n uniform float bottomFocusLevel;\n uniform float focusFallOffRate;\n \n void main()\n {\n vec4 sharpImageColor = texture2D(inputImageTexture, textureCoordinate);\n vec4 blurredImageColor = texture2D(inputImageTexture2, textureCoordinate2);\n \n float blurIntensity = 1.0 - smoothstep(topFocusLevel - focusFallOffRate, topFocusLevel, textureCoordinate2.y);\n blurIntensity += smoothstep(bottomFocusLevel, bottomFocusLevel + focusFallOffRate, textureCoordinate2.y);\n \n gl_FragColor = mix(sharpImageColor, blurredImageColor, blurIntensity);\n }\n " +public let ToneCurveFragmentShader = "varying vec2 textureCoordinate;\n uniform sampler2D inputImageTexture;\n uniform sampler2D toneCurveTexture;\n \n void main()\n {\n vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n float redCurveValue = texture2D(toneCurveTexture, vec2(textureColor.r, 0.0)).r;\n float greenCurveValue = texture2D(toneCurveTexture, vec2(textureColor.g, 0.0)).g;\n float blueCurveValue = texture2D(toneCurveTexture, vec2(textureColor.b, 0.0)).b;\n \n gl_FragColor = vec4(redCurveValue, greenCurveValue, blueCurveValue, textureColor.a);\n }\n " public let ToonFragmentShader = "varying vec2 textureCoordinate;\n varying vec2 leftTextureCoordinate;\n varying vec2 rightTextureCoordinate;\n \n varying vec2 topTextureCoordinate;\n varying vec2 topLeftTextureCoordinate;\n varying vec2 topRightTextureCoordinate;\n \n varying vec2 bottomTextureCoordinate;\n varying vec2 bottomLeftTextureCoordinate;\n varying vec2 bottomRightTextureCoordinate;\n \n uniform sampler2D inputImageTexture;\n \n uniform float intensity;\n uniform float threshold;\n uniform float quantizationLevels;\n \n const vec3 W = vec3(0.2125, 0.7154, 0.0721);\n \n void main()\n {\n vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n \n float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;\n float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;\n float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;\n float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;\n float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;\n float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;\n float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;\n float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;\n float h = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;\n float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;\n \n float mag = length(vec2(h, v));\n \n vec3 posterizedImageColor = floor((textureColor.rgb * quantizationLevels) + 0.5) / quantizationLevels;\n \n float thresholdTest = 1.0 - step(threshold, mag);\n \n gl_FragColor = vec4(posterizedImageColor * thresholdTest, textureColor.a);\n }\n " public let TransformVertexShader = "attribute vec4 position;\n attribute vec4 inputTextureCoordinate;\n \n uniform mat4 transformMatrix;\n uniform mat4 orthographicMatrix;\n \n varying vec2 textureCoordinate;\n \n void main()\n {\n gl_Position = transformMatrix * vec4(position.xyz, 1.0) * orthographicMatrix;\n textureCoordinate = inputTextureCoordinate.xy;\n }\n " public let TwoInputVertexShader = "attribute vec4 position;\n attribute vec4 inputTextureCoordinate;\n attribute vec4 inputTextureCoordinate2;\n \n varying vec2 textureCoordinate;\n varying vec2 textureCoordinate2;\n \n void main()\n {\n gl_Position = position;\n textureCoordinate = inputTextureCoordinate.xy;\n textureCoordinate2 = inputTextureCoordinate2.xy;\n }\n " diff --git a/framework/Source/Operations/Shaders/ConvertedShaders_GLES.swift b/framework/Source/Operations/Shaders/ConvertedShaders_GLES.swift index 8e934039..56a9c913 100644 --- a/framework/Source/Operations/Shaders/ConvertedShaders_GLES.swift +++ b/framework/Source/Operations/Shaders/ConvertedShaders_GLES.swift @@ -129,6 +129,7 @@ public let ThresholdEdgeDetectionFragmentShader = "precision highp float;\n \n v public let ThresholdSketchFragmentShader = "precision highp float;\n \n varying vec2 textureCoordinate;\n varying vec2 leftTextureCoordinate;\n varying vec2 rightTextureCoordinate;\n \n varying vec2 topTextureCoordinate;\n varying vec2 topLeftTextureCoordinate;\n varying vec2 topRightTextureCoordinate;\n \n varying vec2 bottomTextureCoordinate;\n varying vec2 bottomLeftTextureCoordinate;\n varying vec2 bottomRightTextureCoordinate;\n \n uniform sampler2D inputImageTexture;\n uniform float threshold;\n \n uniform float edgeStrength;\n \n void main()\n {\n float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;\n float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;\n float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;\n float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;\n float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;\n float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;\n float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;\n float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;\n float h = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;\n float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;\n \n float mag = length(vec2(h, v)) * edgeStrength;\n mag = 1.0 - step(threshold, mag);\n \n gl_FragColor = vec4(vec3(mag), 1.0);\n }\n " public let ThresholdedNonMaximumSuppressionFragmentShader = " uniform sampler2D inputImageTexture;\n \n varying highp vec2 textureCoordinate;\n varying highp vec2 leftTextureCoordinate;\n varying highp vec2 rightTextureCoordinate;\n \n varying highp vec2 topTextureCoordinate;\n varying highp vec2 topLeftTextureCoordinate;\n varying highp vec2 topRightTextureCoordinate;\n \n varying highp vec2 bottomTextureCoordinate;\n varying highp vec2 bottomLeftTextureCoordinate;\n varying highp vec2 bottomRightTextureCoordinate;\n \n uniform lowp float threshold;\n \n void main()\n {\n lowp float bottomColor = texture2D(inputImageTexture, bottomTextureCoordinate).r;\n lowp float bottomLeftColor = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;\n lowp float bottomRightColor = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;\n lowp vec4 centerColor = texture2D(inputImageTexture, textureCoordinate);\n lowp float leftColor = texture2D(inputImageTexture, leftTextureCoordinate).r;\n lowp float rightColor = texture2D(inputImageTexture, rightTextureCoordinate).r;\n lowp float topColor = texture2D(inputImageTexture, topTextureCoordinate).r;\n lowp float topRightColor = texture2D(inputImageTexture, topRightTextureCoordinate).r;\n lowp float topLeftColor = texture2D(inputImageTexture, topLeftTextureCoordinate).r;\n \n // Use a tiebreaker for pixels to the left and immediately above this one\n lowp float multiplier = 1.0 - step(centerColor.r, topColor);\n multiplier = multiplier * (1.0 - step(centerColor.r, topLeftColor));\n multiplier = multiplier * (1.0 - step(centerColor.r, leftColor));\n multiplier = multiplier * (1.0 - step(centerColor.r, bottomLeftColor));\n \n lowp float maxValue = max(centerColor.r, bottomColor);\n maxValue = max(maxValue, bottomRightColor);\n maxValue = max(maxValue, rightColor);\n maxValue = max(maxValue, topRightColor);\n \n lowp float finalValue = centerColor.r * step(maxValue, centerColor.r) * multiplier;\n finalValue = step(threshold, finalValue);\n \n gl_FragColor = vec4(finalValue, finalValue, finalValue, 1.0);\n //\n // gl_FragColor = vec4((centerColor.rgb * step(maxValue, step(threshold, centerColor.r)) * multiplier), 1.0);\n }\n " public let TiltShiftFragmentShader = "varying highp vec2 textureCoordinate;\n varying highp vec2 textureCoordinate2;\n \n uniform sampler2D inputImageTexture;\n uniform sampler2D inputImageTexture2; \n \n uniform highp float topFocusLevel;\n uniform highp float bottomFocusLevel;\n uniform highp float focusFallOffRate;\n \n void main()\n {\n lowp vec4 sharpImageColor = texture2D(inputImageTexture, textureCoordinate);\n lowp vec4 blurredImageColor = texture2D(inputImageTexture2, textureCoordinate2);\n \n lowp float blurIntensity = 1.0 - smoothstep(topFocusLevel - focusFallOffRate, topFocusLevel, textureCoordinate2.y);\n blurIntensity += smoothstep(bottomFocusLevel, bottomFocusLevel + focusFallOffRate, textureCoordinate2.y);\n \n gl_FragColor = mix(sharpImageColor, blurredImageColor, blurIntensity);\n }\n " +public let ToneCurveFragmentShader = "varying highp vec2 textureCoordinate;\n uniform sampler2D inputImageTexture;\n uniform sampler2D toneCurveTexture;\n \n void main()\n {\n lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n lowp float redCurveValue = texture2D(toneCurveTexture, vec2(textureColor.r, 0.0)).r;\n lowp float greenCurveValue = texture2D(toneCurveTexture, vec2(textureColor.g, 0.0)).g;\n lowp float blueCurveValue = texture2D(toneCurveTexture, vec2(textureColor.b, 0.0)).b;\n \n gl_FragColor = vec4(redCurveValue, greenCurveValue, blueCurveValue, textureColor.a);\n }\n " public let ToonFragmentShader = "precision highp float;\n \n varying vec2 textureCoordinate;\n varying vec2 leftTextureCoordinate;\n varying vec2 rightTextureCoordinate;\n \n varying vec2 topTextureCoordinate;\n varying vec2 topLeftTextureCoordinate;\n varying vec2 topRightTextureCoordinate;\n \n varying vec2 bottomTextureCoordinate;\n varying vec2 bottomLeftTextureCoordinate;\n varying vec2 bottomRightTextureCoordinate;\n \n uniform sampler2D inputImageTexture;\n \n uniform highp float intensity;\n uniform highp float threshold;\n uniform highp float quantizationLevels;\n \n const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);\n \n void main()\n {\n vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n \n float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;\n float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;\n float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;\n float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;\n float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;\n float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;\n float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;\n float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;\n float h = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;\n float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;\n \n float mag = length(vec2(h, v));\n \n vec3 posterizedImageColor = floor((textureColor.rgb * quantizationLevels) + 0.5) / quantizationLevels;\n \n float thresholdTest = 1.0 - step(threshold, mag);\n \n gl_FragColor = vec4(posterizedImageColor * thresholdTest, textureColor.a);\n }\n " public let TransformVertexShader = "attribute vec4 position;\n attribute vec4 inputTextureCoordinate;\n \n uniform mat4 transformMatrix;\n uniform mat4 orthographicMatrix;\n \n varying vec2 textureCoordinate;\n \n void main()\n {\n gl_Position = transformMatrix * vec4(position.xyz, 1.0) * orthographicMatrix;\n textureCoordinate = inputTextureCoordinate.xy;\n }\n " public let TwoInputVertexShader = "attribute vec4 position;\n attribute vec4 inputTextureCoordinate;\n attribute vec4 inputTextureCoordinate2;\n \n varying vec2 textureCoordinate;\n varying vec2 textureCoordinate2;\n \n void main()\n {\n gl_Position = position;\n textureCoordinate = inputTextureCoordinate.xy;\n textureCoordinate2 = inputTextureCoordinate2.xy;\n }\n " diff --git a/framework/Source/Operations/Shaders/ToneCurve_GL.fsh b/framework/Source/Operations/Shaders/ToneCurve_GL.fsh new file mode 100755 index 00000000..e9d5c7cb --- /dev/null +++ b/framework/Source/Operations/Shaders/ToneCurve_GL.fsh @@ -0,0 +1,13 @@ +varying vec2 textureCoordinate; +uniform sampler2D inputImageTexture; +uniform sampler2D toneCurveTexture; + +void main() +{ + vec4 textureColor = texture2D(inputImageTexture, textureCoordinate); + float redCurveValue = texture2D(toneCurveTexture, vec2(textureColor.r, 0.0)).r; + float greenCurveValue = texture2D(toneCurveTexture, vec2(textureColor.g, 0.0)).g; + float blueCurveValue = texture2D(toneCurveTexture, vec2(textureColor.b, 0.0)).b; + + gl_FragColor = vec4(redCurveValue, greenCurveValue, blueCurveValue, textureColor.a); +} diff --git a/framework/Source/Operations/Shaders/ToneCurve_GLES.fsh b/framework/Source/Operations/Shaders/ToneCurve_GLES.fsh new file mode 100755 index 00000000..9911c9de --- /dev/null +++ b/framework/Source/Operations/Shaders/ToneCurve_GLES.fsh @@ -0,0 +1,13 @@ +varying highp vec2 textureCoordinate; +uniform sampler2D inputImageTexture; +uniform sampler2D toneCurveTexture; + +void main() +{ + lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate); + lowp float redCurveValue = texture2D(toneCurveTexture, vec2(textureColor.r, 0.0)).r; + lowp float greenCurveValue = texture2D(toneCurveTexture, vec2(textureColor.g, 0.0)).g; + lowp float blueCurveValue = texture2D(toneCurveTexture, vec2(textureColor.b, 0.0)).b; + + gl_FragColor = vec4(redCurveValue, greenCurveValue, blueCurveValue, textureColor.a); +} diff --git a/framework/Source/Operations/ToneCurveFilter.swift b/framework/Source/Operations/ToneCurveFilter.swift new file mode 100644 index 00000000..05fc7323 --- /dev/null +++ b/framework/Source/Operations/ToneCurveFilter.swift @@ -0,0 +1,280 @@ +#if os(Linux) +#if GLES + import COpenGLES.gles2 +#else + import COpenGL +#endif +#else +#if GLES + import OpenGLES +#else + import OpenGL.GL3 +#endif +#endif +import Foundation + +public class ToneCurveFilter: BasicOperation { + + public var redControlPoints: [Position] { + didSet { + redCurve = getPreparedSplineCurve(points: redControlPoints) ?? [] + updateToneCurveTexture() + } + } + public var greenControlPoints: [Position] { + didSet { + greenCurve = getPreparedSplineCurve(points: greenControlPoints) ?? [] + updateToneCurveTexture() + } + } + public var blueControlPoints: [Position] { + didSet { + blueCurve = getPreparedSplineCurve(points: blueControlPoints) ?? [] + updateToneCurveTexture() + } + } + public var rgbCompositeControlPoints: [Position] { + didSet { + rgbCompositeCurve = getPreparedSplineCurve(points: rgbCompositeControlPoints) ?? [] + updateToneCurveTexture() + } + } + + fileprivate var toneCurveTexture: GLuint = 0 + fileprivate var toneCurveByteArray: [GLubyte] = [] + + fileprivate var redCurve: [Float] = [] + fileprivate var greenCurve: [Float] = [] + fileprivate var blueCurve: [Float] = [] + fileprivate var rgbCompositeCurve: [Float] = [] + + public init() { + let curve = [Position(0, 0), Position(0.5, 0.5), Position(1, 1)] + + self.redControlPoints = curve + self.greenControlPoints = curve + self.blueControlPoints = curve + self.rgbCompositeControlPoints = curve + + super.init(fragmentShader: ToneCurveFragmentShader, numberOfInputs: 1) + + ({redControlPoints = curve})() + ({greenControlPoints = curve})() + ({blueControlPoints = curve})() + ({rgbCompositeControlPoints = curve})() + } + + public convenience init?(fileName: String) throws { + guard let url = Bundle.main.url(forResource: fileName, withExtension: "acv") else { + return nil + } + try self.init(fileUrl: url) + } + + public convenience init?(fileUrl: URL) throws { + let data = try Data(contentsOf: fileUrl) + self.init(acvData: data) + } + + public init?(acvData: Data) { + guard let curve = ACVFile(data: acvData) else { + return nil + } + + self.redControlPoints = curve.redControlPoints + self.greenControlPoints = curve.greenControlPoints + self.blueControlPoints = curve.blueControlPoints + self.rgbCompositeControlPoints = curve.rgbCompositeControlPoints + + super.init(fragmentShader: ToneCurveFragmentShader, numberOfInputs: 1) + + ({redControlPoints = curve.redControlPoints})() + ({greenControlPoints = curve.greenControlPoints})() + ({blueControlPoints = curve.blueControlPoints})() + ({rgbCompositeControlPoints = curve.rgbCompositeControlPoints})() + } + + deinit { + glDeleteTextures(1, &toneCurveTexture) + toneCurveTexture = 0 + } + + // MARK Curve calculation + + func secondDerivative(_ points: [Position]) -> [Float]? { + guard points.count > 1 else { return nil } + + let n = points.count + var matrix: [[Float]] = Array(repeatElement(Array(repeatElement(0, count: 3)), count: n)) + var result: [Float] = Array(repeatElement(0, count: n)) + + matrix[0][0] = 0 + matrix[0][1] = 1 + matrix[0][2] = 0 + + for i in 1 ..< n - 1 { + let p1 = points[i - 1] + let p2 = points[i] + let p3 = points[i + 1] + + matrix[i][0] = (p2.x - p1.x) / 6 + matrix[i][1] = (p3.x - p1.x) / 3 + matrix[i][2] = (p3.x - p2.x) / 6 + result[i] = (p3.y - p2.y) / (p3.x - p2.x) - (p2.y - p1.y) / (p2.x - p1.x) + } + result[0] = 0 + result[n - 1] = 0 + + matrix[n - 1][1] = 1 + matrix[n - 1][0] = 0 + matrix[n - 1][2] = 0 + + // solving pass1 (up->down) + for i in 1 ..< n { + let k = matrix[i][0] / matrix[i - 1][1] + matrix[i][1] -= k * matrix[i - 1][2] + matrix[i][0] = 0 + result[i] -= k * result[i - 1] + } + // solving pass2 (down->up) + for i in (0 ... n - 2).reversed() { + let k = matrix[i][2] / matrix[i + 1][1] + matrix[i][1] -= k * matrix[i + 1][0] + matrix[i][2] = 0 + result[i] -= k * result[i + 1] + } + + var y2: [Float] = Array(repeatElement(0, count: n)) + for i in 0 ..< n { + y2[i] = result[i] / matrix[i][1] + } + + return y2 + } + + + func splineCurve(points: [Position]) -> [Position]? { + guard let sdA = secondDerivative(points), sdA.count >= 1 else { + return nil + } + let n = sdA.count + var sd = sdA + + var output: [Position] = [] + + for i in 0 ..< n - 1 { + let current = points[i] + let next = points[i + 1] + + var x = Int(current.x) + repeat { + let t = (Float(x) - current.x) / (next.x - current.x) + let a = 1 - t + let b = t + let h = next.x - current.x + + let aFactor = (a * a * a - a) * sd[i] + let bFactor = (b * b * b - b) * sd[i + 1] + var y = a * current.y + b * next.y + (h * h / 6) * (aFactor + bFactor) + if y > 255 { + y = 255 + } else if y < 0 { + y = 0 + } + + output.append(Position(Float(x), y)) + x += 1 + } while x < Int(next.x) + } + // The above always misses the last point because the last point is the last next, so we approach but don't equal it. + output.append(points.last!) + + return output + } + + func getPreparedSplineCurve(points: [Position]) -> [Float]? { + guard points.count > 0 else { return nil } + let sortedPoints = points.sorted { $0.x < $1.x } + + // Convert from (0, 1) to (0, 255). + let convertedPoints = sortedPoints.map { point in + return Position(point.x * 255, point.y * 255) + } + + guard var splinePoints = splineCurve(points: convertedPoints) else { return nil } + + // If we have a first point like (0.3, 0) we'll be missing some points at the beginning + // that should be 0. + let firstSplinePoint = splinePoints.first! + + if firstSplinePoint.x > 0 { + for i in (0 ... Int(firstSplinePoint.x)).reversed() { + let newPoint = Position(Float(i), 0) + splinePoints.insert(newPoint, at: 0) + } + } + + // Insert points similarly at the end, if necessary. + let lastSplinePoint = splinePoints.last! + + if lastSplinePoint.x < 255 { + for i in Int(lastSplinePoint.x + 1) ... 255 { + let newPoint = Position(Float(i), 255) + splinePoints.append(newPoint) + } + } + + // Prepare the spline points. + let preparedSplinePoints = splinePoints.compactMap { (point: Position) -> Float in + let origPoint = Position(point.x, point.x) + var distance = sqrt(pow(origPoint.x - point.x, 2) + pow(origPoint.y - point.y, 2)) + if origPoint.y > point.y { + distance = -distance + } + return distance + } + return preparedSplinePoints + } + + override func internalRenderFunction(_ inputFramebuffer: Framebuffer, textureProperties: [InputTextureProperties]) { + renderQuadWithShader(shader, uniformSettings:uniformSettings, vertexBufferObject:sharedImageProcessingContext.standardImageVBO, inputTextures:textureProperties) + + glActiveTexture(GLenum(GL_TEXTURE3)) + glBindTexture(GLenum(GL_TEXTURE_2D), toneCurveTexture) + shader.setValue(GLint(3), forUniform:"toneCurveTexture") + + releaseIncomingFramebuffers() + } + + func updateToneCurveTexture() { + sharedImageProcessingContext.runOperationSynchronously { + if toneCurveTexture == 0 { + toneCurveTexture = generateTexture(minFilter: GL_LINEAR, magFilter: GL_LINEAR, wrapS: GL_CLAMP_TO_EDGE, wrapT: GL_CLAMP_TO_EDGE) + toneCurveByteArray = Array(repeatElement(0, count: 256 * 4)) + } else { + glActiveTexture(GLenum(GL_TEXTURE3)) + glBindTexture(GLenum(GL_TEXTURE_2D), toneCurveTexture) + } + + if redCurve.count >= 256 && greenCurve.count >= 256 && + blueCurve.count >= 256 && rgbCompositeCurve.count >= 256 { + for currentCurveIndex in 0 ..< 256 { + // BGRA for upload to texture + + let b = fmin(fmax(Float(currentCurveIndex) + blueCurve[currentCurveIndex], 0), 255) + + toneCurveByteArray[currentCurveIndex * 4] = GLubyte(fmin(fmax(b + rgbCompositeCurve[Int(b)], 0), 255)) + let g = fmin(fmax(Float(currentCurveIndex) + greenCurve[currentCurveIndex], 0), 255) + + toneCurveByteArray[currentCurveIndex * 4 + 1] = GLubyte(fmin(fmax(g + rgbCompositeCurve[Int(g)], 0), 255)) + let r = fmin(fmax(Float(currentCurveIndex) + redCurve[currentCurveIndex], 0), 255) + + toneCurveByteArray[currentCurveIndex * 4 + 2] = GLubyte(fmin(fmax(r + rgbCompositeCurve[Int(r)], 0), 255)) + toneCurveByteArray[currentCurveIndex * 4 + 3] = 255 + } + + glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GLint(GL_RGBA), 256, 1, 0, GLenum(GL_BGRA), GLenum(GL_UNSIGNED_BYTE), toneCurveByteArray) + } + } + } +}