SCNNode from MDLMesh not rendered

I am writing an app to create 3D objects with curved surfaces such as a metal cabinet knob using SceneKit and Model I/O. I want the surfaces to be smooth so that edges between adjacent polygon faces are not visible. According to the documentation for MDLMesh.addNormals(withAttributeNamed: creaseThreshold:), a positive creaseThreshold value lower than 1.0 will interpolate sharper angles between faces into smooth surfaces. I have not been able to get this to work, and I need help with it.

The lines of code where the problem occurs are shown here.

let mesh = MDLMesh(scnGeometry: surfaceGeometry)

// mesh.addNormals(withAttributeNamed: "MDLVertexAttributeNormal", creaseThreshold: 0.9) surfaceGeometry = SCNGeometry(mdlMesh: mesh)

When the code is executed with middle line commented out, the knob object is rendered as shown in the screenshot. When that line is not commented out, mesh is altered and the SCNNode for the knob is created with no errors, but the node is not rendered.

The questions I have are: (1) What changes do I need the make to the code so that the node will be rendered with a smooth surface?, and (2) what is the recommended way of smoothing a curved surface so that edges between faces are not visible?

The full code for the function and a screenshot of the faceted knob object are attached.

![]("https://developer.apple.com/forums/content/attachment/a17feca7-ed6f-440c-add6-760a1cbf8778" "title=Screenshot cabinet knob with faceted surface.png;width=790;height=568")

