I am making a brick breaker style game in Xcode for iOS. There is no paddle and balls are all launched from the same location (determined by where the first ball falls from the last level). There are a varying number of balls launched each time based on the number of balls the player has created. Each game starts with one ball but the user collects more in playing. When applying an impulse to the ball sprites I can only either move one sprite or all ball sprites at once. How do I move the ball sprites with a delay between each sprites launch?
Below I will go over what I have tried already. Thank you in advance for any help!
So far I have tried a couple of things. First I was trying to create all ball sprites prior to allowing the user to launch and having all ball sprites at the same location. Then I enumerated over them and tried to launch them with a delay there. However this simply launched all balls simultaneously after the delay, not each individually.
gameWorldNode.enumerateChildNodes(withName: BallCategoryName) {
node, stop in
let ball = node as! SKSpriteNode
ball.move(toParent: gameWorldNode)
//Calculate Vector
var dx = CGFloat(touchLocation.x - ball.position.x)
var dy = CGFloat(touchLocation.y - ball.position.y)
//Calculate Magnitude
let magnitude = sqrt(dx * dx + dy * dy)
dx /= magnitude
dy /= magnitude
//Create Vector
let vector = CGVector(dx: 30.0 * dx, dy: 30.0 * dy)
let secondsToDelay = 0.25
DispatchQueue.main.asyncAfter(deadline: .now() + secondsToDelay){
//Apply impulse
ball.physicsBody!.applyImpulse(vector)
}
}
Secondly, It occurred to me that it would probably be better for game performance if I didn't create the ball sprite until right before the impulse is applied, after the previous ball is launched. So I tried the following code. However, while I think I may be on the right path now, the newly created ball sprites don't launch. They get created but for some reason they don't apply the impulse.
let ball = gameWorldNode.childNode(withName: BallCategoryName) as! SKSpriteNode
ball.move(toParent: gameWorldNode)
//Calculate Vector
var dx = CGFloat(touchLocation.x - ball.position.x)
var dy = CGFloat(touchLocation.y - ball.position.y)
//Calculate Magnitude
let magnitude = sqrt(dx * dx + dy * dy)
dx /= magnitude
dy /= magnitude
//Create Vector
let vector = CGVector(dx: 30.0 * dx, dy: 30.0 * dy)
ball.physicsBody!.applyImpulse(vector)
var i = 1
while (i != ballsAvailabe) {
//Create new ball sprite
let nextBall: SKSpriteNode = Ball()
nextBall.position = CGPoint(x: locationX, y: locationY)
gameWorldNode.addChild(nextBall)
let secondsToDelay = 0.1
DispatchQueue.main.asyncAfter(deadline: .now() + secondsToDelay) {
//Apply impulse to ball
nextBall.physicsBody!.applyImpulse(vector)
}
//Update i
i += 1
}
I have also tried to add the sprites to an array and iterate over the array. However, this simply launches each ball at once still.
for _ in 0..<ballsAvailabe {
//Create new ball sprite
let ball: SKSpriteNode = Ball()
ball.position = CGPoint(x: locationX, y: locationY)
ball.physicsBody!.categoryBitMask = nextBallCategory
ball.color = nextBallInPlay
ball.colorBlendFactor = 1
ball.physicsBody!.contactTestBitMask = BottomCategory | AddABallCategory | RedBlockCategory | BlueBlockCategory | OrangeBlockCategory | PurpleBlockCategory | GreenBlockCategory | CyanBlockCategory
ballSprites.append(ball)
gameWorldNode.addChild(ball)
}
for sprite in ballSprites {
let secondsToDelay = 0.3
DispatchQueue.main.asyncAfter(deadline: .now() + secondsToDelay) {
sprite.physicsBody!.applyImpulse(vector)
}
}
EDIT:
Following Johns comment below, I have added code to my second attempt to ensure the physics bodies are created properly. Sorry for all the variables in the code they are just global variables I have created and keep in its own file to ensure I am keeping values consistent throughout the code. This works better. When the user only has one ball it works perfectly. However, when the user has collected more balls to launch it will sometimes launch perfectly with the delay to the expected location. Other times it will launch only the first ball. Other times it will launch to an unexpected location.
Heres the updated code:
//Grab existing ball object
let ball = gameWorldNode.childNode(withName: BallCategoryName) as! SKSpriteNode
//Assign to game world node
ball.move(toParent: gameWorldNode)
//Apply ball properties
ball.color = nextBallInPlay
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width / 2)
ball.physicsBody!.categoryBitMask = RedBallCategory
ball.name = BallCategoryName
ball.colorBlendFactor = 1
ball.physicsBody?.pinned = false
ball.physicsBody?.mass = ballMass
ball.physicsBody?.affectedByGravity = ballGravity
ball.physicsBody?.allowsRotation = ballRotation
ball.physicsBody?.isDynamic = ballDynamic
ball.zPosition = ballZPos
ball.physicsBody?.friction = ballFriction
ball.physicsBody?.restitution = ballRestitution
ball.physicsBody?.linearDamping = ballLinearDaming
ball.physicsBody?.angularDamping = ballAngularDamping
//Stop more launches and accidently using power ups
isFingerOnScreen = true
canBallBeLaunched = false
canUsePowerUps = false
//Ensure physics world speed is accurate
physicsWorld.speed = 1
//Launching location
var dx = CGFloat(touchLocation.x - locationX)
var dy = CGFloat(touchLocation.y - locationY)
//Calculate Magnitude
let magnitude = sqrt(dx * dx + dy * dy)
dx /= magnitude
dy /= magnitude
//Create Vector
let vector = CGVector(dx: 30.0 * dx, dy: 30.0 * dy)
//Apply impule to first ball
ball.physicsBody!.applyImpulse(vector)
//loop to create x number of balls based on what the user has collected
var i = 1
while (i != ballsAvailabe) {
//Create new ball sprite
let nextBall: SKSpriteNode = Ball()
nextBall.position = CGPoint(x: locationX, y: locationY)
gameWorldNode.addChild(nextBall)
//Ball properties
nextBall.color = nextBallInPlay
nextBall.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width / 2)
nextBall.physicsBody!.categoryBitMask = RedBallCategory
nextBall.name = BallCategoryName
nextBall.colorBlendFactor = 1
nextBall.physicsBody?.pinned = false
nextBall.physicsBody?.mass = ballMass
nextBall.physicsBody?.affectedByGravity = ballGravity
nextBall.physicsBody?.allowsRotation = ballRotation
nextBall.physicsBody?.isDynamic = ballDynamic
nextBall.zPosition = ballZPos
nextBall.physicsBody?.friction = ballFriction
nextBall.physicsBody?.restitution = ballRestitution
nextBall.physicsBody?.linearDamping = ballLinearDaming
nextBall.physicsBody?.angularDamping = ballAngularDamping
//Delay between each ball
let secondsToDelay = 0.1
DispatchQueue.main.asyncAfter(deadline: .now() + secondsToDelay) {
//Apply impulse to ball
nextBall.physicsBody!.applyImpulse(vector)
}
//Update i
i += 1
}
I am creating a iOS Game where a player has a bow and arrow that spins in a 360 degree circle, and the player must shoot the bow at the right time to hit the target. Right now I am having trouble getting the arrow to shoot in the direction the bow is facing, as well as getting the arrow to shoot at the right angle towards that direction.
let bullet = SKSpriteNode(fileNamed: "Bullet")
bullet?.size = CGSize(width: 100, height: 100)
bullet.zPosition = -5
bullet.position = CGPointMake(player.position.x, player.position.y)
bullet.zRotation = player.zRotation
let action = SKAction.moveToY(self.size.height + 30, duration: 0.8)
let actionDone = SKAction.removeFromParent()
bullet.runAction(SKAction.sequence([action, actionDone]))
bullet.physicsBody = SKPhysicsBody(rectangleOfSize: bullet.size)
bullet.physicsBody?.affectedByGravity = false
bullet.physicsBody?.dynamic = false
self.addChild(bullet)
The player refers the the bow for reference.
Instead of moving the bullet to self.size.height + 30 in the Y direction and 0 pixels in the X direction, you can rotate that movement direction by the zRotation using trigonometry.
let amount = self.size.height + 30
let action = SKAction.moveTo(CGPointMake(bullet.position.x + amount * sin(bullet.zRotation), bullet.position.y + amount * cos(bullet.zRotation)), duration: 0.8)
You can get the behavior you are looking for by calculating a force vector from the bullet's zRotation and then use it to apply a force to the bullet's physicsBody.
To do this we will use trigonometry.
//adjust rotation by pi/2 radians to match spriteKits rotation system
let adjustedRotation = bullet.zRotation + .pi/2
//intensity scalar
let intensity:CGFloat = 4000 //adjust this value
//find x and y components using adjustedRotation and scale by intensity
let vx = intensity * cos(adjustedRotation)
let vy = intensity * sin(adjustedRotation)
//make vector using vx and vy components
let forceVector = CGVector(dx:vx, dy: vy)
//apply force to physicsBody
bullet.physicsBody?.applyForce(forceVector)
I have a flying aircraft which I am following and I am also showing the path the aircraft has followed. I am drawing cylinders as a line for drawing the path. Its kind of drawing a line between 2 points. I have a cameraNode which is set to (0,200,200) initially. At that point I can see the aircraft. But when I start my flight. It goes out of the screen. I want 2 things :
Follow just the aircraft (Path won't matter).
Show whole path and also the aircraft.
I tried finding the min ad max x,y and z and taking average but it din't work. If you see below gif its too zoomed and aircraft has moved out of the screen
Here is how I set my camera:
- (void)setUpCamera {
SCNScene *workingScene = [self getWorkingScene];
_cameraNode = [[SCNNode alloc] init];
_cameraNode.camera = [SCNCamera camera];
_cameraNode.camera.zFar = 500;
_cameraNode.position = SCNVector3Make(0, 60, 50);
[workingScene.rootNode addChildNode:_cameraNode];
SCNNode *frontCameraNode = [SCNNode node];
frontCameraNode.position = SCNVector3Make(0, 100, 50);
frontCameraNode.camera = [SCNCamera camera];
frontCameraNode.camera.xFov = 75;
frontCameraNode.camera.zFar = 500;
[_assetActivity addChildNode:frontCameraNode]; //_assetActivity is the aircraft node.
}
Here is how I am changing camera position which is not working:
- (void)showRealTimeFlightPath {
DAL3DPoint *point = [self.aircraftLocation convertCooridnateTo3DPoint];
DAL3DPoint *previousPoint = [self.previousAircraftLocation convertCooridnateTo3DPoint];
self.minCoordinate = [self.minCoordinate findMinPoint:self.minCoordinate currentPoint:point];
self.maxCoordinate = [self.minCoordinate findMaxPoint:self.maxCoordinate currentPoint:point];
DAL3DPoint *averagePoint = [[DAL3DPoint alloc] init];
averagePoint = [averagePoint averageBetweenCoordiantes:self.minCoordinate maxPoint:self.maxCoordinate];
SCNVector3 positions[] = {
SCNVector3Make(point.x,point.y,point.z) ,
SCNVector3Make(previousPoint.x,previousPoint.y,previousPoint.z)
};
SCNScene *workingScene = [self getWorkingScene];
DALLineNode *lineNodeA = [[DALLineNode alloc] init];
[lineNodeA init:workingScene.rootNode v1:positions[0] v2:positions[1] radius:0.1 radSegementCount:6 lineColor:[UIColor greenColor]] ;
[workingScene.rootNode addChildNode:lineNodeA];
self.previousAircraftLocation = [self.aircraftLocation mutableCopy];
self.cameraNode.position = SCNVector3Make(averagePoint.x, averagePoint.y, z);
self.pointOfView = self.cameraNode;
}
Code in swift or objective c are welcomed.
Thanks!!
The first behavior you describe would most easily be achieved by chaining a look-at constraint and a distance constraint, both targeting the aircraft.
let lookAtConstraint = SCNLookAtConstraint(target: aircraft)
let distanceConstraint = SCNDistanceConstraint(target: aircraft)
distanceConstraint.minimumDistance = 10 // set to whatever minimum distance between the camera and aircraft you'd like
distanceConstraint.maximumDistance = 10 // set to whatever maximum distance between the camera and aircraft you'd like
camera.constraints = [lookAtConstraint, distanceConstraint]
For iOS 10 and earlier, you can implement a distance constraint using SCNTransformConstraint. Here's a basic (though slightly ugly š) implementation that uses linear interpolation to update the node's position.
func normalize(_ value: Float, in range: ClosedRange<Float>) -> Float {
return (value - range.lowerBound) / (range.upperBound - range.lowerBound)
}
func interpolate(from start: Float, to end: Float, alpha: Float) -> Float {
return (1 - alpha) * start + alpha * end
}
let target = airplane
let minimumDistance: Float = 10
let maximumDistance: Float = 15
let distanceConstraint = SCNTransformConstraint(inWorldSpace: false) { (node, transform) -> SCNMatrix4 in
let distance = abs(sqrt(pow(target.position.x - node.position.x, 2) + pow(target.position.y - node.position.y, 2) + pow(target.position.z - node.position.z, 2)))
let normalizedDistance: Float
switch distance {
case ...minimumDistance:
normalizedDistance = self.normalize(minimumDistance, in: 0 ... distance)
case maximumDistance...:
normalizedDistance = self.normalize(maximumDistance, in: 0 ... distance)
default:
return transform
}
node.position.x = self.interpolate(from: target.position.x, to: node.position.x, alpha: normalizedDistance)
node.position.y = self.interpolate(from: target.position.y, to: node.position.y, alpha: normalizedDistance)
node.position.z = self.interpolate(from: target.position.z, to: node.position.z, alpha: normalizedDistance)
return transform
}
The second behavior could be implemented by determining the bounding box of your aircraft and all of its path segments in the camera's local coordinate space, then updating the camera's distance from the center of that bounding box to frame all of those nodes in the viewport. frameNodes(_:), a convenience method that implements this functionality, was introduced in iOS 11 and is defined on SCNCameraController. I'd recommend using it if possible, unless you want to dive into the trigonometry yourself. You could use your scene view's default camera controller or create a temporary instance, whichever suits the needs of your app.
You need to calculate the angle of the velocity so that the camera points in the direction of the moving SCNNode.
This code will point you in the right direction.
func renderer(_ aRenderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) {
// get velocity angle using velocity of vehicle
var degrees = convertVectorToAngle(vector: vehicle.chassisBody.velocity)
// get rotation of current camera on X and Z axis
let eX = cameraNode.eulerAngles.x
let eZ = cameraNode.eulerAngles.z
// offset rotation on y axis by 90 degrees
// this needs work, buggy
let ninety = deg2rad(90)
// default camera Y Euler angle facing north at 0 degrees
var eY : Float = 0.0
if degrees != 0 {
eY = Float(-degrees) - Float(ninety)
}
// rotate camera direction using cameraNode.eulerAngles and direction of velocity as eY
cameraNode.eulerAngles = SCNVector3Make(eX, eY, eZ)
// put camera 25 points behind vehicle facing direction of velocity
let dir = calculateCameraDirection(cameraNode: vehicleNode)
let pos = pointInFrontOfPoint(point: vehicleNode.position, direction:dir, distance: 25)
// camera follows driver view from 25 points behind, and 10 points above vehicle
cameraNode.position = SCNVector3Make(pos.x, vehicleNode.position.y + 10, pos.z)
}
func convertVectorToAngle(vector: SCNVector3) -> CGFloat {
let degrees = atan2(vector.z, vector.x)
return CGFloat(degrees)
}
func pointInFrontOfPoint(point: SCNVector3, direction: SCNVector3, distance: Float) -> SCNVector3 {
var x = Float()
var y = Float()
var z = Float()
x = point.x + distance * direction.x
y = point.y + distance * direction.y
z = point.z + distance * direction.z
let result = SCNVector3Make(x, y, z)
return result
}
func calculateCameraDirection(cameraNode: SCNNode) -> SCNVector3 {
let x = -cameraNode.rotation.x
let y = -cameraNode.rotation.y
let z = -cameraNode.rotation.z
let w = cameraNode.rotation.w
let cameraRotationMatrix = GLKMatrix3Make(cos(w) + pow(x, 2) * (1 - cos(w)),
x * y * (1 - cos(w)) - z * sin(w),
x * z * (1 - cos(w)) + y*sin(w),
y*x*(1-cos(w)) + z*sin(w),
cos(w) + pow(y, 2) * (1 - cos(w)),
y*z*(1-cos(w)) - x*sin(w),
z*x*(1 - cos(w)) - y*sin(w),
z*y*(1 - cos(w)) + x*sin(w),
cos(w) + pow(z, 2) * ( 1 - cos(w)))
let cameraDirection = GLKMatrix3MultiplyVector3(cameraRotationMatrix, GLKVector3Make(0.0, 0.0, -1.0))
return SCNVector3FromGLKVector3(cameraDirection)
}
func deg2rad(_ number: Double) -> Double {
return number * .pi / 180
}
I've been trying to draw a cylinder between two points on the outer edge of a sphere using SceneKit. I have already produced a line between these two points using primitive geometry and openGL with SCNRendering Delegate, but now I need to produce a cylinder between these two (well, not just two, but any two 3D vectors that sit on the surface of the sphere). I've been working on this for about 3 days straight now, and I've gone through everything I could find on implementing Quaternions to make this happen, but as it stands, I can't get it to work. Academic articles, scientific studies, and nothing, nothing is working to realign a cylinder between two fixed points. I need an algorithm to do this.
Anyway, here's my most recent code that doesn't work, but this is just a small snippet of nearly 2k lines of code I've worked through so far without the intended result. I know I can move to something more advanced like building my own SCNProgram and/or SCNRenderer to then access GLSL, OpenGL, and Metal complexity, but this seems like something that should be possible using Scenekit and converting between GLKit vector structs to and from SCNVector structs, but so far it's impossible:
Code:
The following code ingests Longitude and Latitude coordinates and projects them onto the surface of a 3D sphere. These coordinates are returned through a proprietary function I build where I received a SCNVector3 of {x,y,z} coordinates that display accurately on my 3D sphere. I draw a line between two sets of Longitude and Latitude coordinates where the lines that are drawn using primitives shoot through the center of the sphere. So, as I mentioned above, I want this same functionality but with cylinders, not lines (by the way, the longitude and latitude coordinates listed here are bogus, they are randomly generated but both fall on the Earth's surface).
drawLine = [self lat1:37.76830 lon1:-30.40096 height1:tall lat2:3.97620 lon2:63.73095 height2:tall];
float cylHeight = GLKVector3Distance(SCNVector3ToGLKVector3(cooridnateSetOne.position), SCNVector3ToGLKVector3(coordinateSetTwo.position));
SCNCylinder * cylTest = [SCNCylinder cylinderWithRadius:0.2 height:cylHeight];
SCNNode * test = [SCNNode nodeWithGeometry:cylTest];
SCNMaterial *material = [SCNMaterial material];
[[material diffuse] setContents:[SKColor whiteColor]];
material.diffuse.intensity = 60;
material.emission.contents = [SKColor whiteColor];
material.lightingModelName = SCNLightingModelConstant;
[cylTest setMaterials:#[material]];
GLKVector3 u = SCNVector3ToGLKVector3(cooridnateSetOne.position);
GLKVector3 v = SCNVector3ToGLKVector3(cooridnateSetTwo.position);
GLKVector3 w = GLKVector3CrossProduct(u, v);
GLKQuaternion q = GLKQuaternionMakeWithAngleAndVector3Axis(GLKVector3DotProduct(u,v), GLKVector3Normalize(w));
q.w += GLKQuaternionLength(q);
q = GLKQuaternionNormalize(q);
SCNVector4 final = SCNVector4FromGLKVector4(GLKVector4Make(q.x, q.y, q.z, q.w));
test.orientation = final;
Other code I've tried includes this same sort of method, in fact, I even built my own SCNVector3 and SCNVector4 Math libraries in Objective-C to see if my math methods produced different values than using GLKit maths, but I get the same results with both methods. Any help would be awesome, but for now, I'm not looking to jump into anything more complicated than SceneKit. I won't be diving into Metal and/or OpenGL for another month or two. Thanks!
EDIT:
The variables "cooridnateSetOne" and "cooridnateSetTwo" are SCNNodes that are produced by another function that forces a primitive line geometry into this node and then returns it to a subclass implementation of SCNScene.
Here's a quick demo using node hierarchy (to get the cylinder situated such that its end is at one point and its length is along the local z-axis) and a constraint (to make that z-axis look at another point).
let root = view.scene!.rootNode
// visualize a sphere
let sphere = SCNSphere(radius: 1)
sphere.firstMaterial?.transparency = 0.5
let sphereNode = SCNNode(geometry: sphere)
root.addChildNode(sphereNode)
// some dummy points opposite each other on the sphere
let rootOneThird = CGFloat(sqrt(1/3.0))
let p1 = SCNVector3(x: rootOneThird, y: rootOneThird, z: rootOneThird)
let p2 = SCNVector3(x: -rootOneThird, y: -rootOneThird, z: -rootOneThird)
// height of the cylinder should be the distance between points
let height = CGFloat(GLKVector3Distance(SCNVector3ToGLKVector3(p1), SCNVector3ToGLKVector3(p2)))
// add a container node for the cylinder to make its height run along the z axis
let zAlignNode = SCNNode()
zAlignNode.eulerAngles.x = CGFloat(M_PI_2)
// and position the zylinder so that one end is at the local origin
let cylinder = SCNNode(geometry: SCNCylinder(radius: 0.1, height: height))
cylinder.position.y = -height/2
zAlignNode.addChildNode(cylinder)
// put the container node in a positioning node at one of the points
p2Node.addChildNode(zAlignNode)
// and constrain the positioning node to face toward the other point
p2Node.constraints = [ SCNLookAtConstraint(target: p1Node) ]
Sorry if you were looking for an ObjC-specific solution, but it was quicker for me to prototype this in an OS X Swift playground. (Also, less CGFloat conversion is needed in iOS, because the element type of SCNVector3 is just Float there.)
Just for reference a more elegant SCNCyclinder implementation to connect a start and end position with a given radius:
func makeCylinder(from: SCNVector3, to: SCNVector3, radius: CGFloat) -> SCNNode
{
let lookAt = to - from
let height = lookAt.length()
let y = lookAt.normalized()
let up = lookAt.cross(vector: to).normalized()
let x = y.cross(vector: up).normalized()
let z = x.cross(vector: y).normalized()
let transform = SCNMatrix4(x: x, y: y, z: z, w: from)
let geometry = SCNCylinder(radius: radius,
height: CGFloat(height))
let childNode = SCNNode(geometry: geometry)
childNode.transform = SCNMatrix4MakeTranslation(0.0, height / 2.0, 0.0) *
transform
return childNode
}
Needs the following extension:
extension SCNVector3 {
/**
* Calculates the cross product between two SCNVector3.
*/
func cross(vector: SCNVector3) -> SCNVector3 {
return SCNVector3Make(y * vector.z - z * vector.y, z * vector.x - x * vector.z, x * vector.y - y * vector.x)
}
func length() -> Float {
return sqrtf(x*x + y*y + z*z)
}
/**
* Normalizes the vector described by the SCNVector3 to length 1.0 and returns
* the result as a new SCNVector3.
*/
func normalized() -> SCNVector3 {
return self / length()
}
}
extension SCNMatrix4 {
public init(x: SCNVector3, y: SCNVector3, z: SCNVector3, w: SCNVector3) {
self.init(
m11: x.x,
m12: x.y,
m13: x.z,
m14: 0.0,
m21: y.x,
m22: y.y,
m23: y.z,
m24: 0.0,
m31: z.x,
m32: z.y,
m33: z.z,
m34: 0.0,
m41: w.x,
m42: w.y,
m43: w.z,
m44: 1.0)
}
}
/**
* Divides the x, y and z fields of a SCNVector3 by the same scalar value and
* returns the result as a new SCNVector3.
*/
func / (vector: SCNVector3, scalar: Float) -> SCNVector3 {
return SCNVector3Make(vector.x / scalar, vector.y / scalar, vector.z / scalar)
}
func * (left: SCNMatrix4, right: SCNMatrix4) -> SCNMatrix4 {
return SCNMatrix4Mult(left, right)
}
Thank you, Rickster! I have taken it a little further and made a class out of it:
class LineNode: SCNNode
{
init( parent: SCNNode, // because this node has not yet been assigned to a parent.
v1: SCNVector3, // where line starts
v2: SCNVector3, // where line ends
radius: CGFloat, // line thicknes
radSegmentCount: Int, // number of sides of the line
material: [SCNMaterial] ) // any material.
{
super.init()
let height = v1.distance(v2)
position = v1
let ndV2 = SCNNode()
ndV2.position = v2
parent.addChildNode(ndV2)
let ndZAlign = SCNNode()
ndZAlign.eulerAngles.x = Float(M_PI_2)
let cylgeo = SCNCylinder(radius: radius, height: CGFloat(height))
cylgeo.radialSegmentCount = radSegmentCount
cylgeo.materials = material
let ndCylinder = SCNNode(geometry: cylgeo )
ndCylinder.position.y = -height/2
ndZAlign.addChildNode(ndCylinder)
addChildNode(ndZAlign)
constraints = [SCNLookAtConstraint(target: ndV2)]
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
I have tested this class successfully in an iOS app, using this function,
which draws 100 lines (oops cylinders :o).
func linesTest3()
{
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.whiteColor()
mat.specular.contents = UIColor.whiteColor()
for _ in 1...100 // draw 100 lines (as cylinders) between random points.
{
let v1 = SCNVector3( x: Float.random(min: -50, max: 50),
y: Float.random(min: -50, max: 50),
z: Float.random(min: -50, max: 50) )
let v2 = SCNVector3( x: Float.random(min: -50, max: 50),
y: Float.random(min: -50, max: 50),
z: Float.random(min: -50, max: 50) )
// Just for testing, add two little spheres to check if lines are drawn correctly:
// each line should run exactly from a green sphere to a red one:
root.addChildNode(makeSphere(v1, radius: 0.5, color: UIColor.greenColor()))
root.addChildNode(makeSphere(v2, radius: 0.5, color: UIColor.redColor()))
// Have to pass the parentnode because
// it is not known during class instantiation of LineNode.
let ndLine = LineNode(
parent: scene.rootNode, // ** needed
v1: v1, // line (cylinder) starts here
v2: v2, // line ends here
radius: 0.2, // line thickness
radSegmentCount: 6, // hexagon tube
material: [mat] ) // any material
root.addChildNode(ndLine)
}
}
Regards. (btw. I can only see 3D objects.. I have never seen a "line" in my life :o)
i use SCNVector3 extensions with:
func cylVector(from : SCNVector3, to : SCNVector3) -> SCNNode {
let vector = to - from,
length = vector.length()
let cylinder = SCNCylinder(radius: cylsRadius, height: CGFloat(length))
cylinder.radialSegmentCount = 6
cylinder.firstMaterial = material
let node = SCNNode(geometry: cylinder)
node.position = (to + from) / 2
node.eulerAngles = SCNVector3Make(CGFloat(Double.pi/2), acos((to.z-from.z)/length), atan2((to.y-from.y), (to.x-from.x) ))
return node
}
Here's an entire method using Objective-C
First, here's how you use it:
SCNNode * testNode = [self lat1:-35 lon1:108 height1:tall lat2:-35 lon2:30 height2:0];
Inputs:
1rst location
lat1 = latitude of 1rst location
lon1 = longitude of 1rst location
height1 = distance from earth for 1rst location
lat2 = latitude of 2nd location
lon2 = latitude of 2nd location
height2 = distance from earth for 2nd location
The second method creates the SCNVector3 points for each location in question above:
-(SCNNode *)lat1:(double)lat1 lon1:(double)lon1 height1:(float)height1 lat2:(double)lat2 lon2:(double)lon2 height2:(float)height2 {
SCNVector3 positions[] = {[self lat:lat1 lon:lon1 height:height1], [self lat:lat2 lon:lon2 height:height2]};
float cylHeight = GLKVector3Distance(SCNVector3ToGLKVector3(positions[0]), SCNVector3ToGLKVector3(positions[1]))/4;
SCNCylinder * masterCylinderNode = [SCNCylinder cylinderWithRadius:0.05 height:cylHeight];
SCNMaterial *material = [SCNMaterial material];
[[material diffuse] setContents:[SKColor whiteColor]];
material.lightingModelName = SCNLightingModelConstant;
material.emission.contents = [SKColor whiteColor];
[masterCylinderNode setMaterials:#[material]];
SCNNode *mainLocationPointNodeTestA = [mainLocationPointNode clone];
SCNNode *mainLocationPointNodeTestB = [mainLocationPointNode clone];
mainLocationPointNodeTestA.position = positions[0];
mainLocationPointNodeTestB.position = positions[1];
SCNNode * mainParentNode = [SCNNode node];
SCNNode * tempNode2 =[SCNNode nodeWithGeometry:masterCylinderNode];
[mainParentNode addChildNode:mainLocationPointNodeTestA];
[mainParentNode addChildNode:mainLocationPointNodeTestB];
[mainParentNode addChildNode:tempNode2];
[mainParentNode setName:#"parentToLineNode"];
tempNode2.position = SCNVector3Make((positions[0].x+positions[1].x)/2, (positions[0].y+positions[1].y)/2, (positions[0].z+positions[1].z)/2);
tempNode2.pivot = SCNMatrix4MakeTranslation(0, cylHeight*1.5, 0);
GLKVector3 normalizedVectorStartingPosition = GLKVector3Make(0.0, 1.0, 0.0);
GLKVector3 magicAxis = GLKVector3Normalize(GLKVector3Subtract(GLKVector3Make(positions[0].x/2, positions[0].y/2, positions[0].z/2), GLKVector3Make(positions[1].x/2, positions[1].y/2, positions[1].z/2)));
GLKVector3 rotationAxis = GLKVector3CrossProduct(normalizedVectorStartingPosition, magicAxis);
CGFloat rotationAngle = GLKVector3DotProduct(normalizedVectorStartingPosition, magicAxis);
GLKVector4 rotation = GLKVector4MakeWithVector3(rotationAxis, acos(rotationAngle));
tempNode2.rotation = SCNVector4FromGLKVector4(rotation);
return mainParentNode;
}
This second method uses hard coded numbers for earth's radius and curvature, I'm showing this just to show the numbers required for total 100% accuracy, this is how it works. You'll want to change this to the correct dimensions for your scene, obviously, but here's the method. This is an adaptation of methods used by Link. An explanation an be found here: Link. I put this together very quickly but it works and is accurate, feel free to change the number formats to your liking.
-(SCNVector3)lat:(double)lat lon:(double)lon height:(float)height {
double latd = 0.0174532925;
double latitude = latd*lat;
double longitude = latd*lon;
Float64 rad = (Float64)(6378137.0);
Float64 f = (Float64)(1.0/298.257223563);
double cosLat = cos(latitude);
double sinLat = sin(latitude);
double FF = pow((1.0-f), 2);
double C = 1/(sqrt(pow(cosLat,2) + FF * pow(sinLat,2)));
double S = C * FF;
double x = ((rad * C)*cosLat * cos(longitude))/(1000000/(1+height));
double y = ((rad * C)*cosLat * sin(longitude))/(1000000/(1+height));
double z = ((rad * S)*sinLat)/(1000000/(1+height));
return SCNVector3Make(y+globeNode.position.x, z+globeNode.position.y, x+globeNode.position.z);
}
I have been looking for a solution to make cylinder between two points and thanks to rickster, I have used his answer to make SCNNode extension. There, I have added missing conditions for a possible cylinder orientation to avoid its wrong opposite direction.
func makeCylinder(positionStart: SCNVector3, positionEnd: SCNVector3, radius: CGFloat , color: NSColor, transparency: CGFloat) -> SCNNode
{
let height = CGFloat(GLKVector3Distance(SCNVector3ToGLKVector3(positionStart), SCNVector3ToGLKVector3(positionEnd)))
let startNode = SCNNode()
let endNode = SCNNode()
startNode.position = positionStart
endNode.position = positionEnd
let zAxisNode = SCNNode()
zAxisNode.eulerAngles.x = CGFloat(M_PI_2)
let cylinderGeometry = SCNCylinder(radius: radius, height: height)
cylinderGeometry.firstMaterial?.diffuse.contents = color
let cylinder = SCNNode(geometry: cylinderGeometry)
cylinder.position.y = -height/2
zAxisNode.addChildNode(cylinder)
let returnNode = SCNNode()
if (positionStart.x > 0.0 && positionStart.y < 0.0 && positionStart.z < 0.0 && positionEnd.x > 0.0 && positionEnd.y < 0.0 && positionEnd.z > 0.0)
{
endNode.addChildNode(zAxisNode)
endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
returnNode.addChildNode(endNode)
}
else if (positionStart.x < 0.0 && positionStart.y < 0.0 && positionStart.z < 0.0 && positionEnd.x < 0.0 && positionEnd.y < 0.0 && positionEnd.z > 0.0)
{
endNode.addChildNode(zAxisNode)
endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
returnNode.addChildNode(endNode)
}
else if (positionStart.x < 0.0 && positionStart.y > 0.0 && positionStart.z < 0.0 && positionEnd.x < 0.0 && positionEnd.y > 0.0 && positionEnd.z > 0.0)
{
endNode.addChildNode(zAxisNode)
endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
returnNode.addChildNode(endNode)
}
else if (positionStart.x > 0.0 && positionStart.y > 0.0 && positionStart.z < 0.0 && positionEnd.x > 0.0 && positionEnd.y > 0.0 && positionEnd.z > 0.0)
{
endNode.addChildNode(zAxisNode)
endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
returnNode.addChildNode(endNode)
}
else
{
startNode.addChildNode(zAxisNode)
startNode.constraints = [ SCNLookAtConstraint(target: endNode) ]
returnNode.addChildNode(startNode)
}
return returnNode
}