SceneKit – Custom geometry does not show up - swift

I should see 2 yellow triangles, but I see nothing.
class Terrain {
private class func createGeometry () -> SCNGeometry {
let sources = [
SCNGeometrySource(vertices:[
SCNVector3(x: -1.0, y: -1.0, z: 0.0),
SCNVector3(x: -1.0, y: 1.0, z: 0.0),
SCNVector3(x: 1.0, y: 1.0, z: 0.0),
SCNVector3(x: 1.0, y: -1.0, z: 0.0)], count:4),
SCNGeometrySource(normals:[
SCNVector3(x: 0.0, y: 0.0, z: -1.0),
SCNVector3(x: 0.0, y: 0.0, z: -1.0),
SCNVector3(x: 0.0, y: 0.0, z: -1.0),
SCNVector3(x: 0.0, y: 0.0, z: -1.0)], count:4)
]
let elements = [
SCNGeometryElement(indices: [0, 2, 3, 0, 1, 2], primitiveType: .Triangles)
]
let geo = SCNGeometry(sources:sources, elements:elements)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.yellowColor()
mat.doubleSided = true
geo.materials = [mat]
return geo
}
class func createNode () -> SCNNode {
let node = SCNNode(geometry: createGeometry())
node.name = "Terrain"
node.position = SCNVector3()
return node
}
}
I use it as follows:
let terrain = Terrain.createNode()
sceneView.scene?.rootNode.addChildNode(terrain)
let camera = SCNCamera()
camera.zFar = 10000
self.camera = SCNNode()
self.camera.camera = camera
self.camera.position = SCNVector3(x: -20, y: 15, z: 30)
let constraint = SCNLookAtConstraint(target: terrain)
constraint.gimbalLockEnabled = true
self.camera.constraints = [constraint]
sceneView.scene?.rootNode.addChildNode(self.camera)
I get other nodes with non-custom geometry which I see. What's wrong?

Hal Mueller is quite correct in that the indices involved must be a specified type, but it should be noted that this functionality has changed significantly in recent versions of the Swift language. Notably, SCNGeometryElement(indices:, primitiveType:) now functions perfectly well in Swift 4 and I would advise against using CInt which did not work for me. Instead use one of the standard integer types that conforms to the FixedWidthInteger protocol, i.e. Int32. If you know there's a maximum number of vertices involved in your mesh, use the smallest bit size you can that will encompass all of them.
Example:
let vertices = [
SCNVector3(x: 5, y: 4, z: 0),
SCNVector3(x: -5 , y: 4, z: 0),
SCNVector3(x: -5, y: -5, z: 0),
SCNVector3(x: 5, y: -5, z: 0)
]
let allPrimitives: [Int32] = [0, 1, 2, 0, 2, 3]
let vertexSource = SCNGeometrySource(vertices: vertices)
let element = SCNGeometryElement(indices: allPrimitives, primitiveType: .triangles)
let geometry = SCNGeometry(sources: [vertexSource], elements: [element])
SCNNode(geometry: geometry)
What's Happening Here?
First we create an array of vertices describing points in three-dimensional space. The allPrimitives array describes how those vertices link up. Each element is an index from the vertices array. Since we're using triangles, these should be considered in groups of three, one for each corner. For simplicity's sake I've done a simple flat square here. We then create a geometry source with the semantic type of vertices using the original array of all vertices, and a geometry element using the allPrimitives array, also informing it that they are triangles so it knows to group them in threes. These can then be used to create the SCNGeometry object with which we initialise our SCNNode.
An easy way to think about it is that the vertex source exists only to list all the vertices in the object. The geometry element exists only to describe how those vertices are linked up. It's the SCNGeometry that combines these two objects together to create the final physical representation.

Note: see Ash's answer, which is a much better approach for modern Swift than this one.
Your index array has the wrong size element. It's being inferred as [Int]. You need [CInt].
I broke out your elements setup into:
let indices = [0, 2, 3, 0, 1, 2] // [Int]
print(sizeof(Int)) // 8
print(sizeof(CInt)) // 4
let elements = [
SCNGeometryElement(indices: indices, primitiveType: .Triangles)
]
To get the indices to be packed like the expected C array, declare the type explicitly:
let indices: [CInt] = [0, 2, 3, 0, 1, 2]
Custom SceneKit Geometry in Swift on iOS not working but equivalent Objective C code does goes into more detail, but it's written against Swift 1, so you'll have to do some translation.
SCNGeometryElement(indices:, primitiveType:) doesn't appear to be documented anywhere, although it does appear in the headers.

Related

Rounding positional data from ARKit

