如何修复Metal中2D对象重叠绘制的问题(模板,剪裁)?

huangapple go评论81阅读模式
英文:

How can I fix drawing overlapping 2D objects in Metal (stencil, clipping)?

问题

我正在使用C++和Apple Metal 3制作一个简单的2D渲染应用程序。我想使用模板测试来绘制重叠的2D对象,但它不起作用。

Renderer类的构造函数。创建一个模板纹理、描述符和模板状态对象。

Renderer::Renderer( MTL::Device* pDevice, MTK::View* pView )
{
  ...
  auto* pTexDesc = MTL::TextureDescriptor::alloc()->init();
  pTexDesc->setTextureType( MTL::TextureType2D );
  pTexDesc->setWidth( _viewWidth );
  pTexDesc->setHeight( _viewHeight );
  pTexDesc->setPixelFormat( MTL::PixelFormatStencil8 );
  pTexDesc->setStorageMode( MTL::StorageModePrivate );
  pTexDesc->setUsage( MTL::TextureUsageRenderTarget );
  _pStencilTexture = _pDevice->newTexture( pTexDesc );

  _pDepthStencilDesc = MTL::DepthStencilDescriptor::alloc()->init();
  _pStencilState = _pDevice->newDepthStencilState( _pDepthStencilDesc );
  ...
}

更新方法(每帧调用)。

void Renderer::update( float delta )
{
  NS::AutoreleasePool* pPool = NS::AutoreleasePool::alloc()->init();
  _pCurrentDrawableView->setClearColor( MTL::ClearColor::Make(1.0f, 1.0f, 1.0f, 1.0f) );

  MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer();
  MTL::RenderPassDescriptor* pRPD = _pCurrentDrawableView->currentRenderPassDescriptor();

  auto* pStencilAttach = pRPD->stencilAttachment();
  pStencilAttach->setTexture( _pStencilTexture );
  pStencilAttach->setClearStencil( 0 );
  pStencilAttach->setLoadAction( MTL::LoadActionClear );
  pStencilAttach->setStoreAction( MTL::StoreActionStore );

  MTL::RenderCommandEncoder* pEnc = pCmd->renderCommandEncoder(pRPD);
  
  // 添加你想要绘制的内容
  pEnc->setDepthStencilState( _pStencilState );

  auto* pStencil = MTL::StencilDescriptor::alloc()->init();
  _pDepthStencilDesc->setFrontFaceStencil( pStencil );
  _pDepthStencilDesc->setBackFaceStencil( pStencil );

  { // 绘制orc
    pStencil->setStencilCompareFunction( MTL::CompareFunctionAlways ); // glStencilFunc(func, _, _);
    pEnc->setStencilReferenceValue( 1 ); // glStencilFunc(_, ref, _);
    pStencil->setWriteMask( 0xff ); // glStencilFunc(_, _, mask);
    pStencil->setStencilFailureOperation( MTL::StencilOperationReplace ); // glStencilOp( _, _, zpass );
    
    orc->update( delta );
    drawImage( orc->texture(), { 0.0f, 0.0f }, { 100.0f, 100.0f }, orc->frame() );
    drawFrame( pCmd, pEnc );
  }

  { // 绘制背景草地
    pStencil->setStencilCompareFunction( MTL::CompareFunctionEqual ); // glStencilFunc(func, _, _);
    pEnc->setStencilReferenceValue( 0 ); // glStencilFunc(_, ref, _);
    pStencil->setWriteMask( 0x00 ); // glStencilFunc(_, _, mask); 
    pStencil->setStencilFailureOperation( MTL::StencilOperationKeep ); // glStencilOp( _, _, zpass );

    _pCurrentTexture = _textureMap["Grass"]->_pTexture;
    fillRect( { 0.0f, 0.0f, _viewWidth / 2.0f, _viewHeight / 2.0f } );
    drawFrame( pCmd, pEnc );
  }

  pEnc->endEncoding();
  pCmd->presentDrawable(_pCurrentDrawableView->currentDrawable());
  pCmd->commit();

  pStencil->release();
  pPool->release();
}

