swift SceneKit calculate node angle - swift

i trying to add a node (pin) to sphere node (when taping on sphere node), but can't correctly calculate an angle, can u help me pls ?
private func addPinToSphere(result: SCNHitTestResult) {
guard let scene = SCNScene(named: "art.scnassets/tree.scn") else { return }
guard let treeNode = scene.rootNode.childNode(withName: "Trunk", recursively: true) else { return }
let x = CGFloat(result.worldCoordinates.x)
let y = CGFloat(result.worldCoordinates.y)
treeNode.position = result.worldCoordinates
treeNode.eulerAngles = SCNVector3((x) * .pi / CGFloat(180), 0, (y - 90) * .pi / CGFloat(180)) // y axis is ignorable
result.node.addChildNode(treeNode)
}
when run code i have this
but want like this

You can do it similar to this code. This is a missile launcher, but it's basically the same thing. Your pin = my fireTube, so create a base node, then add a subnode with your materials to it and rotate it into place. You'll have to experiment with where to place the tube initially, but once you get it into the right place the lookat constraint will always point it in the right direction.
case .d1, .d2, .d3, .d4, .d5, .d6, .d7, .d8, .d9, .d10:
let BoxGeometry = SCNBox(width: 0.8, height: 0.8, length: 0.8, chamferRadius: 0.0)
let vNode = SCNNode(geometry: BoxGeometry)
BoxGeometry.materials = setDefenseTextures(vGameType: vGameType)
let tubeGeometry = SCNTube(innerRadius: 0.03, outerRadius: 0.05, height: 0.9)
let fireTube = SCNNode(geometry: tubeGeometry)
tubeGeometry.firstMaterial?.diffuse.contents = data.getTextureColor(vTheme: 0, vTextureType: .barrelColor)
fireTube.position = SCNVector3(0, 0.2, -0.3)
let vRotateX = SCNAction.rotateBy(x: CGFloat(Float(GLKMathDegreesToRadians(-90))), y: 0, z: 0, duration: 0)
fireTube.runAction(vRotateX)
vNode.addChildNode(fireTube)
return vNode
Then set target on your base node and your subnode will rotate with it:
func setTarget()
{
node.constraints = []
let vConstraint = SCNLookAtConstraint(target: targetNode)
vConstraint.isGimbalLockEnabled = true
node.constraints = [vConstraint]
}
In your case, target equals center mass of your sphere and the pin will always point to it, provided you built and aligned your box and pin correctly.

Related

SceneKit SCNPhysicsBody performance

SceneKit SCNPhysicsBody performance doesn't seem to take advantage of the multiple cores.
I created a quick bit of code that puts a bunch of small spheres on top of a cube where they drop under gravity. As the number of spheres increases things get very slow. I don't see a way of getting optimal multicore performance. Anyone have any ideas?
I did some quick tests with a stopwatch and observing the simulation to a specific point launching 1,5 and 8 instances of the app.
1 instance running : 15 seconds
2 instances running : 25 seconds
8 instances running : 35 seconds
It is clear that the multcores are not being used optimally for simulation with just one instance.
func createCubeWithSpheres(bottomLeft: SCNVector3, topRight: SCNVector3, radiusOfSpheres: Double) -> SCNNode {
// Create the box using the bottomLeft and topRight coordinates
let box = SCNBox(
width: topRight.x - bottomLeft.x,
height: topRight.y - bottomLeft.y,
length: topRight.z - bottomLeft.z,
chamferRadius: 0.0
)
let node = SCNNode(geometry: box)
// Add a static physics body to the box
node.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
// Calculate the dimensions of the box
let width = box.width
let height = box.height
let depth = box.length
// Calculate the maximum number of spheres that can fit inside the box
let maxNumSpheresWidth = Int(width / (radiusOfSpheres * 2))
let maxNumSpheresHeight = Int(height / (radiusOfSpheres * 2))
let maxNumSpheresDepth = Int(depth / (radiusOfSpheres * 2))
let maxNumSpheres = maxNumSpheresWidth * maxNumSpheresHeight * maxNumSpheresDepth
// Create an array to hold the spheres
var spheres = [Sphere]()
// Create the spheres and add them to the array
for zIndex in 0..<maxNumSpheresDepth {
for yIndex in 0..<maxNumSpheresHeight {
for xIndex in 0..<maxNumSpheresWidth {
let xPos = bottomLeft.x + radiusOfSpheres + (radiusOfSpheres * 2) * CGFloat(xIndex)
let yPos = bottomLeft.y + radiusOfSpheres + (radiusOfSpheres * 2) * CGFloat(yIndex)
let zPos = bottomLeft.z + radiusOfSpheres + (radiusOfSpheres * 2) * CGFloat(zIndex)
let sphere = Sphere(radius: radiusOfSpheres, position: SCNVector3(xPos, yPos, zPos))
spheres.append(sphere)
}
}
}
// Add the spheres to the box's node
for sphere in spheres {
node.addChildNode(sphere.node)
}
return node
}
class Sphere {
let node = SCNNode()
// Initialize the sphere with a radius and position.
init(radius: CGFloat, position: SCNVector3? = nil) {
node.geometry = SCNSphere(radius: radius)
if let position = position {
node.position = position
}
// Add a physics body to the sphere so that it can interact with other objects.
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
// Add a "stickiness" to the sphere by applying a static electric field to it.
// let electricField = SCNPhysicsField.electric()
// electricField.strength = 10 // Adjust this value to increase or decrease the stickiness of the sphere.
// node.physicsField = electricField
}
}
let box = createCubeWithSpheres(bottomLeft: SCNVector3(x: 0.0, y: 0.0, z: 0.0), topRight: SCNVector3(x: 5.0, y: 5.0, z: 5.0), radiusOfSpheres: 0.1)
scene.rootNode.addChildNode(box)
Anyway to speed this up with one instance running without giving up collision accuracy?