I have this code that gets X, Y, Z positions from each frame in ARKit.
let CamPosition = SCNVector3(transform.m41, transform.m42, transform.m43)
How would I round the numbers down because they output occasionally in scientific notation like this?
SCNVector3(x: 7.276927e-09, y: 2.4679738e-09, z: 3.395949e-10)
Instead of the desired output like this:
SCNVector3(x: 0.026048008, y: 0.0069037788, z: 0.010655182)
Any help is greatly appreciated!
Rounding to Meters
For that you can use three holy methods: round(_:), and ceil(_:), and floor(_:).
import SceneKit
import Foundation
let node = SCNNode()
node.position = SCNVector3(x: floor(12.856288),
y: ceil(67.235459),
z: round(34.524305))
node.position.x // 12
node.position.y // 68
node.position.z // 35
Rounding XYZ values to integer, you make them to translate intermittently (discretely) in meters.
Rounding to Centimeters
Rounding XYZ values to 2 decimal places:
node.position = SCNVector3(x: round(12.856288 * 100) / 100.0,
y: round(67.235459 * 100) / 100.0,
z: round(34.524305 * 100) / 100.0)
node.position.x // 12.86 (hundredths)
node.position.y // 67.24
node.position.z // 34.52
It seems to me like you're trying to fix that doesn't need to be fixed.
As you can see from this very comprehensive answer, computers are just not good at storing some decimal numbers, so what you're getting is the nearest neighbour that can be represented in binary.
Unless this is causing you functional issues, I would recommend ignoring it. If the problem is that your debug logs have numbers that are not easy to parse, use a number formatter.

Metal/Swift - First element of vertex buffer has one wrong value

I'm just trying to render a red square using metal, and I'm creating a vertex buffer from an array of Vertex structures that look like this:
struct Vertex {
var position: SIMD3<Float>
var color: SIMD4<Float>
}
This is where I'm rendering the square:
var vertices: [Vertex] = [
Vertex(position: [-0.5, -0.5, 0], color: [1, 0, 0, 1]),
Vertex(position: [-0.5, 0.5, 0], color: [1, 0, 0, 1]),
Vertex(position: [0.5, -0.5, 0], color: [1, 0, 0, 1]),
Vertex(position: [0.5, 0.5, 0], color: [1, 0, 0, 1])
]
var vertexBuffer: MTLBuffer?
func render(using renderCommandEncoder: MTLRenderCommandEncoder) {
if self.vertexBuffer == nil {
self.vertexBuffer = self.device.makeBuffer(
bytes: self.vertices,
length: MemoryLayout<Vertex>.stride * self.vertices.count,
options: []
)
}
if let vertexBuffer = self.vertexBuffer {
renderCommandEncoder.setRenderPipelineState(RenderPipelineStates.defaultState)
renderCommandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderCommandEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexBuffer.length / MemoryLayout<Vertex>.stride)
}
}
This is what my render pipeline state looks like:
let library = device.makeDefaultLibrary()!
let vertexShader = library.makeFunction(name: "basicVertexShader")
let fragmentShader = library.makeFunction(name: "basicFragmentShader")
let renderPipelineDescriptor = MTLRenderPipelineDescriptor()
renderPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
renderPipelineDescriptor.vertexFunction = vertexShader
renderPipelineDescriptor.fragmentFunction = fragmentShader
renderPipelineDescriptor.sampleCount = 4
let vertexDescriptor = MTLVertexDescriptor()
vertexDescriptor.attributes[0].format = .float3
vertexDescriptor.attributes[0].bufferIndex = 0 // Position
vertexDescriptor.attributes[0].offset = 0
vertexDescriptor.attributes[1].format = .float4
vertexDescriptor.attributes[1].bufferIndex = 0 // Color
vertexDescriptor.attributes[1].offset = MemoryLayout<SIMD3<Float>>.stride
vertexDescriptor.layouts[0].stride = MemoryLayout<Vertex>.stride
renderPipelineDescriptor.vertexDescriptor = vertexDescriptor
self.defaultState = try! device.makeRenderPipelineState(descriptor: renderPipelineDescriptor)
The vertex and fragment shaders just pass through the position and color. For some reason, when this is rendered the first float of the color of the first vertex comes into the vertex shader as an extremely small value, effectively showing black. It only happens for the red value of the first vertex in the array.
Red square with one black vertex
I can see from debugging the GPU frame that the first vertex has a red color component of 5E-41 (essentially 0).
I have no idea why this is the case, it happens some time when the vertices are added to the vertex buffer. I'm guessing it has something to do with my render pipeline vertex descriptor, but I haven't been able to figure out what's wrong. Thanks for any help!
This is, with high likelihood, a duplicate of this question. I'd encourage you to consider the workarounds there, and also to file your own feedback to raise visibility of this bug. - warrenm
Correct, this appears to be a driver bug of some sorts. I fixed it by adding the cpuCacheModeWriteCombined option to makeBuffer and have filed feedback.
self.vertexBuffer = self.device.makeBuffer(
bytes: self.vertices,
length: MemoryLayout<Vertex>.stride * self.vertices.count,
options: [.cpuCacheModeWriteCombined]
)

