问题
I am trying to implement a metal-backed drawing application where brushstrokes are drawn on an MTKView by stamping a textured square repeatedly along a path. The problem I'm having is that, while each brush stamp properly shows the texture's opacity, overlapping squares do not build value, but rather override each other. In the caption below, each stamp is a textured circle with an alpha component
I have a feeling that because all the stamps are being rendered at once, there is no way for the renderer to "build up" value. However, I'm a little out of my depth with my metal know-how, so I'm hoping someone can point me in the right direction.
Below is further pertinent information:
For a single brush stroke, all geometry is stored in an array vertexArrayBrush3DMesh that contains all the square stamps (each square is made up of 2 triangles). The coordinates for each vertex have a z-value of 0.0 which means they all occupy the same 3d 'plane'. Could this be an issue? (I tested putting randomized z-values, but I saw no visual difference in behavior)
Below is my renderPipeline set up. Note that ".isBlendingEnabled = true" and ".alphaBlendingOperation = .add" are both commented out, as they had no effect in solving my problem
// 5a. Define render pipeline settings
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
renderPipelineDescriptor.vertexFunction = vertexProgram
renderPipelineDescriptor.sampleCount = self.sampleCount
renderPipelineDescriptor.colorAttachments[0].pixelFormat = self.colorPixelFormat
//renderPipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
//renderPipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
renderPipelineDescriptor.fragmentFunction = fragmentProgram
Note that adding the following properties to renderPassDescriptor did have an effect in setting transparency for the entire canvas
// ensure canvas is transparent
renderPassDescriptor?.colorAttachments[0].loadAction = .clear
renderPassDescriptor?.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0)
Below is the portion of my code that does the rendering.
func metalRenderColoredMesh(){
// the key to this routine is that it operates on a prepopulated array of points stored in vertexArrayBrush3DMesh
// whenever we want to render the current mesh, this routine gets called from draw()
if vertexArrayBrush3DMesh.count > 1 { // we must have more than 2 points to be able to draw a line
// 6. Set buffer size of objects to be drawn
let dataSize = vertexArrayBrush3DMesh.count * MemoryLayout<Vertex3DColor>.stride // apple recommendation size of the vertex data in bytes
let vertexBuffer: MTLBuffer = device!.makeBuffer(bytes: vertexArrayBrush3DMesh, length: dataSize, options: [])! // create a new buffer on the GPU
let renderPassDescriptor: MTLRenderPassDescriptor? = self.currentRenderPassDescriptor
let samplerState: MTLSamplerState? = defaultSampler(device: self.device!)
let texture = MetalTexture(resourceName: "opaqueRound", ext: "png", mipmaped: true)
texture.loadTexture(device: device!, commandQ: commandQueue, flip: true)
// If the renderPassDescriptor is valid, begin the commands to render into its drawable
if renderPassDescriptor != nil {
// ensure canvas is transparent
renderPassDescriptor?.colorAttachments[0].loadAction = .clear
renderPassDescriptor?.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0)
// Create a new command buffer for each tessellation pass
let commandBuffer: MTLCommandBuffer? = commandQueue.makeCommandBuffer()
// 7a. Create a renderCommandEncoder four our renderPipeline
let renderCommandEncoder: MTLRenderCommandEncoder? = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor!)
renderCommandEncoder?.label = "Render Command Encoder"
renderCommandEncoder?.setRenderPipelineState(renderPipeline!)
renderCommandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderCommandEncoder?.setFragmentTexture(texture.texture, index: 0)
renderCommandEncoder?.setFragmentSamplerState(samplerState, index: 0)
// most important below: we tell the GPU to draw a set of triangles, based on the vertex buffer. Each triangle consists of three vertices, starting at index 0 inside the vertex buffer, and there are vertexCount/3 triangles total
renderCommandEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexArrayBrush3DMesh.count)
///////////renderCommandEncoder?.popDebugGroup()
renderCommandEncoder?.endEncoding() // finalize renderEncoder set up
commandBuffer?.present(self.currentDrawable!) // needed to make sure the new texture is presented as soon as the drawing completes
// 7b. Render to pipeline
commandBuffer?.commit() // commit and send task to gpu
} // end of if renderPassDescriptor
} // end of if vertexArrayBrush3DMesh.count > 1 }// end of func metalRenderColoredMesh()
2017/01/17 Update
After implementing the suggestion provided by @warrenm, my brushstrokes are looking infinitely more promising.
However, some new questions/issues have arisen as a result.
I'm not sure how Metal treats additive color values that are > 1. Do they get clamped at 1? Is this the cause of the ringing I see on the saturated portions of the stroke?
Due to the irregular nature of bezier curve sampling I've implemented, there are areas portions of the brushstroke that appear somewhat patchy. In order for this method of stamping to work, I have to figure out a way to evenly distribute the stamps across the entire stroke.
回答1:
Your blend factors need some work. By default, even with blending enabled, the output of your fragment shader replaces the current contents of the color buffer (note that I'm ignoring the depth buffer here, since that's probably irrelevant).
The blend equation you currently have is:
cdst′ = 1 * csrc + 0 * cdst
For classic source-over compositing, what you want is something more like:
cdst′ = αsrc * csrc + (1 - αsrc) * cdst
renderPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
renderPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
renderPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
renderPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
For your particular use case, you might instead want to use additive blending, where the new fragment value is simply added to what's already there:
cdst′ = 1 * csrc + 1 * cdst
renderPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
renderPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .one
renderPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
renderPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
Whether or not you actually want additive blending depends on the exact effect you're after. With values less than 1, it will create a sort of "accumulating" paint effect, but it will also saturate once the cumulative color values exceed 1, which may not be pleasing. On the other hand, additive blending is commutative, which means you don't have to care about the order in which you draw your brush strokes.
(In the preceding discussion, I've ignored premultiplied alpha, which you absolutely must account for when drawing to a non-opaque image. You can read all about the gnarly details here.)
来源:https://stackoverflow.com/questions/48276449/transparency-issues-with-repeated-stamping-of-textures-on-an-mtkview