본문 바로가기
IOS

[iOS] RealityKit, ARKit - MotionCapture 예제

by eigen96 2024. 2. 14.
728x90

 

 

ARViewContainer


SwiftUI는 기본적으로 UIKit 또는 AppKit 컴포넌트를 직접 지원하지 않는다.

즉, ARView와 같은 UIKit 컴포넌트를 SwiftUI에서 바로 사용할 수 없다.

따라서, UIKit의 ARView를 SwiftUI 뷰에서 사용하고자 할 때는 UIViewRepresentable 프로토콜을 채택하여 UIKit 뷰를 SwiftUI 뷰로 변환해야 한다.

 

ARView, Entity란?

 

struct ARViewContainer: UIViewRepresentable {
    typealias UIViewType = ARView
    
    //이 메서드는 SwiftUI 뷰가 생성될 때 호출. 
		//여기서 실제 ARView 인스턴스를 생성하고 초기 설정을 수행.
    func makeUIView(context: Context) -> ARView {
        let arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: true)
        
        return arView
    }
    
//이 메서드는 SwiftUI 뷰의 상태나 프로퍼티가 변할 때마다 호출되어, 
//UIKit 뷰(ARView 인스턴스)를 업데이트.
    func updateUIView(_ uiView: ARView, context: Context) {
        
    }
}

 

 

Skeleton Joint

SkeletonJoint

관절의 이름과 3차원 공간에서의 위치를 나타내는 데 사용되는 구조체를 선언한다.

SIMD3<Float> 타입은 3개의 Float 값(x, y, z 좌표)을 포함하며, SIMD(Single Instruction, Multiple Data) 연산을 위해 설계되었다.

 

 

SIMD3란?

 

SkeletonJoint 구조체는 3D 모델이나 캐릭터의 각 관절의 위치를 추적하고 저장하는 데 유용하다.

예를 들어, 모션 캡처 데이터를 처리하거나, 가상 현실(VR) 또는 증강 현실(AR) 애플리케이션에서 사용자의 움직임을 정밀하게 모델링하는 경우에 사용될 수 있다.

이 구조체는 모션 캡처, 애니메이션, 실시간 모션 추적 등 다양한 분야에서 활용될 수 있는 기본적인 데이터 구조이다.

 

struct SkeletonJoint{
    let name: String
    var position: SIMD3<Float>
}

let elbowJoint = SkeletonJoint(name: "elbow", position: SIMD3<Float>(x: 0.5, y: 0.5, z: 0.0))
let kneeJoint = SkeletonJoint(name: "knee", position: SIMD3<Float>(x: -0.5, y: -0.5, z: 0.0))

 

 

SkeletonBone

SkeletonBone이라는 구조체를 정의한다

이는 3D 모델링이나 모션 캡처에서 사용되는 두 관절 사이의 뼈대(bone)이다.

SkeletonBone 인스턴스는 뼈대의 양 끝을 나타내는 두 개의 SkeletonJoint 인스턴스(fromJointtoJoint)를 가지며, 뼈대의 중심 위치(centerPosition)와 길이(length)를 계산한다.

import Foundation
import RealityKit

struct SkeletonBone{
    var fromJoint: SkeletonJoint
    var toJoint: SkeletonJoint
    
    var centerPosition: SIMD3<Float> {
        [(fromJoint.position.x + toJoint.position.x)/2, (fromJoint.position.y + toJoint.position.y)/2,
         (fromJoint.position.z + toJoint.position.z)/2
        ]
    }
    
    var length: Float {
        simd_distance(fromJoint.position, toJoint.position)
    }
}

 

Bone

다음으로 Bones라는 열거형(enum)을 정의한다..

각 케이스는 인체의 뼈대 구조를 나타내는 연결들을 표현한다.

 

enum Bones: CaseIterable {
    case leftShoulderToLeftArm
    case leftArmToLeftForearm
    case leftForearmToLeftHand
    
    case rightShoulderToRightArm
    case rightArmToRightForearm
    case rightForearmToRightHand
    
    case spine7ToLeftShoulder
    case spine7ToRightShoulder

    case neck1ToSpine7
    case spine7ToSpine6
    case spine6ToSpine5

    case hipsToLeftUpLeg
    case leftUpLegToLeftLeg
    case leftLegToLeftFoot

