SceneKit AR场景中阴影平面应在何处定义、何时更新?
ARSCNView 阴影平面实现方案解答
两种阴影平面放置方式的适用场景
viewDidLoad中单次添加固定阴影平面:仅适用于提前锁定了地面坐标、不需要运行时动态检测平面的固定场景Demo,正式AR场景不推荐。因为ARKit的平面检测是持续迭代的,初始识别到的平面位置、范围误差很大,固定位置的阴影平面很容易出现阴影浮空、和实际地面错位的问题。renderer(_:didAdd:for:)中动态添加:这是正式场景下的正确做法,注意不需要给所有检测到的平面都加阴影平面,只需要给你实际放置AR物体的目标水平平面对应的锚点节点添加即可,避免不必要的性能开销。
阴影平面的更新时机
你必须同步更新阴影平面的位置和尺寸,对应的回调是renderer(_:didUpdate:for:):
- ARKit每次优化平面锚点的坐标、扩展平面检测范围时都会触发这个回调
- 你需要在回调里把阴影平面的
position更新为最新的planeAnchor.center坐标 - 同步把阴影平面几何的尺寸更新为和
planeAnchor.extent匹配的大小,保证阴影接收范围和检测到的真实平面一致 - 如果后续平面被移除(
renderer(_:didRemove:for:)回调触发),记得把对应的阴影平面节点从父节点移除
你当前代码的问题与修正
你现有代码有几个会导致效果异常的问题:
- 语法错误:
let anchorY: planeAnchor.center.y写法不对,应该是赋值语法let anchorY = planeAnchor.center.y - 几何类型使用错误:
SCNFloor是无限延伸的几何对象,本身没有width、length属性,你设置的这两个参数不会生效,实际运行会出现全场景都接收阴影的问题。建议改用SCNPlane做阴影接收面,将其绕X轴旋转-90度使其水平朝上,就可以正常匹配平面尺寸。 - 缺少必要的阴影配置:需要把阴影平面节点的
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
相关产品推荐
相关产品推荐

