Arutyun Enfendzhyan
Arutyun Enfendzhyan

Reputation: 1909

How can I make SceneView's background transparent?

I want to open a 3D model and make its background transparent, so that I can see the UI behind the SceneView. I've tried this code, but sceneView becomes white, not transparent.


struct ModelView: View {
    var body: some View {
        ZStack {
            Text("Behind Text Behind Text Behind Text")
            SceneView(
                scene: { () -> SCNScene in
                    let scene = SCNScene()
                    scene.background.contents = UIColor.clear
                    return scene
                }(),
                pointOfView: { () -> SCNNode in
                    let cameraNode = SCNNode()
                    cameraNode.camera = SCNCamera()
                    cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
                    return cameraNode
                }(),
                options: [
                    .allowsCameraControl,
                    .temporalAntialiasingEnabled,
                ]
            )
        }
    }
}

I use XCode 12.5 and IPhone 8.

EDIT 1:

Thanks to the comments below, I decided to try new approaches but they still don't work.

Approach #1

First, I tried to create a MySceneView using SCNView through UIViewRepresentable:

struct MySceneView: UIViewRepresentable {
    typealias UIViewType = SCNView
    typealias Context = UIViewRepresentableContext<MySceneView>

    func updateUIView(_ uiView: UIViewType, context: Context) {}
    func makeUIView(context: Context) -> UIViewType {
        let view = SCNView()
        view.allowsCameraControl = true
        view.isTemporalAntialiasingEnabled = true
        view.autoenablesDefaultLighting = true
        view.scene = MySceneView.scene
        return view
    }
    
    static let scene: SCNScene = {
        let scene = SCNScene(named: "art.scnassets/man.obj")!
        scene.background.contents = UIColor.clear
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
        scene.rootNode.addChildNode(cameraNode)
        return scene
    }()
}

Approach #2 I tried using SpriteView, here is the code:

        ZStack {
            Text("Behind Text Behind Text Behind Text")
            SpriteView(scene: { () -> SKScene in
                let scene = SKScene()
                scene.backgroundColor = UIColor.clear
                let model = SK3DNode(viewportSize: .init(width: 200, height: 200))
                model.scnScene = MySceneView.scene
                scene.addChild(model)
                return scene
            }(), options: [.allowsTransparency])
}

Upvotes: 9

Views: 3391

Answers (5)

Alex Brown
Alex Brown

Reputation: 42932

Inspired by the examples above, I ported it to SwiftUI for Mac

  1. In Xcode, create a Game App for Mac
  2. Port the @main App to SwiftUI (below)
  3. Create a UIViewRepresentable
  4. Remove the 'background' from the ship.scn file
struct MySceneView: NSViewRepresentable {
    typealias NSViewType = SCNView

    let scene: SCNScene

    func updateNSView(_ uiView: SCNView, context: Context) {
        uiView.scene = scene
    }

    func makeNSView(context: Context) -> SCNView {
        let view = SCNView()
        
        view.backgroundColor = NSColor.clear
        view.allowsCameraControl = true
        view.autoenablesDefaultLighting = true
        return view
    }
  }

SwiftUI App

@main
struct MyApp: App {

    let scene = makeScene() // extracted from the viewController

    var body: some Scene {
        WindowGroup {
            ZStack {
                Image(systemName: "lizard.fill")
                    .symbolRenderingMode(.multicolor)
                    .resizable(resizingMode: .tile)

                MySceneView(scene: scene)
                    .background(.clear)
            }
        }
    }
}

makeScene (copied from GameViewController.viewDidLoad:

func makeScene() -> SCNScene {
    // create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.scn")!
    
… all the rest
     
    // animate the 3d object
    ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
    return scene
}

Then remove the background from the spaceship scene ship.scn:

Scene inspector

Result: transparent background!

Spaceship

Upvotes: 1

Jeff
Jeff

Reputation: 850

I didn't like using SpriteKit to make a SceneKit scenes background contents transparent because you completely loose access to the SCNView. Here is what I believe to be the correct approach.

  1. Create a Scene Kit Scene file named GameScene.scn
  2. Drop your 3D object into the GameScene
  3. Use the code below
/// The SCNView view
struct GameSceneView: UIViewRepresentable {
    @ObservedObject var viewModel: GameSceneViewModel
    
    func makeUIView(context: UIViewRepresentableContext<GameSceneView>) -> SCNView {
        let view = SCNView()
        view.backgroundColor = viewModel.backgroundColor
        view.allowsCameraControl = viewModel.allowsCameraControls
        view.autoenablesDefaultLighting = viewModel.autoenablesDefaultLighting
        view.scene = viewModel.scene
        return view
    }
    
    func updateUIView(_ uiView: SCNView, context: UIViewRepresentableContext<GameSceneView>) {}
}

/// The view model supplying the SCNScene and its properties
class GameSceneViewModel: ObservableObject {
    @Published var scene: SCNScene?
    @Published var backgroundColor: UIColor
    @Published var allowsCameraControls: Bool
    @Published var autoenablesDefaultLighting: Bool
    
    init(
        sceneName: String = "GameScene.scn",
        cameraName: String = "camera",
        backgroundColor: UIColor = .clear,
        allowsCameraControls: Bool = true,
        autoenablesDefaultLighting: Bool = true
    ) {
        self.scene = SCNScene(named: sceneName)
        self.backgroundColor = backgroundColor
        self.allowsCameraControls = allowsCameraControls
        self.autoenablesDefaultLighting = autoenablesDefaultLighting
        
        scene?.background.contents = backgroundColor
        scene?.rootNode.childNode(withName: cameraName, recursively: false)
    }
}

/// Usage
struct ContentView: View {
    var body: some View {
        VStack {
            GameSceneView(viewModel: GameSceneViewModel())
        }
        .background(Color.blue)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here

Upvotes: 1

kukabi
kukabi

Reputation: 1313

I wasn't able to find a fully working snippet here, but thanks to the answers from Arutyun I managed to compile a working solution without the need for SpriteKit.

import SceneKit
import SwiftUI

struct MySceneView: UIViewRepresentable {
    typealias UIViewType = SCNView
    typealias Context = UIViewRepresentableContext<MySceneView>

    func updateUIView(_ uiView: UIViewType, context: Context) {}
    func makeUIView(context: Context) -> UIViewType {
        let view = SCNView()
        view.backgroundColor = UIColor.clear // this is key!
        view.allowsCameraControl = true
        view.autoenablesDefaultLighting = true
        // load the object here, could load a .scn file too
        view.scene = SCNScene(named: "model.obj")!
        return view
    }
}

And use it just like a regular view:

import SwiftUI

struct MySceneView: View {
    var body: some View { 
        ZStack{
            // => background views here
            MySceneView()
                .frame( // set frame as required
                    maxWidth: .infinity,
                    maxHeight: .infinity,
                    alignment: .center
                )
        }
    }
}

struct MySceneView_Previews: PreviewProvider {
    static var previews: some View {
        MySceneView()
    }
}

Upvotes: 5

LukeSideWalker
LukeSideWalker

Reputation: 7940

With SwiftUI:

It is slightly different in SwiftUI using a SpriteView.

To implement a transparent SpriteView in SwiftUI, you have to use the 'options' parameter:

  1. Configure your SKScene with clear background (view AND scene)
  2. Configure your SpriteView with the correct option
// 1. configure your scene in 'didMove'
override func didMove(to view: SKView) {
    self.backgroundColor = .clear
    view.backgroundColor = .clear
}

and most importantly:

// 2. configure your SpriteView with 'allowsTranspanency'
SpriteView(scene: YourSKScene(), options: [.allowsTransparency])

Upvotes: 0

Arutyun Enfendzhyan
Arutyun Enfendzhyan

Reputation: 1909

Update:

A much simpler solution is to use UIViewRepresentable, create SCNView and set backgroundColor to clear

Old:

Thanks George_E, your idea with SpriteKit worked perfectly. Here is the code:

SpriteView(scene: { () -> SKScene in
    let scene = SKScene()
    scene.backgroundColor = UIColor.clear
    let model = SK3DNode(viewportSize: .init(width: 200, height: 200))
    model.scnScene = {
        let scene = SCNScene(named: "art.scnassets/man.obj")!
        scene.background.contents = UIColor.clear
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
        scene.rootNode.addChildNode(cameraNode)
        return scene
    }()
    scene.addChild(model)
    return scene
}(), options: [.allowsTransparency])

Upvotes: 2

Related Questions