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)
}
}
}
Related
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]
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]
}
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!
how can I make a grid with 3d objects(box). I already know how to setup scnscene and how to create an object. But I don't know how to make the layout. The grid should look like this one, with 3d object in a 3D space.
Here's what I tried:
convenience init(create: Bool) {
self.init()
let geometry = SCNBox(width: 0.8 , height: 0.8,
length: 0.1, chamferRadius: 0.005)
geometry.firstMaterial?.diffuse.contents = UIColor.red
geometry.firstMaterial?.specular.contents = UIColor.white
geometry.firstMaterial?.emission.contents = UIColor.blue
let offset: Int = 10
for xIndex:Int in 0...2 {
for yIndex:Int in 0...2 {
// create a geometry copy
let geoCopy = geometry.copy() as! SCNGeometry
var images:[UIImage]=[]
for i in 1...5 {
if let img = UIImage(named: "\(i)"){
images.append(img)
let material = SCNMaterial()
material.diffuse.contents = img
geoCopy.firstMaterial = material
}
}
let boxnode = SCNNode(geometry: geoCopy)
let boxCopy = boxnode.copy() as! SCNNode
boxCopy.position.x = Float(xIndex - offset)
boxCopy.position.y = Float(yIndex - offset)
self.rootNode.addChildNode(boxCopy)
}
}
}
But I only see one box.
Thanks!
Picture of my Images:
You need to create one geometry, one box node and then copy that boxNode. You use clone when you have node with children and flattenedClone when you want to merge geometries/materials of the entire subtree at the node. In your case, copy should suffice. Just change the position of your copied node.
GameScene
import Foundation
import SceneKit
class GameScene: SCNScene {
override init() {
super.init()
let geometry = SCNBox(width: 0.6 , height: 0.6,
length: 0.1, chamferRadius: 0.005)
geometry.firstMaterial?.diffuse.contents = UIColor.red
geometry.firstMaterial?.specular.contents = UIColor.white
geometry.firstMaterial?.emission.contents = UIColor.blue
let boxnode = SCNNode(geometry: geometry)
let offset: Int = 16
for xIndex:Int in 0...32 {
for yIndex:Int in 0...32 {
let boxCopy = boxnode.copy() as! SCNNode
boxCopy.position.x = Float(xIndex - offset)
boxCopy.position.y = Float(yIndex - offset)
self.rootNode.addChildNode(boxCopy)
}
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
In your view controller, viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = GameScene()
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
scnView.pointOfView?.position = SCNVector3Make(0, 0, 100)
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.white
}
Note that I have just pushed the camera point of view back on the +Z axis to have a better view of your grid.
The grid screenshot
Edit: New material for each geometry
If you want to assign a new material to each geometry, you need to create a copy of the geometry and assign a new material to that geometry copy. See the code below which randomly assign a UIImage for each diffuse property, from a set of seven images named 1.png to 8.png.
import Foundation
import SceneKit
class GameScene: SCNScene {
override init() {
super.init()
let geometry = SCNBox(width: 6 , height: 6,
length: 6, chamferRadius: 0.5)
for xIndex:Int in stride(from: 0, to: 32, by:8) {
for yIndex:Int in stride(from: 0, to: 32, by: 8) {
// create a geometry copy
let geoCopy = geometry.copy() as! SCNGeometry
// create a random material
let r = arc4random_uniform(7) + 1
let img = UIImage(named: "\(r).png")
let mat = SCNMaterial()
mat.diffuse.contents = img
geoCopy.firstMaterial = mat
// create a copy node with new material and geo copy
let boxnode = SCNNode(geometry: geoCopy)
let boxCopy = boxnode.copy() as! SCNNode
boxCopy.position.x = Float(xIndex - offset)
boxCopy.position.y = Float(yIndex - offset)
self.rootNode.addChildNode(boxCopy)
}
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Screenshot
You need to put the statement -let boxnode = SCNNode(geometry: self.geometry) - inside the loop. If you wish to use the same materials, you can use the same geometry for all nodes (just store the geometry in a variable and assign it). Otherwise, if you wish to have different materials, copy the geometry and assign different materials each time.