Question
How do you define the material on a custom geometry from vertex data, so that it renders the same as 'typical' SCNNodes?
Details
In this scene there are
- A directional light
- A red sphere using physicallybased lighting model
- A blue sphere using physicallybased lighting model
- A custom SCNGeometry using vertex data, using a physicallybased lighting model
The red and blue spheres render as I would expect. The two points / spheres in the custom geometry are black.
Why?
Here is the playgrond code:
Setting the scene
import UIKit
import SceneKit
import PlaygroundSupport
// create a scene view with an empty scene
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 600, height: 600))
var scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = UIColor(white: 0.75, alpha: 1.0)
sceneView.allowsCameraControl = true
PlaygroundPage.current.liveView = sceneView
let directionalLightNode: SCNNode = {
let n = SCNNode()
n.light = SCNLight()
n.light!.type = SCNLight.LightType.directional
n.light!.color = UIColor(white: 0.75, alpha: 1.0)
return n
}()
directionalLightNode.simdPosition = simd_float3(0,5,0) // Above the scene
directionalLightNode.simdOrientation = simd_quatf(angle: -90 * Float.pi / 180.0, axis: simd_float3(1,0,0)) // pointing down
scene.rootNode.addChildNode(directionalLightNode)
// a camera
var cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.simdPosition = simd_float3(0,0,5)
scene.rootNode.addChildNode(cameraNode)
Adding the blue and red spheres
// ----------------------------------------------------
// Example creating SCNSphere Nodes directly
// Sphere 1
let sphere1 = SCNSphere(radius: 0.3)
let sphere1Material = SCNMaterial()
sphere1Material.diffuse.contents = UIColor.red
sphere1Material.lightingModel = .physicallyBased
sphere1.materials = [sphere1Material]
let sphere1Node = SCNNode(geometry: sphere1)
sphere1Node.simdPosition = simd_float3(-2,0,0)
// Sphere2
let sphere2 = SCNSphere(radius: 0.3)
let sphere2Material = SCNMaterial()
sphere2Material.diffuse.contents = UIColor.blue
sphere2Material.lightingModel = .physicallyBased
sphere2.materials = [sphere2Material]
let sphere2Node = SCNNode(geometry: sphere2)
sphere2Node.simdPosition = simd_float3(-1,0,0)
scene.rootNode.addChildNode(sphere1Node)
scene.rootNode.addChildNode(sphere2Node)
Adding the custom SCNGeometry
// ----------------------------------------------------
// Example creating SCNGeometry using vertex data
struct Vertex {
let x: Float
let y: Float
let z: Float
let r: Float
let g: Float
let b: Float
}
let vertices: [Vertex] = [
Vertex(x: 0.0, y: 0.0, z: 0.0, r: 1.0, g: 0.0, b: 0.0),
Vertex(x: 1.0, y: 0.0, z: 0.0, r: 0.0, g: 0.0, b: 1.0)
]
let vertexData = Data(
bytes: vertices,
count: MemoryLayout<Vertex>.size * vertices.count
)
let positionSource = SCNGeometrySource(
data: vertexData,
semantic: SCNGeometrySource.Semantic.vertex,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<Vertex>.size
)
let colorSource = SCNGeometrySource(
data: vertexData,
semantic: SCNGeometrySource.Semantic.color,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: MemoryLayout<Float>.size * 3,
dataStride: MemoryLayout<Vertex>.size
)
let elements = SCNGeometryElement(
data: nil,
primitiveType: .point,
primitiveCount: vertices.count,
bytesPerIndex: MemoryLayout<Int>.size
)
elements.pointSize = 100
elements.minimumPointScreenSpaceRadius = 100
elements.maximumPointScreenSpaceRadius = 100
let spheres = SCNGeometry(sources: [positionSource, colorSource], elements: [elements])
let sphereNode = SCNNode(geometry: spheres)
let sphereMaterial = SCNMaterial()
sphereMaterial.lightingModel = .physicallyBased
spheres.materials = [sphereMaterial]
sphereNode.simdPosition = simd_float3(0,0,0)
scene.rootNode.addChildNode(sphereNode)
Some Exploration
Adding normals now shows the colours, but in all directions (i.e, there's no shadow).
And I've added a black SCNSphere() and a 3rd point to my VertexData, both using the same RGB values, but the black in the VertexData object appears too 'light'
let vertices: [Vertex] = [
Vertex(x: 0.0, y: 0.0, z: 0.0, r: 1.0, g: 0.0, b: 0.0),
Vertex(x: 1.0, y: 0.0, z: 0.0, r: 0.0, g: 0.0, b: 1.0),
Vertex(x: 0.0, y: 1.0, z: 0.0, r: 0.07, g: 0.11, b: 0.12)
]
let vertexData = Data(
bytes: vertices,
count: MemoryLayout<Vertex>.size * vertices.count
)
let normals = Array(repeating: SCNVector3(1,1,1), count: vertices.count)
let normalSource = SCNGeometrySource(normals: normals)
///
///
let spheres = SCNGeometry(
sources: [
positionSource,
normalSource,
colorSource
],
elements: [elements]
)



According to the documentation, making a custom geometry takes 3 steps.
SCNGeometrySourcethat contains the 3D shape's vertices.SCNGeometryElementthat contains an array of indices, showing how the vertices connect.SCNGeometrySourcesource andSCNGeometryElementinto aSCNGeometry.Let's start from step 1. You want your custom geometry to be a 3D shape, right? You only have 2 vertices, though.
This will form a line...
A common way of making 3D shapes is from triangles. Let's add 2 more vertices to make a pyramid.
Now, we need to convert the vertices into something that SceneKit can handle. In your current code, you convert
verticesintoData, then use theinit(data:semantic:vectorCount:usesFloatComponents:componentsPerVector:bytesPerComponent:dataOffset:dataStride:)initializer.This is very advanced and complicated. It's way easier with
init(vertices:).Now that you've got the
SCNGeometrySource, it's time for step 2 — connecting the vertices viaSCNGeometryElement. In your current code, you useinit(data:primitiveType:primitiveCount:bytesPerIndex:), then pass innil...If the data itself is
nil, how will SceneKit know how to connect your vertices? But anyway, there's once again an easier initializer:init(indices:primitiveType:). This takes in an array ofFixedWidthInteger, each representing a vertex back in yourpositionSource.So how is each vertex represented by a
FixedWidthInteger? Well, remember how you passed inverticesConverted, an array ofSCNVector3, topositionSource? SceneKit sees eachFixedWidthIntegeras an index and uses it accessverticesConverted.Since indices are always integers and positive,
UInt16should do fine (it conforms toFixedWidthInteger).The order here is very specific. By default, SceneKit only renders the front face of triangles, and in order to distinguish between the front and back, it relies on your ordering. The basic rule is: counterclockwise means front.
So to refer to the first triangle, you could say:
All are fine. Finally, step 3 is super simple. Just combine the
SCNGeometrySourceandSCNGeometryElement.And that's it! Now that both your
SCNGeometrySourceandSCNGeometryElementare set up correctly,lightingModelwill work properly.Notes:
SCNGeometrySources. The second one was to add color withSCNGeometrySource.Semantic.color, right? The simpler initializer that I used,init(vertices:), defaults to.vertex. If you want per-vertex color or something, you'll probably need to go back toinit(data:semantic:vectorCount:usesFloatComponents:componentsPerVector:bytesPerComponent:dataOffset:dataStride:).sceneView.autoenablesDefaultLighting = truefor some better lightingEdit: Single Vertex Sphere?
You shouldn't be using a single point to make a sphere. If you're going to do...
... then a 2D Circle is going to be the best you can get.
That's because, according to the
pointSizedocumentation:Since what's rendered is really just a circle that rotates to face you,
.physicallyBasedlighting won't work (.constantwill, but that's it). It's better to make your sphere with many small triangles, like the pyramid in the above answer. This is also what Apple does with their built in geometry, includingSCNSphere.