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.
Create a SCNGeometrySource that contains the 3D shape's vertices.
Create a SCNGeometryElement that contains an array of indices, showing how the vertices connect.
Combine the SCNGeometrySource source and SCNGeometryElement into a SCNGeometry.
Let's start from step 1. You want your custom geometry to be a 3D shape, right? You only have 2 vertices, though.
let vertices: [Vertex] = [ /// what's `r`, `g`, `b` for btw?
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)
]
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.
let vertices: [Vertex] = [
Vertex(x: 0.0, y: 0.0, z: 0.0, r: 1.0, g: 0.0, b: 0.0), /// vertex 0
Vertex(x: 1.0, y: 0.0, z: 0.0, r: 0.0, g: 0.0, b: 1.0), /// vertex 1
Vertex(x: 1.0, y: 0.0, z: -0.5, r: 0.0, g: 0.0, b: 1.0), /// vertex 2
Vertex(x: 0.0, y: 1.0, z: 0.0, r: 0.0, g: 0.0, b: 1.0), /// vertex 3
]
Now, we need to convert the vertices into something that SceneKit can handle. In your current code, you convert vertices into Data, then use the init(data:semantic:vectorCount:usesFloatComponents:componentsPerVector:bytesPerComponent:dataOffset:dataStride:) initializer.
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
)
This is very advanced and complicated. It's way easier with init(vertices:).
let verticesConverted = vertices.map { SCNVector3($0.x, $0.y, $0.z) } /// convert to `[SCNVector3]`
let positionSource = SCNGeometrySource(vertices: verticesConverted)
Now that you've got the SCNGeometrySource, it's time for step 2 — connecting the vertices via SCNGeometryElement. In your current code, you use init(data:primitiveType:primitiveCount:bytesPerIndex:), then pass in nil...
let elements = SCNGeometryElement(
data: nil,
primitiveType: .point,
primitiveCount: vertices.count,
bytesPerIndex: MemoryLayout<Int>.size
)
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 of FixedWidthInteger, each representing a vertex back in your positionSource.
So how is each vertex represented by a FixedWidthInteger? Well, remember how you passed in verticesConverted, an array of SCNVector3, to positionSource? SceneKit sees each FixedWidthInteger as an index and uses it access verticesConverted.
Since indices are always integers and positive, UInt16 should do fine (it conforms to FixedWidthInteger).
/// pairs of 3 indices, each representing a vertex
let indices: [UInt16] = [
0, 1, 3, /// front triangle
1, 2, 3, /// right triangle
2, 0, 3, /// back triangle
3, 0, 2, /// left triangle
0, 2, 1 /// bottom triangle
]
let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
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:
0, 1, 3
1, 3, 0
3, 0, 1
All are fine. Finally, step 3 is super simple. Just combine the SCNGeometrySource and SCNGeometryElement.
let geometry = SCNGeometry(sources: [positionSource], elements: [element])
And that's it! Now that both your SCNGeometrySource and SCNGeometryElement are set up correctly, lightingModel will work properly.
/// add some color
let material = SCNMaterial()
material.diffuse.contents = UIColor.orange
material.lightingModel = .physicallyBased
geometry.materials = [material]
/// add the node
let node = SCNNode(geometry: geometry)
scene.rootNode.addChildNode(node)
Notes:
I noticed that you were trying to use 2 SCNGeometrySources. The second one was to add color with SCNGeometrySource.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 to init(data:semantic:vectorCount:usesFloatComponents:componentsPerVector:bytesPerComponent:dataOffset:dataStride:).
Try sceneView.autoenablesDefaultLighting = true for some better lighting
Full demo playground here
Edit: Single Vertex Sphere?
You shouldn't be using a single point to make a sphere. If you're going to do...
elements.pointSize = 100
elements.minimumPointScreenSpaceRadius = 100
elements.maximumPointScreenSpaceRadius = 100
... then a 2D Circle is going to be the best you can get.
That's because, according to the pointSize documentation:
SceneKit can render each point as a small 2D surface that always faces the camera. By applying a texture or custom shader to that surface, you can efficiently render many small objects at once.
Since what's rendered is really just a circle that rotates to face you, .physicallyBased lighting won't work (.constant will, 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, including SCNSphere.
let sphere = SCNSphere(radius: 1)
let sphereMaterial = SCNMaterial()
sphereMaterial.diffuse.contents = UIColor.purple
sphereMaterial.fillMode = .lines /// add this to see the triangles
sphereMaterial.lightingModel = .physicallyBased
sphere.materials = [sphereMaterial]
let sphereNode = SCNNode(geometry: sphere)
scene.rootNode.addChildNode(sphereNode)
i trying to add a node (pin) to sphere node (when taping on sphere node), but can't correctly calculate an angle, can u help me pls ?
private func addPinToSphere(result: SCNHitTestResult) {
guard let scene = SCNScene(named: "art.scnassets/tree.scn") else { return }
guard let treeNode = scene.rootNode.childNode(withName: "Trunk", recursively: true) else { return }
let x = CGFloat(result.worldCoordinates.x)
let y = CGFloat(result.worldCoordinates.y)
treeNode.position = result.worldCoordinates
treeNode.eulerAngles = SCNVector3((x) * .pi / CGFloat(180), 0, (y - 90) * .pi / CGFloat(180)) // y axis is ignorable
result.node.addChildNode(treeNode)
}
when run code i have this
but want like this
You can do it similar to this code. This is a missile launcher, but it's basically the same thing. Your pin = my fireTube, so create a base node, then add a subnode with your materials to it and rotate it into place. You'll have to experiment with where to place the tube initially, but once you get it into the right place the lookat constraint will always point it in the right direction.
case .d1, .d2, .d3, .d4, .d5, .d6, .d7, .d8, .d9, .d10:
let BoxGeometry = SCNBox(width: 0.8, height: 0.8, length: 0.8, chamferRadius: 0.0)
let vNode = SCNNode(geometry: BoxGeometry)
BoxGeometry.materials = setDefenseTextures(vGameType: vGameType)
let tubeGeometry = SCNTube(innerRadius: 0.03, outerRadius: 0.05, height: 0.9)
let fireTube = SCNNode(geometry: tubeGeometry)
tubeGeometry.firstMaterial?.diffuse.contents = data.getTextureColor(vTheme: 0, vTextureType: .barrelColor)
fireTube.position = SCNVector3(0, 0.2, -0.3)
let vRotateX = SCNAction.rotateBy(x: CGFloat(Float(GLKMathDegreesToRadians(-90))), y: 0, z: 0, duration: 0)
fireTube.runAction(vRotateX)
vNode.addChildNode(fireTube)
return vNode
Then set target on your base node and your subnode will rotate with it:
func setTarget()
{
node.constraints = []
let vConstraint = SCNLookAtConstraint(target: targetNode)
vConstraint.isGimbalLockEnabled = true
node.constraints = [vConstraint]
}
In your case, target equals center mass of your sphere and the pin will always point to it, provided you built and aligned your box and pin correctly.
I am trying to render a collection of points in SceneKit all of which have an alpha value less than 1 as part of their colour. While this seems to work, it depends on the angle at which you are looking at the point.
My current method has been to create an array of vertices with corresponding colours and indices, wrap these up inside SCNGeometrySource/SCNGeometryElement and create a custom SCNGeometry from theses. The SCNGeometryElement for the indices is set to render as points. Some sample code below of how I'm setting this up.
var vertices = [SCNVector3]()
var colors = [float4]()
var indices = [UInt32]()
for x in 0...10 {
for y in 0...10 {
for z in 0...10 {
vertices.append(SCNVector3(x - 5, y - 5, z - 5))
colors.append(float4(0.7, 0.7, 0.7, 0.1))
indices.append(UInt32(vertices.count - 1))
}
}
}
let vertexSource = SCNGeometrySource(vertices: vertices)
let colorsData = Data(bytes: &colors, count: colors.count * MemoryLayout<float4>.stride)
let colorSource = SCNGeometrySource(data: colorsData, semantic: .color, vectorCount: colors.count, usesFloatComponents: true, componentsPerVector: 4, bytesPerComponent: MemoryLayout<Float>.stride, dataOffset: 0, dataStride: MemoryLayout<float4>.stride)
let pointsElement = SCNGeometryElement(indices: indices, primitiveType: .point)
pointsElement.pointSize = 0.2
pointsElement.minimumPointScreenSpaceRadius = 1
pointsElement.maximumPointScreenSpaceRadius = 50
let geometry = SCNGeometry(sources: [vertexSource, colorSource], elements: [pointsElement])
let node = SCNNode(geometry: geometry)
scene.rootNode.addChildNode(node)
When my camera node is located as follow, everything looks good:
cameraNode.eulerAngles = SCNVector3(0, 0.5 * CGFloat.pi, 0)
cameraNode.position = SCNVector3(x: 10, y: 0, z: 0)
However if I move my camera to the next face of the cube clockwise the alpha no longer renders:
cameraNode.eulerAngles = SCNVector3(0, -0.5 * CGFloat.pi, 0)
cameraNode.position = SCNVector3(x: -10, y: 0, z: 0)
Placing the camera half way gives a combination of the two:
cameraNode.eulerAngles = SCNVector3(0, 0, 0)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
I want to translate a plane without rotating the image. For any reason my image is being rotated.
var translation = matrix_identity_float4x4
translation.colum = -0.2
let transform = simd_mul(currentFrame.camera.transform, translation)
planeNode.simdWorldTransform = matrix_multiply(currentFrame.camera.transform, translation)
Also, I notice that matrix_identity_float4x4 contains 4 columns but the documentation is not available.
Why 4 columns? Are there the frame of the plane?
The simplest way to do it is to use the following code for positioning:
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 20, height: 20)
// At first we need to rotate a plane about its x axis in radians:
planeNode.rotation = SCNVector4(1, 0, 0, -Double.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
planeNode.position.x = 10
planeNode.position.z = 10
// planeNode.position = SCNVector3(x: 10, y: 0, z: 10)
scene.rootNode.addChildNode(planeNode)
or this way:
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
scene.rootNode.addChildNode(cameraNode)
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 20, height: 20)
planeNode.rotation = SCNVector4(1, 0, 0, -Double.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
let distance: Float = 50
planeNode.simdPosition = cameraNode.simdWorldFront * distance // -Z axis
planeNode.simdPosition = cameraNode.simdWorldRight * distance // +X axis
scene.rootNode.addChildNode(planeNode)
If you wanna know more about matrices used in ARKit and SceneKit frameworks just look at Figure 1-8 Matrix configurations for common transformations.
Hope this helps.