    case hipsToRightUpLeg
    case rightUpLegToRightLeg
    case rightLegToRightFoot
    //각 뼈대 연결의 이름을 반환하는 계산 속성입니다. 
	//시작 관절과 끝 관절의 이름을 하이픈(-)으로 연결하여 구성된 문자열을 반환합니다.
    var name: String {
        return "\\(self.jointFromName)-\\(self.jointToName)"
    }
    //연결의 시작점이 되는 관절의 이름을 반환합니다. 이는 각 연결이 시작하는 부위를 나타냅니다.
    var jointFromName: String {
        switch self {
            
        case .leftShoulderToLeftArm:
            return "left_shoulder_1_joint"
        case .leftArmToLeftForearm:
            return "left_arm_joint"
        case .leftForearmToLeftHand:
            return "left_forearm_joint"
            
        case .rightShoulderToRightArm:
            return "right_shoulder_1_joint"
        case .rightArmToRightForearm:
            return "right_arm_joint"
        case .rightForearmToRightHand:
            return "right_forearm_joint"
            
        case .spine7ToLeftShoulder:
            return "spine_7_joint"
        case .spine7ToRightShoulder:
            return "spine_7_joint"

        case .neck1ToSpine7:
            return "neck_1_joint"
        case .spine7ToSpine6:
            return "spine_7_joint"
        case .spine6ToSpine5:
            return "spine_6_joint"

        case .hipsToLeftUpLeg:
            return "hips_joint"
        case .leftUpLegToLeftLeg:
            return "left_upLeg_joint"
        case .leftLegToLeftFoot:
            return "left_leg_joint"

        case .hipsToRightUpLeg:
            return "hips_joint"
        case .rightUpLegToRightLeg:
            return "right_upLeg_joint"
        case .rightLegToRightFoot:
            return "right_leg_joint"

        }
    }
    //연결의 끝점이 되는 관절의 이름을 반환합니다. 이는 각 연결이 끝나는 부위를 나타냅니다.
    var jointToName: String{
        switch self {
            
        case .leftShoulderToLeftArm:
            return "left_arm_joint"
        case .leftArmToLeftForearm:
            return "left_forearm_joint"
        case .leftForearmToLeftHand:
            return "left_hand_joint"
            
        case .rightShoulderToRightArm:
            return "right_arm_joint"
        case .rightArmToRightForearm:
            return "right_forearm_joint"
        case .rightForearmToRightHand:
            return "right_hand_joint"

        case .spine7ToLeftShoulder:
            return "left_shoulder_1_joint"
        case .spine7ToRightShoulder:
            return "right_shoulder_1_joint"

        case .neck1ToSpine7:
            return "spine_7_joint"
        case .spine7ToSpine6:
            return "spine_6_joint"
        case .spine6ToSpine5:
            return "spine_5_joint"

        case .hipsToLeftUpLeg:
            return "left_upLeg_joint"
        case .leftUpLegToLeftLeg:
            return "left_leg_joint"
        case .leftLegToLeftFoot:
            return "left_foot_joint"

        case .hipsToRightUpLeg:
            return "right_upLeg_joint"
        case .rightUpLegToRightLeg:
            return "right_leg_joint"
        case .rightLegToRightFoot:
            return "right_foot_joint"

        }
    }
}

 

Bones 열거형에 정의된 모든 뼈대 연결의 이름과 해당 시작점과 끝점의 관절 이름을 출력하는 코드

for bone in Bones.allCases {
    print("Bone name: \\(bone.name), From: \\(bone.jointFromName), To: \\(bone.jointToName)")
}

 

 

BodySkeleton

RealityKit의 Entity를 상속받아 몸통 구조를 표현하는 데 사용되는 BodySkeleton을 정의합니다.

  • 두 개의 주요 (dictionary) 속성인 jointsbones를 가지고 있으며, 각각은 몸의 관절과 뼈대를 표현하는 Entity 객체들을 저장합니다.
  • ARKit의 ARBodyAnchor를 사용하여 실시간으로 사람의 몸통을 추적하고 시각화한다.

ARBodyAnchor를 통해 얻은 실시간 몸통 데이터를 시각화하기 위해 다음과 같은 과정을 수행한다.

  1. 사용자의 몸통에 대한 ARBodyAnchor 업데이트를 받는다.
  2. createSkeletonBone(bone:bodyAnchor:) 메서드를 사용하여 사용자의 모든 주요 뼈대를 생성.
  3. 각 SkeletonBone에 대해 createBoneEntity(for:diameter:color:) 메서드를 호출하여 시각적으로 표현할 수 있는 Entity를 생성.
  4. 생성된 Entity들을 jointsbones 사전에 추가하여 관리.
