Converting worldPosition into a SceneView position with SceneKit/Swift - swift

iOS 15, Swift 5
I am trying to get sceneView [SwiftUI SceneKit plugin] to work with adding/finding nodes within my scene. And I feel I am almost there and yet I am not.
Sadly the hits function here finds nothing, which is why I ended up commenting it out. I tried used projectPoint after that, but the results make no sense?
Now I did get this working using UIViewRespresentable, but that solution doesn't use sceneView at all.
I want to convert the drag coordinates to the SCNView coordinates since since I think I can find/match objects.
import SwiftUI
import SceneKit
struct ContentView: View {
let settings = SharedView.shared
var (scene,cameraNode) = GameScene.shared.makeView()
var body: some View {
GameView(scene: scene, cameraNode: cameraNode)
}
}
struct GameView: View {
let settings = SharedView.shared
#State var scene: SCNScene
#State var cameraNode: SCNNode
var body: some View {
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.autoenablesDefaultLighting, .rendersContinuously ], delegate: SceneDelegate())
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ gesture in
settings.location = gesture.location
}).sequenced(before: TapGesture().onEnded({ _ in
GameScene.shared.tapV()
}))
)
}
}
class SharedView: ObservableObject {
#Published var location:CGPoint!
#Published var view:UIView!
#Published var scene:SCNScene!
#Published var sphereNode:SCNNode!
static var shared = SharedView()
}
#MainActor class SceneDelegate: NSObject, SCNSceneRendererDelegate {
}
class GameScene: UIView {
static var shared = GameScene()
let settings = SharedView.shared
var view: SCNView!
var scene: SCNScene!
func makeView() -> (SCNScene,SCNNode) {
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
let sphere = SCNSphere(radius: 1.0)
sphere.materials = [material]
let sphereNode = SCNNode()
sphereNode.geometry = sphere
sphere.name = "sphereNode"
let camera = SCNCamera()
camera.fieldOfView = 90.0
let light = SCNLight()
light.color = UIColor.white
light.type = .omni
let cameraNode = SCNNode()
cameraNode.simdPosition = SIMD3<Float>(0.0, 0.0, 6)
cameraNode.camera = camera
cameraNode.light = light
scene = SCNScene()
scene.background.contents = UIColor.black
scene.rootNode.addChildNode(sphereNode)
scene.rootNode.addChildNode(cameraNode)
view = SCNView()
view.scene = scene
view.pointOfView = cameraNode
settings.sphereNode = sphereNode
return (scene, cameraNode)
}
func tapV() {
let location = settings.location
let hits = view.hitTest(location!, options: [.boundingBoxOnly: true, .firstFoundOnly: true, .searchMode: true])
print("hits \(hits.count)")
// for hit in hits {
let material = SCNMaterial()
material.diffuse.contents = UIColor.yellow
let geometry = SCNSphere(radius: 0.1)
geometry.materials = [material]
let node = SCNNode()
node.geometry = geometry
// node.simdPosition = hit.simdWorldCoordinates
let projectedOrigin = view.projectPoint(SCNVector3Zero)
var worldPoint = view.unprojectPoint(SCNVector3(location!.x, location!.y, CGFloat(0)))
node.worldPosition = worldPoint
view.scene!.rootNode.addChildNode(node)
print("node \(node.position) \(hits.count)")
// }
}
}

Related

SCNLookAtConstraint && SCNDistanceConstraint

