3D World & Physics
Swift ์ธ์ด๋ก ๊ตฌํํ๋ 3D ๊ทธ๋ํฝ์ค. ์ฑ ์์์ ๊ณต๊ฐ์ ์ด์ด๋ณด์ธ์.
๊ฐ์
์ด ๋ชจ๋์ SwiftUI ์์ ๋ ๋๋ง ๋ฃจํ๋ฅผ ๊ฐ์ง ์์ ๋ทฐ๋ฅผ ์น๋ ์ธ ๊ฐ์ง ํจํด์ ํ ๊ณณ์์ ๋น๊ตํฉ๋๋ค.
| Framework | ํตํฉ ๋ฐฉ๋ฒ | ๋ค๋ฃจ๋ ๋์ |
|---|---|---|
| SpriteKit | SpriteView(scene:) |
2D ๋ ธ๋ + ๋ฌผ๋ฆฌ |
| SceneKit | SceneView(scene:) |
3D ์ฌ ๊ทธ๋ํ |
CoreAnimation (CAEmitterLayer) |
UIViewRepresentable |
2D ํํฐํด |
์ธ ํ๋ ์์ํฌ์ ๊ณตํต์ ์ ์ฌ์ ํ ๋ฒ ์์ฑํด ์ฌ์ฌ์ฉํ๊ณ , SwiftUI๋ ๊ทธ ์ฌ์ ํ๋ฉด์ ๋ด๋ ์ปจํ
์ด๋ ์ญํ ๋ง ํ๋ค๋ ๊ฒ์
๋๋ค. SwiftUI์ @State๋ก ์ฌ์ ๋ณด์ ํ๋ฉด ๋ทฐ๊ฐ ๋ค์ ๊ทธ๋ ค์ ธ๋ ๊ฐ์ ์ฌ์ ์ ์งํ ์ ์์ต๋๋ค.
@State private var scene: GravityScene = {
let scene = GravityScene()
scene.scaleMode = .resizeFill
return scene
}()
var body: some View {
SpriteView(scene: scene)
}
์ด ๋ชจ๋์ 4๊ฐ ๋ฐ๋ชจ๋ ์ ์ธ ํ๋ ์์ํฌ์ ๋ํ์ ํ์ฉ์ ํ๋์ฉ ๋ณด์ฌ์ค๋๋ค.
1. Gravity Balls (SpriteKit)
๋ฌด์์ ๋ฐฐ์ฐ๋ โ
SKPhysicsBody์physicsWorld.gravity๋ฅผ ์ด์ฉํ 2D ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์ , ๊ทธ๋ฆฌ๊ณCoreMotion์ผ๋ก ๋๋ฐ์ด์ค ๊ธฐ์ธ๊ธฐ์ ๋ฐ๋ผ ์ค๋ ฅ ๋ฐฉํฅ์ ํ์ ์ํค๋ ํตํฉ.
final class GravityScene: SKScene {
override func didMove(to view: SKView) {
// ํ๋ฉด ๊ฒฝ๊ณ๋ฅผ ์ถฉ๋ ๋ฒฝ์ผ๋ก
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let ball = SKShapeNode(circleOfRadius: 20)
ball.position = t.location(in: self)
let body = SKPhysicsBody(circleOfRadius: 20)
body.restitution = 0.7 // ํ์ฑ
ball.physicsBody = body
addChild(ball)
}
}
CoreMotion ํตํฉ โ ๋๋ฐ์ด์ค ์ขํ โ ํ๋ฉด ์ขํ ๋งคํ์ด ํต์ฌ์ ๋๋ค. SpriteKit์ lower-left origin์ด๋ผ ์ธํฐํ์ด์ค ํ์ ์ ๋ฐ๋ผ ๋ณํ์ด ๋ฌ๋ผ์ง๋๋ค.
motionManager.startAccelerometerUpdates(to: .main) { data, _ in
guard let d = data else { return }
let v = OrientationAwareGravity.spriteKitVector(
deviceX: d.acceleration.x,
deviceY: d.acceleration.y
)
self.physicsWorld.gravity = CGVector(dx: v.dx * 9.8, dy: v.dy * 9.8)
}
2. Solar System (SceneKit)
๋ฌด์์ ๋ฐฐ์ฐ๋ โ
SCNScene/SCNNode์ ๊ณ์ธต์ ํธ๋์คํผ์ผ๋ก ๋ถ๋ชจ-์์ ๊ด๊ณ๋ฅผ ๋ง๋ค๊ณ ,SCNAction์ผ๋ก ๊ณต์ ยท์์ ์ ํํํ๋ ํจํด.
let scene = SCNScene()
// ํ์
let sun = SCNNode(geometry: SCNSphere(radius: 1.0))
sun.geometry?.firstMaterial?.diffuse.contents = UIColor.yellow
scene.rootNode.addChildNode(sun)
// ์ง๊ตฌ์ "๊ณต์ ์ถ" โ ํ์์ ๋ถ์ฐฉ๋ ๋น ๋
ธ๋
let earthOrbit = SCNNode()
sun.addChildNode(earthOrbit)
// ์ง๊ตฌ ๋ณธ์ฒด โ ๊ณต์ ์ถ์ ์์
let earth = SCNNode(geometry: SCNSphere(radius: 0.3))
earth.position = SCNVector3(3, 0, 0) // ๊ณต์ ์ถ์์ ๊ฑฐ๋ฆฌ 3
earthOrbit.addChildNode(earth)
// ๊ณต์ ์ถ์ด ํ์ โ ์ง๊ตฌ๊ฐ ํ์ ์ฃผ๋ณ์ ๋๋ค
let orbit = SCNAction.rotateBy(x: 0, y: .pi * 2, z: 0, duration: 6)
earthOrbit.runAction(.repeatForever(orbit))
// ์์
let spin = SCNAction.rotateBy(x: 0, y: .pi * 2, z: 0, duration: 1)
earth.runAction(.repeatForever(spin))
ํต์ฌ ๊ฐ๋
: ๋น ๋
ธ๋(SCNNode())๋ฅผ ํ์ ์ถ์ผ๋ก ์ฌ์ฉํด, ์์ ๋
ธ๋์ ์์น๋ฅผ ์ฎ๊ธฐ๋ฉด ๊ทธ ๊ฑฐ๋ฆฌ์์ ๊ณต์ ์ด ๋ฉ๋๋ค. ๋ฌ๋ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ง๊ตฌ์ ์์ ๋
ธ๋ โ ์ง๊ตฌ ์์ฒด๊ฐ ์์ง์ฌ๋ ๋ฌ์ ๋ฐ๋ผ์ต๋๋ค.
3. Weather (CAEmitterLayer)
๋ฌด์์ ๋ฐฐ์ฐ๋ โ
CAEmitterLayer+CAEmitterCell๋ก GPU ๊ฐ์ ํํฐํด์ ์ค์ ๊ธฐ๋ฐ์ผ๋ก ๋ง๋๋ ํจํด.
let emitter = CAEmitterLayer()
emitter.emitterShape = .line
emitter.emitterPosition = CGPoint(x: bounds.midX, y: 0)
emitter.emitterSize = CGSize(width: bounds.width, height: 1)
let snow = CAEmitterCell()
snow.contents = UIImage(named: "snowflake")?.cgImage
snow.birthRate = 30 // ์ด๋น 30๊ฐ
snow.lifetime = 8 // 8์ด ์ด์ ์์
snow.scale = 0.05
snow.scaleRange = 0.03
snow.velocity = 80
snow.velocityRange = 30
snow.yAcceleration = 20 // ๋จ์ด์ง๋ ๊ฐ์๋
snow.spinRange = .pi
snow.emissionRange = .pi / 8 // ์ด์ง ํ๋ค๋ฆผ
emitter.emitterCells = [snow]
view.layer.addSublayer(emitter)
๊ธฐ์ตํด์ผ ํ ๊ณต์:
ํ๋ฉด์ ํ๊ท ์ ์ผ๋ก ์กด์ฌํ๋ ํํฐํด ์ โ
birthRate ร lifetime
๋ฐ๋ชจ๋ segmented picker๋ก ๋ / ๋น / ๋ฒ๊ฝ ์ธ ๋ชจ๋๋ฅผ ์ ํํฉ๋๋ค. ๊ฐ์ API์ cell์ contentsยทvelocityยทyAcceleration๋ง ๋ฐ๊พธ๋ฉด ๋ค๋ฅธ ํจ๊ณผ๊ฐ ๋์ต๋๋ค.
4. Confetti (CAEmitterLayer)
๋ฌด์์ ๋ฐฐ์ฐ๋ โ ๋ฒํผ ํธ๋ฆฌ๊ฑฐ๋ก ์ผํ์ฑ ํญ๋ฐ์ ๋ง๋ค๊ณ , 3D ํ ๋ธ๋ง ํจ๊ณผ๋ฅผ ์ํด ํ์ ์์ฑ์ ์ค์ ํ๋ ํจํด.
func fire() {
cell.birthRate = 200 // ์ ๊น ํญ๋ฐ
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
cell.birthRate = 0 // ๋ ์ด์ ์์ฑ ์ ํจ (์ด๋ฏธ ๋ ์๋ ์
์๋ lifetime๊น์ง ์ ์ง)
}
}
cell.spin = 0
cell.spinRange = .pi * 4 // ๋ค์ํ ๊ฐ์๋๋ก ํ์ โ 3D ํ
๋ธ๋ง ๋๋
cell.color = UIColor.systemPink.cgColor
cell.redRange = 0.6 // ์์ ๋ณ์ด
cell.greenRange = 0.6
cell.blueRange = 0.6
continuous ๋ชจ๋ ํ ๊ธ์ birthRate๋ฅผ ์ผ์ ๊ฐ์ผ๋ก ์ ์ง/0์ผ๋ก ํ ๊ธํ๋ ์ฐจ์ด์
๋๋ค.
ํ์ ํ
- ์ฌ์
@State1๋ฒ ์์ฑ โbody์์์ ๋งค๋ฒSKScene()๋ง๋ค๋ฉด ๋งค ํ๋ ์ ์๋ก ๋ง๋ค์ด์ง๋๋ค. ๋ฐ๋์@Stateํ๋กํผํฐ์ ํ ๋ฒ๋ง ๋ณด๊ด. - ์ขํ๊ณ ํจ์ :
- SpriteKit: ์ขํ๋จ ์์
- SceneKit: ์ฐ์ ์ขํ๊ณ (Y ์, Z ํ๋ฉด ์)
- CAEmitterLayer: UIKit๊ณผ ๋์ผํ ์ข์๋จ ์์
- ์ฑ๋ฅ ํ๊ณ:
CAEmitterLayerโ GPU์์ ์์ฒ ๊ฐ OK- SpriteKit โ ์๋ฐฑ ๊ฐ ๋ฌผ๋ฆฌ body๋ถํฐ CPU ๋ณ๋ชฉ ์์
- SceneKit โ ๋จ์ ์ฌ์ ๊ฐ๋ณ์ง๋ง ๊ทธ๋ฆผ์ยท๋ฐ์ฌ ์ผ๋ฉด ๋น ๋ฅด๊ฒ ๋ฌด๊ฑฐ์์ง
- ์๋ฎฌ๋ ์ดํฐ ํ๊ณ โ SceneKit์ Metal ์ ฐ์ด๋ ๋ชจ๋ํ์ด์ด๋ ์๋ฎฌ๋ ์ดํฐ์์ ์ ์ ๋ณด์ผ ์ ์์ต๋๋ค. ์ค๊ธฐ๊ธฐ ๊ฒ์ฆ ํ์.