๐ŸŒ KO

๐ŸŽฒ RealityKit

โญ Difficulty: โญโญโญโญ โฑ๏ธ Est. Time: 3-4h ๐Ÿ“‚ Graphics & Media

์‚ฌ์‹ค์ ์ธ 3D ๋ Œ๋”๋ง๊ณผ AR ๊ฒฝํ—˜

iOS 13+visionOS ์ตœ์ ํ™”

โœจ RealityKit is?

RealityKit is Apple's high-performance 3D rendering engine, integrated with ARKit to deliver realistic AR experiences. It supports physics, animation, spatial audio, photorealistic rendering, and integrates seamlessly with SwiftUI.

๐Ÿ’ก Key Features: PBR Rendering ยท Physics Simulation ยท Animation ยท Spatial Audio ยท Entity Component System ยท USD Support ยท Reality Composer ยท visionOS Support

๐ŸŽฏ 1. Basic Scene Setup

RealityKit์œผ๋กœ 3D ์”ฌ์„ ์ƒ์„ฑ.

RealityKitView.swift โ€” Basic Setup
import SwiftUI
import RealityKit

struct RealityKitView: View {
    var body: some View {
        RealityView { content in
            // 3D ๋ฐ•์Šค ์ƒ์„ฑ
            let mesh = MeshResource.generateBox(size: 0.3)

            // PBR ๋จธํ‹ฐ๋ฆฌ์–ผ (Physically Based Rendering)
            var material = PhysicallyBasedMaterial()
            material.baseColor = .init(tint: .blue)
            material.roughness = .init(floatLiteral: 0.3)
            material.metallic = .init(floatLiteral: 0.8)

            // ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ
            let entity = ModelEntity(mesh: mesh, materials: [material])

            // ์œ„์น˜ ์„ค์ •
            entity.position = [0, 0, -0.5]

            // ์”ฌ์— ์ถ”๊ฐ€
            content.add(entity)
        }
    }
}

๐ŸŽจ 2. ๋จธํ‹ฐ๋ฆฌ์–ผ๊ณผ ํ…์Šค์ฒ˜

๋‹ค์–‘ํ•œ ๋จธํ‹ฐ๋ฆฌ์–ผ๋กœ ์‚ฌ์‹ค์ ์ธ ํ‘œ๋ฉด์„ ๋งŒ๋“ญ.

Materials.swift โ€” ๋จธํ‹ฐ๋ฆฌ์–ผ ์„ค์ •
import RealityKit

class MaterialsManager {
    // 1. ๋ฌผ๋ฆฌ ๊ธฐ๋ฐ˜ ๋จธํ‹ฐ๋ฆฌ์–ผ (PBR)
    func createMetalMaterial() -> PhysicallyBasedMaterial {
        var material = PhysicallyBasedMaterial()

        // ๊ธฐ๋ณธ ์ƒ‰์ƒ
        material.baseColor = .init(tint: .gray)

        // ๊ธˆ์†์„ฑ (0.0 = ๋น„๊ธˆ์†, 1.0 = ์™„์ „ ๊ธˆ์†)
        material.metallic = .init(floatLiteral: 1.0)

        // ๊ฑฐ์น ๊ธฐ (0.0 = ๋งค๋„๋Ÿฌ์›€, 1.0 = ๊ฑฐ์นจ)
        material.roughness = .init(floatLiteral: 0.2)

        return material
    }

    // 2. ๋‹จ์ˆœ ๋จธํ‹ฐ๋ฆฌ์–ผ
    func createSimpleMaterial() -> SimpleMaterial {
        var material = SimpleMaterial()
        material.color = .init(tint: .red, texture: nil)
        return material
    }

    // 3. ํ…์Šค์ฒ˜ ์ ์šฉ
    func createTexturedMaterial() async throws -> PhysicallyBasedMaterial {
        var material = PhysicallyBasedMaterial()

        // ์ด๋ฏธ์ง€์—์„œ ํ…์Šค์ฒ˜ ๋กœ๋“œ
        if let texture = try? await TextureResource(named: "wood.jpg") {
            material.baseColor = .init(texture: .init(texture))
        }

        return material
    }

    // 4. ๋ฐœ๊ด‘ ๋จธํ‹ฐ๋ฆฌ์–ผ
    func createEmissiveMaterial() -> PhysicallyBasedMaterial {
        var material = PhysicallyBasedMaterial()

        // ๋ฐœ๊ด‘ ์ƒ‰์ƒ ๋ฐ ๊ฐ•๋„
        material.emissiveColor = .init(color: .yellow)
        material.emissiveIntensity = 2.0

        return material
    }

    // 5. ํˆฌ๋ช… ๋จธํ‹ฐ๋ฆฌ์–ผ
    func createTransparentMaterial() -> PhysicallyBasedMaterial {
        var material = PhysicallyBasedMaterial()
        material.baseColor = .init(tint: .blue.withAlphaComponent(0.5))
        material.blending = .transparent
        return material
    }
}

๐ŸŽฌ 3. ์• ๋‹ˆ๋ฉ”์ด์…˜