*drawImage和fillRect是一种推送顶点和索引信息的方法。

drawFrame

void Renderer::drawFrame( MTL::CommandBuffer* pCmd, MTL::RenderCommandEncoder* pEnc )
{
  NS::AutoreleasePool* pPool = NS::AutoreleasePool::alloc()->init();

  pEnc->setRenderPipelineState(_pPSO);

  _pVertexBuffer = _pDevice->newBuffer(_vertices.data(), sizeof(shader_types::VertexData) * _vertices.size(), MTL::StorageModeManaged);
  _pIndexBuffer = _pDevice->newBuffer(_indices.data(), sizeof(uint) * _indices.size(), MTL::StorageModeManaged);

  pEnc->setVertexBuffer(_pVertexBuffer, /* offset */ 0, /* index */ 0);
  pEnc->setFragmentTexture(_pCurrentTexture, 0);

  pEnc->drawIndexedPrimitives(MTL::PrimitiveTypeTriangle,
                            _pIndexBuffer->length(), MTL::IndexTypeUInt16,
                            _pIndexBuffer, 0);

  // 刷新向量
  _vertices.clear();
  _indices.clear();
  _indexBufferIndex = 0;

  pPool->release();
}

我没有使用深度测试来简化代码。这段代码的结果是只绘制了背景(Metal绘制了最后的元素)。据我所知,orc像素的内容应为1,背景像素的内容应为0。请给我一些建议。任何帮助都对我有帮助。非常感谢。

我尝试更改了在update(delta)方法中创建和设置变量的顺序。但这也没有起作用。此外,我更熟悉OpenGL而不是Metal。似乎Metal API与OpenGL的API不匹配。

英文:

I am currently making a simple 2D rendering application using Apple Metal 3 in C++. I want to draw overlapping 2D objects using stencil test, but it does not work.

A constructor for the Renderer class. Creating a stencil texture, descriptor and stencilState object.

Renderer::Renderer( MTL::Device* pDevice, MTK::View* pView )
{
  ...
  auto* pTexDesc = MTL::TextureDescriptor::alloc()->init();
  pTexDesc->setTextureType( MTL::TextureType2D );
  pTexDesc->setWidth( _viewWidth );
  pTexDesc->setHeight( _viewHeight );
  pTexDesc->setPixelFormat( MTL::PixelFormatStencil8 );
  pTexDesc->setStorageMode( MTL::StorageModePrivate );
  pTexDesc->setUsage( MTL::TextureUsageRenderTarget );
  _pStencilTexture = _pDevice->newTexture( pTexDesc );

  _pDepthStencilDesc = MTL::DepthStencilDescriptor::alloc()->init();
  _pStencilState = _pDevice->newDepthStencilState( _pDepthStencilDesc );
  ...
}

Update method(called every frame).

