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

基于GLB数据配置SCNSkinner骨骼层级时模型变形问题排查

问题

我尝试在SceneKit中展示GLB格式的人体扫描3D模型,由于SceneKit原生不支持该格式,所以手动读取了文件内容。我想用SceneKit的SCNSkinner结合GLB自带的Mixamo绑定系统搭建骨骼,但遇到了问题:当骨骼位置、旋转设为(0,0,0),或boneInverseBindTransforms保持单位矩阵时,模型显示正常;但一旦设置真实的骨骼属性和逆绑定矩阵,模型就会畸形。

现象对比

  • 应用真实骨骼位置、旋转及逆绑定矩阵后:模型呈现畸形状态
  • 基础状态(骨骼属性全为0,逆绑定矩阵为单位矩阵):模型显示正常
  • 期望效果:模型呈现标准A姿态

相关代码

创建SCNSkinner的代码

private func createSCNSkinnerFrom(bgArmature: BgArmature, baseGeometry: SCNGeometry) -> SCNSkinner? {
        var joints: [UInt8] = bgArmature.mesh.joints.joined().map { element in UInt8(element) }
        var weights: [Float] = bgArmature.mesh.weights.joined().map { element in element }
        
        let bones: [SCNNode] = self.createBoneNodesList(bgRig: bgArmature.rig!)
    
        let boneIndicesData = Data(bytesNoCopy: &joints, count: joints.count * MemoryLayout<UInt8>.size, deallocator: .none)
        let boneIndicesGeometrySource = SCNGeometrySource(data: boneIndicesData, semantic: .boneIndices, vectorCount: joints.count/4, usesFloatComponents: false, componentsPerVector: 4, bytesPerComponent: MemoryLayout<UInt8>.size, dataOffset: 0, dataStride: MemoryLayout<UInt8>.size * 4)
        
        let boneWeightsData = Data(bytesNoCopy: &weights, count: weights.count * MemoryLayout<Float>.size, deallocator: .none)
        let boneWeightsGeometrySource = SCNGeometrySource(data: boneWeightsData, semantic: .boneWeights, vectorCount: weights.count/4, usesFloatComponents: true, componentsPerVector: 4, bytesPerComponent: MemoryLayout<Float>.size, dataOffset: 0, dataStride: MemoryLayout<Float>.size * 4)
        
        let boneInverseBindTransforms: [NSValue]? = self.createListOfBoneInverseBindTransforms(bgBones: bgArmature.rig!.bones)
        
        let skinner = SCNSkinner(baseGeometry: baseGeometry,
                                 bones: bones,
                                 boneInverseBindTransforms: boneInverseBindTransforms,
                                 boneWeights: boneWeightsGeometrySource,
                                 boneIndices: boneIndicesGeometrySource)
        return skinner
    }

创建骨骼节点的代码

private func createBoneNodeWithoutChildren(bgBone: BgBone) -> SCNNode {
        let bone = SCNNode()
        if let name = bgBone.name {
            bone.name = name
        }
        if let translation = bgBone.translation {
            bone.simdPosition = SIMD3<Float>(translation[0]!, translation[1]!, translation[2]!) 
        }
        if let rotation = bgBone.rotation {
            bone.simdOrientation = simd_quatf(ix: rotation[0]!, iy: rotation[1]!, iz: rotation[2]!, r: rotation[3]!)
        }
        if let scale = bgBone.scale {
            bone.simdScale = SIMD3<Float>(scale[0]!, scale[1]!, scale[2]!)
        }
        return bone
    }

private func createBoneNodesList(bgRig: BgRig) -> [SCNNode] {
        var bonesList: [SCNNode] = []
        for bone in bgRig.bones {
            bonesList.append(self.createBoneNodeWithoutChildren(bgBone: bone))
        }
        return bonesList
    }

创建逆绑定矩阵的代码

private func createListOfBoneInverseBindTransforms(bgBones: [BgBone]!) -> [NSValue]? {
        var boneInverseBindTransforms: [NSValue]? = []
        for bone in bgBones {
            boneInverseBindTransforms?.append(NSValue(scnMatrix4: bone.inverseBindMatrix!.getSCNMatrix4()))
        }
        return boneInverseBindTransforms
    }

BgBone类及BgMat4结构体

class BgBone {
    var index: Int?
    var children: [Int?]?
    var name: String?
    var rotation: [Float?]?
    var scale: [Float?]?
    var translation: [Float?]?
    var inverseBindMatrix: BgMat4?
    
    init(index: Int? = nil, children: [Int?]? = nil, name: String? = nil, rotation: [Float?]? = nil, scale: [Float?]? = nil, translation: [Float?]? = nil) {
        self.index = index
        self.children = children
        self.name = name
        self.rotation = rotation
        self.scale = scale
        self.translation = translation
    }
}

struct BgMat4: sizeable {
    var r1c1: Float
    var r2c1: Float
    var r3c1: Float
    var r4c1: Float
    
