Get scale, translation and rotation from CATransform3D - swift

Given a CATransform3D transform, I want to extract the scale, translation and rotation as separate transforms. From some digging, I was able to accomplish this for CGAffineTransform in Swift, like so:
extension CGAffineTransform {
var scaleDelta:CGAffineTransform {
let xScale = sqrt(a * a + c * c)
let yScale = sqrt(b * b + d * d)
return CGAffineTransform(scaleX: xScale, y: yScale)
}
var rotationDelta:CGAffineTransform {
let rotation = CGFloat(atan2f(Float(b), Float(a)))
return CGAffineTransform(rotationAngle: rotation)
}
var translationDelta:CGAffineTransform {
return CGAffineTransform(translationX: tx, y: ty)
}
}
How would one do something similar for CATransform3D using math? (I am looking for a solution that doesn't use keypaths.)
(implementation or math-only answers at your discretion)

If you're starting from a proper affine matrix that can be decomposed correctly (if not unambiguously) into a sequence of scale, rotate, translate, this method will perform the decomposition into a tuple of vectors representing the translation, rotation (Euler angles), and scale components:
extension CATransform3D {
func decomposeTRS() -> (float3, float3, float3) {
let m0 = float3(Float(self.m11), Float(self.m12), Float(self.m13))
let m1 = float3(Float(self.m21), Float(self.m22), Float(self.m23))
let m2 = float3(Float(self.m31), Float(self.m32), Float(self.m33))
let m3 = float3(Float(self.m41), Float(self.m42), Float(self.m43))
let t = m3
let sx = length(m0)
let sy = length(m1)
let sz = length(m2)
let s = float3(sx, sy, sz)
let rx = m0 / sx
let ry = m1 / sy
let rz = m2 / sz
let pitch = atan2(ry.z, rz.z)
let yaw = atan2(-rx.z, hypot(ry.z, rz.z))
let roll = atan2(rx.y, rx.x)
let r = float3(pitch, yaw, roll)
return (t, r, s)
}
}
To show that this routine correctly extracts the various components, construct a transform and ensure that it decomposes as expected:
let rotationX = CATransform3DMakeRotation(.pi / 2, 1, 0, 0)
let rotationY = CATransform3DMakeRotation(.pi / 3, 0, 1, 0)
let rotationZ = CATransform3DMakeRotation(.pi / 4, 0, 0, 1)
let translation = CATransform3DMakeTranslation(1, 2, 3)
let scale = CATransform3DMakeScale(0.1, 0.2, 0.3)
let transform = CATransform3DConcat(CATransform3DConcat(CATransform3DConcat(CATransform3DConcat(scale, rotationX), rotationY), rotationZ), translation)
let (T, R, S) = transform.decomposeTRS()
print("\(T), \(R), \(S))")
This produces:
float3(1.0, 2.0, 3.0), float3(1.5708, 1.0472, 0.785398), float3(0.1, 0.2, 0.3))
Note that this decomposition assumes an Euler multiplication order of XYZ, which is only one of several possible orderings.
Caveat: There are certainly values for which this method is not numerically stable. I haven't tested it extensively enough to know where these pitfalls lie, so caveat emptor.

For symmetry with the CGAffineTransform extension in my question, here is the CATransform3D extension that provides the "deltas" for scale, translation and rotation, based on Warren's decomposeTRS, which I have marked as the accepted answer.
extension CATransform3D {
var scaleDelta:CATransform3D {
let s = decomposeTRS().2
return CATransform3DMakeScale(CGFloat(s.x), CGFloat(s.y), CGFloat(s.z))
}
var rotationDelta:CATransform3D {
let r = decomposeTRS().1
let rx = CATransform3DMakeRotation(CGFloat(r.x), 1, 0, 0)
let ry = CATransform3DMakeRotation(CGFloat(r.y), 0, 1, 0)
let rz = CATransform3DMakeRotation(CGFloat(r.z), 0, 0, 1)
return CATransform3DConcat(CATransform3DConcat(rx, ry), rz)
}
var translationDelta:CATransform3D {
let t = decomposeTRS().0
return CATransform3DMakeTranslation(CGFloat(t.x), CGFloat(t.y), CGFloat(t.z))
}
}

Related

Generate random Gaussian noise MTLTexture or MTLBuffer of size (width, height)