Identify the top left, top right, bottom left and bottom right position of a rectangle in Swift (elegant solution)

I'd like to find an elegant solution to identify the corners of a rectangle given a list of points (that I'm sure will define a rectangle).
Let's say we have this array of CGPoint:
var points:[CGPoint] = []
points.append(CGPoint(x:1, y:0)) //TL
points.append(CGPoint(x:3, y:0)) //TR
points.append(CGPoint(x:1, y:2)) //BL
points.append(CGPoint(x:3, y:2)) //BR
Which would be an elegant solution to understand that TopLeft corner is at index 0, Top Right at index 1... and so on?
I could cycle through the array multiple times and find it using a comparison... can you think at a better solution maybe using sort or filter ?
EDIT: Please note that the points array is unordered. I don't have a precise sequence of points.
These are Core Graphics structs, so ask Core Graphics to help you. Construct a path from any point through each of the other points in any order and ask for its bounding box. Now you have a CGRect whose corner points are your points, but now you know which is which, and matching them up to yours is trivial.
Example:
var points:[CGPoint] = []
points.append(CGPoint(x:1, y:0)) //TL
points.append(CGPoint(x:3, y:0)) //TR
points.append(CGPoint(x:1, y:2)) //BL
points.append(CGPoint(x:3, y:2)) //BR
let path = CGMutablePath()
path.move(to: points[0])
for ix in 1...3 {path.addLine(to: points[ix])}
let rect = path.boundingBox
The answer is CGRect(x:1.0, y:0.0, width:2.0, height:2.0) and now you know its minX, minY, maxX, and maxY and can easily match those up to your original points.
And you get the same result regardless of the order in which the points were supplied.
You can use map / reduce to achieve relatively simple syntax. Assuming:
var points: [CGPoint] = []
points.append(CGPoint(x: 3, y: 6))
points.append(CGPoint(x: 4, y: 6))
points.append(CGPoint(x: 4, y: 2))
points.append(CGPoint(x: 3, y: 2))
You can then:
let minX = points.map { $0.x } .reduce(points[0].x) { min($0,$1) }
At which point minX = 3. You could also use a sorted(by:)
let minX = points.sorted { $0.x < $1.x }.first!.x
Both have the advantages of working on any shape built from points. Matt also suggested using min() on the array, which looks like this:
let minX = points.min { $0.x < $1.x }!.x
I guess that's as clean as can be.
Oh, one more for the books... if you are going to be converting points to CGRect in a lot of places.. you can create yourself a CGRect extension:
extension CGRect {
init(from points: [CGPoint]) {
let xAxis = points.sorted { $0.x < $1.x }
let yAxis = points.sorted { $0.y < $1.y }
self.init(x: xAxis.first!.x, y: yAxis.first!.y, width: xAxis.last!.x - xAxis.first!.x, height: yAxis.last!.y - yAxis.first!.y)
}
}
Which you can then use with:
let rect = CGRect(from: points)
// rect.minX
Cheers!

SceneKit pass uniform vector to shader modifiers

