๐ฒ RealityKit
์ฌ์ค์ ์ธ 3D ๋ ๋๋ง๊ณผ AR ๊ฒฝํ
iOS 13+visionOS ์ต์ ํ
โจ RealityKit์ด๋?
RealityKit์ Apple์ ๊ณ ์ฑ๋ฅ 3D ๋ ๋๋ง ์์ง์ผ๋ก, ARKit๊ณผ ํตํฉ๋์ด ์ฌ์ค์ ์ธ AR ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค. ๋ฌผ๋ฆฌ ์์ง, ์ ๋๋ฉ์ด์ , ๊ณต๊ฐ ์ค๋์ค, ์ฌ์ค์ ์ธ ๋ ๋๋ง ๋ฑ์ ์ง์ํ๋ฉฐ, SwiftUI์ ์๋ฒฝํ๊ฒ ํตํฉ๋ฉ๋๋ค.
๐ก ํต์ฌ ๊ธฐ๋ฅ: PBR ๋ ๋๋ง ยท ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์
ยท ์ ๋๋ฉ์ด์
ยท ๊ณต๊ฐ ์ค๋์ค ยท Entity Component System ยท USD ์ง์ ยท Reality Composer ยท visionOS ์ง์
๐ฏ 1. ๊ธฐ๋ณธ ์ฌ ์ค์
RealityKit์ผ๋ก 3D ์ฌ์ ์์ฑํฉ๋๋ค.
RealityKitView.swift โ ๊ธฐ๋ณธ ์ค์
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. ์ ๋๋ฉ์ด์
์ํฐํฐ์ ์์ง์์ ์ถ๊ฐํฉ๋๋ค.
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. ๋ฌผ๋ฆฌ ์์ง
ํ์ค์ ์ธ ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์ ์ ์ถ๊ฐํฉ๋๋ค.
Physics.swift โ ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์
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 ํตํฉ ์์
RealityKitDemoView.swift โ ์ข
ํฉ ๋ฐ๋ชจ
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 ๊ฐ์ด๋๋ผ์ธ
- ์ฑ๋ฅ: ๋ณต์กํ ๋ชจ๋ธ์ LOD(Level of Detail) ์ต์ ํ
- ์กฐ๋ช : ํ์ค์ ์ธ ์กฐ๋ช ์ผ๋ก ๊น์ด๊ฐ ์ ๊ณต
- ์ค์ผ์ผ: ์ค์ ํฌ๊ธฐ์ ๋ง๋ ์ค์ผ์ผ ์ฌ์ฉ
- ๋ฌผ๋ฆฌ: ๊ณผ๋ํ ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์ ์ ์ฑ๋ฅ ์ ํ
- ํ ์ค์ฒ: ์์ถ๋ ํ ์ค์ฒ ์ฌ์ฉ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ
๐ฏ ์ค์ ํ์ฉ
- ์ ํ ์๊ฐํ: 3D ์ ํ ๋ทฐ์ด
- AR ๊ฒ์: ๋ฌผ๋ฆฌ ๊ธฐ๋ฐ ๊ฒ์ ๋ฉ์ปค๋
- ๊ฑด์ถ ์๊ฐํ: ๊ฑด๋ฌผ ๋ด๋ถ ํ์
- ๊ต์ก: 3D ๋ชจ๋ธ๋ก ํ์ต
- visionOS ์ฑ: ๊ณต๊ฐ ์ปดํจํ ๊ฒฝํ
๐ ๋ ์์๋ณด๊ธฐ
โก๏ธ Reality Composer: ์ฝ๋ ์์ด AR ์ฌ์ ๋์์ธํ ์ ์๋ Apple์ ํด์
๋๋ค. Reality Composer๋ก ๋ง๋ .rcproject ํ์ผ์ RealityKit์์ ๋ฐ๋ก ๋ก๋ํ ์ ์์ต๋๋ค.