iosswiftscenekitscnmaterialscngeometry

SceneKit selectively color a face by a certain index


My objective: given some index i and some UIColor c, make the face at that index turn into that color.

let vertices = // all of my vertices in the format of SCNVector3

let vertexSource = SCNGeometrySource(data: verticesData,
                                    semantic: .vertex,
                                    vectorCount: vertices.count,
                                    usesFloatComponents: true,
                                    componentsPerVector: 3,
                                    bytesPerComponent: MemoryLayout<Float>.size,
                                    dataOffset: 0,
                                    dataStride: MemoryLayout<SCNVector3>.size)


var colors: [SCNVector3] = vertices.map { SCNVector3(1, 1, 1) } // Make it all white color at first
colors[10] = SCNVector3(1, 0, 0)                                // Pick one at random and make it red
let colorSource = SCNGeometrySource(data: NSData(bytes: colors, length: MemoryLayout<SCNVector3>.size * colors.count) as Data,
                                    semantic: .color,
                                    vectorCount: colors.count,
                                    usesFloatComponents: true,
                                    componentsPerVector: 3,
                                    bytesPerComponent: MemoryLayout<Float>.size,
                                    dataOffset: 0,
                                    dataStride: MemoryLayout<SCNVector3>.size)

let elements = SCNGeometryElement(data: NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count) as Data,
                                primitiveType: .polygon,
                                primitiveCount: indices.count / 4,
                                bytesPerIndex: MemoryLayout<Int32>.size)

let g = SCNGeometry(sources: [vertexSource, colorSource], elements: [element])

My 3D object renders correctly. It is all white color, as expected. The red face, however, doesn't appear. I see a hint of red, but it looks like SceneKit is trying to color around the vertices rather than the face those vertices form. How can I force it to just color the face/polygon created by those vertices?


Solution

  • What is happening here is best explained using a simple example. Here's a plane that consists of two triangles:

    let vertices: [SCNVector3] = [
        SCNVector3(-1, 1, 0),
        SCNVector3( 1, 1, 0),
        SCNVector3( 1, -1, 0),
        SCNVector3(-1, -1, 0)
    ]
    let indices: [UInt16] = [
        0, 3, 1,
        1, 3, 2
    ]
    var colors: [SCNVector3] = vertices.map { _ in SCNVector3(1, 1, 1) }
    

    Now when you assign the red color to just one of entries in the colors array you get this result:

    enter image description here

    Which makes sense because you're assigning a color to a single vertex, not the whole face. The red color needs to be assigned to each index of the face (0, 3, 1) to make sure the whole face is red:

    colors[0] = SCNVector3(1, 0, 0)
    colors[3] = SCNVector3(1, 0, 0)
    colors[1] = SCNVector3(1, 0, 0)
    

    Which results in:

    enter image description here

    The red-white gradient is there because the colors are interpolated between vertices. The vertices that now have a red color also belong to the second face which also has a vertex with a white color. If you want to achieve this result:

    enter image description here

    Then you'll have to duplicate the vertices that are shared between the faces:

    let vertices: [SCNVector3] = [
        SCNVector3(-1, 1, 0),
        SCNVector3(-1, -1, 0),
        SCNVector3( 1, 1, 0),
    
        SCNVector3( 1, 1, 0),
        SCNVector3(-1, -1, 0),
        SCNVector3( 1, -1, 0)
        ]
    
    let indices: [UInt16] = [
        0, 1, 2,
        3, 4, 5
    ]
    
    colors[0] = SCNVector3(1, 0, 0)
    colors[1] = SCNVector3(1, 0, 0)
    colors[2] = SCNVector3(1, 0, 0)