void Renderer::update( float delta )
{
  NS::AutoreleasePool* pPool = NS::AutoreleasePool::alloc()->init();
  _pCurrentDrawableView->setClearColor( MTL::ClearColor::Make(1.0f, 1.0f, 1.0f, 1.0f) );

  MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer();
  MTL::RenderPassDescriptor* pRPD = _pCurrentDrawableView->currentRenderPassDescriptor();

  auto* pStencilAttach = pRPD->stencilAttachment();
  pStencilAttach->setTexture( _pStencilTexture );
  pStencilAttach->setClearStencil( 0 );
  pStencilAttach->setLoadAction( MTL::LoadActionClear );
  pStencilAttach->setStoreAction( MTL::StoreActionStore );

  MTL::RenderCommandEncoder* pEnc = pCmd->renderCommandEncoder(pRPD);
  
  // ADD WHAT YOU WANT TO DRAW
  pEnc->setDepthStencilState( _pStencilState );

  auto* pStencil = MTL::StencilDescriptor::alloc()->init();
  _pDepthStencilDesc->setFrontFaceStencil( pStencil );
  _pDepthStencilDesc->setBackFaceStencil( pStencil );

  { // draw orc
    pStencil->setStencilCompareFunction( MTL::CompareFunctionAlways ); // glStencilFunc(func, _, _);
    pEnc->setStencilReferenceValue( 1 ); // glStencilFunc(_, ref, _);
    pStencil->setWriteMask( 0xff ); // glStencilFunc(_, _, mask);
    pStencil->setStencilFailureOperation( MTL::StencilOperationReplace ); // glStencilOp( _, _, zpass );
    
    orc->update( delta );
    drawImage( orc->texture(), { 0.0f, 0.0f }, { 100.0f, 100.0f }, orc->frame() );
    drawFrame( pCmd, pEnc );
  }

  { // draw grass backgroud
    pStencil->setStencilCompareFunction( MTL::CompareFunctionEqual ); // glStencilFunc(func, _, _);
    pEnc->setStencilReferenceValue( 0 ); // glStencilFunc(_, ref, _);
    pStencil->setWriteMask( 0x00 ); // glStencilFunc(_, _, mask); 
    pStencil->setStencilFailureOperation( MTL::StencilOperationKeep ); // glStencilOp( _, _, zpass );

    _pCurrentTexture = _textureMap["Grass"]->_pTexture;
    fillRect( { 0.0f, 0.0f, _viewWidth / 2.0f, _viewHeight / 2.0f } );
    drawFrame( pCmd, pEnc );
  }

  pEnc->endEncoding();
  pCmd->presentDrawable(_pCurrentDrawableView->currentDrawable());
  pCmd->commit();

  pStencil->release();
  pPool->release();
}

*drawImage and fillRect is a kind of method that pushes vertices and indices information.

drawFrame

void Renderer::drawFrame( MTL::CommandBuffer* pCmd, MTL::RenderCommandEncoder* pEnc )
{
  NS::AutoreleasePool* pPool = NS::AutoreleasePool::alloc()->init();

  pEnc->setRenderPipelineState(_pPSO);

  _pVertexBuffer = _pDevice->newBuffer(_vertices.data(), sizeof(shader_types::VertexData) * _vertices.size(), MTL::StorageModeManaged);
  _pIndexBuffer = _pDevice->newBuffer(_indices.data(), sizeof(uint) * _indices.size(), MTL::StorageModeManaged);

  pEnc->setVertexBuffer(_pVertexBuffer, /* offset */ 0, /* index */ 0);
  pEnc->setFragmentTexture(_pCurrentTexture, 0);

  pEnc->drawIndexedPrimitives(MTL::PrimitiveTypeTriangle,
                            _pIndexBuffer->length(), MTL::IndexTypeUInt16,
                            _pIndexBuffer, 0);

  // flush vectors
  _vertices.clear();
  _indices.clear();
  _indexBufferIndex = 0;

  pPool->release();
}

I am not using the depth test to simplify the code. The result of this code is drawing only background. (Metal draws the last elements) As far as I know, the content of the orc pixels should be 1 and the content of the background pixels should be 0. Please give me any advice. Anything would be helpful to me. Thanks a lot.

I have tried to change an order to create and set variables in update(delta) method. But it does not work either. Also, I am more familiar with OpenGL than Metal. It seems like Metal API are not matched in OpenGL's API.

答案1

得分: 2

你误解了状态和描述符的工作原理。

MTLDepthStencilState 是一个不可变对象,不像OpenGL状态,这意味着即使你更改描述符,也不能在事后更改它。因此,在调用 newDepthStencilState 之前,你需要创建一个合适的描述符。

另外,属性 frontFaceStencilbackFaceStencil 在Metal头文件中定义为 @property (copy, nonatomic, null_resettable)

@property (copy, nonatomic, null_resettable) MTLStencilDescriptor *frontFaceStencil;
@property (copy, nonatomic, null_resettable) MTLStencilDescriptor *backFaceStencil;

这意味着 MTLStencilDescriptorMTLDepthStencilDescriptor 内部是按值复制的,而不是按引用设置的。

所有这些意味着为了使 update 工作,你需要像这样编写 Renderer::Renderer

