MDLMesh初始化器生成退化三角形的原因咨询
问题描述
通过MDLMesh初始化器生成的球体,其几何体顶部和底部存在退化三角形(表现为极点处的细线状三角形,面积趋近于零)。使用MDLMesh初始化器生成圆柱体时,同样会出现该问题。当调用addNormals()方法且折痕阈值小于1时,退化三角形会导致模型无法正确着色。
复现代码
Renderer代码
import MetalKit class Renderer: NSObject { let mdlMesh: MDLMesh let commandQueue: MTLCommandQueue let renderPipelineState: MTLRenderPipelineState let depthStencilState: MTLDepthStencilState init(mtkView: MTKView) { guard let device = MTLCreateSystemDefaultDevice() else { fatalError("Failed to create system default device.") } mtkView.device = device mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1) mtkView.colorPixelFormat = .bgra8Unorm let allocator = MTKMeshBufferAllocator(device: device) let mdlMesh = MDLMesh(sphereWithExtent: [1,1,1], segments: [10,10], inwardNormals: false, geometryType: .triangles, allocator: allocator) self.mdlMesh = mdlMesh guard let commandQueue = device.makeCommandQueue() else { fatalError("Failed to create command queue.") } self.commandQueue = commandQueue guard let library = device.makeDefaultLibrary() else { fatalError("Failed to make default library.") } let vertexFunction = library.makeFunction(name: "vertex_function") let fragmentFunction = library.makeFunction(name: "fragment_function") let renderPipelineDescriptor = MTLRenderPipelineDescriptor() renderPipelineDescriptor.vertexFunction = vertexFunction renderPipelineDescriptor.fragmentFunction = fragmentFunction renderPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm renderPipelineDescriptor.vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(mdlMesh.vertexDescriptor) guard let renderPipelineState = try? device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) else { fatalError("Failed to make render pipeline state.") } self.renderPipelineState = renderPipelineState let depthStencilDescriptor = MTLDepthStencilDescriptor() depthStencilDescriptor.isDepthWriteEnabled = true depthStencilDescriptor.depthCompareFunction = .less guard let depthStencilState = device.makeDepthStencilState(descriptor: depthStencilDescriptor) else { fatalError("Failed to make depth stencil state.") } self.depthStencilState = depthStencilState } } extension Renderer: MTKViewDelegate { func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { } func draw(in view: MTKView) { guard let commandBuffer = commandQueue.makeCommandBuffer() else { fatalError("Failed to make command buffer.") } guard let renderPassDescriptor = view.currentRenderPassDescriptor else { fatalError("Failed to get current render pass descriptor.") } guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { fatalError("Failed to make render command encoder.") } renderCommandEncoder.setRenderPipelineState(renderPipelineState) let mesh = try! MTKMesh(mesh: mdlMesh, device: view.device!) for submesh in mesh.submeshes { renderCommandEncoder.setVertexBuffer(mesh.vertexBuffers[0].buffer, offset: 0, index: 0) renderCommandEncoder.drawIndexedPrimitives(type: .triangle, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: 0) } renderCommandEncoder.endEncoding() guard let drawable = view.currentDrawable else { fatalError("Failed to get current drawable.") } commandBuffer.present(drawable) commandBuffer.commit() } }
Shader代码
#include <metal_stdlib> using namespace metal; struct VertexIn { float4 position [[ attribute(0) ]]; }; vertex float4 vertex_function(const VertexIn vIn [[ stage_in ]]) { return vIn.position; } fragment float4 fragment_function(const float4 vIn [[ stage_in ]]) { return float4(1,1,1,1); }
问题原因
MDLMesh内置的参数化几何体生成器(如球体、圆柱体)在极点处采用了共享顶点的拓扑设计:所有环形分段的顶点都连接到同一个极点顶点。当分段数较少时,这些三角形的三个顶点会因极度接近而几乎共线/重合,最终退化为面积趋近于0的“退化三角形”。
而当调用addNormals(withAttributeNamed:creaseThreshold:)且折痕阈值小于1时,ModelIO会基于相邻面的夹角计算平滑法线。退化三角形的顶点几乎重合,其法向量计算会出现异常(叉乘结果趋近于零向量),进而干扰整个模型的法线插值逻辑,导致着色错误。
解决方案
1. 增加分段数
提升几何体的分段精度(比如将segments: [10,10]改为segments: [32,32]),这样极点处的三角形会变得更小,退化程度大幅降低,视觉上几乎不可见,同时法线计算也会更稳定。
2. 调整法线生成参数
将addNormals()的折痕阈值设为1.0,这样ModelIO会为每个面生成独立的硬法线,避免退化三角形影响法线插值。示例代码:
mdlMesh.addNormals(withAttributeNamed: MDLVertexAttributeNormal, creaseThreshold: 1.0)
缺点是模型的平滑度会下降,适合硬表面风格的模型。
3. 自定义几何体拓扑
手动修改Mesh的顶点和索引数据,将极点的单个顶点拆分为多个独立顶点,让每个三角形都拥有独立的顶点,彻底避免共享顶点导致的退化。这种方式需要修改ModelIO的缓冲区数据,实现较为繁琐,但能从根源解决问题。
4. 替换几何体生成方式
使用第三方网格生成库,或者自定义算法生成球体/圆柱体的Mesh数据,这类方案通常会生成更优质的拓扑结构,避免极点处的退化三角形。
内容的提问来源于stack exchange,提问作者pamplemousse

