앱 개발자로서 우리는 단순한 코더가 아닙니다. 우리는 창작자, 빌더, 때로는 마술사이기도 합니다. 앱 개발 기술은 코드와 디자인 그 이상입니다. 때로는 사용자의 관심을 끌고 몰입형 경험을 만들어내는 놀라움과 환상의 요소를 만드는 것이 중요합니다. 이번에 우리는 2D 세계의 안락한 영역에서 벗어나 매혹적인 3D 세계로 대담하게 도약하고 있습니다.
UIKit은 사용자 인터페이스를 구축하기 위한 도구 세트 그 이상입니다. 올바르게 사용하면 놀라운 시각 효과를 만들어낼 수 있는 강력한 툴킷입니다. 이 기사에서는 UIKit에 대해 자세히 알아보고 거울과 같은 반사를 만드는 기술을 선보일 것입니다. 이 효과는 일반적으로 복잡한 그래픽 도구를 통해서만 달성할 수 있는 것처럼 보이지만 코드만으로 제작되는 시각적으로 인상적이고 매력적인 앱 모양을 제공할 수 있습니다.
이 아름답고 반짝이는 큐브를 확인해 보세요. 금속을 사용하지 않기 때문에 절대 녹슬지 않습니다.
이제 코드를 사용하여 생성하는 방법을 알아 보겠습니다.
우리의 목적을 위해 UIKit은 Quartz Core 위에 얇은 레이어 역할을 하여 3D 기능에 무료로 액세스할 수 있도록 해줍니다. UIView
OS가 화면 렌더링에 사용하는 실제 구성 요소인 CALayer
개체에 대한 참조를 보유합니다. 화면 표시에 영향을 미치는 CALayer
의 세 가지 속성은 위치, 경계 및 변환입니다. 처음 두 개는 설명이 매우 간단하지만 transform
임의의 4x4 행렬로 초기화될 수 있습니다. 여러 3D 레이어를 동시에 표시해야 하는 경우 하위 레이어를 2D 평면으로 병합하는 대신 하위 레이어의 3D 공간을 고유하게 보존하는 특수 CATransformLayer를 사용해야 합니다.
간단한 큐브를 그리는 것부터 시작해 보겠습니다. 먼저 각 측면의 위치를 조정하는 도우미 함수를 만듭니다.
func setupFace( layer: CALayer, size: CGFloat, baseTransform: CATransform3D, translation: (x: CGFloat, y: CGFloat, z: CGFloat), rotation: (angle: CGFloat, x: CGFloat, y: CGFloat, z: CGFloat) ) { layer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)) var transform = baseTransform transform = CATransform3DTranslate(transform, translation.x, translation.y, translation.z) transform = CATransform3DRotate(transform, rotation.angle, rotation.x, rotation.y, rotation.z) layer.transform = transform }
다음으로 ViewController의 viewDidLoad
함수 본문에서 큐브의 6개 측면을 모두 조립합니다.
let cubeLayer = CATransformLayer() cubeLayer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY) view.layer.addSublayer(cubeLayer) let cubeSize: CGFloat = 200.0 var baseTransform = CATransform3DIdentity baseTransform = CATransform3DRotate(baseTransform, 0.5, 0.0, 1.0, 0.0) baseTransform = CATransform3DRotate(baseTransform, -0.5, 1.0, 0.0, 0.0) let frontFace = CALayer() frontFace.isDoubleSided = false frontFace.backgroundColor = UIColor.blue.cgColor setupFace(layer: frontFace, size: cubeSize, baseTransform: baseTransform, translation: (0.0, 0.0, cubeSize * 0.5), rotation: (0.0, 0.0, 1.0, 0.0)) cubeLayer.addSublayer(frontFace) let backFace = CALayer() backFace.isDoubleSided = false backFace.backgroundColor = UIColor.red.cgColor setupFace(layer: backFace, size: cubeSize, baseTransform: baseTransform, translation: (0.0, 0.0, -cubeSize * 0.5), rotation: (-.pi, 0.0, 1.0, 0.0)) cubeLayer.addSublayer(backFace) let leftFace = CALayer() leftFace.isDoubleSided = false leftFace.backgroundColor = UIColor.green.cgColor setupFace(layer: leftFace, size: cubeSize, baseTransform: baseTransform, translation: (-cubeSize * 0.5, 0.0, 0.0), rotation: (-.pi * 0.5, 0.0, 1.0, 0.0)) cubeLayer.addSublayer(leftFace) let rightFace = CALayer() rightFace.isDoubleSided = false rightFace.backgroundColor = UIColor.yellow.cgColor setupFace(layer: rightFace, size: cubeSize, baseTransform: baseTransform, translation: (cubeSize * 0.5, 0.0, 0.0), rotation: (.pi * 0.5, 0.0, 1.0, 0.0)) cubeLayer.addSublayer(rightFace) let topFace = CALayer() topFace.isDoubleSided = false topFace.backgroundColor = UIColor.cyan.cgColor setupFace(layer: topFace, size: cubeSize, baseTransform: baseTransform, translation: (0.0, -cubeSize * 0.5, 0.0), rotation: (.pi * 0.5, 1.0, 0.0, 0.0)) cubeLayer.addSublayer(topFace) let bottomFace = CALayer() bottomFace.isDoubleSided = false bottomFace.backgroundColor = UIColor.gray.cgColor setupFace(layer: bottomFace, size: cubeSize, baseTransform: baseTransform, translation: (0.0, cubeSize * 0.5, 0.0), rotation: (-.pi * 0.5, 1.0, 0.0, 0.0)) cubeLayer.addSublayer(bottomFace)
이 코드의 실제 모습은 다음과 같습니다.
확실히 3D이긴 한데 뭔가 기분이 이상하지 않나요? 예술에서 3D 원근법의 개념은 15세기 이탈리아 르네상스 화가들이 처음으로 마스터했습니다. 다행히도 원근 투영 행렬을 사용하여 비슷한 효과를 얻을 수 있습니다.
var baseTransform = CATransform3DIdentity baseTransform.m34 = -1.0 / 400.0 baseTransform = CATransform3DRotate(baseTransform, 0.5, 0.0, 1.0, 0.0) baseTransform = CATransform3DRotate(baseTransform, -0.5, 1.0, 0.0, 0.0)
더 좋지 않나요? m34의 -1.0 / 400.0
항은 원근 효과를 생성하는 것입니다. 실제 수학은 https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html 을 참조하세요.
우리의 목표는 거울 효과를 보여주는 것이므로 반사할 무언가가 필요합니다. 3D 그래픽에서 큐브 맵은 일반적으로 반사 표면을 시뮬레이션하는 데 사용됩니다. 이 예에서는 이전에 만든 실제 큐브를 사용하여 하나를 만들 수 있습니다. 먼저 해당 얼굴에 이미지를 할당합니다.
frontFace.contents = UIImage(named: "front")?.cgImage backFace.contents = UIImage(named: "back")?.cgImage leftFace.contents = UIImage(named: "left")?.cgImage rightFace.contents = UIImage(named: "right")?.cgImage topFace.contents = UIImage(named: "up")?.cgImage bottomFace.contents = UIImage(named: "down")?.cgImage
다음으로, 모든 면에 대해 isDoubleSided = true
설정하고 큐브 크기를 cubeSize: CGFloat = 2000.0
으로 늘립니다. 이는 본질적으로 큐브 내부에 "카메라"를 배치합니다.
다음으로, 한 번에 여러 개의 큐브를 생성할 것이므로 설정 기능을 단순화해 보겠습니다.
enum CubeFace: CaseIterable { case front case back case left case right case top case bottom func translationAndRotation(size: CGFloat) -> (translation: (x: CGFloat, y: CGFloat, z: CGFloat), rotation: (angle: CGFloat, x: CGFloat, y: CGFloat, z: CGFloat)) { switch self { case .front: return ((0.0, 0.0, size * 0.5), (0.0, 0.0, 1.0, 0.0)) case .back: return ((0.0, 0.0, -size * 0.5), (-.pi, 0.0, 1.0, 0.0)) case .left: return ((-size * 0.5, 0.0, 0.0), (-.pi * 0.5, 0.0, 1.0, 0.0)) case .right: return ((size * 0.5, 0.0, 0.0), (.pi * 0.5, 0.0, 1.0, 0.0)) case .top: return ((0.0, -size * 0.5, 0.0), (.pi * 0.5, 1.0, 0.0, 0.0)) case .bottom: return ((0.0, size * 0.5, 0.0), (-.pi * 0.5, 1.0, 0.0, 0.0)) } } func texture() -> UIImage? { ... } func color() -> UIColor { ... } } func setupFace( layer: CALayer, size: CGFloat, baseTransform: CATransform3D, face: CubeFace, textured: Bool ) { layer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: size, height: size)) layer.isDoubleSided = textured let (translation, rotation) = face.translationAndRotation(size: size) var transform = baseTransform transform = CATransform3DTranslate(transform, translation.x, translation.y, translation.z) transform = CATransform3DRotate(transform, rotation.angle, rotation.x, rotation.y, rotation.z) layer.transform = transform if textured { layer.contents = face.texture()?.cgImage } else { layer.backgroundColor = face.color().cgColor } } func setupCube( view: UIView, size: CGFloat, textured: Bool, baseTransform: CATransform3D, faces: [CubeFace] ) -> CATransformLayer { let cubeLayer = CATransformLayer() cubeLayer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY) for face in faces { let faceLayer = CALayer() setupFace(layer: faceLayer, size: size, baseTransform: baseTransform, face: face, textured: textured) cubeLayer.addSublayer(faceLayer) } return cubeLayer }
이제 큐브 맵과 작은 큐브를 동시에 렌더링해 보겠습니다.
var baseTransform = CATransform3DIdentity baseTransform.m34 = -1.0 / 400.0 baseTransform = CATransform3DRotate(baseTransform, 0.5, 0.0, 1.0, 0.0) view.layer.addSublayer(setupCube(view: view, size: 2000.0, textured: true, baseTransform: baseTransform)) view.layer.addSublayer(setupCube(view: view, size: 100.0, textured: false, baseTransform: baseTransform))
UIKit은 강력한 프레임워크이지만 복잡한 시각 효과를 위한 내장 기능이 부족합니다. 그러나 객체에 임의의 마스크를 적용할 수 있는 기능을 제공하며 이것이 바로 우리가 거울 효과를 생성하기 위해 활용하려는 것입니다. 기본적으로 환경을 6번 렌더링하며 각각 해당 큐브 면으로 마스크됩니다.
까다로운 측면은 CATransformLayer
를 직접 마스킹할 수 없다는 것입니다. 그러나 CALayer
컨테이너 내에 중첩하여 이 제한을 피할 수 있습니다.
func setupReflectiveFace( view: UIView, size: CGFloat, baseTransform: CATransform3D, face: CubeFace ) -> CALayer { let maskLayer = CALayer() maskLayer.frame = view.bounds maskLayer.addSublayer(setupCube(view: view, size: size, textured: false, baseTransform: baseTransform, faces: [face])) let colorLayer = CALayer() colorLayer.frame = view.bounds colorLayer.mask = maskLayer colorLayer.addSublayer(setupCube(view: view, size: 2000.0, textured: true, baseTransform: baseTransform, faces: [.front, .back, .left, .right, .top, .bottom])) return colorLayer }
이제 viewDidLoad는 다음과 같아야 합니다.
var baseTransform = CATransform3DIdentity baseTransform.m34 = -1.0 / 400.0 baseTransform = CATransform3DRotate(baseTransform, 0.5, 0.0, 1.0, 0.0) for face in CubeFace.allCases { view.layer.addSublayer(setupReflectiveFace(view: view, size: 100.0, baseTransform: baseTransform, face: face)) }
이 이미지는 이미 우리가 달성하려는 것과 매우 유사하지만 이 시점에서 큐브는 큐브 맵 위의 3D와 같은 마스크일 뿐입니다. 그렇다면 어떻게 실제 거울로 변환할 수 있을까요?
3D 공간의 임의의 평면을 기준으로 세계를 미러링하는 간단한 방법이 있다는 것이 밝혀졌습니다. 복잡한 수학을 탐구하지 않고 이것이 우리가 찾고 있는 행렬입니다.
func mirrorMatrix(planePoint: Vector4D, planeTransform: CATransform3D, planeNormal: Vector4D) -> CATransform3D { let pt = applyTransform(transform: planeTransform, point: planePoint) let normalTransform = CATransform3DInvert(planeTransform).transposed let normal = applyTransform(transform: normalTransform, point: planeNormal).normalized() let a = normal.x let b = normal.y let c = normal.z let d = -(a * pt.x + b * pt.y + c * pt.z) return CATransform3D([ 1 - 2 * a * a, -2 * a * b, -2 * a * c, -2 * a * d, -2 * a * b, 1 - 2 * b * b, -2 * b * c, -2 * b * d, -2 * a * c, -2 * b * c, 1 - 2 * c * c, -2 * c * d, 0.0, 0.0, 0.0, 1.0 ]).transposed }
다음으로 다음 코드를 큐브 설정 함수에 통합합니다.
func setupCube( view: UIView, size: CGFloat, textured: Bool, baseTransform: CATransform3D, faces: [CubeFace], mirrorFace: CubeFace? = nil ) -> CATransformLayer { ... if let mirrorFace { let mirrorPlane = mirrorFace.transform(size: size, baseTransform: baseTransform) let mirror = mirrorMatrix(planePoint: Vector4D(x: 0.0, y: 0.0, z: 0.0, w: 1.0), planeTransform: mirrorPlane, planeNormal: Vector4D(x: 0.0, y: 0.0, z: 1.0, w: 1.0)) cubeLayer.sublayerTransform = mirror } }
그리고 마지막으로, 우리가 그동안 노력해왔던 반짝이는 큐브를 볼 수 있습니다.
물론, Metal이나 SceneKit과 같은 Metal 기반 프레임워크를 사용하면 동일한 효과를 얻는 것이 더 쉬워 보일 수도 있습니다. 그러나 여기에는 고유한 한계가 있습니다. 큰 것? Metal로 그린 3D 컨텐츠에 라이브 UIKit 뷰를 가져올 수 없습니다.
이 문서에서 살펴본 방법을 사용하면 3D 설정에서 모든 종류의 콘텐츠를 표시할 수 있습니다. 여기에는 지도, 비디오, 대화형 보기가 포함됩니다. 또한 사용하려는 UIKit 애니메이션과 원활하게 혼합될 수 있습니다.
이 기사의 소스 코드는 일부 도우미 기능과 함께 https://github.com/petertechstories/uikit-mirrors 에서 찾을 수 있습니다.
즐거운 코딩하세요!