_pOrcDepthStencilDesc = MTL::DepthStencilDescriptor::alloc()->init();
// 设置兽人深度模板状态
auto* pOrcStencil = MTL::StencilDescriptor::alloc()->init();
pOrcStencil->setStencilCompareFunction(MTL::CompareFunctionAlways); // glStencilFunc(func, _, _);
pOrcStencil->setWriteMask(0xff); // glStencilFunc(_, _, mask);
pOrcStencil->setStencilFailureOperation(MTL::StencilOperationReplace); // glStencilOp( _, _, zpass );

_pOrcDepthStencilDesc->setFrontFacingState(...);
_pOrcDepthStencilDesc->setBackFacingState(...);
_pOrcStencilState = _pDevice->newDepthStencilState(_pOrcDepthStencilDesc);

_pGrassDepthStencilDesc = MTL::DepthStencilDescriptor::alloc()->init();
// 设置草地深度模板状态
_pGrassDepthStencilDesc->setFrontFacingState(...);
_pGrassDepthStencilDesc->setBackFacingState(...);
_pGrassStencilState = _pDevice->newDepthStencilState(_pGrassDepthStencilDesc);

然后在 update 中:

{ // 绘制兽人
    pEnc->setDepthStencilState(_pOrcStencilState);
    pEnc->setStencilReferenceValue(1); // glStencilFunc(_, ref, _);
    
    orc->update(delta);
    drawImage(orc->texture(), {0.0f, 0.0f}, {100.0f, 100.0f}, orc->frame());
    drawFrame(pCmd, pEnc);
}
英文:

You are misunderstanding how states and descriptors work.

MTLDepthStencilState is an immutable object, not like OpenGL states, which means that you can't change it after the fact, even if you change the descriptor. So you need to create a proper descriptor before you call newDepthStencilState.

Also, Property frontFaceStencil and backFaceStencil are defined as @property (copy, nonatomic, null_resettable) in Metal headers:

@property (copy, nonatomic, null_resettable) MTLStencilDescriptor *frontFaceStencil;
@property (copy, nonatomic, null_resettable) MTLStencilDescriptor *backFaceStencil;

This means the MTLStencilDescriptor inside MTLDepthStencilDescriptor is copied by value, and not set by reference.

All of this means that in order for update to work, you need to write Renderer::Renderer like this:

_pOrcDepthStencilDesc = MTL::DepthStencilDescriptor::alloc()->init();
// Set up orc depth stencil state
auto* pOrcStencil = MTL::StencilDescriptor::alloc()->init();
pOrcStencil->setStencilCompareFunction( MTL::CompareFunctionAlways ); // glStencilFunc(func, _, _);
pOrcStencil->setWriteMask( 0xff ); // glStencilFunc(_, _, mask);
pOrcStencil->setStencilFailureOperation( MTL::StencilOperationReplace ); // glStencilOp( _, _, zpass );
_pOrcDepthStencilDesc->setFrontFacingState(...);
_pOrcDepthStencilDesc->setBackFacingState(...);
_pOrcStencilState = _pDevice->newDepthStencilState( _pOrcDepthStencilDesc );
_pGrassDepthStencilDesc = MTL::DepthStencilDescriptor::alloc()->init();
// Set up grass depth stencil state
_pGrassDepthStencilDesc->setFrontFacingState(...);
_pGrassDepthStencilDesc->setBackFacingState(...);
_pGrassStencilState = _pDevice->newDepthStencilState( _pGrassDepthStencilDesc );

and then in update:

{ // draw orc
pEnc->setDepthStencilState( _pOrcStencilState );
pEnc->setStencilReferenceValue( 1 ); // glStencilFunc(_, ref, _);
orc->update( delta );
drawImage( orc->texture(), { 0.0f, 0.0f }, { 100.0f, 100.0f }, orc->frame() );
drawFrame( pCmd, pEnc );
}

答案2

得分: 0

我找到了一个解决方案。我只是误解了模板测试的工作原理。

在Renderer的初始化点构建一个用于模板测试的纹理。

