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

如何在SceneKit中为自定义枢轴正确设置物理体

在SceneKit中为带缩放/旋转的自定义枢轴节点正确设置物理体

当节点仅设置自定义枢轴(无缩放、旋转)时,通过对物理形状应用枢轴的逆变换即可让物理体与几何对齐,但当节点同时存在缩放或旋转时,这种方法会导致物理形状错位。

例如以下错误代码会出现对齐问题:

box.scale = SCNVector3Make(1.5, 1.5, 1.5)
box.pivot = SCNMatrix4MakeTranslation(1, 1, 1)

// 错误:变换顺序颠倒,未处理旋转
let boxPhysicsShape = SCNPhysicsShape(geometry: box.geometry!)
  .scaled(by: box.scale)
  .transformed(by: SCNMatrix4Invert(box.pivot))

问题根源

节点的变换逻辑是先基于枢轴调整原点,再应用缩放,最后应用旋转。物理形状的变换顺序必须与节点的变换逻辑匹配,否则会出现错位。之前的错误在于颠倒了缩放与枢轴逆变换的顺序,且未对物理形状应用节点的旋转变换。

解决方案

正确的变换顺序应为:

  1. 对物理形状应用枢轴的逆变换,将物理形状的原点对齐到节点的枢轴位置
  2. 应用节点的缩放变换,让物理形状与几何的缩放比例一致
  3. 应用节点的旋转变换,让物理形状与几何的旋转角度一致

可以通过链式调用辅助方法实现,也可以直接构建组合变换矩阵一次性应用。

修正后的物理形状创建代码

// 获取节点的变换参数
let pivotInverse = SCNMatrix4Invert(box.pivot)
let scale = box.scale
let rotation = box.rotation // 直接使用节点的旋转参数,或从eulerAngles转换

// 按正确顺序变换物理形状
let boxPhysicsShape = SCNPhysicsShape(geometry: box.geometry!)
  .transformed(by: pivotInverse)
  .scaled(by: scale)
  .rotated(by: rotation)

box.physicsBody = SCNPhysicsBody(type: .static, shape: boxPhysicsShape)

完整可运行示例

class My3DScene: SCNScene {
  override init() {
    super.init()
    
    // 设置相机
    let camera = SCNCamera()
    let cameraNode = SCNNode()
    cameraNode.camera = camera
    cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
    rootNode.addChildNode(cameraNode)
    
    // 创建带变换的节点
    let boxGeo = SCNBox(width: 5, height: 5, length: 1, chamferRadius: 0)
    boxGeo.firstMaterial?.diffuse.contents = UIColor.red
    let box = SCNNode(geometry: boxGeo)
    box.scale = SCNVector3Make(1.5, 1.5, 1.5)
    box.eulerAngles = SCNVector3Make(1, 2, 3)
    box.pivot = SCNMatrix4MakeTranslation(1, 1, 1)
    rootNode.addChildNode(box)
    
    // 正确创建物理形状
    let pivotInverse = SCNMatrix4Invert(box.pivot)
    // 将欧拉角转换为SCNVector4格式的旋转参数
    let rotationX = SCNMatrix4MakeRotation(box.eulerAngles.x, 1, 0, 0)
    let rotationY = SCNMatrix4MakeRotation(box.eulerAngles.y, 0, 1, 0)
    let rotationZ = SCNMatrix4MakeRotation(box.eulerAngles.z, 0, 0, 1)
    let rotationMatrix = SCNMatrix4Mult(SCNMatrix4Mult(rotationX, rotationY), rotationZ)
    let rotationVector = SCNVector4Make(
      rotationMatrix.m12, rotationMatrix.m13, rotationMatrix.m23,
      acos(rotationMatrix.m11)
    )
    
    let boxPhysicsShape = SCNPhysicsShape(geometry: box.geometry!)
      .transformed(by: pivotInverse)
      .scaled(by: box.scale)
      .rotated(by: rotationVector)
    
    box.physicsBody = SCNPhysicsBody(type: .static, shape: boxPhysicsShape)
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

class GameViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    let scnView = SCNView(frame: self.view.frame)
    scnView.debugOptions = .showPhysicsShapes
    self.view.addSubview(scnView)
    
    let scene = My3DScene()
    scnView.present(scene, with: .fade(withDuration: 1), incomingPointOfView: nil)
  }
}

// 物理形状辅助扩展
public extension SCNPhysicsShape {
  func transformed(by transform: SCNMatrix4) -> SCNPhysicsShape {
    return SCNPhysicsShape(
      shapes: [self],
      transforms: [NSValue(scnMatrix4: transform)]
    )
  }

  func translated(by translation: SCNVector3) -> SCNPhysicsShape {
    let transform = SCNMatrix4MakeTranslation(translation.x, translation.y, translation.z)
    return transformed(by: transform)
  }
  
  func scaled(by scale: SCNVector3) -> SCNPhysicsShape {
    let transform = SCNMatrix4MakeScale(scale.x, scale.y, scale.z)
    return transformed(by: transform)
  }
  
  func rotated(by rotation: SCNVector4) -> SCNPhysicsShape {
    let transform = SCNMatrix4MakeRotation(rotation.w, rotation.x, rotation.y, rotation.z)
    return transformed(by: transform)
  }
}

另一种方式:直接使用组合矩阵

如果不想链式调用,可以直接构建组合变换矩阵,一次性应用到物理形状:

let pivotInverse = SCNMatrix4Invert(box.pivot)
let scaleMatrix = SCNMatrix4MakeScale(box.scale.x, box.scale.y, box.scale.z)
// 构建旋转矩阵
let rotationX = SCNMatrix4MakeRotation(box.eulerAngles.x, 1, 0, 0)
let rotationY = SCNMatrix4MakeRotation(box.eulerAngles.y, 0, 1, 0)
let rotationZ = SCNMatrix4MakeRotation(box.eulerAngles.z, 0, 0, 1)
let rotationMatrix = SCNMatrix4Mult(SCNMatrix4Mult(rotationX, rotationY), rotationZ)

// 组合变换矩阵:旋转 → 缩放 → 枢轴逆变换(矩阵乘法顺序从右到左)
let combinedTransform = SCNMatrix4Mult(SCNMatrix4Mult(rotationMatrix, scaleMatrix), pivotInverse)

let boxPhysicsShape = SCNPhysicsShape(geometry: box.geometry!)
  .transformed(by: combinedTransform)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 23:45:56