Align a node with its neighbor in SceneKit

Using Swift 5.5, iOS 14
Trying to create a simple 3D bar chart and I immediately find myself in trouble.
I wrote this code...
var virgin = true
for i in stride(from: 0, to: 6, by: 0.5) {
let rnd = CGFloat.random(in: 1.0...4.0)
let targetGeometry = SCNBox(width: 0.5,
height: rnd,
length: 0.5,
chamferRadius: 0.2)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let box = SCNNode(geometry: targetGeometry)
box.simdPosition = SIMD3(x: 0, y: 0, z: 0)
coreNode.addChildNode(box)
}
This works well, but all the bars a centred around their centre. But how can I ask SceneKit to change the alignment?
Almost got this working with this code...
box.simdPosition = SIMD3(x: Float(i) - 3,
y: Float(rnd * 0.5),
z: 0)
But the result isn't right... I want/need the bar to grow from the base.
https://youtu.be/KJgvdBFBfyc
How can I make this grow from the base?
--Updated with working solution--
Tried Andy Jazz suggestion replacing simdposition with the following formulae.
box.simdPosition = SIMD3(x:box.simdPosition.x, y:0, z:0)
box.simdPivot.columns.3.y = box.boundingBox.max.y - Float(rnd * 0.5)
Worked well, to which I added some animation! Thanks Andy.
changer = changeling.sink(receiveValue: { [self] _ in
var rnds:[CGFloat] = []
for i in 0..<12 {
let rnd = CGFloat.random(in: 1.0...2.0)
let targetGeometry = SCNBox(width: 0.45, height: rnd, length: 0.45, chamferRadius: 0.01)
let newNode = SCNNode(geometry: targetGeometry)
sourceNodes[i].simdPivot.columns.3.y = newNode.boundingBox.min.y
sourceNodes[i].simdPosition = SIMD3(x: sourceNodes[i].simdPosition.x, y: 0, z: 0)
rnds.append(rnd)
}
for k in 0..<12 {
if virgin {
coreNode.addChildNode(sourceNodes[k])
}
let targetGeometry = SCNBox(width: 0.45, height: rnds[k], length: 0.45, chamferRadius: 0.01)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let morpher = SCNMorpher()
morpher.targets = [targetGeometry]
sourceNodes[k].morpher = morpher
let animation = CABasicAnimation(keyPath: "morpher.weights[0]")
animation.toValue = 1.0
animation.repeatCount = 0.0
animation.duration = 1.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
sourceNodes[k].addAnimation(animation, forKey: nil)
}
virgin = false
})
You have to position a pivot point of each bar to its base:
boxNode.simdPivot.columns.3.y = someFloatNumber
To reposition a pivot to bar's base use bounding box property:
boxNode.simdPivot.columns.3.y += (boxNode.boundingBox.min.y as? simd_float1)!
After pivot's offset, reposition boxNode towards negative direction of Y-axis.
boxNode.position.y = 0