Running on iOS16
Trying to get a better handle on SCNLookATConstraints, but struggling to get them to work in this demo app.
When I tap on the scene the box moves, but the camera doesn't follow it.
import SwiftUI
import SceneKit
import Combine
let screenSize: CGRect = UIScreen.main.bounds
let screenWidth: CGFloat = UIScreen.main.bounds.width
let screenHeight: CGFloat = UIScreen.main.bounds.height
let push = PassthroughSubject<Float,Never>()
var pusher: AnyCancellable!
struct CoreView: View {
#State var scene = SCNScene()
#State var place:Float = 0.0
var body: some View {
CustomSceneView(scene: scene, options: [])
.frame(width: screenWidth, height: screenHeight)
.onTapGesture {
place += 0.1
push.send(place)
}
}
}
struct ContentView: View {
var body: some View {
CoreView()
}
}
struct CustomSceneView: UIViewRepresentable {
var scene: SCNScene
var options: [Any]
var delegate: SCNRenderer!
var view = SCNView()
func makeUIView(context: Context) -> SCNView {
view.scene = scene
view.allowsCameraControl = true
view.autoenablesDefaultLighting = false
let cubeGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
cubeGeometry.firstMaterial?.diffuse.contents = UIColor.red
let cubeNode = SCNNode(geometry: cubeGeometry)
cubeNode.name = "cubeNode"
cubeNode.simdPosition = SIMD3(0,0,0)
let camera = SCNCamera()
camera.fieldOfView = 60.0
camera.focalLength = 100.0
camera.sensorHeight = 0.0
camera.wantsDepthOfField = true
camera.focusDistance = 0.8
camera.fStop = 5.6
camera.wantsHDR = false
camera.apertureBladeCount = 4
camera.motionBlurIntensity = 1.0
camera.name = "camera"
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.name = "cameraNode"
let lookAtConstraint = SCNLookAtConstraint(target: cubeNode)
lookAtConstraint.influenceFactor = 1.0
lookAtConstraint.isGimbalLockEnabled = true
view.delegate = context.coordinator
scene.rootNode.addChildNode(cameraNode)
view.pointOfView = scene.rootNode.childNode(withName: "cameraNode", recursively: true)
view.pointOfView?.simdPosition = SIMD3<Float>(0, 0.0, 2)
scene.rootNode.addChildNode(cubeNode)
view.defaultCameraController.interactionMode = .fly
view.defaultCameraController.inertiaEnabled = true
//view.defaultCameraController.maximumVerticalAngle = 45
view.allowsCameraControl = true
pusher = push.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.throttle(for: 0.01, scheduler: RunLoop.main, latest: true)
.sink(receiveValue: { [self] distance in
cubeNode.simdPosition = SIMD3(distance,0,0)
})
return view
}
func updateUIView(_ view: SCNView, context: Context) {
print("update")
}
func makeCoordinator() -> Coordinator {
Coordinator(view)
}
}
class Coordinator: NSObject, SCNSceneRendererDelegate {
private let view: SCNView
private var cubeNode: SCNNode!
private var cameraNode: SCNNode!
init(_ view: SCNView) {
self.view = view
super.init()
}
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
cameraNode = renderer.scene?.rootNode.childNode(withName: "cameraNode", recursively: true)
cubeNode = renderer.scene?.rootNode.childNode(withName: "cubeNode", recursively: true)
print("cubeNode \(cubeNode)")
print("cameraNode \(cameraNode)")
if cubeNode != nil && cameraNode != nil {
let distanceConstraint = SCNDistanceConstraint(target: cubeNode)
distanceConstraint.minimumDistance = 1
distanceConstraint.maximumDistance = 2
let lookAtConstraint = SCNLookAtConstraint(target: cubeNode)
lookAtConstraint.influenceFactor = 1.0
lookAtConstraint.isGimbalLockEnabled = true
cameraNode.constraints = [lookAtConstraint, distanceConstraint]
}
}
}
Tried adding the camera constraints to the built, the coordinator and shown here the renderer. The code is seeing it, but ignoring it.
Tried moving the scene with my mouse and clicking on it to move it, neither work.

How to make SceneView's background clear in SwiftUI?

I am showing a 3D object in SwiftUI, and I have a problem with making the object's background clear. I have searched and did not find any solution. Is there any solution for this?
private var scene: SCNScene? {
SCNScene(named: "Globe.scnassets/sphere.scn")
}
private var cameraNode: SCNNode? {
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
return cameraNode
}
var body: some View {
SceneView(scene: scene,
pointOfView: cameraNode,
options: [.allowsCameraControl, .autoenablesDefaultLighting])
}
There are, at least, 3 ways to programmatically change a BG in SwiftUI's SceneView.
Default white background
Changing SCNScene's background color
import SwiftUI
import SceneKit
struct ContentView: View {
var scene = SCNScene(named: "model.usdz")
var options: SceneView.Options = [.autoenablesDefaultLighting,
.allowsCameraControl ]
var body: some View {
ZStack {
SceneView(scene: scene, options: options)
.ignoresSafeArea()
let _ = scene?.background.contents = UIColor.black
}
}
}
Using textured double-sided SCNGeometry
struct ContentView: View {
var scene = SCNScene(named: "model.usdz")
var options: SceneView.Options = [.autoenablesDefaultLighting,
.allowsCameraControl ]
let node = SCNNode(geometry: SCNSphere(radius: 500.0))
let img = UIImage(named: "image.jpg")
var body: some View {
ZStack {
let _ = node.geometry?.firstMaterial?.diffuse.contents = img
let _ = node.geometry?.firstMaterial?.isDoubleSided = true
let _ = scene?.rootNode.addChildNode(node)
SceneView(scene: scene, options: options)
.ignoresSafeArea()
}
}
}
Using Procedural Sky Box Texture
You can use MDLSkyCubeTexture as background and lightingEnvironment.
I have found a solution via SceneKit editor, if you want to show an image as background, you can manually do it by going to:
Scene Inspector -> Background and lighting -> Background
and set the image as background:

