How to add 3D shapes in Swift UI? - swift

I want to know how to add 3D shapes (eg. Sphere) in swift UI
I have tried adding a scnscene and using that in swift UI but I get a error message
//SceneKit
class myscene: SCNScene{
override init(){
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder: ) has not been implemented")
}
}
//Swift UI
struct ContentView: View {
var body: some View {
let sphere = SCNSphere(radius: 2.0)
sphere.firstMaterial?.diffuse.contents = UIColor.blue
let spherenode = SCNNode(geometry: sphere)
spherenode.position = SCNVector3(x: 0.0, y: 3.0, z: 0.0)
}
}
The error message is on var body: some View { line and reads as follows :
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
Please help me with this problem......

Here is simplest code to demo how to setup SceneKit scene with your sphere. Hope it helps.
import SwiftUI
import SceneKit
struct SceneKitView: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.black
let sphere = SCNSphere(radius: 2.0)
sphere.firstMaterial?.diffuse.contents = UIColor.blue
let spherenode = SCNNode(geometry: sphere)
spherenode.position = SCNVector3(x: 0.0, y: 3.0, z: 0.0)
sceneView.scene?.rootNode.addChildNode(spherenode)
return sceneView
}
func updateUIView(_ uiView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
}
typealias UIViewType = SCNView
}
struct DemoSceneKit: View {
var body: some View {
SceneKitView()
}
}
struct DemoSceneKit_Previews: PreviewProvider {
static var previews: some View {
DemoSceneKit()
}
}

Related

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

Converting worldPosition into a SceneView position with SceneKit/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)")
// }
}
}

Adding multiple SCNNode(s) at the same time

I have this function named addShapes. I want it to create 3 shapes
import ARKit
import SwiftUI
struct SceneKitView: UIViewRepresentable {
let arView = ARSCNView(frame: .zero)
let config = ARWorldTrackingConfiguration()
#Binding var addBox: Int
#Binding var reset: Bool
#Binding var node: SCNNode
fileprivate func addShapes() {
let letBall1Geo = SCNSphere(radius: 0.05)
letBall1Geo.firstMaterial?.diffuse.contents = UIColor.red
letBall1Geo.firstMaterial?.specular.contents = UIColor.white
let ball1 = SCNNode(geometry: letBall1Geo)
ball1.position = SCNVector3(0, 0, 0)
self.arView.scene.rootNode.addChildNode(ball1)
let letBall2Geo = SCNSphere(radius: 0.05)
letBall2Geo.firstMaterial?.diffuse.contents = UIColor.white
letBall2Geo.firstMaterial?.specular.contents = UIColor.white
let ball2 = SCNNode(geometry: letBall2Geo)
ball1.position = SCNVector3(1, 1, 1)
self.arView.scene.rootNode.addChildNode(ball2)
let stickGeo = SCNCapsule(capRadius: 0.05, height: 0.1)
stickGeo.firstMaterial?.diffuse.contents = UIColor.blue
stickGeo.firstMaterial?.specular.contents = UIColor.white
let stick = SCNNode(geometry: stickGeo)
stick.position = SCNVector3(2, 2, 2)
self.arView.scene.rootNode.addChildNode(stick)
}
fileprivate func removeCube() {
/////
}
func makeUIView(context: Context) -> ARSCNView {
arView.scene = SCNScene()
arView.autoenablesDefaultLighting = true
arView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
arView.session.run(self.config)
return arView
}
func updateUIView(_ uiView: ARSCNView,
context: Context) {
if addBox > 0 {
self.addShapes()
}
if reset {
self.removeCube()
}
}
}
The problem is that it's only adding the first shape (ball1) and not all of them. Do you know why?
Thanks in advance!
Your code works fine (a position was a problem):
import SwiftUI
import ARKit
struct ContentView : View {
#State var addBox: Int = 3
#State var reset: Bool = false
#State var node: SCNNode = SCNNode()
var body: some View {
return SceneKitView(addBox: $addBox,
reset: $reset,
node: $node).edgesIgnoringSafeArea(.all)
}
}
...
struct SceneKitView: UIViewRepresentable {
let arView = ARSCNView(frame: .zero)
let config = ARWorldTrackingConfiguration()
#Binding var addBox: Int
#Binding var reset: Bool
#Binding var node: SCNNode
fileprivate func addShapes() {
let letBall1Geo = SCNSphere(radius: 0.05)
letBall1Geo.firstMaterial?.diffuse.contents = UIColor.red
letBall1Geo.firstMaterial?.specular.contents = UIColor.white
let ball1 = SCNNode(geometry: letBall1Geo)
ball1.position = SCNVector3(0, 0, -1)
self.arView.scene.rootNode.addChildNode(ball1)
let letBall2Geo = SCNSphere(radius: 0.05)
letBall2Geo.firstMaterial?.diffuse.contents = UIColor.green
letBall2Geo.firstMaterial?.specular.contents = UIColor.white
let ball2 = SCNNode(geometry: letBall2Geo)
ball1.position = SCNVector3(0, 0.2, -1)
self.arView.scene.rootNode.addChildNode(ball2)
let stickGeo = SCNCapsule(capRadius: 0.05, height: 0.1)
stickGeo.firstMaterial?.diffuse.contents = UIColor.blue
stickGeo.firstMaterial?.specular.contents = UIColor.white
let stick = SCNNode(geometry: stickGeo)
stick.position = SCNVector3(0, 0.4, -1)
self.arView.scene.rootNode.addChildNode(stick)
}
func makeUIView(context: Context) -> ARSCNView {
arView.scene = SCNScene()
arView.autoenablesDefaultLighting = true
arView.debugOptions = [.showFeaturePoints, .showWorldOrigin]
arView.session.run(self.config)
return arView
}
func updateUIView(_ uiView: ARSCNView, context: Context) {
if addBox > 0 {
self.addShapes()
}
}
}

How to zoom in to a node of a scene that's being displayed in swift ui

I have a Scenekit scene in swift UI, I made the scene a UIViewRepresentable, how can I zoom in on one of the nodes of the scene when the user touches that specific node in the Scenekit scene?
import SceneKit
struct HouseView : UIViewRepresentable {
func makeUIView(context: Context) -> SCNView {
return SCNView(frame: .zero)
}
func updateUIView(_ scnView: SCNView, context: Context) {
let HouseScene = SCNScene(named: "House.scn")
scnView.scene = HouseScene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = false
scnView.backgroundColor = UIColor.systemBackground
scnView.defaultCameraController.maximumVerticalAngle = 10
scnView.defaultCameraController.minimumVerticalAngle = -10
scnView.defaultCameraController.maximumHorizontalAngle = 180
scnView.defaultCameraController.minimumHorizontalAngle = -10
scnView.isJitteringEnabled = true
let CameraNode = HouseScene?.rootNode.childNode(withName: "CameraNode", recursively: true)
CameraNode?.position = SCNVector3(x: 12, y:2, z: 0)
}
}
struct HouseView_Previews: PreviewProvider {
static var previews: some View {
HouseView()
}
}`''
Please see this post: 54058938
Create a basic camera class and you can focus on the node touched and set the distance away from it.

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.