透明 SCNFloor() 上的 SceneKit 阴影

SceneKit shadow on a transparent SCNFloor()

我有一个 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 的最后一行更改背景颜色 - 我需要它清晰,所以我可以在它下面显示相机预览。

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

There are two steps to get a transparent shadow :

首先 :您需要将它作为 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() 下:

For getting a transparent shadow you need to set a shadow color, not the object's transparency itself.

其次 : A shadow color 必须像这样设置 macOS:

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) 是阴影的黑色。

P.S.

sceneView.backgroundColor switching between .clear colour and .white colour.

在这种特殊情况下,当 sceneView.backgroundColor = .clear 时我无法捕捉到强烈的阴影,因为您需要在 RGBA=1,1,1,1 之间切换( 白色模式 :白色颜色,alpha=1) 和 RGBA=0,0,0,0清晰模式:黑色,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 图像上获得 令人满意的阴影 ,但在白色图像上获得 纯黑色阴影

SOLUTION:

You should grab a render of 3D objects to have premultiplied RGBA image with its useful Alpha channel. After that, you can composite rgba image of cube and its shadow over image of nature using classical OVER compositing operation in another View.

这里是 OVER 运算的公式:

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

自发布以来已经有一段时间了,但也许有人会发现这个替代解决方案很有用。我遇到了类似的情况,我最终做的是通过 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);
}

最后是一个示例,展示了将所有部分组合在一起后的样子。