├── .gitignore ├── LICENSE.txt ├── MetalPaintDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── MetalPaintDemo.xcscheme ├── MetalPaintDemo ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Classes │ ├── AppDelegate.swift │ ├── Extensions │ │ ├── FloatingPointExtension.swift │ │ └── UIViewExtension.swift │ ├── Shaders │ │ └── PaintShader.metal │ └── ViewController.swift └── Info.plist └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | 70 | # Other 71 | .DS_Store 72 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, aptpod, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MetalPaintDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | EA784D66239E444000B64981 /* FloatingPointExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA784D65239E444000B64981 /* FloatingPointExtension.swift */; }; 11 | EA889AA023991E7E00BD7BD4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA889A9F23991E7E00BD7BD4 /* AppDelegate.swift */; }; 12 | EA889AA223991E7E00BD7BD4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA889AA123991E7E00BD7BD4 /* ViewController.swift */; }; 13 | EA889AA523991E7E00BD7BD4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EA889AA323991E7E00BD7BD4 /* Main.storyboard */; }; 14 | EA889AA723991E8200BD7BD4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EA889AA623991E8200BD7BD4 /* Assets.xcassets */; }; 15 | EA889AAA23991E8200BD7BD4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EA889AA823991E8200BD7BD4 /* LaunchScreen.storyboard */; }; 16 | EA889AB42399214C00BD7BD4 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA889AB32399214C00BD7BD4 /* UIViewExtension.swift */; }; 17 | EA889AB72399292E00BD7BD4 /* PaintShader.metal in Sources */ = {isa = PBXBuildFile; fileRef = EA889AB62399292E00BD7BD4 /* PaintShader.metal */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | EA784D65239E444000B64981 /* FloatingPointExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPointExtension.swift; sourceTree = ""; }; 22 | EA889A9C23991E7E00BD7BD4 /* MetalPaintDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MetalPaintDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | EA889A9F23991E7E00BD7BD4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | EA889AA123991E7E00BD7BD4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | EA889AA423991E7E00BD7BD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | EA889AA623991E8200BD7BD4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | EA889AA923991E8200BD7BD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | EA889AAB23991E8200BD7BD4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | EA889AB32399214C00BD7BD4 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; 30 | EA889AB62399292E00BD7BD4 /* PaintShader.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = PaintShader.metal; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | EA889A9923991E7E00BD7BD4 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | EA889A9323991E7E00BD7BD4 = { 45 | isa = PBXGroup; 46 | children = ( 47 | EA889A9E23991E7E00BD7BD4 /* MetalPaintDemo */, 48 | EA889A9D23991E7E00BD7BD4 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | EA889A9D23991E7E00BD7BD4 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | EA889A9C23991E7E00BD7BD4 /* MetalPaintDemo.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | EA889A9E23991E7E00BD7BD4 /* MetalPaintDemo */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | EA889AB123991EB500BD7BD4 /* Classes */, 64 | EA889AA323991E7E00BD7BD4 /* Main.storyboard */, 65 | EA889AA623991E8200BD7BD4 /* Assets.xcassets */, 66 | EA889AA823991E8200BD7BD4 /* LaunchScreen.storyboard */, 67 | EA889AAB23991E8200BD7BD4 /* Info.plist */, 68 | ); 69 | path = MetalPaintDemo; 70 | sourceTree = ""; 71 | }; 72 | EA889AB123991EB500BD7BD4 /* Classes */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | EA889AB52399290A00BD7BD4 /* Shaders */, 76 | EA889AB22399213D00BD7BD4 /* Extensions */, 77 | EA889A9F23991E7E00BD7BD4 /* AppDelegate.swift */, 78 | EA889AA123991E7E00BD7BD4 /* ViewController.swift */, 79 | ); 80 | path = Classes; 81 | sourceTree = ""; 82 | }; 83 | EA889AB22399213D00BD7BD4 /* Extensions */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | EA889AB32399214C00BD7BD4 /* UIViewExtension.swift */, 87 | EA784D65239E444000B64981 /* FloatingPointExtension.swift */, 88 | ); 89 | path = Extensions; 90 | sourceTree = ""; 91 | }; 92 | EA889AB52399290A00BD7BD4 /* Shaders */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | EA889AB62399292E00BD7BD4 /* PaintShader.metal */, 96 | ); 97 | path = Shaders; 98 | sourceTree = ""; 99 | }; 100 | /* End PBXGroup section */ 101 | 102 | /* Begin PBXNativeTarget section */ 103 | EA889A9B23991E7E00BD7BD4 /* MetalPaintDemo */ = { 104 | isa = PBXNativeTarget; 105 | buildConfigurationList = EA889AAE23991E8200BD7BD4 /* Build configuration list for PBXNativeTarget "MetalPaintDemo" */; 106 | buildPhases = ( 107 | EA889A9823991E7E00BD7BD4 /* Sources */, 108 | EA889A9923991E7E00BD7BD4 /* Frameworks */, 109 | EA889A9A23991E7E00BD7BD4 /* Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = MetalPaintDemo; 116 | productName = "iOS-MetalPaintDemo"; 117 | productReference = EA889A9C23991E7E00BD7BD4 /* MetalPaintDemo.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | EA889A9423991E7E00BD7BD4 /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastSwiftUpdateCheck = 1030; 127 | LastUpgradeCheck = 1030; 128 | ORGANIZATIONNAME = "aptpod,Inc."; 129 | TargetAttributes = { 130 | EA889A9B23991E7E00BD7BD4 = { 131 | CreatedOnToolsVersion = 10.3; 132 | }; 133 | }; 134 | }; 135 | buildConfigurationList = EA889A9723991E7E00BD7BD4 /* Build configuration list for PBXProject "MetalPaintDemo" */; 136 | compatibilityVersion = "Xcode 9.3"; 137 | developmentRegion = en; 138 | hasScannedForEncodings = 0; 139 | knownRegions = ( 140 | en, 141 | Base, 142 | ); 143 | mainGroup = EA889A9323991E7E00BD7BD4; 144 | productRefGroup = EA889A9D23991E7E00BD7BD4 /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | EA889A9B23991E7E00BD7BD4 /* MetalPaintDemo */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | EA889A9A23991E7E00BD7BD4 /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | EA889AAA23991E8200BD7BD4 /* LaunchScreen.storyboard in Resources */, 159 | EA889AA723991E8200BD7BD4 /* Assets.xcassets in Resources */, 160 | EA889AA523991E7E00BD7BD4 /* Main.storyboard in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXSourcesBuildPhase section */ 167 | EA889A9823991E7E00BD7BD4 /* Sources */ = { 168 | isa = PBXSourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | EA889AB72399292E00BD7BD4 /* PaintShader.metal in Sources */, 172 | EA889AB42399214C00BD7BD4 /* UIViewExtension.swift in Sources */, 173 | EA889AA223991E7E00BD7BD4 /* ViewController.swift in Sources */, 174 | EA784D66239E444000B64981 /* FloatingPointExtension.swift in Sources */, 175 | EA889AA023991E7E00BD7BD4 /* AppDelegate.swift in Sources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXSourcesBuildPhase section */ 180 | 181 | /* Begin PBXVariantGroup section */ 182 | EA889AA323991E7E00BD7BD4 /* Main.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | EA889AA423991E7E00BD7BD4 /* Base */, 186 | ); 187 | name = Main.storyboard; 188 | sourceTree = ""; 189 | }; 190 | EA889AA823991E8200BD7BD4 /* LaunchScreen.storyboard */ = { 191 | isa = PBXVariantGroup; 192 | children = ( 193 | EA889AA923991E8200BD7BD4 /* Base */, 194 | ); 195 | name = LaunchScreen.storyboard; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXVariantGroup section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | EA889AAC23991E8200BD7BD4 /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 228 | CLANG_WARN_STRICT_PROTOTYPES = YES; 229 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | CODE_SIGN_IDENTITY = "iPhone Developer"; 234 | COPY_PHASE_STRIP = NO; 235 | DEBUG_INFORMATION_FORMAT = dwarf; 236 | ENABLE_STRICT_OBJC_MSGSEND = YES; 237 | ENABLE_TESTABILITY = YES; 238 | GCC_C_LANGUAGE_STANDARD = gnu11; 239 | GCC_DYNAMIC_NO_PIC = NO; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_OPTIMIZATION_LEVEL = 0; 242 | GCC_PREPROCESSOR_DEFINITIONS = ( 243 | "DEBUG=1", 244 | "$(inherited)", 245 | ); 246 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 247 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 248 | GCC_WARN_UNDECLARED_SELECTOR = YES; 249 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 250 | GCC_WARN_UNUSED_FUNCTION = YES; 251 | GCC_WARN_UNUSED_VARIABLE = YES; 252 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 253 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 254 | MTL_FAST_MATH = YES; 255 | ONLY_ACTIVE_ARCH = YES; 256 | SDKROOT = iphoneos; 257 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 258 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 259 | }; 260 | name = Debug; 261 | }; 262 | EA889AAD23991E8200BD7BD4 /* Release */ = { 263 | isa = XCBuildConfiguration; 264 | buildSettings = { 265 | ALWAYS_SEARCH_USER_PATHS = NO; 266 | CLANG_ANALYZER_NONNULL = YES; 267 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 268 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 269 | CLANG_CXX_LIBRARY = "libc++"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_ENABLE_OBJC_WEAK = YES; 273 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_COMMA = YES; 276 | CLANG_WARN_CONSTANT_CONVERSION = YES; 277 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 278 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 279 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 280 | CLANG_WARN_EMPTY_BODY = YES; 281 | CLANG_WARN_ENUM_CONVERSION = YES; 282 | CLANG_WARN_INFINITE_RECURSION = YES; 283 | CLANG_WARN_INT_CONVERSION = YES; 284 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 286 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 288 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 289 | CLANG_WARN_STRICT_PROTOTYPES = YES; 290 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 291 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 292 | CLANG_WARN_UNREACHABLE_CODE = YES; 293 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 294 | CODE_SIGN_IDENTITY = "iPhone Developer"; 295 | COPY_PHASE_STRIP = NO; 296 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 297 | ENABLE_NS_ASSERTIONS = NO; 298 | ENABLE_STRICT_OBJC_MSGSEND = YES; 299 | GCC_C_LANGUAGE_STANDARD = gnu11; 300 | GCC_NO_COMMON_BLOCKS = YES; 301 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 302 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 303 | GCC_WARN_UNDECLARED_SELECTOR = YES; 304 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 305 | GCC_WARN_UNUSED_FUNCTION = YES; 306 | GCC_WARN_UNUSED_VARIABLE = YES; 307 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 308 | MTL_ENABLE_DEBUG_INFO = NO; 309 | MTL_FAST_MATH = YES; 310 | SDKROOT = iphoneos; 311 | SWIFT_COMPILATION_MODE = wholemodule; 312 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 313 | VALIDATE_PRODUCT = YES; 314 | }; 315 | name = Release; 316 | }; 317 | EA889AAF23991E8200BD7BD4 /* Debug */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 321 | CODE_SIGN_STYLE = Automatic; 322 | DEVELOPMENT_TEAM = ""; 323 | INFOPLIST_FILE = "$(SRCROOT)/MetalPaintDemo/Info.plist"; 324 | LD_RUNPATH_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "@executable_path/Frameworks", 327 | ); 328 | PRODUCT_BUNDLE_IDENTIFIER = "jp.co.aptpod.iOS-MetalPaintDemo"; 329 | PRODUCT_NAME = "$(TARGET_NAME)"; 330 | SWIFT_VERSION = 5.0; 331 | TARGETED_DEVICE_FAMILY = "1,2"; 332 | }; 333 | name = Debug; 334 | }; 335 | EA889AB023991E8200BD7BD4 /* Release */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | CODE_SIGN_STYLE = Automatic; 340 | DEVELOPMENT_TEAM = ""; 341 | INFOPLIST_FILE = "$(SRCROOT)/MetalPaintDemo/Info.plist"; 342 | LD_RUNPATH_SEARCH_PATHS = ( 343 | "$(inherited)", 344 | "@executable_path/Frameworks", 345 | ); 346 | PRODUCT_BUNDLE_IDENTIFIER = "jp.co.aptpod.iOS-MetalPaintDemo"; 347 | PRODUCT_NAME = "$(TARGET_NAME)"; 348 | SWIFT_VERSION = 5.0; 349 | TARGETED_DEVICE_FAMILY = "1,2"; 350 | }; 351 | name = Release; 352 | }; 353 | /* End XCBuildConfiguration section */ 354 | 355 | /* Begin XCConfigurationList section */ 356 | EA889A9723991E7E00BD7BD4 /* Build configuration list for PBXProject "MetalPaintDemo" */ = { 357 | isa = XCConfigurationList; 358 | buildConfigurations = ( 359 | EA889AAC23991E8200BD7BD4 /* Debug */, 360 | EA889AAD23991E8200BD7BD4 /* Release */, 361 | ); 362 | defaultConfigurationIsVisible = 0; 363 | defaultConfigurationName = Release; 364 | }; 365 | EA889AAE23991E8200BD7BD4 /* Build configuration list for PBXNativeTarget "MetalPaintDemo" */ = { 366 | isa = XCConfigurationList; 367 | buildConfigurations = ( 368 | EA889AAF23991E8200BD7BD4 /* Debug */, 369 | EA889AB023991E8200BD7BD4 /* Release */, 370 | ); 371 | defaultConfigurationIsVisible = 0; 372 | defaultConfigurationName = Release; 373 | }; 374 | /* End XCConfigurationList section */ 375 | }; 376 | rootObject = EA889A9423991E7E00BD7BD4 /* Project object */; 377 | } 378 | -------------------------------------------------------------------------------- /MetalPaintDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MetalPaintDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MetalPaintDemo.xcodeproj/xcshareddata/xcschemes/MetalPaintDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /MetalPaintDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /MetalPaintDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MetalPaintDemo/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 | -------------------------------------------------------------------------------- /MetalPaintDemo/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 | 44 | 62 | 80 | 98 | 119 | 133 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /MetalPaintDemo/Classes/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS-MetalPaintDemo 4 | // 5 | // Created by Ueno Masamitsu on 2019/12/05. 6 | // Copyright © 2019 aptpod,Inc. 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: [UIApplication.LaunchOptionsKey: Any]?) -> 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 invalidate graphics rendering callbacks. 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 active 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 | -------------------------------------------------------------------------------- /MetalPaintDemo/Classes/Extensions/FloatingPointExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatingPointExtension.swift 3 | // MetalPaintDemo 4 | // 5 | // Created by Ueno Masamitsu on 2019/12/09. 6 | // Copyright © 2019 aptpod,Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension FloatingPoint { 12 | 13 | var degreesToRadians: Self { return self * .pi / 180 } 14 | var radiansToDegrees: Self { return self * 180 / .pi } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /MetalPaintDemo/Classes/Extensions/UIViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtension.swift 3 | // iOS-MetalPaintDemo 4 | // 5 | // Created by Ueno Masamitsu on 2019/12/05. 6 | // Copyright © 2019 aptpod,Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | @IBInspectable var borderColor: UIColor? { 14 | get { 15 | return layer.borderColor.map { UIColor(cgColor: $0) } 16 | } 17 | set { 18 | layer.borderColor = newValue?.cgColor 19 | } 20 | } 21 | 22 | @IBInspectable var borderWidth: CGFloat { 23 | get { 24 | return layer.borderWidth 25 | } 26 | set { 27 | layer.borderWidth = newValue 28 | } 29 | } 30 | 31 | @IBInspectable var cornerRadius: CGFloat { 32 | get { 33 | return layer.cornerRadius 34 | } 35 | set { 36 | layer.cornerRadius = newValue 37 | layer.masksToBounds = newValue > 0 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /MetalPaintDemo/Classes/Shaders/PaintShader.metal: -------------------------------------------------------------------------------- 1 | // 2 | // PaintShader.metal 3 | // iOS-MetalPaintDemo 4 | // 5 | // Created by Ueno Masamitsu on 2019/12/05. 6 | // Copyright © 2019 aptpod,Inc. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | /* 13 | * |--|--|--|--| 14 | * | 0| 4| 8|12| 15 | * | 1| 5| 9|13| 16 | * | 2| 6|10|14| 17 | * | 3| 7|11|15| 18 | * |--|--|--|--| 19 | */ 20 | kernel void computeTexture2d(texture2d output [[texture(0)]], 21 | device float4 *color_buffer [[buffer(1)]], 22 | uint2 gid [[thread_position_in_grid]]) { 23 | int h = output.get_height(); 24 | int index = (gid.x * h) + gid.y; 25 | half r = color_buffer[index].x; 26 | half g = color_buffer[index].y; 27 | half b = color_buffer[index].z; 28 | half a = color_buffer[index].w; 29 | output.write(half4(r, g, b, a), gid); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /MetalPaintDemo/Classes/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MetalPaintDemo 4 | // 5 | // Created by Ueno Masamitsu on 2019/12/05. 6 | // Copyright © 2019 aptpod,Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MetalKit 11 | 12 | // Colors 13 | let kRed = UIColor.red 14 | let kGreen = UIColor.green 15 | let kBlue = UIColor.blue 16 | let kWhite = UIColor.white 17 | let kBlack = UIColor.black 18 | 19 | // Metal 20 | let kMetalTextureHeightDotSize: Int = 512 21 | let kMetalThreadGroupCount: Int = 16 22 | let kMetalTextureClearColor: simd_float4 = [255/255.0, 255/255.0, 255/255.0, 1.0] 23 | 24 | // PointSize 25 | let kMaxPointSize = 64 26 | let kDefaultPointSize = 1 27 | let kMinPointSize = 1 28 | 29 | class ViewController: UIViewController, MTKViewDelegate { 30 | 31 | // MARK:- viewDidLoad 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | // Do any additional setup after loading the view. 35 | self.log("viewDidLoad") 36 | 37 | // Setup Funcions 38 | self.setupViewEvents() 39 | self.setupMetalView() 40 | } 41 | 42 | //MARK:- View Events 43 | @IBOutlet weak var redBtn: UIButton! 44 | @IBOutlet weak var greenBtn: UIButton! 45 | @IBOutlet weak var blueBtn: UIButton! 46 | @IBOutlet weak var clearBtn: UIButton! 47 | 48 | private var isRed: Bool = false { 49 | didSet { 50 | self.redBtn.setTitleColor(self.isRed ? kWhite : kRed, for: .normal) 51 | self.redBtn.backgroundColor = self.isRed ? kRed : kBlack 52 | self.redBtn.layer.borderColor = self.isRed ? kWhite.cgColor : kRed.cgColor 53 | } 54 | } 55 | private var isGreen: Bool = false { 56 | didSet { 57 | self.greenBtn.setTitleColor(self.isGreen ? kWhite : kGreen, for: .normal) 58 | self.greenBtn.backgroundColor = self.isGreen ? kGreen : kBlack 59 | self.greenBtn.layer.borderColor = self.isGreen ? kWhite.cgColor : kGreen.cgColor 60 | } 61 | } 62 | private var isBlue: Bool = false { 63 | didSet { 64 | self.blueBtn.setTitleColor(self.isBlue ? kWhite : kBlue, for: .normal) 65 | self.blueBtn.backgroundColor = self.isBlue ? kBlue : kBlack 66 | self.blueBtn.layer.borderColor = self.isBlue ? kWhite.cgColor : kBlue.cgColor 67 | } 68 | } 69 | 70 | // PointSize 71 | @IBOutlet weak var pointSizeLabel: UILabel! 72 | var pointSize: Int = -1 { 73 | didSet { 74 | self.pointSizeLabel.text = "\(self.pointSize)pt" 75 | } 76 | } 77 | 78 | func setupViewEvents() { 79 | self.redBtn.addTarget(self, action: #selector(redBtnPushed(sender:)), for: .touchUpInside) 80 | self.greenBtn.addTarget(self, action: #selector(greenBtnPushed(sender:)), for: .touchUpInside) 81 | self.blueBtn.addTarget(self, action: #selector(blueBtnPushed(sender:)), for: .touchUpInside) 82 | self.clearBtn.addTarget(self, action: #selector(clearBtnPushed(sender:)), for: .touchUpInside) 83 | self.pointSize = kDefaultPointSize 84 | } 85 | 86 | @IBAction func redBtnPushed(sender: Any) { 87 | self.isRed = !self.isRed 88 | } 89 | 90 | @IBAction func greenBtnPushed(sender: Any) { 91 | self.isGreen = !self.isGreen 92 | } 93 | 94 | @IBAction func blueBtnPushed(sender: Any) { 95 | self.isBlue = !self.isBlue 96 | } 97 | 98 | @IBAction func clearBtnPushed(sender: Any) { 99 | self.setupMetalBuffer() 100 | } 101 | 102 | //MARK:- Change PointSize Events 103 | @IBAction func upBtnPushed(sender: Any) { 104 | var newPointSize = self.pointSize * 2 105 | if newPointSize > kMaxPointSize { 106 | newPointSize = kMaxPointSize 107 | } 108 | self.pointSize = newPointSize 109 | } 110 | 111 | @IBAction func downBtnPushed(sender: Any) { 112 | var newPointSize = self.pointSize / 2 113 | if newPointSize < kMinPointSize { 114 | newPointSize = kMinPointSize 115 | } 116 | self.pointSize = newPointSize 117 | } 118 | 119 | //MARK:- TouchEvents 120 | // 前回のタッチ位置 121 | var lastPoint: CGPoint? = nil 122 | 123 | // タッチイベントが開始された 124 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 125 | guard let touch = event?.touches(for: self.metalView)?.first else { return } 126 | let point = touch.location(in: self.metalView) 127 | self.log("touchBegan: \(point) - metalView") 128 | self.lastPoint = nil 129 | self.drawCanvas(point: point) 130 | } 131 | 132 | // タッチ位置が移動した 133 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 134 | guard let touch = event?.touches(for: self.metalView)?.first else { return } 135 | let point = touch.location(in: self.metalView) 136 | self.log("touchesMoved: \(point) - metalView") 137 | self.drawCanvas(point: point) 138 | } 139 | 140 | // タッチが終了した 141 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 142 | guard let touch = event?.touches(for: self.metalView)?.first else { return } 143 | let point = touch.location(in: self.metalView) 144 | self.log("touchesEnded: \(point) - metalView") 145 | self.drawCanvas(point: point) 146 | } 147 | 148 | // タッチがキャンセルされた 149 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 150 | self.log("touchesCancelled") 151 | } 152 | 153 | // MARK:- viewDidAppear 154 | override func viewDidAppear(_ animated: Bool) { 155 | self.log("viewDidAppear") 156 | self.updateMetalViewDrawableSize() 157 | } 158 | 159 | // MARK:- viewWillTransition 160 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 161 | self.log("viewWillTransition") 162 | self.metalView.delegate = nil // ここでMetalViewにDelegateを呼ばれない様にしてあげると管理が楽 163 | coordinator.animate(alongsideTransition: nil) { (_) in 164 | self.metalView.delegate = self 165 | self.updateMetalViewDrawableSize() 166 | } 167 | } 168 | 169 | // MARK:- Metal View 170 | @IBOutlet weak var metalView: MTKView! 171 | 172 | var mDevice = MTLCreateSystemDefaultDevice() 173 | var mCommandQueue: MTLCommandQueue! 174 | var mComputePiplineState: MTLComputePipelineState! 175 | var metalViewDrawableSize: CGSize? = nil 176 | var targetMetalTextureSize: CGSize = CGSize.zero 177 | var bufferWidth: Int = -1 178 | var mTextureBuffer: MTLBuffer? 179 | 180 | func setupMetalView() { 181 | guard let library = self.mDevice?.makeDefaultLibrary() else { return } 182 | // Register Texture Shader 183 | guard let kernel = library.makeFunction(name: "computeTexture2d") else { return } 184 | guard let computePipeline = try? self.mDevice?.makeComputePipelineState(function: kernel) else { return } 185 | self.mComputePiplineState = computePipeline 186 | self.metalView.device = self.mDevice 187 | self.metalView.delegate = self 188 | self.metalView.framebufferOnly = false // ← これがないとXcode11以降では落ちます 189 | self.mCommandQueue = self.mDevice?.makeCommandQueue() 190 | self.log("Success to setup metal view.") 191 | } 192 | 193 | func updateMetalViewDrawableSize() { 194 | // Viewの実際のフレームサイズから縦横比を参考に高さのドットサイズから幅のドットサイズを求める 195 | var width = Int(ceil((self.metalView.frame.width/self.metalView.frame.height)*CGFloat(kMetalTextureHeightDotSize))) 196 | // 書き換えた内容(1) ////// 197 | // 指定するThreadGroupCountで割り切れなければならない為調整をする 198 | let v: Int = width % kMetalThreadGroupCount 199 | if v > 0 { width -= v } 200 | // 書き換えた内容(1) /////// 201 | self.targetMetalTextureSize = CGSize(width: width, height: kMetalTextureHeightDotSize) 202 | self.log("drawableSize:\(self.metalView.drawableSize), frame:\(self.metalView.frame) => targetMetalTextureSize:\(self.targetMetalTextureSize) - MetalView") 203 | self.metalView.drawableSize = self.targetMetalTextureSize 204 | } 205 | 206 | func setupMetalBuffer() { 207 | guard let device = self.mDevice else { return } 208 | let colors = [simd_float4].init(repeating: kMetalTextureClearColor, count: self.bufferWidth*kMetalTextureHeightDotSize) 209 | let bufferLength = colors.count * MemoryLayout.stride // <- ここ要チェック 210 | self.mTextureBuffer = device.makeBuffer(bytes: UnsafeRawPointer(colors), length: bufferLength, options: .cpuCacheModeWriteCombined) 211 | } 212 | 213 | func drawCanvas(point: CGPoint) { 214 | let x: Int = Int(ceil((point.x/self.metalView.frame.width)*CGFloat(self.bufferWidth))) 215 | let y: Int = Int(ceil((point.y/self.metalView.frame.height)*CGFloat(kMetalTextureHeightDotSize))) 216 | var color: simd_float4 = [ self.isRed ? 1.0 : 0.0, self.isGreen ? 1.0 : 0.0, self.isBlue ? 1.0 : 0.0, 1.0 ] 217 | let dataSize = MemoryLayout.stride 218 | if let ptr = self.mTextureBuffer?.contents() { 219 | if 0 <= x, x < self.bufferWidth, 0 <= y, y < kMetalTextureHeightDotSize { 220 | let index = (x * kMetalTextureHeightDotSize) + y 221 | self.log("draw color[[\(x), \(y)] => \(index)]: \(color)") 222 | memcpy(ptr.advanced(by: index*dataSize), &color, dataSize) 223 | } 224 | } 225 | 226 | // 現在地点と前回地点の間に線を入れます 227 | var linePoints: [CGPoint] = [CGPoint]() 228 | let newPoint = CGPoint.init(x: x, y: y) 229 | defer { self.lastPoint = newPoint } // 前回値を保持しておく 230 | if let last = self.lastPoint, !last.equalTo(newPoint) { 231 | // 2点間の線を引く為のポイント一覧を取得し追加する 232 | linePoints.append(contentsOf: self.getLinePoints(p0: last, p1: newPoint)) 233 | } else { 234 | // ※同じ点を描画する事になるのであまり処理効率は良く無いですが説明上描画した値を入れます。 235 | linePoints.append(newPoint) 236 | } 237 | 238 | if self.pointSize > 1 { 239 | // 円描画 240 | var cirlePoints: [CGPoint] = [CGPoint]() 241 | for p in linePoints { 242 | cirlePoints.append(contentsOf: self.getCircleFillPoints(center: p, radius: self.pointSize/2)) 243 | } 244 | linePoints.append(contentsOf: cirlePoints) 245 | } 246 | 247 | if linePoints.count > 0, let ptr = self.mTextureBuffer?.contents() { 248 | for p in linePoints { 249 | let x = Int(p.x) 250 | let y = Int(p.y) 251 | guard x < self.bufferWidth, y < kMetalTextureHeightDotSize else { continue } 252 | let index = (x * kMetalTextureHeightDotSize) + y 253 | memcpy(ptr.advanced(by: index*dataSize), &color, dataSize) 254 | } 255 | } 256 | } 257 | 258 | // 2点間の線を引く為のポイント一覧を取得する 259 | // 参考(プレゼンハムのアルゴリズム): https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%AC%E3%82%BC%E3%83%B3%E3%83%8F%E3%83%A0%E3%81%AE%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0 260 | func getLinePoints(p0: CGPoint, p1: CGPoint) -> [CGPoint] { 261 | var points = [CGPoint]() 262 | var x0: Int = Int(p0.x) 263 | var y0: Int = Int(p0.y) 264 | let x1: Int = Int(p1.x) 265 | let y1: Int = Int(p1.y) 266 | let dx: Int = Int(abs(p1.x - p0.x)) // DeltaX 267 | let dy: Int = Int(abs(p1.y - p0.y)) // DeltaY 268 | let sx: Int = (p1.x>p0.x) ? 1 : -1 // StepX 269 | let sy: Int = (p1.y>p0.y) ? 1 : -1 // StepT 270 | var err = dx - dy 271 | while true { 272 | if x0 >= 0, y0 >= 0 { points.append(CGPoint(x: x0, y: y0)) } 273 | if x0 == x1, y0 == y1 { break } 274 | let e2 = 2*err 275 | if e2 > -dy { 276 | err -= dy 277 | x0 += sx 278 | } 279 | if e2 < dx { 280 | err += dx 281 | y0 += sy 282 | } 283 | } 284 | return points 285 | } 286 | 287 | // 中心点と半径から縁を描く為のポイント一覧を取得する 288 | // 参考(ブレゼンハム円描画のアルゴリズム): http://dencha.ojaru.jp/programs_07/pg_graphic_09a1.html 289 | func getCircleFillPoints(center: CGPoint, radius: Int) -> [CGPoint] { 290 | var points = [CGPoint]() 291 | 292 | let centerX: Int = Int(center.x) 293 | let centerY: Int = Int(center.y) 294 | var cx: Int = 0 295 | var cy: Int = radius 296 | var d: Int = 2 - 2 * radius 297 | 298 | // Left Top 299 | var ltx: Int = 0 300 | var lty: Int = 0 301 | // Right Top 302 | var rtx: Int = 0 303 | var rty: Int = 0 304 | // Left Bottom 305 | var lbx: Int = 0 306 | var lby: Int = 0 307 | // Right Bottom 308 | var rbx: Int = 0 309 | var rby: Int = 0 310 | 311 | // Top(0, R) 312 | var vx: Int = cx + centerX 313 | var vy: Int = cy + centerY 314 | if vx >= 0, vy >= 0 { points.append(CGPoint(x: vx, y: vy)) } 315 | // Bottom(0, -R) 316 | vx = cx + centerX 317 | vy = -cy + centerY 318 | if vx >= 0, vy >= 0 { points.append(CGPoint(x: vx, y: vy)) } 319 | // Right(R, 0) 320 | vx = cy + centerX 321 | vy = cx + centerY 322 | if vx >= 0, vy >= 0 { points.append(CGPoint(x: vx, y: vy)) } 323 | // Left(-R, 0) 324 | vx = -cy + centerX 325 | vy = cx + centerY 326 | if vx >= 0, vy >= 0 { points.append(CGPoint(x: vx, y: vy)) } 327 | 328 | while true { 329 | if d > -cy { 330 | cy -= 1 331 | d += 1 - 2 * cy 332 | } 333 | 334 | if d <= cx { 335 | cx += 1 336 | d += 1 + 2 * cx 337 | } 338 | 339 | guard cy > 0 else { break } 340 | 341 | // Right Bottom (Bottom To Right) 342 | rbx = cx + centerX 343 | rby = cy + centerY 344 | if rbx >= 0, rby >= 0 { 345 | points.append(CGPoint(x: rbx, y: rby)) // 0 ~ 90 346 | } 347 | // Left Bottom (Bottom To Left) 348 | lbx = -cx + centerX 349 | lby = cy + centerY 350 | if lbx >= 0, lby >= 0 { 351 | points.append(CGPoint(x: lbx, y: lby)) // 90 ~ 180 352 | } 353 | // Left Top (Top To Left) 354 | ltx = -cx + centerX 355 | lty = -cy + centerY 356 | if ltx >= 0, lty >= 0 { 357 | points.append(CGPoint(x: ltx, y: lty))// 180 ~ 270 358 | } 359 | // Right Top (Top To Right) 360 | rtx = cx + centerX 361 | rty = -cy + centerY 362 | if rtx >= 0, rty >= 0 { 363 | points.append(CGPoint(x: rtx, y: rty)) // 270 ~ 360 364 | } 365 | // 上半分は上部分から左右に、下半分はした部分から左右に伸びている 366 | //print("[\(ltx), \(lty)], [\(rtx), \(rty)], [\(lbx), \(lby)], [\(rbx), \(rby)]") 367 | // Y軸は左右同じ地点を指している事から上版分、下半分でX軸の左端から右端にポイントを追加する事で円を塗り潰します。 368 | for i in lbx...rbx { 369 | if i >= 0, rby >= 0 { points.append(CGPoint(x: i, y: rby)) } 370 | } 371 | for i in ltx...rtx { 372 | if i >= 0, lty >= 0 { points.append(CGPoint(x: i, y: lty)) } 373 | } 374 | } 375 | 376 | // 中心線 377 | for i in centerX-radius...centerX+radius { 378 | if i >= 0, centerY >= 0 { points.append(CGPoint(x: i, y: centerY)) } 379 | } 380 | 381 | return points 382 | } 383 | 384 | //MARK:- MTKViewDelegate 385 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 386 | self.log("drawableSizeWillChange \(view.drawableSize) => size:\(size), frame:\(self.metalView.frame), targetSize:\(self.targetMetalTextureSize) - MTKViewDelegate") 387 | guard !self.targetMetalTextureSize.equalTo(CGSize.zero) else { return } 388 | if self.metalViewDrawableSize != nil { 389 | guard !self.metalViewDrawableSize!.equalTo(self.targetMetalTextureSize) else { 390 | // 前回と同じ値だった場合は更新しない 391 | return 392 | } 393 | } 394 | self.metalViewDrawableSize = self.targetMetalTextureSize 395 | self.bufferWidth = Int(self.metalViewDrawableSize!.width) 396 | self.setupMetalBuffer() 397 | } 398 | 399 | 400 | func draw(in view: MTKView) { 401 | guard let drawable = view.currentDrawable else { return } 402 | guard let commandBuffer = self.mCommandQueue.makeCommandBuffer() else { return } 403 | guard let textureBuffer = self.mTextureBuffer else { return } 404 | let computeEncoder = commandBuffer.makeComputeCommandEncoder() 405 | computeEncoder?.setComputePipelineState(self.mComputePiplineState) 406 | let texture = drawable.texture 407 | // 書き込む対象のテクスチャをセット 408 | computeEncoder?.setTexture(texture, index: 0) 409 | // GPUに渡すテクスチャ用バッファをセット 410 | computeEncoder?.setBuffer(textureBuffer, offset: 0, index: 1) 411 | let threadGroupCount = MTLSizeMake(kMetalThreadGroupCount, kMetalThreadGroupCount, 1) 412 | let threadGroups = MTLSizeMake(Int(self.targetMetalTextureSize.width) / threadGroupCount.width, 413 | Int(self.targetMetalTextureSize.height) / threadGroupCount.height, 1) 414 | computeEncoder?.dispatchThreadgroups(threadGroups, 415 | threadsPerThreadgroup: threadGroupCount) 416 | computeEncoder?.endEncoding() 417 | commandBuffer.present(drawable) 418 | commandBuffer.commit() 419 | commandBuffer.waitUntilCompleted() 420 | } 421 | 422 | // MARK:- Other 423 | func log(_ message: String) { 424 | NSLog(message) 425 | } 426 | } 427 | 428 | -------------------------------------------------------------------------------- /MetalPaintDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarStyle 32 | UIStatusBarStyleLightContent 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UIViewControllerBasedStatusBarAppearance 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS-MetalPaintDemo 2 | 3 | MetalKitを利用したペイントツール開発デモ 4 | 5 | ## 動作・確認環境 6 | Xcode: Version 10.3、 11.3 7 | 8 | Swift: 5 9 | 10 | iPhone11 11 | 12 | iPad Pro11-inch 13 | --------------------------------------------------------------------------------