I am using sceneKit in my project, and would like to add a red sphere (as a marker)on the location that the user taps on a 3d model of a human body (see picture below). With the code I have currently, the sphere is added in the correct position - however, it not added on top of the human body, but rather extremely close to the camera (the z value is off). How can change the z value of the red sphere so that it is added on top of the human body rather than in front of the camera? Thank you so much :)
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController {
var selectedNode: SCNNode!
var markerSphereNode: SCNNode!
var bodyNode: SCNNode!
#IBOutlet weak var sceneView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/femaleBodySceneKit Scene.scn")
markerSphereNode = scene?.rootNode.childNode(withName: "markerSphere", recursively: true)
bodyNode = scene?.rootNode.childNode(withName: "Body_M_GeoRndr", recursively: true)
sceneView.scene = scene
sceneView.allowsCameraControl = true
_ = sceneView.pointOfView
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let touchPoint = touch.location(in: sceneView)
if sceneView.hitTest(touch.location(in: sceneView), options: nil).first != nil {
markerSphereNode.position = sceneView.unprojectPoint(
SCNVector3(x: Float(touchPoint.x),
y: Float(touchPoint.y),
z: 0.56182814))
}
}
}
how the red sphere appears when you tap on a location in the body
how far it is from the body when you rotate the camera (it is in the background if you look closely
how the red dot SHOULD appear
SCNHitTestResult provides you with localCoordinates and worldCoordinates already. There's a need to use the unprojectPoint method.
Related
I am trying to attach a sphere at the center of the device screen and as I move the device around the sphere should stay in the centre of the screen (like a crosshair)
I have attached a sphere entity and added it to sphere_anchor like this in makeUIView function
sphere_anchor.addChild(modelEntity)
But as i move my device the sphere just moves in the initial frame the entity was attached to as I move the device.Hoping someone could point me to the correct way of doing this
//Implement ARSession didUpdate session delegate method
public func session(_ session: ARSession, didUpdate frame: ARFrame) {
let trasnform = frame.camera.transform
if ((self.scene.findEntity(named: "sphere")) != nil) {
let position = simd_make_float3(trasnform.columns.3)
//print(position)
sphere_anchor.position = position
sphere_anchor.orientation = Transform(matrix: trasnform).rotation
}
}
Try AnchorEntity(.camera). If you implement it there's no need for session(_:didUpdate:) instance method because RealityKit's anchor automatically tracks ARCamera position.
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
let mesh = MeshResource.generateSphere(radius: 0.1)
let sphere = ModelEntity(mesh: mesh)
let anchor = AnchorEntity(.camera)
sphere.setParent(anchor)
arView.scene.addAnchor(anchor)
sphere.transform.translation.z = -0.75
}
AnchorEntity(.camera) works only when real iOS device in Active Scheme is chosen.
I am not able to figure out how to make the "ball" entity a physics entity/body and apply a force to it.
// I'm using UIKit for the user interface and RealityKit +
// the models made in Reality Composer for the Augmented reality and Code
import RealityKit
import ARKit
class ViewController: UIViewController {
var ball: (Entity & HasPhysics)? {
try? Entity.load(named: "golfball") as? Entity & HasPhysics
}
#IBOutlet var arView: ARView!
// referencing the play now button on the home screen
#IBAction func playNow(_ sender: Any) { }
// referencing the slider in the AR View - this slider will be used to
// control the power of the swing. The slider values range from 10% to
// 100% of swing power with a default value of 55%. The user will have
// to gain experience in the game to know how much power to use.
#IBAction func slider(_ sender: Any) { }
//The following code will fire when the view loads
override func viewDidLoad() {
super.viewDidLoad()
// defining the Anchor - it looks for a flat surface .3 by .3
// meters so about a foot by a foot - on this surface, it anchors
// the golf course and ball when you tap
let anchor = AnchorEntity(plane: .horizontal, minimumBounds: [0.3, 0.3])
// placing the anchor in the scene
arView.scene.addAnchor(anchor)
// defining my golf course entity - using modelentity so it
// participates in the physics of the scene
let entity = try? ModelEntity.load(named: "golfarnew")
// defining the ball entity - again using modelentity so it
// participates in the physics of the scene
let ball = try? ModelEntity.load(named: "golfball")
// loading my golf course entity
anchor.addChild(entity!)
// loading the golf ball
anchor.addChild(ball!)
// applying a force to the ball at the balls position and the
// force is relative to the ball
ball.physicsBody(SIMD3(1.0, 1.0, 1.0), at: ball.position, relativeTo: ball)
// sounds, add physics body to ball, iPad for shot direction,
// connect slider to impulse force
}
}
Use the following code to find out how to implement a RealityKit's physics.
Pay particular attention: Participates in Physics is ON in Reality Composer.
import ARKit
import RealityKit
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let boxScene = try! Experience.loadBox()
let secondBoxAnchor = try! Experience.loadBox()
let boxEntity = boxScene.steelBox as! (Entity & HasPhysics)
let kinematics: PhysicsBodyComponent = .init(massProperties: .default,
material: nil,
mode: .kinematic)
let motion: PhysicsMotionComponent = .init(linearVelocity: [0.1 ,0, 0],
angularVelocity: [3, 3, 3])
boxEntity.components.set(kinematics)
boxEntity.components.set(motion)
let anchor = AnchorEntity()
anchor.addChild(boxEntity)
arView.scene.addAnchor(anchor)
arView.scene.addAnchor(secondBoxAnchor)
print(boxEntity.isActive) // Entity must be active!
}
}
Also, look at THIS POST to find out how to implement RealityKit's physics with a custom class.
I am trying to make a maze app. The goal is to move a ball (drag the ball with your finger) through a "maze" like level which has traps and other dangers. However, when I made it so the ball/player moves when its dragged, it started to now phase though the other sprite nodes instead of staying inside of the level. How can I make it so it doesn't go outside of the level?
Code:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var ball = SKSpriteNode()
var danger1 = SKSpriteNode()
var danger2 = SKSpriteNode()
var goal = SKSpriteNode()
override func didMove(to view: SKView) {
ball = self.childNode(withName: "ball") as! SKSpriteNode
danger1 = self.childNode(withName: "danger1") as! SKSpriteNode
danger2 = self.childNode(withName: "danger2") as! SKSpriteNode
goal = self.childNode(withName: "goal") as! SKSpriteNode
let border = SKPhysicsBody(edgeLoopFrom: self.frame)
border.friction = 0
border.restitution = 0
print("x: \(ball.position.x), y: \(ball.position.y)")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
ball.position.x = location.x
ball.position.y = location.y
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
Screenshot of game:
Maze Level
You need to set up a collisionBitMask for your nodes and set up your desired collision rules accordingly (i.e. prevent ball from clipping through the maze walls or dangers):
I'd recommend looking at the corresponding apple developer docs: https://developer.apple.com/documentation/spritekit/skphysicsbody/1520003-collisionbitmask
This other question's answer may also prove helpful: What are Sprite Kit's "Category Mask" and "Collision Mask"?
This was also a video that I found helpful when learning about bitmasks for detecting collisions & contact between SKSpriteNodes: https://youtu.be/467Doas5J6I?t=1114
*Edit*
You will also need to set up contact detection via contactTestBitMask so that you know when the ball has contacted the wall, at which point you can implement logic to ‘drop’ it from the dragging gesture. As #Steve Ives correctly noted, the dragging will override the collision physics and you would still get clipping without this.
Your major problem is you are moving your ball directly, ala hand of god.
What you need to do is play by the systems rules. Instead of applying the position directly, set the velocity of your ball to push your ball to where your finger is. This requires some math:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
//set the location's coordinate system to match the ball
let location = touch.location(in: ball.parent)
let angle = atan2(location.x,location.y)
let velocity = 1.0f //(change this to whatever speed you want your ball to move at)
ball.velocity = CGVector(velocity * cos(angle),velocity * sin(angle)) // you may have to swap around the sin/-sin/cos/-cos to get it the way you like it, I am assuming at angle 0 your ball is facing right, and it rotates in a counter clockwise direction)
}
}
Now your ball will forever be pushed towards the touching point.
To avoid bouncing, make sure you set the restitution to 0 on the physics body so that it loses all of its energy when it hits a wall.
Also check categoryBitmask is of some power of 2 on all objects that require physics (1 works in your example) so that it can collide with whatever is in its way
I'm in the process of learning both ARKit and Scenekit concurrently, and it's been a bit of a challenge.
With a ARWorldTrackingSessionConfiguration session created, I was wondering if anyone knew of a way to get the position of the user's 'camera' in the scene session. The idea is I want to animate an object towards the user's current position.
let reaperScene = SCNScene(named: "reaper.dae")!
let reaperNode = reaperScene.rootNode.childNode(withName: "reaper", recursively: true)!
reaperNode.position = SCNVector3Make(0, 0, -1)
let scene = SCNScene()
scene.rootNode.addChildNode(reaperNode)
// some unknown amount of time later
let currentCameraPosition = sceneView.pointOfView?.position
let moveAction = SCNAction.move(to: currentCameraPosition!, duration: 1.0)
reaperNode.runAction(moveAction)
However, it seems that currentCameraPosition is always [0,0,0], even though I am moving the camera around. Any idea what I'm doing wrong? Eventually the idea is I would rotate the object around an invisible sphere until it is in front of the camera and then animate it in, doing something similar to this: Rotate SCNCamera node looking at an object around an imaginary sphere (that way the user sees the object animate towards them)
Thanks for any help.
Set yourself as the ARSession.delegate. Than you can implement session(_:didUpdate:) which will give you an ARFrame for every frame processed in your session. The frame has an camera property that holds information on the cameras transform, rotation and position.
func session(_ session: ARSession, didUpdate frame: ARFrame) {
// Do something with the new transform
let currentTransform = frame.camera.transform
doSomething(with: currentTransform)
}
As rickster pointed out you always can get the current ARFrame and the camera position through it by calling session.currentFrame.
This is useful if you need the position just once, eg to move a node where the camera has been but you should use the delegate method if you want to get updates on the camera's position.
I know it had been solved but i have a little neat solution for it ..
I would prefere adding a renderer delegate method.. it's a method in ARSCNViewDelegate
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else { return }
let transform = pointOfView.transform
let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33)
let location = SCNVector3(transform.m41, transform.m42, transform.m43)
let currentPositionOfCamera = orientation + location
print(currentPositionOfCamera)
}
of course you can't by default add the two SCNVector3 out of the box.. so you need to paste out of the class the following
func +(lhv:SCNVector3, rhv:SCNVector3) -> SCNVector3 {
return SCNVector3(lhv.x + rhv.x, lhv.y + rhv.y, lhv.z + rhv.z)
}
ARKit + SceneKit
For your convenience you can create a ViewController extension with an instance method session(_:didUpdate:) where an update will be occured.
import ARKit
import SceneKit
extension ViewController: ARSessionDelegate {
func session(_ session: ARSession, didUpdate frame: ARFrame) {
let transform = frame.camera.transform
let position = transform.columns.3
print(position.x, position.y, position.z) // UPDATING
}
}
class ViewController: UIViewController {
#IBOutlet var sceneView: ARSCNView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
sceneView.session.delegate = self // ARSESSION DELEGATE
let config = ARWorldTrackingConfiguration()
sceneView.session.run(config)
}
}
RealityKit
In RealityKit, ARView's object contains a camera's transform as well:
import RealityKit
import UIKit
import Combine
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
var subs: [AnyCancellable] = []
override func viewDidLoad() {
super.viewDidLoad()
arView.scene.subscribe(to: SceneEvents.Update.self) { _ in
let camTransform = self.arView.cameraTransform.matrix
print(camTransform) // UPDATING
}.store(in: &subs)
}
}
I'm currently trying to make a node that can change it's texture, but it's not visible on the simulator. I know it's there, cause I set showNodesCount to true and it displays that 2 nodes are on scene. It's displaying a cannon node, but not rgyBox node, that should change it's texture if I tap on cannon node. Here is my code:
class GameScene: SKScene {
var gameOver = false
let cannon = SKSpriteNode(imageNamed: "cannon")
let rgyArray = ["redBox", "greenBox", "yellowBox"]
var rgyBlock = SKSpriteNode()
func changeRgyColor() {
var randomRGY = Int(arc4random_uniform(3))
rgyBlock.texture = SKTexture(imageNamed: rgyArray[randomRGY])
}
override func didMoveToView(view: SKView) {
/* Setup your scene here */
cannon.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addChild(cannon)
rgyBlock.texture = SKTexture(imageNamed: rgyArray[1])
rgyBlock.position = CGPointMake(self.cannon.position.x, self.cannon.position.y + 20)
self.addChild(rgyBlock)
rgyBlock.zPosition = 10
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) == self.cannon {
changeRgyColor()
}
}
}
What am I doing wrong?
You need to recreate the rgyBlock by calling rgyBlock = SKSpriteNode(imageNamed: rgyArray[1]). Where it is defined at the top, it is defined as nothing. You then need to recreate it in didMoveToView. You can remove the texture set in didMoveToView for the rgyBlock.
You could do it a different way, and put imageNamed: "aTextureNameHere" into the define of rgyBlock. This will give rgyBlock a value. Then you can set the texture (as you already do) in didMoveToView. This way may be quicker and easier.