// 为模板测试构建纹理
void Renderer::buildStencilTest()
{
  auto* pStencilDesc = MTL::TextureDescriptor::texture2DDescriptor(
      MTL::PixelFormatDepth32Float_Stencil8, _viewSize[0], _viewSize[1], false);
  pStencilDesc->setStorageMode( MTL::StorageModePrivate );
  pStencilDesc->setUsage( MTL::TextureUsageRenderTarget | MTL::TextureUsageShaderRead );
  _pDepthStencilTexture = _pDevice->newTexture( pStencilDesc );
}

在update()方法中,将我们为模板测试构建的纹理设置为视图的当前渲染通道的模板附件。

  1. 将模板值清除为零。

  2. 绘制我想要遮罩的区域(我不会使用深度测试,所以禁用它)。

  3. 将模板比较函数设置为“始终通过”并深度/模板通过操作设置为“替换”。

  4. 将模板参考值设置为1。模板测试将比较这个值和模板纹理的值。在这个时候,模板测试“始终通过”并用“参考值(1)”和“写入掩码(0xff)”的逻辑AND结果替换纹理的绘制区域,因此为1。

  5. 绘制元素。

void Renderer::update() {
  // 清除模板
  pRPD->stencilAttachment()->setLoadAction( MTL::LoadActionClear );
  pRPD->stencilAttachment()->setStoreAction( MTL::StoreActionStore );
  pRPD->stencilAttachment()->setTexture( _pDepthStencilTexture );
  pRPD->stencilAttachment()->setClearStencil( 0 );

  auto* pDsDesc = MTL::DepthStencilDescriptor::alloc()->init();
  pDsDesc->setDepthCompareFunction( MTL::CompareFunctionAlways );
  pDsDesc->setDepthWriteEnabled( false );
  auto* pStencil = MTL::StencilDescriptor::alloc()->init();
  
  pStencil->setStencilCompareFunction(MTL::CompareFunctionAlways);

  pStencil->setDepthFailureOperation( MTL::StencilOperationKeep );
  pStencil->setStencilFailureOperation( MTL::StencilOperationKeep );
  pStencil->setDepthStencilPassOperation( MTL::StencilOperationReplace );
  pStencil->setReadMask(0xff);
  pStencil->setWriteMask(0xff);

  pDsDesc->setFrontFaceStencil(pStencil);
  pDsDesc->setBackFaceStencil(pStencil);
  pEnc->setStencilReferenceValue(1);
  pEnc->setDepthStencilState( _pDevice->newDepthStencilState( pDsDesc ) );

  drawImage( _orcTexture, { 0.0f, 0.0f }, { 50.0f, 50.0f }, _currentFrame );
  drawFrame(pEnc);
  ... 
}

绘制其他区域,这将成为背景。

  1. 将模板比较函数设置为“等于”。

  2. 将深度/模板通过操作设置为“保持”。如果模板测试失败,模板纹理的区域将为0。

  3. 绘制元素。

void Renderer::update() {
  pDsDesc->setDepthCompareFunction( MTL::CompareFunctionAlways );
  pDsDesc->setDepthWriteEnabled( false );

  pStencil->setStencilCompareFunction(MTL::CompareFunctionEqual);

  pStencil->setDepthFailureOperation(MTL::StencilOperationKeep);
  pStencil->setStencilFailureOperation(MTL::StencilOperationKeep);
  pStencil->setDepthStencilPassOperation(MTL::StencilOperationKeep);
  pStencil->setReadMask(0xff);
  pStencil->setWriteMask(0xff);

  pDsDesc->setFrontFaceStencil(pStencil);
  pDsDesc->setBackFaceStencil(pStencil);
  pEnc->setDepthStencilState( _pDevice->newDepthStencilState( pDsDesc ) );

  drawImage( grassTexture, { 0.0f, 0.0f }, { 200.0f, 200.0f } );
  drawFrame( pEnc );
  ... 
}

它将仅绘制第二个元素的遮罩纹理的小遮罩区域。

英文:

I found a solution. I just misunderstood how the stencil test works.

Building a texture for stencil test at the initializing point of the Renderer.

