How to add button and label to the scene - ios

I am new in Scenekit and ARKit, I wanted to add button and label inside UIView to the scenekit's scene. I am getting the world coordinates by the hit test to place the view but SCNNode don't have addSubView Method to add view.
I want to achieve following output :
My attempts to achieve this :
func addHotspot(result : SCNHitTestResult, parentNode : SCNNode? = nil) {
let view = UIView()
view.backgroundColor = .red
let material = SCNMaterial()
material.diffuse.contents = view
let plane = SCNPlane(width: 100, height: 100)
plane.materials = [material]
let node = SCNNode()
node.geometry = plane
node.position = result.worldCoordinates
parentNode?.addChildNode(node)
}
error :
Please suggest the way I can complete it.

To add a UIView object to a scene node, you can assign it to the node's geometry material diffuse content. It will be added to the node and will maintain its position as you move your device around:
DispatchQueue.main.async {
let view: UIView = getView() // this is your UIView instance that you want to add to the node
let material = SCNMaterial()
material.diffuse.contents = view
node.geometry.materials = [material]
}

Related

Scenekit - Add child node (Plane node) to the parent node (sphere node) in front of camera

I am trying to add child node to the parent node which have it's own camera. on tap of the user on scene, using hit test I am adding child node (i.e plane node) to the the parent node (sphere node). But I don't know why the child node is not facing properly to the camera. it's looking different on different location. I wanted it to fix looking at the camera.
Code :
// Creating sphere node and adding camera to it
func setup() {
let sphere = SCNSphere(radius: 10)
sphere.segmentCount = 360
let material = getTextureMaterial()
material.diffuse.contents = UIImage(named: "sample")
sphere.firstMaterial = material
let sphereNode = SCNNode()
sphereNode.geometry = sphere
sceneView.scene?.rootNode.addChildNode(sphereNode)
}
//Creating texture material to show 360 degree image...
func getTextureMaterial() -> SCNMaterial {
let material = SCNMaterial()
material.diffuse.mipFilter = .nearest
material.diffuse.magnificationFilter = .linear
material.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1)
material.diffuse.wrapS = .repeat
material.cullMode = .front
material.isDoubleSided = true
return material
}
#objc func handleTap(rec: UITapGestureRecognizer){
if rec.state == .ended {
let location: CGPoint = rec.location(in: sceneView)
let hits = sceneView.hitTest(location, options: nil)
if !hits.isEmpty {
let result: SCNHitTestResult = hits[0]
createPlaneNode(result: result)
}
}
}
func createPlaneNode(result : SCNHitTestResult) {
let plane = SCNPlane(width: 5, height: 5)
plane.firstMaterial?.diffuse.contents = UIColor.red
plane.firstMaterial?.isDoubleSided = true
let planeNode = SCNNode()
planeNode.name = "B"
planeNode.geometry = plane
planeNode.position = result.worldCoordinates
result.node.addChildNode(planeNode)
}
Results I am getting using above code:
Added Plane node is looking weird
I don't know if you ever found your answer but, it would be done freeing the node axes with SCNBillboardConstraint :)
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = [.all]
node.constraints = [billboardConstraint]

ARKit: How can I add a UIView to ARKit Scene?

I am working on an AR project using ARKit.
I want to add a UIView to ARKit Scene. When I tap on an object, I want to get information as a "pop-up" next to the object. This information is in a UIView.
Is it possible to add this UIView to ARKit Scene?
I set up this UIView as a scene and what can I do then?
Can I give it a node and then add it to the ARKit Scene? If so, how it works?
Or is there another way?
Thank you!
EDIT: Code of my SecondViewController
class InformationViewController: UIViewController {
#IBOutlet weak var secondView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.view = secondView
}
}
EDIT 2: Code in firstViewController
guard let secondViewController = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else {
print ("No secondController")
return
}
let plane = SCNPlane(width: CGFloat(0.1), height: CGFloat(0.1))
plane.firstMaterial?.diffuse.contents = secondViewController.view
let node = SCNNode(geometry: plane)
I only get a white screen of a plane, not the view.
The simplest (although undocumented) way to achieve that is to set a UIView backed by a view controller as diffuse contents of a material on a SCNPlane (or any other geometry really, but it works best with planes for obvious reasons).
let plane = SCNPlane()
plane.firstMaterial?.diffuse.contents = someViewController.view
let planeNode = SCNNode(geometry: plane)
You will have to persist the view controller somewhere otherwise it's going to be released and plane will not be visible. Using just a UIView without any UIViewController will throw an error.
The best thing about it is that it keeps all of the gestures and practically works just as a simple view. For example, if you use UITableViewController's view you will be able to scroll it right inside a scene.
I haven't tested it on iOS 10 and lower, but it's been working on iOS 11 so far. Works both in plain SceneKit scenes and with ARKit.
I cannot provide you code now but this is how to do it.
Create a SCNPlane.
Create your UIView with all elements you need.
Create image context from UIView.
Use this image as material for SCNPlane.
Or even easier make SKScene with label and add it as material for SCNPlane.
Example: https://stackoverflow.com/a/74380559/294884
To place text in a label in the world you draw it into an image and then attach that image to a SCNNode.
For example:
let text = "Hello, Stack Overflow."
let font = UIFont(name: "Arial", size: CGFloat(size))
let width = 128
let height = 128
let fontAttrs: [NSAttributedStringKey: Any] =
[NSAttributedStringKey.font: font as UIFont]
let stringSize = self.text.size(withAttributes: fontAttrs)
let rect = CGRect(x: CGFloat((width / 2.0) - (stringSize.width/2.0)),
y: CGFloat((height / 2.0) - (stringSize.height/2.0)),
width: CGFloat(stringSize.width),
height: CGFloat(stringSize.height))
let renderer = UIGraphicsImageRenderer(size: CGSize(width: CGFloat(width), height: CGFloat(height)))
let image = renderer.image { context in
let color = UIColor.blue.withAlphaComponent(CGFloat(0.5))
color.setFill()
context.fill(rect)
text.draw(with: rect, options: .usesLineFragmentOrigin, attributes: fontAttrs, context: nil)
}
let plane = SCNPlane(width: CGFloat(0.1), height: CGFloat(0.1))
plane.firstMaterial?.diffuse.contents = image
let node = SCNNode(geometry: plane)
EDIT:
I added these lines:
let color = UIColor.blue.withAlphaComponent(CGFloat(0.5))
color.setFill()
context.fill(rect)
This lets you set the background color and the opacity. There are other ways of doing this - which also let you draw complex shapes - but this is the easiest for basic color.
EDIT 2: Added reference to stringSize and rect

