Developing an ARKit app that leaves text for others to view - ios

I am creating an iOS AR app that sets text in a specific location and leaves it there for others to view. Is there a better way to implement it than what I am doing?
Currently, I have it set so that the text is saved to Firebase and loads it by setting the nodes relative to the camera’s position. I’m wondering if there is a way to save ARAnchors in a fashion similar to what I am doing but is that possible?
My current function for saving the text to the location via a user tapping the screen:
/*
* Variables for saving the user touch
*/
var touchX : Float = 0.0
var touchY : Float = 0.0
var touchZ : Float = 0.0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// will be used for getting the text
let textNode = SCNNode()
var writing = SCNText()
// gets the user’s touch upon tapping the screen
guard let touch = touches.first else {return}
let result = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitResult = result.last else {return}
let hitTransform = SCNMatrix4.init(hitResult.worldTransform)
let hitVector = SCNVector3Make(hitTransform.m41, hitTransform.m42, hitTransform.m43)
// saves X, Y, and Z coordinates of touch relative to the camera
touchX = hitTransform.m41
touchY = hitTransform.m42
touchZ = hitTransform.m43
// Was thinking of adding the ability to change colors. Probably can skip next seven lines
var colorArray = [UIColor]()
colorArray.append(UIColor.red)
writing = SCNText(string: input.text, extrusionDepth: 1)
material.diffuse.contents = colorArray[0]
writing.materials = [material]
// modifies the node’s position and size
textNode.scale = SCNVector3(0.01, 0.01, 0.01)
textNode.geometry = writing
textNode.position = hitVector
sceneView.scene.rootNode.addChildNode(textNode)
// last few lines save the info to Firebase
let values = ["X" : touchX, "Y" : touchY, "Z" : touchZ, "Text" : input.text!] as [String : Any]
let childKey = reference.child("Test").childByAutoId().key
if input.text != nil && input.text != "" {
let child = reference.child("Test").child(childKey!)
child.updateChildValues(values)
} else {
let child = reference.child("Test").child(childKey!)
child.updateChildValues(values)
} // if
} // override func
/*
* Similar to the previous function but used in next function
*/
func placeNode(x: Float, y: Float, z: Float, text: String) -> Void {
let textNode = SCNNode()
var writing = SCNText()
let hitVector = SCNVector3Make(x, y, z)
touchX = x
touchY = y
touchZ = z
var colorArray = [UIColor]()
colorArray.append(UIColor.red)
writing = SCNText(string: text, extrusionDepth: 1)
material.diffuse.contents = colorArray[0]
writing.materials = [material]
textNode.scale = SCNVector3(0.01, 0.01, 0.01)
textNode.geometry = writing
textNode.position = hitVector
sceneView.scene.rootNode.addChildNode(textNode)
} // func
/*
* This next function is used in my viewDidLoad to load the data
*/
func handleData() {
reference.child("Test").observeSingleEvent(of: .value, with: { (snapshot) in
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let xCoord = Float(truncating: child.childSnapshot(forPath: "X").value as! NSNumber)
let yCoord = Float(truncating: child.childSnapshot(forPath: "Y").value as! NSNumber)
let zCoord = Float(truncating: child.childSnapshot(forPath: "Z").value as! NSNumber)
let inscription = child.childSnapshot(forPath: "Text").value
self.placeNode(x: xCoord , y: yCoord , z: zCoord , text: inscription as! String)
} // for
} // if
}) // reference
} // func
I have looked into a few things such as ARCore but that looks like it uses Objective-C. I’ve made this app in Swift and I am not sure if I can incorporate ARCore with how I have implemented my current application.
Do I just need to get over it and learn Objective-C? Can I still work with what I have?

I think that ARCore anchors are only available for 24 hours, so that could be a problem.
You probably need to use ARKit2.0's ARWorldMap and save it as data on firebase for others to see the text in the same place, otherwise you are assuming in your code that future users will start their AR session in the exact same position and direction as the person who left the text. You probably need to use core location first to see where in the world the user is.

Related

ARKit - getting distance from camera to anchor

