I am using Mapbox in my iOS Project with Swift language. I am currently looking a way to programatically rotate the mapview at a fix altitude with certain coordinates inside. I have tried alot and even have browsed their API Reference as well but could not find any help.
Has anyone tried to do anything like this in which MapView is being continously rotating keeping certain coornidates in bounds using MAPBOX Library.
Help would be really appreciated.
Thanks to #riastrad for the guidance and I have come up with a bunch of code that can help to acheive this feature.
Sharing with everyone so that they can get help if they require:
Code is for Swift 4.2
//Create a bound using two coordinates.
let coordinateBounds = MGLCoordinateBounds(sw: coordinateOne, ne: coordinateTwo)
//Add Insets for the bounds if needed
let mapEdgeInsets = UIEdgeInsets(top: 10.0, left: 0.0, bottom: 0.0, right: 10.0)
//get the camera that fit those bounds and edge insets
let camera = self.mapView.cameraThatFitsCoordinateBounds(coordinateBounds, edgePadding: mapEdgeInsets)
//Update camera pitch (if required)
camera.pitch = 60
//setup CameraHeading
let zoomLevel = self.mapView.zoomLevel
var cameraHeading = camera.heading
if zoomLevel > 14 {
cameraHeading += 2.2
} else {
cameraHeading += 0.7
}
if cameraHeading > 359 {
cameraHeading = 1
}
camera.heading = cameraHeading
//set new camera with animation
let newCamera = camera
self.mapView.setCamera(newCamera, withDuration: 0.1, animationTimingFunction: CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut))
put the above set of code inside a method and call that method repeatedly every 0.1 seconds.
private func enableRotationTimer(_ enable:Bool) {
guard self.store != nil else { return }
if enable == true {
if mapRotationTimer == nil {
mapRotationTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(rotateCamera), userInfo: nil, repeats: true)
}
} else {
mapRotationTimer?.invalidate()
mapRotationTimer = nil
}
}
Hope this helps for others.
Thanks
The setCamera function allows you to pass an animation function. You can see an example of this here: https://docs.mapbox.com/ios/maps/examples/camera-animation/
For limiting the bounds of your map camera, this requires some sleight of hand and a bit of calculating to determine where the camera is in relation to the bounds you’d like to set. You can find an example of this here: https://docs.mapbox.com/ios/maps/examples/constraining-gestures/
Based on how you’ve phrased your question, it sounds like you’ll need to combine these two approaches.
⚠️ disclaimer: I currently work at Mapbox⚠️
Related
I am stuck on a problem. I need to apply transformation (scale, rotation, position) right after i add model to my rootNode. Right after when i apply transformation on child model added to rootNode it shows fine on screen but when i apply transformation on rootNode it doesn't refresh. i experimented that as soon i touch screen UI updates. I also tried putting delay of 2,3 secs.
expected
UIView should update as soon i apply transformation to rootNode.
let res = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0.5, z: 0, duration: 1))
// let res = SCNAction.sequence([SCNAction.wait(duration: 2000), SCNAction.rotateTo(x: CGFloat(180), y: CGFloat(90), z: CGFloat(0), duration: 1.0)])
self.rootNode.runAction(res)
i tried putting code in
RunLoop.main.perform {}
i tried using
scnView.preferredFramesPerSecond = 30
scnView.rendersContinuously = true
But none works. i am using sdk IOS 13.2. Any help please.
Edit:
var rootNode = SCNNode()
viewDidload(){
scnScene.rootNode.addChildNode(rootNode)
....
}
func initSceneWithModel(modelURL: URL) {
do {
try personModel = addModel(url: modelURL)
menuButton.setImage(UIImage.fontAwesomeIcon(name: .bars, style: .solid, textColor: .white, size: XConstants.FONT_AWSOME_SIZE), for: .normal)
selectedModel = personModel
centerPivot(for: personModel!)
moveNodeToCenter(node: personModel!)
setupEyeBlocker()
// selectedModel = eyeBlocker
updateFieldUI()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.applyInitTransformations()
}
} catch let error {
Utilities.xalert(inView: self.view, desc: error.localizedDescription)
}
}
func applyInitTransformations() {
if let info = vm.physicialFile.extraInfo {
// personModel?.position = info.person.position
// personModel?.scale = info.person.scale
// personModel?.eulerAngles = info.person.rotation
var valueRotPos = SCNMatrix4Mult(SCNMatrix4MakeRotation(0,0,0,0), SCNMatrix4MakeTranslation(0,0,0))
var valueScale = SCNMatrix4MakeScale(7.0,7.0,7.0) // scales to 0.1 of original size
rootNode.transform = SCNMatrix4Mult(valueRotPos, valueScale)
// rootNode.position = info.root.position
// rootNode.scale = info.root.scale
// rootNode.eulerAngles = info.root.rotation
}
else {
applyEyeBlockerDefaultPosition()
}
}
Apple clearly says:
...
You should not modify the transform property of the root node.
...
(https://developer.apple.com/documentation/scenekit/scnscene/1524029-rootnode)
This might be causing the issues you have with your scene. Avoid SCNActions to be run on the rootNode. They are designed to run on the content of the rootNode (any SCNNode added to the rootNode).
You could probably take a common SCNNode, call it like myRootNode, add it to the real rootNode and add all your other content to myRootNode. Transformations should then apply correctly to all your sub-content, if this is your goal.
BTW: scnView.preferredFramesPerSecond = 30 never gave me more performence or any benefits. Leave it default. Scenekit switches automatically to lower framerates if required.
EDIT:
apply transformation like so:
// Precalculate the Rotation the Position and the Scale
var valueRotPos = SCNMatrix4Mult(SCNMatrix4MakeRotation(0,0,0,0), SCNMatrix4MakeTranslation(0,0,0))
var valueScale = SCNMatrix4MakeScale(0.1,0.1,0.1) // scales to 0.1 of original size
then you do:
myRootNode.transform = SCNMatrix4Mult(valueRotPos, valueScale)
(you could also try to use the worldTransform of the node or the other transform properties of the nodes presentation node-object)
I am creating a simple app with ARKit in which I add some text to the scene to the tapped position:
#objc func tapped(sender: UITapGestureRecognizer){
let sceneView = sender.view as! ARSCNView
let tapLocation = sender.location(in: sceneView)
let hitTest = sceneView.hitTest(tapLocation, types: .featurePoint)
if !hitTest.isEmpty{
self.addTag(tag: "A", hitTestResult: hitTest.first!)
}
else{
print("no match")
}
}
func addTag(tag: String, hitTestResult: ARHitTestResult){
let tag = SCNText(string:tag, extrusionDepth: 0.1)
tag.font = UIFont(name: "Optima", size: 1)
tag.firstMaterial?.diffuse.contents = UIColor.red
let tagNode = SCNNode(geometry: tag)
let transform = hitTestResult.worldTransform
let thirdColumn = transform.columns.3
tagNode.position = SCNVector3(thirdColumn.x,thirdColumn.y - tagNode.boundingBox.max.y / 2,thirdColumn.z)
print("\(thirdColumn.x) \(thirdColumn.y) \(thirdColumn.z)")
self.sceneView.scene.rootNode.addChildNode(tagNode)
}
It works, but I have problem with the orientation of the text. When I add it with the camera's original position, the text orientation is ok, I can see the text frontwise (Sample 1). But when I turn camera to the left / right, and add the text by tapping, I can see the added text from the side (Sample 2).
Sample 1:
Sample 2:
I know there should be some simple trick to solve it, but as a beginner in this topic I could not find it so far.
You want the text to always face the camera? SCNBillboardConstraint is your friend:
tagNode.constraints = [SCNBillboardConstraint()]
Am I correct in saying that you want the text to face the camera when you tap (wherever you happen to be facing), but then remain stationary?
There are a number of ways of adjusting the orientation of any node. For this case I would suggest simply setting the eulerAngles of the text node to be equal to those of the camera, at the point in which you instantiate the text.
In your addTag() function you add:
let eulerAngles = self.sceneView.session.currentFrame?.camera.eulerAngles
tagNode.eulerAngles = SCNVector3(eulerAngles.x, eulerAngles.y, eulerAngles.z + .pi / 2)
The additional .pi / 2 is there to ensure the text is in the correct orientation, as the default with ARKit is for a landscape orientation and therefore the text comes out funny. This applies a rotation around the local z axis.
It's also plausible (and some may argue it's better) to use .localRotate() of the node, or to access its transform property, however I like the approach of manipulating both the position and eulerAngles directly.
Hope this helps.
EDIT: replaced Float(1.57) with .pi / 2.
I'm using ARKit to display 3D objects. I managed to place the nodes in the real world in front of the user (aka the camera). But I don't manage to make them to face the camera when I drop them.
let tap_point=CGPoint(x: x, y: y)
let results=arscn_view.hitTest(tap_point, types: .estimatedHorizontalPlane)
guard results.count>0 else{
return
}
guard let r=results.first else{
return
}
let hit_tf=SCNMatrix4(r.worldTransform)
let new_pos=SCNVector3Make(hit_tf.m41, hit_tf.m42+Float(0.2), hit_tf.m43)
guard let scene=SCNScene(named: file_name) else{
return
}
guard let node=scene.rootNode.childNode(withName: "Mesh", recursively: true) else{
return
}
node.position=new_pos
arscn_view.scene.rootNode.addChildNode(node)
The nodes are well positioned on the plane, in front of the camera. But they are all looking in the same direction. I guess I should rotate the SCNNode but I didn't manage to do this.
First, get the rotation matrix of the camera:
let rotate = simd_float4x4(SCNMatrix4MakeRotation(sceneView.session.currentFrame!.camera.eulerAngles.y, 0, 1, 0))
Then, combine the matrices:
let rotateTransform = simd_mul(r.worldTransform, rotate)
Lastly, apply a transform to your node, casting as SCNMatrix4:
node.transform = SCNMatrix4(rotateTransform)
Hope that helps
EDIT
here how you can create SCNMatrix4 from simd_float4x4
let rotateTransform = simd_mul(r.worldTransform, rotate)
node.transform = SCNMatrix4(m11: rotateTransform.columns.0.x, m12: rotateTransform.columns.0.y, m13: rotateTransform.columns.0.z, m14: rotateTransform.columns.0.w, m21: rotateTransform.columns.1.x, m22: rotateTransform.columns.1.y, m23: rotateTransform.columns.1.z, m24: rotateTransform.columns.1.w, m31: rotateTransform.columns.2.x, m32: rotateTransform.columns.2.y, m33: rotateTransform.columns.2.z, m34: rotateTransform.columns.2.w, m41: rotateTransform.columns.3.x, m42: rotateTransform.columns.3.y, m43: rotateTransform.columns.3.z, m44: rotateTransform.columns.3.w)
guard let frame = self.sceneView.session.currentFrame else {
return
}
node.eulerAngles.y = frame.camera.eulerAngles.y
here's my code for the SCNNode facing the camera..hope help for someone
let location = touches.first!.location(in: sceneView)
var hitTestOptions = [SCNHitTestOption: Any]()
hitTestOptions[SCNHitTestOption.boundingBoxOnly] = true
let hitResultsFeaturePoints: [ARHitTestResult] = sceneView.hitTest(location, types: .featurePoint)
let hitTestResults = sceneView.hitTest(location)
guard let node = hitTestResults.first?.node else {
if let hit = hitResultsFeaturePoints.first {
let rotate = simd_float4x4(SCNMatrix4MakeRotation(sceneView.session.currentFrame!.camera.eulerAngles.y, 0, 1, 0))
let finalTransform = simd_mul(hit.worldTransform, rotate)
sceneView.session.add(anchor: ARAnchor(transform: finalTransform))
}
return
}
Do you want the nodes to always face the camera, even as the camera moves? That's what SceneKit constraints are for. Either SCNLookAtConstraint or SCNBillboardConstraint can keep a node always pointing at the camera.
Do you want the node to face the camera when placed, but then hold still (so you can move the camera around and see the back of it)? There are a few ways to do that. Some involve fun math, but a simpler way to handle it might just be to design your 3D assets so that "front" is always in the positive Z-axis direction. Set a placed object's transform based on the camera transform, and its initial orientation will match the camera's.
Here's how I did it:
func faceCamera() {
guard constraints?.isEmpty ?? true else {
return
}
SCNTransaction.begin()
SCNTransaction.animationDuration = 5
SCNTransaction.completionBlock = { [weak self] in
self?.constraints = []
}
constraints = [billboardConstraint]
SCNTransaction.commit()
}
private lazy var billboardConstraint: SCNBillboardConstraint = {
let constraint = SCNBillboardConstraint()
constraint.freeAxes = [.Y]
return constraint
}()
As stated earlier a SCNBillboardConstraint will make the node always look at the camera. I am animating it so the node doesn't just immediately snap into place, this is optional. In the SCNTransaction.completionBlock I remove the constraint, also optional.
Also I set the SCNBillboardConstraint's freeAxes, which customizes on what axis the node follows the camera, again optional.
I want the node to face the camera when I place it then keep it here (and be able to move around). – Marie Dm
Blockquote
You can put object facing to camera, using this:
if let rotate = sceneView.session.currentFrame?.camera.transform {
node.simdTransform = rotate
}
This code will save you from gimbal lock and other troubles.
The four-component rotation vector specifies the direction of the rotation axis in the first three components and the angle of rotation (in radians) in the fourth. The default rotation is the zero vector, specifying no rotation. Rotation is applied relative to the node’s simdPivot property.
The simdRotation, simdEulerAngles, and simdOrientation properties all affect the rotational aspect of the node’s simdTransform property. Any change to one of these properties is reflected in the others.
https://developer.apple.com/documentation/scenekit/scnnode/2881845-simdrotation
https://developer.apple.com/documentation/scenekit/scnnode/2881843-simdtransform
I am currently making an iPhone game using SpriteKit in which two archers fire arrows at each other in an attempt to take away all of their opponent's health. I've got the mechanism for firing the arrow when the user drags back on the screen, but am wondering if there is a way to make the arrow sprite rotate in mid air as it follows the arc (facing upwards immediately after release and facing downward immediately prior to landing) so that it actually looks as if it were fired from a bow in real life.
This is the piece of code that is ran when the user's touch ends. You can see where I apply the impulse but I am not sure how to go about implementing the rotation. I understand how I could get the initial rotation to match the angle of the trajectory line using simple trigonometry, but don't know how to make it rotate with its arc.
func touchUp(atPoint pos : CGPoint) {
if arrowFired == false && firstTouchMade == true && pointOne != nil {
//Set pointTwo
pointTwo = pos
//Define the vert/hor of the pull-back
let verticalLeg = ((pointOne?.y)! - (pointTwo?.y)!)
let horizontalLeg = ((pointOne?.x)! - (pointTwo?.x)!)
//Apply the impulse
arrow?.run(SKAction.applyImpulse(CGVector(dx: horizontalLeg / 9, dy: verticalLeg / 9), duration: 0.001))
arrow?.physicsBody?.affectedByGravity = true
let array = [trajectoryLine, trajectoryLabel] as [Any]
removeChildren(in: array as! [SKNode])
arrowFired = true
} else if firstTouchMade == false && zoomingIn == false {
zoomingIn = true
cameraNode.run(SKAction.move(to: (arrow?.position)!, duration: 1), completion: {
self.firstTouchMade = true
self.zoomingIn = false
})
cameraNode.run(SKAction.scale(by: 0.5, duration: 1))
}
}
Sorry if this is an obvious question, I am relatively new to programming, Swift, and SpriteKit especially.
So I am developing an Ipad app that allows the user to solve a jigsaw puzzle. I've worked out getting the panning motion for each piece, but getting them where I want to has not worked properly. I am trying to make a piece snap into it's final destination when it's within a small range, which is followed by a clicking sound.
Here is a bit of code for a single puzzle piece. When my new game button is pressed, an Image View gets set to the corresponding picture, and randomly placed on the canvas.
#IBAction func NewGameTapped(sender: UIButton){
let bounds = UIScreen.mainScreen().bounds
let height = bounds.size.height
let width = bounds.size.width
image1.image = UIImage(named:"puzzleImage1.png")
image1.center.x = CGFloat(100 + arc4random_uniform(UInt32(width)-300))
image1.center.y = CGFloat(100 + arc4random_uniform(UInt32(height)-300))
//Create Panning (Dragging) Gesture Recognizer for Image View 1
let panRecognizer1 = UIPanGestureRecognizer(target: self, action: "handlePanning1:")
// Add Panning (Dragging) Gesture Recognizer to Image View 1
image1.addGestureRecognizer(panRecognizer1)
}
This is where I am having some issues.
func handlePanning1(recognizer: UIPanGestureRecognizer) {
let center = dict1_image_coordinates["puzzleImage1"] as![Int]
let newTranslation: CGPoint = recognizer.translationInView(image1)
recognizer.view?.transform = CGAffineTransformMakeTranslation(lastTranslation1.x + newTranslation.x, lastTranslation1.y + newTranslation.y)
if recognizer.state == UIGestureRecognizerState.Ended {
lastTranslation1.x += newTranslation.x
lastTranslation1.y += newTranslation.y
}
checkPosition(image1, center: center)
}
func checkPosition(image: UIImageView, center: [Int]){
let distance: Double = sqrt(pow((Double(image.center.x) - Double(center[0])),2) + pow((Double(image.center.y) - Double(center[1])),2))
//if the distance is within range, set image to new location.
if distance <= 20{
image.center.x = CGFloat(center[0])
image.center.y = CGFloat(center[1])
AudioServicesPlaySystemSound(clickSoundID)
}
For whatever reason, the puzzle piece only wants to snap to it's spot when the piece begins the game within the acceptable snap distance. I have tried checking for the object position in various different parts of my program, but nothing has worked so far. Any help or other tips are greatly appreciated.
The issue is likely caused by this line
image1.addGestureRecognizer(panRecognizer1)
Usually people add gestureRecognizer on the parentView, or the rootView of the view controller instead of the image1 itself. The benefit is that the parentView never moves, where as the image1 is constantly being transformed, which may or may not affect the recognizer.translationInView(x) method return value.
do this instead:
self.view.addGestureRecognizer(panRecognizer1)
and change to this line in handlePanning1 function:
image1.transform = CGAffineTransformMakeTranslation(lastTranslation1.x + newTranslation.x, lastTranslation1.y + newTranslation.y)