I am writing a real-time video filter application and for one of the algorithms I want to try out, I need to generate a random, gaussian univariate distributed buffer (or texture) based on the input source.
Coming from a Python background, the following few lines are running in about 0.15s (which is not real-time worthy but a lot faster than the Swift code I tried below):
h = 1170
w = 2532
with Timer():
noise = np.random.normal(size=w * h * 3)
plt.imshow(noise.reshape(w,h,3))
plt.show()
My Swift code try:
private func generateNoiseTextureBuffer(width: Int, height: Int) -> [Float] {
let w = Float(width)
let h = Float(height)
var noiseData = [Float](repeating: 0, count: width * height * 4)
for xi in (0 ..< width) {
for yi in (0 ..< height) {
let index = yi * width + xi
let x = Float(xi)
let y = Float(yi)
let random = GKRandomSource()
let gaussianGenerator = GKGaussianDistribution(randomSource: random, mean: 0.0, deviation: 1.0)
let randX = gaussianGenerator.nextUniform()
let randY = gaussianGenerator.nextUniform()
let scale = sqrt(2.0 * min(w, h) * (2.0 / Float.pi))
let rx = floor(max(min(x + scale * randX, w - 1.0), 0.0))
let ry = floor(max(min(y + scale * randY, h - 1.0), 0.0))
noiseData[index * 4 + 0] = rx + 0.5
noiseData[index * 4 + 1] = ry + 0.5
noiseData[index * 4 + 2] = 1
noiseData[index * 4 + 3] = 1
}
}
return noiseData
}
...
let noiseData = self.generateNoiseTextureBuffer(width: context.sourceColorTexture.width, height: context.sourceColorTexture.height)
let noiseDataSize = noiseData.count * MemoryLayout.size(ofValue: noiseData[0])
self.noiseBuffer = device.makeBuffer(bytes: noiseData, length: noiseDataSize)
How can I accomplish this fast and easily in Swift?

Swift SIMD rounding errors in making 90 degree rotation matrix with sin, cos

I'm following this guide about working with matrices with the accelerate framework.
There they using something similar to this to rotate a vector:
func makeRotationMatrix(angle: Float) -> simd_float3x3 {
let rows = [
simd_float3(cos(angle), -sin(angle), 0),
simd_float3(sin(angle), cos(angle), 0),
simd_float3(0, 0, 1)
]
return float3x3(rows: rows)
}
let vector = simd_float3(x: 1, y: 1, z: 1)
let angle = Measurement(value: 180, unit: UnitAngle.degrees)
let radians = Float(angle.converted(to: .radians).value)
let rotationMatrix = makeRotationMatrix(angle: radians)
let rotatedVector = rotationMatrix * vector
print("vector:", vector) // SIMD3<Float>(1.0, 1.0, 1.0)
print("angle:", angle) // 180.0 °
print("radians:", radians) // 3.1415927
print("rotatedVector:", rotatedVector) // SIMD3<Float>(-0.99999994, -1.0000001, 1.0)
I expected the x of the rotated vector to be -1 instead of -0.99999994. I guess this is caused by the radians being a float? We could correct for this by rounding by hand:
let correctedVector = simd_float3(
x: rotatedVector.x.rounded(),
y: rotatedVector.y.rounded(),
z: rotatedVector.z.rounded()
)
print("correctedVector:", correctedVector) // SIMD3<Float>(-1.0, -1.0, 1.0)
But I'm wondering if there is a way to rotate this vector without rounding errors?

SCNNode SCNAction.move(to:) not running but being called - Swift

