10

This area is hardly documented online and it would be great to see a working Swift 3 example, of say, a custom drawn cube with manual SCNvector3s. There is this in objective-C but not Swift. This might not be a usual form of question but I know it would help many. If there is somewhere I missed, please mention.

The documentation is not very helpful

scngeometrysource, etc.

Thanks

harry lakins
  • 803
  • 1
  • 13
  • 28

1 Answers1

33

A custom geometry is constructed from a set of vertices and normals.

Vertices

In this context, a vertex is a point where two or more lines intersect. For a cube, the vertices are the corners shown in the following figure

enter image description here

We construct the geometry by building the cube's faces with a set of triangles, two triangles per face. Our first triangle is defined by vertices 0, 2, and 3 as shown in the below figure, and the second triangle is defined by vertices 0, 1, and 2. It is important to note that each triangle has a front and back side. The side of the triangle is determined by the order of the vertices, where the front side is specified in counter-clockwise order. For our cube, the front side will always be the outside of the cube.

If the cube's center is the origin, the six vertices that define one of the cube's face can be defined by

let vertices:[SCNVector3] = [
        SCNVector3(x:-1, y:-1, z:1),    // 0
        SCNVector3(x:1, y:1, z:1),      // 2
        SCNVector3(x:-1, y:1, z:1)      // 3

        SCNVector3(x:-1, y:-1, z:1),    // 0
        SCNVector3(x:1, y:-1, z:1),     // 1
        SCNVector3(x:1, y:1, z:1)       // 2
]

and we create the vertex source by

let vertexSource = SCNGeometrySource(vertices: vertices)

At this point, we have a vertex source that can be use to construct a face of the cube; however, SceneKit doesn't know how the triangle should react to light sources in the scene. To properly reflect light, we need to provide our geometry with a least one normal vector for each vertex.

Normals

A normal is a vector that specifies the orientation of a vertex that affects how light reflects off the corresponding triangle. In this case, the normal vectors for the six vertices of the triangle are the same; they all point in the positive z direction (i.e., x = 0, y = 0, and z = 1); see the red arrows in the below figure.

enter image description here

The normals are defined by

let normals:[SCNVector3] = [
        SCNVector3(x:0, y:0, z:1),      // 0
        SCNVector3(x:0, y:0, z:1),      // 2
        SCNVector3(x:0, y:0, z:1),      // 3

        SCNVector3(x:0, y:0, z:1),      // 0
        SCNVector3(x:0, y:0, z:1),      // 1
        SCNVector3(x:0, y:0, z:1)       // 2
]

and the source is defined by

let normalSource = SCNGeometrySource(normals: normals)

We now have the sources (vertices and normals) needed to construct a limited geometry, i.e., one cube face (two triangles). The final piece is to create an array of indices into the vertex and normal arrays. In this case, the indices are sequential because the vertices are in the order they are used.

var indices:[Int32] = [0, 1, 2, 3, 4, 5]

From the indices, we create an geometry element. The setup is a bit more involved because SCNGeometryElement requires an NSData as a parameter.

let indexData = NSData(bytes: &indices, length: MemoryLayout<Int32>.size * indices.count)

let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: indices.count, bytesPerIndex: MemoryLayout<Int32>.size)

We can now create the custom geometry with

let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element])

    

and lastly create a node and assign the custom geometry to its geometry property

let node = SCNNode()
node.geometry = geometry

scene.rootNode.addChildNode(node)