// build a texture for stencil test
void Renderer::buildStencilTest()
{
  auto* pStencilDesc = MTL::TextureDescriptor::texture2DDescriptor(
      MTL::PixelFormatDepth32Float_Stencil8, _viewSize[0], _viewSize[1], false);
  pStencilDesc->setStorageMode( MTL::StorageModePrivate );
  pStencilDesc->setUsage( MTL::TextureUsageRenderTarget | MTL::TextureUsageShaderRead );
  _pDepthStencilTexture = _pDevice->newTexture( pStencilDesc );
}

In the update() method, set the texture what we built for stencil test to the stencil attachment of current render pass of a view.

  1. Clear stencil value as ZERO.

  2. Draw the area what I want to mask. (I am not gonna use Depth test, so disable it.)

  3. Set the stencil compare function to "Pass Always" and depth/stencil pass operation as "Replace".

  4. Set stencil reference value to 1. Stencil test is gonna compare this and the value of stencil texture. At this time, stencil test "always PASS" and replace the drawing area of texture as the result of logical AND of "reference value (1)" and "write mask (0xff)" therefore 1.

  5. Draw elements.

void Renderer::update() {
  // Clear stencil 
  pRPD->stencilAttachment()->setLoadAction( MTL::LoadActionClear );
  pRPD->stencilAttachment()->setStoreAction( MTL::StoreActionStore );
  pRPD->stencilAttachment()->setTexture( _pDepthStencilTexture );
  pRPD->stencilAttachment()->setClearStencil( 0 );

  auto* pDsDesc = MTL::DepthStencilDescriptor::alloc()->init();
  pDsDesc->setDepthCompareFunction( MTL::CompareFunctionAlways );
  pDsDesc->setDepthWriteEnabled( false );
  auto* pStencil = MTL::StencilDescriptor::alloc()->init();
  
  pStencil->setStencilCompareFunction(MTL::CompareFunctionAlways);

  pStencil->setDepthFailureOperation( MTL::StencilOperationKeep );
  pStencil->setStencilFailureOperation( MTL::StencilOperationKeep );
  pStencil->setDepthStencilPassOperation( MTL::StencilOperationReplace );
  pStencil->setReadMask(0xff);
  pStencil->setWriteMask(0xff);

  pDsDesc->setFrontFaceStencil(pStencil);
  pDsDesc->setBackFaceStencil(pStencil);
  pEnc->setStencilReferenceValue(1);
  pEnc->setDepthStencilState( _pDevice->newDepthStencilState( pDsDesc ) );

  drawImage( _orcTexture, { 0.0f, 0.0f }, { 50.0f, 50.0f }, _currentFrame );
  drawFrame(pEnc);
  ... 
}

Drawing the other area which is gonna be the background.

  1. Set stencil compare function to "Equal".

  2. Set depth/stencil pass operation to "Keep". If the stencil test failed, the area of stencil texture is gonna be 0.

  3. Draw elements.

void Renderer::update() {
  pDsDesc->setDepthCompareFunction( MTL::CompareFunctionAlways );
  pDsDesc->setDepthWriteEnabled( false );

  pStencil->setStencilCompareFunction(MTL::CompareFunctionEqual);

  pStencil->setDepthFailureOperation(MTL::StencilOperationKeep);
  pStencil->setStencilFailureOperation(MTL::StencilOperationKeep);
  pStencil->setDepthStencilPassOperation(MTL::StencilOperationKeep);
  pStencil->setReadMask(0xff);
  pStencil->setWriteMask(0xff);

  pDsDesc->setFrontFaceStencil(pStencil);
  pDsDesc->setBackFaceStencil(pStencil);
  pEnc->setDepthStencilState( _pDevice->newDepthStencilState( pDsDesc ) );

  drawImage( grassTexture, { 0.0f, 0.0f }, { 200.0f, 200.0f } );
  drawFrame( pEnc );
  ... 
}

It will draw only the small masked area with the masked texture(grass) of second elements.

huangapple
  • 本文由 发表于 2023年6月26日 13:55:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76553856.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定