How to place nodes on the ground?

I am creating a game with SpriteKit. The background is a png image, endlessly moving (parallax scroll):
func parallaxScroll(image: String, y: CGFloat, z: CGFloat, duration: Double, needsPhysics: Bool) {
for i in 0 ... 1 {
// position the first node on the left, and position second on the right
let node = SKSpriteNode(imageNamed: image)
node.position = CGPoint(x: 1023 * CGFloat(i), y: y)
node.zPosition = z
addChild(node)
if needsPhysics {
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.texture!.size())
node.physicsBody?.isDynamic = false
node.physicsBody?.contactTestBitMask = 1
node.name = "ground"
}
// make this node move the width of the screen by whatever duration was passed in
let move = SKAction.moveBy(x: -1024, y: 0, duration: duration)
// make it jump back to the right edge
let wrap = SKAction.moveBy(x: 1024, y: 0, duration: 0)
// make these two as a sequence that loops forever
let sequence = SKAction.sequence([move, wrap])
let forever = SKAction.repeatForever(sequence)
// run the animations
node.run(forever)
}
}
The example function below places a box at random y position:
#objc func createObstacle() {
let obstacle = SKSpriteNode(imageNamed: "rectangle")
obstacle.zPosition = -2
obstacle.position.x = 768
addChild(obstacle)
obstacle.physicsBody = SKPhysicsBody(texture: obstacle.texture!, size: obstacle.texture!.size())
obstacle.physicsBody?.isDynamic = false
obstacle.physicsBody?.contactTestBitMask = 1
obstacle.name = "obstacle"
let rand = GKRandomDistribution(lowestValue: -200, highestValue: 350)
obstacle.position.y = CGFloat(rand.nextInt())
// make it move across the screen
let action = SKAction.moveTo(x: -768, duration: 9)
obstacle.run(action)
}
func playerHit(_ node: SKNode) {
if node.name == "obstacle" {
player.removeFromParent()
}
}
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node else { return }
guard let nodeB = contact.bodyB.node else { return }
if nodeA == player {
playerHit(nodeB)
} else if nodeB == player {
playerHit(nodeA)
}
}
Instead of placing it at random, I would like to place it on the "ground". The following image illustrates the current placement and the desired placement:
Does anyone know how to place an obstacle node (object) on the ground which is not flat instead of random y position?
You could cast a ray, at the box's intended x position, from the top of the screen to the bottom, and when the ray hits the ground, place the box directly above the hitpoint. See enumerateBodies(alongRayStart:end:using:). In your case you might want to insert into createObstacle something like:
let topOfScreen = size.height / 2
let bottomOfScreen = -size.height / 2
obstacle.position.x = -768
physicsWorld.enumerateBodies(alongRayStart: CGPoint(x: obstacle.position.x, y: topOfScreen), end: CGPoint(x: obstacle.position.x, y: bottomOfScreen)) { body, point, normal, stop in
if body.node?.name == "ground" {
// body is the ground's physics body. point is the point that an object
// falling from the sky would hit the ground.
// Assuming the obstacle's anchor point is at its center, its actual position
// would be slightly above this point
let positionAboveBody = point + (obstacle.size.height / 2)
obstacle.position.y = positionAboveBody
}
}

Rotate SCNNode then move relative to rotation?

