├── LICENCE ├── MTLTextureView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── astemireleev.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MTLTextureView ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── barca.imageset │ │ ├── Contents.json │ │ └── pnp58EI9BY0jhz3kPNTcLk1Y2IdYgRM9nMC75bUgNcY.jpeg │ └── comic-magazine-cover-template.imageset │ │ ├── Contents.json │ │ └── comic-magazine-cover-template.heic ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Core │ ├── MetalRenderer.swift │ └── TextureView.swift ├── Info.plist ├── SceneDelegate.swift ├── Shaders │ ├── Defaults.metal │ └── PageCurl.metal └── ViewController.swift └── README.md /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Astemir Eleev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MTLTextureView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A9A84A2529DA43820006411D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A84A2429DA43820006411D /* AppDelegate.swift */; }; 11 | A9A84A2729DA43820006411D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A84A2629DA43820006411D /* SceneDelegate.swift */; }; 12 | A9A84A2929DA43820006411D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A84A2829DA43820006411D /* ViewController.swift */; }; 13 | A9A84A2C29DA43820006411D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A9A84A2A29DA43820006411D /* Main.storyboard */; }; 14 | A9A84A2E29DA43830006411D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9A84A2D29DA43830006411D /* Assets.xcassets */; }; 15 | A9A84A3129DA43830006411D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A9A84A2F29DA43830006411D /* LaunchScreen.storyboard */; }; 16 | A9A84A3E29DA43A60006411D /* Defaults.metal in Sources */ = {isa = PBXBuildFile; fileRef = A9A84A3929DA43A60006411D /* Defaults.metal */; }; 17 | A9A84A3F29DA43A60006411D /* PageCurl.metal in Sources */ = {isa = PBXBuildFile; fileRef = A9A84A3A29DA43A60006411D /* PageCurl.metal */; }; 18 | A9A84A4029DA43A60006411D /* MetalRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A84A3C29DA43A60006411D /* MetalRenderer.swift */; }; 19 | A9A84A4129DA43A60006411D /* TextureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A84A3D29DA43A60006411D /* TextureView.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | A9A84A2129DA43820006411D /* MTLTextureView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MTLTextureView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | A9A84A2429DA43820006411D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | A9A84A2629DA43820006411D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 26 | A9A84A2829DA43820006411D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | A9A84A2B29DA43820006411D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | A9A84A2D29DA43830006411D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | A9A84A3029DA43830006411D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | A9A84A3229DA43830006411D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | A9A84A3929DA43A60006411D /* Defaults.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = Defaults.metal; sourceTree = ""; }; 32 | A9A84A3A29DA43A60006411D /* PageCurl.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = PageCurl.metal; sourceTree = ""; }; 33 | A9A84A3C29DA43A60006411D /* MetalRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetalRenderer.swift; sourceTree = ""; }; 34 | A9A84A3D29DA43A60006411D /* TextureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextureView.swift; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | A9A84A1E29DA43820006411D /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | A9A84A1829DA43820006411D = { 49 | isa = PBXGroup; 50 | children = ( 51 | A9A84A2329DA43820006411D /* MTLTextureView */, 52 | A9A84A2229DA43820006411D /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | A9A84A2229DA43820006411D /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | A9A84A2129DA43820006411D /* MTLTextureView.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | A9A84A2329DA43820006411D /* MTLTextureView */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | A9A84A2429DA43820006411D /* AppDelegate.swift */, 68 | A9A84A2629DA43820006411D /* SceneDelegate.swift */, 69 | A9A84A2829DA43820006411D /* ViewController.swift */, 70 | A9A84A2A29DA43820006411D /* Main.storyboard */, 71 | A9A84A2D29DA43830006411D /* Assets.xcassets */, 72 | A9A84A2F29DA43830006411D /* LaunchScreen.storyboard */, 73 | A9A84A3229DA43830006411D /* Info.plist */, 74 | A9A84A3B29DA43A60006411D /* Core */, 75 | A9A84A3829DA43A60006411D /* Shaders */, 76 | ); 77 | path = MTLTextureView; 78 | sourceTree = ""; 79 | }; 80 | A9A84A3829DA43A60006411D /* Shaders */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | A9A84A3929DA43A60006411D /* Defaults.metal */, 84 | A9A84A3A29DA43A60006411D /* PageCurl.metal */, 85 | ); 86 | path = Shaders; 87 | sourceTree = ""; 88 | }; 89 | A9A84A3B29DA43A60006411D /* Core */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | A9A84A3C29DA43A60006411D /* MetalRenderer.swift */, 93 | A9A84A3D29DA43A60006411D /* TextureView.swift */, 94 | ); 95 | path = Core; 96 | sourceTree = ""; 97 | }; 98 | /* End PBXGroup section */ 99 | 100 | /* Begin PBXNativeTarget section */ 101 | A9A84A2029DA43820006411D /* MTLTextureView */ = { 102 | isa = PBXNativeTarget; 103 | buildConfigurationList = A9A84A3529DA43830006411D /* Build configuration list for PBXNativeTarget "MTLTextureView" */; 104 | buildPhases = ( 105 | A9A84A1D29DA43820006411D /* Sources */, 106 | A9A84A1E29DA43820006411D /* Frameworks */, 107 | A9A84A1F29DA43820006411D /* Resources */, 108 | ); 109 | buildRules = ( 110 | ); 111 | dependencies = ( 112 | ); 113 | name = MTLTextureView; 114 | productName = MTLTextureView; 115 | productReference = A9A84A2129DA43820006411D /* MTLTextureView.app */; 116 | productType = "com.apple.product-type.application"; 117 | }; 118 | /* End PBXNativeTarget section */ 119 | 120 | /* Begin PBXProject section */ 121 | A9A84A1929DA43820006411D /* Project object */ = { 122 | isa = PBXProject; 123 | attributes = { 124 | BuildIndependentTargetsInParallel = 1; 125 | LastSwiftUpdateCheck = 1430; 126 | LastUpgradeCheck = 1430; 127 | TargetAttributes = { 128 | A9A84A2029DA43820006411D = { 129 | CreatedOnToolsVersion = 14.3; 130 | }; 131 | }; 132 | }; 133 | buildConfigurationList = A9A84A1C29DA43820006411D /* Build configuration list for PBXProject "MTLTextureView" */; 134 | compatibilityVersion = "Xcode 14.0"; 135 | developmentRegion = en; 136 | hasScannedForEncodings = 0; 137 | knownRegions = ( 138 | en, 139 | Base, 140 | ); 141 | mainGroup = A9A84A1829DA43820006411D; 142 | productRefGroup = A9A84A2229DA43820006411D /* Products */; 143 | projectDirPath = ""; 144 | projectRoot = ""; 145 | targets = ( 146 | A9A84A2029DA43820006411D /* MTLTextureView */, 147 | ); 148 | }; 149 | /* End PBXProject section */ 150 | 151 | /* Begin PBXResourcesBuildPhase section */ 152 | A9A84A1F29DA43820006411D /* Resources */ = { 153 | isa = PBXResourcesBuildPhase; 154 | buildActionMask = 2147483647; 155 | files = ( 156 | A9A84A3129DA43830006411D /* LaunchScreen.storyboard in Resources */, 157 | A9A84A2E29DA43830006411D /* Assets.xcassets in Resources */, 158 | A9A84A2C29DA43820006411D /* Main.storyboard in Resources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXResourcesBuildPhase section */ 163 | 164 | /* Begin PBXSourcesBuildPhase section */ 165 | A9A84A1D29DA43820006411D /* Sources */ = { 166 | isa = PBXSourcesBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | A9A84A4129DA43A60006411D /* TextureView.swift in Sources */, 170 | A9A84A3F29DA43A60006411D /* PageCurl.metal in Sources */, 171 | A9A84A2929DA43820006411D /* ViewController.swift in Sources */, 172 | A9A84A4029DA43A60006411D /* MetalRenderer.swift in Sources */, 173 | A9A84A2529DA43820006411D /* AppDelegate.swift in Sources */, 174 | A9A84A2729DA43820006411D /* SceneDelegate.swift in Sources */, 175 | A9A84A3E29DA43A60006411D /* Defaults.metal in Sources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXSourcesBuildPhase section */ 180 | 181 | /* Begin PBXVariantGroup section */ 182 | A9A84A2A29DA43820006411D /* Main.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | A9A84A2B29DA43820006411D /* Base */, 186 | ); 187 | name = Main.storyboard; 188 | sourceTree = ""; 189 | }; 190 | A9A84A2F29DA43830006411D /* LaunchScreen.storyboard */ = { 191 | isa = PBXVariantGroup; 192 | children = ( 193 | A9A84A3029DA43830006411D /* Base */, 194 | ); 195 | name = LaunchScreen.storyboard; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXVariantGroup section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | A9A84A3329DA43830006411D /* 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++20"; 208 | CLANG_ENABLE_MODULES = YES; 209 | CLANG_ENABLE_OBJC_ARC = YES; 210 | CLANG_ENABLE_OBJC_WEAK = YES; 211 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 212 | CLANG_WARN_BOOL_CONVERSION = YES; 213 | CLANG_WARN_COMMA = YES; 214 | CLANG_WARN_CONSTANT_CONVERSION = YES; 215 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INFINITE_RECURSION = YES; 221 | CLANG_WARN_INT_CONVERSION = YES; 222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 224 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 225 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 226 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 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 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = dwarf; 235 | ENABLE_STRICT_OBJC_MSGSEND = YES; 236 | ENABLE_TESTABILITY = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu11; 238 | GCC_DYNAMIC_NO_PIC = NO; 239 | GCC_NO_COMMON_BLOCKS = YES; 240 | GCC_OPTIMIZATION_LEVEL = 0; 241 | GCC_PREPROCESSOR_DEFINITIONS = ( 242 | "DEBUG=1", 243 | "$(inherited)", 244 | ); 245 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 246 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 247 | GCC_WARN_UNDECLARED_SELECTOR = YES; 248 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 249 | GCC_WARN_UNUSED_FUNCTION = YES; 250 | GCC_WARN_UNUSED_VARIABLE = YES; 251 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 252 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 253 | MTL_FAST_MATH = YES; 254 | ONLY_ACTIVE_ARCH = YES; 255 | SDKROOT = iphoneos; 256 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 257 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 258 | }; 259 | name = Debug; 260 | }; 261 | A9A84A3429DA43830006411D /* Release */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 268 | CLANG_ENABLE_MODULES = YES; 269 | CLANG_ENABLE_OBJC_ARC = YES; 270 | CLANG_ENABLE_OBJC_WEAK = YES; 271 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 272 | CLANG_WARN_BOOL_CONVERSION = YES; 273 | CLANG_WARN_COMMA = YES; 274 | CLANG_WARN_CONSTANT_CONVERSION = YES; 275 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 276 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 277 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 278 | CLANG_WARN_EMPTY_BODY = YES; 279 | CLANG_WARN_ENUM_CONVERSION = YES; 280 | CLANG_WARN_INFINITE_RECURSION = YES; 281 | CLANG_WARN_INT_CONVERSION = YES; 282 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 283 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 284 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 286 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 287 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 288 | CLANG_WARN_STRICT_PROTOTYPES = YES; 289 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 290 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 291 | CLANG_WARN_UNREACHABLE_CODE = YES; 292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 293 | COPY_PHASE_STRIP = NO; 294 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 295 | ENABLE_NS_ASSERTIONS = NO; 296 | ENABLE_STRICT_OBJC_MSGSEND = YES; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | GCC_NO_COMMON_BLOCKS = YES; 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 306 | MTL_ENABLE_DEBUG_INFO = NO; 307 | MTL_FAST_MATH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_COMPILATION_MODE = wholemodule; 310 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 311 | VALIDATE_PRODUCT = YES; 312 | }; 313 | name = Release; 314 | }; 315 | A9A84A3629DA43830006411D /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 320 | CODE_SIGN_STYLE = Automatic; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEVELOPMENT_TEAM = T3K58FE38R; 323 | GENERATE_INFOPLIST_FILE = YES; 324 | INFOPLIST_FILE = MTLTextureView/Info.plist; 325 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 326 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 327 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 328 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 329 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 330 | LD_RUNPATH_SEARCH_PATHS = ( 331 | "$(inherited)", 332 | "@executable_path/Frameworks", 333 | ); 334 | MARKETING_VERSION = 1.0; 335 | PRODUCT_BUNDLE_IDENTIFIER = eleev.MTLTextureView; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | SWIFT_EMIT_LOC_STRINGS = YES; 338 | SWIFT_VERSION = 5.0; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Debug; 342 | }; 343 | A9A84A3729DA43830006411D /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 348 | CODE_SIGN_STYLE = Automatic; 349 | CURRENT_PROJECT_VERSION = 1; 350 | DEVELOPMENT_TEAM = T3K58FE38R; 351 | GENERATE_INFOPLIST_FILE = YES; 352 | INFOPLIST_FILE = MTLTextureView/Info.plist; 353 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 354 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 355 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 356 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 357 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 358 | LD_RUNPATH_SEARCH_PATHS = ( 359 | "$(inherited)", 360 | "@executable_path/Frameworks", 361 | ); 362 | MARKETING_VERSION = 1.0; 363 | PRODUCT_BUNDLE_IDENTIFIER = eleev.MTLTextureView; 364 | PRODUCT_NAME = "$(TARGET_NAME)"; 365 | SWIFT_EMIT_LOC_STRINGS = YES; 366 | SWIFT_VERSION = 5.0; 367 | TARGETED_DEVICE_FAMILY = "1,2"; 368 | }; 369 | name = Release; 370 | }; 371 | /* End XCBuildConfiguration section */ 372 | 373 | /* Begin XCConfigurationList section */ 374 | A9A84A1C29DA43820006411D /* Build configuration list for PBXProject "MTLTextureView" */ = { 375 | isa = XCConfigurationList; 376 | buildConfigurations = ( 377 | A9A84A3329DA43830006411D /* Debug */, 378 | A9A84A3429DA43830006411D /* Release */, 379 | ); 380 | defaultConfigurationIsVisible = 0; 381 | defaultConfigurationName = Release; 382 | }; 383 | A9A84A3529DA43830006411D /* Build configuration list for PBXNativeTarget "MTLTextureView" */ = { 384 | isa = XCConfigurationList; 385 | buildConfigurations = ( 386 | A9A84A3629DA43830006411D /* Debug */, 387 | A9A84A3729DA43830006411D /* Release */, 388 | ); 389 | defaultConfigurationIsVisible = 0; 390 | defaultConfigurationName = Release; 391 | }; 392 | /* End XCConfigurationList section */ 393 | }; 394 | rootObject = A9A84A1929DA43820006411D /* Project object */; 395 | } 396 | -------------------------------------------------------------------------------- /MTLTextureView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MTLTextureView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MTLTextureView.xcodeproj/xcuserdata/astemireleev.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MTLTextureView.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MTLTextureView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MTLTextureView 4 | // 5 | // Created by Astemir Eleev on 02.04.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /MTLTextureView/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MTLTextureView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MTLTextureView/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MTLTextureView/Assets.xcassets/barca.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pnp58EI9BY0jhz3kPNTcLk1Y2IdYgRM9nMC75bUgNcY.jpeg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MTLTextureView/Assets.xcassets/barca.imageset/pnp58EI9BY0jhz3kPNTcLk1Y2IdYgRM9nMC75bUgNcY.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleev/MTLTextureView/14b62789fc4aabd74e860c4e60342070c8faa9e2/MTLTextureView/Assets.xcassets/barca.imageset/pnp58EI9BY0jhz3kPNTcLk1Y2IdYgRM9nMC75bUgNcY.jpeg -------------------------------------------------------------------------------- /MTLTextureView/Assets.xcassets/comic-magazine-cover-template.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "comic-magazine-cover-template.heic", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MTLTextureView/Assets.xcassets/comic-magazine-cover-template.imageset/comic-magazine-cover-template.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleev/MTLTextureView/14b62789fc4aabd74e860c4e60342070c8faa9e2/MTLTextureView/Assets.xcassets/comic-magazine-cover-template.imageset/comic-magazine-cover-template.heic -------------------------------------------------------------------------------- /MTLTextureView/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 | -------------------------------------------------------------------------------- /MTLTextureView/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 | -------------------------------------------------------------------------------- /MTLTextureView/Core/MetalRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalRenderer.swift 3 | // TextureView 4 | // 5 | // Created by Astemir Eleev on 02.04.2023. 6 | // 7 | 8 | import Foundation 9 | import Metal 10 | import MetalKit 11 | 12 | /// Bare-bones metal renderer implementation. By default it's lazy e.g. draws only when data changes. The shaders are pretty straightforward, however you can use it create additional shader transitions and adjust for your needs. 13 | final class MetalRenderer: NSObject, MTKViewDelegate { 14 | 15 | // MARK: - Properties 16 | 17 | private weak var mtkView: MTKView? 18 | private let device: MTLDevice 19 | private let commandQueue: MTLCommandQueue 20 | private let renderPipelineState: MTLRenderPipelineState 21 | private let samplerState: MTLSamplerState 22 | private var texture: MTLTexture 23 | 24 | private let defaultImageScaleFactor: CGFloat = 1.0 25 | private let vertexShaderName: String = "vertex_passthrough" 26 | private let fragmentShaderName: String = "sampling_linear" 27 | 28 | // MARK: - Initializers 29 | 30 | init?(mtkView: MTKView, imageName name: String, in bundle: Bundle = .main) { 31 | guard let device = MTLCreateSystemDefaultDevice(), 32 | let commandQueue = device.makeCommandQueue() else { 33 | return nil 34 | } 35 | self.device = device 36 | self.commandQueue = commandQueue 37 | 38 | self.mtkView = mtkView 39 | mtkView.device = device 40 | mtkView.colorPixelFormat = .bgra8Unorm 41 | mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1) 42 | mtkView.autoResizeDrawable = true 43 | 44 | let layer = mtkView.layer as? CAMetalLayer 45 | layer?.framebufferOnly = true 46 | layer?.isOpaque = false 47 | layer?.maximumDrawableCount = 2 48 | 49 | guard let library = device.makeDefaultLibrary(), 50 | let vertexFunction = library.makeFunction(name: vertexShaderName), 51 | let fragmentFunction = library.makeFunction(name: fragmentShaderName) else { 52 | return nil 53 | } 54 | 55 | func pipelineDescriptor() -> MTLRenderPipelineDescriptor { 56 | let pipelineDescriptor = MTLRenderPipelineDescriptor() 57 | pipelineDescriptor.vertexFunction = vertexFunction 58 | pipelineDescriptor.fragmentFunction = fragmentFunction 59 | pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat 60 | pipelineDescriptor.colorAttachments[0].isBlendingEnabled = false 61 | return pipelineDescriptor 62 | } 63 | 64 | func vertexDescriptor() -> MTLVertexDescriptor { 65 | let vertexDescriptor = MTLVertexDescriptor() 66 | 67 | // Position attribute 68 | vertexDescriptor.attributes[0].format = .float2 69 | vertexDescriptor.attributes[0].offset = 0 70 | vertexDescriptor.attributes[0].bufferIndex = 0 71 | 72 | // Texture coordinates attribute 73 | vertexDescriptor.attributes[1].format = .float2 74 | vertexDescriptor.attributes[1].offset = MemoryLayout.stride 75 | vertexDescriptor.attributes[1].bufferIndex = 0 76 | 77 | vertexDescriptor.layouts[0].stride = MemoryLayout.stride 78 | vertexDescriptor.layouts[0].stepRate = 1 79 | vertexDescriptor.layouts[0].stepFunction = .perVertex 80 | 81 | return vertexDescriptor 82 | } 83 | 84 | let pipelineDescriptor = pipelineDescriptor() 85 | pipelineDescriptor.vertexDescriptor = vertexDescriptor() 86 | 87 | do { 88 | renderPipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) 89 | } catch { 90 | print("Failed to create render pipeline state: \(error)") 91 | return nil 92 | } 93 | 94 | func samplerDescriptor() -> MTLSamplerDescriptor { 95 | let samplerDescriptor = MTLSamplerDescriptor() 96 | samplerDescriptor.minFilter = .linear 97 | samplerDescriptor.magFilter = .linear 98 | samplerDescriptor.mipFilter = .linear 99 | samplerDescriptor.sAddressMode = .clampToEdge 100 | samplerDescriptor.tAddressMode = .clampToEdge 101 | 102 | return samplerDescriptor 103 | } 104 | 105 | guard let sampler = device.makeSamplerState(descriptor: samplerDescriptor()) else { 106 | print("Failed to create sampler state") 107 | return nil 108 | } 109 | samplerState = sampler 110 | 111 | let textureLoader = MTKTextureLoader(device: device) 112 | guard let texture = try? textureLoader.newTexture( 113 | name: name, 114 | scaleFactor: defaultImageScaleFactor, 115 | bundle: bundle 116 | ) else { 117 | print("Failed to load texture") 118 | return nil 119 | } 120 | self.texture = texture 121 | 122 | super.init() 123 | 124 | mtkView.delegate = self 125 | } 126 | 127 | // MARK: - Delegates 128 | 129 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 130 | // Handle any necessary updates due to a drawable size change 131 | _draw(in: view) 132 | } 133 | 134 | func draw(in view: MTKView) { 135 | // If you need to redraw each update cycle 136 | /* 137 | _draw(in: view) 138 | */ 139 | } 140 | 141 | // MARK: - Methods 142 | 143 | func setImage(named name: String, in bundle: Bundle = .main) { 144 | guard let mtkView else { 145 | fatalError("Attempted to assign an image while the view has been released") 146 | } 147 | let textureLoader = MTKTextureLoader(device: device) 148 | 149 | guard let texture = try? textureLoader.newTexture( 150 | name: name, 151 | scaleFactor: defaultImageScaleFactor, 152 | bundle: bundle 153 | ) else { 154 | print("Failed to load texture") 155 | return 156 | } 157 | 158 | self.texture = texture 159 | _draw(in: mtkView) 160 | } 161 | 162 | // MARK: - Implmentation Details 163 | 164 | private func _draw(in view: MTKView) { 165 | guard let drawable = view.currentDrawable, 166 | let renderPassDescriptor = view.currentRenderPassDescriptor, 167 | let commandBuffer = commandQueue.makeCommandBuffer(), 168 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { 169 | return 170 | } 171 | 172 | renderPassDescriptor.colorAttachments[0].loadAction = .clear 173 | renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0) 174 | 175 | func aspectFit() -> [Vertex] { 176 | let imageAspect = Float(texture.width) / Float(texture.height) 177 | let viewAspect = Float(view.bounds.width) / Float(view.bounds.height) 178 | 179 | var scaleX: Float = 1.0 180 | var scaleY: Float = 1.0 181 | 182 | if viewAspect > imageAspect { 183 | scaleY = imageAspect / viewAspect 184 | } else { 185 | scaleX = viewAspect / imageAspect 186 | } 187 | 188 | let quadVertices = [ 189 | Vertex(position: [-scaleY, -scaleX], texCoord: [0, 1]), 190 | Vertex(position: [-scaleY, scaleX], texCoord: [0, 0]), 191 | Vertex(position: [ scaleY, -scaleX], texCoord: [1, 1]), 192 | Vertex(position: [ scaleY, scaleX], texCoord: [1, 0]) 193 | ] 194 | return quadVertices 195 | } 196 | 197 | let quadVertices = aspectFit() 198 | 199 | guard let vertexBuffer = device.makeBuffer(bytes: quadVertices, length: MemoryLayout.stride * quadVertices.count, options: []) else { 200 | print("Failed to create vertex buffer") 201 | return 202 | } 203 | renderEncoder.setRenderPipelineState(renderPipelineState) 204 | 205 | renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) 206 | renderEncoder.setFragmentTexture(texture, index: 0) 207 | renderEncoder.setFragmentSamplerState(samplerState, index: 0) 208 | renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) 209 | 210 | renderEncoder.endEncoding() 211 | commandBuffer.present(drawable) 212 | commandBuffer.commit() 213 | } 214 | } 215 | 216 | struct Vertex { 217 | var position: simd_float2 218 | var texCoord: simd_float2 219 | } 220 | -------------------------------------------------------------------------------- /MTLTextureView/Core/TextureView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextureView.swift 3 | // TextureView 4 | // 5 | // Created by Astemir Eleev on 02.04.2023. 6 | // 7 | 8 | import UIKit 9 | import MetalKit 10 | 11 | final class TextureView: UIView { 12 | 13 | // MARK: - Properties 14 | 15 | weak var mtkView: MTKView? 16 | private var metalRenderer: MetalRenderer? 17 | 18 | // MARK: - Initializers 19 | 20 | init(named name: String, in bundle: Bundle = .main) { 21 | super.init(frame: .zero) 22 | 23 | let metalView = MTKView(frame: bounds) 24 | metalView.translatesAutoresizingMaskIntoConstraints = false 25 | addSubview(metalView) 26 | 27 | NSLayoutConstraint.activate([ 28 | metalView.leadingAnchor.constraint(equalTo: leadingAnchor), 29 | metalView.trailingAnchor.constraint(equalTo: trailingAnchor), 30 | metalView.topAnchor.constraint(equalTo: topAnchor), 31 | metalView.bottomAnchor.constraint(equalTo: bottomAnchor), 32 | ]) 33 | mtkView = metalView 34 | 35 | metalRenderer = MetalRenderer( 36 | mtkView: metalView, 37 | imageName: name, 38 | in: bundle 39 | ) 40 | } 41 | 42 | required init?(coder: NSCoder) { 43 | fatalError("init(coder:) has not been implemented") 44 | } 45 | 46 | // MARK: - Methods 47 | 48 | func setImage(named name: String, in bundle: Bundle = .main) { 49 | metalRenderer?.setImage(named: name, in: bundle) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MTLTextureView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /MTLTextureView/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // MTLTextureView 4 | // 5 | // Created by Astemir Eleev on 02.04.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /MTLTextureView/Shaders/Defaults.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Shaders.metal 3 | // TextureView 4 | // 5 | // Created by Astemir Eleev on 02.04.2023. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | // MARK: - Vertex Shaders 12 | 13 | // Define the input vertex attributes 14 | struct VertexIn { 15 | float2 position [[attribute(0)]]; 16 | float2 texCoord [[attribute(1)]]; 17 | }; 18 | 19 | // Define the output vertex attributes 20 | struct VertexOut { 21 | float2 texCoord [[user(locn0)]]; 22 | float4 position [[position]]; 23 | }; 24 | 25 | // The vertex_passthrough function 26 | vertex VertexOut vertex_passthrough(VertexIn in [[stage_in]]) { 27 | VertexOut out; 28 | 29 | // Pass through the position and texture coordinates 30 | out.position = float4(in.position, 0.0, 1.0); 31 | out.texCoord = in.texCoord; 32 | 33 | return out; 34 | } 35 | 36 | // Experimental page curl vertex transformation. Note that `curlAmount` needs to be set and update in the Renderer 37 | vertex VertexOut page_curl_vertex(VertexIn in [[stage_in]], constant float2 &curlAmount [[buffer(1)]]) { 38 | VertexOut out; 39 | 40 | float2 pos = in.position; 41 | float2 uv = in.texCoord; 42 | 43 | float2 curlPos = pos * curlAmount; 44 | float curlLength = length(curlPos); 45 | float angle = (1.0 - curlLength) * 1.5 * atan2(curlAmount.y, curlAmount.x); 46 | 47 | float2x2 rotationMatrix = float2x2(cos(angle), -sin(angle), 48 | sin(angle), cos(angle)); 49 | 50 | pos = pos - curlPos; 51 | pos = rotationMatrix * pos; 52 | pos = pos + curlPos; 53 | 54 | float folded = pos.x < curlPos.x ? 1.0 : 0.0; 55 | 56 | out.position = float4(pos, folded, 1.0); 57 | out.texCoord = uv; 58 | 59 | return out; 60 | } 61 | 62 | // MARK: - Fragment Shaders 63 | 64 | // Define the input fragment attributes 65 | struct FragmentIn { 66 | float2 texCoord [[user(locn0)]]; 67 | }; 68 | 69 | // The sampling_linear function 70 | fragment float4 sampling_linear(FragmentIn in [[stage_in]], 71 | texture2d inputTexture [[texture(0)]], 72 | sampler inputSampler [[sampler(0)]]) { 73 | // Sample the input texture using the input sampler and texture coordinates 74 | float4 color = inputTexture.sample(inputSampler, in.texCoord); 75 | 76 | return color; 77 | } 78 | 79 | 80 | fragment float4 page_curl_fragment(VertexOut in [[stage_in]], 81 | texture2d tex [[texture(0)]], 82 | sampler smp [[sampler(0)]]) { 83 | float2 uv = in.texCoord; 84 | 85 | float folded = in.position.z; 86 | 87 | float2 flippedTexCoord = float2(1.0 - uv.x, uv.y); 88 | float4 color = tex.sample(smp, uv); 89 | float4 flippedColor = tex.sample(smp, flippedTexCoord); 90 | 91 | float reflection = 0.25 * (1.0 - smoothstep(0.1, 0.9, uv.x)) * folded; 92 | color = mix(color, flippedColor, reflection); 93 | 94 | return color; 95 | } 96 | -------------------------------------------------------------------------------- /MTLTextureView/Shaders/PageCurl.metal: -------------------------------------------------------------------------------- 1 | // 2 | // PageCurl.metal 3 | // TextureView 4 | // 5 | // Created by Astemir Eleev on 02.04.2023. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | /* 12 | Experimental additions for shader page flip transition 13 | */ 14 | 15 | struct VertexIn { 16 | float2 position [[attribute(0)]]; 17 | float2 texCoord [[attribute(1)]]; 18 | }; 19 | 20 | struct VertexOut { 21 | float4 position [[position]]; 22 | float2 texCoord; 23 | float3 normal; 24 | float folded; 25 | }; 26 | 27 | vertex VertexOut page_flip_vertex(VertexIn in [[stage_in]], 28 | constant float2 &curlPosition [[buffer(0)]], 29 | constant float &curlAngle [[buffer(1)]]) { 30 | VertexOut out; 31 | 32 | float2 pos = in.position; 33 | float2 uv = in.texCoord; 34 | 35 | float2 curlPos = curlPosition * float2(2.0, 1.0) - 1.0; 36 | float foldDirection = uv.x <= curlPosition.x ? -1.0 : 1.0; 37 | float angle = curlAngle * foldDirection; 38 | 39 | float2x2 rotationMatrix = float2x2(cos(angle), -sin(angle), 40 | sin(angle), cos(angle)); 41 | 42 | pos = pos - curlPos; 43 | pos = rotationMatrix * pos; 44 | pos = pos + curlPos; 45 | 46 | out.position = float4(pos, 0.0, 1.0); 47 | out.texCoord = uv; 48 | out.normal = float3(0.0, 0.0, 1.0); 49 | out.folded = uv.x <= curlPosition.x ? 1.0 : 0.0; 50 | 51 | return out; 52 | } 53 | 54 | fragment float4 page_flip_fragment(VertexOut in [[stage_in]], 55 | texture2d tex [[texture(0)]], 56 | sampler smp [[sampler(0)]]) { 57 | float2 uv = in.texCoord; 58 | 59 | float folded = in.folded; 60 | 61 | float2 flippedTexCoord = float2(1.0 - uv.x, uv.y); 62 | float4 color = tex.sample(smp, uv); 63 | float4 flippedColor = tex.sample(smp, flippedTexCoord); 64 | 65 | float reflection = 0.25 * (1.0 - smoothstep(0.1, 0.9, uv.x)) * folded; 66 | color = mix(color, flippedColor, reflection); 67 | 68 | return color; 69 | } 70 | -------------------------------------------------------------------------------- /MTLTextureView/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MTLTextureView 4 | // 5 | // Created by Astemir Eleev on 02.04.2023. 6 | // 7 | 8 | import UIKit 9 | import Metal 10 | import MetalKit 11 | import os 12 | 13 | class ViewController: UIViewController { 14 | let metalViewLog = OSLog(subsystem: "com.eleev.textureview", category: "Metal View Initialization") 15 | let imageNames: [String] = [ 16 | "comic-magazine-cover-template", 17 | "barca" 18 | ] 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | let begin = OSSignpostID(log: metalViewLog) 24 | os_signpost(.begin, log: metalViewLog, name: "Init & Layout", signpostID: begin) 25 | 26 | // Instantiate the view 27 | let metalView = TextureView(named: imageNames[0]) 28 | metalView.translatesAutoresizingMaskIntoConstraints = false 29 | view.addSubview(metalView) 30 | 31 | // Add constraints 32 | NSLayoutConstraint.activate([ 33 | metalView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 34 | metalView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 35 | metalView.topAnchor.constraint(equalTo: view.topAnchor), 36 | metalView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 37 | ]) 38 | 39 | 40 | func update(imageIndex: Int, delay: TimeInterval = 5) { 41 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in 42 | guard let self else { return } 43 | metalView.setImage(named: imageNames[imageIndex]) 44 | update(imageIndex: (imageIndex + 1) % imageNames.count) 45 | } 46 | } 47 | update(imageIndex: 0) 48 | 49 | os_signpost(.end, log: metalViewLog, name: "Init & Layout", signpostID: begin) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MTLTextureView [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) 2 | 3 | [![Language](https://img.shields.io/badge/Language-Swift_5.8-orange.svg)]() 4 | [![Framework](https://img.shields.io/badge/Framework-Metal-red.svg)]() 5 | [![Shaders](https://img.shields.io/badge/Platforms-iOS|iPadOS-green.svg)]() 6 | [![Last Commit](https://img.shields.io/github/last-commit/jvirus/MTLTextureView)]() 7 | [![NLOC](https://img.shields.io/tokei/lines/github/jvirus/MTLTextureView)]() 8 | [![Contributors](https://img.shields.io/github/contributors/jvirus/MTLTextureView)]() 9 | [![Repo Size](https://img.shields.io/github/repo-size/jvirus/MTLTextureView)]() 10 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)]() 11 | 12 | ### If you like the project, please give it a star ⭐ It will show the creator your appreciation and help others to discover the repo. 13 | 14 | # ✍️ About 15 | 🖼️ Render images as Metal textures. Expandalbe for effects and transtiions. 16 | 17 | # 👨‍💻 Author 18 | [Astemir Eleev](https://github.com/jVirus) 19 | 20 | # 🔖 Licence 21 | The project is availabe under the [MIT License](https://github.com/jVirus/MTLTextureView/blob/main/LICENCE). 22 | --------------------------------------------------------------------------------