I'm new to ARKit and I'm trying to make a relatively simple app where you create a "wall" and the user "throws" balls at it, and it bounces back.
I create the wall as a SCNPlane where the user is pointing the camera to like so:
private func createWall(withPosition position: SCNVector3) -> SCNNode {
let wallGeometry = SCNPlane(width: 0.3, height: 0.3)
wallGeometry.firstMaterial?.isDoubleSided = true
wallGeometry.firstMaterial?.diffuse.contents = UIColor.green.withAlphaComponent(0.7)
let parentNode = SCNNode(geometry: wallGeometry)
let (position, _) = cameraPosition()
parentNode.position = position
parentNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: wallGeometry, options: nil))
parentNode.physicsBody?.isAffectedByGravity = false
self.wall = parentNode
return parentNode
}
I get the direction and position of camera with this function:
cameraPosition():
func cameraPosition() -> (SCNVector3, SCNVector3) {
guard let frame = sceneView.session.currentFrame else { return (SCNVector3(0, 0, -1), (SCNVector3(0, 0, 0))) }
let matrix = SCNMatrix4(frame.camera.transform)
let direction = SCNVector3(-matrix.m31, -matrix.m32, -matrix.m33)
let location = SCNVector3(matrix.m41, matrix.m42, matrix.m43)
return ((location + direction), direction)
}
// Helper function
func +(left: SCNVector3, right: SCNVector3) -> SCNVector3 {
return SCNVector3(left.x + right.x, left.y + right.y, left.z + right.z)
}
I create instances of Ball() and throw them like this:
let (position, direction) = cameraPosition()
// throw ball
let ball = Ball()
ball.position = position //SCNVector3(0,0,0)
sceneView.scene.rootNode.addChildNode(ball)
let impulseModifier = Float(10)
ball.physicsBody!.applyForce(SCNVector3(direction.x*impulseModifier, direction.y*impulseModifier, direction.z*impulseModifier), asImpulse: true)
The Ball class:
class Ball: SCNNode {
override init() {
super.init()
let sphere = SCNSphere(radius: 0.05)
sphere.firstMaterial?.diffuse.contents = UIColor.red
self.geometry = sphere
self.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(geometry: sphere, options: nil))
}
}
The problem is that many times, instead of the ball actually hitting the wall, it will just travel through it, as if the physics body does not function properly. I noticed that sometimes it will work better when I change the distance and angle between the camera and the wall, but the results are never consistent as far as I've tried.
I have also tried to change the wall position to: SCNVector3(0, 0, -1) to position it exactly 1 meter deep away from the world origin, and the results were somewhat better but still not consistent.
Where could the problem lay and why?
Thanks in advance!
Set your wall physics to be kinematic so that the ball can react with it.
Static objects don't react with anything.
Kinematic objects react with other objects but other objects don't affect them.
Dynamic of course means it works in both directions.
Most likely this is due to the ball being too small for the collision detection to work properly. Try setting continuousCollisionDetectionThreshold equal to half of the ball radius. In this case:
ball.physicsBody?.continuousCollisionDetectionThreshold = 0.025
Related
I'm trying to incorporate GameplayKit into a SpriteKit project.
Specifically, I'm trying to use GameplayKit's seek-and-avoid behavior wherein an "enemy" character chases a moving "player" character while avoiding obstacles.
I have managed to make the enemy seek the player, but the enemy's movement is weird/undesirable: The enemy's speed should adjust so that it comes to a halt at the desired end point, however its speed does not change, which makes it overshoot its target considerably.
Think of a fast-moving train: Once it gets up to speed, it's hard to stop; it wants to just keep on moving. It's the same phenomenon with the enemy character sprite. It moves like a heavy, lumbering object that can't stop quickly enough.
Here's what I'm doing (please assume that properties such as node are defined):
Configure the player character's GameplayKit stuff:
let entity1 = GKEntity()
heroAgent = GKAgent2D()
heroComponent = GKSKNodeComponent(node: node)
heroAgent.delegate = self
node.entity = entity1
heroEntity = entity1
if let comp = heroComponent {
entity1.addComponent(comp)
entity1.addComponent(heroAgent)
}
Configure the enemy character's GameplayKit stuff:
let entity = GKEntity()
let agent = GKAgent2D()
let avoid = GKGoal(toAvoid: obstaclesForThisLevel!, maxPredictionTime: 1000.0)
let pursue = GKGoal(toInterceptAgent: heroAgent, maxPredictionTime: 0.0)
nodeComponent = GKSKNodeComponent(node: node)
agent.maxSpeed = 100.0
agent.maxAcceleration = 50.0
agent.position = vector_float2(x: Float(levelData.enemies[i].x), y: Float(levelData.enemies[i].y))
agent.radius = Float(node.size.width*0.5)
agent.behavior = GKBehavior(goals: [avoid, pursue], andWeights: [100.0, 100.0])
agent.delegate = self
node.entity = entity
enemyAgent = agent
if let comp = nodeComponent {
entity.addComponent(comp)
entity.addComponent(agent)
}
nodeEntity = entity
In the SKScene's update(_:) method, set the player agent's position and call the enemy character's update(deltaTime:) method:
//Coordinate the hero agent's position with the hero's actual position. Without this, the values in agentDidUpdate() are nan.
heroAgent.position = vector_float2(x: Float(mainCharacter?.position.x ?? 0.0), y: Float(mainCharacter?.position.y ?? 0.0))
nodeComponent?.node.entity?.update(deltaTime: currentTime-lastFrameTime)
Set the enemy character's position in agentDidUpdate(_:):
func agentDidUpdate(_ agent: GKAgent) {
if let a = agent as? GKAgent2D {
nodeComponent?.node.position = CGPoint(x: CGFloat(a.position.x), y: CGFloat(a.position.y))
}
}
Question: Why is the enemy character overshooting its designated target point instead of managing its speed correctly, and how do I fix the issue?
Thank you!
I decided to only use GameplayKit for the purpose of generating a path around the obstacles (findPath(from:to:) is very helpful), and to simply move the enemy character along that path via SKAction.follow(_:asOffset:orientToPath:speed:) when I want to send the enemy back to its starting position.
So, to accomplish the "seeking" behavior, I'm just doing the following from inside my SKScene's update(_:) method:
let dx = mainCharacter.position.x - enemy.position.x
let dy = mainCharacter.position.y - enemy.position.y
let angle = atan2(dy, dx)
let vx = cos(angle) * enemySpeed
let vy = sin(angle) * enemySpeed
enemy.physicsBody?.velocity.dx = vx
enemy.physicsBody?.velocity.dy = vy
This seeking behavior does not consider obstacles, so the enemy can get kind of stuck behind an obstacle that's located between the enemy and the player. So, it's not ideal, but I think it's going to work for me because of other factors in my game.
To generate a path that does consider obstacles, for use with SKAction, I'm doing the following:
let graph = GKObstacleGraph(obstacles: obstaclesForThisLevel, bufferRadius: 0.0)
let startingPoint = GKGraphNode2D(point: vector_float2(x: Float(enemy.position.x), y: Float(enemy.position.y)))
let endPoint = GKGraphNode2D(point: vector_float2(x: Float(enemy.enemyOriginalPosition.x), y: Float(enemy.enemyOriginalPosition.y)))
graph.connectUsingObstacles(node: startingPoint)
graph.connectUsingObstacles(node: endPoint)
let path = (graph as GKGraph).findPath(from: startingPoint, to: endPoint)
if path.count >= 2 {
let realPath = GKPath(graphNodes: path, radius: 100.0)
let myPath: UIBezierPath = UIBezierPath()
for j in 1..<realPath.numPoints {
let previousPoint = realPath.float2(at: j-1)
let nextPoint = realPath.float2(at: j)
if j == 1 {
myPath.move(to: CGPoint(x: CGFloat(previousPoint.x), y: CGFloat(previousPoint.y)))
}
myPath.addLine(to: CGPoint(x: CGFloat(nextPoint.x), y: CGFloat(nextPoint.y)))
}
}
And finally:
let followPath = SKAction.follow(myPath.cgPath, asOffset: false, orientToPath: false, speed: enemySpeed)
enemy.run(followPath)
I have created Wall with SCNPlane. on that plane I am adding another Plane where User point the device
But because of both plane has same position newly added plane is flickering.
So How can I keep distance from wall node and plane node
how i add plane node.
let hitTestScene = self.sceneView.hitTest(self.sceneView.center, options:[SCNHitTestOption.categoryBitMask : 16])
if let first = hitTestScene.first {
if first.node.name == NodeNames.wallNode {
let value = MathHelper().getMeasurementBetween(vector1: lastNodePosition, and: first.worldCoordinates)
let node = SCNNode(geometry: SCNPlane(width: CGFloat( value) , height: CGFloat(value)))
node.geometry?.firstMaterial?.isDoubleSided = true
node.name = "Plane1"
node.geometry?.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(1)
node.eulerAngles = first.node.eulerAngles
node.position = lastNodePosition
self.sceneView.scene.rootNode.addChildNode(node)
}
}
Plane in red is continually flicked (appear - disappear )
I have tried node.position.z -= 0.2 but it is not working and node position changes completely because of node.eulerAngles
Any help would be appreciated
I have figure it out.
by setting rendringOrder and readsFromDepthBuffer I was able to stop that flick effect
extension SCNNode {
func renderOnTop() {
self.renderingOrder = 2
if let geom = self.geometry {
for material in geom.materials {
material.readsFromDepthBuffer = false
}
}
for child in self.childNodes {
child.renderOnTop()
}
}
}
Hope some can use it if same issue
I have been trying to move car body along with wheels. I have creating augmented reality project, where placing car model in horizontal plane and Car are controlled by four buttons namelu acceleration, steering, reverse and brake. car left, right are controller by steering while acceleration and reverse.
Actually now i can able to placing 3d car model in horizontal plane but dont know how to rotate car wheels like forward and backward along with car body.
Here is the code where i have been trying for placing 3d object:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
// gives us the location of where we touched on the 2D screen.
let touchLocation = touch.location(in: sceneView)
// hitTest is performed to get the 3D coordinates corresponding to the 2D coordinates that we got from touching the screen.
// That 3d coordinate will only be considered when it is on the existing plane that we detected.
let results = sceneView.hitTest(touchLocation, types: .existingPlaneUsingExtent)
// if we have got some results using the hitTest then do this.
if let hitResult = results.first {
let boxScene = SCNScene(named: "art.scnassets/porsche.scn")!
if let boxNode = boxScene.rootNode.childNode(withName: "car", recursively: true) {
print("box:::\(boxNode.childNodes)")
boxNode.position = SCNVector3(x: hitResult.worldTransform.columns.3.x, y: hitResult.worldTransform.columns.3.y + 0.15, z: hitResult.worldTransform.columns.3.z)
// finally the box is added to the scene.
sceneView.scene.rootNode.addChildNode(boxNode)
}
}
}
}
Detecting horizontal plane functionality code:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if anchor is ARPlaneAnchor {
// anchors can be of many types, as we are just dealing with horizontal plane detection we need to downcast anchor to ARPlaneAnchor
let planeAnchor = anchor as! ARPlaneAnchor
// creating a plane geometry with the help of dimentions we got using plane anchor.
let plane = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z))
// a node is basically a position.
let planeNode = SCNNode()
// setting the position of the plane geometry to the position we got using plane anchor.
planeNode.position = SCNVector3(x: planeAnchor.center.x, y: 0, z: planeAnchor.center.z)
// when a plane is created its created in xy plane instead of xz plane, so we need to rotate it along x axis.
planeNode.transform = SCNMatrix4MakeRotation(-Float.pi/2, 1, 0, 0)
//create a material object
let gridMaterial = SCNMaterial()
//setting the material as an image. A material can also be set to a color.
gridMaterial.diffuse.contents = UIImage(named: "art.scnassets/grid.png")
// assigning the material to the plane
plane.materials = [gridMaterial]
// assigning the position to the plane
planeNode.geometry = plane
//adding the plane node in our scene
node.addChildNode(planeNode)
}
else {
return
}
}
Any help much appreciated!!!
This is quite comprehensive task.
You should make your car as SCNPhysicsVehicle subclass.
SCNPhysicsVehicle is a physics behavior that modifies a physics body to behave like a car, motorcycle, or other wheeled vehicle.
To build a vehicle, designate an SCNPhysicsBody object as its chassis and an array of SCNPhysicsVehicleWheel objects as its wheels. For each wheel, you define physical characteristics such as suspension and traction, and associate a node in your scene to provide the wheel’s size and visual representation. After you construct a vehicle, you can control it in terms of acceleration, braking, and steering.
General: https://developer.apple.com/documentation/scenekit/scnphysicsbody
For wheels: https://developer.apple.com/documentation/scenekit/scnphysicsvehiclewheel
Example:
var vehicle = SCNPhysicsVehicle()
You need to create ground with static body.
func createGround(planeAnchor: ARPlaneAnchor) -> SCNNode {
let ground = SCNNode(geometry: SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(CGFloat(planeAnchor.extent.z))))
ground.position = SCNVector3(planeAnchor.center.x,planeAnchor.center.y,planeAnchor.center.z)
ground.eulerAngles = SCNVector3(90.degreesToRadians, 0, 0)
let staticBody = SCNPhysicsBody.static() // it must be static
ground.physicsBody = staticBody
return ground
}
You need to setup your car.
func setupCar() {
let scene = SCNScene(named: "YourScene.scn")
let chassis = (scene?.rootNode.childNode(withName: "chassis", recursively: true))! // Your chassis
let frontLeftWheel = chassis.childNode(withName: "frontLeftWheel", recursively: true)!
let frontRightWheel = chassis.childNode(withName: "frontRightWheel", recursively: true)!
let rearLeftWheel = chassis.childNode(withName: "rearLeftWheel", recursively: true)!
let rearRightWheel = chassis.childNode(withName: "rearRightWheel", recursively: true)!
// physic behavior for wheels
let v_frontLeftWheel = SCNPhysicsVehicleWheel(node: frontLeftWheel)
let v_frontRightWheel = SCNPhysicsVehicleWheel(node: frontRightWheel)
let v_rearRightWheel = SCNPhysicsVehicleWheel(node: rearLeftWheel)
let v_rearLeftWheel = SCNPhysicsVehicleWheel(node: rearRightWheel)
chassis.position = SCNVector(0,0,0) // insert your desired position
let body = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(node: chassis, options: [SCNPhysicsShape.Option.keepAsCompound: true]))
body.mass = 1
chassis.physicsBody = body
self.vehicle = SCNPhysicsVehicle(chassisBody: chassis.physicsBody!, wheels: [v_rearRightWheel, v_rearLeftWheel, v_frontRightWheel, v_frontLeftWheel])
self.sceneView.scene.physicsWorld.addBehavior(self.vehicle)
self.sceneView.scene.rootNode.addChildNode(chassis)
}
Now you can manipulate with your car.
func renderer(_ renderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) {
var engineForce: CGFloat = 5
var brakingForce: CGFloat = 0
// here you can manipulate your car steering angle
self.vehicle.setSteeringAngle(0, forWheelAt: 2)
self.vehicle.setSteeringAngle(0, forWheelAt: 3)
self.vehicle.applyEngineForce(engineForce, forWheelAt: 0)
self.vehicle.applyEngineForce(engineForce, forWheelAt: 1)
self.vehicle.applyBrakingForce(0, forWheelAt: 0)
self.vehicle.applyBrakingForce(0, forWheelAt: 1)
}
P.S. Don't forget to add to your viewDidLoad():
self.sceneView.delegate = self
self.sceneView.scene?.physicsWorld.contactDelegate = self
P.P.S This is only a general idea, you must do some research and find a working solution to your specific situation.
I hope it helped!
I'm trying to place objects on top of each others, but for some reason the physics are not working as they should. When I drop items on top of other items there's always some space which remains between the items. What is that space and how can I get rid of it?
As you can see in the picture below, there're three objects stacked on top of each others. This is what always happens when I place them on top of each others.
This is how I create the plane and add physics to it:
private func createPlaneWithPhysics(planeCenter pos: vector_float3) -> SCNNode {
let planeMaterial = SCNMaterial()
planeMaterial.diffuse.contents = UIColor.white.withAlphaComponent(0)
let planeGeometry = SCNBox(width: defaultPlaneWidth, height: 0.01, length: defaultPlaneHeight, chamferRadius: 0)
planeGeometry.materials = [planeMaterial]
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.position = SCNVector3(pos.x, pos.y - DimensionCorrection.yAxisError, pos.z)
let shape = SCNPhysicsShape(geometry: planeGeometry, options: nil)
let physicsBody = SCNPhysicsBody(type: .kinematic, shape: shape)
planeNode.physicsBody = physicsBody
planeNode.physicsBody?.categoryBitMask = Int(SCNPhysicsCollisionCategory.static.rawValue)
planeNode.physicsBody?.contactTestBitMask = Int(SCNPhysicsCollisionCategory.default.rawValue)
return planeNode
}
And in the VirtualContainer I add physics for the VirtualItem(.physicsBody) in the following way:
private func physicsForvirtualItem() -> SCNPhysicsBody {
let shape = SCNPhysicsShape(node: virtualItem, options: [SCNPhysicsShape.Option.type : SCNPhysicsShape.ShapeType.boundingBox])
let physics = SCNPhysicsBody(type: .dynamic, shape: shape)
physics.mass = virtualItem.mass
physics.isAffectedByGravity = true
physics.velocityFactor = SCNVector3(0, 1, 0)
physics.angularVelocityFactor = SCNVector3(0, 1, 0)
physics.categoryBitMask = Int(SCNPhysicsCollisionCategory.default.rawValue)
return physics
}
The physicShape wasn’t moving when I used node: instead of geometry: in the physicsShape method... see my approach below - set a primitive geometry to the physicShape node.
let shape = SCNPhysicsShape(node: virtualItem, options: [SCNPhysicsShape.Option.type : SCNPhysicsShape.ShapeType.convexHull])
When you import geometry created in external 3D authoring packages, the default pivots & bounding boxes can be very unpredictable. To troubleshoot you might just create a simple box geometry and apply it to your imported or irregular geometry.
let box = SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0)
let shape = SCNPhysicsShape(geometry: box, options: [SCNPhysicsShape.Option.type : SCNPhysicsShape.ShapeType.convexHull])
let physics = SCNPhysicsBody(type: .dynamic, shape: shape)
I think it might have something to do with using node instead of geometry
if the box is not centered on the geometry the pivot needs to be centred using a centerPivot function like...
func centerPivot(for node: SCNNode) {
var min = SCNVector3Zero
var max = SCNVector3Zero
node.__getBoundingBoxMin(&min, max: &max)
node.pivot = SCNMatrix4MakeTranslation(
min.x + (max.x - min.x)/2,
min.y + (max.y - min.y)/2,
min.z + (max.z - min.z)/2
)
}
to use just
centerPivot(for: node!)
Using nil or the standard shapes for the collision geometry is designed for speed -- not for correctness. Just use the actual geometry of the SCNNode instead of one of the standard shapes. Today's devices are fast so you are probably fine doing this in many cases.
Something like this, assuming you have your SCNNode already set up with its associated geometry, either from code, or by importing a .dae or .scn file:
let node = SCNNode()
// (add geometry via code or init node from scene loaded from file)
let pShape = SCNPhysicsShape(geometry: node.geometry)
let pBody = SCNPhysicsBody(type: .dynamic, shape: pShape)
// (add any other physicsBody setup stuff)
node.physicsBody = pBody
I am creating an app that uses SceneKit. The model is at a fixed location and the user can translate and rotate the camera around the scene.
Translating works fine, and rotating the camera also works - as long as only one axis was rotated.
When the camera faces down or up and the camera is rotated to the left or right, it not only rotates around that axis, but also around a second axis which looks really weird.
I tried moving the pivot point, but that didn't help.
Here is the code that I use for rotating and moving the camera:
fileprivate func translateCamera(_ x: Float, _ y: Float)
{
if let cameraNode = self.cameraNode
{
let moveX = x * 2 // TODO Settings.speed
let moveY = -y * 2 // TODO Settings.speed
let position = SCNVector3Make(moveX, 0, moveY)
let rotatedPosition = self.position(position, cameraNode.rotation)
let translated = SCNMatrix4Translate(cameraNode.transform, rotatedPosition.x, rotatedPosition.y, rotatedPosition.z)
cameraNode.transform = translated
if cameraNode.position.y < 25
{
cameraNode.position.y = 25
}
}
}
fileprivate func position(_ position: SCNVector3, _ rotation: SCNVector4) -> SCNVector3
{
if rotation.w == 0
{
return position
}
let gPosition: GLKVector3 = SCNVector3ToGLKVector3(position)
let gRotation = GLKMatrix4MakeRotation(rotation.w, rotation.x, rotation.y, rotation.z)
let r = GLKMatrix4MultiplyVector3(gRotation, gPosition)
return SCNVector3FromGLKVector3(r)
}
fileprivate func rotateCamera(_ x: Float, _ y: Float)
{
if let cameraNode = self.cameraNode
{
let moveX = x / 50.0
let moveY = y / 50.0
let rotated = SCNMatrix4Rotate(SCNMatrix4Identity, -moveX, 0, 1, 0)
cameraNode.transform = SCNMatrix4Mult(rotated, cameraNode.transform)
let rotated2 = SCNMatrix4Rotate(SCNMatrix4Identity, moveY, 1, 0, 0)
cameraNode.transform = SCNMatrix4Mult(rotated2, cameraNode.transform)
}
}
What would be the correct approach to "lock" the camera so it only moves around the desired axis? I made a small video showing the behavior:
https://www.youtube.com/watch?v=ctK-hnw7JxY
As long as only one axis was rotated it works fine.
But as soon as the second axis gets rotated, it also tilts to the side.
Create empty node and add cameraNode as its child. rotate cameraNode for x and emptyNode for y.