├── .gitignore ├── Atmos ├── Atmos.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Atmos │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── leohoho-PaMdJ6LpF84-unsplash-1024.png │ ├── Contents.json │ └── tofino.imageset │ │ ├── Contents.json │ │ └── thomas-lardeau-B5bwyCKgj4M-unsplash.jpg │ ├── AtmosApp.swift │ ├── Atmospheric Material │ ├── AtmosphericEffect.swift │ ├── Shaders │ │ ├── Common.h │ │ └── rain.metal │ └── Utils │ │ ├── AtmosphereView.swift │ │ ├── AtmosphericDestination.swift │ │ ├── AtmosphericMTKView.swift │ │ ├── AtmosphericPresenter.swift │ │ ├── AtmosphericRenderer.swift │ │ ├── AtmosphericSettings.swift │ │ ├── AtmosphericStack.swift │ │ ├── AtmosphericTexture.swift │ │ ├── GaussianBlur.swift │ │ └── ShaderUtils.swift │ ├── ContentView.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /Atmos/Atmos.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8B004F892853C96E00FE2D7B /* AtmosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004F882853C96E00FE2D7B /* AtmosApp.swift */; }; 11 | 8B004F8B2853C96E00FE2D7B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004F8A2853C96E00FE2D7B /* ContentView.swift */; }; 12 | 8B004F8D2853C96F00FE2D7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B004F8C2853C96F00FE2D7B /* Assets.xcassets */; }; 13 | 8B004F902853C96F00FE2D7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B004F8F2853C96F00FE2D7B /* Preview Assets.xcassets */; }; 14 | 8B004F982853C9A800FE2D7B /* AtmosphereView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004F972853C9A800FE2D7B /* AtmosphereView.swift */; }; 15 | 8B004F9E2853C9DF00FE2D7B /* AtmosphericTexture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004F9D2853C9DF00FE2D7B /* AtmosphericTexture.swift */; }; 16 | 8B004FA32853CD7C00FE2D7B /* ShaderUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004FA22853CD7C00FE2D7B /* ShaderUtils.swift */; }; 17 | 8B004FA52853CFA700FE2D7B /* AtmosphericRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004FA42853CFA700FE2D7B /* AtmosphericRenderer.swift */; }; 18 | 8B004FA72853D25300FE2D7B /* AtmosphericPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004FA62853D25300FE2D7B /* AtmosphericPresenter.swift */; }; 19 | 8B004FA92853D49300FE2D7B /* AtmosphericMTKView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004FA82853D49300FE2D7B /* AtmosphericMTKView.swift */; }; 20 | 8B004FB02853FB1B00FE2D7B /* rain.metal in Sources */ = {isa = PBXBuildFile; fileRef = 8B004FAF2853FB1B00FE2D7B /* rain.metal */; }; 21 | 8B004FB42853FD3700FE2D7B /* AtmosphericEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B004FB32853FD3700FE2D7B /* AtmosphericEffect.swift */; }; 22 | 8BEF8B822859AA41005EB80E /* AtmosphericStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEF8B812859AA41005EB80E /* AtmosphericStack.swift */; }; 23 | 8BEF8B88285A390E005EB80E /* AtmosphericDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEF8B87285A390E005EB80E /* AtmosphericDestination.swift */; }; 24 | 8BEF8B8A285A39B8005EB80E /* AtmosphericSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEF8B89285A39B8005EB80E /* AtmosphericSettings.swift */; }; 25 | 8BFD4E2E285C05BB00D759F4 /* GaussianBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFD4E2D285C05BB00D759F4 /* GaussianBlur.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 8B004F852853C96E00FE2D7B /* Atmos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Atmos.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 8B004F882853C96E00FE2D7B /* AtmosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosApp.swift; sourceTree = ""; }; 31 | 8B004F8A2853C96E00FE2D7B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 32 | 8B004F8C2853C96F00FE2D7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | 8B004F8F2853C96F00FE2D7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 34 | 8B004F972853C9A800FE2D7B /* AtmosphereView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphereView.swift; sourceTree = ""; }; 35 | 8B004F9D2853C9DF00FE2D7B /* AtmosphericTexture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphericTexture.swift; sourceTree = ""; }; 36 | 8B004FA22853CD7C00FE2D7B /* ShaderUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShaderUtils.swift; sourceTree = ""; }; 37 | 8B004FA42853CFA700FE2D7B /* AtmosphericRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphericRenderer.swift; sourceTree = ""; }; 38 | 8B004FA62853D25300FE2D7B /* AtmosphericPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphericPresenter.swift; sourceTree = ""; }; 39 | 8B004FA82853D49300FE2D7B /* AtmosphericMTKView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphericMTKView.swift; sourceTree = ""; }; 40 | 8B004FAF2853FB1B00FE2D7B /* rain.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = rain.metal; sourceTree = ""; }; 41 | 8B004FB32853FD3700FE2D7B /* AtmosphericEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphericEffect.swift; sourceTree = ""; }; 42 | 8B9B400C285D3FA800E8DFBE /* Common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Common.h; sourceTree = ""; }; 43 | 8BEF8B812859AA41005EB80E /* AtmosphericStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtmosphericStack.swift; sourceTree = ""; }; 44 | 8BEF8B87285A390E005EB80E /* AtmosphericDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphericDestination.swift; sourceTree = ""; }; 45 | 8BEF8B89285A39B8005EB80E /* AtmosphericSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtmosphericSettings.swift; sourceTree = ""; }; 46 | 8BFD4E2D285C05BB00D759F4 /* GaussianBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GaussianBlur.swift; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 8B004F822853C96E00FE2D7B /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | 8B004F7C2853C96E00FE2D7B = { 61 | isa = PBXGroup; 62 | children = ( 63 | 8B004F872853C96E00FE2D7B /* Atmos */, 64 | 8B004F862853C96E00FE2D7B /* Products */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | 8B004F862853C96E00FE2D7B /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 8B004F852853C96E00FE2D7B /* Atmos.app */, 72 | ); 73 | name = Products; 74 | sourceTree = ""; 75 | }; 76 | 8B004F872853C96E00FE2D7B /* Atmos */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 8B004F962853C97B00FE2D7B /* Atmospheric Material */, 80 | 8B004F882853C96E00FE2D7B /* AtmosApp.swift */, 81 | 8B004F8A2853C96E00FE2D7B /* ContentView.swift */, 82 | 8B004F8C2853C96F00FE2D7B /* Assets.xcassets */, 83 | 8B004F8E2853C96F00FE2D7B /* Preview Content */, 84 | ); 85 | path = Atmos; 86 | sourceTree = ""; 87 | }; 88 | 8B004F8E2853C96F00FE2D7B /* Preview Content */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 8B004F8F2853C96F00FE2D7B /* Preview Assets.xcassets */, 92 | ); 93 | path = "Preview Content"; 94 | sourceTree = ""; 95 | }; 96 | 8B004F962853C97B00FE2D7B /* Atmospheric Material */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 8B004FAE2853FB0300FE2D7B /* Shaders */, 100 | 8B004FA12853CD3200FE2D7B /* Utils */, 101 | 8B004FB32853FD3700FE2D7B /* AtmosphericEffect.swift */, 102 | ); 103 | path = "Atmospheric Material"; 104 | sourceTree = ""; 105 | }; 106 | 8B004FA12853CD3200FE2D7B /* Utils */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 8B004F972853C9A800FE2D7B /* AtmosphereView.swift */, 110 | 8BEF8B87285A390E005EB80E /* AtmosphericDestination.swift */, 111 | 8B004FA82853D49300FE2D7B /* AtmosphericMTKView.swift */, 112 | 8B004FA62853D25300FE2D7B /* AtmosphericPresenter.swift */, 113 | 8B004FA42853CFA700FE2D7B /* AtmosphericRenderer.swift */, 114 | 8BEF8B89285A39B8005EB80E /* AtmosphericSettings.swift */, 115 | 8BEF8B812859AA41005EB80E /* AtmosphericStack.swift */, 116 | 8B004F9D2853C9DF00FE2D7B /* AtmosphericTexture.swift */, 117 | 8B004FA22853CD7C00FE2D7B /* ShaderUtils.swift */, 118 | 8BFD4E2D285C05BB00D759F4 /* GaussianBlur.swift */, 119 | ); 120 | path = Utils; 121 | sourceTree = ""; 122 | }; 123 | 8B004FAE2853FB0300FE2D7B /* Shaders */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 8B9B400C285D3FA800E8DFBE /* Common.h */, 127 | 8B004FAF2853FB1B00FE2D7B /* rain.metal */, 128 | ); 129 | path = Shaders; 130 | sourceTree = ""; 131 | }; 132 | /* End PBXGroup section */ 133 | 134 | /* Begin PBXNativeTarget section */ 135 | 8B004F842853C96E00FE2D7B /* Atmos */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = 8B004F932853C96F00FE2D7B /* Build configuration list for PBXNativeTarget "Atmos" */; 138 | buildPhases = ( 139 | 8B004F812853C96E00FE2D7B /* Sources */, 140 | 8B004F822853C96E00FE2D7B /* Frameworks */, 141 | 8B004F832853C96E00FE2D7B /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = Atmos; 148 | productName = Atmos; 149 | productReference = 8B004F852853C96E00FE2D7B /* Atmos.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 8B004F7D2853C96E00FE2D7B /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | BuildIndependentTargetsInParallel = 1; 159 | LastSwiftUpdateCheck = 1400; 160 | LastUpgradeCheck = 1400; 161 | TargetAttributes = { 162 | 8B004F842853C96E00FE2D7B = { 163 | CreatedOnToolsVersion = 14.0; 164 | }; 165 | }; 166 | }; 167 | buildConfigurationList = 8B004F802853C96E00FE2D7B /* Build configuration list for PBXProject "Atmos" */; 168 | compatibilityVersion = "Xcode 14.0"; 169 | developmentRegion = en; 170 | hasScannedForEncodings = 0; 171 | knownRegions = ( 172 | en, 173 | Base, 174 | ); 175 | mainGroup = 8B004F7C2853C96E00FE2D7B; 176 | productRefGroup = 8B004F862853C96E00FE2D7B /* Products */; 177 | projectDirPath = ""; 178 | projectRoot = ""; 179 | targets = ( 180 | 8B004F842853C96E00FE2D7B /* Atmos */, 181 | ); 182 | }; 183 | /* End PBXProject section */ 184 | 185 | /* Begin PBXResourcesBuildPhase section */ 186 | 8B004F832853C96E00FE2D7B /* Resources */ = { 187 | isa = PBXResourcesBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | 8B004F902853C96F00FE2D7B /* Preview Assets.xcassets in Resources */, 191 | 8B004F8D2853C96F00FE2D7B /* Assets.xcassets in Resources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXResourcesBuildPhase section */ 196 | 197 | /* Begin PBXSourcesBuildPhase section */ 198 | 8B004F812853C96E00FE2D7B /* Sources */ = { 199 | isa = PBXSourcesBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | 8B004FA32853CD7C00FE2D7B /* ShaderUtils.swift in Sources */, 203 | 8BEF8B8A285A39B8005EB80E /* AtmosphericSettings.swift in Sources */, 204 | 8B004FB02853FB1B00FE2D7B /* rain.metal in Sources */, 205 | 8BEF8B822859AA41005EB80E /* AtmosphericStack.swift in Sources */, 206 | 8B004FA92853D49300FE2D7B /* AtmosphericMTKView.swift in Sources */, 207 | 8B004F8B2853C96E00FE2D7B /* ContentView.swift in Sources */, 208 | 8B004FA72853D25300FE2D7B /* AtmosphericPresenter.swift in Sources */, 209 | 8B004FB42853FD3700FE2D7B /* AtmosphericEffect.swift in Sources */, 210 | 8BEF8B88285A390E005EB80E /* AtmosphericDestination.swift in Sources */, 211 | 8B004F982853C9A800FE2D7B /* AtmosphereView.swift in Sources */, 212 | 8B004F892853C96E00FE2D7B /* AtmosApp.swift in Sources */, 213 | 8BFD4E2E285C05BB00D759F4 /* GaussianBlur.swift in Sources */, 214 | 8B004FA52853CFA700FE2D7B /* AtmosphericRenderer.swift in Sources */, 215 | 8B004F9E2853C9DF00FE2D7B /* AtmosphericTexture.swift in Sources */, 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | /* End PBXSourcesBuildPhase section */ 220 | 221 | /* Begin XCBuildConfiguration section */ 222 | 8B004F912853C96F00FE2D7B /* Debug */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | ALWAYS_SEARCH_USER_PATHS = NO; 226 | CLANG_ANALYZER_NONNULL = YES; 227 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 228 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_ENABLE_OBJC_WEAK = YES; 232 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 233 | CLANG_WARN_BOOL_CONVERSION = YES; 234 | CLANG_WARN_COMMA = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 245 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 247 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 248 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 249 | CLANG_WARN_STRICT_PROTOTYPES = YES; 250 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 251 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 252 | CLANG_WARN_UNREACHABLE_CODE = YES; 253 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 254 | COPY_PHASE_STRIP = NO; 255 | DEBUG_INFORMATION_FORMAT = dwarf; 256 | ENABLE_STRICT_OBJC_MSGSEND = YES; 257 | ENABLE_TESTABILITY = YES; 258 | GCC_C_LANGUAGE_STANDARD = gnu11; 259 | GCC_DYNAMIC_NO_PIC = NO; 260 | GCC_NO_COMMON_BLOCKS = YES; 261 | GCC_OPTIMIZATION_LEVEL = 0; 262 | GCC_PREPROCESSOR_DEFINITIONS = ( 263 | "DEBUG=1", 264 | "$(inherited)", 265 | ); 266 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 267 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 268 | GCC_WARN_UNDECLARED_SELECTOR = YES; 269 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 270 | GCC_WARN_UNUSED_FUNCTION = YES; 271 | GCC_WARN_UNUSED_VARIABLE = YES; 272 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 273 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 274 | MTL_FAST_MATH = YES; 275 | ONLY_ACTIVE_ARCH = YES; 276 | SDKROOT = iphoneos; 277 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 278 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 279 | }; 280 | name = Debug; 281 | }; 282 | 8B004F922853C96F00FE2D7B /* Release */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ALWAYS_SEARCH_USER_PATHS = NO; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_ENABLE_OBJC_WEAK = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 309 | CLANG_WARN_STRICT_PROTOTYPES = YES; 310 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 316 | ENABLE_NS_ASSERTIONS = NO; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu11; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 322 | GCC_WARN_UNDECLARED_SELECTOR = YES; 323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 324 | GCC_WARN_UNUSED_FUNCTION = YES; 325 | GCC_WARN_UNUSED_VARIABLE = YES; 326 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 327 | MTL_ENABLE_DEBUG_INFO = NO; 328 | MTL_FAST_MATH = YES; 329 | SDKROOT = iphoneos; 330 | SWIFT_COMPILATION_MODE = wholemodule; 331 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 332 | VALIDATE_PRODUCT = YES; 333 | }; 334 | name = Release; 335 | }; 336 | 8B004F942853C96F00FE2D7B /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 340 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 341 | CODE_SIGN_STYLE = Automatic; 342 | CURRENT_PROJECT_VERSION = 1; 343 | DEVELOPMENT_ASSET_PATHS = "\"Atmos/Preview Content\""; 344 | DEVELOPMENT_TEAM = PWW92BU2F4; 345 | ENABLE_PREVIEWS = YES; 346 | GENERATE_INFOPLIST_FILE = YES; 347 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 348 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 349 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 350 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; 351 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 352 | LD_RUNPATH_SEARCH_PATHS = ( 353 | "$(inherited)", 354 | "@executable_path/Frameworks", 355 | ); 356 | MARKETING_VERSION = 1.0; 357 | PRODUCT_BUNDLE_IDENTIFIER = dejager.nate.Atmos; 358 | PRODUCT_NAME = "$(TARGET_NAME)"; 359 | SWIFT_EMIT_LOC_STRINGS = YES; 360 | SWIFT_VERSION = 5.0; 361 | TARGETED_DEVICE_FAMILY = "1,2"; 362 | }; 363 | name = Debug; 364 | }; 365 | 8B004F952853C96F00FE2D7B /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 370 | CODE_SIGN_STYLE = Automatic; 371 | CURRENT_PROJECT_VERSION = 1; 372 | DEVELOPMENT_ASSET_PATHS = "\"Atmos/Preview Content\""; 373 | DEVELOPMENT_TEAM = PWW92BU2F4; 374 | ENABLE_PREVIEWS = YES; 375 | GENERATE_INFOPLIST_FILE = YES; 376 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 377 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 378 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 379 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; 380 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 381 | LD_RUNPATH_SEARCH_PATHS = ( 382 | "$(inherited)", 383 | "@executable_path/Frameworks", 384 | ); 385 | MARKETING_VERSION = 1.0; 386 | PRODUCT_BUNDLE_IDENTIFIER = dejager.nate.Atmos; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | SWIFT_EMIT_LOC_STRINGS = YES; 389 | SWIFT_VERSION = 5.0; 390 | TARGETED_DEVICE_FAMILY = "1,2"; 391 | }; 392 | name = Release; 393 | }; 394 | /* End XCBuildConfiguration section */ 395 | 396 | /* Begin XCConfigurationList section */ 397 | 8B004F802853C96E00FE2D7B /* Build configuration list for PBXProject "Atmos" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | 8B004F912853C96F00FE2D7B /* Debug */, 401 | 8B004F922853C96F00FE2D7B /* Release */, 402 | ); 403 | defaultConfigurationIsVisible = 0; 404 | defaultConfigurationName = Release; 405 | }; 406 | 8B004F932853C96F00FE2D7B /* Build configuration list for PBXNativeTarget "Atmos" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | 8B004F942853C96F00FE2D7B /* Debug */, 410 | 8B004F952853C96F00FE2D7B /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | /* End XCConfigurationList section */ 416 | }; 417 | rootObject = 8B004F7D2853C96E00FE2D7B /* Project object */; 418 | } 419 | -------------------------------------------------------------------------------- /Atmos/Atmos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Atmos/Atmos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Atmos/Atmos/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 | -------------------------------------------------------------------------------- /Atmos/Atmos/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "leohoho-PaMdJ6LpF84-unsplash-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Atmos/Atmos/Assets.xcassets/AppIcon.appiconset/leohoho-PaMdJ6LpF84-unsplash-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dejager/atmos/44a5c8bc1b652a086bec216c58a2dc6ff968a8a5/Atmos/Atmos/Assets.xcassets/AppIcon.appiconset/leohoho-PaMdJ6LpF84-unsplash-1024.png -------------------------------------------------------------------------------- /Atmos/Atmos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Atmos/Atmos/Assets.xcassets/tofino.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "thomas-lardeau-B5bwyCKgj4M-unsplash.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Atmos/Atmos/Assets.xcassets/tofino.imageset/thomas-lardeau-B5bwyCKgj4M-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dejager/atmos/44a5c8bc1b652a086bec216c58a2dc6ff968a8a5/Atmos/Atmos/Assets.xcassets/tofino.imageset/thomas-lardeau-B5bwyCKgj4M-unsplash.jpg -------------------------------------------------------------------------------- /Atmos/Atmos/AtmosApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosApp.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct AtmosApp: App { 12 | 13 | @StateObject var atmosphericSettings = AtmosphericSettings() 14 | 15 | var body: some Scene { 16 | WindowGroup { 17 | ContentView() 18 | .preferredColorScheme(.dark) 19 | .environmentObject(atmosphericSettings) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/AtmosphericEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericEffect.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct AtmosphericEffect: View { 11 | 12 | @EnvironmentObject var settings: AtmosphericSettings 13 | 14 | var content: Content 15 | 16 | public init(@ViewBuilder content: () -> Content) { 17 | self.content = content() 18 | } 19 | 20 | public var body: some View { 21 | Representable(targetFrame: settings.targetFrame, 22 | sourceFrame: settings.sourceFrame, 23 | cornerRadius: settings.cornerRadius, 24 | content: ZStack { 25 | content 26 | }) 27 | .accessibility(hidden: Content.self == EmptyView.self) 28 | } 29 | } 30 | 31 | // MARK: - Representable 32 | 33 | extension AtmosphericEffect { 34 | struct Representable: UIViewRepresentable { 35 | 36 | var targetFrame: CGRect 37 | var sourceFrame: CGRect 38 | var cornerRadius: CGFloat 39 | var content: Content 40 | 41 | func makeUIView(context: Context) -> UIView { 42 | context.coordinator.atmosphereView 43 | } 44 | 45 | func updateUIView(_ view: UIView, context: Context) { 46 | context.coordinator.update(targetFrame: targetFrame, 47 | sourceFrame: sourceFrame, 48 | cornerRadius: cornerRadius, 49 | content: content) 50 | } 51 | 52 | func makeCoordinator() -> Coordinator { 53 | Coordinator(targetFrame: targetFrame, 54 | sourceFrame: sourceFrame, 55 | cornerRadius: cornerRadius, 56 | content: content) 57 | } 58 | } 59 | } 60 | 61 | // MARK: - Coordinator 62 | 63 | extension AtmosphericEffect.Representable { 64 | class Coordinator { 65 | 66 | let hostingController: UIHostingController 67 | var atmosphereView: AtmosphereView 68 | 69 | init(targetFrame: CGRect, sourceFrame: CGRect, cornerRadius: CGFloat, content: Content) { 70 | 71 | hostingController = UIHostingController(rootView: content) 72 | hostingController.view.frame = CGRect(origin: .zero, size: sourceFrame.size) 73 | hostingController.view.backgroundColor = nil 74 | 75 | atmosphereView = AtmosphereView(targetFrame: targetFrame, 76 | sourceFrame: sourceFrame, 77 | cornerRadius: cornerRadius) 78 | atmosphereView.contentView?.addSubview(hostingController.view) 79 | } 80 | 81 | func update(targetFrame: CGRect, sourceFrame: CGRect, cornerRadius: CGFloat, content: Content) { 82 | 83 | guard targetFrame != .zero else { return } 84 | 85 | atmosphereView.targetFrame = targetFrame 86 | atmosphereView.sourceFrame = sourceFrame 87 | atmosphereView.cornerRadius = cornerRadius 88 | hostingController.view.frame = CGRect(origin: .zero, size: sourceFrame.size) 89 | hostingController.rootView = content 90 | hostingController.view.setNeedsDisplay() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Shaders/Common.h: -------------------------------------------------------------------------------- 1 | // 2 | // Common.h 3 | // Atmos 4 | // 5 | // 6 | 7 | #ifndef Common_h 8 | #define Common_h 9 | 10 | #include 11 | 12 | using namespace metal; 13 | 14 | #define aspectRatio float2(7.0, 1.0) 15 | #define grid aspectRatio * 2.0 16 | #define scale 0.5 17 | #define epsilon2 float2(0.001, 0.0) 18 | 19 | // noise generators by Dave Hoskins 20 | 21 | float noise(float pos) { 22 | return fract(sin(pos * 12345.564) * 7658.76); 23 | } 24 | 25 | float3 noiseCube(float pos) { 26 | float3 pos3 = fract(float3(pos) * float3(0.1031, 0.11369, 0.13787)); 27 | pos3 += dot(pos3, pos3.yzx + 19.19); 28 | return fract(float3((pos3.x + pos3.y) * pos3.z, 29 | (pos3.x + pos3.z) * pos3.y, 30 | (pos3.y + pos3.z) * pos3.x)); 31 | } 32 | 33 | float offset(float bounds, float time) { 34 | return smoothstep(0.0, bounds, time) * smoothstep(1.0, bounds, time); 35 | } 36 | 37 | float wobble(float pos) { 38 | return sin(pos + sin(pos)); 39 | } 40 | 41 | #endif /* Common_h */ 42 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Shaders/rain.metal: -------------------------------------------------------------------------------- 1 | // 2 | // rain.metal 3 | // Atmos 4 | // 5 | // Check out Martijn Steinrucken's video series which will walk you through the steps of drawing 6 | // rain! https://www.youtube.com/watch?v=EBrAdahFtuo You will see a simplified application of 7 | // the effect below. 8 | // 9 | 10 | #include 11 | #include "Common.h" 12 | 13 | using namespace metal; 14 | 15 | float2 dropSurface(float2 uv, float time) { 16 | float2 pos = uv; 17 | 18 | uv.y += time * 0.75; 19 | 20 | float2 id = floor(uv * grid); 21 | 22 | uv.y += noise(id.x); 23 | 24 | float2 cell = floor(uv * grid); 25 | float3 noise = noiseCube(cell.x * 32.5 + cell.y * 2376.1); 26 | float2 cellPos = fract(uv * grid) - float2(0.5, 0.0); 27 | 28 | float x = noise.x - 0.5; 29 | 30 | float y = pos.y * 20.0; 31 | x += wobble(y) * (0.5 - abs(x)) * (noise.z - 0.5); 32 | x *= 0.7; 33 | 34 | float timeInterval = fract(time + noise.z); 35 | 36 | y = (offset(0.75, timeInterval) - 0.5) * 0.9 + 0.5; 37 | float2 p = float2(x, y); 38 | 39 | float dropSource = length((cellPos - p) * aspectRatio.yx); 40 | 41 | float drop = smoothstep(0.4, 0.0, dropSource); 42 | 43 | float r = sqrt(smoothstep(1.0, y, cellPos.y)); 44 | float cd = abs(cellPos.x - x); 45 | float path = smoothstep(0.23 * r, 0.15 * r * r, cd); 46 | float tracer = smoothstep(-0.02, 0.02, cellPos.y - y); 47 | path *= tracer * r * r; 48 | 49 | y = pos.y; 50 | float interpolatedPath = smoothstep(0.2 * r, .0, cd); 51 | float droplets = max(0.0, (sin(y * (1.0 - y) * 120.0) - cellPos.y)) * interpolatedPath * tracer * noise.z; 52 | y = fract(y * 10.0) + (cellPos.y - 0.5); 53 | float diff = length(cellPos - float2(x, y)); 54 | droplets = smoothstep(0.3, 0.0, diff); 55 | float m = drop + droplets * r * tracer; 56 | 57 | return float2(m, path); 58 | } 59 | 60 | float condensation(float2 uv, float time) { 61 | uv *= 40.0; 62 | 63 | float2 id = floor(uv); 64 | uv = fract(uv) - 0.5; 65 | float3 noise = noiseCube(id.x * 107.45 + id.y * 3543.654); 66 | float2 p = (noise.xy - 0.5) * 0.7; 67 | float d = length(uv - p); 68 | 69 | float timeInterval = fract(time + noise.z); 70 | float fade = offset(0.025, timeInterval); 71 | return smoothstep(0.3, 0.0, d) * fract(noise.z * 10.0) * fade; 72 | } 73 | 74 | float2 rainDrops(float2 uv, float time, float staticDrops, float dynamicDropsA, float dynamicDropsB) { 75 | float2 pos = float2(uv.x, uv.y * -1.0); 76 | float staticDrips = condensation(pos, time) * staticDrops; 77 | float2 dynamicDripsA = dropSurface(pos, time) * dynamicDropsA; 78 | float2 dynamicDripsB = dropSurface(pos * 1.85, time) * dynamicDropsB; 79 | 80 | float x = staticDrips + dynamicDripsA.x + dynamicDripsB.x; 81 | x = smoothstep(0.3, 1.0, x); 82 | 83 | return float2(x, max(dynamicDripsA.y * staticDrops, dynamicDripsB.y * dynamicDropsA)); 84 | } 85 | 86 | kernel void rain(texture2d o[[texture(0)]], 87 | texture2d i[[texture(1)]], 88 | texture2d j[[texture(2)]], 89 | constant float &time [[buffer(0)]], 90 | ushort2 gid [[thread_position_in_grid]]) { 91 | 92 | int width = o.get_width(); 93 | int height = o.get_height(); 94 | 95 | float2 resolution = float2(width, height); 96 | float2 position = float2(gid); 97 | 98 | float2 uv = (position - 0.5 * resolution) / resolution.y; 99 | uv *= 0.8 + scale * 0.4; 100 | 101 | float2 pos = ((position / resolution) - 0.5) * (0.8 + scale * 0.1) + 0.5; 102 | 103 | float progress = time * 0.2; 104 | float rainAmount = 1; 105 | 106 | float staticDrops = smoothstep(-0.5, 1.0, rainAmount) * 2.0; 107 | float dynamicDropsA = smoothstep(0.25, 0.75, rainAmount); 108 | float dynamicDropsB = smoothstep(0.0, 0.5, rainAmount); 109 | 110 | float2 drops = rainDrops(uv, progress, staticDrops, dynamicDropsA, dynamicDropsB); 111 | 112 | float cx = rainDrops(uv + epsilon2, progress, staticDrops, dynamicDropsA, dynamicDropsB).x; 113 | float cy = rainDrops(uv + epsilon2.yx, progress, staticDrops, dynamicDropsA, dynamicDropsB).x; 114 | float2 normals = float2(cx - drops.x, cy - drops.x); 115 | 116 | constexpr sampler bilinear_sampler (coord::normalized, 117 | address::clamp_to_edge, 118 | filter::linear); 119 | 120 | float4 result = i.sample(bilinear_sampler, pos + normals); 121 | o.write(result, gid); 122 | } 123 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphereView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphereView.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import UIKit 9 | import Metal 10 | import MetalKit 11 | 12 | class AtmosphereView: UIView { 13 | 14 | var contentView: AtmosphericStack? 15 | 16 | private var atmosView: AtmosphericMTKView 17 | private var hasAPoopInTheShoot: Bool = false 18 | 19 | var cornerRadius: CGFloat { 20 | didSet { 21 | self.atmosView.layer.cornerRadius = cornerRadius 22 | } 23 | } 24 | 25 | var targetFrame: CGRect { 26 | didSet { 27 | atmosView.frame = targetFrame 28 | contentView?.targetFrame = targetFrame 29 | } 30 | } 31 | 32 | var sourceFrame: CGRect { 33 | didSet { 34 | let localBounds = AtmosphereView.boundsFor(frame: sourceFrame) 35 | frame = localBounds 36 | contentView?.frame = localBounds 37 | } 38 | } 39 | 40 | private static func boundsFor(frame: CGRect) -> CGRect { 41 | CGRect(origin: .zero, size: frame.size) 42 | } 43 | 44 | init(targetFrame: CGRect, sourceFrame: CGRect, cornerRadius: CGFloat) { 45 | self.targetFrame = targetFrame 46 | self.sourceFrame = sourceFrame 47 | self.cornerRadius = cornerRadius 48 | 49 | let localBounds = AtmosphereView.boundsFor(frame: sourceFrame) 50 | 51 | self.atmosView = AtmosphericMTKView(frame: targetFrame) 52 | self.atmosView.layer.cornerRadius = cornerRadius 53 | super.init(frame: localBounds) 54 | 55 | contentView = AtmosphericStack(targetFrame: targetFrame) { [weak self] in 56 | self?.setNeedsRendering() 57 | } 58 | 59 | addSubview(contentView!) 60 | addSubview(atmosView) 61 | } 62 | 63 | required init?(coder: NSCoder) { 64 | fatalError("init(coder:) has not been implemented") 65 | } 66 | 67 | private func setNeedsRendering() { 68 | if !hasAPoopInTheShoot { 69 | DispatchQueue.main.asyncAfter(deadline: .now()) { 70 | self.atmosView.renderer.atmosTexture?.render(view: self.contentView, 71 | frame: self.targetFrame) 72 | self.atmosView.setNeedsDisplay(self.targetFrame) 73 | self.hasAPoopInTheShoot = false 74 | } 75 | hasAPoopInTheShoot = true 76 | } 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphericDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericDestination.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AtmosphericDestination: ViewModifier { 11 | 12 | @EnvironmentObject var settings: AtmosphericSettings 13 | 14 | @State var cornerRadius: CGFloat 15 | 16 | private var frameView: some View { 17 | GeometryReader { geometry in 18 | settings.updateTargetFrame(geometry.frame(in: .global)) 19 | settings.updateCornerRadius(cornerRadius) 20 | return Color.clear 21 | } 22 | } 23 | 24 | init(cornerRadius: CGFloat = 0) { 25 | self.cornerRadius = cornerRadius 26 | } 27 | 28 | func body(content: Content) -> some View { 29 | content 30 | .background(frameView) 31 | } 32 | } 33 | 34 | extension View { 35 | func makeItRainHere(cornerRadius: CGFloat = 0) -> some View { 36 | modifier(AtmosphericDestination(cornerRadius: cornerRadius)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphericMTKView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericMTKView.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import MetalKit 9 | 10 | class AtmosphericMTKView: MTKView, AtmosphericPresenter { 11 | var renderer: AtmosphericRenderer! 12 | 13 | required init(frame: CGRect) { 14 | super.init(frame: frame, device: MTLCreateSystemDefaultDevice()) 15 | layer.masksToBounds = true 16 | drizzle() 17 | } 18 | 19 | required init(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | func renderer(forDevice device: MTLDevice, functionName: String) -> AtmosphericRenderer { 24 | return AtmosphericRenderer(device: device, functionName: functionName) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphericPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericPresenter.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import MetalKit 9 | 10 | protocol AtmosphericPresenter: MTKView { 11 | var renderer: AtmosphericRenderer! { get set } 12 | 13 | // Needs more atmospheric conditions! 👩🏽‍🚀 14 | func drizzle(device: MTLDevice?) 15 | } 16 | 17 | extension AtmosphericPresenter { 18 | func drizzle(device: MTLDevice? = MTLCreateSystemDefaultDevice()) { 19 | guard let device = device else { fatalError("Device loading error") } 20 | renderer = AtmosphericRenderer(device: device, functionName: "rain") 21 | shader.setRenderer(renderer) 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphericRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericRenderer.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import MetalKit 9 | import MetalPerformanceShaders 10 | 11 | final class AtmosphericRenderer: NSObject { 12 | 13 | weak var device: MTLDevice? 14 | var library: MTLLibrary? 15 | var atmosTexture: AtmosphericTexture? 16 | 17 | let commandQueue: MTLCommandQueue? 18 | private var computePipelineState: MTLComputePipelineState? 19 | private var startDate: Date = Date() 20 | 21 | init(device: MTLDevice, functionName: String) { 22 | self.device = device 23 | library = device.makeDefaultLibrary() 24 | commandQueue = device.makeCommandQueue() 25 | 26 | super.init() 27 | 28 | atmosTexture = AtmosphericTexture(device: device) 29 | 30 | guard let library = library else { 31 | assertionFailure("MetalLibrary is missing 🫢.") 32 | return 33 | } 34 | 35 | guard let function = library.makeFunction(name: functionName) else { 36 | assertionFailure("Function named \(functionName) wasn't created 😬.") 37 | return 38 | } 39 | 40 | do { 41 | computePipelineState = try device.makeComputePipelineState(function: function) 42 | } catch { 43 | assertionFailure("computePipelineState: \(error) wasn't created 😬.") 44 | return 45 | } 46 | } 47 | } 48 | 49 | extension AtmosphericRenderer: MTKViewDelegate { 50 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} 51 | 52 | func draw(in view: MTKView) { 53 | guard let drawable = view.currentDrawable, 54 | let computePipelineState = computePipelineState else { 55 | return 56 | } 57 | 58 | let threadsPerThreadgroup: MTLSize = MTLSize(width: 16, height: 16, depth: 1) 59 | let scale: CGFloat = UIScreen.main.nativeScale 60 | 61 | var threadgroupCount: MTLSize { 62 | let width = Int(ceilf(Float(view.frame.width * scale) / Float(threadsPerThreadgroup.width))) 63 | let height = Int(ceilf(Float(view.frame.height * scale) / Float(threadsPerThreadgroup.height))) 64 | return MTLSize(width: width, height: height, depth: 1) 65 | } 66 | 67 | var time = Float(Date().timeIntervalSince(startDate)) 68 | 69 | guard let commandBuffer = commandQueue?.makeCommandBuffer() else { 70 | return assertionFailure("Command buffer wasn't created 😬.") 71 | } 72 | 73 | let blurredTexture = atmosTexture?.blurredTexture(commandBuffer: commandBuffer) 74 | 75 | let commandEncoder = commandBuffer.makeComputeCommandEncoder() 76 | commandEncoder?.setComputePipelineState(computePipelineState) 77 | commandEncoder?.setTexture(drawable.texture, index: 0) 78 | commandEncoder?.setTexture(blurredTexture, index: 1) 79 | commandEncoder?.setBytes(&time, length: MemoryLayout.size * 1, index: 0) 80 | commandEncoder?.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadsPerThreadgroup) 81 | commandEncoder?.endEncoding() 82 | 83 | commandBuffer.present(drawable) 84 | commandBuffer.commit() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphericSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericSettings.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | class AtmosphericSettings: ObservableObject { 12 | 13 | @Published private(set) var targetFrame: CGRect = .zero 14 | @Published private(set) var sourceFrame: CGRect = UIScreen.main.bounds 15 | @Published private(set) var cornerRadius: CGFloat = 0 16 | 17 | func updateTargetFrame(_ frame: CGRect) { 18 | guard targetFrame != frame else { return } 19 | targetFrame = frame 20 | } 21 | 22 | func updateSourceFrame(_ frame: CGRect) { 23 | guard sourceFrame != frame else { return } 24 | sourceFrame = frame 25 | } 26 | 27 | func updateCornerRadius(_ cornerRadius: CGFloat) { 28 | guard self.cornerRadius != cornerRadius else { return } 29 | self.cornerRadius = cornerRadius 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphericStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericStack.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import UIKit 9 | 10 | typealias OnChange = (() ->Void)? 11 | 12 | class AtmosphericStack: UIView { 13 | 14 | var onChange: OnChange 15 | 16 | var targetFrame: CGRect { 17 | didSet { 18 | frame = AtmosphericStack.boundsFor(frame: targetFrame) 19 | onChange?() 20 | } 21 | 22 | } 23 | 24 | init(targetFrame: CGRect, onChange: OnChange) { 25 | self.onChange = onChange 26 | self.targetFrame = targetFrame 27 | 28 | super.init(frame: AtmosphericStack.boundsFor(frame: targetFrame)) 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | private static func boundsFor(frame: CGRect) -> CGRect { 36 | CGRect(origin: .zero, size: frame.size) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/AtmosphericTexture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtmosphericTexture.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | import Metal 11 | import MetalPerformanceShaders 12 | 13 | class AtmosphericTexture: NSObject { 14 | 15 | weak var device: MTLDevice? 16 | 17 | private(set) var rawTexture: MTLTexture? 18 | private(set) var texture: MTLTexture? 19 | 20 | private var needsBlur: Bool = false 21 | 22 | private var blur: GaussianBlur? 23 | 24 | init(device: MTLDevice) { 25 | self.device = device 26 | blur = GaussianBlur(device: device) 27 | } 28 | 29 | func render(view: UIView?, frame: CGRect) { 30 | guard frame != .zero else { return } 31 | 32 | guard let view = view else { 33 | return assertionFailure("View is missing 🫢.") 34 | } 35 | 36 | guard let device = device else { 37 | return assertionFailure("Metal Device is missing 🫢.") 38 | } 39 | 40 | let width = Int(frame.size.width) 41 | let height = Int(frame.size.height) 42 | 43 | let pixelRowAlignment = device.minimumTextureBufferAlignment(for: .bgra8Unorm) 44 | let bytesPerRow = width * pixelRowAlignment 45 | 46 | let pagesize = Int(getpagesize()) 47 | var bytes: UnsafeMutableRawPointer? = nil 48 | let result = posix_memalign(&bytes, pagesize, bytesPerRow * height) 49 | if result != noErr { 50 | return assertionFailure("Something bad happened during allocation 💀.") 51 | } 52 | 53 | let colorSpace = CGColorSpaceCreateDeviceRGB() 54 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue).union(.byteOrder32Little) 55 | 56 | guard let context = CGContext(data: bytes, 57 | width: width, 58 | height: height, 59 | bitsPerComponent: 8, 60 | bytesPerRow: bytesPerRow, 61 | space: colorSpace, 62 | bitmapInfo: bitmapInfo.rawValue) else { 63 | return assertionFailure("Could not create CGContext 🪦.") 64 | } 65 | 66 | let snapshotFrame = CGRect(origin: CGPoint(x: -frame.origin.x, 67 | y: view.layer.bounds.height - frame.maxY), 68 | size: view.layer.bounds.size) 69 | UIGraphicsPushContext(context) 70 | let flip = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: view.layer.bounds.size.height) 71 | context.concatenate(flip) 72 | view.drawHierarchy(in: snapshotFrame, afterScreenUpdates: false) 73 | UIGraphicsPopContext() 74 | 75 | if (rawTexture == nil || rawTexture?.width != width || rawTexture?.height != height) { 76 | rawTexture = backingTexture(device: device, 77 | width: context.width, 78 | height: context.height) 79 | } 80 | 81 | if (texture == nil || texture?.width != width || texture?.height != height) { 82 | texture = displayTexture(texture: rawTexture) 83 | } 84 | 85 | rawTexture!.replace(region: MTLRegionMake2D(0, 0, width, height), 86 | mipmapLevel: 0, 87 | withBytes: bytes!, 88 | bytesPerRow: bytesPerRow) 89 | 90 | needsBlur = true 91 | 92 | free(bytes) 93 | } 94 | 95 | func blurredTexture(commandBuffer: MTLCommandBuffer) -> MTLTexture? { 96 | guard let blur = blur, 97 | let sourceTexture = rawTexture, 98 | let destinationTexture = texture else { return nil } 99 | if needsBlur { 100 | blur.encode(to: commandBuffer, 101 | sourceTexture: sourceTexture, 102 | destinationTexture: destinationTexture) 103 | needsBlur = false 104 | } 105 | return destinationTexture 106 | } 107 | 108 | private func backingTexture(device: MTLDevice, width: Int, height: Int) -> MTLTexture? { 109 | let descriptor = MTLTextureDescriptor 110 | .texture2DDescriptor(pixelFormat: .bgra8Unorm, 111 | width: width, 112 | height: height, 113 | mipmapped: true) 114 | descriptor.usage = .shaderRead 115 | descriptor.storageMode = .shared 116 | return device.makeTexture(descriptor: descriptor) 117 | } 118 | 119 | private func displayTexture(texture: MTLTexture?) -> MTLTexture? { 120 | guard let texture = texture else { return nil} 121 | let descriptor = MTLTextureDescriptor 122 | .texture2DDescriptor(pixelFormat: .bgra8Unorm, 123 | width: texture.width, 124 | height: texture.height, 125 | mipmapped: false) 126 | descriptor.usage = .shaderRead.union(.shaderWrite) 127 | 128 | return device?.makeTexture(descriptor: descriptor) 129 | } 130 | } 131 | 132 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/GaussianBlur.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GaussianBlur.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import Metal 9 | import MetalPerformanceShaders 10 | 11 | class GaussianBlur { 12 | let gaussian: MPSImageGaussianBlur 13 | 14 | required init?(device: MTLDevice?) { 15 | guard let device = device else { return nil } 16 | gaussian = MPSImageGaussianBlur(device: device, sigma: 7.0) 17 | } 18 | 19 | func encode(to commandBuffer: MTLCommandBuffer, 20 | sourceTexture: MTLTexture, 21 | destinationTexture: MTLTexture) { 22 | gaussian.encode(commandBuffer: commandBuffer, 23 | sourceTexture: sourceTexture, 24 | destinationTexture: destinationTexture) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Atmos/Atmos/Atmospheric Material/Utils/ShaderUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShaderUtils.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import MetalKit 9 | 10 | struct ShaderExtension { 11 | let base: Base 12 | 13 | init(_ base: Base) { 14 | self.base = base 15 | } 16 | } 17 | 18 | protocol ShaderExtendable { 19 | associatedtype Extendable 20 | 21 | static var shader: ShaderExtension.Type { get set } 22 | var shader: ShaderExtension { get set } 23 | } 24 | 25 | extension ShaderExtendable { 26 | static var shader: ShaderExtension.Type { 27 | get { 28 | return ShaderExtension.self 29 | } 30 | set {} 31 | } 32 | 33 | var shader: ShaderExtension { 34 | get { 35 | return ShaderExtension(self) 36 | } 37 | set {} 38 | } 39 | } 40 | 41 | extension NSObject: ShaderExtendable {} 42 | 43 | extension ShaderExtension where Base: MTKView { 44 | func setRenderer(_ renderer: AtmosphericRenderer) { 45 | base.framebufferOnly = false 46 | base.drawableSize = base.frame.size 47 | base.delegate = renderer 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Atmos/Atmos/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Atmos 4 | // 5 | // Created by Nate de Jager on 2022-06-08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | 12 | var body: some View { 13 | VStack { 14 | VStack { 15 | Text("The universe smiles upon you.") 16 | .multilineTextAlignment(.center) 17 | .font(Font.system(.title)) 18 | .fontWeight(.light) 19 | .foregroundColor(.white) 20 | } 21 | .frame(width: 300, height: 300) 22 | .overlay( 23 | RoundedRectangle(cornerRadius: 18) 24 | .stroke(lineGradient, 25 | lineWidth: 1)) 26 | .makeItRainHere(cornerRadius: 18) // 👈 defines an area where the rain effect will appear 27 | } 28 | .frame(maxWidth: .infinity, maxHeight: .infinity) 29 | .background( 30 | // 👇 coordinates the rendering of the background into a metal texture which is blurred 31 | // and has a rain effect applied. 32 | AtmosphericEffect { 33 | Image("tofino") 34 | .resizable() 35 | .scaledToFill() 36 | .edgesIgnoringSafeArea(.all) 37 | } 38 | 39 | ) 40 | .ignoresSafeArea() 41 | } 42 | 43 | private var lineGradient: LinearGradient { 44 | LinearGradient(gradient: Gradient(colors: [.white.opacity(0.2), .black.opacity(0.3)]), 45 | startPoint: .topLeading, 46 | endPoint: .bottomTrailing) 47 | } 48 | } 49 | 50 | struct ContentView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | ContentView() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Atmos/Atmos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atmos 2 | A SwiftUI proof-of-concept, and some sleight-of-hand, which adds rain to a view's background. 3 | 4 | 5 | 6 | 7 | #### "Ima use this in my app..." 8 | Introducing Metal to SwiftUI workflow requires the same level of care and attention you'd give to adopting a rescue hyena. In many ways it's an architectural decision. Have fun, but proceed with caution. ☺️ 9 | 10 | #### "This approach could be more efficient!" 11 | Absolutely it can! In multiple ways. This is a proof of concept I put together over a couple of lunches during WWDC 2022. There's plenty of room for improvement and I have a short attention span. Run wild and make it better. Or stare in wonder at the waste of a perfectly good GPU. 🫠 12 | 13 | ## I'd love to hear from you 14 | You can reach me through [Twitter](https://twitter.com/dejager) or [LinkedIn](https://t.co/y0K7rKBYpy). 15 | 16 | ## Special Thanks 17 | This example is a blend of ideas, tutorials, and code samples shared by these fine folks: 18 | [Bartosz Ciechanowski](https://ciechanow.ski) 19 | | [Martijn Steinrucken](https://www.youtube.com/TheArtOfCodeIsCool) 20 | | [Warren Moore](https://metalbyexample.com) 21 | 22 | Photo by [Thomas Lardeau](https://unsplash.com/@thomaslrdeau) 23 | --------------------------------------------------------------------------------