基于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) } }
解决方案
核心错误点
- 骨骼节点未建立层级关系:GLB中的骨骼是树形层级结构,父骨骼的变换会影响子骨骼,但当前代码仅创建了平级的骨骼节点列表,未根据
BgBone的children属性挂载父子节点,导致变换作用空间错误。 - 矩阵行/列顺序转换错误:GLB矩阵采用列优先存储,而SceneKit的
SCNMatrix4是行优先,当前getSCNMatrix4()方法的转换逻辑完全颠倒了行和列,导致逆绑定矩阵失效。 - 骨骼初始变换与绑定姿态不匹配:未建立层级的情况下,骨骼的局部变换(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
相关产品推荐
相关产品推荐

