How do i return the radius of a sphere geometry(SCNSphere) used to set up a SCNNode.
I want to use the radius in a method where i move some child nodes in relation to a parent node. The code below fails since radius appears to be unknown to the resulting node, should i not pass the node to the method?
Also my array index fails saying that Int is not a Range.
I am trying to build something from this
import UIKit
import SceneKit
class PrimitivesScene: SCNScene {
override init() {
super.init()
self.addSpheres();
}
func addSpheres() {
let sphereGeometry = SCNSphere(radius: 1.0)
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
let sphereNode = SCNNode(geometry: sphereGeometry)
self.rootNode.addChildNode(sphereNode)
let secondSphereGeometry = SCNSphere(radius: 0.5)
secondSphereGeometry.firstMaterial?.diffuse.contents = UIColor.greenColor()
let secondSphereNode = SCNNode(geometry: secondSphereGeometry)
secondSphereNode.position = SCNVector3(x: 0, y: 1.25, z: 0.0)
self.rootNode.addChildNode(secondSphereNode)
self.attachChildrenWithAngle(sphereNode, children:[secondSphereNode, sphereNode], angle:20)
}
func attachChildrenWithAngle(parent: SCNNode, children:[SCNNode], angle:Int) {
let parentRadius = parent.geometry.radius //This fails cause geometry does not know radius.
for var index = 0; index < 3; ++index{
children[index].position=SCNVector3(x:Float(index),y:parentRadius+children[index].radius/2, z:0);// fails saying int is not convertible to range.
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The issue with radius is that parent.geometry returns a SCNGeometry and not an SCNSphere. If you need to get the radius, you'll need to cast parent.geometry to SCNSphere first. To be safe, it's probably best to use some optional binding and chaining to do that:
if let parentRadius = (parent.geometry as? SCNSphere)?.radius {
// use parentRadius here
}
You'll also need to do that when accessing the radius on your children nodes. If you put all that together and clean things up a little, you get something like this:
func attachChildrenWithAngle(parent: SCNNode, children:[SCNNode], angle:Int) {
if let parentRadius = (parent.geometry as? SCNSphere)?.radius {
for var index = 0; index < 3; ++index{
let child = children[index]
if let childRadius = (child.geometry as? SCNSphere)?.radius {
let radius = parentRadius + childRadius / 2.0
child.position = SCNVector3(x:CGFloat(index), y:radius, z:0.0);
}
}
}
}
Note though that you're calling attachChildrenWithAngle with an array of 2 children:
self.attachChildrenWithAngle(sphereNode, children:[secondSphereNode, sphereNode], angle:20)
If you do that, you're going to get a runtime crash in that for loop when accessing the 3rd element. You'll either need to pass an array with 3 children every time you call that function, or change the logic in that for loop.
At this point I'm only working with spheres, hence the radius is quite easy to manipulate. But still, I think you should try to look at the geometry of the SCNNode. I use this function besides the code snippet you posted (the two works together just fine for me).
func updateRadiusOfNode(_ node: SCNNode, to radius: CGFloat) {
if let sphere = (node.geometry as? SCNSphere) {
if (sphere.radius != radius) {
sphere.radius = radius
}
}
}
Also the scaling factor (at the radius) is also important. I use a baseRadius then i'm multiplying that with
maxDistanceObserved / 2
Always updating the max distance if a hitTest has futher results.
Related
I place 3d object in the world space. After that I try to move camera randomly. Then right now I need to know after I knew object has became inside frustum by isNode method, if the object is in center, top or bottom of camera view.
For a solution that's not a hack you can use the projectPoint: API.
It's probably better to work with pixel coordinates because this method uses the actual camera's settings to determine where the object appears on screen.
let projectedPoint = sceneView.projectPoint(self.sphereNode.worldPosition)
let xOffset = projectedPoint.x - screenCenter.x;
let yOffset = projectedPoint.y - screenCenter.y;
if xOffset * xOffset + yOffset * yOffset < R_squared {
// inside a disc of radius 'R' at the center of the screen
}
Solution
To achieve this you need to use a trick. Create new SCNCamera, make it a child of pointOfView default camera and set its FoV to approximately 10 degrees.
Then inside renderer(_:updateAtTime:) instance method use isNode(:insideFrustumOf:) method.
Here's working code:
import ARKit
class ViewController: UIViewController,
ARSCNViewDelegate,
SCNSceneRendererDelegate {
#IBOutlet var sceneView: ARSCNView!
#IBOutlet var label: UILabel!
let cameraNode = SCNNode()
let sphereNode = SCNNode()
let config = ARWorldTrackingConfiguration()
public func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
DispatchQueue.main.async {
if self.sceneView.isNode(self.sphereNode,
insideFrustumOf: self.cameraNode) {
self.label.text = "In the center..."
} else {
self.label.text = "Out OF CENTER"
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.allowsCameraControl = true
let scene = SCNScene()
sceneView.scene = scene
cameraNode.camera = SCNCamera()
cameraNode.camera?.fieldOfView = 10
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sceneView.pointOfView!.addChildNode(self.cameraNode)
}
sphereNode.geometry = SCNSphere(radius: 0.05)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
sphereNode.position.z = -1.0
sceneView.scene.rootNode.addChildNode(sphereNode)
sceneView.session.run(config)
}
}
Also, in this solution you may turn on an orthographic projection for child camera, instead of perspective one. It helps when a model is far from the camera.
cameraNode.camera?.usesOrthographicProjection = true
Here's how your screen might look like:
Next steps
The same way you can append two additional SCNCameras, place them above and below central SCNCamera, and test your object with two extra isNode(:insideFrustumOf:) instance methods.
I solved problem with another way:
let results = self.sceneView.hitTest(screenCenter!, options: [SCNHitTestOption.rootNode: parentnode])
where parentnode is the parent of target node, because I have multiple nodes.
func nodeInCenter() -> SCNNode? {
let x = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).x - sceneView.projectPoint(sphereNode.worldPosition).x) ^^ 2) < 9
let y = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).y - sceneView.projectPoint(sphereNode.worldPosition).y) ^^ 2) < 9
if x && y {
return node
}
return nil
}
I am trying to build a simple iOS game using entity-component architecture similar to what is described here.
What I would like to achieve in my game is when a user touches the screen, detect where the touch occurred and move all entities of one type towards a specific direction (direction depends on where the user touched, right of screen = up, left of screen = down).
So far, the game is really simple and I am only getting started, but I am stuck in this simple functionality:
My issue is that an SKAction is supposed to run on all entities of a type, but happens at all.
Before I redesigned my game to an ECS approach, this worked fine.
Here is the GKEntity subclass that I declared in Lines.swift:
class Lines: GKEntity {
override init() {
super.init()
let LineSprite = SpriteComponent(color: UIColor.white, size: CGSize(width: 10.0, height: 300))
addComponent(LineSprite)
// Set physics body
if let sprite = component(ofType: SpriteComponent.self)?.node {
sprite.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: sprite.size.width, height: sprite.size.height))
sprite.physicsBody?.isDynamic = false
sprite.physicsBody?.restitution = 1.0
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.linearDamping = 0.0
sprite.physicsBody?.angularDamping = 0.0
sprite.physicsBody?.mass = 0.00
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.usesPreciseCollisionDetection = true
sprite.physicsBody?.categoryBitMask = 0b1
sprite.zPosition = 10
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In TouchesBegan I am calling the function Move(XAxisPoint: t.location(in: self)) which is declared in GameScene and here is what Move() does:
///Determines direction of movement based on touch location, calls MoveUpOrDown for movement
func move(XAxisPoint: CGPoint){
let Direction: SKAction
let Key: String
if XAxisPoint.x >= 0 {
Direction = SKAction.moveBy(x: 0, y: 3, duration: 0.01)
Key = "MovingUp"
} else {
Direction = SKAction.moveBy(x: 0, y: -3, duration: 0.01)
Key = "MovingDown"
}
moveUpOrDown(ActionDirection: Direction, ActionKey: Key)
}
///Moves sprite on touch
func moveUpOrDown(ActionDirection: SKAction, ActionKey: String) {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
if sprite.action(forKey: ActionKey) == nil {
stopMoving()
let repeatAction = SKAction.repeatForever(ActionDirection)
sprite.run(repeatAction, withKey: ActionKey)
}
}
}
///Stops movement
func stopMoving() {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
sprite.removeAllActions()
}
}
I am guessing there is some issue with this line of code Line.component(ofType: SpriteComponent.self)?.node but the compiler doesn't throw any errors and I am not sure where my mistake is.
Any help/guidance will be greatly appreciated!
The issue is the following line in MoveUpOrDown and StopMoving
let Line = Lines()
It's creating a new Lines object then telling it to run an action. Since it's new, it hasn't been added to the scene so it isn't drawn or acted on.
You should be getting an existing Lines object and modifying that instead of creating a new one.
As a side note, the common convention for naming methods and variables is to use camelCase which means MoveUpOrDown should be moveUpOrDown. On the other hand SnakeCase is used For classes structs and protocols so SpriteComponent is current. That allows you to know at a glance whether your working with a type or a variable.
I am making an app using ARKit to measure between two points. The goal is to be able to measure length, store that value, then measure width and store.
The problem I am having is disposing of the nodes after I get the measurement.
Steps so far:
1) added a button with a restartFunction. this worked to reset the measurements but did not remove the spheres from scene, and also made getting the next measurement clunky.
2) set a limit on > 2 nodes. This functionaly works best. But again the spheres just stay floating in the scene.
Here is a screen shot of the best result I have had.
#objc func handleTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation, types: .featurePoint)
if let result = hitTestResults.first {
let position = SCNVector3.positionFrom(matrix: result.worldTransform)
let sphere = SphereNode(position: position)
sceneView.scene.rootNode.addChildNode(sphere)
let tail = nodes.last
nodes.append(sphere)
if tail != nil {
let distance = tail!.position.distance(to: sphere.position)
infoLabel.text = String(format: "Size: %.2f inches", distance)
if nodes.count > 2 {
nodes.removeAll()
}
} else {
nodes.append(sphere)
}
}
}
I am new to Swift (coding in general) and most of my code has come from piecing together tutorials.
I think the issue here is that your are not actually removing the SCNNodes you have added to hierarchy.
Although you are removing the nodes from what I assume is an array of SCNNodes by calling: nodes.removeAll(), you first need to actually remove them from the scene hierarchy.
So what you need to do is call the following function on any node you wish to remove:
removeFromParentNode()
Which simply:
Removes the node from its parent’s array of child nodes.
As such you would do something like this which first removes the nodes from the hierarchy, and then removes them from the array:
for nodeAdded in nodesArray{
nodeAdded.removeFromParentNode()
}
nodesArray.removeAll()
So based on the code provided you could do the following:
if nodes.count > 2 {
for nodeAdded in nodes{
nodeAdded.removeFromParentNode()
}
nodes.removeAll()
}
For future reference, if you want to remove all SCNNodes from you hierarchy you can also call:
self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (existingNode, _) in
existingNode.removeFromParentNode()
}
Whereby self.augmentedRealityView refers to the variable:
var augmentedRealityView: ARSCNView!
Here is a very basic working example based on (and modified from) the code you have provided:
/// Places A Marker Node At The Desired Tap Point
///
/// - Parameter sender: UITapGestureRecognizer
#objc func handleTap(_ sender: UITapGestureRecognizer) {
//1. Get The Current Tap Location
let currentTapLocation = sender.location(in: sceneView)
//2. Check We Have Hit A Feature Point
guard let hitTestResult = self.augmentedRealityView.hitTest(currentTapLocation, types: .featurePoint).first else { return }
//3. Get The World Position From The HitTest Result
let worldPosition = positionFromMatrix(hitTestResult.worldTransform)
//4. Create A Marker Node
createSphereNodeAt(worldPosition)
//5. If We Have Two Nodes Then Measure The Distance
if let distance = distanceBetweenNodes(){
print("Distance == \(distance)")
}
}
/// Creates A Marker Node
///
/// - Parameter position: SCNVector3
func createSphereNodeAt(_ position: SCNVector3){
//1. If We Have More Than 2 Nodes Remove Them All From The Array & Hierachy
if nodes.count >= 2{
nodes.forEach { (nodeToRemove) in
nodeToRemove.removeFromParentNode()
}
nodes.removeAll()
}
//2. Create A Marker Node With An SCNSphereGeometry & Add It To The Scene
let markerNode = SCNNode()
let markerGeometry = SCNSphere(radius: 0.01)
markerGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
markerNode.geometry = markerGeometry
markerNode.position = position
sceneView.scene.rootNode.addChildNode(markerNode)
//3. Add It To The Nodes Array
nodes.append(markerNode)
}
/// Converts A matrix_float4x4 To An SCNVector3
///
/// - Parameter matrix: matrix_float4x4
/// - Returns: SCNVector3
func positionFromMatrix(_ matrix: matrix_float4x4) -> SCNVector3{
return SCNVector3(matrix.columns.3.x, matrix.columns.3.y, matrix.columns.3.z)
}
/// Calculates The Distance Between 2 Nodes
///
/// - Returns: Float?
func distanceBetweenNodes() -> Float? {
guard let firstNode = nodes.first, let endNode = nodes.last else { return nil }
let startPoint = GLKVector3Make(firstNode.position.x, firstNode.position.y, firstNode.position.z)
let endPoint = GLKVector3Make(endNode.position.x, endNode.position.y, endNode.position.z)
let distance = GLKVector3Distance(startPoint, endPoint)
return distance
}
For an example of a measuringApp which might help your development you can look here: ARKit Measuring Example
Hope it helps...
This looks like a logic issue. You're assigning nodes.last to tail just before checking if tail is not nil. So it will never be != nil so you'll never execute the nodes.append(sphere) in the else.
I agree with #dfd. Set a breakpoint to make sure the code is being executed before continuing.
Trying to learn how to use GameplayKit, and in particular, agents & behaviors. Trying to boil down all the tutorials and examples out there into a small, simple piece of code that I can use as a starting point for my own app. Unfortunately, what I've come up with doesn't work properly, and I can't figure out why. It's just a simple sprite with a simple GKGoal(toWander:). Rather than wandering, it just moves in a straight line to the right, forever. It also starts out impossibly slowly and speeds up impossibly slowly, despite my setting the max speed & acceleration to ridiculously high values. I can't figure out the fundamental difference between my simple code and all the complex examples out there. Here's the code, minus required init?(coder aDecoder: NSCoder):
class GremlinAgent: GKAgent2D {
override init() {
super.init()
maxAcceleration = 100000
maxSpeed = 1000000
radius = 20
}
override func update(deltaTime seconds: TimeInterval) {
super.update(deltaTime: seconds)
let goal = GKGoal(toWander: 100)
behavior = GKBehavior(goal: goal, weight: 1)
}
}
class Gremlin: GKEntity {
let sprite: SKShapeNode
init(scene: GameScene) {
sprite = SKShapeNode(circleOfRadius: 20)
sprite.fillColor = .blue
scene.addChild(sprite)
super.init()
let agent = GremlinAgent()
addComponent(agent)
let node = GKSKNodeComponent(node: sprite)
addComponent(node)
agent.delegate = node
}
}
And in GameScene.swift, didMove(to view:):
let gremlin = Gremlin(scene: self)
entities.append(gremlin)
Can anyone help me out?
As has been pointed out elsewhere, you have to set the weight for the goal very high. Try 100, or even 1000, and notice the difference in behavior. But even with these large weights, there's still a problem in your example: the maxSpeed value. You can't set it so high, or your sprite will just fly off in a straight line. Set it to a value closer to the speed you set in the GKGoal object.
Also notice that the wandering will always start off in the direction your sprite is pointing, so if you don't want it to always start off moving to the right, set zRotation to some random value.
Finally, don't create a new behavior in every call to update(). For wandering, you can just set it once, say, in init().
Here's some code that works:
class GremlinAgent: GKAgent2D {
override init() {
super.init()
maxAcceleration = 100000
maxSpeed = 100
let goal = GKGoal(toWander: 100)
behavior = GKBehavior(goal: goal, weight: 1000)
}
}
class Gremlin: GKEntity {
let sprite: SKShapeNode
init(scene: GameScene) {
sprite = SKShapeNode(circleOfRadius: 20)
sprite.fillColor = .blue
sprite.zRotation = CGFloat(GKRandomDistribution(lowestValue: 0, highestValue: 360).nextInt())
scene.addChild(sprite)
super.init()
let agent = GremlinAgent()
addComponent(agent)
let node = GKSKNodeComponent(node: sprite)
addComponent(node)
agent.delegate = node
}
}
In my program, I am trying to make myself collidable with other objects so that I can test collisions. Is there a way to add a physics body that moves with myself in the new AR Kit which follows you? I can make one which is at my place at the beginning of the AR but I'm sure there is a way to add it to your actual self.
Good day!
There is a solution to your problem.
First of all you need to create a collider. For example:
func addCollider(position: SCNVector3, name: String) {
let collider = SCNNode(geometry: SCNSphere(radius: 0.5))
collider.position = position
collider.geometry?.firstMaterial?.diffuse.contents = UIColor.clear
collider.physicsBody?.categoryBitMask = 0 // example
collider.physicsBody?.contactTestBitMask = 1 // example
collider.name = name
sceneView.scene.rootNode.addChildNode(collider)
}
Don't forget to add proper bit masks, to collide with other objects.
Next you need to implement ARSCNViewDelegate protocol and call method willRenderScene. Next you need to locate your point of view and transform it into the matrix. Than you need to extract values from third column of the matrix to get current orientation. To get current location you need to extract values of forth column from your matrix. And finally you need to combine your location and orientation to get your current position.
To combine to SCNVector3 values you need to write global function "+". Example:
func +(left: SCNVector3, right: SCNVector3) -> SCNVector3 {
return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z)
}
Be sure, that you wrote this method outside of class or struct.
Final accord! A function that will move a collider accordingly to your current position.
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pontOfView = self.sceneView.pointOfView else { return }
let transform = pontOfView.transform
let orientation = SCNVector3(-transform.m31, -transform.m32, -transform.m33)
let location = SCNVector3(transform.m41, transform.m42, transform.m43)
let currentPosition = orientation + location
for node in sceneView.scene.rootNode.childNodes {
if node.name == "your collider name" {
node.position = currentPosition
}
}
}
P.S. Don't forget to add
self.sceneView.delegate = self
to your viewDidLoad.
Best of luck. I hope it helped!