Unable to create Cylinder for certain situations - Arkit - ios

I have two vectors as inputs and I am trying to create a cylinder between them.
But this fails, when x and z coordinates of my points are same and y coordinate is different
I have the following functions:
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
}
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)
}
I am not still sure, as to why it fails, when the x,z coordinates of both points are same, but y coordinate is completely different.
I would be really glad, if someone can explain me.
Thanks in Advance.

Looking at your code, it looks very complicated for something which can be achieved in a lot less code.
This code can be used with an SCNBox or SCNCylinder Geometry.
class MeasuringLineNode: SCNNode{
init(startingVector vectorA: GLKVector3, endingVector vectorB: GLKVector3) {
super.init()
//1. Create The Height Our Box Which Is The Distance Between Our 2 Vectors
let height = CGFloat(GLKVector3Distance(vectorA, vectorB))
//2. Set The Nodes Position At The Same Position As The Starting Vector
self.position = SCNVector3(vectorA.x, vectorA.y, vectorA.z)
//3. Create A Second Node Which Is Placed At The Ending Vectors Posirions
let nodeVectorTwo = SCNNode()
nodeVectorTwo.position = SCNVector3(vectorB.x, vectorB.y, vectorB.z)
//4. Create An SCNNode For Alignment Purposes At 90 Degrees
let nodeZAlign = SCNNode()
nodeZAlign.eulerAngles.x = Float.pi/2
//5. Create An SCNCyclinder Geometry To Act As Our Line
let cylinder = SCNCylinder(radius: 0.001, height: height)
let material = SCNMaterial()
material.diffuse.contents = UIColor.white
cylinder.materials = [material]
/*
If you want to use an SCNBox then use the following:
let box = SCNBox(width: 0.001, height: height, length: 0.001, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.white
box.materials = [material]
*/
//6. Create The LineNode Centering On The Alignment Node
let nodeLine = SCNNode(geometry: cylinder)
nodeLine.position.y = Float(-height/2)
nodeZAlign.addChildNode(nodeLine)
self.addChildNode(nodeZAlign)
//7. Force The Node To Look At Our End Vector
self.constraints = [SCNLookAtConstraint(target: nodeVectorTwo)]
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Assuming you have 2 SCNNodes (which in my example, I will simply call nodeA and nodeB), you can create a joining line like so:
//1. Convert The Nodes SCNVector3 to GLVector3
let nodeAVector3 = GLKVector3Make(nodeA.position.x, nodeA.position.y, nodeA.position.z)
let nodeBVector3 = GLKVector3Make(nodeB.position.x, nodeB.position.y, nodeB.position.z)
//2. Draw A Line Between The Nodes
let line = MeasuringLineNode(startingVector: nodeAVector3 , endingVector: nodeBVector3)
self.augmentedRealityView.scene.rootNode.addChildNode(line)
Hope this helps...

Related

How to calculate the distance between a SCNText's Center and its Left and Right Corners

How can I calculate distance between SCNText center and its left edge corner and its right edge corner.
For eg using the code below, if the word was "texas" the middle of the letter "x" would be the pivot point because it's in the middle of the word. How would I get the distance between the left side of the letter "t" and the letter "x". How would I get the distance between the right side of the letter "s" and the letter "x"
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
let geometry = SCNText(string: "texas", extrusionDepth: 0.1)
geometry.flatness = 0
geometry.font = UIFont.systemFont(ofSize: 6.3)
geometry.materials = [material]
let textNode = SCNNode()
textNode.geometry = geometry
textNode.geometry?.firstMaterial?.isDoubleSided = true
// set pivot point
let min = textNode.boundingBox.min
let max = textNode.boundingBox.max
textNode.pivot = SCNMatrix4MakeTranslation(
min.x + (max.x - min.x)/2,
min.y + (max.y - min.y)/2,
min.z + (max.z - min.z)/2
)
let totalWidth: Float = textNode.boundingBox.max.x - textNode.boundingBox.min.x
let halfWidth: Float = totalWidth / 2
print("total width:", totalWidth)
print("half width:", halfWidth)

Spritekit Camera Node scale but pin the bottom of the scene

I have a camera node that is scaled at 1. When I run the game, I want it to scale it down (i.e. zoom out) but keep the "floor" at the bottom. How would I go about pinning the camera node to the bottom of the scene and effectively zooming "up" (difficult to explain). So the bottom of the scene stays at the bottom but the rest zooms out.
I have had a go with SKConstraints but not having any luck (I'm quite new at SpriteKit)
func setConstraints(with scene: SKScene, and frame: CGRect, to node: SKNode?) {
let scaledSize = CGSize(width: scene.size.width * xScale, height: scene.size.height * yScale)
let boardContentRect = frame
let xInset = min((scaledSize.width / 2), boardContentRect.width / 2)
let yInset = min((scaledSize.height / 2), boardContentRect.height / 2)
let insetContentRect = boardContentRect.insetBy(dx: xInset, dy: yInset)
let xRange = SKRange(lowerLimit: insetContentRect.minX, upperLimit: insetContentRect.maxX)
let yRange = SKRange(lowerLimit: insetContentRect.minY, upperLimit: insetContentRect.maxY)
let levelEdgeConstraint = SKConstraint.positionX(xRange, y: yRange)
if let node = node {
let zeroRange = SKRange(constantValue: 0.0)
let positionConstraint = SKConstraint.distance(zeroRange, to: node)
constraints = [positionConstraint, levelEdgeConstraint]
} else {
constraints = [levelEdgeConstraint]
}
}
then calling the function with:
gameCamera.setConstraints(with: self, and: scene!.frame, to: nil)
(This was code from a tutorial I was following) The "setConstraints" function is an extension of SKCameraNode
I'm not sure this will give me the correct output, but when I run the code to scale, it just zooms from the middle and shows the surrounding area of the scene .sks file.
gameCamera.run(SKAction.scale(to: 0.2, duration: 100))
This is the code to scale the gameCamera
EDIT: Answer below is nearly what I was looking for, this is my updated answer:
let scaleTo = 0.2
let duration = 100
let scaleTop = SKAction.customAction(withDuration:duration){
(node, elapsedTime) in
let newScale = 1 - ((elapsedTime/duration) * (1-scaleTo))
let currentScaleY = node.yScale
let currentHeight = node.scene!.size.height * currentScaleY
let newHeight = node.scene!.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.setScale(newScale)
node.position.y += yOffset
}
You cannot use a constraint because your scale size is dynamic.
Instead you need to move your camera position to give the illusion it is only scaling in 3 directions.
To do this, I would recommend creating a custom action.
let scaleTo = 2.0
let duration = 1.0
let currentNodeScale = 0.0
let scaleTop = SKCustomAction(withDuration:duration){
(node, elapsedTime) in
if elapsedTime == 0 {currentNodeScale = node.scale}
let newScale = currentNodeScale - ((elapsedTime/duration) * (currentNodeScale-scaleTo))
let currentYScale = node.yScale
let currentHeight = node.scene.size.height * currentYScale
let newHeight = node.scene.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.scale(to:newScale)
node.position.y += yOffset
}
What this is doing is comparing the new height of your camera with the old height, and moving it 1/2 the distance.
So if your current height is 1, this means your camera sees [-1/2 to 1/2] on the y axis. If you new scale height is 2, then your camera sees [-1 to 1] on the y axis. We need to move the camera up so that the camera sees [-1/2 to 3/2], meaning we need to add 1/2. So we do 2 - 1, which is 1, then go 1/2 that distance. This makes our yOffset 1/2, which you add to the camera.

How to display (add) plane text node between two points?

How to display distance value plane surface between two points in measurement ARKit/SceneKit? Same as on image.
Here's a simple principle how to place a label with 3D text between two points.
let p1 = SCNNode() // POINT 1
p1.geometry = SCNSphere(radius: 0.3)
p1.position = SCNVector3(-1, -2, -3)
p1.geometry?.firstMaterial?.diffuse.contents = NSColor.red
scene.rootNode.addChildNode(p1)
let p2 = SCNNode() // POINT 2
p2.geometry = SCNSphere(radius: 0.2)
p2.position = SCNVector3(4, 5, 6)
p2.geometry?.firstMaterial?.diffuse.contents = NSColor.red
scene.rootNode.addChildNode(p2)
let x = (p1.position.x + p2.position.x) / 2
let y = (p1.position.y + p2.position.y) / 2
let z = (p1.position.z + p2.position.z) / 2
let label = SCNNode()
let text = SCNText(string: "label", extrusionDepth: 5)
label.geometry = text
// Shifting text's pivot to its center.
// Default pivot's position is lower left corner.
label.simdPivot.columns.3.x = Float((text.boundingBox.min.x +
text.boundingBox.max.x) / 2)
label.simdPivot.columns.3.y = Float((text.boundingBox.min.y +
text.boundingBox.max.y) / 2)
label.scale = SCNVector3(0.02, 0.02, 0.02)
label.position = SCNVector3(x, y, z)
label.geometry?.firstMaterial?.diffuse.contents = NSColor.red
scene.rootNode.addChildNode(label)
let constraint = SCNBillboardConstraint()
label.constraints = [constraint]
let plane = SCNNode()
plane.geometry = SCNPlane(width: 2, height: 1)
plane.position = SCNVector3(x, y, z)
plane.geometry?.firstMaterial?.diffuse.contents = NSColor.white
scene.rootNode.addChildNode(plane)
plane.constraints = [constraint]
The same way you may add ARAnchor and attach any label to it.

Swift/SpriteKit - Ball node downwards angle of release

I need the ball node to be be initially released in my game in a downwards angle (which is randomly generated each time). I found out the velocity of the ball includes both the direction and speed (please correct me if this is wrong) therefore I generated a random angle and multiplied this by a value such as '0.5' to represent the impulse/speed. My code for this can be seen below:
let minAngle : UInt32 = 181
let maxAngle : UInt32 = 359
let randomAngle = arc4random_uniform(maxAngle - minAngle) + minAngle
let dx:CGFloat = 0.5 * CGFloat(randomAngle)
let dy:CGFloat = 0.5 * CGFloat(randomAngle)
ball.physicsBody?.velocity = CGVector(dx: dx, dy: dy)
However I realised this isn't working as the ball goes in upwards direction. Is my range in-correct or is my method of doing so incorrect? If it is my method how could I guarantee that the ball always is released in a downwards angle (I must also add a speed to the ball)?
Thanks
You need to use SIN COS and ATAN on angles to get where you are going.
If you know your angle and speed, you use SIN and COS:
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
var radiansToDegrees: Double { return Double(self) * 180 / .pi }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
credit to Leo Dabus How can I convert from degrees to radians?
let minAngle = 181.degreesToRadans
let maxAngle = 359.degreesToRadans
let randomAngle = arc4random_uniform(maxAngle - minAngle) + minAngle
let dx:CGFloat = 0.5 * cos(CGFloat(randomAngle))
let dy:CGFloat = 0.5 * sin(CGFloat(randomAngle))
ball.physicsBody?.velocity = CGVector(dx: dx, dy: dy)
If you only know 2 points, and need to get the angle, you use arctan2:
(We need to make point1 our origin, so we subtract point2 from point1)
let point1 = CGPoint(x:0,y:1)
let point2 = CGPoint(x:0,y:2)
let distancePoint = CGPoint(x:point2.x - point1.x, y:point2.y - point1.y)
let angle = atan2(distancePoint.y,distancePoint.x)
spritekit works using radians not degrees. better to learn to think in terms of radians rather than degrees since that's what you'll be using. If you want to continue using degrees you can use this kind of formula.
func convertToRadians(degrees: Int) -> CGFloat {
return CGFloat(M_PI) * CGFloat(degrees) / 180
}
let minAngle = convertToRadians(degrees: 181)
let maxAngle = convertToRadians(degrees: 359)
another way of calculating it
let minAngle = CGFloat(Measurement(value: 181, unit: UnitAngle.degrees).converted(to: .radians).value)
let maxAngle = CGFloat(Measurement(value: 359, unit: UnitAngle.degrees).converted(to: .radians).value)
get a random angle in radians between two sets of degrees
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func randomDegreesToRadians(min: CGFloat, max: CGFloat) -> CGFloat {
assert(min < max)
let randomDegrees = random() * (max - min) + min
return CGFloat(M_PI) * CGFloat(randomDegrees) / 180
}
randomDegreesToRadians(min: 100, max: 200)

How do I add a button at every x degrees? [duplicate]

I have an array of buttons and when I append them to a view I want the to be positioned around a image view which is in the center. Based on how many objects there are in the array, I want them to be evenly spaced around the whole circle. Below is my attempt to do so. What am I doing wrong and how should I fix it? There is more than one button behind the moose.
var userbutton = [UIButton]()
var upimage = [UIImage]()
var locationpic = [AnyObject]()
func locationsSet(){
for (index, users) in upimage.enumerate() {
let userbutton = UIButton()
userbutton.addTarget(self, action: "buttonAction:", forControlEvents: .TouchUpInside)
userbutton.frame = CGRectMake(100, 100, 50, 50)
userbutton.layer.cornerRadius = userbutton.frame.size.width/2
userbutton.clipsToBounds = true
userbutton.setImage(users, forState: .Normal)
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * (radius + 40)
let pointy = (centery) + (sin(radians)) * (radius + 40)
userbutton.center.x = pointx
userbutton.center.y = pointy
self.userbutton.append(userbutton)
self.view.addSubview(userbutton)
print("x\(pointx)")
print("y\(pointy)")
}
}
How I would do this:
Create an extension to UIView to get the diagonal and radius. These are handy because we want our "satellites" to have predictable placing even when the "planet" isn't square.
extension UIView {
var diagonal : CGFloat {
return sqrt(pow(self.frame.width, 2) + pow(self.frame.height, 2))
}
var radius : CGFloat {
return diagonal / 2
}
}
This will return a point based on an angle and a distance from an origin.
It uses dreadful trigonometry.
func getPoint(fromPoint point: CGPoint, atDistance distance: CGFloat, withAngleRadians angle:CGFloat) -> CGPoint {
let x = point.x
let y = point.y
let dx = (distance * cos(angle))
let dy = (distance * sin(angle))
return CGPoint(x: (dx + x), y: (dy + y))
}
Now the real function. Generate a bunch of points in a circle pattern. I used a running sum for the angle instead of multiplying each time by the index. This just returns the centre points for the views.
func encirclePoint(point : CGPoint, distance:CGFloat, inParts parts: Int) -> [CGPoint] {
let angle = 2 * CGFloat(M_PI) / CGFloat(parts) // critical part, you need radians for trigonometry
var runningAngle : CGFloat = -(CGFloat(M_PI) / 2) // start at the top
var points : [CGPoint] = []
for _ in 0..<parts {
let circlePoint = getPoint(fromPoint: point, atDistance: distance, withAngleRadians: runningAngle)
points.append(circlePoint)
runningAngle += angle
}
return points
}
Now you can create a simple function that takes a view, a margin and an array of "satellite" views. It will set their centre and add them to the superview of the view we used to input. It makes sense not to add them to the view itself since they might not be placed inside it.
func encircleView(view : UIView, withSubViews subViews : [UIView], withMargin margin : CGFloat) {
guard !(subViews.isEmpty) else { // if there are no subviews : abort
return
}
let distance = view.radius + margin
let points = encirclePoint(view.center, distance: distance, inParts: subViews.count)
guard subViews.count == points.count, let uberView = view.superview else { // if the count is not the same or there is no superview: abort
return
}
for (point, subView) in zip(points, subViews) { subView.center = point }
}
Notice how I did nothing except for the centre calculations in these functions. Styling them goes in another function. This makes it super easy to maintain and debug.
I might even let the last function just return the subviews with updated frames and add them later.
Or negative margin :)
Gist
A full circle is 2 * pi radians. You need to divide that by the number of items you have and multiply that by the index of the item you are currently processing. Use trig to find the location on the circle:
for (index, users) in upimage.enumerate() {
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
......
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * radius
let pointy = centery + sin(radians) * radius
......
}

Resources