I am aiming to move the inhabitants in a scene randomly around inside a bounded area. Every 3 seconds the x, y, and z positions of the SCNVector3 are changed then the node moves to those points.
The problem im having is the nodes are not moving to those points. The points change as seen in the print("running: x\(self.x) y\(self.y) z\(self.z) on - \(node.name!)") line. I assume the SCNAction.move(to:) action is being called but for some reason that line does nothing.
No errors are being thrown the nodes just spawn at the random point and never move.
This is the code I have to achive the current movements:
var x = Float.random(in: -0.25...0.265)
var y:Float = 0
var z = Float.random(in: -0.3...0.215)
func spawnInhabitants() {
let type = skyBalls[_index].getInhabitantType().rawValue
let scene = SCNScene(named: "art.scnassets/inhabitants/\(type).scn")!
let inhabitantNode:SCNNode = scene.rootNode.childNode(withName: type, recursively: false)!
inhabitantNode.scale = SCNVector3(0.000075, 0.000075, 0.000075)
inhabitantNode.physicsBody?.physicsShape = SCNPhysicsShape(node: inhabitantNode)
inhabitantNode.physicsBody?.type = .dynamic
inhabitantNode.physicsBody?.isAffectedByGravity = false
inhabitantNode.physicsBody?.categoryBitMask = 1
inhabitantNode.physicsBody?.collisionBitMask = 6
inhabitantNode.physicsBody?.contactTestBitMask = 1
for inhab in skyBalls[_index].getInhabitants() {
x = Float.random(in: -0.25...0.265)
y = 0
z = Float.random(in: -0.3...0.215)
inhabitantNode.position = SCNVector3(x, y, z)
let move_around = SCNAction.run { node in
self.x = Float.random(in: -0.25...0.265)
self.y = 0
self.z = Float.random(in: -0.3...0.215)
print("running: x\(self.x) y\(self.y) z\(self.z) on - \(node.name!)")
}
let move = SCNAction.move(to: SCNVector3(x, y, z), duration: 3) //<- This is the problem
let seq = SCNAction.sequence([move_around, move])
let i = skyBalls[_index].getInhabitants().firstIndex(of: inhab)
inhabitantNode.name = "\(type)\(i!)"
inhabitantNode.runAction(SCNAction.repeatForever(seq))
colonyScene.rootNode.addChildNode(inhabitantNode.copy() as! SCNNode)
}
}
As a note:
skyBalls[_index].getInhabitantType().rawValue
^Returns a String of the enum type for the inhabitant (mostly used for naming)
skyBalls[_index].getInhabitants()
^returns the list of inhabitants form the instance of skyBalls and an index. more specifically an array of inhabitants.
colonyScene
^This is the SCNScene for the view.
Thanks in advance.
Your move action might be using x, y, z values at the time the action is created. So you instead might try to create a new action inside of the run action after generating random values... (I simplified your code snippet to test this approach)
for inhab in skyBalls {
inhab.position = SCNVector3(x, y, z)
let move_around = SCNAction.run { node in
self.x = Float.random(in: -0.25...0.265)
self.y = 0
self.z = Float.random(in: -0.3...0.215)
print("running: x\(self.x) y\(self.y) z\(self.z) on - \(node.name)")
let moveAction = SCNAction.move(to: SCNVector3(self.x, self.y, self.z), duration: 3)
node.runAction(moveAction)
}
let wait = SCNAction.wait(duration: 3) // make same as moveAction duration
//let move = SCNAction.move(to: SCNVector3(x, y, z), duration: 3) //<- This is the problem
let seq = SCNAction.sequence([move_around, wait])
inhab.runAction(SCNAction.repeatForever(seq))
scnView.scene!.rootNode.addChildNode(inhab)
}
Hope this helps

Procedural mesh not rendering lighting [SceneKit - Xcode]