Mask SKSpriteNode

I add few nodes to my sks file, and now I want to add some mask to the most lowest node SKSpriteNode. Structure is as shown below:
where
green - wordInfoHolder
red - container for Label
whiteText - label
Now I want to hide part that is shown in red. To perform such action I read that SKCropNode can be used.
I able to find all my nodes in scene file and save them to variables.
if let holder = childNode(withName: "wordInfoHolder") as? SKSpriteNode {
wordInfoHolder = holder
if let wordSwitchNode = wordInfoHolder?.childNode(withName:"wordSwitchNode") as? SKSpriteNode {
self.wordSwitchNode = wordSwitchNode
if let label = self.wordSwitchNode?.childNode(withName:"infoLabel") as? SKLabelNode {
wordSwitchLabelNode = label
}
}
}
all 3 objects are stored and correct.
Now I want to add some mask to root object.
For such purpose I prepared mask image:
and try to do something like:
guard let holder = wordInfoHolder else { return }
let positionToSet = holder.position
let mask = SKSpriteNode(imageNamed: "rectangle_mask")
let cropNode = SKCropNode()
holder.removeFromParent()
cropNode.addChild(holder)
cropNode.maskNode = mask
cropNode.position = positionToSet
self.addChild(cropNode)
But I see nothing. I expect to see green part of SKSpriteNode.
What was done wrong?
Assuming that you have set zPosition on all your objects.
I'm quite sure the issue is that because you are moving your holder object from the scene to the cropNode it is retaining it's position info from the scene (for example if it's position in the scene was 500, 500 it's position in the cropNode is now 500, 500)
I was able to recreate your issue and by setting the holder.position to zero the issue went away.
In the picture below I am using the yellow box as mask and the blue and pink boxes are test objects to ensure the the cropNode is placed between them.
if let holder = self.childNode(withName: "holder") as? SKSpriteNode {
self.holder = holder
if let switcher = holder.childNode(withName: "//switcher") as? SKSpriteNode {
self.switcher = switcher
}
}
if let mask = self.childNode(withName: "mask") as? SKSpriteNode {
mask.removeFromParent()
let positionToSet = holder.position
holder.position = CGPoint.zero
mask.position = CGPoint.zero
let cropNode = SKCropNode()
holder.removeFromParent()
cropNode.addChild(holder)
cropNode.maskNode = mask
cropNode.position = positionToSet
cropNode.zPosition = 10
self.addChild(cropNode)
}
added tidbit
holder.move(toParent: cropNode)
can be used in place of
holder.removeFromParent()
cropNode.addChild(holder)

How to join multiple nodes to one node in iOS scene

