You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SceneKit AR场景中阴影平面应在何处定义、何时更新?

ARSCNView 阴影平面实现方案解答

两种阴影平面放置方式的适用场景

  • viewDidLoad中单次添加固定阴影平面:仅适用于提前锁定了地面坐标、不需要运行时动态检测平面的固定场景Demo,正式AR场景不推荐。因为ARKit的平面检测是持续迭代的,初始识别到的平面位置、范围误差很大,固定位置的阴影平面很容易出现阴影浮空、和实际地面错位的问题。
  • renderer(_:didAdd:for:)中动态添加:这是正式场景下的正确做法,注意不需要给所有检测到的平面都加阴影平面,只需要给你实际放置AR物体的目标水平平面对应的锚点节点添加即可,避免不必要的性能开销。

阴影平面的更新时机

你必须同步更新阴影平面的位置和尺寸,对应的回调是renderer(_:didUpdate:for:):

  • ARKit每次优化平面锚点的坐标、扩展平面检测范围时都会触发这个回调
  • 你需要在回调里把阴影平面的position更新为最新的planeAnchor.center坐标
  • 同步把阴影平面几何的尺寸更新为和planeAnchor.extent匹配的大小,保证阴影接收范围和检测到的真实平面一致
  • 如果后续平面被移除(renderer(_:didRemove:for:)回调触发),记得把对应的阴影平面节点从父节点移除

你当前代码的问题与修正

你现有代码有几个会导致效果异常的问题:

  1. 语法错误:let anchorY: planeAnchor.center.y写法不对,应该是赋值语法let anchorY = planeAnchor.center.y
  2. 几何类型使用错误:SCNFloor是无限延伸的几何对象,本身没有width、length属性,你设置的这两个参数不会生效,实际运行会出现全场景都接收阴影的问题。建议改用SCNPlane做阴影接收面,将其绕X轴旋转-90度使其水平朝上,就可以正常匹配平面尺寸。
  3. 缺少必要的阴影配置:需要把阴影平面节点的castsShadow属性设为false,避免平面自身产生错误阴影,同时确认场景内的方向光已经开启castsShadow属性,阴影模式设置为.deferred效果会更稳定。

修正后的核心代码参考:

// 记录阴影平面节点的标识,方便更新时查找
let shadowPlaneNodeName = "ar_shadow_plane"

private func addShadowPlane(node: SCNNode, planeAnchor: ARPlaneAnchor) {
    // 避免重复添加
    if node.childNode(withName: shadowPlaneNodeName, recursively: false) != nil {
        return
    }
    
    // 用SCNPlane替代SCNFloor,方便控制尺寸
    let planeGeometry = SCNPlane(
        width: CGFloat(planeAnchor.extent.x),
        height: CGFloat(planeAnchor.extent.z)
    )
    planeGeometry.materials = [shadowMaterialStandard()]
    
    let shadowNode = SCNNode(geometry: planeGeometry)
    shadowNode.name = shadowPlaneNodeName
    // 平面默认是垂直的,绕X轴转-90度变为水平朝上
    shadowNode.eulerAngles.x = -.pi / 2
    // 平面本身不投射阴影
    shadowNode.castsShadow = false
    // 对齐锚点中心
    shadowNode.position = SCNVector3(
        planeAnchor.center.x,
        planeAnchor.center.y,
        planeAnchor.center.z
    )
    
    node.addChildNode(shadowNode)
}

// didUpdate回调里更新阴影平面
private func updateShadowPlane(node: SCNNode, planeAnchor: ARPlaneAnchor) {
    guard let shadowNode = node.childNode(withName: shadowPlaneNodeName, recursively: false),
          let plane = shadowNode.geometry as? SCNPlane else {
        return
    }
    // 更新尺寸
    plane.width = CGFloat(planeAnchor.extent.x)
    plane.height = CGFloat(planeAnchor.extent.z)
    // 更新位置
    shadowNode.position = SCNVector3(
        planeAnchor.center.x,
        planeAnchor.center.y,
        planeAnchor.center.z
    )
}

func shadowMaterialStandard() -> SCNMaterial {
    let material = SCNMaterial()
    material.lightingModel = .physicallyBased
    material.writesToDepthBuffer = true
    material.readsFromDepthBuffer = true
    // 平面本身透明,只渲染阴影
    material.colorBufferWriteMask = []
    material.isDoubleSided = false
    return material
}

补充提示:如果你的AR物体是可以在平面上拖动移动的,只需要保证阴影平面覆盖物体当前所在的平面范围即可,不需要跟着物体单独移动阴影平面。

内容的提问来源于stack exchange,提问作者martin wotterman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 20:51:07