SceneKit透明SCNFloor()的阴影

7
我有一个“地板节点”,我需要从“定向光”投射阴影。这个节点需要是透明的(在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为白色时它看起来像什么,而下面是透明的。 在透明版本中没有阴影。

在这里,您可以看到场景视图的白色背景颜色效果-阴影可见

如果使用sceneView.backgroundColor == .clear版本,则会出现此情况。 这下面有UIImageView。 我需要使用此版本,但没有阴影可见

2个回答

4

获取透明阴影有两个步骤:

第一步:您需要将其连接为节点,而不是作为几何类型添加到场景中。

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上的阴影: enter image description here

可见的SCNPlane上的阴影,我们的相机在SCNFloor下方: enter image description here

为了获得透明的阴影,您需要设置阴影颜色,而不是对象本身的透明度。

第二点: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)是阴影的透明度,RGB组件(white: 0)是阴影的黑色颜色。

enter image description here

enter image description here

P.S.

sceneView.backgroundColor 在透明和白色之间切换。

在这种情况下,当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时,阴影非常微弱。

这里是一个可容忍的解决方法(请查看SOLUTION部分以获取解决方案):

@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),我会在背景图片上得到令人满意的阴影,但在白色背景上会得到纯黑色阴影

enter image description here

解决方案
您应该获取3D对象的渲染,以获得具有其有用Alpha通道的预乘RGBA图像。之后,您可以使用另一个视图中的经典OVER合成操作,在自然图像上组合立方体和其阴影的rgba图像

这是OVER操作的公式:

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

enter image description here


1
评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
我猜这不是完美的解决方案,所以我正在探索其他解决方法。我已经注意到,当影子颜色与黑色不同时,就会显示出阴影。颜色越接近黑色,它变得越透明。目前我只能显示白色阴影。也许这会在某种程度上有所帮助。 - Damian Dudycz
你在谈论什么解决方案?是覆盖合成操作 (RGB1 * A1) + (RGB2 * (1 - A1)) 还是 UIColor(white: 1, alpha: 0.02) - Andy Jazz
寻找CISourceOverCompositing。它使用了诸如CICategoryStillImage和CICategoryVideo之类的操作数。 - Andy Jazz
@ARGeo 你能帮我解答这个问题吗?https://stackoverflow.com/questions/58073121/the-correct-way-to-add-direction-light-to-virtual-object-and-not-be-affected-by - Weizhi
显示剩余2条评论

4
这篇文章已经发布有一段时间了,但或许某些人会觉得这个替代方案很有用。我也遇到了类似的情况,最后采用了使用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);
}

最后,这是所有组成部分放在一起的示例。enter image description here。这是与所有部件组合在一起的样子。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接