My node setup:
let node = SCNNode()
node.position = SCNVector3(0.0, 0.0, 0.0)
I then wish to rotate the node by 90 degrees to the left which can be achieved with:
node.transform = SCNMatrix4Rotate(node.transform, .pi/2, 0.0, 1.0, 0.0)
Next, I want to translate the node forward one unit along the negative z axis relative to current rotation and end up with:
node.position = SCNVector3(-1.0, 0.0, 0.0)
I have no idea how to get from the rotation of the node to the position of the node programmatically.
Basically node begins at (0, 0, 0), its forward vector is the -z axis, node turns left and moves one unit forward to end up at (-1, 0, 0).
This isn't working:
func move(_ direction: moveDirection) {
switch direction {
case .forward: characterNode.position = SCNVector3(characterNode.position.x, characterNode.position.y, characterNode.position.z - 1.0)
case .left: characterNode.pivot = SCNMatrix4Rotate(characterNode.pivot, -.pi/32, 0.0, 1.0, 0.0)
case .backward: characterNode.position = SCNVector3(characterNode.position.x, characterNode.position.y, characterNode.position.z + 1.0)
case .right: characterNode.pivot = SCNMatrix4Rotate(characterNode.pivot, .pi/32, 0.0, 1.0, 0.0)
}
}
Assuming I have understood you correctly, once you have rotated your SCNNode, you want to move it forward it in that direction.
This can be done by using the worldFront value which is simply:
The local unit -Z axis (0,0,-1) in world space.
As such this may be the answer you are looking for:
//1. Create A Node Holder
let nodeToAdd = SCNNode()
//2. Create An SCNBox Geometry
let nodeGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
nodeGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
nodeGeometry.firstMaterial?.lightingModel = .constant
nodeToAdd.geometry = nodeGeometry
//3. Position It 1.5m Away From The Camera
nodeToAdd.position = SCNVector3(0, 0, -1.5)
//4. Rotate The Node By 90 Degrees On It's Y Axis
nodeToAdd.rotation = SCNVector4Make(0, 1, 0, .pi / 2)
//5. Add The Node To The ARSCNView
augmentedRealityView.scene.rootNode.addChildNode(nodeToAdd)
//6. Use The World Front To Move The Node Forward e.g
/*
nodeToAdd.simdPosition += nodeToAdd.simdWorldFront * 1.2
*/
nodeToAdd.simdPosition += nodeToAdd.simdWorldFront
//7. Print The Position
print(nodeToAdd.position)
/*
SCNVector3(x: -0.999999881, y: 0.0, z: -1.50000024)
*/

Creating a box with SceneKit and ARKit

I am trying to create a primitive with SceneKit and ARKit. For whatever reason, it is not working.
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
let node = SCNNode(geometry: box)
node.position = SCNVector3(0,0,0)
sceneView.scene.rootNode.addChildNode(node)
Do I need to take in the camera coordinates as well?
Your code looks good and it should work. I have tried it as the below code: after creating a new app with ARKit template, I have replaced the function viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
let node = SCNNode(geometry: box)
node.position = SCNVector3(0,0,0)
sceneView.scene.rootNode.addChildNode(node)
}
It creates a box at the original point (0, 0, 0). Unfortunately your device is inside the box thus you cannot see that box straightly. To see the box, move your device far aways a bit.
The attached image is the box after moving my device:
If you want to see it immediately, move the box to front a bit, add colour and make the first material be double side (to see it even in or out side):
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
box.firstMaterial?.diffuse.contents = UIColor.red
box.firstMaterial?.isDoubleSided = true
let boxNode = SCNNode(geometry: box)
boxNode.position = SCNVector3(0, 0, -1)
sceneView.scene.rootNode.addChildNode(boxNode)
You should get the location tapped and use the world coordinates to place the cube properly. I'm not sure (0,0,0) is a normal location for ARKit. You can try something like this:
Put this in your viewDidLoad:
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapFrom))
tapGestureRecognizer.numberOfTapsRequired = 1
self.sceneView.addGestureRecognizer(tapGestureRecognizer)
Then add this method:
#objc func handleTapFrom(recognizer: UITapGestureRecognizer) {
let tapPoint = recognizer.location(in: self.sceneView)
let result = self.sceneView.hitTest(tapPoint, types: ARHitTestResult.ResultType.existingPlaneUsingExtent)
if result.count == 0 {
return
}
let hitResult = result.first
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
let node = SCNNode(geometry: box)
node.physicsBody = SCNPhysicsBody(type: SCNPhysicsBodyType.static, shape: nil)
node.position = SCNVector3Make(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
sceneView.scene.rootNode.addChildNode(node)
}
Then when you tap on a plane surface detected, it will add a box on the plane where you tapped.