You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MDLMesh初始化器生成退化三角形的原因咨询

为何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 08:45:26