├── MercurialPaint.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── simongladman.xcuserdatad │ └── xcschemes │ ├── MercurialPaint.xcscheme │ └── xcschememanagement.plist ├── MercurialPaint ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.swift ├── components │ └── MercurialPaint.swift ├── screenshot.jpg ├── shaders │ └── Shaders.metal └── shadingImageEditor │ ├── ItemRenderer.swift │ ├── ShadingImageEditor.swift │ └── SupportClasses.swift └── README.md /MercurialPaint.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3E02B1831C1156AB00258127 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E02B1821C1156AB00258127 /* AppDelegate.swift */; }; 11 | 3E02B1851C1156AB00258127 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E02B1841C1156AB00258127 /* ViewController.swift */; }; 12 | 3E02B1881C1156AB00258127 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E02B1861C1156AB00258127 /* Main.storyboard */; }; 13 | 3E02B18A1C1156AB00258127 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3E02B1891C1156AB00258127 /* Assets.xcassets */; }; 14 | 3E02B18D1C1156AB00258127 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E02B18B1C1156AB00258127 /* LaunchScreen.storyboard */; }; 15 | 3E02B1961C1156E000258127 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 3E02B1951C1156E000258127 /* Shaders.metal */; }; 16 | 3E02B1991C11576300258127 /* MercurialPaint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E02B1981C11576300258127 /* MercurialPaint.swift */; }; 17 | 3E5747F61C14686D0082ABF4 /* screenshot.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 3E5747F51C14686D0082ABF4 /* screenshot.jpg */; }; 18 | 3EC593C01C12C10A00A23D8A /* ShadingImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC593BF1C12C10A00A23D8A /* ShadingImageEditor.swift */; }; 19 | 3EC593C21C12C12100A23D8A /* ItemRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC593C11C12C12100A23D8A /* ItemRenderer.swift */; }; 20 | 3EC593C41C12C14500A23D8A /* SupportClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC593C31C12C14500A23D8A /* SupportClasses.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 3E02B17F1C1156AB00258127 /* MercurialPaint.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MercurialPaint.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 3E02B1821C1156AB00258127 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 3E02B1841C1156AB00258127 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | 3E02B1871C1156AB00258127 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | 3E02B1891C1156AB00258127 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 3E02B18C1C1156AB00258127 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 3E02B18E1C1156AB00258127 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 3E02B1951C1156E000258127 /* Shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; name = Shaders.metal; path = shaders/Shaders.metal; sourceTree = ""; }; 32 | 3E02B1981C11576300258127 /* MercurialPaint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MercurialPaint.swift; path = components/MercurialPaint.swift; sourceTree = ""; }; 33 | 3E5747F51C14686D0082ABF4 /* screenshot.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = screenshot.jpg; path = MercurialPaint/screenshot.jpg; sourceTree = ""; }; 34 | 3EC593BF1C12C10A00A23D8A /* ShadingImageEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShadingImageEditor.swift; path = shadingImageEditor/ShadingImageEditor.swift; sourceTree = ""; }; 35 | 3EC593C11C12C12100A23D8A /* ItemRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ItemRenderer.swift; path = shadingImageEditor/ItemRenderer.swift; sourceTree = ""; }; 36 | 3EC593C31C12C14500A23D8A /* SupportClasses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SupportClasses.swift; path = shadingImageEditor/SupportClasses.swift; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 3E02B17C1C1156AB00258127 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | 3E02B1761C1156AB00258127 = { 51 | isa = PBXGroup; 52 | children = ( 53 | 3E5747F51C14686D0082ABF4 /* screenshot.jpg */, 54 | 3E02B1811C1156AB00258127 /* MercurialPaint */, 55 | 3E02B1801C1156AB00258127 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 3E02B1801C1156AB00258127 /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 3E02B17F1C1156AB00258127 /* MercurialPaint.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | 3E02B1811C1156AB00258127 /* MercurialPaint */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 3EC593BE1C12C0E100A23D8A /* shadingImageEditor */, 71 | 3E02B1971C11573900258127 /* components */, 72 | 3E02B1941C1156C100258127 /* shaders */, 73 | 3E02B1821C1156AB00258127 /* AppDelegate.swift */, 74 | 3E02B1841C1156AB00258127 /* ViewController.swift */, 75 | 3E02B1861C1156AB00258127 /* Main.storyboard */, 76 | 3E02B1891C1156AB00258127 /* Assets.xcassets */, 77 | 3E02B18B1C1156AB00258127 /* LaunchScreen.storyboard */, 78 | 3E02B18E1C1156AB00258127 /* Info.plist */, 79 | ); 80 | path = MercurialPaint; 81 | sourceTree = ""; 82 | }; 83 | 3E02B1941C1156C100258127 /* shaders */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 3E02B1951C1156E000258127 /* Shaders.metal */, 87 | ); 88 | name = shaders; 89 | sourceTree = ""; 90 | }; 91 | 3E02B1971C11573900258127 /* components */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 3E02B1981C11576300258127 /* MercurialPaint.swift */, 95 | ); 96 | name = components; 97 | sourceTree = ""; 98 | }; 99 | 3EC593BE1C12C0E100A23D8A /* shadingImageEditor */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 3EC593BF1C12C10A00A23D8A /* ShadingImageEditor.swift */, 103 | 3EC593C11C12C12100A23D8A /* ItemRenderer.swift */, 104 | 3EC593C31C12C14500A23D8A /* SupportClasses.swift */, 105 | ); 106 | name = shadingImageEditor; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | 3E02B17E1C1156AB00258127 /* MercurialPaint */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = 3E02B1911C1156AB00258127 /* Build configuration list for PBXNativeTarget "MercurialPaint" */; 115 | buildPhases = ( 116 | 3E02B17B1C1156AB00258127 /* Sources */, 117 | 3E02B17C1C1156AB00258127 /* Frameworks */, 118 | 3E02B17D1C1156AB00258127 /* Resources */, 119 | ); 120 | buildRules = ( 121 | ); 122 | dependencies = ( 123 | ); 124 | name = MercurialPaint; 125 | productName = MercurialPaint; 126 | productReference = 3E02B17F1C1156AB00258127 /* MercurialPaint.app */; 127 | productType = "com.apple.product-type.application"; 128 | }; 129 | /* End PBXNativeTarget section */ 130 | 131 | /* Begin PBXProject section */ 132 | 3E02B1771C1156AB00258127 /* Project object */ = { 133 | isa = PBXProject; 134 | attributes = { 135 | LastSwiftUpdateCheck = 0710; 136 | LastUpgradeCheck = 0710; 137 | ORGANIZATIONNAME = "Simon Gladman"; 138 | TargetAttributes = { 139 | 3E02B17E1C1156AB00258127 = { 140 | CreatedOnToolsVersion = 7.1.1; 141 | }; 142 | }; 143 | }; 144 | buildConfigurationList = 3E02B17A1C1156AB00258127 /* Build configuration list for PBXProject "MercurialPaint" */; 145 | compatibilityVersion = "Xcode 3.2"; 146 | developmentRegion = English; 147 | hasScannedForEncodings = 0; 148 | knownRegions = ( 149 | en, 150 | Base, 151 | ); 152 | mainGroup = 3E02B1761C1156AB00258127; 153 | productRefGroup = 3E02B1801C1156AB00258127 /* Products */; 154 | projectDirPath = ""; 155 | projectRoot = ""; 156 | targets = ( 157 | 3E02B17E1C1156AB00258127 /* MercurialPaint */, 158 | ); 159 | }; 160 | /* End PBXProject section */ 161 | 162 | /* Begin PBXResourcesBuildPhase section */ 163 | 3E02B17D1C1156AB00258127 /* Resources */ = { 164 | isa = PBXResourcesBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | 3E02B18D1C1156AB00258127 /* LaunchScreen.storyboard in Resources */, 168 | 3E02B18A1C1156AB00258127 /* Assets.xcassets in Resources */, 169 | 3E02B1881C1156AB00258127 /* Main.storyboard in Resources */, 170 | 3E5747F61C14686D0082ABF4 /* screenshot.jpg in Resources */, 171 | ); 172 | runOnlyForDeploymentPostprocessing = 0; 173 | }; 174 | /* End PBXResourcesBuildPhase section */ 175 | 176 | /* Begin PBXSourcesBuildPhase section */ 177 | 3E02B17B1C1156AB00258127 /* Sources */ = { 178 | isa = PBXSourcesBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | 3E02B1961C1156E000258127 /* Shaders.metal in Sources */, 182 | 3EC593C01C12C10A00A23D8A /* ShadingImageEditor.swift in Sources */, 183 | 3E02B1851C1156AB00258127 /* ViewController.swift in Sources */, 184 | 3E02B1831C1156AB00258127 /* AppDelegate.swift in Sources */, 185 | 3E02B1991C11576300258127 /* MercurialPaint.swift in Sources */, 186 | 3EC593C21C12C12100A23D8A /* ItemRenderer.swift in Sources */, 187 | 3EC593C41C12C14500A23D8A /* SupportClasses.swift in Sources */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXSourcesBuildPhase section */ 192 | 193 | /* Begin PBXVariantGroup section */ 194 | 3E02B1861C1156AB00258127 /* Main.storyboard */ = { 195 | isa = PBXVariantGroup; 196 | children = ( 197 | 3E02B1871C1156AB00258127 /* Base */, 198 | ); 199 | name = Main.storyboard; 200 | sourceTree = ""; 201 | }; 202 | 3E02B18B1C1156AB00258127 /* LaunchScreen.storyboard */ = { 203 | isa = PBXVariantGroup; 204 | children = ( 205 | 3E02B18C1C1156AB00258127 /* Base */, 206 | ); 207 | name = LaunchScreen.storyboard; 208 | sourceTree = ""; 209 | }; 210 | /* End PBXVariantGroup section */ 211 | 212 | /* Begin XCBuildConfiguration section */ 213 | 3E02B18F1C1156AB00258127 /* Debug */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | ALWAYS_SEARCH_USER_PATHS = NO; 217 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 218 | CLANG_CXX_LIBRARY = "libc++"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_WARN_BOOL_CONVERSION = YES; 222 | CLANG_WARN_CONSTANT_CONVERSION = YES; 223 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 224 | CLANG_WARN_EMPTY_BODY = YES; 225 | CLANG_WARN_ENUM_CONVERSION = YES; 226 | CLANG_WARN_INT_CONVERSION = YES; 227 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 228 | CLANG_WARN_UNREACHABLE_CODE = YES; 229 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 230 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 231 | COPY_PHASE_STRIP = NO; 232 | DEBUG_INFORMATION_FORMAT = dwarf; 233 | ENABLE_STRICT_OBJC_MSGSEND = YES; 234 | ENABLE_TESTABILITY = YES; 235 | GCC_C_LANGUAGE_STANDARD = gnu99; 236 | GCC_DYNAMIC_NO_PIC = NO; 237 | GCC_NO_COMMON_BLOCKS = YES; 238 | GCC_OPTIMIZATION_LEVEL = 0; 239 | GCC_PREPROCESSOR_DEFINITIONS = ( 240 | "DEBUG=1", 241 | "$(inherited)", 242 | ); 243 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 244 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 245 | GCC_WARN_UNDECLARED_SELECTOR = YES; 246 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 247 | GCC_WARN_UNUSED_FUNCTION = YES; 248 | GCC_WARN_UNUSED_VARIABLE = YES; 249 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 250 | MTL_ENABLE_DEBUG_INFO = YES; 251 | ONLY_ACTIVE_ARCH = YES; 252 | SDKROOT = iphoneos; 253 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 254 | TARGETED_DEVICE_FAMILY = 2; 255 | }; 256 | name = Debug; 257 | }; 258 | 3E02B1901C1156AB00258127 /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | ALWAYS_SEARCH_USER_PATHS = NO; 262 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 263 | CLANG_CXX_LIBRARY = "libc++"; 264 | CLANG_ENABLE_MODULES = YES; 265 | CLANG_ENABLE_OBJC_ARC = YES; 266 | CLANG_WARN_BOOL_CONVERSION = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 269 | CLANG_WARN_EMPTY_BODY = YES; 270 | CLANG_WARN_ENUM_CONVERSION = YES; 271 | CLANG_WARN_INT_CONVERSION = YES; 272 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 273 | CLANG_WARN_UNREACHABLE_CODE = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 276 | COPY_PHASE_STRIP = NO; 277 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 278 | ENABLE_NS_ASSERTIONS = NO; 279 | ENABLE_STRICT_OBJC_MSGSEND = YES; 280 | GCC_C_LANGUAGE_STANDARD = gnu99; 281 | GCC_NO_COMMON_BLOCKS = YES; 282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 284 | GCC_WARN_UNDECLARED_SELECTOR = YES; 285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 286 | GCC_WARN_UNUSED_FUNCTION = YES; 287 | GCC_WARN_UNUSED_VARIABLE = YES; 288 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 289 | MTL_ENABLE_DEBUG_INFO = NO; 290 | SDKROOT = iphoneos; 291 | TARGETED_DEVICE_FAMILY = 2; 292 | VALIDATE_PRODUCT = YES; 293 | }; 294 | name = Release; 295 | }; 296 | 3E02B1921C1156AB00258127 /* Debug */ = { 297 | isa = XCBuildConfiguration; 298 | buildSettings = { 299 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 300 | INFOPLIST_FILE = MercurialPaint/Info.plist; 301 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 302 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.MercurialPaint; 303 | PRODUCT_NAME = "$(TARGET_NAME)"; 304 | }; 305 | name = Debug; 306 | }; 307 | 3E02B1931C1156AB00258127 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | INFOPLIST_FILE = MercurialPaint/Info.plist; 312 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 313 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.MercurialPaint; 314 | PRODUCT_NAME = "$(TARGET_NAME)"; 315 | }; 316 | name = Release; 317 | }; 318 | /* End XCBuildConfiguration section */ 319 | 320 | /* Begin XCConfigurationList section */ 321 | 3E02B17A1C1156AB00258127 /* Build configuration list for PBXProject "MercurialPaint" */ = { 322 | isa = XCConfigurationList; 323 | buildConfigurations = ( 324 | 3E02B18F1C1156AB00258127 /* Debug */, 325 | 3E02B1901C1156AB00258127 /* Release */, 326 | ); 327 | defaultConfigurationIsVisible = 0; 328 | defaultConfigurationName = Release; 329 | }; 330 | 3E02B1911C1156AB00258127 /* Build configuration list for PBXNativeTarget "MercurialPaint" */ = { 331 | isa = XCConfigurationList; 332 | buildConfigurations = ( 333 | 3E02B1921C1156AB00258127 /* Debug */, 334 | 3E02B1931C1156AB00258127 /* Release */, 335 | ); 336 | defaultConfigurationIsVisible = 0; 337 | defaultConfigurationName = Release; 338 | }; 339 | /* End XCConfigurationList section */ 340 | }; 341 | rootObject = 3E02B1771C1156AB00258127 /* Project object */; 342 | } 343 | -------------------------------------------------------------------------------- /MercurialPaint.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MercurialPaint.xcodeproj/xcuserdata/simongladman.xcuserdatad/xcschemes/MercurialPaint.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /MercurialPaint.xcodeproj/xcuserdata/simongladman.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MercurialPaint.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3E02B17E1C1156AB00258127 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /MercurialPaint/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MercurialPaint 4 | // 5 | // Created by Simon Gladman on 04/12/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /MercurialPaint/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "29x29", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "29x29", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "40x40", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "40x40", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "76x76", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "76x76", 31 | "scale" : "2x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /MercurialPaint/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MercurialPaint/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MercurialPaint/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIRequiresFullScreen 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MercurialPaint/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MercurialPaint 4 | // 5 | // Created by Simon Gladman on 04/12/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController 12 | { 13 | 14 | let mercurialPaint = MercurialPaint(frame: CGRect(x: 0, y: 0, width: 1024, height: 1024)) 15 | let shadingImageEditor = ShadingImageEditor() 16 | 17 | override func viewDidLoad() 18 | { 19 | super.viewDidLoad() 20 | 21 | view.backgroundColor = UIColor.blackColor() 22 | 23 | view.addSubview(mercurialPaint) 24 | view.addSubview(shadingImageEditor) 25 | 26 | shadingImageEditor.addTarget(self, 27 | action: "shadingImageChange", 28 | forControlEvents: UIControlEvents.ValueChanged) 29 | } 30 | 31 | func shadingImageChange() 32 | { 33 | mercurialPaint.shadingImage = shadingImageEditor.image 34 | } 35 | 36 | override func viewDidLayoutSubviews() 37 | { 38 | mercurialPaint.frame = CGRect(x: 0, 39 | y: 0, 40 | width: 1024, 41 | height: 1024) 42 | 43 | shadingImageEditor.frame = CGRect(x: 1026, 44 | y: 0, 45 | width: view.frame.width - 1026, 46 | height: view.frame.height) 47 | } 48 | 49 | override func prefersStatusBarHidden() -> Bool 50 | { 51 | return true 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /MercurialPaint/components/MercurialPaint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MercurialPaint.swift 3 | // MercurialPaint 4 | // 5 | // Created by Simon Gladman on 04/12/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MetalKit 11 | import MetalPerformanceShaders 12 | 13 | let particleCount: Int = 2048 14 | 15 | class MercurialPaint: UIView 16 | { 17 | // MARK: Constants 18 | 19 | let device = MTLCreateSystemDefaultDevice()! 20 | let alignment:Int = 0x4000 21 | let particlesMemoryByteSize:Int = particleCount * sizeof(Int) 22 | let halfPi = CGFloat(M_PI_2) 23 | 24 | let ciContext = CIContext(EAGLContext: EAGLContext(API: EAGLRenderingAPI.OpenGLES2), options: [kCIContextWorkingColorSpace: NSNull()]) 25 | let heightMapFilter = CIFilter(name: "CIHeightFieldFromMask")! 26 | let shadedMaterialFilter = CIFilter(name: "CIShadedMaterial")! 27 | let maskToAlpha = CIFilter(name: "CIMaskToAlpha")! 28 | 29 | // MARK: Private variables 30 | 31 | private var threadsPerThreadgroup:MTLSize! 32 | private var threadgroupsPerGrid:MTLSize! 33 | 34 | private var particlesMemory:UnsafeMutablePointer = nil 35 | private var particlesVoidPtr: COpaquePointer! 36 | private var particlesParticlePtr: UnsafeMutablePointer! 37 | private var particlesParticleBufferPtr: UnsafeMutableBufferPointer! 38 | 39 | private var particlesBufferNoCopy: MTLBuffer! 40 | private var touchLocations = [CGPoint](count: 4, repeatedValue: CGPoint(x: -1, y: -1)) 41 | private var touchForce:Float = 0 42 | 43 | private var pendingUpdate = false 44 | private var isBusy = false 45 | private var isDrawing = false 46 | { 47 | didSet 48 | { 49 | if isDrawing 50 | { 51 | metalView.paused = false 52 | } 53 | } 54 | } 55 | 56 | // MARK: Public 57 | 58 | var shadingImage: UIImage? 59 | { 60 | didSet 61 | { 62 | applyCoreImageFilter() 63 | } 64 | } 65 | 66 | // MARK: UI components 67 | 68 | var metalView: MTKView! 69 | let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 1024, height: 1024)) 70 | 71 | // MARK: Lazy variables 72 | 73 | let textureDescriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(MTLPixelFormat.RGBA8Unorm, 74 | width: 2048, 75 | height: 2048, 76 | mipmapped: false) 77 | 78 | lazy var paintingTexture: MTLTexture = 79 | { 80 | [unowned self] in 81 | 82 | return self.device.newTextureWithDescriptor(self.textureDescriptor) 83 | }() 84 | 85 | lazy var intermediateTexture: MTLTexture = 86 | { 87 | [unowned self] in 88 | 89 | return self.device.newTextureWithDescriptor(self.textureDescriptor) 90 | }() 91 | 92 | lazy var paintingShaderPipelineState: MTLComputePipelineState = 93 | { 94 | [unowned self] in 95 | 96 | do 97 | { 98 | let library = self.device.newDefaultLibrary()! 99 | 100 | let kernelFunction = library.newFunctionWithName("mercurialPaintShader") 101 | let pipelineState = try self.device.newComputePipelineStateWithFunction(kernelFunction!) 102 | 103 | return pipelineState 104 | } 105 | catch 106 | { 107 | fatalError("Unable to create censusTransformMonoPipelineState") 108 | } 109 | }() 110 | 111 | lazy var commandQueue: MTLCommandQueue = 112 | { 113 | [unowned self] in 114 | 115 | return self.device.newCommandQueue() 116 | }() 117 | 118 | lazy var blur: MPSImageGaussianBlur = 119 | { 120 | [unowned self] in 121 | 122 | return MPSImageGaussianBlur(device: self.device, sigma: 3) 123 | }() 124 | 125 | lazy var threshold: MPSImageThresholdBinary = 126 | { 127 | [unowned self] in 128 | 129 | return MPSImageThresholdBinary(device: self.device, thresholdValue: 0.5, maximumValue: 1, linearGrayColorTransform: nil) 130 | }() 131 | 132 | 133 | 134 | // MARK: Initialisation 135 | 136 | override init(frame frameRect: CGRect) 137 | { 138 | super.init(frame: frameRect) 139 | 140 | metalView = MTKView(frame: CGRect(x: 0, y: 0, width: 1024, height: 1024), device: device) 141 | 142 | metalView.framebufferOnly = false 143 | metalView.colorPixelFormat = MTLPixelFormat.BGRA8Unorm 144 | 145 | metalView.delegate = self 146 | 147 | layer.borderColor = UIColor.whiteColor().CGColor 148 | layer.borderWidth = 1 149 | 150 | metalView.drawableSize = CGSize(width: 2048, height: 2048) 151 | 152 | addSubview(metalView) 153 | addSubview(imageView) 154 | 155 | setUpMetal() 156 | 157 | metalView.paused = true 158 | } 159 | 160 | required init(coder: NSCoder) 161 | { 162 | fatalError("init(coder:) has not been implemented") 163 | } 164 | 165 | private func setUpMetal() 166 | { 167 | posix_memalign(&particlesMemory, alignment, particlesMemoryByteSize) 168 | 169 | particlesVoidPtr = COpaquePointer(particlesMemory) 170 | particlesParticlePtr = UnsafeMutablePointer(particlesVoidPtr) 171 | particlesParticleBufferPtr = UnsafeMutableBufferPointer(start: particlesParticlePtr, count: particleCount) 172 | 173 | for index in particlesParticleBufferPtr.startIndex ..< particlesParticleBufferPtr.endIndex 174 | { 175 | particlesParticleBufferPtr[index] = Int(arc4random_uniform(9999)) 176 | } 177 | 178 | let threadExecutionWidth = paintingShaderPipelineState.threadExecutionWidth 179 | 180 | threadsPerThreadgroup = MTLSize(width:threadExecutionWidth,height:1,depth:1) 181 | threadgroupsPerGrid = MTLSize(width:particleCount / threadExecutionWidth, height:1, depth:1) 182 | 183 | particlesBufferNoCopy = device.newBufferWithBytesNoCopy(particlesMemory, 184 | length: Int(particlesMemoryByteSize), 185 | options: MTLResourceOptions.StorageModeShared, 186 | deallocator: nil) 187 | } 188 | 189 | // MARK: Touch handlers 190 | 191 | override func touchesBegan(touches: Set, withEvent event: UIEvent?) 192 | { 193 | guard let touch = touches.first else 194 | { 195 | return 196 | } 197 | 198 | touchForce = touch.type == .Stylus 199 | ? Float(touch.force / touch.maximumPossibleForce) 200 | : 0.5 201 | 202 | touchLocations = [touch.locationInView(self)] 203 | 204 | isDrawing = true 205 | } 206 | 207 | override func touchesMoved(touches: Set, withEvent event: UIEvent?) 208 | { 209 | guard let touch = touches.first, coalescedTouches = event?.coalescedTouchesForTouch(touch) else 210 | { 211 | return 212 | } 213 | 214 | touchForce = touch.type == .Stylus 215 | ? Float(touch.force / touch.maximumPossibleForce) 216 | : 0.5 217 | 218 | touchLocations = coalescedTouches.map{ return $0.locationInView(self) } 219 | } 220 | 221 | override func touchesEnded(touches: Set, withEvent event: UIEvent?) 222 | { 223 | touchLocations = [CGPoint](count: 4, repeatedValue: CGPoint(x: -1, y: -1)) 224 | 225 | applyCoreImageFilter() 226 | 227 | isDrawing = false 228 | } 229 | 230 | // MARK: Core Image Stuff 231 | 232 | func applyCoreImageFilter() 233 | { 234 | guard let drawable = metalView.currentDrawable else 235 | { 236 | print("currentDrawable returned nil") 237 | 238 | return 239 | } 240 | 241 | guard !isBusy else 242 | { 243 | pendingUpdate = true 244 | return 245 | } 246 | 247 | guard let shadingImage = shadingImage, ciShadingImage = CIImage(image: shadingImage) else 248 | { 249 | return 250 | } 251 | 252 | isBusy = true 253 | 254 | let mercurialImage = CIImage(MTLTexture: drawable.texture, options: nil) 255 | 256 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) 257 | { 258 | let heightMapFilter = self.heightMapFilter.copy() 259 | let shadedMaterialFilter = self.shadedMaterialFilter.copy() 260 | let maskToAlpha = self.maskToAlpha.copy() 261 | 262 | maskToAlpha.setValue(mercurialImage, 263 | forKey: kCIInputImageKey) 264 | 265 | heightMapFilter.setValue(maskToAlpha.valueForKey(kCIOutputImageKey), 266 | forKey: kCIInputImageKey) 267 | 268 | shadedMaterialFilter.setValue(heightMapFilter.valueForKey(kCIOutputImageKey), 269 | forKey: kCIInputImageKey) 270 | 271 | shadedMaterialFilter.setValue(ciShadingImage, 272 | forKey: "inputShadingImage") 273 | 274 | let filteredImageData = shadedMaterialFilter.valueForKey(kCIOutputImageKey) as! CIImage 275 | let filteredImageRef = self.ciContext.createCGImage(filteredImageData, 276 | fromRect: filteredImageData.extent) 277 | 278 | let finalImage = UIImage(CGImage: filteredImageRef) 279 | 280 | dispatch_async(dispatch_get_main_queue()) 281 | { 282 | self.imageView.image = finalImage 283 | self.isBusy = false 284 | 285 | if self.pendingUpdate 286 | { 287 | self.pendingUpdate = false 288 | 289 | self.applyCoreImageFilter() 290 | } 291 | } 292 | } 293 | } 294 | 295 | func touchLocationsToVector(xy: XY) -> vector_int4 296 | { 297 | func getValue(point: CGPoint, xy: XY) -> Int32 298 | { 299 | switch xy 300 | { 301 | case .X: 302 | return Int32(point.x * 2) 303 | case .Y: 304 | return Int32(point.y * 2) 305 | } 306 | } 307 | 308 | let x = touchLocations.count > 0 ? getValue(touchLocations[0], xy: xy) : -1 309 | let y = touchLocations.count > 1 ? getValue(touchLocations[1], xy: xy) : -1 310 | let z = touchLocations.count > 2 ? getValue(touchLocations[2], xy: xy) : -1 311 | let w = touchLocations.count > 3 ? getValue(touchLocations[3], xy: xy) : -1 312 | 313 | let returnValue = vector_int4(x, y, z, w) 314 | 315 | return returnValue 316 | } 317 | 318 | } 319 | 320 | extension MercurialPaint: MTKViewDelegate 321 | { 322 | func mtkView(view: MTKView, drawableSizeWillChange size: CGSize) 323 | { 324 | 325 | } 326 | 327 | func drawInMTKView(view: MTKView) 328 | { 329 | let commandBuffer = commandQueue.commandBuffer() 330 | let commandEncoder = commandBuffer.computeCommandEncoder() 331 | 332 | commandEncoder.setComputePipelineState(paintingShaderPipelineState) 333 | 334 | commandEncoder.setBuffer(particlesBufferNoCopy, offset: 0, atIndex: 0) 335 | 336 | var xLocation = touchLocationsToVector(.X) 337 | let xLocationBuffer = device.newBufferWithBytes(&xLocation, 338 | length: sizeof(vector_int4), 339 | options: MTLResourceOptions.CPUCacheModeDefaultCache) 340 | 341 | var yLocation = touchLocationsToVector(.Y) 342 | let yLocationBuffer = device.newBufferWithBytes(&yLocation, 343 | length: sizeof(vector_int4), 344 | options: MTLResourceOptions.CPUCacheModeDefaultCache) 345 | 346 | let touchForceBuffer = device.newBufferWithBytes(&touchForce, 347 | length: sizeof(Float), 348 | options: MTLResourceOptions.CPUCacheModeDefaultCache) 349 | 350 | commandEncoder.setBuffer(xLocationBuffer, offset: 0, atIndex: 1) 351 | commandEncoder.setBuffer(yLocationBuffer, offset: 0, atIndex: 2) 352 | commandEncoder.setBuffer(touchForceBuffer, offset: 0, atIndex: 3) 353 | 354 | commandEncoder.setTexture(paintingTexture, atIndex: 0) 355 | 356 | commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) 357 | 358 | commandEncoder.endEncoding() 359 | 360 | guard let drawable = metalView.currentDrawable else 361 | { 362 | print("currentDrawable returned nil") 363 | 364 | return 365 | } 366 | 367 | blur.encodeToCommandBuffer(commandBuffer, 368 | sourceTexture: paintingTexture, 369 | destinationTexture: intermediateTexture) 370 | 371 | threshold.encodeToCommandBuffer(commandBuffer, 372 | sourceTexture: intermediateTexture, 373 | destinationTexture: drawable.texture) 374 | 375 | commandBuffer.commit() 376 | 377 | drawable.present() 378 | 379 | view.paused = !isDrawing 380 | } 381 | } 382 | 383 | enum XY 384 | { 385 | case X, Y 386 | } 387 | 388 | -------------------------------------------------------------------------------- /MercurialPaint/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlexMonkey/MercurialPaint/daf7100845c193865790bf21b383f59e1f7f4075/MercurialPaint/screenshot.jpg -------------------------------------------------------------------------------- /MercurialPaint/shaders/Shaders.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Shaders.metal 3 | // MercurialPaint 4 | // 5 | // Created by Simon Gladman on 04/12/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | float rand(int x, int y, int z); 13 | 14 | // Generate a random float in the range [0.0f, 1.0f] using x, y, and z (based on the xor128 algorithm) 15 | float rand(int x, int y, int z) 16 | { 17 | int seed = x + y * 57 + z * 241; 18 | seed= (seed<< 13) ^ seed; 19 | return (( 1.0 - ( (seed * (seed * seed * 15731 + 789221) + 1376312589) & 2147483647) / 1073741824.0f) + 1.0f) / 2.0f; 20 | } 21 | 22 | // mercurialPaintShader 23 | 24 | kernel void mercurialPaintShader(texture2d outTexture [[texture(0)]], 25 | const device int *inParticles [[ buffer(0) ]], 26 | 27 | constant int4 &xPosition [[ buffer(1) ]], 28 | constant int4 &yPosition [[ buffer(2) ]], 29 | constant float &touchForce [[ buffer(3) ]], 30 | 31 | uint id [[thread_position_in_grid]]) 32 | { 33 | 34 | for (int i = 0; i < 4; i++) 35 | { 36 | if (xPosition[i] < 0 || yPosition[i] < 0) 37 | { 38 | return; 39 | } 40 | 41 | const int randomSeed = inParticles[id]; 42 | 43 | const float randomAngle = rand(randomSeed + i, xPosition[i], yPosition[i]) * 6.283185; 44 | 45 | const float randomRadius = rand(randomSeed + i, yPosition[i], xPosition[i]) * (touchForce * 200); 46 | 47 | const int writeAtX = xPosition[i] + int(sin(randomAngle) * randomRadius); 48 | const int writeAtY = yPosition[i] + int(cos(randomAngle) * randomRadius); 49 | 50 | outTexture.write(float4(1, 1, 1, 1), uint2(writeAtX, writeAtY)); 51 | } 52 | } -------------------------------------------------------------------------------- /MercurialPaint/shadingImageEditor/ItemRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelledSlider.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | 13 | class ItemRenderer: UITableViewCell 14 | { 15 | let slider = LabelledSlider() 16 | 17 | var enabled: Bool = true 18 | { 19 | didSet 20 | { 21 | slider.enabled = enabled 22 | } 23 | } 24 | 25 | var parameter: Parameter? 26 | { 27 | didSet 28 | { 29 | slider.parameter = parameter 30 | } 31 | } 32 | 33 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) 34 | { 35 | super.init(style: style, reuseIdentifier: "ItemRenderer") 36 | 37 | backgroundColor = UIColor.blackColor() 38 | 39 | contentView.addSubview(slider) 40 | } 41 | 42 | override func layoutSubviews() 43 | { 44 | slider.frame = bounds 45 | } 46 | 47 | required init?(coder aDecoder: NSCoder) 48 | { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | } 52 | 53 | // ------------------- 54 | 55 | class LabelledSlider: UIControl 56 | { 57 | let label = UILabel() 58 | let valueLabel = UILabel() 59 | let slider = UISlider() 60 | 61 | override var enabled: Bool 62 | { 63 | didSet 64 | { 65 | userInteractionEnabled = enabled 66 | 67 | label.enabled = enabled 68 | valueLabel.enabled = enabled 69 | slider.enabled = enabled 70 | } 71 | } 72 | 73 | var parameter: Parameter? 74 | { 75 | didSet 76 | { 77 | guard let parameter = parameter else 78 | { 79 | label.text = "" 80 | return 81 | } 82 | 83 | label.text = parameter.name 84 | 85 | slider.minimumValue = parameter.minMax.min 86 | slider.maximumValue = parameter.minMax.max 87 | 88 | valueLabel.text = String(format: "%.2f", parameter.value) 89 | 90 | slider.value = Float(parameter.value) 91 | } 92 | } 93 | 94 | override init(frame: CGRect) 95 | { 96 | super.init(frame: frame) 97 | 98 | label.textColor = UIColor.whiteColor() 99 | valueLabel.textColor = UIColor.whiteColor() 100 | 101 | label.adjustsFontSizeToFitWidth = true 102 | valueLabel.adjustsFontSizeToFitWidth = true 103 | 104 | valueLabel.textAlignment = NSTextAlignment.Right 105 | 106 | addSubview(label) 107 | addSubview(valueLabel) 108 | addSubview(slider) 109 | 110 | label.numberOfLines = 0 111 | 112 | slider.addTarget(self, action: "sliderChangeHandler", forControlEvents: UIControlEvents.ValueChanged) 113 | } 114 | 115 | required init?(coder aDecoder: NSCoder) 116 | { 117 | fatalError("init(coder:) has not been implemented") 118 | } 119 | 120 | func sliderChangeHandler() 121 | { 122 | parameter?.value = CGFloat(slider.value) 123 | 124 | guard let parameter = parameter else 125 | { 126 | return 127 | } 128 | 129 | valueLabel.text = String(format: "%.2f", parameter.value) 130 | 131 | sendActionsForControlEvents(UIControlEvents.ValueChanged) 132 | } 133 | 134 | override func layoutSubviews() 135 | { 136 | label.frame = CGRect(x: 5, 137 | y: 2, 138 | width: frame.width / 2, 139 | height: label.intrinsicContentSize().height).insetBy(dx: 2, dy: 0) 140 | 141 | valueLabel.frame = CGRect(x: frame.width / 2, 142 | y: 2, 143 | width: frame.width / 2, 144 | height: valueLabel.intrinsicContentSize().height).insetBy(dx: 2, dy: 0) 145 | 146 | slider.frame = CGRect(x: 0, 147 | y: frame.height / 2 - 2, 148 | width: frame.width, 149 | height: slider.intrinsicContentSize().height).insetBy(dx: 2, dy: 0) 150 | 151 | layer.borderColor = UIColor.darkGrayColor().CGColor 152 | layer.borderWidth = 1 153 | } 154 | 155 | } -------------------------------------------------------------------------------- /MercurialPaint/shadingImageEditor/ShadingImageEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShadingImageEditor.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SceneKit 11 | 12 | 13 | class ShadingImageEditor: UIControl 14 | { 15 | let lights = [ 16 | OmniLight(), 17 | OmniLight(), 18 | OmniLight(), 19 | OmniLight() 20 | ] 21 | 22 | let parameterGroups = [ 23 | ParameterGroup(name: "Material", parameters: [ 24 | Parameter(name: "Shininess", parameterFunction: .AdjustMaterialShininess, value: 0.02, minMax: MinMax(min:0.001, max: 0.25)) 25 | ]), 26 | 27 | ParameterGroup(name: "Light 1", parameters: [ 28 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 0), value: 0.1, minMax: MinMaxNorm), 29 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 0), value: 1, minMax: MinMaxNorm), 30 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 0, axis: .X), value: 0, minMax: MinMaxXY), 31 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 0, axis: .Y), value: 25, minMax: MinMaxXY), 32 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 0, axis: .Z), value: 0, minMax: MinMaxZ) 33 | ]), 34 | 35 | ParameterGroup(name: "Light 2", parameters: [ 36 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 1), value: 0.48, minMax: MinMaxNorm), 37 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 1), value: 1, minMax: MinMaxNorm), 38 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 1, axis: .X), value: 25, minMax: MinMaxXY), 39 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 1, axis: .Y), value: -35, minMax: MinMaxXY), 40 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 1, axis: .Z), value: 0, minMax: MinMaxZ) 41 | ]), 42 | 43 | ParameterGroup(name: "Light 3", parameters: [ 44 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 2), value: 0.85, minMax: MinMaxNorm), 45 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 2), value: 1, minMax: MinMaxNorm), 46 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 2, axis: .X), value: -35, minMax: MinMaxXY), 47 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 2, axis: .Y), value: -20, minMax: MinMaxXY), 48 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 2, axis: .Z), value: -10, minMax: MinMaxZ) 49 | ]), 50 | 51 | ParameterGroup(name: "Light 4", parameters: [ 52 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 3), value: 0.18, minMax: MinMaxNorm), 53 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 3), value: 1, minMax: MinMaxNorm), 54 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 3, axis: .X), value: -35, minMax: MinMaxXY), 55 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 3, axis: .Y), value: 10, minMax: MinMaxXY), 56 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 3, axis: .Z), value: 35, minMax: MinMaxZ) 57 | ]) 58 | ] 59 | 60 | var lightPositionWidgets = [LightPositionWidget]() 61 | 62 | let material = SCNMaterial() 63 | 64 | let sceneKitView = SCNView() 65 | let tableView = UITableView() 66 | 67 | var sceneChanged = false 68 | 69 | var image: UIImage? 70 | { 71 | return sceneKitView.snapshot() 72 | } 73 | 74 | override init(frame: CGRect) 75 | { 76 | super.init(frame: frame) 77 | 78 | sceneKitView.layer.borderColor = UIColor.darkGrayColor().CGColor 79 | sceneKitView.layer.borderWidth = 1 80 | 81 | sceneKitView.delegate = self 82 | 83 | addSubview(sceneKitView) 84 | addSubview(tableView) 85 | 86 | tableView.dataSource = self 87 | tableView.delegate = self 88 | 89 | setUpSceneKit() 90 | applyAllParameters() 91 | 92 | tableView.rowHeight = 60 93 | 94 | tableView.registerClass(ItemRenderer.self, 95 | forCellReuseIdentifier: "ItemRenderer") 96 | 97 | for (index, _) in lights.enumerate() 98 | { 99 | let lightWidget = LightPositionWidget(index: index) 100 | 101 | lightPositionWidgets.append(lightWidget) 102 | sceneKitView.addSubview(lightWidget) 103 | } 104 | 105 | positionLightWidgets() 106 | colorLightWidgets() 107 | } 108 | 109 | required init?(coder aDecoder: NSCoder) 110 | { 111 | fatalError("init(coder:) has not been implemented") 112 | } 113 | 114 | func positionLightWidgets() 115 | { 116 | let width = frame.width 117 | 118 | for (widget, light) in zip(lightPositionWidgets, lights) 119 | { 120 | let lightPosition = CGPoint(x: width * CGFloat((light.position.x - MinMaxXY.min) / (MinMaxXY.max - MinMaxXY.min)) - 10, 121 | y: width - (width * CGFloat((light.position.y - MinMaxXY.min) / (MinMaxXY.max - MinMaxXY.min))) - 10 122 | ) 123 | 124 | widget.frame = CGRect(origin: lightPosition, 125 | size: CGSize(width: 20, height: 20)) 126 | } 127 | } 128 | 129 | func colorLightWidgets() 130 | { 131 | for (widget, light) in zip(lightPositionWidgets, lights) 132 | { 133 | widget.backgroundColor = UIColor(hue: light.hue, 134 | saturation: 1, 135 | brightness: light.brightness, 136 | alpha: 1) 137 | } 138 | } 139 | 140 | func setUpSceneKit() 141 | { 142 | sceneKitView.backgroundColor = UIColor.blackColor() 143 | 144 | let sphere = SCNSphere(radius: 1) 145 | let sphereNode = SCNNode(geometry: sphere) 146 | 147 | let scene = SCNScene() 148 | 149 | sceneKitView.scene = scene 150 | 151 | let camera = SCNCamera() 152 | 153 | camera.usesOrthographicProjection = true 154 | camera.orthographicScale = 1 155 | 156 | let cameraNode = SCNNode() 157 | 158 | cameraNode.camera = camera 159 | cameraNode.position = SCNVector3(x: 0, y: 0, z: 2) 160 | 161 | scene.rootNode.addChildNode(cameraNode) 162 | 163 | // sphere... 164 | 165 | sphereNode.position = SCNVector3(x: 0, y: 0, z: 0) 166 | scene.rootNode.addChildNode(sphereNode) 167 | 168 | for light in lights 169 | { 170 | scene.rootNode.addChildNode(light) 171 | } 172 | 173 | material.lightingModelName = SCNLightingModelPhong 174 | material.specular.contents = UIColor.whiteColor() 175 | material.diffuse.contents = UIColor.darkGrayColor() 176 | material.shininess = 0.15 177 | 178 | sphere.materials = [material] 179 | } 180 | 181 | func sliderChangeHandler(slider: LabelledSlider) 182 | { 183 | guard let parameter = slider.parameter else 184 | { 185 | return 186 | } 187 | 188 | updateSceneFromParameter(parameter) 189 | } 190 | 191 | func applyAllParameters() 192 | { 193 | for group in parameterGroups 194 | { 195 | for parameter in group.parameters 196 | { 197 | updateSceneFromParameter(parameter) 198 | } 199 | } 200 | } 201 | 202 | func updateSceneFromParameter(parameter: Parameter) 203 | { 204 | switch parameter.parameterFunction 205 | { 206 | case let .AdjustLightPosition(index, axis): 207 | switch axis 208 | { 209 | case .X: 210 | lights[index].position.x = Float(parameter.value) 211 | case .Y: 212 | lights[index].position.y = Float(parameter.value) 213 | case .Z: 214 | lights[index].position.z = Float(parameter.value) 215 | } 216 | 217 | positionLightWidgets() 218 | 219 | case .AdjustMaterialShininess: 220 | material.shininess = parameter.value 221 | 222 | case let .AdjustLightHue(index): 223 | lights[index].hue = parameter.value 224 | colorLightWidgets() 225 | 226 | case let .AdjustLightBrightness(index): 227 | lights[index].brightness = parameter.value 228 | colorLightWidgets() 229 | } 230 | 231 | sceneChanged = true 232 | } 233 | 234 | override func layoutSubviews() 235 | { 236 | sceneKitView.frame = CGRect(x: 0, 237 | y: 0, 238 | width: frame.width, 239 | height: frame.width) 240 | 241 | tableView.frame = CGRect(x: 0, 242 | y: frame.width, 243 | width: frame.width, 244 | height: frame.height - frame.width) 245 | 246 | tableView.separatorStyle = UITableViewCellSeparatorStyle.None 247 | 248 | positionLightWidgets() 249 | } 250 | 251 | } 252 | 253 | // MARK: scene renderer delegate 254 | 255 | extension ShadingImageEditor: SCNSceneRendererDelegate 256 | { 257 | func renderer(renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: NSTimeInterval) 258 | { 259 | guard sceneChanged else 260 | { 261 | return 262 | } 263 | 264 | sceneChanged = false 265 | 266 | dispatch_async(dispatch_get_main_queue()) 267 | { 268 | self.sendActionsForControlEvents(UIControlEvents.ValueChanged) 269 | } 270 | } 271 | } 272 | 273 | // MARK: table delegate 274 | 275 | extension ShadingImageEditor: UITableViewDelegate 276 | { 277 | 278 | } 279 | 280 | // MARK: table view datasource 281 | 282 | extension ShadingImageEditor: UITableViewDataSource 283 | { 284 | func numberOfSectionsInTableView(tableView: UITableView) -> Int 285 | { 286 | return parameterGroups.count 287 | } 288 | 289 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int 290 | { 291 | return parameterGroups[section].parameters.count 292 | } 293 | 294 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell 295 | { 296 | let cell = tableView.dequeueReusableCellWithIdentifier("ItemRenderer", 297 | forIndexPath: indexPath) as! ItemRenderer 298 | 299 | cell.parameter = parameterGroups[indexPath.section].parameters[indexPath.item] 300 | 301 | cell.slider.addTarget(self, action: "sliderChangeHandler:", forControlEvents: UIControlEvents.ValueChanged) 302 | 303 | return cell 304 | } 305 | 306 | func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? 307 | { 308 | return parameterGroups[section].name 309 | } 310 | } 311 | 312 | // MARK: Light position widget 313 | 314 | class LightPositionWidget: UIView 315 | { 316 | let label = UILabel() 317 | 318 | required init(index: Int) 319 | { 320 | super.init(frame: CGRectZero) 321 | 322 | layer.borderWidth = 1 323 | layer.borderColor = UIColor.whiteColor().CGColor 324 | 325 | label.text = "\(index + 1)" 326 | label.font = UIFont.boldSystemFontOfSize(12) 327 | label.textColor = UIColor.darkGrayColor() 328 | label.textAlignment = NSTextAlignment.Center 329 | 330 | addSubview(label) 331 | } 332 | 333 | required init?(coder aDecoder: NSCoder) 334 | { 335 | fatalError("init(coder:) has not been implemented") 336 | } 337 | 338 | override var backgroundColor:UIColor? 339 | { 340 | didSet 341 | { 342 | if let color = backgroundColor 343 | { 344 | var white: CGFloat = 0 345 | var alpha: CGFloat = 0 346 | 347 | color.getWhite(&white, alpha: &alpha) 348 | 349 | label.textColor = white < 0.5 ? 350 | UIColor.whiteColor() : 351 | UIColor.blackColor() 352 | } 353 | } 354 | } 355 | 356 | override func layoutSubviews() 357 | { 358 | layer.cornerRadius = min(frame.width, frame.height) / 2 359 | 360 | label.frame = bounds 361 | 362 | label.shadowColor = UIColor.whiteColor() 363 | label.shadowOffset = CGSize(width: 0, height: 0) 364 | } 365 | 366 | } 367 | -------------------------------------------------------------------------------- /MercurialPaint/shadingImageEditor/SupportClasses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SupportClasses.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | class ParameterGroup 12 | { 13 | init(name: String, parameters: [Parameter]) 14 | { 15 | self.name = name 16 | self.parameters = parameters 17 | } 18 | 19 | let name: String 20 | let parameters: [Parameter] 21 | } 22 | 23 | class Parameter 24 | { 25 | init(name: String, parameterFunction: ParameterFunction, value: CGFloat, minMax: MinMax) 26 | { 27 | self.name = name 28 | self.parameterFunction = parameterFunction 29 | self.value = value 30 | self.minMax = minMax 31 | } 32 | 33 | let name: String 34 | let parameterFunction: ParameterFunction 35 | var value: CGFloat 36 | let minMax: MinMax 37 | } 38 | 39 | enum ParameterFunction 40 | { 41 | case AdjustLightPosition(index: Int, axis: PositionAxis) 42 | case AdjustLightHue(index: Int) 43 | case AdjustLightBrightness(index: Int) 44 | case AdjustMaterialShininess 45 | } 46 | 47 | enum PositionAxis 48 | { 49 | case X 50 | case Y 51 | case Z 52 | } 53 | 54 | typealias MinMax = (min: Float, max: Float) 55 | 56 | let MinMaxNorm = MinMax(min: 0, max: 1) 57 | let MinMaxXY = MinMax(min: -50, max: 50) 58 | let MinMaxZ = MinMax(min: -10, max: 50) 59 | 60 | class OmniLight: SCNNode 61 | { 62 | init(x: Float = 0, y: Float = 0, z: Float = 0, hue: CGFloat = 0, brightness: CGFloat = 0) 63 | { 64 | self.x = x 65 | self.y = y 66 | self.z = z 67 | 68 | self.hue = hue 69 | self.brightness = brightness 70 | 71 | super.init() 72 | 73 | let omniLight = SCNLight() 74 | omniLight.type = SCNLightTypeOmni 75 | 76 | light = omniLight 77 | 78 | position = SCNVector3(x: x, y: y, z: z) 79 | 80 | updateLightColor() 81 | } 82 | 83 | required init?(coder aDecoder: NSCoder) 84 | { 85 | fatalError("init(coder:) has not been implemented") 86 | } 87 | 88 | let x: Float 89 | let y: Float 90 | let z: Float 91 | 92 | var hue: CGFloat 93 | { 94 | didSet 95 | { 96 | updateLightColor() 97 | } 98 | } 99 | 100 | var brightness: CGFloat 101 | { 102 | didSet 103 | { 104 | updateLightColor() 105 | } 106 | } 107 | 108 | func updateLightColor() 109 | { 110 | light?.color = UIColor(hue: hue, 111 | saturation: 1, 112 | brightness: brightness, 113 | alpha: 1) 114 | } 115 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MercurialPaint 2 | Mercurial Painting using Metal and Core Image 3 | 4 | ![screenshot](MercurialPaint/screenshot.jpg) 5 | 6 | Following on from my recent MercurialText experiment, here's another implementation of `CIHeightFieldFromMask` and `CIShadedMaterial`, MercurialPaint. MercurialPaint is an Apple Pencil driven sketching app that uses Metal and Metal Performance Shaders to create a skeletal or scaffold texture followed by a Core Image step to apply the 3D embossed effect based on an image of a hemisphere created with Scene Kit. 7 | 8 | My MercurialText post discusses the Core Image and Scene Kit steps, so this article begins by discussing how I create the scaffold image using Metal. 9 | 10 | ## Initialising Metal 11 | 12 | MercurialPaint is a `UIView` that contains a Metal Kit `MTKView` to display the "in-flight" scaffold image and a standard `UIImageView` for displaying the final image. The technique I use for drawing the pixels borrows from my ParticleLab project: for each touch move, I want to colour in 2,048 pixels randomly positioned around the touch location. To do that, I create a handful of variables that will hold the particle data: 13 | 14 | private var particlesMemory:UnsafeMutablePointer = nil 15 | private var particlesVoidPtr: COpaquePointer! 16 | private var particlesParticlePtr: UnsafeMutablePointer! 17 | private var particlesParticleBufferPtr: UnsafeMutableBufferPointer! 18 | 19 | private var particlesBufferNoCopy: MTLBuffer! 20 | 21 | Each particle item will hold a random value that the Metal shader will use as a seed for its own random number generator which will define the particle's position. So, after using the magic `posix_memalign()` function and initialising the pointers and buffers: 22 | 23 | posix_memalign(&particlesMemory, alignment, particlesMemoryByteSize) 24 | 25 | particlesVoidPtr = COpaquePointer(particlesMemory) 26 | particlesParticlePtr = UnsafeMutablePointer(particlesVoidPtr) 27 | particlesParticleBufferPtr = UnsafeMutableBufferPointer(start: particlesParticlePtr, 28 | count: particleCount) 29 | 30 | I populate the buffer pointer with random values: 31 | 32 | for index in particlesParticleBufferPtr.startIndex ..< particlesParticleBufferPtr.endIndex 33 | { 34 | particlesParticleBufferPtr[index] = Int(arc4random_uniform(9999)) 35 | 36 | } 37 | 38 | ...and create the a new Metal buffer to share the particles between Swift and Metal: 39 | 40 | particlesBufferNoCopy = device.newBufferWithBytesNoCopy(particlesMemory, 41 | length: Int(particlesMemoryByteSize), 42 | options: MTLResourceOptions.StorageModeShared, 43 | deallocator: nil) 44 | 45 | ## Touch Handling 46 | 47 | The Apple Pencil can sample at 240hz and the `touchesMoved` will only ever be invoked at a maximum of 60hz, so MercurialPaint makes use of coalesced touches. To simplify the transfer of the touch data between Swift and Metal, I only support up to four coalesced touches and in `touchesMoved()`, I take a note of the locations of the coalesced touches and the force of the first: 48 | 49 | override func touchesMoved(touches: Set, withEvent event: UIEvent?) 50 | { 51 | guard let touch = touches.first, 52 | coalescedTouches = event?.coalescedTouchesForTouch(touch) else 53 | { 54 | return 55 | } 56 | 57 | touchForce = touch.type == .Stylus 58 | ? Float(touch.force / touch.maximumPossibleForce) 59 | : 0.5 60 | 61 | touchLocations = coalescedTouches.map 62 | { 63 | return $0.locationInView(self) 64 | } 65 | } 66 | 67 | When the touches end, I reset the touch locations to (-1, -1): 68 | 69 | touchLocations = [CGPoint](count: 4, 70 | repeatedValue: CGPoint(x: -1, y: -1)) 71 | 72 | To simplify the transfer of data between Swift and Metal, the touch locations are converted to two separate `vector_int4` values - one the the four 'x' co-ordinates and one for the four 'y' co-ordinates. So, inside the Metal view's delegate's `drawInMTKView()`, I create buffers to hold that location data and populate them using a little helper function, `touchLocationsToVector()`: 73 | 74 | var xLocation = touchLocationsToVector(.X) 75 | let xLocationBuffer = device.newBufferWithBytes(&xLocation, 76 | length: sizeof(vector_int4), 77 | options: MTLResourceOptions.CPUCacheModeDefaultCache) 78 | 79 | var yLocation = touchLocationsToVector(.Y) 80 | let yLocationBuffer = device.newBufferWithBytes(&yLocation, 81 | length: sizeof(vector_int4), 82 | options: MTLResourceOptions.CPUCacheModeDefaultCache) 83 | 84 | The force of the touch is passed to Metal as a float value. 85 | 86 | ## Mercurial Paint Compute Shader 87 | 88 | The compute shader to create the scaffold image is pretty basic stuff. Along with the particles list, it's also passed the four 'x' and four 'y' positions and the normalised force of the touch. 89 | 90 | We know from above if there's no coalesced touch for one of the items in the position vectors, the value will be -1, so the first job of the shader is to loop over the vectors and exit the function if 'x' or 'y' is -1: 91 | 92 | for (int i = 0; i < 4; i++) 93 | { 94 | if (xPosition[i] < 0 || yPosition[i] < 0) 95 | { 96 | return; 97 | 98 | } 99 | 100 | ...then use the value of the 'particle' as a random seed and create a random angle and radius, which is based on the touch force: 101 | 102 | const float randomAngle = rand(randomSeed + i, xPosition[i], yPosition[i]) * 6.283185; 103 | 104 | const float randomRadius = rand(randomSeed + i, yPosition[i], xPosition[i]) * (touchForce * 200); 105 | 106 | ...and, finally, draw a pixel to the texture using those randomly generated values: 107 | 108 | const int writeAtX = xPosition[i] + int(sin(randomAngle) * randomRadius); 109 | const int writeAtY = yPosition[i] + int(cos(randomAngle) * randomRadius); 110 | 111 | outTexture.write(float4(1, 1, 1, 1), uint2(writeAtX, writeAtY)); 112 | 113 | ## Metaball Effect with Metal Performance Shaders 114 | 115 | Much like my Globular project, I use a Gaussian blur and threshold to convert the individual pixels drawn by the shader into a more liquid type image. Because I'm using Metal, rather than using Core Image, I use Metal Performance Shaders. These are lazily created: 116 | 117 | lazy var blur: MPSImageGaussianBlur = 118 | { 119 | [unowned self] in 120 | 121 | return MPSImageGaussianBlur(device: self.device, 122 | sigma: 3) 123 | }() 124 | 125 | lazy var threshold: MPSImageThresholdBinary = 126 | { 127 | [unowned self] in 128 | 129 | return MPSImageThresholdBinary(device: self.device, 130 | thresholdValue: 0.5, 131 | maximumValue: 1, 132 | linearGrayColorTransform: nil) 133 | 134 | }() 135 | 136 | ..and once the compute shader has finished, the two filters are applied to the output texture and end up targeting the Metal Kit view's drawable's texture: 137 | 138 | blur.encodeToCommandBuffer(commandBuffer, 139 | sourceTexture: paintingTexture, 140 | destinationTexture: intermediateTexture) 141 | 142 | threshold.encodeToCommandBuffer(commandBuffer, 143 | sourceTexture: intermediateTexture, 144 | destinationTexture: drawable.texture) 145 | 146 | ## Core Image Embossing Step 147 | 148 | As I mentioned above, for the full description of the embossing step, see my MercurialText post. In MercurialPaint, I wait for the touches end to apply these filters. The only real differences are that I create a CIImage from the Metal Kit view's drawable's texture: 149 | 150 | let mercurialImage = CIImage(MTLTexture: drawable.texture, options: nil) 151 | 152 | I've also added an additional Core Image filter, CIMaskToAlpha to use as the source image to the height map filter. 153 | 154 | ## Conclusion 155 | 156 | This could be one of my favourite experimental projects so far: it mashes up Scene Kit, Core Image, Metal and Metal Performance Shaders and it uses force data! Hopefully, it's another example of not only the ludicrous power of the iPad Pro, Metal and Core Image but the creative potential of what can be achieved by combining Apple's range of different frameworks. 157 | 158 | This project isn't a final product and, as such, is sprinkled with magic numbers. 159 | 160 | As always, the source code for this project is available at my GitHub repository here. 161 | --------------------------------------------------------------------------------