├── .DS_Store ├── .gitignore ├── MetalPlayground.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── MetalPlayground.xcscheme └── xcuserdata │ └── raheel.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── MetalPlayground ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── reset.imageset │ │ ├── Contents.json │ │ └── reset.pdf ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── MetalPlayground.entitlements ├── MetalPlaygroundApp.swift ├── R.png ├── Rendering │ ├── MathUtils.swift │ ├── MetalView.swift │ └── Renderer.swift ├── Root │ ├── ConfigView.swift │ ├── MetalSwiftUIView.swift │ ├── RootView.swift │ └── ViewModel.swift ├── Scenes │ ├── BookShaders │ │ ├── 06Colors.metal │ │ ├── 07ShapePrimitives.metal │ │ ├── 07Shapes.metal │ │ ├── 08LeftRightTiler.metal │ │ ├── BoSShapes.swift │ │ └── Shaping.metal │ ├── Explorations │ │ ├── 05Algorithmic.metal │ │ ├── 07FuturisticUI.metal │ │ ├── AudioViz.metal │ │ ├── AudioViz.swift │ │ ├── CellularNoise.metal │ │ ├── Girih.metal │ │ ├── Girih.swift │ │ ├── HappyJumping.metal │ │ ├── PolarExperiments.metal │ │ ├── RaysConfig.swift │ │ ├── ShaderToyDistortions.metal │ │ ├── ShaderToySmiley.metal │ │ ├── ShaderToyStarField.metal │ │ ├── Simplest3D.metal │ │ └── Starfield.swift │ ├── Helpers.metal │ ├── JumpingBalls │ │ ├── JumpingBalls.metal │ │ └── JumpingBalls.swift │ ├── MetalHeaders.h │ ├── Playground.swift │ ├── ShaderHeaders.h │ └── SimonShaders │ │ ├── FractAndFriends.metal │ │ ├── SimonCloudyDay.metal │ │ ├── SimonNoiseIntro.metal │ │ └── SimonSDFs.metal ├── UI │ └── TitledSlider.swift ├── raga.mp3 └── wallpaper.png ├── README.md └── images ├── circles.png ├── colors.png ├── futuristic-UI.gif ├── futuristic-UI.mov ├── leftright-tiler.gif ├── leftright-tiler.mov └── starfield.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MetalPlayground.xcodeproj/project.xcworkspace/xcuserdata/raheel.xcuserdatad/UserInterfaceState.xcuserstate 2 | -------------------------------------------------------------------------------- /MetalPlayground.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MetalPlayground.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MetalPlayground.xcodeproj/xcshareddata/xcschemes/MetalPlayground.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 58 | 59 | 63 | 64 | 68 | 69 | 73 | 74 | 75 | 76 | 82 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /MetalPlayground.xcodeproj/xcuserdata/raheel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 57 | 69 | 70 | 71 | 73 | 85 | 86 | 87 | 89 | 101 | 102 | 103 | 105 | 117 | 118 | 119 | 121 | 133 | 134 | 135 | 137 | 149 | 150 | 151 | 153 | 165 | 166 | 167 | 169 | 181 | 182 | 183 | 185 | 197 | 198 | 199 | 201 | 213 | 214 | 215 | 217 | 229 | 230 | 231 | 233 | 245 | 246 | 247 | 249 | 261 | 262 | 263 | 265 | 277 | 278 | 279 | 281 | 293 | 294 | 295 | 297 | 309 | 310 | 311 | 313 | 325 | 326 | 327 | 329 | 341 | 342 | 343 | 345 | 357 | 358 | 359 | 361 | 373 | 374 | 375 | 377 | 389 | 390 | 391 | 393 | 405 | 406 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /MetalPlayground.xcodeproj/xcuserdata/raheel.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Glitter.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | MetalPlayground.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 59C2BD20246A709D00335E59 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MetalPlayground/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MetalPlayground/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MetalPlayground/Assets.xcassets/reset.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "reset.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MetalPlayground/Assets.xcassets/reset.imageset/reset.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/MetalPlayground/Assets.xcassets/reset.imageset/reset.pdf -------------------------------------------------------------------------------- /MetalPlayground/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2020 Raheel Ahmad. All rights reserved. 27 | NSSupportsAutomaticTermination 28 | 29 | NSSupportsSuddenTermination 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MetalPlayground/MetalPlayground.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MetalPlayground/MetalPlaygroundApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 5/11/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftUI 11 | 12 | 13 | @main 14 | struct MetalPlaygroundApp: App { 15 | var body: some Scene { 16 | WindowGroup { 17 | RootView() 18 | .frame(minWidth: 420) 19 | } 20 | .windowStyle(.hiddenTitleBar) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MetalPlayground/R.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/MetalPlayground/R.png -------------------------------------------------------------------------------- /MetalPlayground/Rendering/MathUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MathUtils.swift 3 | // MetalByTutorialsScratch 4 | // 5 | // Created by Raheel Ahmad on 8/23/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import simd 11 | typealias float3 = SIMD3 12 | typealias float4 = SIMD4 13 | 14 | extension float4 { 15 | init(_ v: float3, _ w: Float) { 16 | self.init(x: v.x, y: v.y, z: v.z, w: w) 17 | } 18 | 19 | // RGB color from HSV color (all parameters in range [0, 1]) 20 | init(hue: Float, saturation: Float, brightness: Float) { 21 | let c = brightness * saturation 22 | let x = c * (1 - fabsf(fmodf(hue * 6, 2) - 1)) 23 | let m = brightness - saturation 24 | 25 | var r: Float = 0 26 | var g: Float = 0 27 | var b: Float = 0 28 | switch hue { 29 | case _ where hue < 0.16667: 30 | r = c; g = x; b = 0 31 | case _ where hue < 0.33333: 32 | r = x; g = c; b = 0 33 | case _ where hue < 0.5: 34 | r = 0; g = c; b = x 35 | case _ where hue < 0.66667: 36 | r = 0; g = x; b = c 37 | case _ where hue < 0.83333: 38 | r = x; g = 0; b = c 39 | case _ where hue <= 1.0: 40 | r = c; g = 0; b = x 41 | default: 42 | break 43 | } 44 | 45 | r += m; g += m; b += m 46 | self.init(x: r, y: g, z: b, w: 1) 47 | } 48 | 49 | var xyz: float3 { 50 | return float3(x, y, z) 51 | } 52 | } 53 | 54 | extension float4x4 { 55 | // MARK:- Translate 56 | init(translation: float3) { 57 | let matrix = float4x4( 58 | [ 1, 0, 0, 0], 59 | [ 0, 1, 0, 0], 60 | [ 0, 0, 1, 0], 61 | [translation.x, translation.y, translation.z, 1] 62 | ) 63 | self = matrix 64 | } 65 | 66 | // MARK:- Scale 67 | init(scaling: float3) { 68 | let matrix = float4x4( 69 | [scaling.x, 0, 0, 0], 70 | [ 0, scaling.y, 0, 0], 71 | [ 0, 0, scaling.z, 0], 72 | [ 0, 0, 0, 1] 73 | ) 74 | self = matrix 75 | } 76 | 77 | init(scaling: Float) { 78 | self = matrix_identity_float4x4 79 | columns.3.w = 1 / scaling 80 | } 81 | 82 | // MARK:- Rotate 83 | init(rotationX angle: Float) { 84 | let matrix = float4x4( 85 | [1, 0, 0, 0], 86 | [0, cos(angle), sin(angle), 0], 87 | [0, -sin(angle), cos(angle), 0], 88 | [0, 0, 0, 1] 89 | ) 90 | self = matrix 91 | } 92 | 93 | init(rotationY angle: Float) { 94 | let matrix = float4x4( 95 | [cos(angle), 0, -sin(angle), 0], 96 | [ 0, 1, 0, 0], 97 | [sin(angle), 0, cos(angle), 0], 98 | [ 0, 0, 0, 1] 99 | ) 100 | self = matrix 101 | } 102 | 103 | init(rotationZ angle: Float) { 104 | let matrix = float4x4( 105 | [ cos(angle), sin(angle), 0, 0], 106 | [-sin(angle), cos(angle), 0, 0], 107 | [ 0, 0, 1, 0], 108 | [ 0, 0, 0, 1] 109 | ) 110 | self = matrix 111 | } 112 | 113 | init(rotation angle: float3) { 114 | let rotationX = float4x4(rotationX: angle.x) 115 | let rotationY = float4x4(rotationY: angle.y) 116 | let rotationZ = float4x4(rotationZ: angle.z) 117 | self = rotationX * rotationY * rotationZ 118 | } 119 | 120 | 121 | init(rotationAroundAxis axis: float3, by angle: Float) { 122 | let unitAxis = normalize(axis) 123 | let ct = cosf(angle) 124 | let st = sinf(angle) 125 | let ci = 1 - ct 126 | let x = unitAxis.x, y = unitAxis.y, z = unitAxis.z 127 | self.init(columns:(float4( ct + x * x * ci, y * x * ci + z * st, z * x * ci - y * st, 0), 128 | float4(x * y * ci - z * st, ct + y * y * ci, z * y * ci + x * st, 0), 129 | float4(x * z * ci + y * st, y * z * ci - x * st, ct + z * z * ci, 0), 130 | float4( 0, 0, 0, 1))) 131 | } 132 | 133 | init(translationBy v: float3) { 134 | self.init(columns:(float4(1, 0, 0, 0), 135 | float4(0, 1, 0, 0), 136 | float4(0, 0, 1, 0), 137 | float4(v.x, v.y, v.z, 1))) 138 | } 139 | 140 | var upperLeft: float3x3 { 141 | let x = columns.0.xyz 142 | let y = columns.1.xyz 143 | let z = columns.2.xyz 144 | return float3x3(columns: (x, y, z)) 145 | } 146 | 147 | init(perspectiveProjectionRHFovY fovy: Float, aspectRatio: Float, nearZ: Float, farZ: Float, lhs: Bool = true) { 148 | let ys = 1 / tanf(fovy * 0.5) 149 | let xs = ys / aspectRatio 150 | let zs = lhs ? farZ / (farZ - nearZ) : farZ / (nearZ - farZ) 151 | let X = float4( xs, 0, 0, 0) 152 | let Y = float4( 0, ys, 0, 0) 153 | let Z = lhs ? float4( 0, 0, zs, 1) : float4( 0, 0, zs, -1) 154 | let W = lhs ? float4( 0, 0, zs * -nearZ, 0) : float4( 0, 0, zs * nearZ, 0) 155 | self.init() 156 | columns = (X,Y,Z,W) 157 | } 158 | } 159 | 160 | func radians_from_degrees(_ degrees: Float) -> Float { 161 | return (degrees / 180) * .pi 162 | } 163 | -------------------------------------------------------------------------------- /MetalPlayground/Rendering/MetalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalView.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/6/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import MetalKit 10 | 11 | final class MetalView: MTKView { 12 | var renderer: Renderer? 13 | 14 | override func viewDidMoveToWindow() { 15 | super.viewDidMoveToWindow() 16 | } 17 | 18 | override func mouseMoved(with event: NSEvent) { 19 | let x = Float(event.locationInWindow.x) 20 | let y = Float(event.locationInWindow.y) 21 | renderer?.mouseLocation = .init(x,y) 22 | } 23 | 24 | override func updateTrackingAreas() { 25 | let area = NSTrackingArea(rect: self.bounds, 26 | options: [NSTrackingArea.Options.activeAlways, 27 | NSTrackingArea.Options.mouseMoved, 28 | NSTrackingArea.Options.enabledDuringMouseDrag], 29 | owner: self, 30 | userInfo: nil) 31 | self.addTrackingArea(area) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MetalPlayground/Rendering/Renderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Renderer.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 5/11/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import Metal 10 | import MetalKit 11 | 12 | struct Vertex { 13 | var position: vector_float2; 14 | } 15 | 16 | struct FragmentUniforms { 17 | var time: Float 18 | var screen_width: Float 19 | var screen_height: Float 20 | var screen_scale: Float 21 | var mouseLocation: vector_float2 22 | } 23 | 24 | final class Renderer: NSObject, MTKViewDelegate { 25 | var mouseLocation: vector_float2 = .init(repeating: 0) { 26 | didSet { 27 | let x = mouseLocation.x / uniforms.screen_width * 2 - 0.5 28 | let y = (0.5 - 2 * mouseLocation.y / uniforms.screen_height) 29 | uniforms.mouseLocation = vector_float2(x, y) 30 | } 31 | } 32 | let device: MTLDevice 33 | let queue: MTLCommandQueue 34 | let pixelFormat: MTLPixelFormat = .bgra8Unorm 35 | 36 | static var aspectRatio: Float = 1.0 37 | 38 | private let compileQueue = DispatchQueue.init(label: "Shader compile queue") 39 | 40 | var pipelineState: MTLRenderPipelineState! 41 | private var uniforms: FragmentUniforms = .init(time: 0, screen_width: 0, screen_height: 0, screen_scale: 0, mouseLocation: .init(0,0)) 42 | 43 | override init() { 44 | device = MTLCreateSystemDefaultDevice()! 45 | queue = device.makeCommandQueue()! 46 | 47 | super.init() 48 | } 49 | 50 | func setup(_ view: MTKView) { 51 | view.device = device 52 | view.colorPixelFormat = pixelFormat 53 | view.preferredFramesPerSecond = 60 54 | view.delegate = self 55 | uniforms.screen_scale = 2 56 | setupPipeline() 57 | } 58 | 59 | var lastRenderTime: CFTimeInterval? = nil 60 | var currentTime: Double = 0 61 | let gpuLock = DispatchSemaphore(value: 1) 62 | 63 | var scene: Playground = SceneKind.allCases[0].scene { 64 | didSet { 65 | setupPipeline() 66 | } 67 | } 68 | 69 | var vertexBuffer: MTLBuffer? 70 | var isPaused = false 71 | 72 | private func setupPipeline() { 73 | isPaused = true 74 | scene.buildPipeline(device: device, pixelFormat: pixelFormat) { [weak self] pipelineState,vertexBuffer in 75 | self?.isPaused = false 76 | self?.pipelineState = pipelineState 77 | self?.vertexBuffer = vertexBuffer 78 | } 79 | } 80 | 81 | func draw(in view: MTKView) { 82 | guard !isPaused else { return } 83 | guard let commandBuffer = queue.makeCommandBuffer() else { return } 84 | guard let passDescriptor = view.currentRenderPassDescriptor else { return } 85 | guard let pipelineState = self.pipelineState else { return } 86 | 87 | if lastRenderTime == nil, var frame = view.window?.frame { 88 | frame.size = .init(width: 500, height: 500) 89 | view.window?.setFrame(frame, display: true, animate: false) 90 | } 91 | 92 | // update time 93 | let systemTime = CACurrentMediaTime() 94 | let timeDiff = lastRenderTime.map { systemTime - $0 } ?? 0 95 | currentTime += timeDiff 96 | lastRenderTime = systemTime 97 | 98 | uniforms.time = Float(currentTime) 99 | 100 | passDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.3, green: 0.3, blue: 0.4, alpha: 1) 101 | guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: passDescriptor) else { return } 102 | 103 | compileQueue.async { 104 | self.compileScenePipeline() 105 | } 106 | 107 | encoder.setRenderPipelineState(pipelineState) 108 | 109 | encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) 110 | 111 | let uniformsBuffer = device.makeBuffer(bytes: &uniforms, length: MemoryLayout.size, options: []) 112 | encoder.setFragmentBuffer(uniformsBuffer, offset: 0, index: 0) 113 | scene.setUniforms(device: device, encoder: encoder) 114 | 115 | guard scene.ready else { return } 116 | scene.draw(encoder: encoder) 117 | 118 | encoder.endEncoding() 119 | commandBuffer.present(view.currentDrawable!) 120 | commandBuffer.commit() 121 | } 122 | 123 | var shaderContents = "" 124 | func compileScenePipeline() { 125 | guard scene.liveReloads else { return } 126 | let fm = FileManager() 127 | let filePath = scene.filePath as NSString 128 | let shaderPath: String = filePath.deletingLastPathComponent.appending("/\(scene.fileName).metal") 129 | let helpersPath = (filePath.deletingLastPathComponent) + "/Helpers.metal" 130 | guard 131 | let shaderContentsData = fm.contents(atPath: shaderPath), 132 | let helpersData = fm.contents(atPath: helpersPath), 133 | var shaderContents = String(data: shaderContentsData, encoding: .utf8), 134 | let helperContents = String(data: helpersData, encoding: .utf8) 135 | else { 136 | assertionFailure() 137 | return 138 | } 139 | var shaderContentLines = shaderContents.split(separator: "\n") 140 | if let headerIndex = shaderContentLines.firstIndex(where: { $0 == "#include \"../ShaderHeaders.h\"" }) { 141 | shaderContentLines.remove(at: headerIndex) 142 | 143 | var headerLines = helperContents.split(separator: "\n") 144 | if let helperHeaderIndex = headerLines.firstIndex(where: { String($0) == "#include \"ShaderHeaders.h\"" }) { 145 | headerLines.remove(at: helperHeaderIndex) 146 | } 147 | shaderContentLines.insert(contentsOf: headerLines, at: headerIndex) 148 | } 149 | shaderContents = shaderContentLines.joined(separator: "\n") 150 | 151 | let oldValue = self.shaderContents 152 | self.shaderContents = shaderContents 153 | 154 | guard shaderContents != oldValue else { 155 | return 156 | } 157 | 158 | do { 159 | let pipelineDesc = MTLRenderPipelineDescriptor() 160 | let library = try device.makeLibrary(source: shaderContents, options: nil) 161 | pipelineDesc.vertexFunction = library.makeFunction(name: scene.vertexFuncName) 162 | pipelineDesc.fragmentFunction = library.makeFunction(name: scene.fragmentFuncName) 163 | pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat 164 | 165 | let pipeline = (try? device.makeRenderPipelineState(descriptor: pipelineDesc))! 166 | 167 | let vertexBuffer = device.makeBuffer(bytes: scene.basicVertices, length: MemoryLayout.stride * scene.basicVertices.count, options: []) 168 | DispatchQueue.main.async { 169 | self.pipelineState = pipeline 170 | self.vertexBuffer = vertexBuffer 171 | } 172 | } catch { 173 | print(error.localizedDescription) 174 | } 175 | 176 | } 177 | 178 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 179 | uniforms.screen_width = Float(size.width) 180 | uniforms.screen_height = Float(size.height) 181 | Self.aspectRatio = Float(uniforms.screen_width/uniforms.screen_height) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /MetalPlayground/Root/ConfigView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigView.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/22/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ConfigView: NSViewRepresentable { 12 | @Environment(ViewModel.self) var viewModel 13 | 14 | func makeNSView(context: Context) -> NSView { 15 | NSView() 16 | } 17 | 18 | func updateNSView(_ nsView: NSView, context: Context) { 19 | nsView.subviews.forEach { $0.removeFromSuperview() } 20 | if let view = viewModel.scene.view { 21 | nsView.addSubview(view) 22 | nsView.translatesAutoresizingMaskIntoConstraints = false 23 | view.translatesAutoresizingMaskIntoConstraints = false 24 | NSLayoutConstraint.activate([ 25 | view.leadingAnchor.constraint(equalTo: nsView.leadingAnchor), 26 | view.trailingAnchor.constraint(equalTo: nsView.trailingAnchor), 27 | view.topAnchor.constraint(equalTo: nsView.topAnchor), 28 | view.bottomAnchor.constraint(equalTo: nsView.bottomAnchor), 29 | ]) 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /MetalPlayground/Root/MetalSwiftUIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalSwiftUIView.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/22/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MetalSwiftView: NSViewRepresentable { 12 | @Environment(ViewModel.self) var viewModel 13 | 14 | init() { 15 | } 16 | 17 | func makeNSView(context: Context) -> MetalView { 18 | MetalView() 19 | } 20 | 21 | func makeCoordinator() -> ViewModel { 22 | viewModel 23 | } 24 | 25 | func updateNSView(_ nsView: MetalView, context: Context) { 26 | nsView.delegate = context.coordinator.renderer 27 | nsView.renderer = context.coordinator.renderer 28 | context.coordinator.update(view: nsView) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MetalPlayground/Root/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootView.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/20/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PlaygroundListItemView: View { 12 | let group: PlaygroundGroup 13 | var viewModel: ViewModel 14 | @State private var isExpanded = false 15 | 16 | 17 | var body: some View { 18 | DisclosureGroup(isExpanded: $isExpanded) { 19 | ForEach(group.scenes) { sceneKind in 20 | HStack { 21 | Text(sceneKind.name) 22 | .foregroundColor(viewModel.sceneKind == sceneKind ? .primary : .secondary) 23 | } 24 | .frame(maxWidth: .infinity, alignment: .leading) 25 | .tag(sceneKind) 26 | .contentShape(.rect) 27 | .onTapGesture { 28 | viewModel.sceneKind = sceneKind 29 | } 30 | } 31 | } label: { 32 | Text(group.rawValue) 33 | .font(.headline) 34 | .foregroundStyle(.secondary) 35 | .contentShape(.rect) 36 | }.onAppear { 37 | isExpanded = group.scenes.contains(viewModel.sceneKind) 38 | }.onChange(of: viewModel.sceneKind) { (_, newValue) in 39 | withAnimation { 40 | isExpanded = group.scenes.contains(newValue) 41 | } 42 | } 43 | } 44 | } 45 | 46 | struct RootView: View { 47 | @State var viewModel = ViewModel() 48 | @State var isOptionsOpen = false 49 | @State var isSideBarOPen = false 50 | 51 | init() { 52 | } 53 | 54 | private var sidebar: some View { 55 | List { 56 | ForEach(PlaygroundGroup.allCases) { sceneGroup in 57 | PlaygroundListItemView(group: sceneGroup, viewModel: viewModel) 58 | } 59 | } 60 | .listStyle(.inset) 61 | .frame(minWidth: 180) 62 | } 63 | 64 | @ViewBuilder 65 | private var options: some View { 66 | if !viewModel.hasConfig { 67 | EmptyView() 68 | } else if !isOptionsOpen { 69 | Text("Options") 70 | .padding(EdgeInsets(top: 5, leading: 8, bottom: 5, trailing: 8)) 71 | .background(Color(NSColor.windowBackgroundColor)) 72 | .cornerRadius(14) 73 | .padding() 74 | .onTapGesture { 75 | withAnimation { 76 | self.isOptionsOpen.toggle() 77 | } 78 | }.zIndex(1) 79 | } else { 80 | VStack(spacing: 28) { 81 | ConfigView() 82 | .environment(viewModel) 83 | } 84 | .frame(maxWidth: 210) 85 | .padding() 86 | .background(Color(NSColor.windowBackgroundColor)) 87 | .cornerRadius(12) 88 | .padding() 89 | .transition(.move(edge: .trailing)) 90 | .zIndex(2) 91 | .opacity(0.96) 92 | } 93 | } 94 | 95 | @State private var columnVisibility = NavigationSplitViewVisibility.all 96 | 97 | var body: some View { 98 | NavigationSplitView(columnVisibility: $columnVisibility) { 99 | sidebar 100 | } detail: { 101 | MetalSwiftView() 102 | .aspectRatio(1.0, contentMode: .fill) 103 | .environment(viewModel) 104 | .onTapGesture { 105 | withAnimation { 106 | if self.isOptionsOpen { 107 | self.isOptionsOpen = false 108 | } 109 | } 110 | } 111 | .zIndex(0) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /MetalPlayground/Root/ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/20/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | @Observable 13 | final class ViewModel { 14 | var sceneKind: SceneKind = .simonNoiseIntro { 15 | didSet { 16 | updateSceneSelection(kind: sceneKind) 17 | } 18 | } 19 | 20 | let renderer: Renderer 21 | 22 | var scene: Playground { 23 | sceneKind.scene 24 | } 25 | var hasConfig: Bool { 26 | sceneKind.scene.view != nil 27 | } 28 | @ObservationIgnored 29 | private var cancellables: [AnyCancellable] = [] 30 | private var view: MetalView? 31 | 32 | init() { 33 | self.renderer = Renderer() 34 | } 35 | 36 | func update(view: MetalView) { 37 | 38 | view.delegate = renderer 39 | view.renderer = renderer 40 | renderer.setup(view) 41 | 42 | self.view = view 43 | view.delegate = renderer 44 | 45 | updateSceneSelection(kind: sceneKind) 46 | } 47 | 48 | 49 | private func updateSceneSelection(kind: SceneKind) { 50 | renderer.scene = scene 51 | view?.enableSetNeedsDisplay = !scene.isPaused 52 | view?.isPaused = scene.isPaused 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/BookShaders/06Colors.metal: -------------------------------------------------------------------------------- 1 | // 2 | // 06Colors.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/9/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexIn { 13 | vector_float2 pos; 14 | }; 15 | 16 | struct FragmentUniforms { 17 | float time; 18 | float screen_width; 19 | float screen_height; 20 | float screen_scale; 21 | float2 mousePos; 22 | }; 23 | 24 | struct VertexOut { 25 | float4 pos [[position]]; 26 | float4 color; 27 | }; 28 | // Function from Iñigo Quiles 29 | // https://www.shadertoy.com/view/MsS3Wc 30 | vector_float3 hsb2rgbf(vector_float3 c ){ 31 | vector_float3 rgb = clamp( 32 | abs( 33 | fmod( 34 | c.x*6.0+vector_float3(0.0,4.0,2.0), 6.0 35 | ) 36 | -3.0)-1.0, 37 | 0.0, 38 | 1.0 ); 39 | rgb = rgb*rgb*(3.0-2.0*rgb); 40 | return c.z * mix(vector_float3(1.0), rgb, c.y); 41 | } 42 | 43 | 44 | 45 | vertex VertexOut bos_colors_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 46 | VertexIn in = vertexArray[vid]; 47 | VertexOut out; 48 | out.pos = float4(in.pos, 0, 1); 49 | return out; 50 | } 51 | 52 | fragment float4 bos_colors_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 53 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_height}; 54 | 55 | st = float2(0.5) - st; 56 | st.x *= uniforms.screen_width/uniforms.screen_height; 57 | // st.y /= 2; 58 | float2 toCenter = st; 59 | 60 | float angleTimeOffset = float(int(uniforms.time * 100) % 360); 61 | angleTimeOffset = (angleTimeOffset / 360) * 2 * 3.14; 62 | 63 | float angle = atan2(toCenter.y, toCenter.x); // between -pi → +pi 64 | 65 | float angle2PI = (angle + angleTimeOffset)/(2 * 3.141); 66 | 67 | float hue = angle2PI; 68 | float saturation = length(toCenter) * 2.0; // extend length from max 0.5 to max 1.0 69 | 70 | hue = smoothstep(0.4, 0.8, hue); 71 | 72 | vector_float3 hsb = {hue, saturation, 1.0}; 73 | vector_float3 col = hsb2rgbf(hsb); 74 | 75 | float4 color = float4(col, 1); 76 | return color; 77 | } 78 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/BookShaders/07ShapePrimitives.metal: -------------------------------------------------------------------------------- 1 | // 2 | // 07ShapePrimitives.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/11/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | float3 blurRect(float2 st, float time, float4 margins) { 13 | float blur = (1 + sin(time * 10))/2 * 0.04; 14 | float2 left_bottom = smoothstep({margins[1], margins[2]}, {margins[1] + blur, margins[2] + blur}, st); 15 | float2 right_top = smoothstep({margins[3], margins[0]}, {margins[3] + blur, margins[0] + blur}, 1 - st); 16 | 17 | float visibility = left_bottom.x * left_bottom.y * right_top.x * right_top.y; 18 | 19 | float3 color = visibility * float3(0.5, 0.1, 0.9); 20 | return color; 21 | } 22 | 23 | float3 rect(float2 st, float time, float4 margins, float3 mainColor) { 24 | float2 left_bottom = step({margins[1], margins[2]}, st); 25 | float2 right_top = step({margins[3], margins[0]}, 1 - st); 26 | 27 | float visibility = left_bottom.x * left_bottom.y * right_top.x * right_top.y; 28 | 29 | float3 color = visibility * mainColor; 30 | return color; 31 | } 32 | 33 | float3 outlineRect(float2 st, float4 margins, float time) { 34 | float thickness = 0.01; 35 | float4 innerMargins = margins + thickness; 36 | 37 | float2 left_bottom = step({margins[1], margins[2]}, st); 38 | float2 right_top = step({margins[3], margins[0]}, 1 - st); 39 | 40 | float2 left_bottom_inner = step({innerMargins[1], innerMargins[2]}, st); 41 | float2 right_top_inner = step({innerMargins[3], innerMargins[0]}, 1 - st); 42 | 43 | float visibility_outer = left_bottom.x * left_bottom.y * right_top.x * right_top.y; 44 | float visibility_inner = left_bottom_inner.x * left_bottom_inner.y * right_top_inner.x * right_top_inner.y; 45 | 46 | float visibility = visibility_outer * (1 - visibility_inner); 47 | 48 | float3 color = visibility * float3(0.5, 0.1, 0.9); 49 | return color; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/BookShaders/07Shapes.metal: -------------------------------------------------------------------------------- 1 | // 2 | // 06Colors.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/9/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | #include "../ShaderHeaders.h" 12 | 13 | struct VertexIn { 14 | vector_float2 pos; 15 | }; 16 | 17 | struct FragmentUniforms { 18 | float time; 19 | float screen_width; 20 | float screen_height; 21 | float screen_scale; 22 | float2 mousePos; 23 | }; 24 | 25 | struct VertexOut { 26 | float4 pos [[position]]; 27 | float4 color; 28 | }; 29 | 30 | 31 | vertex VertexOut bos_shapes_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 32 | VertexIn in = vertexArray[vid]; 33 | VertexOut out; 34 | out.pos = float4(in.pos, 0, 1); 35 | return out; 36 | } 37 | 38 | float3 hash32(float2 p) { 39 | float3 p3 = fract(float3(p.xyx) * float3(.1031, .1030, .0973)); 40 | p3 += dot(p3, p3.yxz+33.33); 41 | return fract((p3.xxy+p3.yzz)*p3.zyx); 42 | } 43 | 44 | float rectangle(float2 st, float width) { 45 | float bl = step(width, st.x) * step(width, st.y); 46 | float tr = (1 - step((1 - width), st.x)) * (1 - step((1 - width), st.y)); 47 | return bl * tr; 48 | } 49 | 50 | float3 circle(float2 st, float time) { 51 | // st = 4 * (st + 0.5); 52 | // st = fract(st) - 0.5; 53 | float t = min(length(st), length(st - 0.3)); 54 | float r = 0.5 * absSin(time); 55 | return smoothstep(r - 0.02, r, t); 56 | } 57 | 58 | fragment float4 bos_shapes_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 59 | float3 green = float3(0.4, 0.7, 0.1); 60 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_height}; 61 | // st = float2(0.5) - st; 62 | 63 | st.x *= uniforms.screen_width/uniforms.screen_height; 64 | 65 | st = 2 * st - 1; 66 | 67 | float l = length(float2(0.3) - abs(st)); 68 | float pct = step(0.5, l); 69 | pct = fract(l * 10); 70 | 71 | float3 col = green * pct; 72 | 73 | float4 color = float4(col, 1); 74 | return color; 75 | } 76 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/BookShaders/08LeftRightTiler.metal: -------------------------------------------------------------------------------- 1 | // 2 | // 07FuturisticUI.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 12/31/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | 13 | struct VertexOut { 14 | float4 pos [[position]]; 15 | float4 color; 16 | }; 17 | 18 | struct VertexIn { 19 | vector_float2 pos; 20 | }; 21 | 22 | 23 | struct FragmentUniforms { 24 | float time; 25 | float screen_width; 26 | float screen_height; 27 | float screen_scale; 28 | float2 mousePos; 29 | }; 30 | 31 | 32 | float3 bricks(float2 uv, float time) { 33 | float zoom = 20.0; 34 | uv *= zoom; 35 | time *= 1.3; 36 | float horizontal = step(fmod(time, 4.0), 2.0); 37 | float offset = step(fmod(horizontal > 0 ? uv.x : uv.y, 2.0), 1.0) * 2.0 - 1.0; 38 | uv = fract(uv); 39 | 40 | uv = 2.0*uv - 1.; 41 | uv.y = -uv.y; 42 | 43 | float t = 0; 44 | 45 | uv.y += fmod(time , 2.0) * offset * horizontal; 46 | uv.x += fmod(time , 2.0) * offset * (1- horizontal); 47 | 48 | float side = 0.52; 49 | t += smoothstep(side,side-0.001, length(uv)); 50 | 51 | if (horizontal > 0) { 52 | uv.y -= 2.0; 53 | } else { 54 | uv.x -= 2.0; 55 | } 56 | t += smoothstep(side,side-0.001, length(uv)); 57 | if (horizontal > 0) { 58 | uv.y += 4.0; 59 | } else { 60 | uv.x += 4.0; 61 | } 62 | t += smoothstep(side,side-0.001, length(uv)); 63 | return (1-t) * float3(0.8); 64 | } 65 | 66 | vertex VertexOut leftright_vertex(const device VertexIn *vertices [[buffer(0)]], unsigned int vid [[vertex_id]]) { 67 | VertexOut v; 68 | v.pos = float4(vertices[vid].pos, 0, 1); 69 | return v; 70 | } 71 | 72 | fragment float4 leftright_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 73 | float2 uv = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y/uniforms.screen_height}; 74 | uv.x *= uniforms.screen_width / uniforms.screen_height; 75 | float time = uniforms.time; 76 | float3 col = bricks(uv, time); 77 | return float4( col, 1.0 ); 78 | } 79 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/BookShaders/BoSShapes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoSShapes.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/17/23. 6 | // Copyright © 2023 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MetalKit 11 | import SwiftUI 12 | 13 | class BoSShapes07: Playground { 14 | var fileName: String { 15 | "BookShaders/07Shapes" 16 | } 17 | var vertexFuncName: String { "bos_shapes_vertex" } 18 | var fragmentFuncName: String { "bos_shapes_fragment" } 19 | required init() {} 20 | 21 | enum SketchKind: Int, CaseIterable, Identifiable { 22 | case bezierCurve 23 | case flowingCurves 24 | 25 | var id: Int { rawValue } 26 | 27 | var name: String { 28 | switch self { 29 | case .bezierCurve: 30 | return "Bezier Curve" 31 | case .flowingCurves: 32 | return "Flowing Curves" 33 | } 34 | } 35 | } 36 | 37 | class Config: ObservableObject { 38 | @Published var kind: SketchKind = .bezierCurve 39 | } 40 | 41 | struct Uniforms { 42 | let kind: Float 43 | } 44 | 45 | fileprivate var config: Config = .init() 46 | var fragmentUniforms: Uniforms { 47 | .init(kind: Float(config.kind.rawValue)) 48 | } 49 | 50 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) { 51 | var uniforms = fragmentUniforms 52 | let length = MemoryLayout.stride(ofValue: uniforms) 53 | encoder.setFragmentBytes(&uniforms, length: length, index: 1) 54 | } 55 | 56 | struct ConfigView: View { 57 | @EnvironmentObject private var config: Config 58 | @State fileprivate var kind = SketchKind.bezierCurve 59 | 60 | var body: some View { 61 | VStack(alignment: .leading, spacing: 19) { 62 | Picker(selection: $config.kind, label: Text("Kind")) { 63 | ForEach(SketchKind.allCases) { 64 | Text($0.name).tag($0) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | var view: NSView? { 72 | NSHostingView( 73 | rootView: ConfigView().environmentObject(config) 74 | ) 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/BookShaders/Shaping.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Shaping.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/10/23. 6 | // Copyright © 2023 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | #include "../ShaderHeaders.h" 12 | 13 | struct VertexOut { 14 | float4 pos [[position]]; 15 | float4 color; 16 | }; 17 | 18 | struct VertexIn { 19 | vector_float2 pos; 20 | }; 21 | 22 | struct FragmentUniforms { 23 | float time; 24 | float screen_width; 25 | float screen_height; 26 | float screen_scale; 27 | float2 mousePos; 28 | }; 29 | 30 | typedef enum { 31 | Bezier = 0, 32 | FlowingCurves = 1, 33 | } SketchKind; 34 | 35 | struct ShapingUniforms { 36 | float kind; 37 | }; 38 | 39 | 40 | vertex VertexOut bos_shaping_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 41 | VertexIn in = vertexArray[vid]; 42 | VertexOut out; 43 | out.pos = float4(in.pos, 0, 1); 44 | return out; 45 | } 46 | 47 | float bezier(float2 uv, float a, float b) { 48 | float epsilon = 0.00001; 49 | float x = uv.x; 50 | a = max(0.0, min(1.0, a)); 51 | b = max(0.0, min(1.0, b)); 52 | if (a == 0.5){ 53 | a += epsilon; 54 | } 55 | 56 | // solve t from x (an inverse operation) 57 | float om2a = 1 - 2*a; 58 | float t = (sqrt(a*a + om2a*x) - a)/om2a; 59 | float y = (1.0-2*b)*(t*t) + (2*b)*t; 60 | return y; 61 | return 0.0; 62 | } 63 | 64 | fragment float4 bos_shaping_fragment( 65 | VertexOut interpolated [[stage_in]], 66 | constant FragmentUniforms &uniforms [[buffer(0)]], 67 | constant ShapingUniforms &shapingUniforms [[buffer(1)]] 68 | ) 69 | { 70 | float t = uniforms.time; 71 | float2 st = {interpolated.pos.x / uniforms.screen_width, interpolated.pos.y / uniforms.screen_height}; 72 | 73 | float3 color; 74 | float d = 0; 75 | if (shapingUniforms.kind == Bezier) { 76 | st.y = 1 - st.y; 77 | st.x *= uniforms.screen_width / uniforms.screen_height; 78 | st.x = fract(st.x * 12 + t); 79 | d = bezier(st, 0.082, 0.89); 80 | d = step(d, st.y); 81 | color = min(d, step(0.5, st.y)); 82 | } else if (shapingUniforms.kind == FlowingCurves) { 83 | float l = 0.3 + 0.2 * (1. + sin(t))/4; 84 | for (int i=0; i<3; i++) { 85 | float2 uv = st; 86 | uv.x *= sin(uv.x + uv.y) + uv.y + sin(t * 1.1) / 15; 87 | uv.y *= sin(uv.y) + uv.x + cos(t * 1.4) / 4; 88 | l += sin(t * i) + 0.2 * length(uv); 89 | color[i] = l; 90 | } 91 | } else { 92 | color = 0.3; 93 | } 94 | 95 | return vector_float4(color, 1.0); 96 | } 97 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/05Algorithmic.metal: -------------------------------------------------------------------------------- 1 | // 2 | // 05Algorithmic.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/6/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexIn { 13 | vector_float2 pos; 14 | }; 15 | 16 | struct FragmentUniforms { 17 | float time; 18 | float screen_width; 19 | float screen_height; 20 | float screen_scale; 21 | float2 mousePos; 22 | }; 23 | 24 | struct VertexOut { 25 | float4 pos [[position]]; 26 | float4 color; 27 | }; 28 | 29 | float plotColor(float2 st, float y) { 30 | if (abs(st.y - y) <= 0.01) { 31 | // it's a plot point 32 | return 1; 33 | } else { 34 | return 0; 35 | } 36 | 37 | } 38 | 39 | vertex VertexOut smoothing_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 40 | VertexIn in = vertexArray[vid]; 41 | VertexOut out; 42 | out.pos = float4(in.pos, 0, 1); 43 | return out; 44 | } 45 | 46 | fragment float4 smoothing_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 47 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_width}; 48 | float x = st.x * 2*M_PI_F; 49 | 50 | float y = fract(4+sin(x))/8; 51 | 52 | float3 color = plotColor(st, y); 53 | 54 | return vector_float4(color, 1.0); 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/07FuturisticUI.metal: -------------------------------------------------------------------------------- 1 | // 2 | // 07FuturisticUI.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 12/31/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | #include "../ShaderHeaders.h" 13 | 14 | struct VertexOut { 15 | float4 pos [[position]]; 16 | float4 color; 17 | }; 18 | 19 | struct VertexIn { 20 | vector_float2 pos; 21 | }; 22 | 23 | 24 | struct FragmentUniforms { 25 | float time; 26 | float screen_width; 27 | float screen_height; 28 | float screen_scale; 29 | float2 mousePos; 30 | }; 31 | 32 | // --- Primitives 33 | 34 | // box: left, top, right, bottom 35 | float3 rectangle(float2 uv, float4 box, float3 color) { 36 | float d = 0.001; 37 | float left = smoothstep(box.x, box.x+d, uv.x); 38 | float top = smoothstep(box.y+d, box.y, uv.y); 39 | float right = smoothstep(box.z+d, box.z, uv.x); 40 | float bottom = smoothstep(box.w, box.w+d, uv.y); 41 | 42 | return 43 | (left*top * right*bottom) * color; 44 | } 45 | 46 | float circleSmooth(float2 uv, float r) { 47 | return smoothstep(r+0.01, r, length(uv)); 48 | } 49 | 50 | float triangle(float2 uv) { 51 | // Number of sides of your shape 52 | int N = 3; 53 | 54 | // angle progress for this pixel 55 | float a = atan2(uv.x,uv.y); 56 | // Angle / polygon's side 57 | float r = (M_PI_F*2.0)/float(N); 58 | 59 | // Shaping function that modulate the distance 60 | float d = cos( 61 | floor(.5 + a/r) 62 | *r 63 | -a 64 | )* 65 | length(uv); 66 | 67 | return 1.0-smoothstep(.4,.4001,d); 68 | // color = vec3(d); 69 | } 70 | 71 | 72 | // --- Transformation 73 | 74 | // --- Shapes 75 | 76 | float circleOutline(float2 uv, float r, float th) { 77 | return circleSmooth(uv, r) - circleSmooth(uv, r - th); 78 | } 79 | 80 | float wedge(float2 uv, float r, float s, float e) { 81 | float angle = atan2(uv.y, uv.x); 82 | float modulate = smoothstep(s, e, angle) * (1. - step(e, angle)) * 0.6; 83 | return circleSmooth(uv, r) * modulate; 84 | } 85 | 86 | float arc(float2 uv, float r, float angleSt, float angleEnd, float th) { 87 | uv = rotate(M_PI_F) * uv; 88 | float angle = atan2(uv.y, uv.x); 89 | angle = lerp(angle, -M_PI_F, M_PI_F, 0, M_PI_F * 2.); 90 | if (angle < angleSt || angle > angleEnd) { return 0.; } 91 | return circleOutline(uv, r, th); 92 | } 93 | 94 | float animatedArc(float2 uv, float r, float time) { 95 | uv = rotate(M_PI_F/2.) * uv; 96 | return arc(uv, r, -M_PI_F, lerp(sin(time), -1.,1., -M_PI_F, M_PI_F ), 0.01); 97 | } 98 | 99 | // --- Scene 100 | 101 | float3 fui(float2 uv, float time) { 102 | float anim = sin(time); 103 | float3 dark = 0.4; 104 | float3 light = 0.9; 105 | float originalR = 0.8; 106 | float r = originalR; 107 | float d = 0.07; 108 | d = d/2.; 109 | float arcSpan = M_PI_F/4.0; 110 | float outerTH = 0.01; 111 | float outerLightArc = 112 | arc(rotate(-M_PI_F/4.0-0.0)*uv, r, d, arcSpan - d, outerTH) 113 | + 114 | arc(uv, r, d, arcSpan - d, outerTH) 115 | + 116 | arc(rotate(M_PI_F*0.75)*uv, r, d, arcSpan - d, outerTH) 117 | + 118 | arc(rotate(M_PI_F*1.0)*uv, r, d, arcSpan - d, outerTH) 119 | ; 120 | float outerDarkArc = 121 | arc(rotate(M_PI_F*0.25)*uv, r, d, arcSpan - d, outerTH) 122 | + 123 | arc(rotate(M_PI_F*0.5)*uv, r, d, arcSpan - d, outerTH) 124 | + 125 | arc(rotate(M_PI_F*1.25)*uv, r, d, arcSpan - d, outerTH) 126 | + 127 | arc(rotate(M_PI_F*1.5)*uv, r, d, arcSpan - d, outerTH) 128 | ; 129 | float3 outerArc = outerLightArc * light 130 | + outerDarkArc * dark 131 | ; 132 | 133 | r -= 0.1; 134 | float smallArcAnimD = lerp(anim, -1., 1., 0., 0.4); 135 | float outerSmallArcs = 0; 136 | for (int i=0; i<2; i++) { 137 | float rotateAngle = M_PI_F * (i+0.25); 138 | outerSmallArcs += 139 | arc(rotate(rotateAngle - smallArcAnimD)*uv, 140 | r, 141 | d - smallArcAnimD/2.0, 142 | arcSpan*2.0 - d + 2.*smallArcAnimD, 143 | outerTH/2.0 144 | ); 145 | } 146 | // + 147 | // arc(rotate(M_PI_F*1.25)*uv, r, d + smallArcAnimD, arcSpan*2 - d, outerTH/2.0); 148 | float3 outerSmallArc = outerSmallArcs * dark; 149 | 150 | r -= 0.07; 151 | float3 middleCircle = 152 | circleOutline(uv, r, outerTH*0.8) * light; 153 | 154 | float grid = 155 | // smoothstep(0.0031, 0.003, abs(uv.y)) 156 | // + 157 | // smoothstep(0.0031, 0.003, abs(uv.x)) 158 | // + 159 | smoothstep(0.0031, 0.003, abs(uv.x - uv.y)) 160 | + 161 | smoothstep(0.0031, 0.003, abs(uv.y + uv.x)) 162 | ; 163 | grid *= 1.0 - step(r, length(uv)); 164 | 165 | float3 innerCircles = 166 | (circleOutline(uv, r-0.2, outerTH*0.8) 167 | + 168 | circleOutline(uv, r-0.4, outerTH*0.8)) 169 | * dark; 170 | 171 | float triOffset = originalR + lerp(anim, -1.,1., 0.06,-0.06); 172 | float2 triUV = abs(uv); 173 | triUV.y -= triOffset; 174 | 175 | triUV = scale(.021)*triUV; 176 | float triTop = triangle(triUV); 177 | triUV = abs(uv); 178 | triUV.x -= triOffset; 179 | triUV = rotate(-M_PI_F/2.) * scale(.021)*triUV; 180 | float triRight = triangle(triUV); 181 | 182 | // move in elliptical angles 183 | float redOffsetAngle = lerp(fract(time/42.), -1., 1., 0, -4.*M_PI_F); 184 | float redOffsetRad = -0.01 * redOffsetAngle + sin(redOffsetAngle)*cos(redOffsetAngle); 185 | float redOffsetY = redOffsetRad*sin(redOffsetAngle); 186 | float redOffsetX = redOffsetRad*cos(redOffsetAngle); 187 | float2 redOffset = {redOffsetX, redOffsetY}; 188 | float redBigCircleRad = 0.2 * fract(time*1.2); 189 | float redBigCircleTH = 0.04; 190 | float2 redBigCircleUV = uv - redOffset; 191 | float redBigCircleT = circleOutline(redBigCircleUV, redBigCircleRad, redBigCircleTH); 192 | redBigCircleT *= smoothstep(redBigCircleRad - redBigCircleTH, redBigCircleRad, length(redBigCircleUV)); 193 | float3 pulseColor = float3(0.76,0.34,0.03); 194 | float3 redBigCircle = redBigCircleT * pulseColor; 195 | 196 | float3 pulseSmallStaticCircle = circleOutline(redBigCircleUV, 0.015, 0.005) * pulseColor; 197 | float smallPulseDuration = step(0.0, (sin(anim*40.))/4.0); 198 | float3 pulseSmallPulsingCircle = circleSmooth(redBigCircleUV, 0.007) * smallPulseDuration * pulseColor; 199 | 200 | // float radarAngle = lerp(anim, -1.0, 1.0, 0, 2*M_PI_F); 201 | float radarSpan = 0.5; 202 | float radarRotate = time*4.2/(2*M_PI_F); 203 | float2 radarUV = rotate(radarRotate)*uv; 204 | float radarArcT = wedge(radarUV, r, 0.0, radarSpan); 205 | float3 radarArc = radarArcT * light; 206 | 207 | return outerArc + outerSmallArc + middleCircle + innerCircles 208 | + triTop 209 | + triRight 210 | 211 | + grid * dark/3.0 212 | + redBigCircle 213 | + pulseSmallStaticCircle 214 | + pulseSmallPulsingCircle 215 | + radarArc 216 | ; 217 | } 218 | 219 | float3 scene(float2 uv, float time) { 220 | return fui(uv, time); 221 | 222 | // return wedge(uv, 0.3, 0.0, 0.9); 223 | } 224 | 225 | 226 | vertex VertexOut futuristic_UI_vertex(const device VertexIn *vertices [[buffer(0)]], unsigned int vid [[vertex_id]]) { 227 | VertexOut v; 228 | v.pos = float4(vertices[vid].pos, 0, 1); 229 | return v; 230 | } 231 | 232 | fragment float4 futuristic_UI_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 233 | float2 uv = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y/uniforms.screen_height}; 234 | uv -= 0.5; 235 | uv *= 2; 236 | uv.x *= uniforms.screen_width / uniforms.screen_height; 237 | float time = uniforms.time; 238 | float3 col = scene(uv, time); 239 | return float4( col, 1.0 ); 240 | } 241 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/AudioViz.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | #include "../ShaderHeaders.h" 4 | 5 | struct VertexIn { 6 | vector_float2 pos; 7 | }; 8 | 9 | struct FragmentUniforms { 10 | float time; 11 | float screen_width; 12 | float screen_height; 13 | float screen_scale; 14 | }; 15 | 16 | struct VertexOut { 17 | float4 pos [[position]]; 18 | float4 color; 19 | }; 20 | 21 | float3 palette01(float t) { 22 | float3 a = float3(0.738, 0.870, 0.870); 23 | float3 b = float3(0.228, 0.500, 0.500); 24 | float3 c = float3(1.0, 1.0, 1.0); 25 | float3 d = float3(0.000, 0.333, 0.667); 26 | 27 | return a + b * cos(6.28318 * (c * t + d)); 28 | } 29 | 30 | vertex VertexOut audioVizVertexShader(const device VertexIn *vertices [[buffer(0)]], unsigned int vid [[vertex_id]]) { 31 | VertexOut in; 32 | in.pos = {vertices[vid].pos.x, vertices[vid].pos.y, 0, 1}; 33 | return in; 34 | } 35 | 36 | struct LiveCodeUniforms { 37 | uint samplesCount; 38 | }; 39 | 40 | float sin01(float v) 41 | { 42 | return 0.5 + 0.5 * sin(v); 43 | } 44 | 45 | float drawCircle(float r, float polarRadius, float thickness) 46 | { 47 | return smoothstep(r, r + thickness, polarRadius) - 48 | smoothstep(r + thickness, r + 2.0 * thickness, polarRadius); 49 | } 50 | 51 | 52 | fragment float4 audioVizFragmentShader( 53 | VertexOut interpolated [[stage_in]], 54 | constant FragmentUniforms &uniforms [[buffer(0)]], 55 | const constant float *loudnessBuffer [[buffer(1)]], 56 | const constant float *frequenciesBuffer [[buffer(2)]] 57 | ) { 58 | 59 | float2 uv = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y/uniforms.screen_height}; 60 | uv = 2 * (uv - 0.5); 61 | if (uniforms.screen_width > uniforms.screen_height) { 62 | uv.x *= uniforms.screen_width/uniforms.screen_height; 63 | } else { 64 | uv.y *= uniforms.screen_height/uniforms.screen_width; 65 | } 66 | 67 | float p = length(uv); 68 | float pa = atan2(uv.y, uv.x); 69 | 70 | // Frequency: 71 | // map -1 → 1 to 0 → 361 72 | // int index = int(lerp(uv.x, -1.0, 1.0, 0, 361)); 73 | float indexRad = (0.5 * pa / 3.1415 + 1) / 2.0; // 0 → 1 74 | 75 | int index = lerp(indexRad, 0, 1, 0, 361); 76 | 77 | float freq = frequenciesBuffer[index]; 78 | freq = sin(indexRad ) * freq; 79 | float o = 0; 80 | float inc = 0; 81 | 82 | float numRings = 7; 83 | float ringSpacing = 0.01; 84 | float ringThickness = 0.10; 85 | float rotationDistortion = 0.3; 86 | for (float i = 0; i < numRings; i += 1.0) { 87 | float baseR; 88 | baseR = 0.2 * sin(freq * 0.8); 89 | float r = baseR + inc; 90 | 91 | r += rotationDistortion * (0.5 + 0.1 * sin(pa * i + 0.3 * uniforms.time * (i - 0.1))); 92 | // r += loudness/4; 93 | r = min(0.8, r); 94 | o += drawCircle(r, p, ringThickness * (1.0 + 0.12 * freq * (i - 1.0))); 95 | 96 | inc += ringSpacing; 97 | } 98 | 99 | float3 bgCol = float3(0.9 - cos(uniforms.time/2) * 0.2 * uv.y, 0.12, 0.3 - sin(uniforms.time/2) * 0.2 * uv.y); 100 | float3 col = mix(bgCol, float3(1, 0.7, 0.3), o); 101 | 102 | return float4(col, 1); 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/AudioViz.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveCode.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 1/9/21. 6 | // Copyright © 2021 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import MetalKit 10 | import SwiftUI 11 | import Accelerate 12 | import AVFoundation 13 | 14 | extension AVAudioPCMBuffer { 15 | /// Returns audio data as an `Array` of `Float` Arrays. 16 | /// 17 | /// If stereo: 18 | /// - `floatChannelData?[0]` will contain an Array of left channel samples as `Float` 19 | /// - `floatChannelData?[1]` will contains an Array of right channel samples as `Float` 20 | func toFloatChannelData() -> [[Float]]? { 21 | // Do we have PCM channel data? 22 | guard let pcmFloatChannelData = floatChannelData else { 23 | return nil 24 | } 25 | 26 | let channelCount = Int(format.channelCount) 27 | let frameLength = Int(self.frameLength) 28 | let stride = self.stride 29 | 30 | // Preallocate our Array so we're not constantly thrashing while resizing as we append. 31 | let zeroes: [Float] = Array(repeating: 0, count: frameLength) 32 | var result = Array(repeating: zeroes, count: channelCount) 33 | 34 | // Loop across our channels... 35 | for channel in 0 ..< channelCount { 36 | // Make sure we go through all of the frames... 37 | for sampleIndex in 0 ..< frameLength { 38 | result[channel][sampleIndex] = pcmFloatChannelData[channel][sampleIndex * stride] 39 | } 40 | } 41 | 42 | return result 43 | } 44 | } 45 | 46 | extension AVAudioFile { 47 | /// converts to a 32 bit PCM buffer 48 | func toAVAudioPCMBuffer() -> AVAudioPCMBuffer? { 49 | guard let buffer = AVAudioPCMBuffer(pcmFormat: processingFormat, 50 | frameCapacity: AVAudioFrameCount(length)) else { return nil } 51 | 52 | do { 53 | framePosition = 0 54 | try read(into: buffer) 55 | print("Created buffer with format") 56 | 57 | } catch let error as NSError { 58 | print("Cannot read into buffer " + error.localizedDescription) 59 | } 60 | 61 | return buffer 62 | } 63 | 64 | /// converts to Swift friendly Float array 65 | public func toFloatChannelData() -> [[Float]]? { 66 | guard let pcmBuffer = toAVAudioPCMBuffer(), 67 | let data = pcmBuffer.toFloatChannelData() else { return nil } 68 | return data 69 | } 70 | } 71 | /// Returns the minimums of chunks of binSize. 72 | func binMin(samples: [Float], binSize: Int) -> [Float] { 73 | var out: [Float] = .init(repeating: 0.0, count: samples.count / binSize) 74 | 75 | // Note: we have to use a dumb while loop to avoid swift's Range and have 76 | // decent perf in debug. 77 | var bin = 0 78 | while bin < out.count { 79 | 80 | // Note: we could do the following but it's too slow in debug 81 | // out[bin] = samples[(bin * binSize) ..< ((bin + 1) * binSize)].min()! 82 | 83 | var v = Float.greatestFiniteMagnitude 84 | let start: Int = bin * binSize 85 | let end: Int = (bin + 1) * binSize 86 | var i = start 87 | while i < end { 88 | v = min(samples[i], v) 89 | i += 1 90 | } 91 | out[bin] = v 92 | bin += 1 93 | } 94 | return out 95 | } 96 | 97 | /// Returns the maximums of chunks of binSize. 98 | func binMax(samples: [Float], binSize: Int) -> [Float] { 99 | var out: [Float] = .init(repeating: 0.0, count: samples.count / binSize) 100 | 101 | // Note: we have to use a dumb while loop to avoid swift's Range and have 102 | // decent perf in debug. 103 | var bin = 0 104 | while bin < out.count { 105 | 106 | // Note: we could do the following but it's too slow in debug 107 | // out[bin] = samples[(bin * binSize) ..< ((bin + 1) * binSize)].max()! 108 | 109 | var v = -Float.greatestFiniteMagnitude 110 | let start: Int = bin * binSize 111 | let end: Int = (bin + 1) * binSize 112 | var i = start 113 | while i < end { 114 | v = max(samples[i], v) 115 | i += 1 116 | } 117 | out[bin] = v 118 | bin += 1 119 | } 120 | return out 121 | } 122 | 123 | public final class SampleBuffer: Sendable { 124 | let samples: [Float] 125 | 126 | /// Initialize the buffer with samples 127 | public init(samples: [Float]) { 128 | self.samples = samples 129 | } 130 | 131 | /// Number of samples 132 | public var count: Int { 133 | samples.count 134 | } 135 | } 136 | 137 | final class AudioVizScene: Playground { 138 | var fileName: String { 139 | "Explorations/AudioViz" 140 | } 141 | 142 | let vertexFuncName = "audioVizVertexShader" 143 | let fragmentFuncName = "audioVizFragmentShader" 144 | 145 | private var shaderContents = "" 146 | var sample: SampleBuffer = .init(samples: []) 147 | 148 | let queue = DispatchQueue(label: "live-code") 149 | let engine: AVAudioEngine 150 | 151 | init() { 152 | engine = AVAudioEngine() 153 | _ = engine.mainMixerNode 154 | engine.prepare() 155 | do { 156 | try engine.start() 157 | } catch { 158 | print(error.localizedDescription) 159 | } 160 | 161 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { 162 | let file = Bundle.main.url(forResource: "raga", withExtension: "mp3")! 163 | // let file = Bundle.main.url(forResource: "who", withExtension: "mp3")! 164 | // let file = Bundle.main.url(forResource: "laila", withExtension: "m4a")! 165 | // let file = Bundle.main.url(forResource: "malabar", withExtension: "mp3")! 166 | let audio = try! AVAudioFile(forReading: file) 167 | let format = audio.processingFormat 168 | let player = AVAudioPlayerNode() 169 | self.engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buffer, time in 170 | self?.processAudioData(buffer: buffer) 171 | } 172 | self.engine.attach(player) 173 | self.engine.connect(player, to: self.engine.mainMixerNode, format: format) 174 | player.scheduleFile(audio, at: nil) 175 | player.play() 176 | } 177 | } 178 | 179 | var ready: Bool { 180 | self.loudnessBuffer != nil && self.freqeuencyBuffer != nil 181 | } 182 | 183 | func processAudioData(buffer: AVAudioPCMBuffer) { 184 | guard let channelData = buffer.floatChannelData?[0] else { return } 185 | let frames = buffer.frameLength 186 | 187 | //rms 188 | let rmsValue = rms(data: channelData, frameLength: UInt(frames)) 189 | loudnessMagnitude = rmsValue 190 | 191 | //fft 192 | let fftMagnitudes = AudioVizScene.fft(data: channelData, setup: fftSetup!) 193 | frequencyVertices = fftMagnitudes 194 | // print(fftMagnitudes.max()!) 195 | } 196 | 197 | static func fft(data: UnsafeMutablePointer, setup: OpaquePointer) -> [Float]{ 198 | //output setup 199 | var realIn = [Float](repeating: 0, count: 1024) 200 | var imagIn = [Float](repeating: 0, count: 1024) 201 | var realOut = [Float](repeating: 0, count: 1024) 202 | var imagOut = [Float](repeating: 0, count: 1024) 203 | 204 | //fill in real input part with audio samples 205 | for i in 0...1023 { 206 | realIn[i] = data[i] 207 | } 208 | 209 | 210 | vDSP_DFT_Execute(setup, &realIn, &imagIn, &realOut, &imagOut) 211 | 212 | //our results are now inside realOut and imagOut 213 | 214 | //package it inside a complex vector representation used in the vDSP framework 215 | var complex = DSPSplitComplex(realp: &realOut, imagp: &imagOut) 216 | 217 | //setup magnitude output 218 | var magnitudes = [Float](repeating: 0, count: 512) 219 | 220 | //calculate magnitude results 221 | vDSP_zvabs(&complex, 1, &magnitudes, 1, 512) 222 | 223 | //normalize 224 | var normalizedMagnitudes = [Float](repeating: 0.0, count: 512) 225 | var scalingFactor = Float(30.0/512) 226 | vDSP_vsmul(&magnitudes, 1, &scalingFactor, &normalizedMagnitudes, 1, 512) 227 | 228 | return normalizedMagnitudes 229 | } 230 | 231 | static func interpolate(current: Float, previous: Float) -> [Float]{ 232 | var vals = [Float](repeating: 0, count: 11) 233 | vals[10] = current 234 | vals[5] = (current + previous)/2 235 | vals[2] = (vals[5] + previous)/2 236 | vals[1] = (vals[2] + previous)/2 237 | vals[8] = (vals[5] + current)/2 238 | vals[9] = (vals[10] + current)/2 239 | vals[7] = (vals[5] + vals[9])/2 240 | vals[6] = (vals[5] + vals[7])/2 241 | vals[3] = (vals[1] + vals[5])/2 242 | vals[4] = (vals[3] + vals[5])/2 243 | vals[0] = (previous + vals[1])/2 244 | 245 | return vals 246 | } 247 | 248 | var prevRMSValue : Float = 0.3 249 | 250 | //fft setup object for 1024 values going forward (time domain -> frequency domain) 251 | let fftSetup = vDSP_DFT_zop_CreateSetup(nil, 1024, vDSP_DFT_Direction.FORWARD) 252 | 253 | 254 | private func rms(data: UnsafeMutablePointer, frameLength: UInt) -> Float { 255 | var val : Float = 0 256 | vDSP_measqv(data, 1, &val, frameLength) 257 | 258 | var db = 10*log10f(val) 259 | //inverse dB to +ve range where 0(silent) -> 160(loudest) 260 | db = 160 + db; 261 | //Only take into account range from 120->160, so FSR = 40 262 | db = db - 120 263 | 264 | let dividor = Float(40/0.3) 265 | var adjustedVal = 0.3 + db/dividor 266 | 267 | //cutoff 268 | if (adjustedVal < 0.3) { 269 | adjustedVal = 0.3 270 | } else if (adjustedVal > 0.6) { 271 | adjustedVal = 0.6 272 | } 273 | return adjustedVal 274 | } 275 | 276 | func tearDown() { 277 | } 278 | 279 | private var device: MTLDevice? { 280 | didSet { 281 | if let device { 282 | loudnessBuffer = device.makeBuffer(bytes: &loudnessMagnitude, length: MemoryLayout.stride) 283 | freqeuencyBuffer = device.makeBuffer(bytes: frequencyVertices, length: frequencyVertices.count * MemoryLayout.stride, options: [])! 284 | 285 | } 286 | } 287 | } 288 | private var pixelFormat: MTLPixelFormat? 289 | 290 | private var loudnessMagnitude: Float = 0 { 291 | didSet { 292 | loudnessBuffer = device?.makeBuffer(bytes: &loudnessMagnitude, length: MemoryLayout.stride) 293 | } 294 | } 295 | private var loudnessBuffer: MTLBuffer? 296 | private var freqeuencyBuffer : MTLBuffer! 297 | public var frequencyVertices : [Float] = [Float](repeating: 0, count: 361) { 298 | didSet{ 299 | let sliced = Array(frequencyVertices[0..<361]) 300 | if let device { 301 | freqeuencyBuffer = device.makeBuffer(bytes: sliced, length: sliced.count * MemoryLayout.stride, options: [])! 302 | } 303 | } 304 | } 305 | 306 | func tick(time: Float) { 307 | } 308 | 309 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) { 310 | if self.device !== device { 311 | self.device = device 312 | } 313 | guard 314 | let loudnessBuffer = loudnessBuffer, 315 | let frequencyBuffer = freqeuencyBuffer 316 | else { 317 | return 318 | } 319 | encoder.setFragmentBuffer(loudnessBuffer, offset: 0, index: 1) 320 | encoder.setFragmentBuffer(frequencyBuffer, offset: 0, index: 2) 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/CellularNoise.metal: -------------------------------------------------------------------------------- 1 | // 2 | // CellularNoise.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 5/16/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | //float randomized(float x) { 13 | // return fract(sin(x) * 10.0); 14 | //} 15 | // 16 | 17 | struct VertexIn { 18 | vector_float2 pos; 19 | }; 20 | 21 | struct FragmentUniforms { 22 | float time; 23 | float screen_width; 24 | float screen_height; 25 | float screen_scale; 26 | }; 27 | 28 | struct VertexOut { 29 | float4 pos [[position]]; 30 | float4 color; 31 | }; 32 | 33 | vertex VertexOut cellularVertexShader(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 34 | VertexIn in = vertexArray[vid]; 35 | VertexOut out; 36 | out.pos = float4(in.pos, 0, 1); 37 | return out; 38 | } 39 | 40 | 41 | vector_float2 random2(vector_float2 p ) { 42 | return fract(sin(vector_float2(dot(p,vector_float2(127.1,311.7)),dot(p,vector_float2(269.5,183.3))))*43758.5453); 43 | } 44 | 45 | fragment float4 tileFragmentShader(VertexOut pixel [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 46 | float2 st = {pixel.pos.x / uniforms.screen_width, 1 - pixel.pos.y / uniforms.screen_height}; 47 | st *= 5; 48 | float2 ist = floor(st); 49 | float2 fst = fract(st); 50 | 51 | float min_dist = 1; 52 | 53 | for(int i=-1; i<=1; i++) { 54 | for(int j=-1; j<=1; j++) { 55 | float2 neighbor_origin = {float(i), float(j)}; 56 | float2 neighbor_point = random2(ist + neighbor_origin); 57 | neighbor_point = 0.5 + 0.5*sin(uniforms.time + 6.2831*neighbor_point); 58 | float neighbor_point_distance = length(neighbor_origin + neighbor_point - fst); 59 | min_dist = min(min_dist, neighbor_point_distance * min_dist); 60 | } 61 | } 62 | 63 | float3 color = float3(0) + step(0.06, min_dist); 64 | if (length(color) != 0) { 65 | color = float3(0.8, 0.7, 0.68); 66 | } 67 | // color[1] = min_dist; 68 | 69 | return float4(color, 1); 70 | } 71 | 72 | fragment float4 tileFragmentShader2(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 73 | float x = interpolated.pos.x / uniforms.screen_width; 74 | float y = 1 - interpolated.pos.y / uniforms.screen_height; 75 | float2 st = {x,y}; 76 | float scale = 3; 77 | st *= scale; 78 | 79 | float2 ist = floor(st); 80 | float2 fst = fract(st); 81 | 82 | float min_dist = 100; 83 | 84 | for (int y= -1; y <= 1; y++) { 85 | for (int x= -1; x <= 1; x++) { 86 | // Neighbor place in the grid 87 | vector_float2 neighborOffset = vector_float2(float(x),float(y)); 88 | 89 | // Random position from current + neighbor place in the grid 90 | vector_float2 pointInNeighbor = random2(ist + neighborOffset); 91 | 92 | // Animate the point 93 | pointInNeighbor = 0.5 + 0.5*sin(uniforms.time + 6.2831*pointInNeighbor); 94 | 95 | // Vector between the pixel and the point 96 | vector_float2 diff = neighborOffset + pointInNeighbor - fst; 97 | 98 | // Distance to the point 99 | float dist = length(diff); 100 | 101 | // Keep the closer distance 102 | min_dist = min(min_dist, dist); 103 | } 104 | } 105 | 106 | vector_float3 color = 0; 107 | // Draw the min distance (distance field) 108 | color += min_dist; 109 | 110 | // Draw cell center 111 | color += 1.-step(.02, min_dist); 112 | 113 | // Draw grid 114 | color.r += step(.98, fst.x) + step(.98, fst.y); 115 | 116 | return float4(float3(min_dist), 1); 117 | } 118 | 119 | fragment float4 cellularFragmentShader(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 120 | unsigned int pointsCounts = int(uniforms.time) % 5 + 1; 121 | float x = interpolated.pos.x / uniforms.screen_width; 122 | float y = 1 - interpolated.pos.y / uniforms.screen_height; 123 | float2 st = { x, y }; 124 | 125 | float2 points[6]; 126 | points[0] = { 127 | 0.1, 128 | 0.4 129 | }; 130 | points[1] = { 131 | 0.3, 132 | 0.8 133 | }; 134 | points[2] = { 135 | 0.7, 136 | 0.5 137 | }; 138 | points[3] = { 139 | 0.9, 140 | 0.32 141 | }; 142 | points[4] = { 143 | 0.81, 144 | 0.72 145 | }; 146 | points[5] = { 147 | 0.51, 148 | 0.1 149 | }; 150 | 151 | float min_dist = 100; 152 | float variation_max = 0.1; 153 | for (unsigned int i = 0; i < pointsCounts; i++) { 154 | float2 center = points[i]; 155 | float variation = (0 + sin(uniforms.time)) * variation_max; 156 | if (i % 2 == 0) { 157 | center[0] += variation; 158 | } else { 159 | center[1] += variation; 160 | } 161 | 162 | float dist = distance(st, center); 163 | if (dist < 0.005) { 164 | return float4(0.3, 0.4, 0.6, 1); 165 | } 166 | min_dist = min(dist, min_dist); 167 | } 168 | 169 | if (pointsCounts > 3) { 170 | min_dist -= step(.7,abs(sin(50.0*min_dist)))*.3; 171 | } 172 | 173 | return float4(float3(min_dist), 1); 174 | } 175 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/Girih.metal: -------------------------------------------------------------------------------- 1 | // 2 | // RepeatingCircles.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 8/2/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | #include "../ShaderHeaders.h" 13 | 14 | struct VertexOut { 15 | float4 pos [[position]]; 16 | float4 color; 17 | }; 18 | 19 | struct VertexIn { 20 | vector_float2 pos; 21 | }; 22 | 23 | typedef enum { 24 | GirihPatternFirstThingsFirst = 0, 25 | GirihPatternSixesInterpolated = 1 26 | } GirihPatternKind; 27 | 28 | struct FragmentUniforms { 29 | float time; 30 | float screen_width; 31 | float screen_height; 32 | float screen_scale; 33 | float2 mousePos; 34 | }; 35 | 36 | struct GirihUniforms { 37 | GirihPatternKind kind; 38 | bool rotating; 39 | float num_rows; 40 | float num_polygons; 41 | float scale; 42 | }; 43 | 44 | vertex VertexOut girih_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 45 | VertexIn in = vertexArray[vid]; 46 | VertexOut out; 47 | out.pos = float4(in.pos, 0, 1); 48 | return out; 49 | } 50 | 51 | // --- 52 | 53 | float Band(float p, float start, float end, float blur) { 54 | float mask = smoothstep(start - blur, start + blur, p); 55 | mask *= smoothstep(end + blur, end - blur, p); 56 | return mask; 57 | } 58 | 59 | 60 | float CircleBand(float2 st, float2 pos, float r, float thickness, float blur) { 61 | float d = length(st - pos); 62 | float color = Band(d, r, r + thickness, blur); 63 | return color; 64 | } 65 | 66 | 67 | // --- 68 | 69 | float lineDistance(float2 p, float2 v, float2 w) { 70 | const float l2 = length_squared(w - v); // i.e. |w-v|^2 - avoid a sqrt 71 | if (l2 == 0.0) return distance(p, v); // v == w case 72 | 73 | // Consider the line extending the segment, parameterized as v + t (w - v). 74 | // We find projection of point p onto the line. 75 | // It falls where t = [(p-v) . (w-v)] / |w-v|^2 76 | // We clamp t from [0,1] to handle points outside the segment vw. 77 | const float t = max(0.0, min(1.0, dot(p - v, w - v) / l2)); 78 | const float2 projection = v + t * (w - v); // Projection falls on the segment 79 | return distance(p, projection); 80 | } 81 | 82 | float onLine(float2 p, float2 v, float2 w) { 83 | float dist = lineDistance(p, v, w); 84 | // return smoothstep(0.004, 0.001, dist); 85 | return 1 - step(0.005, dist); 86 | } 87 | 88 | float2 lineIntersection(float2 p1, float2 p2, float2 p3, float2 p4) { 89 | float pxNum = (p1.x*p2.y - p1.y*p2.x)*(p3.x - p4.x) - (p1.x - p2.x)*(p3.x*p4.y - p3.y*p4.x); 90 | float pyNum = (p1.x*p2.y - p1.y*p2.x)*(p3.y - p4.y) - (p1.y - p2.y)*(p3.x*p4.y - p3.y*p4.x); 91 | 92 | float den = (p1.x - p2.x)*(p3.y - p4.y) - (p1.y - p2.y)*(p3.x - p4.x); 93 | float px = pxNum / den; 94 | float py = pyNum / den; 95 | return {px, py}; 96 | } 97 | 98 | /// Intersection point between two circles of same radius r, 99 | /// and positioned at center0 and center1. top: whether we want the top or bottom intersection. 100 | /// From http://paulbourke.net/geometry/circlesphere/ 101 | float2 circlesIntersctionPoint(float r, bool at_top, float2 center0, float2 center1) { 102 | float d = distance(center0, center1); 103 | float a = d/2; 104 | float2 p2 = center0 + a * (center1 - center0) / d; 105 | float h = sqrt(pow(r, 2) - pow(a, 2)); 106 | float xPart = (h * (center1.y - center0.y) / d); 107 | float x3 = p2.x; 108 | if (at_top) { 109 | x3 -= xPart; 110 | } else { 111 | x3 += xPart; 112 | } 113 | float yPart = (h * (center1.x - center0.x) / d); 114 | float y3 = p2.y; 115 | if (at_top) { 116 | y3 += yPart; 117 | } else { 118 | y3 -= yPart; 119 | } 120 | return float2(x3, y3); 121 | } 122 | 123 | struct Mask { 124 | float circleMask; 125 | float polygonMask; 126 | // circle intersections from which we build new circles 127 | float2 intersections [6]; 128 | }; 129 | 130 | Mask girih_circles_mask(float2 st, float r, float rotating, float scaleFactor, float time) { 131 | if (rotating) { 132 | st *= rotate(sin(time/4) * M_PI_F); 133 | } 134 | 135 | // scale down a bit so we are away from the boundary 136 | st = scale(1/scaleFactor) * st; 137 | 138 | float2 centerPos = {0,0}; 139 | 140 | int totalCircles = 6; 141 | float thickness = 0.004; 142 | float blur = 0.001; 143 | 144 | Mask mask; 145 | mask.circleMask = CircleBand(st, centerPos, r, thickness, blur); // start w/ middle circle 146 | mask.polygonMask = 0; 147 | 148 | // midpoints between consecutive intersections 149 | float2 midpoints[6]; 150 | 151 | // for outer intersections 152 | float2 innerCircleCenters[6]; 153 | 154 | // Build circles and intersections, and collect midpoints 155 | for (int idx = 0; idx < totalCircles; idx++) { 156 | float2 circle2Pos = (idx == 0) ? 157 | circle2Pos = {centerPos.x + r, 0} // to the right first 158 | : 159 | mask.intersections[idx - 1]; // or the last circle 160 | 161 | float2 intersection = circlesIntersctionPoint(r, true, centerPos, circle2Pos); 162 | mask.circleMask += CircleBand(st, intersection, r, thickness, blur); 163 | 164 | innerCircleCenters[idx] = circle2Pos; 165 | 166 | // if (idx > 0) { 167 | float2 outerIntersection = circlesIntersctionPoint(r, true, circle2Pos, intersection); 168 | mask.circleMask += CircleBand(st, outerIntersection, r, thickness, blur); 169 | // } 170 | 171 | float2 line_point1 = circle2Pos; 172 | float2 line_point2 = intersection; 173 | 174 | midpoints[idx] = float2((line_point1.x+line_point2.x)/2, (line_point1.y+line_point2.y)/2); 175 | 176 | mask.intersections[idx] = intersection; 177 | } 178 | 179 | // Lines between midpoints of the inner hexagon 180 | for (int idx = 0; idx < totalCircles; idx++) { 181 | int n = totalCircles; 182 | float2 p1 = lineIntersection( 183 | midpoints[idx], midpoints[(idx + 2)%n], 184 | midpoints[(idx + 1)%n], 185 | midpoints[(idx + (n - 1))%n] 186 | ); 187 | float2 p2 = midpoints[idx]; 188 | float fallsOnLine1 = onLine(st, p1, p2); 189 | mask.polygonMask = max(mask.polygonMask, fallsOnLine1); 190 | 191 | float2 p3 = lineIntersection( 192 | midpoints[idx], midpoints[(idx + n - 2)%n], 193 | midpoints[(idx + n - 1)%n], 194 | midpoints[(idx + 1)%n] 195 | ); 196 | float2 p4 = midpoints[idx]; 197 | float fallsOnLine2 = onLine(st, p3, p4); 198 | mask.polygonMask = max(mask.polygonMask, fallsOnLine2); 199 | } 200 | 201 | mask.circleMask = min(mask.circleMask, 1.0); 202 | return mask; 203 | } 204 | 205 | float3 colorForMask(Mask mask, float2 st, float scale) { 206 | float3 basePerimeterColor = float3(0.4, 0.2, 0.42); 207 | float3 baseLineColor = float3(0.9, 0.8, 0.1); 208 | float3 color = 0; 209 | 210 | // Circle outline 211 | color += (1 - mask.polygonMask) * mask.circleMask * basePerimeterColor; 212 | // Polygon 213 | color += mask.polygonMask * baseLineColor; 214 | 215 | // Intersection circles 216 | if (scale == 1) { 217 | float3 intersectionColor = 0; 218 | for (int i=0; i<6; i++) { 219 | float dist = distance(st, mask.intersections[i]); 220 | float thickness = 0.01; 221 | dist = smoothstep(thickness, thickness - 0.001, dist); 222 | intersectionColor += dist * basePerimeterColor; 223 | } 224 | 225 | if (length(intersectionColor) > 0) { 226 | color = intersectionColor; 227 | } 228 | } 229 | 230 | color = min(color, 1.0); 231 | return color; 232 | } 233 | 234 | 235 | float3 girih_color(float2 st, int rows_count, int polygons_count, bool rotating, float scale, float time) { 236 | float r = 0.5; 237 | 238 | st *= rows_count; 239 | st = fract(st); 240 | 241 | st -= 0.5; 242 | st *= 1.01; // inset a little bit from the edges 243 | 244 | float angleBetweenPoints = M_PI_F / 3.0; // 6 in 2_PI 245 | float rotationAngle = angleBetweenPoints / polygons_count; 246 | 247 | float3 color = 0; 248 | for (int i=0; i 10 | using namespace metal; 11 | 12 | struct VertexOut { 13 | float4 pos [[position]]; 14 | float4 color; 15 | }; 16 | 17 | typedef struct { 18 | float time; 19 | float3 appResolution; 20 | } CustomUniforms; 21 | 22 | 23 | struct VertexIn { 24 | vector_float2 pos; 25 | }; 26 | 27 | struct FragmentUniforms { 28 | float time; 29 | float screen_width; 30 | float screen_height; 31 | float screen_scale; 32 | float2 mousePos; 33 | }; 34 | 35 | 36 | vertex VertexOut happy_jumping_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 37 | VertexIn in = vertexArray[vid]; 38 | VertexOut out; 39 | out.pos = float4(in.pos, 0, 1); 40 | return out; 41 | } 42 | 43 | float sdEllipsoid(float3 pos, float3 radii) { 44 | float k0 = length(pos/radii); 45 | float k1 = length(pos/radii/radii); 46 | return k0 * (k0 - 1.0) / k1; 47 | } 48 | 49 | float sdSphere(float3 sphere, float radius) { 50 | return length(sphere) - radius; 51 | } 52 | 53 | float smin(float a, float b, float k) { 54 | float h = max(k - abs(a-b), 0.0); 55 | float val = min(a, b) - h * h / (k * 4.0); 56 | return val; 57 | } 58 | 59 | 60 | float smax(float a, float b, float k) { 61 | float h = max(k - abs(a-b), 0.0); 62 | float val = max(a, b) + h * h / (k * 4.0); 63 | return val; 64 | } 65 | 66 | float2 sdGuy(float3 pos, float time, float3 center) { 67 | time = fract(time / 2.0); 68 | time = 0.5; // for modeling 69 | float y = 4.0 * (1 - time) * time; 70 | center.y = y; 71 | 72 | float sy = 0.5 + 0.5 * y; // deform in y 73 | float sz = 1.0 / sy; // inverse deform in z, so volume is preserved 74 | float3 radii = float3(0.25, 0.25 * sy, 0.25 * sz); 75 | 76 | float3 q = pos - center; 77 | 78 | // main body 79 | float d = sdEllipsoid(q, radii); 80 | 81 | float3 h = q; 82 | 83 | // head 84 | float d2 = sdEllipsoid(h - float3(0.,0.28,0.), float3(0.2)); 85 | float d3 = sdEllipsoid(h - float3(0.,0.28,-0.1), float3(0.2)); // head's back 86 | float head = smin(d3, d2, 0.03); 87 | d = smin(d, head, 0.1); 88 | 89 | float3 heye = {abs(h.x), h.y, h.z}; 90 | 91 | // eye brows 92 | float3 eyebrowCoordinates = heye - float3(0.12,0.34,0.15); 93 | // to rotate, can multiply by a pythagorean triplet; 94 | // {3, 4, 5} → gives us 40-ish angle 95 | float2x2 rotator = float2x2(3,4,-4,3); 96 | float denom = 5.0; 97 | eyebrowCoordinates.xy = ((rotator) * eyebrowCoordinates.xy) / denom; 98 | float dEyebrow = sdEllipsoid(eyebrowCoordinates, float3(0.06, 0.035, 0.05)); 99 | d = smin(d, dEyebrow, 0.04); 100 | 101 | // mouth 102 | float dMouth = sdEllipsoid(h - float3(0.0, 0.1, 0.1), float3(0.1,0.035,0.3)); 103 | // carve out the mouth using smax 104 | d =smax(d, -dMouth, 0.03); 105 | 106 | float2 result = float2(d, 2.0); // 1 is the body identifier, 2 is head 107 | 108 | // eye 109 | float eye = sdSphere(heye - float3(0.08, 0.28, 0.16), 0.05); 110 | if (eye < d) { 111 | result = float2(eye, 3.0); // 3 is the eye id 112 | } 113 | float pupil = sdSphere(heye - float3(0.09, 0.28, 0.19), 0.02); 114 | if (pupil < d) { 115 | result = float2(pupil, 4.0); // 4 is the pupil id 116 | } 117 | 118 | return result; 119 | } 120 | 121 | float2 RayMarch(float3 pos, float t) { 122 | float2 guy1 = sdGuy(pos, t, float3(0, 0, 0.0)); 123 | 124 | float planeY = -0.25; 125 | float planeD = pos.y - planeY; 126 | 127 | return (planeD < guy1.x) ? float2(planeD, 1.0) : guy1; // 1 is plane-id 128 | } 129 | 130 | float3 calcNormal(float3 pos, float t) { 131 | float2 e = float2(0.001, 0.); 132 | return normalize( 133 | float3( 134 | RayMarch(pos+e.xyy, t).x - RayMarch(pos-e.xyy, t).x, 135 | RayMarch(pos+e.yxy, t).x - RayMarch(pos-e.yxy, t).x, 136 | RayMarch(pos+e.yyx, t).x - RayMarch(pos-e.yyx, t).x 137 | ) 138 | ); 139 | } 140 | 141 | #define MAX_ITER 200 142 | #define MAX_DIST 20 143 | 144 | /// ro → position of the point (probably on some surface) that we are checking if it's in shadow 145 | /// rd → direction to light source 146 | float castShadow(float3 ro, float3 rd, float current_material) { 147 | float res = 1.0; 148 | 149 | float t = 0.001; 150 | for (int i=0; i<100; i++) { 151 | // if we hit something, we are in shadow, since we can't reach the light 152 | // we'll also mark how close we are from hitting something, even if it didn't hit any objects 153 | 154 | float3 pos = ro + t * rd; 155 | float2 closest = RayMarch(pos, t); 156 | float h = closest.x; 157 | 158 | 159 | // t → how far we have come from pos 160 | // h → the distance to the closest thing at t 161 | // divide h by → closer things will have a stronger shadow 162 | if (closest.y != current_material) { 163 | res = min(res, 16.0 * h / t); // find the closest thing, so min 164 | } 165 | 166 | h += t; 167 | 168 | if (t > MAX_DIST) { break; } 169 | } 170 | return res; 171 | } 172 | 173 | float2 castRay(float3 ro, float3 rd, float time) { 174 | float material = -1.0 ; // unknown 175 | // distance from camera in to the scene 176 | float t = 0; 177 | for (int i = 0; i < MAX_ITER; i++) { 178 | float3 pos = ro + t * rd; // current sampling position in to the scene 179 | float2 h = RayMarch(pos, time); // distance and material of nearest item in scene from pos 180 | material = h.y; 181 | if (h.x < 0.001) { // we have hit something 182 | break; 183 | } 184 | t += h.x; // march further in to scene (by the safe distance h) 185 | if (t > MAX_DIST) { 186 | break; // we have gone too far without hitting 187 | } 188 | } 189 | 190 | if (t > MAX_DIST) { 191 | material = -1.0; // if not hit, send -1.0 192 | } 193 | return float2(t, material); 194 | } 195 | 196 | fragment float4 happy_jumping_fragment( VertexOut in [[stage_in]], 197 | constant FragmentUniforms &uniforms [[buffer( 0 )]] ) 198 | { 199 | // float2 uv = in.pos.xy; 200 | float2 uv = {in.pos.x / uniforms.screen_width, in.pos.y / uniforms.screen_height}; 201 | uv = 2 * uv - 1.0; 202 | uv.y = -uv.y; 203 | 204 | float time = uniforms.time; 205 | float mouseOffset = 30.0 * uniforms.mousePos.x; 206 | // mouseOffset = 0; 207 | 208 | float3 lookAt = float3(0,0.95,0); // camera target 209 | float3 ro = lookAt + float3(1.5 * sin(mouseOffset), 0 , 1.5*cos(mouseOffset)); 210 | float3 forward = normalize(lookAt - ro); 211 | float3 right = normalize(cross(forward, float3(0,1,0))); 212 | float3 up = normalize(cross(right, forward)); 213 | float screenZPos = 1.3; 214 | float3 rd = normalize( uv.x*right + uv.y*up + screenZPos*forward); 215 | 216 | float3 sky_col = float3(.7,.75,.89); 217 | float sky_gradient = uv.y * 0.4; 218 | float3 col = sky_col - sky_gradient; 219 | 220 | float2 tAndMaterial = castRay(ro, rd, time); 221 | 222 | float t = tAndMaterial.x; 223 | float tMaterial = tAndMaterial.y; 224 | if (tMaterial > 0) { // we did hit something (-1 for not hit) 225 | /// Calculate lighting 226 | float3 pos = ro + t * rd; // position at the param t 227 | float3 normal = calcNormal(pos, time); 228 | 229 | // grass has albedo 0.2. Can use that as base color for objects. 230 | 231 | float3 material = 0.18; 232 | 233 | if (tMaterial < 1.5) { 234 | material = {0.03, 0.13, 0.01}; 235 | } else if (tMaterial < 2.5) { 236 | material = {0.1, 0.12, 0.01}; 237 | } else if (tMaterial < 3.5) { 238 | material = {0.4, 0.52, 0.41}; 239 | } else if (tMaterial < 4.5) { 240 | material = float3(0.001); 241 | } 242 | 243 | float3 sun_dir = {0.8,0.4,0.2}; 244 | float sun_dif = clamp(dot(normal, sun_dir), 0.,1.0); 245 | float3 sun_col = material * float3(7.0,5.0,2.6); 246 | float sun_sha = 247 | // step(castShadow(pos + normal * 0.001, sun_dir, tMaterial), 0); 248 | step(castRay(pos + normal * 0.001, sun_dir, time).y, 0); 249 | col = sun_col * sun_dif * sun_sha; 250 | 251 | float3 sky_col = material * float3(0.5, 0.8, 0.9); 252 | float3 sky_dir = float3(0,1.0,0.0); 253 | float sky_dif = clamp(0.5 + 0.5 * dot(normal, sky_dir), 0.,1.); 254 | col += sky_col * sky_dif; 255 | 256 | // Bounce calculates light in "up" direction. Otherwise, similar to sky. 257 | float3 bounce_dir = float3(0,-1,0); 258 | float bounce_dif = clamp(0.5 + 0.5 * dot(normal, bounce_dir), 0.,1.); 259 | float3 bounce_col = float3(0.7,0.3,0.2); 260 | col += material * bounce_col * bounce_dif; 261 | } 262 | 263 | // gamma correction 264 | col = pow(col, 0.4545); 265 | 266 | return float4(col, 1.0 ); 267 | } 268 | 269 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/PolarExperiments.metal: -------------------------------------------------------------------------------- 1 | // 2 | // PolarExperiments.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 8/4/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexOut { 13 | float4 pos [[position]]; 14 | float4 color; 15 | }; 16 | 17 | struct VertexIn { 18 | vector_float2 pos; 19 | }; 20 | 21 | 22 | struct FragmentUniforms { 23 | float time; 24 | float screen_width; 25 | float screen_height; 26 | float screen_scale; 27 | float2 mousePos; 28 | }; 29 | 30 | vertex VertexOut polar_experiments_vertex(const device VertexIn *vertices [[buffer(0)]], unsigned int vid [[vertex_id]]) { 31 | VertexOut v; 32 | v.pos = float4(vertices[vid].pos, 0, 1); 33 | return v; 34 | } 35 | 36 | // --- 37 | 38 | float remaps(float a, float b, float c, float d, float t) { 39 | float val = (t - a) / (b - a) * (d - c) + c; 40 | return clamp(val, 0.0, 1.0); 41 | } 42 | 43 | float2 polar(float2 st) { 44 | float2 polSt = float2(atan2(st.y, st.x), length(st)); 45 | return polSt; 46 | } 47 | 48 | 49 | fragment float4 polar_experiments_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 50 | float3 col = {0.7, 0.5, 0.1}; 51 | 52 | float2 st = { 53 | interpolated.pos.x / uniforms.screen_width, 54 | 1 - interpolated.pos.y/uniforms.screen_height 55 | }; 56 | st -= 0.5; 57 | st.x *= uniforms.screen_width / uniforms.screen_height; 58 | float2 polSt = polar(st); 59 | // polSt.x = remaps(-M_PI_F, M_PI_F, 0, 1, polSt.x); 60 | 61 | float c = polSt.y * abs(cos(polSt.x)*sin(polSt.x)*2); 62 | 63 | // float m = min(fract(x), fract(1-x)); 64 | // float c = smoothstep(0., .01, m*.5 + .2 - st.y); 65 | col *= c; 66 | 67 | return float4(col, 1); 68 | } 69 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/RaysConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RaysConfig.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/5/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import simd 11 | 12 | class RaysConfig: ObservableObject { 13 | @Published var cameraPosX: Float = 0 14 | @Published var cameraPosY: Float = 0 15 | @Published var cameraPosZ: Float = -30 16 | @Published var cameraRotX: Float = 0 17 | @Published var cameraRotY: Float = 0 18 | @Published var cameraRotZ: Float = 0 19 | 20 | @Published var modelPosX: Float = 0 21 | @Published var modelPosY: Float = 0 22 | @Published var modelPosZ: Float = 0 23 | @Published var modelRotX: Float = 0 24 | @Published var modelRotY: Float = 0 25 | @Published var modelRotZ: Float = 0 26 | 27 | @Published var modelScale: Float = 1 28 | 29 | 30 | func resetModelPos() { 31 | modelPosX = 0 32 | modelPosY = 0 33 | modelPosZ = 0 34 | } 35 | 36 | func resetModelRot() { 37 | modelRotX = 0 38 | modelRotY = 0 39 | modelRotZ = 0 40 | } 41 | 42 | func resetModelScale() { 43 | modelScale = 1 44 | } 45 | 46 | func resetCameraPos() { 47 | cameraPosX = 0 48 | cameraPosY = 0 49 | cameraPosZ = -30 50 | } 51 | 52 | func resetCameraRot() { 53 | cameraRotX = 0 54 | cameraRotY = 0 55 | cameraRotZ = 0 56 | } 57 | } 58 | 59 | let raysConfig = RaysConfig() 60 | 61 | struct RaysConfigView: View { 62 | @EnvironmentObject fileprivate var config: RaysConfig 63 | 64 | private var modelPos: some View { 65 | VStack { 66 | Text("Model").font(.subheadline) 67 | Spacer().frame(height: 10) 68 | HStack { 69 | Text("Position") 70 | Button(action: { 71 | raysConfig.resetModelPos() 72 | }, label: { Text("Reset") }) 73 | } 74 | VStack(spacing: 0) { 75 | numSlider($config.modelPosX, "x", extent: 40) 76 | numSlider($config.modelPosY, "y", extent: 40) 77 | numSlider($config.modelPosZ, "z", extent: 40) 78 | } 79 | } 80 | } 81 | 82 | var modelRot: some View { 83 | VStack { 84 | HStack { 85 | Text("Rotation") 86 | Button(action: { 87 | raysConfig.resetModelRot() 88 | }, label: { Text("Reset") }) 89 | } 90 | VStack(spacing: 0) { 91 | numSlider($config.modelRotX, "x", extent: 3) 92 | numSlider($config.modelRotY, "y", extent: 3) 93 | numSlider($config.modelRotZ, "z", extent: 3) 94 | } 95 | } 96 | } 97 | 98 | var modelScale: some View { 99 | VStack { 100 | HStack { 101 | Text("Scale") 102 | Button(action: { 103 | raysConfig.resetModelScale() 104 | }, label: { Text("Reset") }) 105 | } 106 | numSlider($config.modelScale, "", extent: 1.0, minExtent: 0) 107 | } 108 | } 109 | 110 | var model: some View { 111 | VStack(spacing: 10) { 112 | modelPos 113 | modelRot 114 | modelScale 115 | } 116 | } 117 | 118 | func numSlider(_ binding: Binding, _ label: String, extent: Float, minExtent: Float? = nil) -> some View { 119 | HStack { 120 | Text(label) 121 | Slider(value: binding, in: (minExtent ?? -extent)...extent) 122 | .frame(width: 120) 123 | HStack { 124 | Spacer() 125 | Text(String(format: "%.2f", binding.wrappedValue)) 126 | } 127 | } 128 | } 129 | 130 | var camera: some View { 131 | VStack { 132 | Text("Camera").font(.subheadline) 133 | Spacer().frame(height: 10) 134 | HStack { 135 | Text("Position") 136 | Button(action: { 137 | raysConfig.resetCameraPos() 138 | }, label: { Text("Reset") }) 139 | } 140 | VStack(spacing: 0) { 141 | numSlider($config.cameraPosX, "x", extent: 40) 142 | numSlider($config.cameraPosY, "y", extent: 40) 143 | numSlider($config.cameraPosZ, "z", extent: 40) 144 | } 145 | Spacer().frame(height: 10) 146 | HStack { 147 | Text("Rotation") 148 | Button(action: { 149 | raysConfig.resetCameraRot() 150 | }, label: { Text("Reset") }) 151 | } 152 | VStack(spacing: 0) { 153 | numSlider($config.cameraRotX, "x", extent: 3.14) 154 | numSlider($config.cameraRotY, "y", extent: 3.14) 155 | numSlider($config.cameraRotZ, "z", extent: 3.14) 156 | } 157 | } 158 | } 159 | 160 | var body: some View { 161 | VStack(alignment: .leading, spacing: 0) { 162 | camera 163 | model 164 | 165 | Spacer() 166 | } 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/ShaderToyDistortions.metal: -------------------------------------------------------------------------------- 1 | // 2 | // ShaderToyDistortions.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 8/2/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | 13 | struct VertexOut { 14 | float4 pos [[position]]; 15 | float4 color; 16 | }; 17 | 18 | struct VertexIn { 19 | vector_float2 pos; 20 | }; 21 | 22 | 23 | struct FragmentUniforms { 24 | float time; 25 | float screen_width; 26 | float screen_height; 27 | float screen_scale; 28 | float2 mousePos; 29 | }; 30 | 31 | vertex VertexOut domain_distortion_vertex(const device VertexIn *vertices [[buffer(0)]], unsigned int vid [[vertex_id]]) { 32 | VertexOut v; 33 | v.pos = float4(vertices[vid].pos, 0, 1); 34 | return v; 35 | } 36 | 37 | // --- 38 | 39 | float baand(float p, float start, float end, float endsBlur) { 40 | float m = smoothstep(start - endsBlur, start + endsBlur, p); 41 | m *= smoothstep(end + endsBlur, end - endsBlur, p); 42 | float2x2 ma = float2x2(0,1,1,0); 43 | float3 x = {0}; 44 | float2 mas = x.xz * ma; 45 | x.xz = mas; 46 | return m; 47 | } 48 | 49 | float rectangle(float2 st, float left, float right, float bottom, float top, float blur) { 50 | return 51 | baand(st.x, left, right, blur) 52 | * 53 | baand(st.y, bottom, top, blur) 54 | ; 55 | } 56 | 57 | float remapper(float a, float b, float c, float d, float t) { 58 | float val = (t - a) / (b - a) * (d - c) + c; 59 | return clamp(val, 0.0, 1.0); 60 | } 61 | 62 | 63 | fragment float4 domain_distortion_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 64 | float t = uniforms.time; 65 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y/uniforms.screen_height}; 66 | st -= 0.5; 67 | st.x *= uniforms.screen_width / uniforms.screen_height; 68 | float3 col = {0.7, 0.5, 0.1}; 69 | 70 | float x = st.x; 71 | float m = sin(x*10 + t*2)/10; 72 | st.y = st.y - m; 73 | 74 | float blur = remapper(-0.5, 0.5, 0, .3, st.x); 75 | blur *= blur; 76 | float val = rectangle(st, -.4, .4, -.1, .1, blur); 77 | col = col * val; 78 | return float4(col, 1); 79 | } 80 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/ShaderToySmiley.metal: -------------------------------------------------------------------------------- 1 | // 2 | // ShaderToySmiley.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/14/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexOut { 13 | float4 pos [[position]]; 14 | float4 color; 15 | }; 16 | 17 | struct VertexIn { 18 | vector_float2 pos; 19 | }; 20 | 21 | 22 | struct FragmentUniforms { 23 | float time; 24 | float screen_width; 25 | float screen_height; 26 | float screen_scale; 27 | float2 mousePos; 28 | }; 29 | 30 | float random1(float2 st) { 31 | return fract(sin(st.x * 0.122190) * 21001.); 32 | } 33 | 34 | float lerp2(float a, float b, float m, float n, float t) { 35 | return saturate(m + (t - a) / (b - a) * (n - m)); 36 | } 37 | 38 | float2 within(float2 st, float4 rect) { 39 | // Like mapping but inside a rect 40 | return (st - rect.xy)/(rect.zw - rect.xy); 41 | } 42 | 43 | float Circle(float2 st, float2 pos, float r, float blur) { 44 | st -= pos; 45 | float c = smoothstep(r, r - blur, length(st)); 46 | return c; 47 | } 48 | 49 | float4 Head(float2 st) { 50 | float d = length(st); 51 | 52 | float4 col = float4(0.9, 0.6, 0.1, 1.); 53 | 54 | // add shadow to the edge 55 | float edgeShadow = smoothstep(0.35, 0.5, d); 56 | edgeShadow *= edgeShadow; 57 | col.rgb *= 1.0 - edgeShadow * 0.2; 58 | 59 | // make everything go inside a circle of 0.5 radius 60 | col.a = smoothstep(0.5, 0.5 - 0.001, d); 61 | 62 | /// top highlight 63 | // white inner circle of r=0.41 64 | float highlight = smoothstep(0.41, 0.405, d); 65 | // circle is light white (0.75 at 0.41, vanishes at -0.1) 66 | highlight *= lerp2(0.41, -0.1, 0.75, 0., st.y); 67 | col.rgb = mix(col.rgb, float3(1), highlight); 68 | 69 | /// Cheek 70 | // on both sides 71 | st.x = abs(st.x); 72 | // new d for cheek position 73 | float2 cheekPos = {0.25, -0.2}; 74 | d = length(st - cheekPos); 75 | // the gradient 76 | float cheek = smoothstep(0.2, 0.0, d); 77 | cheek *= cheek; 78 | col.rgb = mix(col.rgb, float3(1.0,0.3,0.1), cheek); 79 | 80 | return col; 81 | } 82 | 83 | 84 | float4 Eye(float2 st) { 85 | // receives in 0 → 1 86 | st -= 0.5; 87 | 88 | float d = length(st); 89 | float4 irisCol = float4(.3,.5,1,1); 90 | // goes from white at .1 to irisCol at .7 (whole effect is then halved) 91 | float4 col = mix(float4(1), irisCol, smoothstep(.1, .7, d) * 0.5); 92 | 93 | // inner shadow next to the nose 94 | col.rgb *= 1. - smoothstep(.45, .5, d) * saturate(-st.y-st.x); 95 | 96 | col.rgb = mix(float3(0), col.rgb, smoothstep(0.3, 0.31, d)); 97 | col.rgb = mix(irisCol.rgb, col.rgb, smoothstep(0.25, 0.3, d)); 98 | col.rgb = mix(float3(0), col.rgb, smoothstep(0.189, 0.19, d)); 99 | 100 | /// small circles 101 | // positioned at .1,.1 102 | float highlight = smoothstep(0.1, 0.09, length(st - float2(0.14,0.1))); 103 | col.rgb = mix(col.rgb, float3(1), highlight); 104 | // positioned at -.1,-.1. Smaller (0.04) 105 | highlight += smoothstep(0.04, 0.03, length(st - float2(-0.1,-0.1))); 106 | col.rgb = mix(col.rgb, float3(1), highlight); 107 | 108 | // only have it go till half the length 109 | col.a = smoothstep(0.5, 0.48, d); 110 | 111 | return col; 112 | } 113 | 114 | float4 Mouth(float2 st) { 115 | st -= .5; 116 | st.y *= 1.5; 117 | st.y -= 2 * st.x * st.x; 118 | float4 col = float4(0.5, 0.18, 0.05, 1.); 119 | float d = length(st); 120 | col.a = smoothstep(0.5, 0.49, d); 121 | 122 | 123 | // teeth 124 | float td = length(st - float2(0, .6)); 125 | float3 toothCol = float3(1) * smoothstep(0.6, 0.35, d); 126 | col.rgb = mix(toothCol, col.rgb, smoothstep(0.49, 0.5, td)); 127 | 128 | return col; 129 | } 130 | 131 | float4 Smiley(float2 st) { 132 | float4 col = float4(0); 133 | 134 | // Duplicate the right to the left as well: 135 | st.x = abs(st.x); 136 | 137 | // the alpha of Eye, Head, etc. will be 0 outside their drawing, 138 | // so that doing the mix with col using the `alpha` would only draw "within" 139 | 140 | float4 eye = Eye(within(st, float4(0.03, -0.1, 0.37, 0.25))); 141 | float4 head = Head(st); 142 | float4 mouth = Mouth(within(st, float4(-.3,-.4,.3,-.1))); 143 | 144 | col = mix(col, head, head.a); 145 | col = mix(col, eye, eye.a); 146 | col = mix(col, mouth, mouth.a); 147 | 148 | return col; 149 | } 150 | 151 | vertex VertexOut shaderToySmileyVertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 152 | VertexIn in = vertexArray[vid]; 153 | VertexOut out; 154 | out.pos = float4(in.pos, 0, 1); 155 | return out; 156 | } 157 | 158 | 159 | fragment float4 shaderToySmiley(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 160 | // float t = uniforms.time; 161 | float2 st = {interpolated.pos.x / uniforms.screen_width, interpolated.pos.y / uniforms.screen_height}; 162 | st.y = 1. - st.y; // make it go from bottom to top 163 | st -= 0.5; // center 164 | 165 | float4 smiley = Smiley(st); 166 | return smiley; 167 | } 168 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/ShaderToyStarField.metal: -------------------------------------------------------------------------------- 1 | // 2 | // ShaderToyStarField.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/16/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexOut { 13 | float4 pos [[position]]; 14 | float4 color; 15 | }; 16 | 17 | struct VertexIn { 18 | vector_float2 pos; 19 | }; 20 | 21 | 22 | struct FragmentUniforms { 23 | float time; 24 | float screen_width; 25 | float screen_height; 26 | float screen_scale; 27 | float2 mousePos; 28 | }; 29 | 30 | struct StarfieldUniforms { 31 | bool rotating; 32 | bool flying; 33 | float num_layers; 34 | float num_density; 35 | }; 36 | 37 | #define S(a, b, t) smoothstep(a, b, t) 38 | 39 | float map(float a, float b, float c, float d, float t) { 40 | float val = (t - a) / (b - a) * (d - c) + c; 41 | return clamp(val, 0.0, 1.0); 42 | } 43 | 44 | /// Normalize st within the box: if st.x == 0, it will be on left side; if st.y == 0, it will be on bottom side; 45 | float2 withinRect(float2 st, float4 rect) { 46 | return (st - rect.xy) / (rect.zw - rect.xy); 47 | } 48 | 49 | float2x2 Rot(float a) { 50 | float s = sin(a), c = cos(a); 51 | return float2x2(c, -s, s, c); 52 | } 53 | 54 | float Star(float2 st, float flare) { 55 | float d = length(st); 56 | 57 | float m = 0; 58 | 59 | float star = .05/d; 60 | m += star; 61 | 62 | // 2 rays: 63 | // 1st 64 | float rays = max(0., 1. - abs(st.y * st.x * 1000)); 65 | m += rays * flare; 66 | // 2nd rotated 67 | st *= Rot(3.1415/4); 68 | rays = max(0., 1. - abs(st.y * st.x * 1000)); 69 | m += rays * .3 * flare; 70 | 71 | // don't project indefinitely outside the grid 72 | // (just in the next neighbor, but not the neighbor's neighbor) 73 | m *= smoothstep(0.9, .2, d); 74 | 75 | return m; 76 | } 77 | 78 | float Hash12(float2 p) { 79 | p = fract(p * float2(123.34, 456.21)); 80 | p += dot(p, p + 45.32); 81 | return fract(p.x * p.y); 82 | } 83 | 84 | float3 starField(float2 st) { 85 | float3 color = 0; 86 | float2 id = floor(st); 87 | float2 gv = fract(st) 88 | - 0.5 // move the coordinate-system so star is expected in the center 89 | ; 90 | 91 | for (int x=-1; x<=1; x++) { 92 | for (int y=-1; y<=1; y++) { 93 | float2 offset = float2(x, y); 94 | float contributingStarId = Hash12(id + offset); 95 | float2 gvPos = gv // place in the unit box for which we are asking for pixel value if star is centered in the grid 96 | - offset // consider the star to be in a different grid if we have an offset 97 | - (float2( 98 | contributingStarId, // add a random offset for x inside the grid 99 | fract(contributingStarId * 12) // same for y, but make it a different random 100 | ) 101 | - .5 102 | ) ; 103 | 104 | float size = fract(contributingStarId * 219.34); 105 | float flare = smoothstep(0.4, .8, size); // only for bigger stars 106 | float star = Star(gvPos, flare); 107 | float3 col = sin(float3(0.6, 0.4, 0.8) * fract(contributingStarId * 210) * 212.) * .5 + .5; 108 | color += star * size * col; 109 | } 110 | } 111 | return color; 112 | } 113 | 114 | vertex VertexOut shadertoy_starfield_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 115 | VertexIn in = vertexArray[vid]; 116 | VertexOut out; 117 | out.pos = float4(in.pos, 0, 1); 118 | return out; 119 | } 120 | 121 | 122 | fragment float4 shaderToyStarfield(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]], 123 | constant StarfieldUniforms &uniforms2 [[buffer(1)]] ) { 124 | float t = uniforms.time * 0.1; 125 | if (!uniforms2.flying) { 126 | t = 0; 127 | } 128 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_height}; 129 | st -= 0.5; 130 | 131 | if (uniforms2.rotating) { 132 | st *= Rot(t); 133 | } 134 | st *= uniforms2.num_density; // divides the screen in to a grid (rather, repeats the 135 | 136 | float numLayers = uniforms2.num_layers; 137 | 138 | float3 color = 0; 139 | for (float i = 0; i < 1; i+=1/numLayers) { 140 | float depth = fract(i + t); // increading depth, b/w 0 and 1 141 | float scale = mix(20, .5, depth); // smaller in the back 142 | float fade = depth; // don't count the color much if it's in the back (depth ~= 0) 143 | float layerOffset = 144 | floor(i + t); 145 | // i * 453.2; 146 | color += starField(st * scale + layerOffset) * fade; 147 | } 148 | 149 | // if (abs(gv.x) > 0.49 || abs(gv.y) > 0.49) color.r = 1; 150 | 151 | return float4(color, 1); 152 | } 153 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/Simplest3D.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Simplest3D.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/17/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | 13 | struct VertexOut { 14 | float4 pos [[position]]; 15 | float4 color; 16 | }; 17 | 18 | struct VertexIn { 19 | vector_float2 pos; 20 | }; 21 | 22 | 23 | struct FragmentUniforms { 24 | float time; 25 | float screen_width; 26 | float screen_height; 27 | float screen_scale; 28 | float2 mousePos; 29 | }; 30 | 31 | vertex VertexOut simplest_3d_vertex(const device VertexIn *vertices [[buffer(0)]], unsigned int vid [[vertex_id]]) { 32 | VertexOut v; 33 | v.pos = float4(vertices[vid].pos, 0, 1); 34 | return v; 35 | } 36 | 37 | float rayDist(float3 r0, float3 rd, float3 p) { 38 | if (length(rd) == 0) { 39 | return 0.2; 40 | } 41 | return length(cross(p - r0, rd)) / length(rd); 42 | } 43 | 44 | float drawPoint(float3 r0, float3 rd, float3 p) { 45 | float d = rayDist(r0, rd, p); 46 | d = smoothstep(0.06, 0.05, d); 47 | return d; 48 | } 49 | 50 | // --- 51 | fragment float4 simplest_3d_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 52 | float2 st = { 53 | interpolated.pos.x / uniforms.screen_width, 54 | 1 - interpolated.pos.y / uniforms.screen_height 55 | }; 56 | st -= 0.5; 57 | st.x *= uniforms.screen_width / uniforms.screen_height; 58 | 59 | float3 rayOrigin = {2,0,-4}; // camera pos 60 | 61 | 62 | float zoom = 1; 63 | 64 | // ray from camera to the screen point (and then in to the screen to hit an object) 65 | float3 lookAt = {0.5}; 66 | float3 forward = normalize(lookAt - rayOrigin); 67 | float3 right = cross( float3(0, 1, 0), forward); 68 | float3 up = cross(forward, right); 69 | 70 | float3 screenCenter = rayOrigin + forward * zoom; 71 | 72 | float3 screenPoint = screenCenter + st.x * right + st.y * up; 73 | float3 rayDirection = screenPoint - rayOrigin; 74 | 75 | float d = 0; 76 | for (int z=0; z<=1; z++) { 77 | for (int y=0; y<=1; y++) { 78 | for (int x=0; x<=1; x++) { 79 | float3 p = {float(x),float(y),float(z)}; 80 | d += drawPoint(rayOrigin, rayDirection, p); 81 | } 82 | } 83 | } 84 | 85 | float3 col = d; 86 | 87 | return float4(col, 1); 88 | } 89 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Explorations/Starfield.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Starfield.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/22/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import MetalKit 10 | import SwiftUI 11 | 12 | class StarField: Playground { 13 | class StarFieldConfig: ObservableObject { 14 | @Published var rotating: Bool = false 15 | @Published var flying: Bool = true 16 | @Published var numDepthLayers: Float = 1 17 | @Published var numDensityLayers: Float = 1 18 | } 19 | 20 | let starfieldConfig = StarFieldConfig() 21 | 22 | struct Uniforms { 23 | var rotating: Bool 24 | var flying: Bool 25 | var numDepthLayers: Float 26 | var numDensityLayers: Float 27 | } 28 | 29 | static var name: String { "Star Field" } 30 | var fileName: String { 31 | "Explorations/ShaderToyStarField" 32 | } 33 | var vertexFuncName: String { "shadertoy_starfield_vertex" } 34 | var fragmentFuncName: String { "shaderToyStarfield" } 35 | var starFieldUniforms: Uniforms { 36 | Uniforms(rotating: starfieldConfig.rotating, flying: starfieldConfig.flying, numDepthLayers: Float(starfieldConfig.numDepthLayers), numDensityLayers: Float(starfieldConfig.numDensityLayers)) 37 | } 38 | 39 | var fragmentUniforms: Any? { 40 | starFieldUniforms 41 | } 42 | struct ConfigView: View { 43 | @EnvironmentObject var config: StarFieldConfig 44 | 45 | var body: some View { 46 | VStack(alignment: .leading, spacing: 19) { 47 | Toggle("Rotating", isOn: $config.rotating) 48 | Toggle("Flying", isOn: $config.flying) 49 | TitledSlider(title: "Depth Layers", value: $config.numDepthLayers, in: 0...10, step: 1) { 50 | self.config.numDepthLayers = 1 51 | } 52 | TitledSlider(title: "Density Layers", value: $config.numDensityLayers, in: 1...10, step: 1) { 53 | self.config.numDensityLayers = 1 54 | } 55 | } 56 | } 57 | } 58 | 59 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) { 60 | var uniform = self.starFieldUniforms 61 | let length = MemoryLayout.size(ofValue: uniform) 62 | encoder.setFragmentBytes(&uniform, length: length, index: 1) 63 | } 64 | 65 | var view: NSView? { 66 | NSHostingView( 67 | rootView: ConfigView().environmentObject(starfieldConfig) 68 | ) 69 | } 70 | required init() {} 71 | } 72 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Helpers.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 1/9/21. 6 | // Copyright © 2021 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | 11 | using namespace metal; 12 | #include "ShaderHeaders.h" 13 | 14 | float2x2 rotate(float angle) { 15 | return float2x2(cos(angle),sin(-angle), sin(angle),cos(angle)); 16 | } 17 | 18 | float2x2 scale(float2 sc) { 19 | sc = 1/sc; 20 | return float2x2( 21 | sc.x, 0., 22 | 0., sc.y 23 | ); 24 | } 25 | 26 | float absSin(float t) { 27 | return (sin(t) + 1)/2.0; 28 | } 29 | 30 | float lerp(float x, float u, float v, float m, float n) { 31 | float prog = (x - u) / (v - u); 32 | return m + (n - m) * prog; 33 | } 34 | 35 | float lerpU(float x, float u, float v) { 36 | return lerp(x, u, v, 0.0, 1.0); 37 | } 38 | 39 | float circle(float2 st, float time, float rad, float2 center) { 40 | float variation = 41 | (1 + cos(time)) / 2.0 42 | ; 43 | float pct = distance(st, center); 44 | float dist = 1 - step(rad * variation, pct); 45 | return dist; 46 | } 47 | 48 | // MARK: SDFs 49 | 50 | float getDistanceToCylinder(float3 point) { 51 | float spheresRadius = 0.4; 52 | float3 startAPosition = float3(-0, 0.5, 1); 53 | float3 endBPosition = float3(4, 0.5, 3); 54 | float3 aToB = endBPosition - startAPosition; 55 | float3 pointToA = point - startAPosition; 56 | float t = dot(pointToA, aToB) / dot(aToB, aToB); 57 | float3 center = startAPosition + t * aToB; 58 | float x = length(point - center) - spheresRadius; 59 | float y = (abs(t - 0.5) - 0.5) * length(aToB); 60 | float exterior = length(max(float2(x, y), 0)); 61 | float interior = min(max(x, y), 0.); 62 | return exterior + interior; 63 | } 64 | 65 | float getDistanceToCapsule(float3 point) { 66 | float spheresRadius = 0.1; 67 | float3 startAPosition = float3(1, 0.5, 1); 68 | float3 endBPosition = float3(1.5, 0.5, 1); 69 | float3 aToB = endBPosition - startAPosition; 70 | float3 pointToA = point - startAPosition; 71 | float t = dot(pointToA, aToB) / dot(aToB, aToB); 72 | t = clamp(t, 0., 1.); 73 | float3 center = startAPosition + t * aToB; 74 | float d = length(point - center) - spheresRadius; 75 | return d; 76 | } 77 | 78 | float getDistanceToTorus(float3 point) { 79 | float innerRadius = 1.06; 80 | float thickness = 0.40; 81 | float x = length(point.xz) - innerRadius; 82 | float y = length(float2(x, point.y)) - thickness; 83 | return y; 84 | } 85 | 86 | float getDistanceToBox(float3 point, float size) { 87 | float3 boxSize = float3(size); 88 | float3 q = abs(point) - boxSize; 89 | return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0); 90 | } 91 | 92 | float getDistanceToPlane(float3 point) { 93 | float p = dot(point, normalize(float3(0, 1, 0))); 94 | return p; 95 | } 96 | 97 | float sdfCircle(float2 p, float r) { 98 | return length(p) - r; 99 | } 100 | 101 | float sdfLine(float2 p, float2 a, float2 b) { 102 | float2 pa = p - a; 103 | float2 ba = b - a; 104 | 105 | float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); 106 | return length(pa - ba * h); 107 | 108 | } 109 | 110 | 111 | // MARK: SDF Operations 112 | 113 | float subtractSDFs(float a, float b) { 114 | return max(-a, b); 115 | } 116 | 117 | float intersectSDFs(float a, float b) { 118 | return max(a, b); 119 | } 120 | float unionSDFs(float a, float b) { 121 | return min(a, b); 122 | } 123 | 124 | float blendSDFs(float a, float b, float t) { 125 | return mix(a, b, t); 126 | } 127 | 128 | float smoothUnionSDFs( float a, float b, float k) { 129 | float h = clamp( 0.5+0.5* (b-a)/k, 0., 1. ); 130 | return mix( b, a, h) - k*h* (1.0-h); 131 | } 132 | 133 | float softMax(float a, float b, float k) { 134 | return log(exp(k * a) + exp(k * b)) / k; 135 | } 136 | 137 | float softMin(float a, float b, float k) { 138 | k *= 1.0; // why? 139 | float r = exp2(-a/k) + exp2(-b/k); 140 | return -k * log2(r); 141 | } 142 | 143 | // MARK: Colors 144 | float3 red() { return float3(1.0, 0.0, 0.0); } 145 | float3 green() { return float3(0.0, 1.0, 0.0); } 146 | float3 blue() { return float3(0.4, 0.4, 1.0); } 147 | float3 yellow() { return float3(1.0, 1.0, 0.0); } 148 | float3 black() { return float3(0.0, 0.0, 0.0); } 149 | float3 gray() { return float3(0.8, 0.8, 0.8); } 150 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/JumpingBalls/JumpingBalls.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | #include "../ShaderHeaders.h" 4 | 5 | struct VertexIn { 6 | vector_float2 pos; 7 | }; 8 | 9 | struct FragmentUniforms { 10 | float time; 11 | float screen_width; 12 | float screen_height; 13 | float screen_scale; 14 | float2 mousePos; 15 | }; 16 | 17 | struct VertexOut { 18 | float4 pos [[position]]; 19 | float4 color; 20 | }; 21 | 22 | vertex VertexOut jumpingVertexShader(const device VertexIn *vertices [[buffer(0)]], unsigned int vid [[vertex_id]]) { 23 | VertexOut in; 24 | in.pos = {vertices[vid].pos.x, vertices[vid].pos.y, 0, 1}; 25 | return in; 26 | } 27 | 28 | float fmodal(float a, float b) { 29 | if(a<0.0) { 30 | return b - modf(abs(a), b); 31 | } 32 | return modf(a, b); 33 | } 34 | 35 | // MARK: - Ray Marching 36 | 37 | float sdfSphere(float3 p, float r) { 38 | return length(p) - r; 39 | } 40 | 41 | float sdPlane(float3 p) { 42 | return p.y - (-1.3); 43 | } 44 | 45 | float2 scene(float3 position, float time) { 46 | // sphere 47 | float3 spherePosition = position - float3(0, 0.5, 1.0); 48 | float distance = sdfSphere(spherePosition, 0.8); 49 | float materialID = 1.0; 50 | 51 | // Other objects 52 | float3 objPosition = position - float3(0, 0.5, 1); // starting point 53 | objPosition.x = fract(objPosition.x + 0.5) - 0.5; 54 | objPosition.z = fmod(objPosition.z + 1.0, 2.0) - 1.0; 55 | objPosition.y += sin(position.x + time) * 0.45; 56 | objPosition.y += cos(position.z + time * 3) * 0.45; 57 | float distanceObjs = sdfSphere(objPosition, 0.2 * fract(objPosition.z)); 58 | if (distanceObjs < distance) { 59 | distance = distanceObjs; 60 | materialID = 2.0; 61 | } 62 | 63 | float distancePlane = sdPlane(position); 64 | if (distancePlane < distance) { 65 | distance = distancePlane; 66 | materialID = 3.0; 67 | } 68 | 69 | return float2(distance, materialID); 70 | } 71 | 72 | const constant float NEAR_CLIPPING_PLANE = 0.21; 73 | const constant float FAR_CLIPPING_PLANE = 20.01; 74 | const constant float MAX_MARCH_STEPS = 200; 75 | const constant float EPSILON = 0.01; 76 | const constant float DISTANCE_BIAS = 0.7; 77 | 78 | float2 rayMarchTo(float3 position, float3 direction, float time) { 79 | float total_distance = NEAR_CLIPPING_PLANE; 80 | float2 result; 81 | for(int i = 0; i < MAX_MARCH_STEPS; i++) { 82 | // result.x is the distance and result.y is the material 83 | result = scene(position + direction * total_distance, time); 84 | if (result.x < EPSILON) { break; } 85 | total_distance += result.x * DISTANCE_BIAS; 86 | if (total_distance > FAR_CLIPPING_PLANE) { break; } 87 | } 88 | 89 | return float2(total_distance, result.y); 90 | } 91 | 92 | float3 getNormal(float3 rayHitPosition, float smoothness, float time) { 93 | float3 n; 94 | float2 dn = float2(smoothness, 0); 95 | // here .x from the scene is the distance 96 | n.x = scene(rayHitPosition + dn.xyy, time).x - scene(rayHitPosition - dn.xyy, time).x; 97 | n.y = scene(rayHitPosition + dn.yxy, time).x - scene(rayHitPosition - dn.yxy, time).x; 98 | n.z = scene(rayHitPosition + dn.yyx, time).x - scene(rayHitPosition - dn.yyx, time).x; 99 | return normalize(n); 100 | } 101 | 102 | // MARK: - Fragment Shader 103 | 104 | fragment float4 jumpingFragmentShader( 105 | VertexOut interpolated [[stage_in]], 106 | constant FragmentUniforms &uniforms [[buffer(0)]] 107 | ) { 108 | float2 uv = 2 * (float2( 109 | interpolated.pos.x / uniforms.screen_width, 110 | 1 - interpolated.pos.y / uniforms.screen_height 111 | ) - 0.5); 112 | uv.x *= uniforms.screen_width / uniforms.screen_height; 113 | 114 | float time = uniforms.time; 115 | 116 | // -- Main image generation 117 | 118 | float3 direction = normalize(float3(uv.x, uv.y, 2.5)); 119 | float3 cameraOrigin = float3(0, 0, -6.5); 120 | float2 result = rayMarchTo(cameraOrigin, direction, time); 121 | 122 | // TODO: use fog 123 | float fog = 1.0 - result.x / FAR_CLIPPING_PLANE; 124 | 125 | float3 materialColor = float3(0.0, 0.0, 0); 126 | if (result.y == 1.0) { 127 | // sphere 128 | materialColor = float3(1.0, 0.25, 1); 129 | } else if (result.y == 2.0) { 130 | // sphere 131 | materialColor = float3(1.0, 0.75, 0.2); 132 | } else if (result.y == 3.0) { 133 | // sphere 134 | materialColor = float3(0.0, 0.15, 0.1); 135 | } 136 | 137 | // Lighting 138 | float3 intersection = cameraOrigin + direction * result.x; 139 | float3 nrml = getNormal(intersection, 0.01, time); 140 | float3 lightDir = normalize(float3(0, 1, 0.0)); 141 | float diffuse = dot(lightDir, nrml); 142 | diffuse = diffuse * 0.5 + 0.5; 143 | // Combine ambient and diffuse 144 | float3 lightColor = float3(1.0, 1.2, 0.7); 145 | float3 ambientColor = float3(0.2, 0.45, 0.6); 146 | float3 diffuseLit = materialColor * (diffuse * lightColor + ambientColor); 147 | 148 | float3 color = diffuseLit * fog; 149 | 150 | // -- Image generation DONE 151 | 152 | return float4(color, 1.0); 153 | } 154 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/JumpingBalls/JumpingBalls.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveCode.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 1/9/21. 6 | // Copyright © 2021 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import MetalKit 10 | import SwiftUI 11 | import Accelerate 12 | import AVFoundation 13 | 14 | 15 | extension MTLDevice { 16 | func makeBuffer(_ values: [Float]) -> MTLBuffer? { 17 | makeBuffer(bytes: values, length: MemoryLayout.size * values.count) 18 | } 19 | } 20 | 21 | public extension MTLRenderCommandEncoder { 22 | func setFragmentBytes(_ value: T, index: Int) { 23 | var copy = value 24 | setFragmentBytes(©, length: MemoryLayout.size, index: index) 25 | } 26 | 27 | func setFragmentBytes(_ value: T, index: Int32) { 28 | var copy = value 29 | setFragmentBytes(©, length: MemoryLayout.size, index: Int(index)) 30 | } 31 | } 32 | 33 | extension Color { 34 | var components: SIMD4 { 35 | 36 | var r: CGFloat = 0 37 | var g: CGFloat = 0 38 | var b: CGFloat = 0 39 | var a: CGFloat = 0 40 | 41 | #if canImport(UIKit) 42 | UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) 43 | #elseif canImport(AppKit) 44 | NSColor(self).usingColorSpace(.deviceRGB)!.getRed(&r, green: &g, blue: &b, alpha: &a) 45 | #endif 46 | 47 | return .init(Float(r), Float(g), Float(b), Float(a)) 48 | } 49 | } 50 | 51 | final class JumpingBalls: Playground { 52 | static let name = "Jumping Balls" 53 | var fileName: String { 54 | "JumpingBalls/JumpingBalls" 55 | } 56 | 57 | let vertexFuncName = "jumpingVertexShader" 58 | let fragmentFuncName = "jumpingFragmentShader" 59 | 60 | init() {} 61 | 62 | func tearDown() { } 63 | 64 | private var device: MTLDevice? 65 | private var pixelFormat: MTLPixelFormat? 66 | 67 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) { } 68 | } 69 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/MetalHeaders.h: -------------------------------------------------------------------------------- 1 | // 2 | // MetalHeaders.h 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/11/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | 10 | #ifndef METAL_CONSTANTS 11 | #define METAL_CONSTANTS 12 | 13 | struct VertexIn { 14 | vector_float2 pos; 15 | }; 16 | 17 | struct FragmentUniforms { 18 | float time; 19 | float screen_width; 20 | float screen_height; 21 | float screen_scale; 22 | float2 mousePos; 23 | }; 24 | 25 | struct VertexOut { 26 | float4 pos [[position]]; 27 | float4 color; 28 | }; 29 | 30 | enum class CircleKind { single, multiple }; 31 | 32 | float3 blurRect(float2 st, float time, float4 margins); 33 | 34 | float3 rect(float2 st, float time, float4 margins, float3 mainColor); 35 | 36 | float3 outlineRect(float2 st, float4 margins, float time); 37 | 38 | float circle(float2 st, float time, float rad, float2 center); 39 | 40 | float remap(float a, float b, float c, float d, float t); 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/Playground.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scene.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 7/26/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import MetalKit 10 | import SwiftUI 11 | 12 | protocol Playground { 13 | typealias Built = (MTLRenderPipelineState, MTLBuffer) -> Void 14 | 15 | /// Filename for the Shader 16 | var fileName: String { get } 17 | var vertexFuncName: String { get } 18 | var fragmentFuncName: String { get } 19 | 20 | // For mostly fragment-based 21 | var liveReloads: Bool { get } 22 | var ready: Bool { get } 23 | 24 | init() 25 | 26 | var view: NSView? { get } 27 | func tick(time: Float) 28 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) 29 | func buildPipeline(device: MTLDevice, pixelFormat: MTLPixelFormat, built: @escaping Built) 30 | func draw(encoder: MTLRenderCommandEncoder) 31 | var isPaused: Bool { get } 32 | } 33 | 34 | extension Playground { 35 | var filePath: String { #filePath } 36 | func tick(time: Float) {} 37 | var isPaused: Bool { false } 38 | var liveReloads: Bool { true } 39 | var basicVertices: [Vertex] { 40 | [ 41 | Vertex(position: [-1, -1]), 42 | Vertex(position: [-1, 1]), 43 | Vertex(position: [1, 1]), 44 | 45 | Vertex(position: [-1, -1]), 46 | Vertex(position: [1, 1]), 47 | Vertex(position: [1, -1]), 48 | ] 49 | } 50 | 51 | var ready: Bool { true } 52 | 53 | func buildPipeline(device: MTLDevice, pixelFormat: MTLPixelFormat, built: @escaping Built) { 54 | let descriptor = buildBasicPipelineDescriptor(device: device, pixelFormat: pixelFormat) 55 | let pipeline = (try? device.makeRenderPipelineState(descriptor: descriptor))! 56 | 57 | let vertexBuffer = device.makeBuffer( 58 | bytes: basicVertices, length: MemoryLayout.stride * basicVertices.count, 59 | options: []) 60 | built(pipeline, vertexBuffer!) 61 | } 62 | 63 | func buildBasicPipelineDescriptor(device: MTLDevice, pixelFormat: MTLPixelFormat) 64 | -> MTLRenderPipelineDescriptor 65 | { 66 | let pipelineDesc = MTLRenderPipelineDescriptor() 67 | let library = device.makeDefaultLibrary() 68 | pipelineDesc.vertexFunction = library?.makeFunction(name: vertexFuncName) 69 | pipelineDesc.fragmentFunction = library?.makeFunction(name: fragmentFuncName) 70 | pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat 71 | return pipelineDesc 72 | } 73 | 74 | /// In most scenes where fragment shaders do the rendering themselves, basicVertices can be used for full screen Clip space coordinates. 75 | /// Hence this default implementation 76 | func draw(encoder: MTLRenderCommandEncoder) { 77 | encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: basicVertices.count) 78 | } 79 | 80 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) {} 81 | 82 | var view: NSView? { 83 | nil 84 | } 85 | } 86 | 87 | enum PlaygroundGroup: String, CaseIterable, Identifiable { 88 | case bookOfShaders = "Book of Shaders" 89 | case artOfCode = "The Art of Code" 90 | case explorations = "Explorations" 91 | case simonDev = "Simon's Shaders Course" 92 | 93 | var id: String { rawValue } 94 | var scenes: [SceneKind] { 95 | switch self { 96 | case .bookOfShaders: 97 | return [ 98 | .bookOfShaders07Shapes, 99 | .bookOfShaders05Shaping, 100 | .leftRightTiler, 101 | .futuristicUI, 102 | .domainDisortion, 103 | .bookOfShaders06Colors, 104 | ] 105 | case .artOfCode: 106 | return [ 107 | .smiley, .starfield, .simplest3D, .polarScene, 108 | ] 109 | case .explorations: 110 | return [ 111 | .audioVisualizer, .happyJumping, .girihPattern, .jumpingBalls, 112 | .cellularNoise 113 | ] 114 | case .simonDev: 115 | return [.simonNoiseIntro, .simonDevFractAndFriends, .simonDevSDFs, .simonDevCloudyDays] 116 | } 117 | } 118 | } 119 | 120 | enum SceneKind: Int, CaseIterable, Identifiable { 121 | case jumpingBalls 122 | case leftRightTiler 123 | case futuristicUI 124 | case domainDisortion 125 | case bookOfShaders05Shaping 126 | case bookOfShaders06Colors 127 | case bookOfShaders07Shapes 128 | 129 | case happyJumping 130 | case smiley 131 | case girihPattern 132 | case starfield 133 | case simplest3D 134 | case polarScene 135 | case audioVisualizer 136 | case cellularNoise 137 | 138 | case simonDevFractAndFriends 139 | case simonDevSDFs 140 | case simonDevCloudyDays 141 | case simonNoiseIntro 142 | 143 | var id: Int { 144 | rawValue 145 | } 146 | 147 | var name: String { 148 | switch self { 149 | case .jumpingBalls: 150 | return "Jumping Balls" 151 | case .audioVisualizer: 152 | return "Audio Visualizer" 153 | case .bookOfShaders05Shaping: 154 | return "Book Of Shaders - 05 - Shaping" 155 | case .bookOfShaders07Shapes: 156 | return "Book Of Shaders - 07 - Shapes" 157 | case .leftRightTiler: 158 | return "Book of Shaders - Left/Right Tiler" 159 | case .smiley: 160 | return "Smiley" 161 | case .girihPattern: 162 | return "Girih" 163 | case .happyJumping: 164 | return "Happy Jumping" 165 | case .starfield: 166 | return "Starfield" 167 | case .simplest3D: 168 | return "Simplest 3D" 169 | case .domainDisortion: 170 | return "Domain Distortion" 171 | case .polarScene: 172 | return "Polar" 173 | case .futuristicUI: 174 | return "Futuristic UI" 175 | case .cellularNoise: 176 | return "Cellular Noise" 177 | case .bookOfShaders06Colors: 178 | return "Colors Mixing" 179 | case .simonDevFractAndFriends: 180 | return "Simon Dev - Fract and Friends" 181 | case .simonDevSDFs: 182 | return "Simon Dev - SDFs" 183 | case .simonDevCloudyDays: 184 | return "Simon Dev - Cloudy Days" 185 | case .simonNoiseIntro: 186 | return "Simon Dev - Noise Intro" 187 | } 188 | } 189 | 190 | var scene: Playground { 191 | switch self { 192 | case .audioVisualizer: return AudioVizScene() 193 | case .jumpingBalls: return JumpingBalls() 194 | case .leftRightTiler: return BoSLeftRightTiler() 195 | case .bookOfShaders05Shaping: return BoSShaping() 196 | case .bookOfShaders06Colors: return BoSColors06() 197 | case .bookOfShaders07Shapes: return BoSShapes07() 198 | case .futuristicUI: return FuturisticUI() 199 | case .happyJumping: return HappyJumping() 200 | case .girihPattern: return Girih() 201 | case .starfield: return StarField() 202 | case .smiley: return Smiley() 203 | case .simplest3D: return Simplest3D() 204 | case .polarScene: return PolarScene() 205 | case .domainDisortion: return DomainDistortion() 206 | case .cellularNoise: return CellularNoise() 207 | case .simonDevFractAndFriends: return SimonFractAndFriends() 208 | case .simonDevSDFs: return SimonSDFs() 209 | case .simonDevCloudyDays: return SimonCloudyDay() 210 | case .simonNoiseIntro: return SimonNoiseIntro() 211 | } 212 | } 213 | } 214 | 215 | extension MDLVertexDescriptor { 216 | var vertexAttributes: [MDLVertexAttribute] { 217 | attributes as! [MDLVertexAttribute] 218 | } 219 | 220 | var bufferLayouts: [MDLVertexBufferLayout] { 221 | layouts as! [MDLVertexBufferLayout] 222 | } 223 | 224 | static var `default`: MDLVertexDescriptor = { 225 | let vd = MDLVertexDescriptor() 226 | // position 227 | vd.vertexAttributes[0].name = MDLVertexAttributePosition 228 | vd.vertexAttributes[0].format = .float3 229 | vd.vertexAttributes[0].offset = 0 230 | vd.vertexAttributes[0].bufferIndex = 0 231 | var nextOffset = MemoryLayout.size 232 | 233 | // normal 234 | // vd.vertexAttributes[1].name = MDLVertexAttributeNormal 235 | // vd.vertexAttributes[1].format = .float3 236 | // vd.vertexAttributes[1].offset = nextOffset 237 | // vd.vertexAttributes[1].bufferIndex = 0 238 | // nextOffset += MemoryLayout.size * 3 239 | vd.bufferLayouts[0].stride = nextOffset 240 | 241 | return vd 242 | }() 243 | } 244 | 245 | // MARK: Simon's Shaders Course 246 | 247 | class SimonFractAndFriends: Playground { 248 | var fileName: String { 249 | "SimonShaders/FractAndFriends" 250 | } 251 | var vertexFuncName: String { "fract_and_friends_vertex" } 252 | var fragmentFuncName: String { "fract_and_friends_fragment" } 253 | var texture: MTLTexture? 254 | 255 | required init() {} 256 | func setUniforms(device: any MTLDevice, encoder: any MTLRenderCommandEncoder) { 257 | if texture == nil { 258 | let imageURL = Bundle.main.url(forResource: "wallpaper.png", withExtension: nil)! 259 | let loader = MTKTextureLoader(device: device) 260 | self.texture = try! loader.newTexture(URL: imageURL) 261 | } 262 | if let texture { 263 | encoder.setFragmentTexture(texture, index: 10) 264 | } 265 | } 266 | static func makeTexture( 267 | size: CGSize, 268 | pixelFormat: MTLPixelFormat, 269 | label: String, 270 | device: any MTLDevice, 271 | storageMode: MTLStorageMode = .private, 272 | usage: MTLTextureUsage = [.shaderRead, .renderTarget] 273 | ) -> MTLTexture? { 274 | let width = Int(size.width) 275 | let height = Int(size.height) 276 | 277 | let textureDesc = MTLTextureDescriptor.texture2DDescriptor( 278 | pixelFormat: pixelFormat, 279 | width: width, 280 | height: height, 281 | mipmapped: false 282 | ) 283 | textureDesc.usage = usage 284 | let texture = device.makeTexture(descriptor: textureDesc) 285 | texture?.label = label 286 | return texture 287 | } 288 | } 289 | 290 | class SimonNoiseIntro: Playground { 291 | var fileName: String { 292 | "SimonShaders/SimonNoiseIntro" 293 | } 294 | var vertexFuncName: String { "simon_noise_intro_vertex" } 295 | var fragmentFuncName: String { "simon_noise_intro_fragment" } 296 | var texture: MTLTexture? 297 | 298 | required init() {} 299 | } 300 | class SimonCloudyDay: Playground { 301 | var fileName: String { 302 | "SimonShaders/SimonCloudyDay" 303 | } 304 | var vertexFuncName: String { "simon_cloudy_day_vertex" } 305 | var fragmentFuncName: String { "simon_cloudy_day_fragment" } 306 | var texture: MTLTexture? 307 | 308 | required init() {} 309 | } 310 | 311 | class SimonSDFs: Playground { 312 | var fileName: String { 313 | "SimonShaders/SimonSDFs" 314 | } 315 | var vertexFuncName: String { "simon_sdfs_vertex" } 316 | var fragmentFuncName: String { "simon_sdfs_fragment" } 317 | var texture: MTLTexture? 318 | 319 | required init() {} 320 | } 321 | 322 | // MARK: Others 323 | 324 | class HappyJumping: Playground { 325 | var fileName: String { 326 | "Explorations/HappyJumping" 327 | } 328 | var vertexFuncName: String { "happy_jumping_vertex" } 329 | var fragmentFuncName: String { "happy_jumping_fragment" } 330 | required init() {} 331 | } 332 | 333 | class Simplest3D: Playground { 334 | var fileName: String { 335 | "Explorations/Simplest3D" 336 | } 337 | var vertexFuncName: String { "simplest_3d_vertex" } 338 | var fragmentFuncName: String { "simplest_3d_fragment" } 339 | required init() {} 340 | } 341 | 342 | class Smiley: Playground { 343 | 344 | var fileName: String { 345 | "Explorations/ShaderToySmiley" 346 | } 347 | var vertexFuncName: String { "shaderToySmileyVertex" } 348 | var fragmentFuncName: String { "shaderToySmiley" } 349 | required init() {} 350 | } 351 | 352 | class CellularNoise: Playground { 353 | var fileName: String { 354 | "Explorations/CellularNoise" 355 | } 356 | 357 | var vertexFuncName: String { "cellularVertexShader" } 358 | var fragmentFuncName: String { "tileFragmentShader2" } 359 | required init() {} 360 | } 361 | 362 | class FuturisticUI: Playground { 363 | var fileName: String { 364 | "Explorations/07FuturisticUI" 365 | } 366 | 367 | var vertexFuncName: String { "futuristic_UI_vertex" } 368 | var fragmentFuncName: String { "futuristic_UI_fragment" } 369 | required init() {} 370 | } 371 | 372 | class BoSLeftRightTiler: Playground { 373 | var fileName: String { 374 | "BookShaders/08LeftRightTiler" 375 | } 376 | var vertexFuncName: String { "leftright_vertex" } 377 | var fragmentFuncName: String { "leftright_fragment" } 378 | required init() {} 379 | } 380 | 381 | class BoSColors06: Playground { 382 | var fileName: String { 383 | "BookShaders/06Colors" 384 | } 385 | var vertexFuncName: String { "bos_colors_vertex" } 386 | var fragmentFuncName: String { "bos_colors_fragment" } 387 | required init() {} 388 | 389 | enum SketchKind: Int, CaseIterable, Identifiable { 390 | case bezierCurve 391 | case flowingCurves 392 | 393 | var id: Int { rawValue } 394 | 395 | var name: String { 396 | switch self { 397 | case .bezierCurve: 398 | return "Bezier Curve" 399 | case .flowingCurves: 400 | return "Flowing Curves" 401 | } 402 | } 403 | } 404 | 405 | class Config: ObservableObject { 406 | @Published var kind: SketchKind = .bezierCurve 407 | } 408 | 409 | struct Uniforms { 410 | let kind: Float 411 | } 412 | 413 | fileprivate var config: Config = .init() 414 | var fragmentUniforms: Uniforms { 415 | .init(kind: Float(config.kind.rawValue)) 416 | } 417 | 418 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) { 419 | var uniforms = fragmentUniforms 420 | let length = MemoryLayout.stride(ofValue: uniforms) 421 | encoder.setFragmentBytes(&uniforms, length: length, index: 1) 422 | } 423 | 424 | struct ConfigView: View { 425 | @EnvironmentObject private var config: Config 426 | @State fileprivate var kind = SketchKind.bezierCurve 427 | 428 | var body: some View { 429 | VStack(alignment: .leading, spacing: 19) { 430 | Picker(selection: $config.kind, label: Text("Kind")) { 431 | ForEach(SketchKind.allCases) { 432 | Text($0.name).tag($0) 433 | } 434 | } 435 | } 436 | } 437 | } 438 | 439 | var view: NSView? { 440 | NSHostingView( 441 | rootView: ConfigView().environmentObject(config) 442 | ) 443 | } 444 | } 445 | 446 | class BoSShaping: Playground { 447 | var fileName: String { 448 | "BookShaders/Shaping" 449 | } 450 | var vertexFuncName: String { "bos_shaping_vertex" } 451 | var fragmentFuncName: String { "bos_shaping_fragment" } 452 | required init() {} 453 | 454 | enum SketchKind: Int, CaseIterable, Identifiable { 455 | case bezierCurve 456 | case flowingCurves 457 | 458 | var id: Int { rawValue } 459 | 460 | var name: String { 461 | switch self { 462 | case .bezierCurve: 463 | return "Bezier Curve" 464 | case .flowingCurves: 465 | return "Flowing Curves" 466 | } 467 | } 468 | } 469 | 470 | class Config: ObservableObject { 471 | @Published var kind: SketchKind = .bezierCurve 472 | } 473 | 474 | struct Uniforms { 475 | let kind: Float 476 | } 477 | 478 | fileprivate var config: Config = .init() 479 | var fragmentUniforms: Uniforms { 480 | .init(kind: Float(config.kind.rawValue)) 481 | } 482 | 483 | func setUniforms(device: MTLDevice, encoder: MTLRenderCommandEncoder) { 484 | var uniforms = fragmentUniforms 485 | let length = MemoryLayout.stride(ofValue: uniforms) 486 | encoder.setFragmentBytes(&uniforms, length: length, index: 1) 487 | } 488 | 489 | struct ConfigView: View { 490 | @EnvironmentObject private var config: Config 491 | @State fileprivate var kind = SketchKind.bezierCurve 492 | 493 | var body: some View { 494 | VStack(alignment: .leading, spacing: 19) { 495 | Picker(selection: $config.kind, label: Text("Kind")) { 496 | ForEach(SketchKind.allCases) { 497 | Text($0.name).tag($0) 498 | } 499 | } 500 | } 501 | } 502 | } 503 | 504 | var view: NSView? { 505 | NSHostingView( 506 | rootView: ConfigView().environmentObject(config) 507 | ) 508 | } 509 | } 510 | 511 | class PolarScene: Playground { 512 | var fileName: String { 513 | 514 | "Explorations/PolarExperiments" 515 | } 516 | var vertexFuncName: String { "polar_experiments_vertex" } 517 | var fragmentFuncName: String { "polar_experiments_fragment" } 518 | required init() {} 519 | } 520 | 521 | class DomainDistortion: Playground { 522 | var fileName: String { 523 | "Explorations/ShaderToyDistortions" 524 | } 525 | 526 | var vertexFuncName: String { "domain_distortion_vertex" } 527 | var fragmentFuncName: String { "domain_distortion_fragment" } 528 | required init() {} 529 | } 530 | 531 | class BookOfShaders05: Playground { 532 | var fileName: String { 533 | "Explorations/05Algorithmic" 534 | } 535 | 536 | var vertexFuncName: String { "smoothing_vertex" } 537 | var fragmentFuncName: String { "smoothing_fragment" } 538 | required init() {} 539 | } 540 | 541 | class BookOfShaders06: Playground { 542 | var fileName: String { 543 | "06Colors" 544 | } 545 | 546 | var vertexFuncName: String { "color_vertex" } 547 | var fragmentFuncName: String { "color_fragment" } 548 | required init() {} 549 | } 550 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/ShaderHeaders.h: -------------------------------------------------------------------------------- 1 | // 2 | // ShaderHeaders.h 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 1/9/21. 6 | // Copyright © 2021 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #ifndef MyDefs 10 | #define MyDefs 11 | 12 | float absSin(float t); 13 | float lerp(float x, float m, float n, float a, float b); 14 | float2x2 rotate(float angle); 15 | float2x2 scale(float2 _scale); 16 | float circle(float2 st, float time, float rad, float2 center); 17 | float getDistanceToCylinder(float3 point); 18 | float getDistanceToCapsule(float3 point); 19 | float getDistanceToTorus(float3 point); 20 | float getDistanceToBox(float3 point); 21 | float getDistanceToPlane(float3 point); 22 | float subtractSDFs(float a, float b); 23 | float sdfCircle(float2 p, float r); 24 | float sdfLine(float2 p, float2 a, float2 b); 25 | float intersectSDFs(float a, float b); 26 | float unionSDFs(float a, float b); 27 | float blendSDFs(float a, float b, float t); 28 | float smoothUnionSDFs( float a, float b, float k); 29 | float softMax(float a, float b, float k); 30 | float softMin(float a, float b, float k); 31 | 32 | float3 red(); 33 | float3 green(); 34 | float3 yellow(); 35 | float3 black(); 36 | float3 gray(); 37 | float3 blue(); 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/SimonShaders/FractAndFriends.metal: -------------------------------------------------------------------------------- 1 | // 2 | // FractAndFriends.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 12/6/24. 6 | // Copyright © 2024 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | #include "../ShaderHeaders.h" 12 | 13 | struct VertexIn { 14 | vector_float2 pos; 15 | }; 16 | 17 | struct FragmentUniforms { 18 | float time; 19 | float screen_width; 20 | float screen_height; 21 | float screen_scale; 22 | float2 mousePos; 23 | }; 24 | 25 | struct VertexOut { 26 | float4 pos [[position]]; 27 | float4 color; 28 | }; 29 | 30 | vertex VertexOut fract_and_friends_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 31 | VertexIn in = vertexArray[vid]; 32 | VertexOut out; 33 | out.pos = float4(in.pos, 0, 1); 34 | return out; 35 | } 36 | 37 | fragment float4 fract_and_friends_fragment(VertexOut interpolated [[stage_in]], texture2d baseColorTexture [[texture(10)]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 38 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_width}; 39 | 40 | constexpr sampler textureSampler(filter::nearest, mip_filter::linear, max_anisotropy(8), address::repeat); 41 | float2 uv = 1 - st; 42 | float3 baseColor = baseColorTexture.sample(textureSampler, uv).rgb; 43 | 44 | // Background 45 | float3 color = baseColor; 46 | 47 | float2 pst = (st + uniforms.time * .012) * 400; 48 | float val = lerp(sin(pst.y), -1,1, 0.2, 1); 49 | color = mix(blue(), green(), val) * color; 50 | 51 | return vector_float4(color, 1.0); 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/SimonShaders/SimonCloudyDay.metal: -------------------------------------------------------------------------------- 1 | // 2 | // SimonCloudyDay.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 12/16/24. 6 | // Copyright © 2024 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | #include "../ShaderHeaders.h" 12 | 13 | 14 | struct VertexIn { 15 | vector_float2 pos; 16 | }; 17 | 18 | struct FragmentUniforms { 19 | float time; 20 | float screen_width; 21 | float screen_height; 22 | float screen_scale; 23 | float2 mousePos; 24 | }; 25 | 26 | struct VertexOut { 27 | float4 pos [[position]]; 28 | float4 color; 29 | }; 30 | 31 | vertex VertexOut simon_cloudy_day_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 32 | VertexIn in = vertexArray[vid]; 33 | VertexOut out; 34 | out.pos = float4(in.pos, 0, 1); 35 | return out; 36 | } 37 | 38 | float hash(float2 v) { 39 | return sin(dot(v, float2(3))); 40 | } 41 | 42 | // -- Fragment 43 | 44 | float dayLength() { 45 | return 20; 46 | } 47 | 48 | float dayTime(float time) { 49 | return fmod(time, dayLength()); 50 | } 51 | 52 | float3 backgroundColor(float2 st, float time) { 53 | float3 morning = mix( 54 | float3(0.44, 0.65, 0.81), 55 | float3(0.36, 0.53, 0.91), 56 | smoothstep(0, 1, pow(st.x * st.y, 0.7)) 57 | ); 58 | 59 | float3 midday = mix( 60 | float3(0.71, 0.75, 0.81), 61 | float3(0.56, 0.53, 0.81), 62 | smoothstep(0, 1, pow(st.x * st.y, 0.7)) 63 | ); 64 | float3 evening = mix( 65 | float3(0.61, 0.55, 0.25), 66 | float3(0.66, 0.33, 0.31), 67 | smoothstep(0, 1, pow(st.x * st.y, 0.7)) 68 | ); 69 | float3 night = mix( 70 | float3(0.11, 0.35, 0.41), 71 | float3(0.05, 0.04, 0.07), 72 | smoothstep(0, 1, pow(st.x * st.y, 0.7)) 73 | ); 74 | float3 color; 75 | float dt = dayTime(time); 76 | float dl = dayLength(); 77 | if (dt < dl * 0.25) { 78 | color = mix(morning, midday, smoothstep(0, dl * 0.25, dt)); 79 | } else if (dt < dl * 0.5) { 80 | color = mix(midday, evening, smoothstep(dl * 0.25, dl * 0.50, dt)); 81 | } else if (dt < dl * 0.75) { 82 | color = mix(evening, night, smoothstep(dl * 0.5, dl * 0.75, dt)); 83 | } else { 84 | color = mix(night, morning, smoothstep(dl * 0.75, dl, dt)); 85 | } 86 | 87 | return color; 88 | } 89 | 90 | float sdfCloud(float2 st) { 91 | float puff = sdfCircle(st, 0.1); 92 | float puffLeft = sdfCircle(st - float2(-0.1, 0), 0.07); 93 | float puffRight = sdfCircle(st + float2(-0.1, 0), 0.07); 94 | return unionSDFs(puff, unionSDFs(puffLeft, puffRight)); 95 | } 96 | 97 | fragment float4 simon_cloudy_day_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 98 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_height}; 99 | st = st - 0.5; 100 | 101 | // Background 102 | float3 color = backgroundColor(st, uniforms.time); 103 | 104 | // Sun 105 | float _dayTime = dayTime(uniforms.time); 106 | float timePassed; 107 | if (_dayTime > 0.75 * dayLength()) { 108 | timePassed = lerp(_dayTime, 0, dayLength() * 0.4, 0.0, 0.99); 109 | } else { 110 | timePassed = lerp(_dayTime, dayLength() * 0.2, dayLength() * 0.4, 0.9, 0.0); 111 | } 112 | float2 sunPos = st - float2(-0.3, 0.3 * timePassed); 113 | float sun = sdfCircle(sunPos, 0.1); 114 | color = mix(yellow() * 0.92, color, smoothstep(0.0, 0.001, sun)); 115 | 116 | // additive blending: 117 | float s = max(0.01, sun); 118 | float p = saturate(exp(-200.0 * s * s)); 119 | color += 0.45 * mix(float(0.0),float3(0.8, 0.85, 0.37), p); 120 | 121 | float numClouds = 6; 122 | for(float i = numClouds; i >= 0; i--) { 123 | // float size = (i + 4.4)/4; 124 | float size = mix(2, 1, (i/numClouds) + 0.1 * hash(float2(i))); 125 | float2 stCloud = st * float(size) + float2(i * 0.4 + uniforms.time/12, 0); 126 | stCloud.x = fract(stCloud.x + 1) - 0.5; 127 | stCloud.y -= -0.1 + hash(float2(i) * 0.08); 128 | // cloud shadow 129 | float cloudOffset = 0.04; 130 | float cloudShadow = sdfCloud(stCloud * float2(2.2) - float2(-cloudOffset, -cloudOffset/2)); 131 | // cloud 132 | float cloud = sdfCloud(stCloud); 133 | 134 | color = mix(float3(0.3), color, smoothstep(0, 0.22, cloudShadow)); 135 | color = mix(float3(1), color, smoothstep(0, 0.001, cloud)); 136 | } 137 | 138 | 139 | return vector_float4(color, 1.0); 140 | } 141 | 142 | 143 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/SimonShaders/SimonNoiseIntro.metal: -------------------------------------------------------------------------------- 1 | // 2 | // SimonCloudyDay.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 12/16/24. 6 | // Copyright © 2024 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | #include "../ShaderHeaders.h" 12 | 13 | 14 | struct VertexIn { 15 | vector_float2 pos; 16 | }; 17 | 18 | struct FragmentUniforms { 19 | float time; 20 | float screen_width; 21 | float screen_height; 22 | float screen_scale; 23 | float2 mousePos; 24 | }; 25 | 26 | struct VertexOut { 27 | float4 pos [[position]]; 28 | float4 color; 29 | }; 30 | 31 | vertex VertexOut simon_noise_intro_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 32 | VertexIn in = vertexArray[vid]; 33 | VertexOut out; 34 | out.pos = float4(in.pos, 0, 1); 35 | return out; 36 | } 37 | 38 | // -- Fragment 39 | 40 | float3 hash( float3 p ) // replace this by something better 41 | { 42 | p = float3( dot(p,float3(127.1,311.7, 74.7)), 43 | dot(p,float3(269.5,183.3,246.1)), 44 | dot(p,float3(113.5,271.9,124.6))); 45 | 46 | return -1.0 + 2.0*fract(sin(p)*43758.5453123); 47 | } 48 | 49 | float noise(float3 p ) 50 | { 51 | float3 i = floor( p ); 52 | float3 f = fract( p ); 53 | 54 | float3 u = f*f*(3.0-2.0*f); 55 | 56 | return mix( mix( mix( dot( hash( i + float3(0.0,0.0,0.0) ), f - float3(0.0,0.0,0.0) ), 57 | dot( hash( i + float3(1.0,0.0,0.0) ), f - float3(1.0,0.0,0.0) ), u.x), 58 | mix( dot( hash( i + float3(0.0,1.0,0.0) ), f - float3(0.0,1.0,0.0) ), 59 | dot( hash( i + float3(1.0,1.0,0.0) ), f - float3(1.0,1.0,0.0) ), u.x), u.y), 60 | mix( mix( dot( hash( i + float3(0.0,0.0,1.0) ), f - float3(0.0,0.0,1.0) ), 61 | dot( hash( i + float3(1.0,0.0,1.0) ), f - float3(1.0,0.0,1.0) ), u.x), 62 | mix( dot( hash( i + float3(0.0,1.0,1.0) ), f - float3(0.0,1.0,1.0) ), 63 | dot( hash( i + float3(1.0,1.0,1.0) ), f - float3(1.0,1.0,1.0) ), u.x), u.y), u.z ); 64 | } 65 | 66 | float fbm(float3 st, int octaves, float persistence, float lacunarity) { 67 | float amplitude = 0.5; 68 | float frequency = 1.0; 69 | float total = 0.0; 70 | float normalization = 0.0; 71 | 72 | for(int i = 0; i < octaves; i++) { 73 | float noiseValue = noise(st * frequency); 74 | total += noiseValue * amplitude; 75 | normalization += amplitude; 76 | amplitude *= persistence; 77 | frequency *= lacunarity; 78 | } 79 | 80 | total /= normalization; 81 | return total; 82 | } 83 | 84 | float ridgedFbm(float3 st, int octaves, float persistence, float lacunarity) { 85 | float amplitude = 0.5; 86 | float frequency = 1.0; 87 | float total = 0.0; 88 | float normalization = 0.0; 89 | 90 | for(int i = 0; i < octaves; i++) { 91 | float noiseValue = noise(st * frequency); 92 | noiseValue = 1.0 - abs(noiseValue); 93 | 94 | total += noiseValue * amplitude; 95 | normalization += amplitude; 96 | amplitude *= persistence; 97 | frequency *= lacunarity; 98 | } 99 | 100 | total /= normalization; 101 | total *= total; 102 | return total; 103 | } 104 | 105 | float turbulenceFbm(float3 st, int octaves, float persistence, float lacunarity) { 106 | float amplitude = 0.5; 107 | float frequency = 1.0; 108 | float total = 0.0; 109 | float normalization = 0.0; 110 | 111 | for(int i = 0; i < octaves; i++) { 112 | float noiseValue = noise(st * frequency); 113 | noiseValue = abs(noiseValue); 114 | 115 | total += noiseValue * amplitude; 116 | normalization += amplitude; 117 | amplitude *= persistence; 118 | frequency *= lacunarity; 119 | } 120 | 121 | total /= normalization; 122 | total *= total; 123 | return total; 124 | } 125 | 126 | float cellularNoise(float3 coords) { 127 | float2 gridBasePos = floor(coords.xy); 128 | float2 gridCoordOffset = fract(coords.xy); 129 | float closest = 1.0; 130 | for(float y = -2.0; y <= 2.0; y += 1) { 131 | for(float x = -2.0; x <= 2.0; x += 1) { 132 | float2 neighboringCellPos = float2(x,y); 133 | float2 cellWorldPos = gridBasePos + neighboringCellPos; 134 | float2 cellOffset = float2( 135 | noise(float3(cellWorldPos, coords.z) + float3(243.432, 324.235, 0.0)), 136 | noise(float3(cellWorldPos, coords.z)) 137 | ); 138 | float distanceToNeighbor = length(neighboringCellPos + cellOffset - gridCoordOffset); 139 | closest = min(closest, distanceToNeighbor); 140 | } 141 | } 142 | return closest; 143 | } 144 | 145 | 146 | /// Useful for creating wood or marble 147 | float stepped(float noiseSample) { 148 | float steppedSample = floor(noiseSample * 10.0) / 10.0; 149 | float remainder = fract(noiseSample * 10); 150 | // darken darks, lighten lights; creates a halo like effect 151 | steppedSample = (steppedSample - remainder); 152 | return steppedSample; 153 | } 154 | 155 | float domainWarping(float3 coords) { 156 | // Use the first sample to get the next 157 | 158 | float3 slightOffset = float3(43.21, 32.122, 0.0); 159 | float3 offset = float3( 160 | fbm(coords, 4, 0.5, 2.0), 161 | fbm(coords + slightOffset, 4, 0.5, 2), 162 | 0.0 163 | ); 164 | // Do we need this? 165 | float noiseSample = fbm(coords + offset, 1, 0.5, 2); 166 | 167 | // second offset includes the 1st offset 168 | float3 slightOffset2 = float3(23.21, 12.122, 0.0); 169 | float3 offset2 = float3( 170 | fbm(coords + offset * slightOffset2, 4, 0.5, 2.0), 171 | fbm(coords + offset + slightOffset*1.1, 4, 0.5, 2), 172 | 0.0 173 | ); 174 | noiseSample = fbm(coords + 4.0 * offset2, 1, 0.5, 2); 175 | 176 | return noiseSample; 177 | 178 | } 179 | 180 | fragment float4 simon_noise_intro_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 181 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_height}; 182 | 183 | float3 noiseCoords = float3(st * 2, uniforms.time * 0.1); 184 | 185 | float noiseSample = 0.0; 186 | 187 | // noiseSample = lerp(noise(noiseCoords), -1, 1, 0, 1); 188 | // noiseSample = lerp(ridgedFbm(noiseCoords, 10, 0.5, 2.0), -1, 1, 0, 1); 189 | // noiseSample = lerp(turbulenceFbm(noiseCoords, 10, 0.5, 2.0), -0.5, 1, 0, 1); 190 | // noiseSample = cellularNoise(noiseCoords); 191 | 192 | noiseSample = lerp(domainWarping(noiseCoords), -1,1, 0,1); 193 | 194 | // can reuse with above: 195 | noiseSample = stepped(noiseSample); 196 | 197 | 198 | float3 color = float3(noiseSample); 199 | 200 | 201 | return vector_float4(color, 1.0); 202 | } 203 | 204 | 205 | -------------------------------------------------------------------------------- /MetalPlayground/Scenes/SimonShaders/SimonSDFs.metal: -------------------------------------------------------------------------------- 1 | // 2 | // FractAndFriends.metal 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 12/6/24. 6 | // Copyright © 2024 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | #include "../ShaderHeaders.h" 12 | 13 | struct VertexIn { 14 | vector_float2 pos; 15 | }; 16 | 17 | struct FragmentUniforms { 18 | float time; 19 | float screen_width; 20 | float screen_height; 21 | float screen_scale; 22 | float2 mousePos; 23 | }; 24 | 25 | struct VertexOut { 26 | float4 pos [[position]]; 27 | float4 color; 28 | }; 29 | 30 | vertex VertexOut simon_sdfs_vertex(const device VertexIn *vertexArray [[buffer(0)]], unsigned int vid [[vertex_id]]) { 31 | VertexIn in = vertexArray[vid]; 32 | VertexOut out; 33 | out.pos = float4(in.pos, 0, 1); 34 | return out; 35 | } 36 | 37 | // --- 38 | 39 | float3 skyBackgroundColor(float2 st) { 40 | float distanceFromCenter = length(st); 41 | float vignette = 1.0 - distanceFromCenter; 42 | vignette = smoothstep(0, 0.7, vignette); 43 | vignette = lerp(vignette, 0, 1, 0.3, 1); 44 | return float3(vignette); 45 | } 46 | 47 | float3 grid(float2 st, float aspect, float3 color, float3 backgroundColor, float cellSpacing, float lineWidth) { 48 | float2 cells = abs(fract(st * cellSpacing) - 0.5); 49 | 50 | float distanceToCenter = (0.5 - max(cells.x, cells.y)); 51 | float t = smoothstep(0.00, lineWidth, distanceToCenter); 52 | 53 | 54 | color = mix(color, backgroundColor, t); 55 | 56 | return color; 57 | } 58 | 59 | float sdBox(float2 p, float2 b ) 60 | { 61 | float2 d = abs(p)-b; 62 | return length(max(d,0.0)) + min(max(d.x,d.y),0.0); 63 | } 64 | 65 | fragment float4 simon_sdfs_fragment(VertexOut interpolated [[stage_in]], constant FragmentUniforms &uniforms [[buffer(0)]]) { 66 | float2 st = {interpolated.pos.x / uniforms.screen_width, 1 - interpolated.pos.y / uniforms.screen_height}; 67 | st = st - 0.5; 68 | 69 | // Background 70 | float3 color = skyBackgroundColor(st); 71 | 72 | // Grid 73 | float aspect = uniforms.screen_width / uniforms.screen_height; 74 | color = grid(st, aspect, gray(), color, 100, 0.08); 75 | color = grid(st, aspect, black(), color, 20, 0.04); 76 | 77 | // Circles 78 | float radius = 1.0/9; 79 | // smooth the edges (antialias) with smoothstep; useful for curves 80 | float offset = 0.3; 81 | float circle1 = sdfCircle(st - float2(-offset, offset), radius); 82 | float circle2 = sdfCircle(st - float2(offset, offset), radius); 83 | float circle3 = sdfCircle(st - float2(0, -offset), radius); 84 | 85 | float circles = unionSDFs(unionSDFs(circle1, circle2), circle3); 86 | 87 | // Box 88 | float box = sdBox(rotate(uniforms.time) * st, float2(0.28, 0.28)); 89 | 90 | float colorK = lerp(circles - box, -1, 1, 0, 1); 91 | float3 shapeCol = mix(blue(), red(), smoothstep(0, 1, colorK)); 92 | 93 | // Final shape 94 | float shape = softMin(circles, box, 0.02); 95 | 96 | color = mix(shapeCol * 0.2, color, smoothstep(0.0, 0.003, shape)); 97 | color = mix(shapeCol, color, smoothstep(0, 0.001, shape)); 98 | 99 | 100 | // float line = sdfLine(rotate(uniforms.time) * st, float2(0.5, 0.0), float2(-0.5, 0.0)); 101 | // line = step(0.01, line); 102 | // 103 | // 104 | // color = mix(yellow(), color, line); 105 | // 106 | 107 | 108 | return vector_float4(color, 1.0); 109 | } 110 | 111 | 112 | -------------------------------------------------------------------------------- /MetalPlayground/UI/TitledSlider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitledSlider.swift 3 | // MetalPlayground 4 | // 5 | // Created by Raheel Ahmad on 9/20/20. 6 | // Copyright © 2020 Raheel Ahmad. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct TitledSlider: View { 12 | let title: String 13 | var value: Binding 14 | let slider: AnyView 15 | let reset: (() -> ()) 16 | 17 | static let nf: NumberFormatter = { 18 | let f = NumberFormatter() 19 | f.maximumFractionDigits = 1 20 | return f 21 | }() 22 | 23 | static func boundLabel(value: Float) -> some View { 24 | Text(nf.string(from: NSNumber(floatLiteral: Double(value)))!) 25 | .font(.caption).foregroundColor(.secondary) 26 | } 27 | 28 | static func valueLabel(value: Float) -> some View { 29 | Text(nf.string(from: NSNumber(floatLiteral: Double(value)))!) 30 | .bold() 31 | } 32 | 33 | init(title: String, value: Binding, in bounds: ClosedRange, step: Float? = nil, reset: @escaping (() -> ())) { 34 | if let step = step { 35 | self.slider = AnyView( 36 | Slider( 37 | value: value, 38 | in: bounds, 39 | step: step, 40 | minimumValueLabel: Self.boundLabel(value: Float(bounds.lowerBound)), 41 | maximumValueLabel: Self.boundLabel(value: Float(bounds.upperBound)), 42 | label: { EmptyView() } 43 | ) 44 | ) 45 | } else { 46 | self.slider = AnyView( 47 | Slider( 48 | value: value, 49 | in: bounds, 50 | minimumValueLabel: Self.boundLabel(value: Float(bounds.lowerBound)), 51 | maximumValueLabel: Self.boundLabel(value: Float(bounds.upperBound)), 52 | label: { EmptyView() } 53 | ) 54 | ) 55 | } 56 | self.title = title 57 | self.reset = reset 58 | self.value = value 59 | } 60 | 61 | public var body: some View { 62 | VStack(alignment: .leading, spacing: 0) { 63 | HStack(spacing: 12) { 64 | Text(title) 65 | Spacer() 66 | Self.valueLabel(value: value.wrappedValue) 67 | Button(action: { 68 | self.reset() 69 | }) { 70 | Image("reset") 71 | .resizable() 72 | .foregroundColor(Color(red: 185/255.0, green: 160/255.0, blue: 176/255.0)) 73 | .aspectRatio(contentMode: .fit) 74 | .frame(width: 19) 75 | } 76 | .buttonStyle(PlainButtonStyle()) 77 | } 78 | slider 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /MetalPlayground/raga.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/MetalPlayground/raga.mp3 -------------------------------------------------------------------------------- /MetalPlayground/wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/MetalPlayground/wallpaper.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metal Playground 2 | 3 | A macOS app to explore Metal, particularly Shaders. 4 | 5 | Allows sending vertex and fragment uniforms over to the shaders. Uses SwiftUI for uniform tweaks. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /images/circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/images/circles.png -------------------------------------------------------------------------------- /images/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/images/colors.png -------------------------------------------------------------------------------- /images/futuristic-UI.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/images/futuristic-UI.gif -------------------------------------------------------------------------------- /images/futuristic-UI.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/images/futuristic-UI.mov -------------------------------------------------------------------------------- /images/leftright-tiler.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/images/leftright-tiler.gif -------------------------------------------------------------------------------- /images/leftright-tiler.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/images/leftright-tiler.mov -------------------------------------------------------------------------------- /images/starfield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raheelahmad/metal-playground/131322ce5d01e1b448128dc2ba2827182860d1b4/images/starfield.png --------------------------------------------------------------------------------