diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 4eb8613..b1ff510 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; }; EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; }; + EC5D40812C21CE270067F090 /* PhotoBackgroundRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */; }; EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */; }; EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; @@ -127,6 +128,7 @@ EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = ""; }; EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = ""; }; EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoBackgroundRenderPass.swift; sourceTree = ""; }; EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingAverage.swift; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; @@ -624,6 +626,7 @@ ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */, ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */, ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */, + EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */, ); path = RenderPasses; sourceTree = ""; @@ -843,6 +846,7 @@ files = ( ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */, ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */, + EC5D40812C21CE270067F090 /* PhotoBackgroundRenderPass.swift in Sources */, ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */, diff --git a/Memola/Canvas/Core/PipelineStates.swift b/Memola/Canvas/Core/PipelineStates.swift index 7d68936..01af9dd 100644 --- a/Memola/Canvas/Core/PipelineStates.swift +++ b/Memola/Canvas/Core/PipelineStates.swift @@ -77,7 +77,7 @@ struct PipelineStates { let library = renderer.library let pipelineDescriptor = MTLRenderPipelineDescriptor() pipelineDescriptor.vertexFunction = library.makeFunction(name: "vertex_stroke") - pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke") + pipelineDescriptor.fragmentFunction = library.makeFunction(name: "fragment_stroke_eraser") pipelineDescriptor.colorAttachments[0].pixelFormat = pixelFormat ?? renderer.pixelFormat pipelineDescriptor.label = "Eraser Pipeline State" diff --git a/Memola/Canvas/Core/Renderer.swift b/Memola/Canvas/Core/Renderer.swift index d8cd644..6086dc7 100644 --- a/Memola/Canvas/Core/Renderer.swift +++ b/Memola/Canvas/Core/Renderer.swift @@ -37,6 +37,9 @@ final class Renderer { lazy var viewPortRenderPass: ViewPortRenderPass = { ViewPortRenderPass(renderer: self) }() + lazy var photoBackgroundRenderPass: PhotoBackgroundRenderPass = { + PhotoBackgroundRenderPass(renderer: self) + }() init(canvasView: MTKView) { guard let device = MTLCreateSystemDefaultDevice() else { @@ -59,6 +62,7 @@ final class Renderer { func resize(on view: MTKView, to size: CGSize) { if !updatesViewPort { + photoBackgroundRenderPass.resize(on: view, to: size, with: self) strokeRenderPass.resize(on: view, to: size, with: self) graphicRenderPass.resize(on: view, to: size, with: self) cacheRenderPass.resize(on: view, to: size, with: self) @@ -69,9 +73,11 @@ final class Renderer { func draw(in view: MTKView, on canvas: Canvas) { if !updatesViewPort { + strokeRenderPass.eraserRenderPass = eraserRenderPass graphicRenderPass.photoRenderPass = photoRenderPass graphicRenderPass.strokeRenderPass = strokeRenderPass graphicRenderPass.eraserRenderPass = eraserRenderPass + graphicRenderPass.photoBackgroundRenderPass = photoBackgroundRenderPass graphicRenderPass.draw(on: canvas, with: self) } @@ -84,6 +90,7 @@ final class Renderer { cacheRenderPass.draw(on: canvas, with: self) viewPortRenderPass.descriptor = view.currentRenderPassDescriptor + viewPortRenderPass.photoBackgroundTexture = photoBackgroundRenderPass.photoBackgroundTexture viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture viewPortRenderPass.draw(on: canvas, with: self) } diff --git a/Memola/Canvas/Core/Textures.swift b/Memola/Canvas/Core/Textures.swift index 24a9268..838ab4a 100644 --- a/Memola/Canvas/Core/Textures.swift +++ b/Memola/Canvas/Core/Textures.swift @@ -116,4 +116,27 @@ class Textures { texture.label = "Stroke Texture" return texture } + + static func createPhotoBackgroundTexture( + from renderer: Renderer, + size: CGSize, + pixelFormat: MTLPixelFormat? = nil + ) -> MTLTexture? { + let width = Int(size.width) + let height = Int(size.height) + guard width > 0, height > 0 else { return nil } + let descriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: pixelFormat ?? renderer.pixelFormat, + width: width, + height: height, + mipmapped: false + ) + descriptor.storageMode = .shared + descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite] + guard let texture = renderer.device.makeTexture(descriptor: descriptor) else { + return nil + } + texture.label = "Photo Background Texture" + return texture + } } diff --git a/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift b/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift index f00857c..d6682d0 100644 --- a/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift +++ b/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift @@ -83,13 +83,17 @@ extension Stroke { extension Stroke { func prepare(device: MTLDevice) { guard texture == nil else { return } - texture = penStyle.loadTexture(on: device) + if penStyle.textureName != nil { + texture = penStyle.loadTexture(on: device) + } } func draw(device: MTLDevice, renderEncoder: MTLRenderCommandEncoder) { guard !isEmpty, let indexBuffer else { return } prepare(device: device) - renderEncoder.setFragmentTexture(texture, index: 0) + if penStyle.textureName != nil { + renderEncoder.setFragmentTexture(texture, index: 0) + } renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) renderEncoder.drawIndexedPrimitives( type: .triangle, diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index a21ec42..544ab96 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -18,6 +18,7 @@ class GraphicRenderPass: RenderPass { weak var photoRenderPass: PhotoRenderPass? weak var strokeRenderPass: StrokeRenderPass? weak var eraserRenderPass: EraserRenderPass? + weak var photoBackgroundRenderPass: PhotoBackgroundRenderPass? var clearsTexture: Bool = true @@ -33,7 +34,7 @@ class GraphicRenderPass: RenderPass { } func draw(on canvas: Canvas, with renderer: Renderer) { - guard let strokeRenderPass, let eraserRenderPass, let photoRenderPass else { return } + guard let strokeRenderPass, let eraserRenderPass, let photoRenderPass, let photoBackgroundRenderPass else { return } guard let descriptor else { return } guard let graphicPipelineState else { return } @@ -55,7 +56,6 @@ class GraphicRenderPass: RenderPass { let stroke = _stroke.value guard stroke.isVisible(in: canvas.bounds) else { continue } descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - clearsTexture = false switch stroke.style { case .eraser: eraserRenderPass.stroke = stroke @@ -67,20 +67,19 @@ class GraphicRenderPass: RenderPass { strokeRenderPass.graphicDescriptor = descriptor strokeRenderPass.graphicPipelineState = graphicPipelineState strokeRenderPass.draw(on: canvas, with: renderer) - - if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads { - descriptor.colorAttachments[0].loadAction = .load - eraserRenderPass.stroke = stroke - eraserRenderPass.descriptor = descriptor - eraserRenderPass.draw(on: canvas, with: renderer) - } } + clearsTexture = false case .photo(let photo): descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - clearsTexture = false photoRenderPass.photo = photo photoRenderPass.descriptor = descriptor photoRenderPass.draw(on: canvas, with: renderer) + + photoBackgroundRenderPass.photo = photo + photoBackgroundRenderPass.clearsTexture = clearsTexture + photoBackgroundRenderPass.draw(on: canvas, with: renderer) + + clearsTexture = false } } renderer.redrawsGraphicRender = false @@ -88,7 +87,6 @@ class GraphicRenderPass: RenderPass { if let element = graphicContext.previousElement { descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - clearsTexture = false switch element { case .stroke(let anyStroke): let stroke = anyStroke.value @@ -103,19 +101,17 @@ class GraphicRenderPass: RenderPass { strokeRenderPass.graphicDescriptor = descriptor strokeRenderPass.graphicPipelineState = graphicPipelineState strokeRenderPass.draw(on: canvas, with: renderer) - - if let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads { - descriptor.colorAttachments[0].loadAction = .load - eraserRenderPass.stroke = stroke - eraserRenderPass.descriptor = descriptor - eraserRenderPass.draw(on: canvas, with: renderer) - } } case .photo(let photo): photoRenderPass.photo = photo photoRenderPass.descriptor = descriptor photoRenderPass.draw(on: canvas, with: renderer) + + photoBackgroundRenderPass.photo = photo + photoBackgroundRenderPass.clearsTexture = clearsTexture + photoBackgroundRenderPass.draw(on: canvas, with: renderer) } + clearsTexture = false graphicContext.previousElement = nil } diff --git a/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift new file mode 100644 index 0000000..33a5a1b --- /dev/null +++ b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift @@ -0,0 +1,57 @@ +// +// PhotoBackgroundRenderPass.swift +// Memola +// +// Created by Dscyre Scotti on 6/18/24. +// + +import MetalKit +import Foundation + +class PhotoBackgroundRenderPass: RenderPass { + var label: String = "Photo Background Render Pass" + + var descriptor: MTLRenderPassDescriptor? + + var photoBackgroundPipelineState: MTLRenderPipelineState? + + var photoBackgroundTexture: MTLTexture? + + var photo: Photo? + + var clearsTexture: Bool = true + + init(renderer: Renderer) { + descriptor = MTLRenderPassDescriptor() + photoBackgroundPipelineState = PipelineStates.createPhotoPipelineState(from: renderer) + } + + func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { + photoBackgroundTexture = Textures.createPhotoBackgroundTexture(from: renderer, size: size, pixelFormat: renderer.pixelFormat) + } + + func draw(on canvas: Canvas, with renderer: Renderer) { + guard let descriptor else { return } + + descriptor.colorAttachments[0].texture = photoBackgroundTexture + descriptor.colorAttachments[0].storeAction = .store + descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load + descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) + + guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } + commandBuffer.label = "Photo Background Command Buffer" + + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } + renderEncoder.label = label + + guard let photoBackgroundPipelineState else { return } + renderEncoder.setRenderPipelineState(photoBackgroundPipelineState) + + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + photo?.draw(device: renderer.device, renderEncoder: renderEncoder) + + renderEncoder.endEncoding() + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + } +} diff --git a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift index 49b9f6f..30c03d4 100644 --- a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift +++ b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift @@ -41,5 +41,6 @@ class PhotoRenderPass: RenderPass { renderEncoder.endEncoding() commandBuffer.commit() + commandBuffer.waitUntilCompleted() } } diff --git a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift index 55c2e13..d5fa1be 100644 --- a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift +++ b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift @@ -21,6 +21,8 @@ class StrokeRenderPass: RenderPass { var stroke: (any Stroke)? var strokeTexture: MTLTexture? + weak var eraserRenderPass: EraserRenderPass? + init(renderer: Renderer) { descriptor = MTLRenderPassDescriptor() strokePipelineState = PipelineStates.createStrokePipelineState(from: renderer) @@ -57,6 +59,13 @@ class StrokeRenderPass: RenderPass { renderEncoder.endEncoding() commandBuffer.commit() + if let eraserRenderPass, let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads { + descriptor.colorAttachments[0].loadAction = .load + eraserRenderPass.stroke = stroke + eraserRenderPass.descriptor = descriptor + eraserRenderPass.draw(on: canvas, with: renderer) + } + drawStrokeTexture(on: canvas, with: renderer) } diff --git a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift index 8589bd2..0fda41b 100644 --- a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift +++ b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift @@ -17,6 +17,7 @@ class ViewPortRenderPass: RenderPass { var viewPortUpdatePipelineState: MTLRenderPipelineState? weak var cacheTexture: MTLTexture? + weak var photoBackgroundTexture: MTLTexture? weak var view: MTKView? @@ -51,6 +52,10 @@ class ViewPortRenderPass: RenderPass { } renderEncoder.setRenderPipelineState(viewPortUpdatePipelineState) + + renderEncoder.setFragmentTexture(photoBackgroundTexture, index: 0) + canvas.renderViewPortUpdate(device: renderer.device, renderEncoder: renderEncoder) + renderEncoder.setFragmentTexture(cacheTexture, index: 0) canvas.renderViewPortUpdate(device: renderer.device, renderEncoder: renderEncoder) } else { @@ -59,6 +64,10 @@ class ViewPortRenderPass: RenderPass { } renderEncoder.setRenderPipelineState(viewPortPipelineState) + + renderEncoder.setFragmentTexture(photoBackgroundTexture, index: 0) + canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder) + renderEncoder.setFragmentTexture(cacheTexture, index: 0) canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder) } diff --git a/Memola/Canvas/Shaders/Stroke.metal b/Memola/Canvas/Shaders/Stroke.metal index 99ec764..046ec36 100644 --- a/Memola/Canvas/Shaders/Stroke.metal +++ b/Memola/Canvas/Shaders/Stroke.metal @@ -53,3 +53,21 @@ fragment float4 fragment_stroke( float4 color = float4(texture.sample(textureSampler, out.textCoord)); return float4(1, 1, 1, color.a); } + +fragment float4 fragment_stroke_eraser( + VertexOut out [[stage_in]], + texture2d texture [[texture(0)]] +) { + float2 circleCenter = float2(0.5, 0.5); + float radius = 0.4; + float4 circleColor = float4(1.0, 0.0, 0.0, 1.0); + + float2 fragCoord = out.textCoord; + float distance = length(fragCoord - circleCenter); + + if (distance < radius) { + return circleColor; + } else { + return float4(0.0); + } +} diff --git a/Memola/Canvas/Tool/Pen/Core/PenStyle.swift b/Memola/Canvas/Tool/Pen/Core/PenStyle.swift index 51f717d..8bf961a 100644 --- a/Memola/Canvas/Tool/Pen/Core/PenStyle.swift +++ b/Memola/Canvas/Tool/Pen/Core/PenStyle.swift @@ -10,7 +10,7 @@ import Foundation protocol PenStyle { var icon: (base: String, tip: String?) { get } - var textureName: String { get } + var textureName: String? { get } var thickness: (min: CGFloat, max: CGFloat) { get } var thicknessSteps: [CGFloat] { get } var color: [CGFloat] { get } @@ -22,6 +22,7 @@ protocol PenStyle { extension PenStyle { @discardableResult func loadTexture(on device: MTLDevice) -> MTLTexture? { - Textures.createPenTexture(with: textureName, on: device) + guard let textureName else { return nil } + return Textures.createPenTexture(with: textureName, on: device) } } diff --git a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift index 4b567e8..f4e96b6 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift @@ -10,8 +10,8 @@ import Foundation struct EraserPenStyle: PenStyle { var icon: (base: String, tip: String?) = ("eraser", nil) - var textureName: String = "point-texture" - + var textureName: String? = nil + var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30) var thicknessSteps: [CGFloat] = [0.5, 1, 2, 5, 7.5, 10, 12.5, 15, 17.5, 20, 25, 30] diff --git a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift index 404d62c..2e608a4 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift @@ -10,7 +10,7 @@ import Foundation struct MarkerPenStyle: PenStyle { var icon: (base: String, tip: String?) = ("marker-base", "marker-tip") - var textureName: String = "point-texture" + var textureName: String? = "point-texture" var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30)