I'm creating an anchor and adding it to my ARSKView at a certain distance in front of the camera like this:
func displayToken(distance: Float) {
print("token dropped at: \(distance)")
guard let sceneView = self.view as? ARSKView else {
return
}
// Create anchor using the camera's current position
if let currentFrame = sceneView.session.currentFrame {
// Create a transform with a translation of x meters in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -distance
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
then the node gets created for the anchor like this:
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
// Create and configure a node for the anchor added to the view's session.
if let image = tokenImage {
let texture = SKTexture(image: image)
let tokenImageNode = SKSpriteNode(texture: texture)
tokenImageNode.name = "token"
return tokenImageNode
} else {
return nil
}
}
This works fine and I see the image get added at the appropriate distance. However, what I'm trying to do is then calculate how far the anchor/node is in front of the camera as you move. The problem is the calculation seems to be off immediately using fabs(cameraZ - anchor.transform.columns.3.z). Please see my code below that is in the update() method to calculate distance between camera and object:
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
guard let sceneView = self.view as? ARSKView else {
return
}
if let currentFrame = sceneView.session.currentFrame {
let cameraZ = currentFrame.camera.transform.columns.3.z
for anchor in currentFrame.anchors {
if let spriteNode = sceneView.node(for: anchor), spriteNode.name == "token", intersects(spriteNode) {
// token is within the camera view
//print("token is within camera view from update method")
print("DISTANCE BETWEEN CAMERA AND TOKEN: \(fabs(cameraZ - anchor.transform.columns.3.z))")
print(cameraZ)
print(anchor.transform.columns.3.z)
}
}
}
}
Any help is appreciated in order to accurately get distance between camera and the anchor.
The last column of a 4x4 transform matrix is the translation vector (or position relative to a parent coordinate space), so you can get the distance in three dimensions between two transforms by simply subtracting those vectors.
let anchorPosition = anchor.transforms.columns.3
let cameraPosition = camera.transform.columns.3
// here’s a line connecting the two points, which might be useful for other things
let cameraToAnchor = cameraPosition - anchorPosition
// and here’s just the scalar distance
let distance = length(cameraToAnchor)
What you’re doing isn’t working right because you’re subtracting the z-coordinates of each vector. If the two points are different in x, y, and z, just subtracting z doesn’t get you distance.
This one is for scenekit, I'll leave it here though.
let end = node.presentation.worldPosition
let start = sceneView.pointOfView?.worldPosition
let dx = (end?.x)! - (start?.x)!
let dy = (end?.y)! - (start?.y)!
let dz = (end?.z)! - (start?.z)!
let distance = sqrt(pow(dx,2)+pow(dy,2)+pow(dz,2))
With RealityKit there is a slightly different way to do this. If you're using the world tracking configuration, your AnchorEntity object conforms to HasAnchoring which gives you a target. Target is an enum of AnchoringComponent.Target. It has a case .world(let transform). You can compare your world transform to the camera's world transform like this:
if case let AnchoringComponent.Target.world(transform) = yourAnchorEntity.anchoring.target {
let theDistance = distance(transform.columns.3, frame.camera.transform.columns.3)
}
This took me a bit to figure out but I figure others that might be using RealityKit might benefit from this.
As mentioned above by #codeman, this is the right solution:
let distance = simd_distance(YOUR_NODE.simdTransform.columns.3, (sceneView.session.currentFrame?.camera.transform.columns.3)!);
3D distance - You can check these utils,
class ARSceneUtils {
/// return the distance between anchor and camera.
class func distanceBetween(anchor : ARAnchor,AndCamera camera: ARCamera) -> CGFloat {
let anchorPostion = SCNVector3Make(
anchor.transform.columns.3.x,
anchor.transform.columns.3.y,
anchor.transform.columns.3.z
)
let cametaPosition = SCNVector3Make(
camera.transform.columns.3.x,
camera.transform.columns.3.y,
camera.transform.columns.3.z
)
return CGFloat(self.calculateDistance(from: cametaPosition , to: anchorPostion))
}
/// return the distance between 2 vectors.
class func calculateDistance(from: SCNVector3, to: SCNVector3) -> Float {
let x = from.x - to.x
let y = from.y - to.y
let z = from.z - to.z
return sqrtf( (x * x) + (y * y) + (z * z))
}
}
And now you can call:
guard let camera = session.currentFrame?.camera else { return }
let anchor = // you anchor
let distanceAchorAndCamera = ARSceneUtils.distanceBetween(anchor: anchor, AndCamera: camera)

ARKit : Handle tap to show / hide a node