class BodySkeleton: Entity {
    var joints: [String: Entity] = [:]
    var bones: [String: Entity] = [:]
    
    
    required init() {
        fatalError("init() has not been implemented")
    }
  //주어진 반지름과 색상으로 구(sphere) 모양의 관절을 생성하는 메서드입니다
	//이 메서드는 MeshResource와 SimpleMaterial을 사용하여 
	//시각적으로 관절을 표현한 ModelEntity를 생성하고 반환합니다.
    private func createJoint(radius: Float, color: UIColor = .white) -> Entity {
        let mesh = MeshResource.generateSphere(radius: radius)
        let material = SimpleMaterial(color: color,roughness: 0.8, isMetallic: true)
        let entity = ModelEntity(mesh: mesh, materials: [material])
        
        return entity
    }
    // Bones 열거형의 값과 ARBodyAnchor를 사용하여 두 관절 사이의 뼈대(SkeletonBone)를 생성
    // 이 메서드는 ARKit의 bodyAnchor.skeleton.modelTransform을 사용하여 관절의 위치를 얻고,
		// SkeletonBone 객체를 생성하여 반환합니다.
    private func createSkeletonBone(bone: Bones, bodyAnchor: ARBodyAnchor) -> SkeletonBone? {
        guard let fromJointEntityTransform = bodyAnchor.skeleton.modelTransform(for: ARSkeleton.JointName(rawValue: bone.jointFromName)),
              let toJointEntityTransform = bodyAnchor.skeleton.modelTransform(for: ARSkeleton.JointName(rawValue: bone.jointToName))
        else { return nil}
        
        let rootPosition = simd_make_float3(bodyAnchor.transform.columns.3)
        let jointFromEntityOffsetFromRoot = simd_make_float3(fromJointEntityTransform.columns.3) // relative to root (i.e. hipjoint)
        let jointFromEntityPosition = jointFromEntityOffsetFromRoot + rootPosition // relative to world reference frame

        let jointToEntityOffsetFromRoot = simd_make_float3(toJointEntityTransform.columns.3) // relative to root (i.e. hipjoint)
        let jointToEntityPosition = jointToEntityOffsetFromRoot + rootPosition // relative to world reference frame

        let fromJoint = SkeletonJoint(name: bone.jointFromName, position: jointFromEntityPosition)
        let toJoint = SkeletonJoint(name: bone.jointToName, position: jointToEntityPosition)
        return SkeletonBone(fromJoint: fromJoint, toJoint: toJoint)
    }

    //SkeletonBone 객체를 받아 해당 뼈대를 시각화하는 Entity를 생성하는 메서드입니다.
		// 이 메서드는 MeshResource를 사용하여 길이가 SkeletonBone의 길이와 일치하고, 
		//지름과 색상이 지정된 막대기 형태의 ModelEntity를 생성합니다.
    private func createBoneEntity(for skeletonBone: SkeletonBone, diameter: Float = 0.04, color: UIColor = .white) -> Entity {
        let mesh = MeshResource.generateBox(size: [diameter, diameter, skeletonBone.length], cornerRadius: diameter/2)
        let material = SimpleMaterial(color: color, roughness: 0.5, isMetallic: true)
        let entity = ModelEntity(mesh: mesh, materials: [material])
        
        return entity
    }
}

 

 

이어서 생성자에 다음과 같은 코드를 추가한다.

ARBodyAnchor를 매개변수로 받아 사람의 몸통에 대한 시각적 표현을 생성하는 데 사용.

이 메서드는 ARKit에서 제공하는 몸통 관절의 위치 데이터(ARBodyAnchor)를 기반으로 하는 관절과 뼈대(bones)를 생성하고,

이를 시각화하기 위한 Entity 객체들을 구성.

 

 

...
...