We now extend the vertices and normals to including all of the cube faces:

    // The vertices
    let v0 = SCNVector3(x:-1, y:-1, z:1)
    let v1 = SCNVector3(x:1, y:-1, z:1)
    let v2 = SCNVector3(x:1, y:1, z:1)
    let v3 = SCNVector3(x:-1, y:1, z:1)
    
    let v4 = SCNVector3(x:-1, y:-1, z:-1)
    let v5 = SCNVector3(x:1, y:-1, z:-1)
    let v6 = SCNVector3(x:-1, y:1, z:-1)
    let v7 = SCNVector3(x:1, y:1, z:-1)
    
    // All the cube faces
    let vertices:[SCNVector3] = [
        // Front face
        v0, v2, v3,
        v0, v1, v2,
        
        // Right face
        v1, v7, v2,
        v1, v5, v7,
        
        // Back
        v5, v6, v7,
        v5, v4, v6,
        
        // Left
        v4, v3, v6,
        v4, v0, v3,
        
        // Top
        v3, v7, v6,
        v3, v2, v7,
        
        // Bottom
        v1, v4, v5,
        v1, v0, v4
    ]
    
    let normalsPerFace = 6
    let plusX = SCNVector3(x:1, y:0, z:0)
    let minusX = SCNVector3(x:-1, y:0, z:0)
    let plusZ = SCNVector3(x:0, y:0, z:1)
    let minusZ = SCNVector3(x:0, y:0, z:-1)
    let plusY = SCNVector3(x:0, y:1, z:0)
    let minusY = SCNVector3(x:0, y:-1, z:0)
    
    // Create an array with the direction of each vertex. Each array element is
    // repeated 6 times with the map function. The resulting array or arrays
    // is then flatten to an array
    let normals:[SCNVector3] = [
        plusZ,
        plusX,
        minusZ,
        minusX,
        plusY,
        minusY
        ].map{[SCNVector3](repeating:$0,count:normalsPerFace)}.flatMap{$0}
    
    // Create an array of indices [0, 1, 2, ..., N-1]
    let indices = vertices.enumerated().map{Int32($0.0)}
    
    let vertexSource = SCNGeometrySource(vertices: vertices)
    
    let normalSource = SCNGeometrySource(normals: normals)
    
    let pointer = UnsafeRawPointer(indices)
    let indexData = NSData(bytes: pointer, length: MemoryLayout<Int32>.size * indices.count)
    
    let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: indices.count/3, bytesPerIndex: MemoryLayout<Int32>.size)
    
    let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element])
    
    // Create a node and assign our custom geometry
    let node = SCNNode()
    node.geometry = geometry
    
    scene.rootNode.addChildNode(node)
0x141E
  • 12,613
  • 2
  • 41
  • 54
  • This is awesome! – Alex McPherson Jul 22 '17 at 18:35
  • 3
    Thank you very much for this code example. However I believe that there is one small issue with it: `let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: **indices.count**, bytesPerIndex: MemoryLayout.size)` there should be `let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: **indices.count/3**, bytesPerIndex: MemoryLayout.size)`, because we have three times less triangles than verticals. – BlueLettuce16 Nov 09 '17 at 18:10
  • Can't assign material to the node. – Shubham Ojha Feb 01 '19 at 06:13
  • @ShubhamOjha you assign a material to a node's `geometry` not a node. – 0x141E Feb 02 '19 at 01:37
  • @0x141E Yes I am doing the same. still doen't render any change. FYI I am using primitive type line to construct line between two points. But cannot customize the lines material – Shubham Ojha Feb 02 '19 at 06:24
  • This is fantastic! Thanks so much. I have a follow up question @0x141E. If I want to dynamically change, say the position of a few verticies. Would the SCNNode get updated accordingly? If not, is it possible to make it happen? – 7ball Mar 26 '19 at 07:02
  • 1
    Nice answer but I got lost at the indices. I'm new to this and I have a book I have to finish reading on it but this answer helps reenforce some thing I learned on youtube, thanks because this definitely helped! For anyone who needs some more info watch these vids https://youtu.be/Z72mAAYvRL0 && https://youtu.be/Z72mAAYvRL0 && https://youtu.be/s1i2odLb6yo – Lance Samaria Jun 28 '19 at 16:38
  • your pointer gives warning **Initialization of 'UnsafeRawPointer' results in a dangling pointer** – Hope Aug 03 '21 at 07:38
  • @Hope I updated the code to resolve the issue – 0x141E Aug 07 '21 at 22:12
  • I made a cube with bumps on that cube's faces using what saw in that answer. Normals and order(counter clockwise) of vertices to create triangles are pain. They are different for each face and the faces of bumps on them. I hope to change bumps dynamically – Hope Aug 09 '21 at 09:04
  • Cubes and bumps on it is one big mesh, surface actually. – Hope Aug 09 '21 at 09:12
  • Regarding indices, a generic convenience initializer is available that makes it simpler (assuming triangles as in the exmple: `let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)` – johnhe4 Mar 22 '22 at 01:23