    var r1c2: Float
    var r2c2: Float
    var r3c2: Float
    var r4c2: Float
    
    var r1c3: Float
    var r2c3: Float
    var r3c3: Float
    var r4c3: Float
    
    var r1c4: Float
    var r2c4: Float
    var r3c4: Float
    var r4c4: Float
    
    init(fromData: Data) {
        /// Accessors of matrix type have data stored in column-major order; start of each column MUST be aligned to 4-byte boundaries.
        /// Specifically, when ROWS * SIZE_OF_COMPONENT (where ROWS is the number of rows of the matrix) is not a multiple of 4,
        /// then (ROWS * SIZE_OF_COMPONENT) % 4 padding bytes MUST be inserted at the end of each column.
        self.r1c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 0, as: Float32.self) }
        self.r2c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 4, as: Float32.self) }
        self.r3c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 8, as: Float32.self) }
        self.r4c1 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 12, as: Float32.self) }
        self.r1c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 16, as: Float32.self) }
        self.r2c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 20, as: Float32.self) }
        self.r3c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 24, as: Float32.self) }
        self.r4c2 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 28, as: Float32.self) }
        self.r1c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 32, as: Float32.self) }
        self.r2c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 36, as: Float32.self) }
        self.r3c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 40, as: Float32.self) }
        self.r4c3 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 44, as: Float32.self) }
        self.r1c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 48, as: Float32.self) }
        self.r2c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 52, as: Float32.self) }
        self.r3c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 56, as: Float32.self) }
        self.r4c4 = fromData.withUnsafeBytes { rawBuffer in rawBuffer.load(fromByteOffset: 60, as: Float32.self) }
    }
    
    func getSCNMatrix4() -> SCNMatrix4 {
        return SCNMatrix4(m11: self.r1c1,
                          m12: self.r2c1,
                          m13: self.r3c1,
                          m14: self.r4c1,
                          m21: self.r1c2,
                          m22: self.r2c2,
                          m23: self.r3c2,
                          m24: self.r4c2,
                          m31: self.r1c3,
                          m32: self.r2c3,
                          m33: self.r3c3,
                          m34: self.r4c3,
                          m41: self.r1c4,
                          m42: self.r2c4,
                          m43: self.r3c4,
                          m44: self.r4c4)
    }
}
解决方案

核心错误点

  1. 骨骼节点未建立层级关系:GLB中的骨骼是树形层级结构,父骨骼的变换会影响子骨骼,但当前代码仅创建了平级的骨骼节点列表,未根据BgBone的children属性挂载父子节点,导致变换作用空间错误。
  2. 矩阵行/列顺序转换错误:GLB矩阵采用列优先存储,而SceneKit的SCNMatrix4是行优先,当前getSCNMatrix4()方法的转换逻辑完全颠倒了行和列,导致逆绑定矩阵失效。
  3. 骨骼初始变换与绑定姿态不匹配:未建立层级的情况下,骨骼的局部变换(translation/rotation/scale)无法正确对应GLB中的绑定姿态,和逆绑定矩阵的计算逻辑冲突。

具体修改步骤

1. 修复骨骼层级结构

修改createBoneNodesList函数,建立正确的骨骼父子关系:

private func createBoneNodesList(bgRig: BgRig) -> [SCNNode] {
    // 先创建所有骨骼节点
    var bonesList: [SCNNode] = bgRig.bones.map { createBoneNodeWithoutChildren(bgBone: $0) }
    // 遍历骨骼,挂载子节点到对应父节点下
    for (parentIndex, bgBone) in bgRig.bones.enumerated() {
        guard let childIndices = bgBone.children else { continue }
        for childIndex in childIndices {
            guard let childIdx = childIndex, childIdx < bonesList.count else { continue }
            let childBone = bonesList[childIdx]
            bonesList[parentIndex].addChildNode(childBone)
        }
    }
    return bonesList
}

2. 修复矩阵转换逻辑

修改BgMat4.getSCNMatrix4()方法,正确将列优先矩阵转置为行优先的SCNMatrix4:

func getSCNMatrix4() -> SCNMatrix4 {
    // GLB列优先矩阵 → SCN行优先矩阵,需要转置处理
    return SCNMatrix4(
        m11: r1c1, m12: r1c2, m13: r1c3, m14: r1c4,
        m21: r2c1, m22: r2c2, m23: r2c3, m24: r2c4,
        m31: r3c1, m32: r3c2, m33: r3c3, m34: r3c4,
        m41: r4c1, m42: r4c2, m43: r4c3, m44: r4c4
    )
}

3. 确认骨骼变换作用空间

createBoneNodeWithoutChildren中设置的simdPosition、simdOrientation、simdScale本身就是局部变换,和GLB的存储逻辑一致,配合修复后的层级结构即可正确应用初始姿态。

完成以上修改后,骨骼层级、矩阵转换都会和GLB的Mixamo绑定系统匹配,模型就能正确显示A姿态,不会出现畸形。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 20:55:57