I am quite new to swift and Xcode however, I have been programming in other languages for several years. I am trying to procedurally create a 3D mesh in SceneKit (iOS). My code works as expected however, when running the application the generated object renders a flat black colour, ignoring all lighting. I have also added a cube to the scene to show that the scene lighting is working.
I would imagine that there is either a problem with the shader or that I need to define the normals of the geometry to fix this. I have tried playing around with a few properties of the SCNMaterial, but they don't seem to change anything.
If it is just a case of defining the normals, please could you advise how I would do this in Swift / SceneKit. Or perhaps I have missed something else, any help would be much appreciated.
Screenshot below:
My code below:
public static func CreateMesh (size: CGFloat, resolution: CGFloat) -> SCNNode? {
let axisCount = Int(floor(size / resolution))
let bottomLeft = CGVector(
dx: CGFloat(-(axisCount / 2)) * resolution,
dy: CGFloat(-(axisCount / 2)) * resolution
)
var verts = Array(
repeating: Array(
repeating: (i: Int(0), pos: SCNVector3.init(x: 0, y: 0, z: 0)),
count: axisCount),
count: axisCount
)
var vertsStream = [SCNVector3]()
var i : Int = 0
for x in 0...axisCount-1 {
for y in 0...axisCount-1 {
verts[x][y] = (
i,
SCNVector3(
x: Float(bottomLeft.dx + CGFloat(x) * resolution),
y: Float.random(in: 0..<0.1),
z: Float(bottomLeft.dy + CGFloat(y) * resolution)
)
)
vertsStream.append(verts[x][y].pos)
i += 1
}
}
var tris = [(a: Int, b: Int, c: Int)]()
var trisStream = [UInt16]()
for x in 0...axisCount - 2 {
for y in 0...axisCount - 2 {
// Quad
tris.append((
a: verts[x][y].i,
b: verts[x][y+1].i,
c: verts[x+1][y+1].i
))
tris.append((
a: verts[x+1][y+1].i,
b: verts[x+1][y].i,
c: verts[x][y].i
))
}
}
for t in tris {
trisStream.append(UInt16(t.a))
trisStream.append(UInt16(t.b))
trisStream.append(UInt16(t.c))
}
// Create scene element
let geometrySource = SCNGeometrySource(vertices: vertsStream)
let geometryElement = SCNGeometryElement(indices: trisStream, primitiveType: .triangles)
let geometryFinal = SCNGeometry(sources: [geometrySource], elements: [geometryElement])
let node = SCNNode(geometry: geometryFinal)
////////////////////////
// FIX MATERIAL
////////////////////////
let mat = SCNMaterial()
mat.diffuse.intensity = 1
mat.lightingModel = .blinn
mat.blendMode = .replace
node.geometry?.materials = [mat]
return node
}
After a lot of searching I managed to find a post with a line of code that looks something like this:
let gsNormals = SCNGeometrySource(normals: normalStream)
So from there I managed to work out how to set the surface normals. It seems like there really isn't a lot of online content / learning material when it comes to the more advanced topics like this in Xcode / Swift, which is quite unfortunate.
I have set it up to create a parabolic shape plane, just for testing. But this code will be used to generate a mesh from a height map, which should now be easy to implement. I think it's pretty useful code, so I have included it below incase anyone else ever has the same issue that I did.
public static func CreateMesh (size: CGFloat, resolution: CGFloat) -> SCNNode? {
let axisCount = Int(floor(size / resolution))
let bottomLeft = CGVector(
dx: CGFloat(-(axisCount / 2)) * resolution,
dy: CGFloat(-(axisCount / 2)) * resolution
)
/// Verticies ///
var verts = Array(
repeating: Array(
repeating: (i: Int(0), pos: SCNVector3.init(x: 0, y: 0, z: 0)),
count: axisCount),
count: axisCount
)
var vertsStream = [SCNVector3]()
var i = 0
for x in 0...axisCount - 1 {
for y in 0...axisCount - 1 {
var dx = axisCount / 2 - x
dx = dx * dx
var dy = axisCount / 2 - y
dy = dy * dy
let yVal = Float(Double(dx + dy) * 0.0125)
verts[x][y] = (
i: i,
pos: SCNVector3(
x: Float(bottomLeft.dx + CGFloat(x) * resolution),
//y: Float.random(in: 0..<0.1),
y: yVal,
z: Float(bottomLeft.dy + CGFloat(y) * resolution)
)
)
vertsStream.append(verts[x][y].pos)
i += 1
}
}
///
/// Triangles ///
var tris = [(a: Int, b: Int, c: Int)]()
var trisStream = [UInt32]()
for x in 0...axisCount - 2 {
for y in 0...axisCount - 2 {
// Quad
tris.append((
a: verts[x][y].i,
b: verts[x][y+1].i,
c: verts[x+1][y].i
))
tris.append((
a: verts[x+1][y].i,
b: verts[x][y+1].i,
c: verts[x+1][y+1].i
))
}
}
for t in tris {
trisStream.append(UInt32(t.a))
trisStream.append(UInt32(t.b))
trisStream.append(UInt32(t.c))
}
///
/// Normals ///
var normalStream = [SCNVector3]()
for x in 0...axisCount - 1 {
for y in 0...axisCount - 1 {
// calculate normal vector perp to average plane
let leftX = x == 0 ? 0 : x - 1
let rightX = x == axisCount - 1 ? axisCount - 1 : x + 1
let leftY = y == 0 ? 0 : y - 1
let rightY = y == axisCount - 1 ? axisCount - 1 : y + 1
let avgXVector = float3(verts[rightX][y].pos) - float3(verts[leftX][y].pos)
let avgYVector = float3(verts[x][rightY].pos) - float3(verts[x][leftY].pos)
// If you are unfamiliar with how to calculate normals
// search for vector cross product, this is used to find
// a vector that is orthogonal to two other vectors, in our
// case perpendicular to the surface
let normal = cross(
normalize(avgYVector),
normalize(avgXVector)
)
normalStream.append(SCNVector3(normal))
}
}
///
// Create scene element
let gsGeometry = SCNGeometrySource(vertices: vertsStream)
let gsNormals = SCNGeometrySource(normals: normalStream)
let geometryElement = SCNGeometryElement(indices: trisStream, primitiveType: .triangles)
let geometryFinal = SCNGeometry(sources: [gsGeometry, gsNormals], elements: [geometryElement])
let node = SCNNode(geometry: geometryFinal)
let mat = SCNMaterial()
mat.isDoubleSided = true
mat.lightingModel = .blinn
node.geometry?.materials = [mat]
return node
}

