Uygulama geliştiricileri olarak biz sadece kodlayıcı değiliz; yaratıcıyız, geliştiriciyiz ve bazen de illüzyonistiz. Uygulama geliştirme sanatı yalnızca kod ve tasarımın ötesine geçer. Bazen bu, kullanıcıların dikkatini çeken ve sürükleyici bir deneyim yaratan bir sürpriz ve yanılsama unsuru yaratmakla ilgilidir. Bu sefer 2D dünyasının konfor alanımızın dışına çıkıyoruz ve 3D'nin büyüleyici dünyasına cesur bir adım atıyoruz.
UIKit, kullanıcı arayüzleri oluşturmaya yönelik bir dizi araçtan daha fazlasıdır. Doğru kullanıldığında muhteşem görsel efektler yaratabilen güçlü bir araç setidir. Bu makalede UIKit'in derinliklerine ineceğiz ve aynaya benzer bir yansıma yaratma tekniğini sergileyeceğiz. Bu efekt, uygulamanıza genellikle yalnızca karmaşık grafik araçlarıyla elde edilebilecek gibi görünen görsel olarak etkileyici ve ilgi çekici bir görünüm verebilir, ancak koddan başka hiçbir şey kullanılmadan hazırlanmıştır.
Bu güzel, parlak küpü inceleyin. Metal kullanılmadığı için asla paslanmaz.
Şimdi kod kullanarak nasıl oluşturulacağını öğrenelim.
Amacımız açısından UIKit, Quartz Core'un üzerinde ince bir katman görevi görerek bize 3D özelliklerine ücretsiz erişim sağlıyor. Bir UIView
işletim sisteminin ekran görüntüsü oluşturmak için kullandığı gerçek bileşen olan CALayer
nesnesine bir referans içerir. CALayer
ekrandaki sunumunu etkileyen üç özelliği vardır: konum, sınırlar ve dönüşüm. İlk ikisi oldukça açıklayıcıdır, transform
ise herhangi bir 4x4 matris ile başlatılabilir. Birden fazla 3 boyutlu katmanın aynı anda sunulması gerektiğinde, alt katmanlarını 2 boyutlu bir düzlem üzerine düzleştirmek yerine 3 boyutlu alanını benzersiz şekilde koruyan özel bir CATransformLayer kullanmamız gerekir.
Basit bir küp çizerek başlayalım. Öncelikle her iki tarafın konumunu ayarlamak için bir yardımcı fonksiyon oluşturacağız:
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 }
Daha sonra ViewController'ın viewDidLoad
fonksiyonunun gövdesinde küpün altı tarafını da birleştireceğiz:
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)
İşte bu kod çalışırken nasıl görünüyor:
İnkar edilemez bir şekilde 3 boyutlu, ama bir şeyler kötü hissettiriyor, değil mi? Sanatta 3 boyutlu perspektif kavramına ilk kez 15. yüzyılda İtalyan Rönesans ressamları hakim oldu. Neyse ki benzer bir etkiyi yalnızca perspektif projeksiyon matrisi kullanarak elde edebiliriz:
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)
Daha iyi, değil mi? Perspektif etkisini yaratan m34'teki -1.0 / 400.0
terimidir. Gerçek matematik için bkz. https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html
Amacımız bir ayna etkisi göstermek, dolayısıyla yansıtacak bir şeye ihtiyacımız olacak. 3 boyutlu grafiklerde yansıtıcı yüzeyleri simüle etmek için küp haritaları yaygın olarak kullanılır. Örneğimizde, daha önce oluşturduğumuz küpü kullanarak bir tane oluşturabiliriz. Öncelikle ilgili yüzlere görseller atarız:
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
Daha sonra, her yüz için isDoubleSided = true
ayarını yapıyoruz ve küpün boyutunu cubeSize: CGFloat = 2000.0
değerine artırıyoruz. Bu aslında "kamerayı" küpün içine yerleştirir:
Daha sonra, aynı anda birkaç küp oluşturacağımız için kurulum işlevlerini basitleştirelim:
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 }
Şimdi hem küp haritasını hem de küçük bir küpü aynı anda oluşturalım:
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 sağlam bir çerçevedir ancak karmaşık görsel efektler için yerleşik özelliklerden yoksundur. Ancak nesnelere isteğe bağlı maskeler uygulama yeteneği sunuyor ve ayna efektini yaratmak için tam olarak bundan yararlanacağız. Temel olarak, ortamı her biri karşılık gelen küp yüzüyle maskelenen altı kez oluşturacağız.
İşin zor yanı, bir CATransformLayer
doğrudan maskeleyemememizdir. Ancak bu sınırlamayı CALayer
konteynerinin içine yerleştirerek aşabiliriz:
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 }
Ve şimdi viewDidLoad'umuz şöyle görünmeli:
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)) }
Bu görüntü zaten elde etmek istediğimiz şeye çok benziyor, ancak bu noktada küp yalnızca küp haritası üzerinde 3 boyutlu bir maskedir. Peki onu gerçek bir aynaya nasıl dönüştürebiliriz?
Dünyayı 3 boyutlu uzayda rastgele bir düzleme göre yansıtmanın basit bir yöntemi olduğu ortaya çıktı. Karmaşık matematiğe dalmadan aradığımız matris budur:
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 }
Daha sonra aşağıdaki kodu küp kurulum fonksiyonuna dahil ediyoruz:
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 } }
Ve sonunda, uğruna çabaladığımız parlak küpü görebiliyoruz:
Elbette aynı etkiyi Metal veya SceneKit gibi Metal tabanlı bir çerçeveyle elde etmek daha kolay görünebilir. Ancak bunların kendi sınırları vardır. Büyük olan? Canlı UIKit görünümlerini Metal tarafından çizilen 3D içeriğe taşıyamazsınız.
Bu makalede incelediğimiz yöntem, her türlü içeriği 3D ortamda görüntülememize olanak tanıyor. Buna haritalar, videolar ve etkileşimli görünümler dahildir. Ayrıca, kullanmak isteyebileceğiniz tüm UIKit animasyonlarıyla sorunsuz bir şekilde harmanlanabilir.
Bu makalenin kaynak kodu ve bazı yardımcı işlevler https://github.com/petertechstories/uikit-mirrors adresinde bulunabilir.
Mutlu kodlama!