Same color rendered differently in SpriteView vs. SceneView

For performance reasons I have to switch from SceneView to SpriteView in my macOS project (showing more than 63 scenes did not work with SceneView, but it does with SpriteView).
But now im facing an issue that SpriteView is rendering colors differently than SceneView. Below is a simple reproduction of the issue I am facing.
I have tried a multitude of material and lighting options, but I seem to miss something more fundamental. Help is very much appreciated.
var body: some View {
HStack {
// SpriteView
SpriteView(scene: { () -> SKScene in
let scene = SKScene()
scene.backgroundColor = .white
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let node = SK3DNode()
node.scnScene = self.sphereScene
scene.addChild(node)
return scene
}())
// SceneView
SceneView(scene: sphereScene,
options: [.autoenablesDefaultLighting])
}
}
var sphereScene: SCNScene {
let scnScene = SCNScene()
let ballGeometry = SCNSphere(radius: 5)
let ballNode = SCNNode(geometry: ballGeometry)
let material = SCNMaterial()
material.diffuse.contents = NSColor.purple
material.lightingModel = .physicallyBased
ballGeometry.materials = [material]
scnScene.rootNode.addChildNode(ballNode)
return scnScene
}
You're absolutely right: SpriteKit processes SceneKit's scenes differently than SceneKit. It's visually noticeable that the lighting intensity, blurring of highlights and decolorization of edges with 90 degree reflection are different. The main tool that can be advised in this case is the use of Ambient Light to additionally illuminate the SpriteKit scene based on the SceneKit content. You should turn a default lighting off (in order to get rid of colorization artifacts) and use regular lights. Here I used directional light.
SpriteView:
import SwiftUI
import SceneKit
import SpriteKit
struct SpriteView: NSViewRepresentable {
var scene = SKScene()
func makeNSView(context: Context) -> SKView {
let skView = SKView(frame: .zero)
skView.presentScene(scene)
scene.backgroundColor = .black
return skView
}
func updateNSView(_ uiView: SKView, context: Context) { }
}
ContentView:
struct ContentView: View {
var body: some View {
ZStack {
HStack {
SpriteView(scene: { () -> SKScene in
let scene = SKScene()
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let ambient = SCNNode()
ambient.light = SCNLight()
ambient.light?.type = .ambient
ambient.light?.intensity = 1200
let node = SK3DNode()
node.autoenablesDefaultLighting = false
node.scnScene = self.sphereScene
node.scnScene?.rootNode.addChildNode(ambient)
scene.addChild(node)
return scene
}() )
SceneView(scene: sphereScene, options: [])
}
}
}
var sphereScene: SCNScene {
let scnScene = SCNScene()
scnScene.background.contents = NSColor.black
let ballNode = SCNNode(geometry: SCNSphere(radius: 5.0))
let directional = SCNNode()
directional.light = SCNLight()
directional.light?.type = .directional
directional.light?.intensity = 500
scnScene.rootNode.addChildNode(directional)
let material = SCNMaterial()
material.lightingModel = .physicallyBased
material.diffuse.contents = NSColor.purple
ballNode.geometry?.materials = [material]
scnScene.rootNode.addChildNode(ballNode)
return scnScene
}
}
The following worked for me, correcting saturation and brightness brought me near to the SceneKit defaultLighting appearance:
// get object and manipulate
let object = scene.rootNode.childNode(withName: "object", recursively: false)
let color = NSColor(named: "\(colorNr)")?
.usingColorSpace(.displayP3) // specify color space, important!
object?.geometry?.firstMaterial?.lightingModel = .physicallyBased
// correct color for SpriteView
let color2 = NSColor(hue: color?.hueComponent ?? 0,
saturation: (color?.saturationComponent ?? 0) * 0.55,
brightness: (color?.brightnessComponent ?? 0) * 0.55 + 0.45,
alpha: 1.0)
object?.geometry?.firstMaterial?.diffuse.contents = color2
object?.geometry?.firstMaterial?.diffuse.intensity = 0.9
object?.geometry?.firstMaterial?.roughness.contents = 0.9

SceneKit – SwiftUI implementation doesn't seem to update anything

