๐ฒ 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
- ์ฑ๋ฅ: Optimize complex models with LOD (Level of Detail)
- ์กฐ๋ช : ํ์ค์ ์ธ ์กฐ๋ช ์ผ๋ก ๊น์ด๊ฐ ์ ๊ณต
- ์ค์ผ์ผ: ์ค์ ํฌ๊ธฐ์ ๋ง๋ ์ค์ผ์ผ ์ฌ์ฉ
- ๋ฌผ๋ฆฌ: ๊ณผ๋ํ ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์ ์ ์ฑ๋ฅ ์ ํ
- ํ ์ค์ฒ: ์์ถ๋ ํ ์ค์ฒ ์ฌ์ฉ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ
๐ฏ Practical Usage
- ์ ํ ์๊ฐํ: 3D ์ ํ ๋ทฐ์ด
- AR Games: ๋ฌผ๋ฆฌ ๊ธฐ๋ฐ ๊ฒ์ ๋ฉ์ปค๋
- ๊ฑด์ถ ์๊ฐํ: ๊ฑด๋ฌผ ๋ด๋ถ ํ์
- ๊ต์ก: 3D ๋ชจ๋ธ๋ก ํ์ต
- visionOS ์ฑ: ๊ณต๊ฐ ์ปดํจํ ๊ฒฝํ
๐ Learn More
โก๏ธ Reality Composer: Apple's tool for designing AR scenes without code. Load .rcproject files created in Reality Composer directly into RealityKit.