As we can see in below pic there all multiple nodes in the scene graph. The requirement is I want to add these nodes in one node as shown in image2. So In scene graph, I will have one node(chair) which will contain multiple nodes
Image1
Image2
You create a parent node and then add all your nodes as children.
i.e.:
var nodes: [SCNNode] = getMyNodes()
var parentNode = SCNNode()
parentNode.name = "chair"
for node in nodes {
parentNode.addChildNodes(node)
}
When you apply an update to a node, the update is also pushed down to the children, for example:
parentNode.transform = SCNMatrix4MakeTranslation(0, 0.5, 1.2)
will apply the same translation transformation to all the children attached to parentNode.
You can access a specific child using:
parentNode.childNode(withName: nameOfChildNode, recursively: true)
and get the parent of any child using:
myChildNode.parent
EDIT:
If you are importing from a scene file, you can still easily access your nodes programmatically:
let scene = SCNScene(named: "myScene.scn")
func getMyNodes() -> [SCNNode] {
var nodes: [SCNNode] = [SCNNode]()
for node in scene.rootNode.childNodes {
nodes.append(node)
}
return nodes
}
which, incidentally, means that you can ignore most of the above and use myScene.rootNode as the parent node.
My suggestions would be :
Statically : Create a new .scn file in which you drag & drop every node you're trying to group
Dynamically : Create a new SCNNode. For each node you're trying to group, use the .addChildNode method
I used the second one a couple of times to build dynamical scenes using ARKit. Worked like a charm.
Basically, I added all the childs of my .dae file to a SCNNode :
var node = SCNNode()
let scene = SCNScene(named: "myScene.dae")
var nodeArray = scene.rootNode.childNodes
for childNode in nodeArray {
node.addChildNode(childNode as SCNNode)
}
You should give it a try with your files, using the same .addChildNode method :)
in case, if somebody is looking for a solution like this yellow-black stick
class VirtualObject: SCNNode {
enum Style {
case anchor
case fence
}
let style: Style
var value: String?
init(_ style: Style) {
self.style = style
super.init()
switch style {
case .anchor:
let material = SCNMaterial()
material.diffuse.contents = UIColor.magenta
material.lightingModel = .physicallyBased
let object = SCNCone(topRadius: 0.02, bottomRadius: 0, height: 0.1) // SCNSphere(radius: 0.02)
object.materials = [material]
self.geometry = object
case .fence:
for index in 0 ... 6 {
let material = SCNMaterial()
material.diffuse.contents = index % 2 == 0 ? UIColor.yellow : UIColor.black
material.lightingModel = .physicallyBased
let node = SCNNode(geometry: SCNCylinder(radius: 0.01, height: 0.05))
node.geometry?.materials = [material]
node.transform = SCNMatrix4MakeTranslation(0, 0.05 * Float(index), 0)
addChildNode(node)
}
}
}

Can you change the properties of scnView.autoenablesDefaultLighting?

I need lights to stay "stationary" in my scene. The best lighting method I've found so far is to actually to use scnView.autoenablesDefaultLighting = true however, I can't figure out if there's any way to control some of the attributes. The intensity of the light is a BIT too bright, the location of the light is a BIT different than where I'd like it to be, those kinds of properties.
I've tried using all sorts of other lights, coding them individually BUT since they add to the scene as nodes, the lights (in those cases) themselves will move when I set scnView.allowsCameraControl = true. The default lighting is the only one that will remain "stationary" once the user begins to move the camera around. Can you access/control the properties of the default lighting?
Forget allowsCameraControl and default cameras and lights if you want control of your scene.
let sceneView = SCNView()
let cameraNode = SCNNode() // the camera
var baseNode = SCNNode() // the basic model-root
let keyLight = SCNLight() ; let keyLightNode = SCNNode()
let ambientLight = SCNLight() ; let ambientLightNode = SCNNode()
func sceneSetup() {
let scene = SCNScene()
// add to an SCNView
sceneView.scene = scene
// add the container node containing all model elements
sceneView.scene!.rootNode.addChildNode(baseNode)
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 0, 50)
scene.rootNode.addChildNode(cameraNode)
keyLight.type = SCNLightTypeOmni
keyLightNode.light = keyLight
keyLightNode.position = SCNVector3(x: 10, y: 10, z: 5)
cameraNode.addChildNode(keyLightNode)
ambientLight.type = SCNLightTypeAmbient
let shade: CGFloat = 0.40
ambientLight.color = UIColor(red: shade, green: shade, blue: shade, alpha: 1.0)
ambientLightNode.light = ambientLight
cameraNode.addChildNode(ambientLightNode)
// view the scene through your camera
sceneView.pointOfView = cameraNode
// add gesture recognizers here
}
Move or rotate cameraNode to effect motion in view. Or, move or rotate baseNode. Either way, your light stay fixed relative to the camera.
If you want your lights fixed relative to the model, make them children of baseNode instead of the camera.
If someone who wondering how to setup entire scene with new Scenekit integration with swift ui go though this. This work fine.
struct TestControllerNew: View {
let sceneView = SCNView()
let cameraNode = SCNNode()
var baseNode = SCNNode()
let id = "D69A09F8-EA80-4231-AD35-4A9908B4343C"
var scene = SCNScene()
var body: some View {
SceneView(
scene: sceneSetup(),
pointOfView: cameraNode,
options: [
.autoenablesDefaultLighting
]
)
}
func getTermsOfUseURL() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0].appendingPathComponent("\(id).usdz")
}
func sceneSetup() -> SCNScene {
let scene = try! SCNScene(url: getTermsOfUseURL())
// add to an SCNView
sceneView.scene = scene
// add the container node containing all model elements
sceneView.scene!.rootNode.addChildNode(baseNode)
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 1, 10)
scene.rootNode.addChildNode(cameraNode)
// view the scene through your camera
sceneView.pointOfView = cameraNode
// add gesture recognizers here
return scene
}
}
Here the USDZ file get from document directory as a URL, you can use name instead of that.

Resources