Swift 5.x iOS 15
Trying to understand SceneKit, but I am stuck. I created a scene and display it with SwiftUI. But when I try and run a function with that class the scene is defined it does nothing. When I tap on the label "push" I want the ball to jump in the air. Nothing happens.
import SwiftUI
import SceneKit
struct ContentView: View {
var body: some View {
let scene = GameScene()
Text("push")
.onTapGesture {
let foo = SharedView.shared
print("push ",foo.xCord,foo.yCord,foo.zCord)
scene.printMe()
}
}
}
class SharedView: ObservableObject {
#Published var xCord:Float = 0
#Published var yCord:Float = 0
#Published var zCord:Float = 0
#Published var xCord2:Float = 0
#Published var yCord2:Float = 1.8
#Published var zCord2:Float = 0
static var shared = SharedView()
}
...
class SceneDelegate: NSObject, SCNSceneRendererDelegate {
let foo = SharedView.shared
func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
print("running!!")
}
static var shared = SceneDelegate()
}
struct GameScene: View {
// #ObservedObject var foo = SharedView.shared
#StateObject var foo = SharedView.shared
#State var scene = SCNScene(named: "MyScene.scn")
#State var cameraNode = SCNNode()
#State var ball = SCNNode()
#State var box = SCNNode()
var body: some View {
scene?.isPaused = false
self.scene?.rootNode.enumerateChildNodes({ (node, _ ) in
if node.name == "ball" {
Task {
ball = node
ball.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(node: ball, options: [:]))
ball.physicsBody?.isAffectedByGravity = true
ball.physicsBody?.restitution = 1.2
}
}
if node.name == "box" {
Task {
box = node
box.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(node: box, options: [:]))
box.physicsBody?.isAffectedByGravity = false
box.physicsBody?.restitution = 1
}
}
})
cameraNode.camera = SCNCamera()
cameraNode.position.x = foo.xCord
cameraNode.position.y = foo.yCord
cameraNode.worldPosition.z = foo.zCord
cameraNode.eulerAngles = SCNVector3(foo.xCord2,foo.yCord2,foo.zCord2)
cameraNode.camera = SCNCamera()
let sceneView = SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.autoenablesDefaultLighting,.allowsCameraControl], delegate: SceneDelegate.shared)
let dummyNode = scene!.rootNode.childNode(withName: "DummyNode", recursively: false)
dummyNode?.position = SCNVector3(0, 0, 0)
return sceneView
.frame(width: 1024, height: 256, alignment: .center)
}
func printMe() {
print(cameraNode.eulerAngles, cameraNode.position)
ball.physicsBody?.applyForce(SCNVector3(0,10,0), asImpulse: true)
}
}

Access Geometry Child Node Scenekit

I am trying to access the childnode: boxNode, and then rotate the node using the input from a pan gesture recognizer. How should I access this node so I can manipulate it? Should I declare the cube node in a global scope and modify that way? I am new to Swift so I apologize if this code looks sloppy. I would like to add rotation actions inside of my panGesture() function. Thanks!
import UIKit
import SceneKit
class GameViewController: UIViewController {
var scnView: SCNView!
var scnScene: SCNScene!
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupScene()
}
// override var shouldAutorotate: Bool {
// return true
// }
//
// override var prefersStatusBarHidden: Bool {
// return true
// }
func setupView() {
scnView = self.view as! SCNView
}
func setupScene() {
scnScene = SCNScene()
scnView.scene = scnScene
scnView.backgroundColor = UIColor.blueColor()
initCube()
initLight()
initCamera()
}
func initCube () {
var cubeGeometry:SCNGeometry
cubeGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
let boxNode = SCNNode(geometry: cubeGeometry)
scnScene.rootNode.addChildNode(boxNode)
}
func initLight () {
let light = SCNLight()
light.type = SCNLightTypeAmbient
let lightNode = SCNNode()
lightNode.light = light
light.castsShadow = false
lightNode.position = SCNVector3(x: 1.5, y: 1.5, z: 1.5)
scnScene.rootNode.addChildNode(lightNode)
}
func initCamera () {
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0.0, y: 0.0, z: 5.0)
scnScene.rootNode.addChildNode(cameraNode)
}
func initRecognizer () {
let panTestRecognizer = UIPanGestureRecognizer(target: self, action: #selector(GameViewController.panGesture(_:)))
scnView.addGestureRecognizer(panTestRecognizer)
}
//TAKE GESTURE INPUT AND ROTATE CUBE ACCORDINGLY
func panGesture(sender: UIPanGestureRecognizer) {
//let translation = sender.translationInView(sender.view!)
}
}
First of all you should update your Xcode. It looks like this is an older Swift version you are using. SCNLightTypeAmbient is .ambient in Swift 3.
So the way you should go is by giving your nodes names to identify them:
myChildNode.name = "FooChildBar"
and then you can call on your parent node
let theNodeYouAreLookingFor = parentNode.childNode(withName: "FooChildBar", recursively: true)
You can also use this on your scene's root node
let theNodeYouAreLookingFor = scene.rootNode.childNode(withName: "FooChildBar", recursively: true)
which will look at all nodes in your scene and return the one that you gave the name.