code-block
func cabinetKnob() -> SCNNode {
    let controlPoints: [(x: Float, y: Float)] = [
        (0.728,-0.237), (0.176,-0.06), (0.202,0.475), (0.989,0.842),
        (-0.066,1.093), (-0.726,0.787) ]
    let pairs = bsplinePath(controlPoints)
    var knobProfile = [SCNVector3]()
    for (x,y) in pairs {
        knobProfile += [ SCNVector3(x: CGFloat(x), y: CGFloat(y), z: 0)]
    }
    let nProfiles = 64
    // create knob by rotating knobProfile about y-axis
    let aIncrement: CGFloat = 2 * CGFloat.pi / CGFloat(nProfiles)    // ~6 degrees
    var angle: CGFloat = 0
    var knobVertices = knobProfile.map( { $0 } )
    angle = 0
    for _ in 1...nProfiles {
        angle += aIncrement
        // rotate knobProfile about y-axis
        knobVertices += knobProfile.map( { $0.rotate(about: .y, by: angle) } )
    }
    let source = SCNGeometrySource(vertices: knobVertices)
    var indices = [[UInt16]]()
    var i: UInt16 = 0
    var j: UInt16 = UInt16(knobProfile.count)    // 1st vertex of next profile
    for k in 0...nProfiles {
        var stripIndices = [UInt16]()
        if k == nProfiles { j = 0 }
        for _ in 0...knobProfile.count-1 {
            stripIndices += [i, j]
            i += 1; j += 1
        }
        indices += [stripIndices]
        }
    let elements: [SCNGeometryElement] = indices.map( {
        SCNGeometryElement(indices: $0, primitiveType: .triangleStrip) } )
    var surfaceGeometry = SCNGeometry(sources: [source], elements: elements)
    let mesh = MDLMesh(scnGeometry: surfaceGeometry)
//    mesh.addNormals(withAttributeNamed: "MDLVertexAttributeNormal", creaseThreshold: 0.9)
    surfaceGeometry = SCNGeometry(mdlMesh: mesh)
    let aluminum = SCNMaterial()
    aluminum.lightingModel = SCNMaterial.LightingModel.physicallyBased
    aluminum.diffuse.contents = NSColor(srgbRed: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
    aluminum.roughness.contents = 0.2
    aluminum.metalness.contents = 0.9
    aluminum.isDoubleSided = true
    surfaceGeometry.materials = [ aluminum ]
    let node = SCNNode(geometry: surfaceGeometry)
    return node
}

I don't see the image I provided. The first code block should include the 2 lines following it.

It's been awhile since I worked with this. I ultimately switched to calculating the normals for more control. Not that I'm a wiz with that stuff. A lot of hacking away. But the vertices need to be referred to uniquely for each face and there is a method for that.

do{
try modelMesh.makeVerticesUniqueAndReturnError()
            modelMesh.addNormals(withAttributeNamed: "normal", creaseThreshold: 0.9)

let flattenedGeom = SCNGeometry(mdlMesh: modelMesh)
            
let flattenedNode = SCNNode(geometry: flattenedGeom)
            
 flattenedNode.geometry?.materials = [greenMaterial]
 return flattenedNode
            
 }catch{
   print("mesh vert error")
            return node
}

I didn't try your code as it doesn't include bsplinePath.

Bill3D,

Thank you for your help. Unfortunately, the node is still not being rendered. Here is the modified code plus the bsplinePath function. I will be grateful if you would try it.

```func cabinetKnob() -> SCNNode {
    let controlPoints: [(x: Float, y: Float)] = [
        (0.728,-0.237), (0.176,-0.06), (0.202,0.475), (0.989,0.842),
        (-0.066,1.093), (-0.726,0.787) ]
    let pairs = bsplinePath(controlPoints)
    var knobProfile = [SCNVector3]()
    for (x,y) in pairs {
        knobProfile += [ SCNVector3(x: CGFloat(x), y: CGFloat(y), z: 0)]
    }
    let nProfiles = 64
    // create knob by rotating knobProfile about y-axis
    let aIncrement: CGFloat = 2 * CGFloat.pi / CGFloat(nProfiles)    // ~6 degrees
    var angle: CGFloat = 0
    var knobVertices = knobProfile.map( { $0 } )
    angle = 0
    for _ in 1...nProfiles {
        angle += aIncrement
        // rotate knobProfile about y-axis
        knobVertices += knobProfile.map( { $0.rotate(about: .y, by: angle) } )
    }
    let source = SCNGeometrySource(vertices: knobVertices)
    var indices = [[UInt16]]()
    var i: UInt16 = 0
    var j: UInt16 = UInt16(knobProfile.count)    // 1st vertex of next profile
    for k in 0...nProfiles {
        var stripIndices = [UInt16]()
        if k == nProfiles { j = 0 }
        for _ in 0...knobProfile.count-1 {
            stripIndices += [i, j]
            i += 1; j += 1
        }
        indices += [stripIndices]
    }
    let elements: [SCNGeometryElement] = indices.map( {
        SCNGeometryElement(indices: $0, primitiveType: .triangleStrip) } )
    let surfaceGeometry = SCNGeometry(sources: [source], elements: elements)
    
    let aluminum = SCNMaterial()
    aluminum.lightingModel = SCNMaterial.LightingModel.physicallyBased
    aluminum.diffuse.contents = NSColor(srgbRed: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
    aluminum.roughness.contents = 0.2
    aluminum.metalness.contents = 0.9
    aluminum.isDoubleSided = true
    
    let modelMesh = MDLMesh(scnGeometry: surfaceGeometry)
    do{
        try modelMesh.makeVerticesUniqueAndReturnError()
        modelMesh.addNormals(withAttributeNamed: "normal", creaseThreshold: 0.9)
        let flattenedGeom = SCNGeometry(mdlMesh: modelMesh)
        let flattenedNode = SCNNode(geometry: flattenedGeom)
        flattenedNode.geometry?.materials = [aluminum]
        return flattenedNode
    }catch{
        fatalError("mesh vert error")
    }
}

func bsplinePath(_ p: [(x: Float, y: Float)]) -> [(Float, Float)] {
    var pairs = [(Float, Float)]()
    let nSegements: Int = 10
    let tIncr: Float = 1.0/Float(nSegements)
    let bmat = simd_float4x4([ simd_float4(-1/6, 0.5, -0.5, 1/6),
                               simd_float4(0.5, -1, 0, 4/6),
                               simd_float4(-0.5, 0.5, 0.5, 1/6),
                               simd_float4(1/6, 0, 0, 0) ])
    for i in 0...p.count-4 {
        let px = simd_float4(p[i].x, p[i+1].x, p[i+2].x, p[i+3].x)
        let py = simd_float4(p[i].y, p[i+1].y, p[i+2].y, p[i+3].y)
        let vx: simd_float4 = simd_mul(bmat, px)
        let vy: simd_float4 = simd_mul(bmat, py)
        //        print("bmat * P = (\(vx), \(vy))")
        var t: Float = 0
        let last: Int = (i == p.count-4) ? nSegements : nSegements-1
        for _ in 0...last {
            let qx = simd_dot(simd_float4(t*t*t, t*t, t, 1), vx)
            let qy = simd_dot(simd_float4(t*t*t, t*t, t, 1), vy)
            pairs += [(qx, qy)]
            t += tIncr
        }
    }
    return pairs
}

Yes, that wasn't the issue. It doesn't like the order and there is overlap. It's easier to see when you reduce the nProfiles and print the arrays to visually inspect them. I don't even remember why I started using .makeVerticesUniqueAndReturnError() but it's not even needed here.

        let controlPoints: [(x: Float, y: Float)] = [
            (0.728,-0.237), (0.176,-0.06), (0.202,0.475), (0.989,0.842),
            (-0.066,1.093), (-0.726,0.787) ]
        let pairs = bsplinePath(controlPoints)
        var knobProfile = [SCNVector3]()
        for (x,y) in pairs {
            knobProfile += [ SCNVector3(x: Float(x), y: Float(y), z: 0)]
        }
        let nProfiles = 20
        
        let aIncrement: CGFloat = 2 * CGFloat.pi / CGFloat(nProfiles)
       
        var knobVertices: [SCNVector3] = []
        for i in 0..<nProfiles {
                let angle = aIncrement * CGFloat(i)
                let rotatedProfile = knobProfile.map { $0.rotate(about: .y, by: Float(angle)) }
                knobVertices.append(contentsOf: rotatedProfile)
            }
        
        let source = SCNGeometrySource(vertices: knobVertices)
        var indices = [UInt16]()
        let profileCount = knobProfile.count
        
        
        for i in 0..<nProfiles {
            let nextProfileIndex = (i + 1) % nProfiles
            for j in 0..<profileCount - 1 {
                let currentIndex = UInt16(i * profileCount + j)
                let nextIndex = UInt16(nextProfileIndex * profileCount + j)
                let nextIndexNext = UInt16(nextProfileIndex * profileCount + j + 1)
                let currentIndexNext = UInt16(i * profileCount + j + 1)
                indices.append(contentsOf: [currentIndex, nextIndex, currentIndexNext])
                indices.append(contentsOf: [currentIndexNext, nextIndex, nextIndexNext])
            }
        }
        
        let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
        let surfaceGeometry = SCNGeometry(sources: [source], elements: [element])
        let modelMesh = MDLMesh(scnGeometry: surfaceGeometry)
        
        let aluminum = SCNMaterial()
        aluminum.lightingModel = .physicallyBased
        aluminum.diffuse.contents = UIColor.green
        aluminum.roughness.contents = 0.2
        aluminum.metalness.contents = 0.9
        aluminum.isDoubleSided = true
        surfaceGeometry.materials = [aluminum]
        
        //let knobNode = SCNNode(geometry: surfaceGeometry)
        //return knobNode
        
        do{
             try modelMesh.makeVerticesUniqueAndReturnError()
             modelMesh.addNormals(withAttributeNamed: "normal", creaseThreshold: 0.1)
              let flattenedGeom = SCNGeometry(mdlMesh: modelMesh)
              let flattenedNode = SCNNode(geometry: flattenedGeom)
              flattenedNode.geometry?.materials = [aluminum]
              return flattenedNode
          }catch{
              fatalError("mesh vert error")
          }
        
        
    }

SCNNode from MDLMesh not rendered
 
 
Q