I am working with SceneKit and ARKit. I have made a collectionView with an array of emoji's. Now I want the user to be able to select the emoji from collectionView and when he/she touches the screen that selected emoji will be placed in 3D.
How can I do that? I think I have to create a function for the Node, but still my idea is blurry in the mind and I am not very much clear.
As far as any emoji is a 2D element, it's better to use a SpriteKit framework to upload them, not a SceneKit. But, of course, you might choose a SceneKit as well. So, there are two ways you can work with emojis in ARKit:
Using SpriteKit. In that case all the 2D sprites you spawn in ARSKView are always face the camera. So, if the camera moves around a definite point of real scene, all the sprites are rotates about their pivot point facing a camera.
Using SceneKit. In ARSCNView you can use all your sprites as a texture for 3D geometry. This texture could be for a plane, cube, sphere, or any custom model, it's up to you. To make, for example, a plane (with emojis texture on it) to face a camera use SCNBillboardConstraint constraint.
Here's how you code in ViewController might look like:
// Element's index coming from `collectionView`
var i: Int = 0
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
let emojiArray = ["🐶","🦊","🐸","🐼","🐹"]
let emojiNode = SKLabelNode(text: emojiArray[i])
emojiNode.horizontalAlignmentMode = .center
emojiNode.verticalAlignmentMode = .center
return emojiNode
}
...and in Scene.swift file:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let sceneView = self.view as? ARSKView else { return }
if let currentFrame = sceneView.session.currentFrame {
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.75 // 75 cm from camera
let transform = simd_mul(currentFrame.camera.transform, translation)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
Or, if you use hit-testing, your code might look like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let sceneView = self.view as? ARSKView else { return }
if let touchLocation = touches.first?.location(in: sceneView) {
if let hit = sceneView.hitTest(touchLocation, types: .featurePoint).first {
sceneView.session.add(anchor: ARAnchor(transform: hit.worldTransform))
}
}
}
If you'd like to create an UICollectionView overlay containing emojis to choose from, read the following post.
If you'd like to create an SKView overlay, containing emojis to choose from, read the following post.
Related
I am pretty new to SpriteKit so I may be missing something quite obvious.
I am attempting to create an interactive map of the US. I have loaded PNG images for each state and placed a couple of them into the main SKScene using the scene editor.
My goal is wanting to detect when each state is tapped by the user. I initially wrote some code that would find the nodes within a touch location however, this meant that the user could tap the frame and it would be counted as a valid tap. I only want to register touches that happen within the state texture and not the frame. Reading online it was suggested to use SKPhysicsBody to register when a tap takes place. So I changed my code to the following.
class GameScene: SKScene {
override func didMove(to view: SKView) {}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location: CGPoint = self.convertPoint(fromView: touch.location(in: self.view))
let body = self.physicsWorld.body(at: location)
if let state = body?.node, let name = state.name {
state.run(SKAction.run({
var sprite = self.childNode(withName: name) as! SKSpriteNode
sprite.color = UIColor.random()
sprite.colorBlendFactor = 1
}))
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
Now, if I choose the Bounding circle body type everything works as expected (shown above in the screenshot). When I click within the boudning circle it runs the SKAction otherwise it does nothing. However, when I change the body type to Alpha mask (the body type I want) it suddenly stops detecting the state. In fact, it returns the SKPhysicsBody for the MainScene entity.
Any advice on how to fix this would be greatly appreciated.
Thanks.
i can reproduce this behavior (bug?) when using the scene editor. however it goes away if you skip the sks file and initialize your sprites in code. (i acknowledge that setting locations for 50 states is more tedious this way.)
class GameScene: SKScene {
let ok = SKSpriteNode(imageNamed: "ok")
let nm = SKSpriteNode(imageNamed: "nm")
let tx = SKSpriteNode(imageNamed: "tx")
override func didMove(to view: SKView) {
tx.name = "Texas"
nm.name = "New Mexico"
ok.name = "Oklahoma"
//set up physics bodies w/ alpha mask
tx.physicsBody = SKPhysicsBody(texture: tx.texture!, size: tx.texture!.size())
tx.physicsBody?.affectedByGravity = false
nm.physicsBody = SKPhysicsBody(texture: nm.texture!, size: nm.texture!.size())
nm.physicsBody?.affectedByGravity = false
ok.physicsBody = SKPhysicsBody(texture: ok.texture!, size: ok.texture!.size())
ok.physicsBody?.affectedByGravity = false
//then position your sprites and add them as children
}
}
That seems a weird behavior but I'm wondering if there is a way to have nodes that have the same size on screen no matter how far it is from the camera?
I'm trying to show 2D elements in a city (just a text and an image) and some of them can be far but I still want the text and images to be visible but I also don't want it to look gigantic when I'm too close from it.
I'm currently using Apple SpriteKit example:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
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 0.2 meters in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.2
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)
}
}
You can measure the distance between pointOfView and the target node. and you can measure the target node accordingly with scale property
The only thing that 'works' for me so far is re-update the scale value for each Node:
func view(_ view: ARSKView, didUpdate node: SKNode, for anchor: ARAnchor)
{
node.setScale(1)
}
Do you know the puzzle game „voi“? That is a game which works with color-XOR-logic. That means: black + black = white.
https://www.youtube.com/watch?v=Aw5BdVcAtII
Is there any way to do the same color logic with two sprite nodes in sprit kit?
Thanks.
Of course, it's possible to do that in Sprite Kit.
Problem:
Let's say you have 2 black squares, squareA and squareB. The user can drag these two squares wherever he wants to. He can drag only one square at a time. You want to color the intersect area to white whenever the two squares intersect.
Initial Setup:
At the top of your scene, there are a few variables that we need to create:
private var squareA: SKSpriteNode?
private var squareB: SKSpriteNode?
private var squares = [SKSpriteNode]()
private var selectedShape: SKSpriteNode?
private var intersectionSquare: SKShapeNode?
squareA and squareB are just the 2 squares that we initially have on screen.
squares is an array and it will store all the squares that are showing on screen.
selectedShape will help us keeping track of the square that is currently being dragged.
intersectionSquare is a white square that represents the intersection area between the two black squares.
Then initialize squareA and squareB, and add them to the squares array like so:
squareA = SKSpriteNode(color: .black, size: CGSize(width: 190.0, height: 190.0))
if let squareA = self.squareA {
squareA.position = CGPoint(x: -200, y: 200)
squareA.name = "Square A"
squares.append(squareA)
self.addChild(squareA)
}
// Do the same for squareB or any other squares that you have on screen..
Note: As you can see, I gave it a name here just to make it easier to differentiate them during the testing phase.
Detect when user is dragging a square:
Now, you need to detect when the user is dragging a square. To do this, you can use:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
These are just helper methods that are going to make our life easier.
Then, you need to setup touchDown, touchMoved and touchUp methods:
func touchDown(atPoint pos : CGPoint) {
let touchedNode = self.nodes(at: pos)
guard let selectedSquare = touchedNode.first as? SKSpriteNode else {
return
}
selectedShape = selectedSquare
}
func touchMoved(toPoint pos : CGPoint) {
guard let selectedSquare = self.selectedShape else {
return
}
selectedSquare.position = pos
checkIntersectionsWith(selectedSquare)
}
func touchUp(atPoint pos : CGPoint) {
selectedShape = nil
}
To explain you in more details what is going on here:
In the touchDown method:
Well, we need the user to be able to drag only one square at a time. Using the nodes(at:) method, it's easy to know which square was touched, and we can know set our selectedShape variable to be equal to the square that was touched.
In the touchMoved method:
Here we are basically just moving the selectedShape to the position the user moves his finger at. We also call the checkIntersectionsWith() method that we will setup in a second.
In the touchUp method:
The user released his finger from the screen, so we can set the selectedShape to nil.
Change the color of the intersection frame:
Now the most important part to make your game actually look like the one you want to make, is how to change the color of the intersection frame to white when two black squares are intersecting ?
Well, you have different possibilities here, and here is one possible way of doing it:
private func checkIntersectionsWith(_ selectedSquare: SKSpriteNode) {
for square in squares {
if selectedSquare != square && square.intersects(selectedSquare) {
let intersectionFrame = square.frame.intersection(selectedSquare.frame)
intersectionSquare?.removeFromParent()
intersectionSquare = nil
intersectionSquare = SKShapeNode(rect: intersectionFrame)
guard let interSquare = self.intersectionSquare else {
return
}
interSquare.fillColor = .white
interSquare.strokeColor = .clear
self.addChild(interSquare)
} else if selectedSquare != square {
intersectionSquare?.removeFromParent()
intersectionSquare = nil
}
}
}
Every time the checkIntersectionsWith() method is called, we are iterating through the nodes that are inside our squares array, and we check, using the frame's intersection() method, if the selected square intersects with any of these (except itself). If it does, then we create a white square, named intersectionSquare, and set its frame to be equal to the intersection frame.
And to save up your memory usage, you can delete the square from the scene and set intersectionSquare to nil if there is no intersection at all.
Final result:
The final result would look like this:
That's just a rapid draft that I made to show you on you could approach the problem, and obviously there are many things that you could add or improve (apply this to a situation where you have not only 2 but many squares on screen, or create a kind of magnetism effect for when your user release his finger from the screen, etc) but I hope at least it will put you on the right track for your project :)
I'm trying to draw a single, straight line using UITouch and Spritekit. Where the line keeps showing during the dragging motion of my finger. if anyone knows a certain tutorial or can run me through the way of doing it i would be thankful
Probably not the best way to implement it, but it's simple. Just copy and paste this into your GameScene.swift
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var line:SKShapeNode = SKShapeNode()
private var path:CGMutablePath = CGMutablePath()
private var initialPointSet:Bool = false
override func didMove(to view: SKView) {
line.strokeColor = UIColor.orange
line.lineWidth = 4
addChild(line)
}
func touchMoved(toPoint pos : CGPoint) {
if !initialPointSet {
path.move(to: pos)
initialPointSet = true
}
path.addLine(to: pos)
line.path = path
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
}
SpriteKit has some of the worst 2D drawing ever presented in a 2D game engine.
Actually, the worst.
SKShapeNode and its CGPath are atrocious nonsense. It's as if the
"designers" of SpriteKit have never once looked at the most primitive
of 2D drawing in anything like DirectX or OpenGL. Let alone the kinds
of things that animators might want to do with lines and shape
creation, mutation, distortion and progress and line animation. ~ Confused
Having gotten that little rant off my chest, in the vain hope that it recalibrates your expectations to not expect a drawing style solution, consider this:
SKSpriteNodes can be a simple box, and they can be scaled in both the X and Y axis, and they can be parented to a "dummy" SKNode that rotates to face the direction of the current touch, relative to the original position of the touch.
SO.... you can draw a skinny box, starting at the point of the initial touch, and as the touch is moved, scale the SKSpriteNode to that point by both rotating the SKDummyNode you create to be the parent of your "line", and then scaling it out along that length from the origin to the current position of the touch.
Viola, a LINE!
Of sorts.
I am using SceneKit to import a 3d image model of a human body. When i select a particular location point in the image, i want the app to recognize the body part and perform a different function for each part. How do i go about implementing this? What is the best way to do this?
P.s. when the image is rotated it shows a different view. I need the app to be able to recognize the body part even when it is rotated by the user. Any guidance as to how to proceed would be appreciated..
Here's a simple SceneKit picking example.
The scene is setup in the viewDidLoad, for your use case I'd expect a scene would be loaded from file (best done in another method). This file will hopefully have the different components you wish to pick as separate components in a tree-like hierarchy. The author of this 3D body model will have hopefully labelled these components appropriately so that your code can identify what to do when your left-femur is selected (and not comp2345).
For a complex model expect several 'hits' for any xy coordinate as you will be returned all nodes intersected by the hit ray. You may wish to only use the first hit.
import UIKit
import SceneKit
class ViewController: UIViewController {
#IBOutlet var scenekitView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let boxNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
boxNode.name = "box"
scene.rootNode.addChildNode(boxNode)
let sphereNode = SCNNode(geometry: SCNSphere(radius: 1))
sphereNode.name = "sphere"
sphereNode.position = SCNVector3Make(2, 0, 0)
boxNode.addChildNode(sphereNode)
let torusNode = SCNNode(geometry: SCNTorus(ringRadius: 1, pipeRadius: 0.3))
torusNode.name = "torus"
torusNode.position = SCNVector3Make(2, 0, 0)
sphereNode.addChildNode(torusNode)
scenekitView.scene = scene
scenekitView.autoenablesDefaultLighting = true
scenekitView.allowsCameraControl = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//get the first touch location in screen coordinates
guard let touch = touches.first else {
return
}
//convert the screen coordinates to view coordinates as the SCNView make not take
//up the entire screen.
let pt = touch.locationInView(self.scenekitView)
//pass a ray from the points 2d coordinates into the scene, returning a list
//of objects it hits
let hits = self.scenekitView.hitTest(pt, options: nil)
for hit in hits {
//do something with each hit
print("touched ", hit.node.name!)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}