Draw SceneKit object between two points

Having made some progress in the geometry side of things I'm moving on to putting together an entire scene. That scene has a couple dozen objects, each defined by a bounding cube whose corners are specified by two SCNVector3s (originally two sets of x,y,z).
Here's an example of what I have so far - it's an 11-element log-periodic antenna, like the old school TV antennas from the 70s. Each of the grey lines is an "element", typically made of aluminum rod. I used SCNCylinders from +ve to -ve Y and the entire thing is less than 100 lines (SK is pretty amazing).
The problem is what happens if the elements are not symmetrical across X and thus the SCNCylinder has to be rotated. I found this example, but I can't understand the specifics... it appears to take advantage of the fact that a sphere is symmetric so angles kind of "go away".
Does anyone have a general function that will take two 3D points and return the SCNVector3 suitable for setting the node's eulerAngle, or a similar solution?
Both solutions mentioned above work very well and I can contribute third solution to this question.
//extension code starts
func normalizeVector(_ iv: SCNVector3) -> SCNVector3 {
let length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z)
if length == 0 {
return SCNVector3(0.0, 0.0, 0.0)
}
return SCNVector3( iv.x / length, iv.y / length, iv.z / length)
}
extension SCNNode {
func buildLineInTwoPointsWithRotation(from startPoint: SCNVector3,
to endPoint: SCNVector3,
radius: CGFloat,
color: UIColor) -> SCNNode {
let w = SCNVector3(x: endPoint.x-startPoint.x,
y: endPoint.y-startPoint.y,
z: endPoint.z-startPoint.z)
let l = CGFloat(sqrt(w.x * w.x + w.y * w.y + w.z * w.z))
if l == 0.0 {
// two points together.
let sphere = SCNSphere(radius: radius)
sphere.firstMaterial?.diffuse.contents = color
self.geometry = sphere
self.position = startPoint
return self
}
let cyl = SCNCylinder(radius: radius, height: l)
cyl.firstMaterial?.diffuse.contents = color
self.geometry = cyl
//original vector of cylinder above 0,0,0
let ov = SCNVector3(0, l/2.0,0)
//target vector, in new coordination
let nv = SCNVector3((endPoint.x - startPoint.x)/2.0, (endPoint.y - startPoint.y)/2.0,
(endPoint.z-startPoint.z)/2.0)
// axis between two vector
let av = SCNVector3( (ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0)
//normalized axis vector
let av_normalized = normalizeVector(av)
let q0 = Float(0.0) //cos(angel/2), angle is always 180 or M_PI
let q1 = Float(av_normalized.x) // x' * sin(angle/2)
let q2 = Float(av_normalized.y) // y' * sin(angle/2)
let q3 = Float(av_normalized.z) // z' * sin(angle/2)
let r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3
let r_m12 = 2 * q1 * q2 + 2 * q0 * q3
let r_m13 = 2 * q1 * q3 - 2 * q0 * q2
let r_m21 = 2 * q1 * q2 - 2 * q0 * q3
let r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3
let r_m23 = 2 * q2 * q3 + 2 * q0 * q1
let r_m31 = 2 * q1 * q3 + 2 * q0 * q2
let r_m32 = 2 * q2 * q3 - 2 * q0 * q1
let r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3
self.transform.m11 = r_m11
self.transform.m12 = r_m12
self.transform.m13 = r_m13
self.transform.m14 = 0.0
self.transform.m21 = r_m21
self.transform.m22 = r_m22
self.transform.m23 = r_m23
self.transform.m24 = 0.0
self.transform.m31 = r_m31
self.transform.m32 = r_m32
self.transform.m33 = r_m33
self.transform.m34 = 0.0
self.transform.m41 = (startPoint.x + endPoint.x) / 2.0
self.transform.m42 = (startPoint.y + endPoint.y) / 2.0
self.transform.m43 = (startPoint.z + endPoint.z) / 2.0
self.transform.m44 = 1.0
return self
}
}
//extension ended.
//in your code, you can like this.
let twoPointsNode1 = SCNNode()
scene.rootNode.addChildNode(twoPointsNode1.buildLineInTwoPointsWithRotation(
from: SCNVector3(1,-1,3), to: SCNVector3( 7,11,7), radius: 0.2, color: .cyan))
//end
you can reference http://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
BTW, you will get same result when you use a cylinder to make a line between two points from above 3 methods. But indeed, they will have different normal lines. In another words, if you use box between two points, sides of box, except top and bottom, will face different direction from above 3 methods.
let me know pls if you need further explanation.
EDIT: For under or equal to IOS 11
I've good news for you ! You can link two points and put a SCNNode on this Vector !
Take this and enjoy drawing line between two point !
class CylinderLine: SCNNode
{
init( parent: SCNNode,//Needed to add destination point of your line
v1: SCNVector3,//source
v2: SCNVector3,//destination
radius: CGFloat,//somes option for the cylinder
radSegmentCount: Int, //other option
color: UIColor )// color of your node object
{
super.init()
//Calcul the height of our line
let height = v1.distance(v2)
//set position to v1 coordonate
position = v1
//Create the second node to draw direction vector
let nodeV2 = SCNNode()
//define his position
nodeV2.position = v2
//add it to parent
parent.addChildNode(nodeV2)
//Align Z axis
let zAlign = SCNNode()
zAlign.eulerAngles.x = Float(M_PI_2)
//create our cylinder
let cyl = SCNCylinder(radius: radius, height: CGFloat(height))
cyl.radialSegmentCount = radSegmentCount
cyl.firstMaterial?.diffuse.contents = color
//Create node with cylinder
let nodeCyl = SCNNode(geometry: cyl )
nodeCyl.position.y = -height/2
zAlign.addChildNode(nodeCyl)
//Add it to child
addChildNode(zAlign)
//set contrainte direction to our vector
constraints = [SCNLookAtConstraint(target: nodeV2)]
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
private extension SCNVector3{
func distance(receiver:SCNVector3) -> Float{
let xd = receiver.x - self.x
let yd = receiver.y - self.y
let zd = receiver.z - self.z
let distance = Float(sqrt(xd * xd + yd * yd + zd * zd))
if (distance < 0){
return (distance * -1)
} else {
return (distance)
}
}
}
#maury-markowitz's answer worked for me, here is the latest (Swift4) version of it.
To anyone working with SCNVector3 in Swift I can only recommend to add the +-*/ operator overloads somewhere in your code (e.g. from here).
extension SCNNode {
static func lineNode(from: SCNVector3, to: SCNVector3, radius: CGFloat = 0.25) -> SCNNode {
let vector = to - from
let height = vector.length()
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.radialSegmentCount = 4
let node = SCNNode(geometry: cylinder)
node.position = (to + from) / 2
node.eulerAngles = SCNVector3.lineEulerAngles(vector: vector)
return node
}
}
extension SCNVector3 {
static func lineEulerAngles(vector: SCNVector3) -> SCNVector3 {
let height = vector.length()
let lxz = sqrtf(vector.x * vector.x + vector.z * vector.z)
let pitchB = vector.y < 0 ? Float.pi - asinf(lxz/height) : asinf(lxz/height)
let pitch = vector.z == 0 ? pitchB : sign(vector.z) * pitchB
var yaw: Float = 0
if vector.x != 0 || vector.z != 0 {
let inner = vector.x / (height * sinf(pitch))
if inner > 1 || inner < -1 {
yaw = Float.pi / 2
} else {
yaw = asinf(inner)
}
}
return SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
}
}
For the sake of another method, I achieved this through trigonometry. This made the code very minimal. Here is the end result:
In my case the nodes are always placed on a fixed plane that slices the Y-Axis.
// Create Cylinder Geometry
let line = SCNCylinder(radius: 0.002, height: node1.distance(to: node2))
// Create Material
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
material.lightingModel = .phong
line.materials = [material]
// Create Cylinder(line) Node
let newLine = SCNNode()
newLine.geometry = line
newLine.position = posBetween(first: node1, second: node2)
// This is the change in x,y and z between node1 and node2
let dirVector = SCNVector3Make(node2.x - node1.x, node2.y - node1.y, node2.z - node1.z)
// Get Y rotation in radians
let yAngle = atan(dirVector.x / dirVector.z)
// Rotate cylinder node about X axis so cylinder is laying down
currentLine.eulerAngles.x = .pi / 2
// Rotate cylinder node about Y axis so cylinder is pointing to each node
currentLine.eulerAngles.y = yAngle
This is the function to get the position between two nodes, place it within your class:
func posBetween(first: SCNVector3, second: SCNVector3) -> SCNVector3 {
return SCNVector3Make((first.x + second.x) / 2, (first.y + second.y) / 2, (first.z + second.z) / 2)
}
This is the extension to get the distance between nodes for the cylinder height, place it somewhere outside of your class:
extension SCNVector3 {
func distance(to destination: SCNVector3) -> CGFloat {
let dx = destination.x - x
let dy = destination.y - y
let dz = destination.z - z
return CGFloat(sqrt(dx*dx + dy*dy + dz*dz))
}
}
If you don't have one fixed axis like myself then you could do the extra trig to use this method.
Here's a solution using simd and quaternions for the rotation. I based the extension off of the answer by #Bersaelor.
I used this derivation (https://stackoverflow.com/a/1171995/6693924) to create the quaternion from two vectors. Hope this helps.
extension SCNNode {
static func lineNode(from: simd_float3, to: simd_float3, radius : CGFloat = 0.25) -> SCNNode
{
let vector = to - from
let height = simd_length(vector)
//cylinder
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.firstMaterial?.diffuse.contents = UIColor.white
//line node
let lineNode = SCNNode(geometry: cylinder)
//adjust line position
let line_axis = simd_float3(0, height/2, 0)
lineNode.simdPosition = from + line_axis
let vector_cross = simd_cross(line_axis, vector)
let qw = simd_length(line_axis) * simd_length(vector) + simd_dot(line_axis, vector)
let q = simd_quatf(ix: vector_cross.x, iy: vector_cross.y, iz: vector_cross.z, r: qw).normalized
lineNode.simdRotate(by: q, aroundTarget: from)
return lineNode
}
}
Sprout's (wow, the autocorrect will not allow me to actually type in his name!) post is indeed a solution, but I have implemented a very different solution in my code.
What I do is calculate the length of the line and the two endpoints, based on the X, Y and Z locations from the two ends:
let w = SCNVector3(x: CGFloat(x2m-x1m), y: CGFloat(y2m-y1m), z: CGFloat(z2m-z1m))
let l = w.length()
The length is simply pythag. Now I make an SCNNode that will hold the SCNCylinder, and position it in the middle of the line:
let node = SCNNode(geometry: cyl)
node.position = SCNVector3(x: CGFloat((x1m+x2m)/2.0), y: CGFloat((y1m+y2m)/2.0), z: CGFloat((z1m+z2m)/2.0))
And now the nasty part, where we calculate the Euler angles and rotate the node:
let lxz = (Double(w.x)**2 + Double(w.z)**2)**0.5
var pitch, pitchB: Double
if w.y < 0 {
pitchB = M_PI - asin(Double(lxz)/Double(l))
} else {
pitchB = asin(Double(lxz)/Double(l))
}
if w.z == 0 {
pitch = pitchB
} else {
pitch = sign(Double(w.z)) * pitchB
}
var yaw: Double
if w.x == 0 && w.z == 0 {
yaw = 0
} else {
let inner = Double(w.x) / (Double(l) * sin (pitch))
if inner > 1 {
yaw = M_PI_2
} else if inner < -1 {
yaw = M_PI_2
} else {
yaw = asin(inner)
}
}
node.eulerAngles = SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
I suspect there is a much simpler way to do this using one of the other rotation inputs, but this works and working is a feature!
Draw the line between two nodes:
func generateLine( startPoint: SCNVector3, endPoint: SCNVector3) -> SCNGeometry {
let vertices: [SCNVector3] = [startPoint, endPoint]
let data = NSData(bytes: vertices, length: MemoryLayout<SCNVector3>.size * vertices.count) as Data
let vertexSource = SCNGeometrySource(data: data,
semantic: .vertex,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.stride)
let indices: [Int32] = [ 0, 1]
let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count) as Data
let element = SCNGeometryElement(data: indexData,
primitiveType: .line,
primitiveCount: indices.count/2,
bytesPerIndex: MemoryLayout<Int32>.size)
return SCNGeometry(sources: [vertexSource], elements: [element])
}
How To Use
let line = generateLine(startPoint: SCNVector3Make(1, 1, 1), endPoint: SCNVector3Make(8, 8, 8))
let lineNode = SCNNode(geometry: line)
lineNode.position = SCNVector3Make(15, 15, 10)
scene.rootNode.addChildNode(lineNode)
The thickness of the line requires implementing the SCNSceneRendererDelegate, in particular:
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval){
glLineWidth(10)
}
Objective-C version of Winchill's answer:
-(void)lineNodeFrom:(SCNVector3)to to:(SCNVector3)from radius:(float)radius{
SCNVector3 w = SCNVector3Make(to.x - from.x, to.y - from.y, from.z - to.z);
float l = sqrtf(powf(w.x, 2) + powf(w.y, 2) + powf(w.z, 2.0f));
SCNCylinder * cylinder = [SCNCylinder cylinderWithRadius:radius height:l];
SCNMaterial * material = [SCNMaterial material];
material.diffuse.contents = [[UIColor darkGrayColor] colorWithAlphaComponent:0.75f];
cylinder.materials = #[material];
[self setGeometry:cylinder];
//original vector of cylinder above 0,0,0
SCNVector3 ov = SCNVector3Make(0, l/2.0,0);
//target vector, in new coordination
SCNVector3 nv = SCNVector3Make((from.x - to.x)/2.0, (from.y - to.y)/2.0, (from.z-to.z)/2.0);
// axis between two vector
SCNVector3 av = SCNVector3Make((ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0);
//normalized axis vector
SCNVector3 av_normalized = [self normaliseVector:av];
float q0 = 0.0f; //cos(angel/2), angle is always 180 or M_PI
float q1 = av_normalized.x; // x' * sin(angle/2)
float q2 = av_normalized.y; // y' * sin(angle/2)
float q3 = av_normalized.z; // z' * sin(angle/2)
float r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3;
float r_m12 = 2 * q1 * q2 + 2 * q0 * q3;
float r_m13 = 2 * q1 * q3 - 2 * q0 * q2;
float r_m21 = 2 * q1 * q2 - 2 * q0 * q3;
float r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3;
float r_m23 = 2 * q2 * q3 + 2 * q0 * q1;
float r_m31 = 2 * q1 * q3 + 2 * q0 * q2;
float r_m32 = 2 * q2 * q3 - 2 * q0 * q1;
float r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3;
SCNMatrix4 transform;
transform.m11 = r_m11;
transform.m12 = r_m12;
transform.m13 = r_m13;
transform.m14 = 0.0;
transform.m21 = r_m21;
transform.m22 = r_m22;
transform.m23 = r_m23;
transform.m24 = 0.0;
transform.m31 = r_m31;
transform.m32 = r_m32;
transform.m33 = r_m33;
transform.m34 = 0.0;
transform.m41 = (to.x + from.x) / 2.0;
transform.m42 = (to.y + from.y) / 2.0;
transform.m43 = (to.z + from.z) / 2.0;
transform.m44 = 1.0;
self.transform = transform;
}
-(SCNVector3)normaliseVector:(SCNVector3)iv{
float length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z);
if (length == 0){
return SCNVector3Make(0.0, 0.0, 0.0);
}
return SCNVector3Make(iv.x / length, iv.y / length, iv.z / length);
}