์—”ํ‹ฐํ‹ฐ์— ์›€์ง์ž„ is added.

Animations.swift โ€” ์• ๋‹ˆ๋ฉ”์ด์…˜
import RealityKit

extension Entity {
    // 1. ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜
    func addRotationAnimation() {
        // 360๋„ ํšŒ์ „
        let rotation = Transform(
            pitch: 0,
            yaw: .Float.pi * 2,
            roll: 0
        )

        // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ
        let rotationAnimation = FromToByAnimation(
            to: rotation,
            duration: 3.0,
            timing: .linear,
            isAdditive: true
        )

        // ๋ฌดํ•œ ๋ฐ˜๋ณต
        let animation = try? AnimationResource.generate(with: rotationAnimation)
        if let animation {
            playAnimation(animation.repeat())
        }
    }

    // 2. ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜
    func moveAnimation(to position: SIMD3<Float>, duration: TimeInterval) {
        var transform = self.transform
        transform.translation = position

        move(
            to: transform,
            relativeTo: parent,
            duration: duration,
            timingFunction: .easeInOut
        )
    }

    // 3. ์Šค์ผ€์ผ ์• ๋‹ˆ๋ฉ”์ด์…˜
    func pulseAnimation() {
        let originalScale = scale
        let targetScale = originalScale * 1.2

        let scaleUp = FromToByAnimation(
            to: Transform(scale: targetScale),
            duration: 0.5,
            timing: .easeInOut
        )

        let scaleDown = FromToByAnimation(
            to: Transform(scale: originalScale),
            duration: 0.5,
            timing: .easeInOut
        )

        // ์‹œํ€€์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜
        if let animation = try? AnimationResource.sequence([
            .generate(with: scaleUp),
            .generate(with: scaleDown)
        ].compactMap { $0 }) {
            playAnimation(animation.repeat())
        }
    }

    // 4. ๋ฐ”์šด์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜
    func bounceAnimation() {
        let originalY = position.y
        let jumpHeight: Float = 0.3

        var upTransform = transform
        upTransform.translation.y += jumpHeight

        // ์œ„๋กœ
        move(to: upTransform, relativeTo: parent, duration: 0.5, timingFunction: .easeOut)

        // ์ž ์‹œ ํ›„ ์•„๋ž˜๋กœ
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            guard let self else { return }
            var downTransform = self.transform
            downTransform.translation.y = originalY
            self.move(to: downTransform, relativeTo: parent, duration: 0.5, timingFunction: .easeIn)
        }
    }
}

โš™๏ธ 4. ๋ฌผ๋ฆฌ ์—”์ง„

ํ˜„์‹ค์ ์ธ ๋ฌผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ is added.

Physics.swift โ€” Physics Simulation
import RealityKit

class PhysicsManager {
    // 1. ๋™์  ๋ฌผ๋ฆฌ (์ค‘๋ ฅ ์ ์šฉ)
    func addDynamicPhysics(to entity: Entity) {
        // ์ถฉ๋Œ ํ˜•ํƒœ ์ƒ์„ฑ
        entity.generateCollisionShapes(recursive: true)

        // ๋™์  ๋ฌผ๋ฆฌ ๋ฐ”๋”” (์ค‘๋ ฅ ์ ์šฉ)
        entity.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
            massProperties: .default,
            mode: .dynamic
        )
    }

    // 2. ์ •์  ๋ฌผ๋ฆฌ (๊ณ ์ •๋œ ๊ฐ์ฒด)
    func addStaticPhysics(to entity: Entity) {
        entity.generateCollisionShapes(recursive: true)

        // ์ •์  ๋ฌผ๋ฆฌ ๋ฐ”๋”” (์›€์ง์ด์ง€ ์•Š์Œ)
        entity.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
            mode: .static
        )
    }

    // 3. ์ปค์Šคํ…€ ์งˆ๋Ÿ‰๊ณผ ๋งˆ์ฐฐ๋ ฅ
    func createCustomPhysicsBody() -> PhysicsBodyComponent {
        // ์งˆ๋Ÿ‰ ์†์„ฑ
        let mass: Float = 2.0
        let massProperties = PhysicsMassProperties(mass: mass)

        // ๋ฌผ๋ฆฌ ๋จธํ‹ฐ๋ฆฌ์–ผ (๋งˆ์ฐฐ, ํƒ„์„ฑ)
        let physicsMaterial = PhysicsMaterialResource.generate(
            friction: .init(staticFriction: 0.5, dynamicFriction: 0.3),
            restitution: 0.8 // ํƒ„์„ฑ (0 = ํก์ˆ˜, 1 = ์™„์ „ ๋ฐ˜๋ฐœ)
        )

        return PhysicsBodyComponent(
            massProperties: massProperties,
            material: physicsMaterial,
            mode: .dynamic
        )
    }

    // 4. ํž˜ ์ ์šฉ
    func applyForce(to entity: Entity, force: SIMD3<Float>) {
        guard var physicsBody = entity.components[PhysicsBodyComponent.self] else { return }

        // ํž˜ ์ ์šฉ
        physicsBody.applyImpulse(force, relativeTo: nil)
        entity.components[PhysicsBodyComponent.self] = physicsBody
    }

    // 5. ์ถฉ๋Œ ๊ฐ์ง€
    func setupCollisionDetection(for entity: Entity) {
        // ์ถฉ๋Œ ๊ทธ๋ฃน ์„ค์ •
        entity.components[CollisionComponent.self] = CollisionComponent(
            shapes: [.generateBox(size: [0.1, 0.1, 0.1])],
            mode: .trigger,
            filter: .init(group: .all, mask: .all)
        )
    }
}