I'm trying to pass a GLKVector4 to a shader that should receive it as a vec4. I'm using a fragment shader modifier:
material.shaderModifiers = [ SCNShaderModifierEntryPoint.fragment: shaderModifier ]
where shaderModifier is:
// color changes
uniform float colorModifier;
uniform vec4 colorOffset;
vec4 color = _output.color;
color = color + colorOffset;
color = color + vec4(0.0, colorModifier, 0.0, 0.0);
_output.color = color;
(I'm simply adding a color offset) I've tried:
material.setValue(GLKVector4(v: (250.0, 0.0, 0.0, 0.0)), "colorOffset")
which doesn't work (no offset is added and the shader uses the default value that is (0, 0, 0, 0)). Same happens if I replace GLKVector4 by SCNVector4
Following this I've also tried:
let points: [float2] = [float2(250.0), float2(0.0), float2(0.0), float2(0.0)]
material.setValue(NSData(bytes: points, length: points.count * sizeof(float2)), "colorOffset")
However, I can pass a float value to the uniform colorModifier easily by doing:
material.setValue(250.0, forKey: "colorModifier")
and that will increase the green channel as excepted
So you have to use NSValue, that has a convenience initialization for SCNVector4, so:
let v = SCNVector4(x: 250.0, y: 0.0, z: 0.0, w: 0.0)
material.setValue(NSValue(scnVector4: v), "colorOffset")
It'd be too good if SceneKit could handle it's own types directly...

SceneKit – Rotate and animate a SCNNode

I'm trying to display a pyramid that points following the z axis and then rotates on itself around z too.
As my camera is on the z axis, I'm expecting to see the pyramid from above. I managed to rotate the pyramid to see it this way but when I add the animation it seems to rotate on multiple axis.
Here is my code:
// The following create the pyramid and place it how I want
let pyramid = SCNPyramid(width: 1.0, height: 1.0, length: 1.0)
let pyramidNode = SCNNode(geometry: pyramid)
pyramidNode.position = SCNVector3(x: 0, y: 0, z: 0)
pyramidNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(M_PI / 2))
scene.rootNode.addChildNode(pyramidNode)
// But the animation seems to rotate aroun 2 axis and not just z
var spin = CABasicAnimation(keyPath: "rotation")
spin.byValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: 2*Float(M_PI)))
spin.duration = 3
spin.repeatCount = HUGE
pyramidNode.addAnimation(spin, forKey: "spin around")
Trying to both manually set and animate the same property can cause issues. Using a byValue animation makes the problem worse -- that concatenates to the current transform, so it's harder to keep track of whether the current transform is what the animation expects to start with.
Instead, separate the fixed orientation of the pyramid (its apex is in the -z direction) from the animation (it spins around the axis it points in). There's two good ways to do this:
Make pyramidNode the child of another node that gets the one-time rotation (π/2 around x-axis), and apply the spin animation directly to pyramidNode. (In this case, the apex of the pyramid will still point in the +y direction of its local space, so you'll want to spin around that axis instead of the z-axis.)
Use the pivot property to transform the local space of pyramidNode's contents, and animate pyramidNode relative to its containing space.
Here's some code to show the second approach:
let pyramid = SCNPyramid(width: 1.0, height: 1.0, length: 1.0)
let pyramidNode = SCNNode(geometry: pyramid)
pyramidNode.position = SCNVector3(x: 0, y: 0, z: 0)
// Point the pyramid in the -z direction
pyramidNode.pivot = SCNMatrix4MakeRotation(CGFloat(M_PI_2), 1, 0, 0)
scene.rootNode.addChildNode(pyramidNode)
let spin = CABasicAnimation(keyPath: "rotation")
// Use from-to to explicitly make a full rotation around z
spin.fromValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: 0))
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: CGFloat(2 * M_PI)))
spin.duration = 3
spin.repeatCount = .infinity
pyramidNode.addAnimation(spin, forKey: "spin around")
Some unrelated changes to improve code quality:
Use CGFloat when explicit conversion is required to initialize an SCNVector component; using Float or Double specifically will break on 32 or 64 bit architecture.
Use .infinity instead of the legacy BSD math constant HUGE. This type-infers to whatever the type of spin.repeatCount is, and uses a constant value that's defined for all floating-point types.
Use M_PI_2 for π/2 to be pedantic about precision.
Use let instead of var for the animation, since we never assign a different value to spin.
More on the CGFloat error business: In Swift, numeric literals have no type until the expression they're in needs one. That's why you can do things like spin.duration = 3 -- even though duration is a floating-point value, Swift lets you pass an "integer literal". But if you do let d = 3; spin.duration = d you get an error. Why? Because variables/constants have explicit types, and Swift doesn't do implicit type conversion. The 3 is typeless, but when it gets assigned to d, type inference defaults to choosing Int because you haven't specified anything else.
If you're seeing type conversion errors, you probably have code that mixes literals, constants, and/or values returned from functions. You can probably just make the errors go away by converting everything in the expression to CGFloat (or whatever the type you're passing that expression to is). Of course, that'll make your code unreadable and ugly, so once you get it working you might start removing conversions one at a time until you find the one that does the job.
SceneKit includes animation helpers which are much simpler & shorter to use than CAAnimations. This is ObjC but gets across the point:
[pyramidNode runAction:
[SCNAction repeatActionForever:
[SCNAction rotateByX:0 y:0 z:2*M_PI duration:3]]];
I changed byValue to toValue and this worked for me. So change the line...
spin.byValue = NSValue(SCNVector4: SCNVector4(...
Change it to...
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 0, y:0, z:1, w: 2*float(M_PI))