required init(for bodyAnchor: ARBodyAnchor) {
        super.init()
        //각 관절에 대한 **Entity**를 생성
        for jointName in ARSkeletonDefinition.defaultBody3D.jointNames {
            var jointRadius: Float = 0.05
            var jointColor: UIColor = .green
            //생성 과정에서 관절의 이름에 따라 반지름(**jointRadius**)과 색상(**jointColor**)을 적용
            switch jointName {
            case "neck_1_joint", "neck_2_joint", "neck_3_joint", "neck_4_joint", "head_joint",
                "left_shoulder_1_joint", "right_shoulder_1_joint":
                jointRadius *= 0.5
                
            case "jaw_joint", "chin_joint", "left_eye_joint", "left_eyeLowerLid_joint", "left_eyeUpperLid_joint",
                "left_eyeball_joint", "nose_joint", "right_eye_joint", "right_eyeLowerLid_joint",
                "right_eyeUpperLid_joint", "right_eyeball_joint":
                jointRadius *= 0.2
                jointColor = .yellow
                
            case _ where jointName.hasPrefix("spine_"):
                jointRadius *= 0.75
                
            case "left_hand_joint", "right_hand_joint":
                jointRadius *= 1
                jointColor = .green
                
            case _ where jointName.hasPrefix("left_hand") || jointName.hasPrefix("right_hand"):
                jointRadius *= 0.25
                jointColor = .yellow
                
            case _ where jointName.hasPrefix("left_toes") || jointName.hasPrefix("right_toes"):
                jointRadius *= 0.5
                jointColor = .yellow
                
            default:
                jointRadius = 0.05
                jointColor = .green
                
            }
            //생성된 각 관절 **Entity**는 **joints** 딕셔너리에 저장되고, 
						//**BodySkeleton**의 자식 엔티티로 추가됩니다.

            // Create an entity for the joint, add to joints directory, and add it to the parent entity (i.e. bodySkeleton)
            let jointEntity = createJoint(radius: jointRadius, color: jointColor)
            joints[jointName] = jointEntity
            self.addChild(jointEntity)
        }
        //**createSkeletonBone** 메서드를 사용하여 
				//각 **Bones** 열거형에 정의된 뼈대에 해당하는 **SkeletonBone** 객체를 생성합니다.
        for bone in Bones.allCases {
            guard let skeletonBone = createSkeletonBone(bone: bone, bodyAnchor: bodyAnchor) else { continue }

						// 생성된 뼈대 **Entity**는 **bones** 딕셔너리에 저장되고, 
						//마찬가지로 **BodySkeleton**의 자식 엔티티로 추가됩니다.
            // Create an entity for the bone, add to bones directory, and add it to the parent entity (i.e. bodySkeleton)
            let boneEntity = createBoneEntity(for: skeletonBone)
            bones[bone.name] = boneEntity
            self.addChild(boneEntity)
        }

    }
...
...

 

 

다음으로 update() 메서드를 추가한다..

update(with bodyAnchor: ARBodyAnchor) 메서드는 BodySkeleton 클래스의 인스턴스를 주어진 ARBodyAnchor에 기반하여 업데이트하는 기능을 구현.

이 메서드는 실시간으로 ARBodyAnchor로부터 얻어진 정보를 사용하여 각 관절(joints)과 뼈대(bones)의 위치 및 방향을 업데이트.

 

func update(with bodyAnchor: ARBodyAnchor) {
				//**ARBodyAnchor**로부터 **rootPosition**을 계산. 이는 몸통의 루트(보통 골반) 위치
        let rootPosition = simd_make_float3(bodyAnchor.transform.columns.3)
        
        for jointName in ARSkeletonDefinition.defaultBody3D.jointNames {
						//**bodyAnchor.skeleton.modelTransform**을 사용하여 각 관절의 변환 정보를 얻고,
						// 이를 기반으로 관절 엔티티의 **position**과 **orientation** 속성을 업데이트
            if let jointEntity = joints[jointName],
               let jointEntityTransform = bodyAnchor.skeleton.modelTransform(for: ARSkeleton.JointName(rawValue: jointName)) {
                
                let jointEntityOffsetFromRoot = simd_make_float3(jointEntityTransform.columns.3) // relative to root
                jointEntity.position = jointEntityOffsetFromRoot + rootPosition // relative to world reference frame
                jointEntity.orientation = Transform(matrix: jointEntityTransform).rotation
            }
        }
        
        for bone in Bones.allCases {
						//뼈대의 중심 위치(**centerPosition**)는 **createSkeletonBone** 메서드를 사용하여 계산되며, 
						//이 위치는 뼈대 엔티티의 새로운 위치
            let boneName = bone.name
            guard let entity = bones[boneName],
                  let skeletonBone = createSkeletonBone(bone: bone, bodyAnchor: bodyAnchor)
            else { continue }
						//**entity.look(at:from:relativeTo:)** 메서드를 사용하여 뼈대 엔티티의 방향을 설정
						//이는 뼈대 엔티티가 끝 관절(**toJoint**)을 바라보도록 하여, 뼈대의 방향을 올바르게 설정
            entity.position = skeletonBone.centerPosition
            entity.look(at: skeletonBone.toJoint.position, from: skeletonBone.centerPosition, relativeTo: nil) // set orientation for bone
        }
 
}

 

 

Apple Motion Capture Using SwiftUI, ARKit + RealityKit

 

 

728x90

댓글