transparency issues with repeated stamping of textures on an MTKView

北慕城南 提交于 2019-12-10 11:43:49

问题


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.

  1. 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?

  2. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!