I am new to ARKit , and i am trying an example to create a SCNBox on tap location. What i am trying to do is on initial touch i create a box and on the second tap on the created box it should be removed from the scene. I am doing the hit test. but it keeps on adding the box. I know this is a simple task, but i am unable to do it
#objc func handleTap(sender: UITapGestureRecognizer) {
print("hande tapp")
guard let _ = sceneView.session.currentFrame
else { return }
guard let scnView = sceneView else { return }
let touchLocation = sender.location(in: scnView)
let hitTestResult = scnView.hitTest(touchLocation, types: [ .featurePoint])
guard let pointOfView = sceneView.pointOfView else {return}
print("point \(pointOfView.name)")
if hitTestResult.count > 0 {
print("Hit")
if let _ = pointOfView as? ARBox {
print("Box Available")
}
else {
print("Adding box")
let transform = hitTestResult.first?.worldTransform.columns.3
let xPosition = transform?.x
let yPosition = transform?.y
let zPosition = transform?.z
let position = SCNVector3(xPosition!,yPosition!,zPosition!)
basketCount = basketCount + 1
let newBasket = ARBox(position: position)
newBasket.name = "basket\(basketCount)"
self.sceneView.scene.rootNode.addChildNode(newBasket)
boxNodes.append(newBasket)
}
}
}
pointOfView of a sceneView, is the rootnode of your scene, which is one used to render your whole scene. For generic cases, it usually is an empty node with lights/ camera. I don't think you should cast it as ARBox/ or any type of SCNNode(s) for that matter.
What you probably can try is the logic below (hitResults are the results of your hitTest):
if hitResults.count > 0 {
if let node = hitResults.first?.node as SCNNode? (or ARBox) {
// node.removeFromParentNode()
// or make the node opaque if you don't want to remove
else {
// add node.

GMSMarker infowindow content(snippet) not updating

I have an array in which each element has Latitude Longitude and some other values in it to draw marker on map. I'm using the following code to update the snippet, position and icon of GMSMarker. I'm using a UISlider to iterate through the array. When I do this the icon and position of the marker gets updated and the InfoWindow of the marker not updating. It shows only the old content. But when I deselect and select it again now it shows correct infowindow content. I have tried to use tracksInfoWindowChanges, but it's not working.
#IBAction func progressChanged(_ sender: UISlider) {
let iVal = Int(sender.value)
if iVal < progressList.count {
let str = progressList[iVal]
let arr = str.components(separatedBy: "!")
let lat = Double(arr[4])!
let lng = Double(arr[5])!
let pos = CLLocationCoordinate2DMake(lat, lng)
let fRotation = Float(arr[9])
let rotation = CGFloat(fRotation!)
let strTime = arr[0]
let idleFlag = arr[1]
let strSpeed = arr[2]
let strName = arr[3]
proMarker.position = pos
proMarker.title = strName
if idleFlag == "Y" {
proMarker.snippet = strSpeed + "!" + strTime + "!" + "idle"
proMarker.tracksInfoWindowChanges = true
let markerImage = UIImage(named: "arrow_red")
let rotated = markerImage?.imageRotatedByDegrees(rotation, flip: false)
proMarker.icon = rotated
} else {
proMarker.snippet = strSpeed + "!" + strTime + "!" + "moving"
proMarker.tracksInfoWindowChanges = true
let markerImage = UIImage(named: "arrow_green")
let rotated = markerImage?.imageRotatedByDegrees(rotation, flip: false)
proMarker.icon = rotated
}
proMarker.groundAnchor = CGPoint(x: 0.5, y: 0.5)
let dTime = sdf5.date(from: strTime)
let sTime = sdf6.string(from: dTime!)
hTime.text = sTime
hSpeedIdle.text = strSpeed + " km/h"
}
}
Please help me solve my problem.
This is not a correct way to add lat_long, You should add marker_obj into array because each marker contain all related info, and it can help you to reduce such type of length of code.
As mentioned in a comment, it may be that you are not making new markers, or if you are, you need to remove the map from the old markers (marker.map = nil). So when you click on overlapped markers, the infowindows will sequentially appear.

detecting if a spritenode exists in swift

I am trying to detect if one of the nodes that I have made through a subclass of SKNode (called Achievements) exists and if it doesn't exist then i'm trying to turn off a boolean variable.
What I use to locate the SKShapeNode (called "Indicator")
func checkIndicatorStatus() {
moveableArea.enumerateChildNodes(withName: "Achievement") {
(node, stop) in
let Indicate = node
Indicate.enumerateChildNodes(withName: "Indicator") {
node, stop in
if let Achievement = node as? Achievements {
menuAchieveNotificationOn = false
}
}
}
}
I have enumerated through the nodes specifically and tried searching for it but it doesn't seem to do anything. what am I doing wrong?
Here is my subclass. I have many of them named achievement displayed in my scene.
class Achievements: SKNode {
//Nodes used throughout the SKNode class
var achievementLabel = SKLabelNode()
var achievementTitleLabel = SKLabelNode()
var achievementNode = SKSpriteNode()
var notifyCircle = SKShapeNode()
//Amount Variables used as Achievement Properties
var image: String = ""
var information: String = ""
var title: String = ""
var amount = 0
var neededAmount = 0
var notification:Bool = false
var stage = 0
func getachievementData(AchName: String) {
let getDataRequest:NSFetchRequest<Achievement> = Achievement.fetchRequest()
getDataRequest.predicate = NSPredicate(format: "theSearchName == %#" , AchName)
do {
let searchResults = try CoreDatabaseContoller.getContext().fetch(getDataRequest)
//print("number of results: \(searchResults.count)")
for result in searchResults as [Achievement] {
title = result.theName!
information = result.theDescription!
image = result.theImage!
amount = Int(result.aAmount!)
neededAmount = Int(result.aNeededAmount!)
stage = Int(result.aStage!)
if result.aHasBeenAchieved!.intValue == 1 {
notification = true
}
}
}
catch {
print("ERROR: \(error)")
}
createAchievement()
}
func createAchievement() {
let tex:SKTexture = SKTexture(imageNamed: image)
achievementNode = SKSpriteNode(texture: tex, color: SKColor.black, size: CGSize(width: 85, height: 85)) //frame.maxX / 20, height: frame.maxY / 20))
achievementNode.zPosition = -10
achievementSprites.append(achievementNode)
self.name = "Achievement"
self.addChild(achievementNode)
self.zPosition = -11
createAchievementLabels()
}
func createAchievementLabels() {
achievementTitleLabel = SKLabelNode(fontNamed: "Avenir-Black")
achievementTitleLabel.fontColor = UIColor.black;
achievementTitleLabel.fontSize = 15 //self.frame.maxY/30
achievementTitleLabel.position = CGPoint (x: 0, y: 50)
achievementTitleLabel.text = "\(title)"
achievementTitleLabel.zPosition = -9
addChild(achievementTitleLabel)
achievementLabel = SKLabelNode(fontNamed: "Avenir-Black")
achievementLabel.fontColor = UIColor.black;
achievementLabel.fontSize = 13 //self.frame.maxY/30
achievementLabel.position = CGPoint (x: 0, y: -55)
achievementLabel.text = ("\(amount) / \(neededAmount)")
achievementLabel.zPosition = -9
addChild(achievementLabel)
if notification == true {
notifyCircle = SKShapeNode(circleOfRadius: 10)
notifyCircle.fillColor = .red
notifyCircle.position = CGPoint(x: 30 , y: 35)
notifyCircle.zPosition = 1000
notifyCircle.name = "Indicator"
addChild(notifyCircle)
}
}
}
EDIT 1
As you can see from the image below, there are a number of different achievement nodes each with their individual names and unlock criteria, when an achievement becomes unlocked it makes an indicator and changes the graphic, which is seen for the two X nodes here with the coloured red circle in the top right corners of each of them which is the "indicator" (if you look at the subclass at the bottom of it the creation of the indicator is there)
Now as you can seen the big red button in the bottom right hand corner of the picture is the achievement menu button which also has an indicator which is controlled by the bool variable (menuAchieveNotificationOn) what i'm trying to achieve is once the achievements with the indicators have each been pressed they are removed from the node.
what i'm trying to do is search each of the nodes to see if the indicator still exists if not I want to turn the variable (menuAchieveNotificationOn) to false.
You should be able to use this:
if let indicatorNode = childNode(withName: "//Indicator") as! SKShapeNode? {
menuAchieveNotificationOn = false
} else {
menuAchieveNotificationOn = true
}
EDIT: to run some code for EVERY "indicator" node in the scene. If any are found, achievementsFound is set to true:
achievementsFound = false
enumerateChildNodes(withName: "//Indicator") { indicatorNode, _ in
// Do something with indicatorNode
achievementsFound = true
}
Although this seems too simple, so I might have misunderstood your aim.

How to choose from an array of sprites in swift

So basically I am looking to choose one of the 4 different coloured balls at random to come into the scene which each have an animation, physics properties and movement & spacing that I have already coded. I am not sure exactly how to make the array then choose at random from the array of the 4 coloured balls so that I have one ball chosen at random to come into the scene.
To make it more clear what I'm asking here's some code (I only use two balls in this code so you don't have to read as much):
var moveandremove = SKAction() < this is in my ballScene.swift
The spawn runBlock is inside didMovetoView
let spawn = SKAction.runBlock({
() in
self.allballs()
})
let delay = SKAction.waitForDuration(2.0)
let SpawnDelay = SKAction.sequence([spawn, delay])
let spawndelayforever = SKAction.repeatActionForever(SpawnDelay)
self.runAction(spawndelayforever)
let distance = CGFloat(brnball.frame.width * 20 + brnball.frame.width)
let moveball = SKAction.moveByX(-distance, y: 0, duration: NSTimeInterval(0.003 * distance))
let removeball = SKAction.removeFromParent()
moveandremove = SKAction.sequence([moveball])
}
func allballs() {
TextureAtlasblk = SKTextureAtlas(named: "blkball")
for i in 1...TextureAtlasblk.textureNames.count{
var Name = "blkball_\(i)"
blkarray.append(SKTexture(imageNamed: Name))
}
blkball = SKSpriteNode(imageNamed: "blkball_1")
blkball.position = CGPoint(x: CGRectGetMidX(self.frame) + 100, y: CGRectGetMidY(self.frame))
blkball.zPosition = 7
blkball.setScale(0.1)
self.addChild(blkball)
blkball.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(blkarray, timePerFrame: 0.2)))
//brownball
TextureAtlasbrn = SKTextureAtlas(named: "brnball")
for i in 1...TextureAtlasbrn.textureNames.count{
var Name = "brnball_\(i)"
brnarray.append(SKTexture(imageNamed: Name))
}
brnball = SKSpriteNode(imageNamed: "brnball_1")
brnball.position = CGPoint(x: CGRectGetMidX(self.frame) + 50, y: CGRectGetMidY(self.frame))
brnball.zPosition = 7
brnball.setScale(0.1)
self.addChild(brnball)
brnball.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(brnarray, timePerFrame: 0.2)))
Here is my terrible starting point at trying to make an array to choose from each ball (this is inside my allballs() function):
var ballarray: NSMutableArray = [blkball, brnball, yelball, bluball]
runAction(moveandremove)
I am new to swift and pretty hopeless, would be awesome if someone could help me out :)
Thanks
It's hard for me to find the array that you're talking about in your code. But nevertheless, I can still show you how.
Let's say we have an [Int]:
let ints = [10, 50, 95, 48, 77]
And we want to get a randomly chosen element of that array.
As you may already know, you use the subscript operator with the index of the element to access an element in the array, e.g. ints[2] returns 95. So if you give a random index to the subscript, a random item in the array will be returned!
Let's see how can we generate a random number.
The arc4random_uniform function returns a uniformly distributed random number between 0 and one less the parameter. Note that this function takes a UInt32 as a parameter and the return value is of the same type. So you need to do some casting:
let randomNumber = Int(arc4random_uniform(UInt32(ints.count)))
With randomNumber, we can access a random element in the array:
let randomItem = ints[randomNumber]
Try to apply this technique to your situation.
Here's a generic method to do this as well:
func randomItemInArray<T> (array: [T]) -> T? {
if array.isEmpty {
return nil
}
let randomNumber = Int(arc4random_uniform(UInt32(array.count)))
return array[randomNumber]
}
Note that if the array passed in is empty, it returns nil.
You could make and extension for Array that returns a random element.
extension Array {
func randomElement() -> Element {
let i = Int(arc4random_uniform(UInt32(count - 1)))
return self[i]
}
}
You could take that a step further and allow a function to be applied directly to a random element.
mutating func randomElement(perform: (Element) -> Element) {
let i = Int(arc4random_uniform(UInt32(count - 1)))
self[i] = perform(self[i])
}
You can use this function when using an array of reference types.
func randomElement(perform: (Element) -> ()) {
let i = Int(arc4random_uniform(UInt32(count - 1)))
perform(self[i])
}

Resources