Skip to content

Instantly share code, notes, and snippets.

@uvolchyk
Last active December 30, 2024 12:42
Show Gist options
  • Save uvolchyk/8248c7b17632a345054c2ff47e192203 to your computer and use it in GitHub Desktop.
Save uvolchyk/8248c7b17632a345054c2ff47e192203 to your computer and use it in GitHub Desktop.
#include <metal_stdlib>
using namespace metal;
struct VertexOut {
float4 position [[position]];
float progress;
float4 color;
};
vertex VertexOut vertexShader(
uint vertexID [[vertex_id]],
constant float* vertices [[buffer(0)]]
) {
VertexOut out;
out.position = float4(
vertices[vertexID * 9],
vertices[vertexID * 9 + 1],
vertices[vertexID * 9 + 2],
vertices[vertexID * 9 + 3]
);
out.progress = vertices[vertexID * 9 + 4];
out.color = float4(
vertices[vertexID * 9 + 5],
vertices[vertexID * 9 + 6],
vertices[vertexID * 9 + 7],
vertices[vertexID * 9 + 8]
);
return out;
}
float rand(float2 n) {
return fract(sin(dot(n, n)) * length(n));
}
float noise(float2 n) {
const float2 d = float2(0.0, 1.0);
float2 b = floor(n);
float2 f = smoothstep(float2(0.0), float2(1.0), fract(n));
return mix(
mix(rand(b), rand(b + d.yx), f.x),
mix(rand(b + d.xy), rand(b + d.yy), f.x),
f.y
);
}
fragment float4 fragmentShader(
VertexOut in [[stage_in]],
constant float &visibilityThreshold [[buffer(1)]]
) {
float2 uv = in.position.xy;
float _delayedAge = visibilityThreshold - in.progress;
float _noise = noise(uv * 0.1);
float _alpha = step(_delayedAge, _noise);
return float4(in.color.rgb, _alpha);
}
import MetalKit
final class DissolveRenderer: NSObject {
private let device: MTLDevice
private let commandQueue: MTLCommandQueue
private let pipelineState: MTLRenderPipelineState
private var vertexBuffer: MTLBuffer!
private var vertexCount = 0
private var visibilityThreshold: Float = 0.0
private var timer: Float = 0.0
var duration: Float = 2.0
init?(mtkView: MTKView) {
guard
let device = MTLCreateSystemDefaultDevice(),
let queue = device.makeCommandQueue(),
let library = device.makeDefaultLibrary(),
let vertexFunc = library.makeFunction(name: "vertexShader"),
let fragmentFunc = library.makeFunction(name: "fragmentShader")
else { return nil }
self.device = device
mtkView.device = device
commandQueue = queue
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunc
pipelineDescriptor.fragmentFunction = fragmentFunc
pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
do {
pipelineState = try device.makeRenderPipelineState(
descriptor: pipelineDescriptor
)
} catch {
return nil
}
super.init()
mtkView.delegate = self
// position.x, position.y, position.z, position.w, progress, color RGBA
let vertexData: [Float] = [
// triangle 1
-1.0, 1.0, 0.0, 1.0, 0.0, 0.9176470588, 0.2235294118, 0.3921568627, 1,
1.0, 1.0, 0.0, 1.0, 0.0, 0.1058823529, 0.5019607843, 0.9490196078, 1,
-1.0, -1.0, 0.0, 1.0, 1.0, 0.4117647059, 0.8352941176, 0.8745098039, 1,
// triangle 2
-1.0, -1.0, 0.0, 1.0, 1.0, 0.4117647059, 0.8352941176, 0.8745098039, 1,
1.0, 1.0, 0.0, 1.0, 0.0, 0.1058823529, 0.5019607843, 0.9490196078, 1,
1.0, -1.0, 0.0, 1.0, 1.0, 0.3882352941, 0.3882352941, 0.8431372549, 1,
]
vertexCount = vertexData.count / 9
vertexBuffer = device.makeBuffer(
bytes: vertexData,
length: MemoryLayout<Float>.stride * vertexData.count
)
}
}
extension DissolveRenderer: MTKViewDelegate {
func draw(in view: MTKView) {
guard
let drawable = view.currentDrawable,
let commandBuffer = commandQueue.makeCommandBuffer(),
let descriptor = view.currentRenderPassDescriptor,
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
else { return }
timer += 1.0 / (Float(view.preferredFramesPerSecond) * duration)
visibilityThreshold = Float(timer).truncatingRemainder(dividingBy: 2.0)
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(
vertexBuffer,
offset: 0,
index: 0
)
renderEncoder.setFragmentBytes(
&visibilityThreshold,
length: MemoryLayout<Float>.stride,
index: 1
)
renderEncoder.drawPrimitives(
type: .triangleStrip,
vertexStart: 0,
vertexCount: vertexCount
)
renderEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
func mtkView(
_ view: MTKView,
drawableSizeWillChange size: CGSize
) {}
}
import SwiftUI
final class DissolveViewController: UIViewController {
private lazy var metalView = MTKView()
private var renderer: DissolveRenderer!
override func loadView() {
view = metalView
}
override func viewDidLoad() {
super.viewDidLoad()
renderer = DissolveRenderer(mtkView: metalView)
}
}
struct DissolveView: UIViewControllerRepresentable {
func makeUIViewController(
context: Context
) -> some UIViewController {
DissolveViewController()
}
func updateUIViewController(
_ uiViewController: UIViewControllerType,
context: Context
) {}
}
@uvolchyk
Copy link
Author

ScreenRecording_12-24-2024.01-47-49_1.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment