【问题标题】:SceneKit shadow on a transparent SCNFloor()透明 SCNFloor() 上的 SceneKit 阴影
【发布时间】:2019-03-10 07:29:50
【问题描述】:

我有一个floor node,我需要从directional light 投射阴影。这个节点需要是透明的(在AR环境中使用)。 这在我使用ARKit 时工作正常,但使用SceneKit 的相同设置没有显示阴影或反射。我怎样才能像这样在SceneKit 中投下阴影? SceneKit 的问题是由我设置sceneView.backgroundColor = .clear 引起的——但我需要在这个应用程序中使用这种行为。这种情况可以避免吗?

演示此问题的示例代码(仅适用于设备,不适用于模拟器):

@IBOutlet weak var sceneView: SCNView! {
    didSet {

        sceneView.scene = SCNScene()

        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        sceneView.pointOfView = cameraNode

        let testNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
        testNode.position = SCNVector3(x: 0, y: 0, z: -5)
        sceneView.scene!.rootNode.addChildNode(testNode)

        let animation = SCNAction.rotateBy(x: 0, y: .pi, z: 0, duration: 3.0)
        testNode.runAction(SCNAction.repeatForever(animation), completionHandler: nil)

        let floor = SCNFloor()
        floor.firstMaterial!.colorBufferWriteMask = []
        floor.firstMaterial!.readsFromDepthBuffer = true
        floor.firstMaterial!.writesToDepthBuffer = true
        floor.firstMaterial!.lightingModel = .constant
        let floorNode = SCNNode(geometry: floor)
        floorNode.position = SCNVector3(x: 0, y: -2, z: 0)
        sceneView.scene!.rootNode.addChildNode(floorNode)

        let light = SCNLight()
        light.type = .directional
        light.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5)
        light.color = UIColor.white
        light.castsShadow = true
        light.automaticallyAdjustsShadowProjection = true
        light.shadowMode = .deferred
        let sunLightNode = SCNNode()
        sunLightNode.position = SCNVector3(x: 1_000, y: 1_000, z: 0)
        sunLightNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi * 1.5)
        sunLightNode.light = light
        sceneView.scene!.rootNode.addChildNode(sunLightNode)

        let omniLightNode: SCNNode = {
            let omniLightNode = SCNNode()
            let light: SCNLight = {
                let light = SCNLight()
                light.type = .omni
                return light
            }()
            omniLightNode.light = light
            return omniLightNode
        }()
        sceneView.scene!.rootNode.addChildNode(omniLightNode)
    }
}

override func viewDidLoad() {
    super.viewDidLoad()
    let tapGR = UITapGestureRecognizer(target: self, action: #selector(toggleTransparent))
    view.addGestureRecognizer(tapGR)
}

@objc func toggleTransparent() {
    transparent = !transparent
}

var transparent = false {
    didSet {
        sceneView.backgroundColor = transparent ? .clear : .white
    }
}

这是 macOS 的相同示例,构建在 SceneKit 游戏项目之上:

import SceneKit
import QuartzCore

class GameViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!

        // create and add a camera to the scene
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)

        // place the camera
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)

        let testNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
        testNode.position = SCNVector3(x: 0, y: 0, z: -5)
        scene.rootNode.addChildNode(testNode)

        let animation = SCNAction.rotateBy(x: 0, y: .pi, z: 0, duration: 3.0)
        testNode.runAction(SCNAction.repeatForever(animation), completionHandler: nil)

        let floor = SCNFloor()
        floor.firstMaterial!.colorBufferWriteMask = []
        floor.firstMaterial!.readsFromDepthBuffer = true
        floor.firstMaterial!.writesToDepthBuffer = true
        floor.firstMaterial!.lightingModel = .constant
        let floorNode = SCNNode(geometry: floor)
        floorNode.position = SCNVector3(x: 0, y: -2, z: 0)
        scene.rootNode.addChildNode(floorNode)

        let light = SCNLight()
        light.type = .directional
        light.shadowColor = NSColor(red: 0, green: 0, blue: 0, alpha: 0.5)
        light.color = NSColor.white
        light.castsShadow = true
        light.automaticallyAdjustsShadowProjection = true
        light.shadowMode = .deferred
        let sunLightNode = SCNNode()
        sunLightNode.position = SCNVector3(x: 1_000, y: 1_000, z: 0)
        sunLightNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi * 1.5)
        sunLightNode.light = light
        scene.rootNode.addChildNode(sunLightNode)

        let omniLightNode: SCNNode = {
            let omniLightNode = SCNNode()
            let light: SCNLight = {
                let light = SCNLight()
                light.type = .omni
                return light
            }()
            omniLightNode.light = light
            return omniLightNode
        }()
        scene.rootNode.addChildNode(omniLightNode)

        // retrieve the SCNView
        let scnView = self.view as! SCNView

        // set the scene to the view
        scnView.scene = scene

        // allows the user to manipulate the camera
        scnView.allowsCameraControl = true

        // configure the view
        scnView.backgroundColor = .clear
//        scnView.backgroundColor = .white // shadow works in this mode, but I need it to be clear
    }
}

示例项目:

MacOS:https://www.dropbox.com/s/1o50mbgzg4gc0fg/Test_macOS.zip?dl=1

iOS:https://www.dropbox.com/s/fk71oay1sopc1vp/Test.zip?dl=1

在 macOS 中,您可以在 ViewController 的最后一行更改 backgroundColor - 我需要它是清晰的,所以我可以在它下面显示相机预览。

在下面的图片中,您可以看到当 sceneView.backgroundColor 为白色时的样子,并且在下面 - 清晰。透明版没有阴影。

【问题讨论】:

    标签: swift scenekit augmented-reality arkit shadow


    【解决方案1】:

    有两个步骤可以获得透明阴影

    首先:您需要将其作为node 连接到scene,而不是作为geometry type

    let floor = SCNNode()
    floor.geometry = SCNFloor()
    floor.geometry?.firstMaterial!.colorBufferWriteMask = []
    floor.geometry?.firstMaterial!.readsFromDepthBuffer = true
    floor.geometry?.firstMaterial!.writesToDepthBuffer = true
    floor.geometry?.firstMaterial!.lightingModel = .constant
    scene.rootNode.addChildNode(floor)
    

    不可见的 SCNFloor() 上的阴影:

    可见 SCNPlane() 上的阴影和我们的相机位于 SCNFloor() 下:

    要获得transparent shadow,您需要设置shadow color,而不是object's transparency itself

    第二:对于 macOS,shadow color 必须像这样设置:

    lightNode.light!.shadowColor = NSColor(calibratedRed: 0,
                                                   green: 0, 
                                                    blue: 0, 
                                                   alpha: 0.5)
    

    ...对于 iOS,它看起来像这样:

    lightNode.light!.shadowColor = UIColor(white: 0, alpha: 0.5)
    

    这里的 Alpha 分量 (alpha: 0.5) 是阴影的 opacity,RGB 分量 (white: 0) 是阴影的黑色。

    附言

    sceneView.backgroundColor.clear颜色和.white颜色之间切换

    在这种特殊情况下,当sceneView.backgroundColor = .clear 时,我无法捕捉到强烈的阴影,因为您需要在RGBA=1,1,1,1白色模式:白色,alpha=1)和@987654352 之间切换@(清除模式:黑色,alpha=0)。

    为了在背景上看到半透明阴影,组件应该是RGB=1,1,1A=0.5,但是由于 SceneKit 的内部合成机制,这些值会使图像变白。但是当我设置RGB=1,1,1A=0.02 时,阴影非常微弱。

    这是目前可以接受的解决方法(在下面的解决方案部分中寻找解决方案):

    @objc func toggleTransparent() {
        transparent = !transparent
    }  
    var transparent = false {
        didSet {
            // this shadow is very FEEBLE and it's whitening BG image a little bit
            sceneView.backgroundColor = transparent ? 
                                        UIColor(white: 1, alpha: 0.02) : 
                                        .white
        }
    }
    
    let light = SCNLight()
    light.type = .directional
    
    if transparent == false {
        light.shadowColor = UIColor(white: 0, alpha: 0.9)
    }
    

    如果我设置light.shadowColor = UIColor(white: 0, alpha: 1),我会在BG图像上获得令人满意的阴影,但在白色图像上获得纯黑色阴影

    解决方案

    您应该抓取 3D 对象的渲染,以使用其有用的 Alpha 通道预乘 RGBA 图像。之后,您可以在另一个 View 中使用经典的 OVER 合成操作将 rgba image of cube and its shadow 合成到 image of nature 上。

    这是OVER 操作的公式:

    (RGB1 * A1) + (RGB2 * (1 – A1))

    【讨论】:

    • 评论不用于扩展讨论;这个对话是moved to chat
    • 我想这不是完美的解决方案,所以我正在探索其他一些方法来解决这个问题。我注意到,当 shadowColor 不同于黑色时会显示阴影。颜色越接近黑色,它就越透明。我现在设法只显示白色阴影。也许这会有所帮助。
    • 你在说什么解决方案?合成过度操作(RGB1 * A1) + (RGB2 * (1 – A1))UIColor(white: 1, alpha: 0.02) ??
    • 从我上面看到的,背景层有点褪色了,对吧?
    • 那么,使用UIColor(white: 1, alpha: 0.02) ??
    【解决方案2】:

    自此发布以来已经有一段时间了,但也许有人会发现此替代解决方案很有用。我遇到了类似的情况,我最终做的是通过SCNTechnique 使用多个通道进行渲染。首先我用纯白色diffuse 渲染了一个地板,然后我渲染了没有地板的场景的其余部分。为此,我将 SCNFloorcategoryBitMask 设置为 3,并将其他的设置为默认值 1。

    接下来我用这个定义创建了我的SCNTechnique,它将地板和场景的其余部分渲染到单独的缓冲区中,然后将它们组合到最终场景中:

    self.sceneView.technique = SCNTechnique(dictionary: [
      "passes" : [
        "store-floor": [
          "draw" : "DRAW_NODE",
          "node" : "floor-node",
          "inputs" : [],
          "outputs" : [ "color" : "color_floor" ]
        ],
        "store-scene": [
          "draw" : "DRAW_SCENE",
          "excludeCategoryMask" : 2,
          "inputs" : [],
          "outputs" : [ "color" : "color_scene" ]
        ],
        "recall-scene": [
          "draw" : "DRAW_QUAD",
          "metalVertexShader" : "vertex_tecnique_basic",
          "metalFragmentShader" : "fragment_tecnique_merge",
          "inputs" : [ "alphaTex" : "color_floor", "sceneTex" : "color_scene" ],
          "outputs" : [ "color" : "COLOR" ]
        ]
      ],
      "symbols" : [
        "vertexSymbol" : [ "semantic" : "vertex" ]
      ],
      "targets" : [
        "color_floor" : [ "type" : "color" ],
        "color_scene" : [ "type" : "color" ],
      ],
      "sequence" : [
        "store-floor",
        "store-scene",
        "recall-scene"
      ]
    ])
    

    接下来是 Metal 共享代码,它采用这两个缓冲区并将它们组合在一起,其中 alpha 值的范围从白色的 0 到黑色的 1。

    using namespace metal;
    #include <SceneKit/scn_metal>
    
    struct TechniqueVertexIn
    {
        float4 position [[attribute(SCNVertexSemanticPosition)]];
    };
    
    struct TechniqueVertexOut
    {
      float4 framePos [[position]];
      float2 centeredLoc;
    };
    
    constexpr sampler s = sampler(coord::normalized, address::repeat, filter::linear);
    
    vertex TechniqueVertexOut vertex_tecnique_basic(
      TechniqueVertexIn     in [[stage_in]],
      constant SCNSceneBuffer&  scnFrame [[buffer(0)]])
    {
      TechniqueVertexOut vert;
    
      vert.framePos = float4(in.position.x, in.position.y, 0.0, 1.0);
      vert.centeredLoc = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5);
    
      return vert;
    }
    
    fragment half4 fragment_tecnique_merge(
      TechniqueVertexOut  vert [[stage_in]],
      texture2d<float>  alphaTex [[texture(0)]],
      texture2d<float>  sceneTex [[texture(1)]])
    {
        float4 alphaColor = alphaTex.sample(s, vert.centeredLoc);
        float4 sceneColor = sceneTex.sample(s, vert.centeredLoc);
      float alpha     = 1.0 - max(max(alphaColor.r, alphaColor.g), alphaColor.b); // since floor should be white, could just pick a chan
    
      alpha *= alphaColor.a;
      alpha = max(sceneColor.a, alpha);
    
      return half4(half3(sceneColor.rgb * alpha), alpha);
    }
    

    最后,这是一个将所有部分放在一起的示例。

    【讨论】:

      猜你喜欢
      • 2016-12-13
      • 2019-09-12
      • 2015-09-07
      • 2015-07-01
      • 2018-01-12
      • 2015-06-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多