๐Ÿ”Š 5. ๊ณต๊ฐ„ ์˜ค๋””์˜ค

3D ๊ณต๊ฐ„์—์„œ ์‚ฌ์‹ค์ ์ธ ์˜ค๋””์˜ค๋ฅผ ์žฌ์ƒ.

SpatialAudio.swift โ€” ๊ณต๊ฐ„ ์˜ค๋””์˜ค
import RealityKit

class SpatialAudioManager {
    // ๊ณต๊ฐ„ ์˜ค๋””์˜ค ์ถ”๊ฐ€
    func addSpatialAudio(to entity: Entity, audioFile: String) async {
        // ์˜ค๋””์˜ค ๋ฆฌ์†Œ์Šค ๋กœ๋“œ
        guard let audioResource = try? await AudioFileResource(named: audioFile) else {
            print("์˜ค๋””์˜ค ๋กœ๋“œ ์‹คํŒจ")
            return
        }

        // ๊ณต๊ฐ„ ์˜ค๋””์˜ค ์ปจํŠธ๋กค๋Ÿฌ
        let audioController = entity.prepareAudio(audioResource)

        // ์žฌ์ƒ ์˜ต์…˜
        audioController.gain = 1.0
        audioController.fade(to: .max, duration: 1.0)

        // ์žฌ์ƒ
        audioController.play()
    }

    // ๋ฐ˜๋ณต ์žฌ์ƒ
    func playLoopingAudio(entity: Entity, audioFile: String) async {
        guard let resource = try? await AudioFileResource(named: audioFile) else { return }

        let controller = entity.playAudio(resource)
        controller.isLooping = true
    }
}

๐Ÿ“ฑ SwiftUI Integration Example

RealityKitDemoView.swift โ€” Complete Demo
import SwiftUI
import RealityKit

struct RealityKitDemoView: View {
    @State private var selectedShape: Shape = .box

    enum Shape: String, CaseIterable {
        case box = "๋ฐ•์Šค"
        case sphere = "๊ตฌ"
        case cylinder = "์›๊ธฐ๋‘ฅ"
    }

    var body: some View {
        VStack {
            // 3D ๋ทฐ
            RealityView { content in
                // ์ดˆ๊ธฐ ์”ฌ ์„ค์ •
                setupScene(content)
            } update: { content in
                // ์„ ํƒ๋œ ๋„ํ˜•์œผ๋กœ ์—…๋ฐ์ดํŠธ
                updateShape(content, shape: selectedShape)
            }

            // ์ปจํŠธ๋กค
            Picker("๋„ํ˜• ์„ ํƒ", selection: $selectedShape) {
                ForEach(Shape.allCases, id: \.self) { shape in
                    Text(shape.rawValue).tag(shape)
                }
            }
            .pickerStyle(.segmented)
            .padding()
        }
    }

    func setupScene(_ content: RealityViewContent) {
        // ์กฐ๋ช… ์ถ”๊ฐ€
        let light = PointLight()
        light.position = [0, 0.5, 0]
        light.light.intensity = 1000
        content.add(light)

        // ์ดˆ๊ธฐ ๋„ํ˜•
        let entity = createShape(.box)
        content.add(entity)
    }

    func updateShape(_ content: RealityViewContent, shape: Shape) {
        // ๊ธฐ์กด ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ
        content.entities.forEach { entity in
            if entity is ModelEntity {
                content.remove(entity)
            }
        }

        // ์ƒˆ ๋„ํ˜• ์ถ”๊ฐ€
        let entity = createShape(shape)
        content.add(entity)
    }

    func createShape(_ shape: Shape) -> ModelEntity {
        let mesh: MeshResource

        switch shape {
        case .box:
            mesh = .generateBox(size: 0.2)
        case .sphere:
            mesh = .generateSphere(radius: 0.1)
        case .cylinder:
            mesh = .generateCylinder(height: 0.2, radius: 0.1)
        }

        var material = PhysicallyBasedMaterial()
        material.baseColor = .init(tint: .blue)
        material.metallic = 0.8
        material.roughness = 0.2

        let entity = ModelEntity(mesh: mesh, materials: [material])
        entity.position = [0, 0, -0.5]

        // ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜
        entity.addRotationAnimation()

        return entity
    }
}

๐Ÿ’ก HIG Guidelines

๐ŸŽฏ Practical Usage

๐Ÿ“š Learn More

โšก๏ธ Reality Composer: Apple's tool for designing AR scenes without code. Load .rcproject files created in Reality Composer directly into RealityKit.

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐ŸŽฌ WWDC Sessions