En tant que développeurs d'applications, nous ne sommes pas seulement des codeurs - nous sommes des créateurs, des constructeurs et parfois des illusionnistes. L'art du développement d'applications va au-delà du simple code et de la conception. Parfois, il s'agit de créer un élément de surprise et d'illusion qui capte l'attention des utilisateurs et crée une expérience immersive. Cette fois-ci, nous sortons de notre zone de confort du monde 2D et faisons un saut audacieux dans le monde captivant de la 3D .
UIKit est plus qu'un ensemble d'outils pour créer des interfaces utilisateur. C'est une boîte à outils puissante qui, lorsqu'elle est utilisée correctement, peut créer des effets visuels étonnants. Dans cet article, nous allons approfondir UIKit et présenter une technique pour créer une réflexion semblable à un miroir. Cet effet peut donner à votre application un aspect visuellement impressionnant et attrayant qui ne semble généralement réalisable qu'avec des outils graphiques complexes, mais qui n'est conçu qu'avec du code.
Découvrez ce beau cube brillant. Il ne rouillera jamais, car il n'utilise aucun métal.
Maintenant, apprenons à le créer en utilisant du code.
Pour nos besoins, UIKit sert de couche mince au-dessus de Quartz Core, nous offrant un accès gratuit à ses capacités 3D. Un UIView
contient une référence à un objet CALayer
, qui est le composant réel que le système d'exploitation utilise pour le rendu à l'écran. Il existe trois propriétés de CALayer
qui influencent sa présentation à l'écran : la position, les limites et la transformation. Les deux premiers sont assez explicites, tandis que transform
peut être initialisée avec n'importe quelle matrice 4x4 arbitraire. Lorsque plusieurs couches 3D doivent être présentées simultanément, nous devons utiliser un CATransformLayer spécialisé, qui préserve de manière unique l'espace 3D de ses couches enfants au lieu de les aplatir sur un plan 2D.
Commençons par dessiner un simple cube. Tout d'abord, nous allons créer une fonction d'assistance pour ajuster la position de chaque côté :
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 }
Ensuite, dans le corps de la fonction viewDidLoad
de notre ViewController, nous assemblerons les six côtés du cube :
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)
Voici à quoi ressemble ce code en action :
C'est indéniablement de la 3D, mais quelque chose ne va pas, n'est-ce pas ? Le concept de perspective 3D dans l'art a été maîtrisé pour la première fois par les peintres italiens de la Renaissance au XVe siècle. Heureusement, nous pouvons obtenir un effet similaire en utilisant simplement une matrice de projection en perspective :
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)
Mieux, n'est-ce pas ? Le terme -1.0 / 400.0
à m34 est ce qui crée l'effet de perspective. Pour les calculs réels, voir https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html
Notre objectif est de démontrer un effet miroir, nous aurons donc besoin de quelque chose pour refléter. Dans les graphiques 3D, les cartes de cube sont couramment utilisées pour simuler des surfaces réfléchissantes. Dans notre exemple, nous pouvons en créer un en utilisant le cube réel que nous avons créé précédemment. Tout d'abord, nous attribuons des images aux faces correspondantes :
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
Ensuite, pour chaque face, nous définissons isDoubleSided = true
et augmentons la taille du cube à cubeSize: CGFloat = 2000.0
. Cela place essentiellement la "caméra" à l'intérieur du cube :
Ensuite, puisque nous allons créer plusieurs cubes à la fois, simplifions les fonctions de configuration :
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 }
Maintenant, rendons simultanément la carte du cube et un petit cube :
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 est un cadre robuste, mais il manque de fonctionnalités intégrées pour des effets visuels complexes. Cependant, il offre la possibilité d'appliquer des masques arbitraires aux objets, et c'est précisément ce que nous allons exploiter pour créer l'effet miroir. Essentiellement, nous rendrons l'environnement six fois, chacune masquée par la face de cube correspondante.
L'aspect délicat est que nous ne pouvons pas masquer directement un CATransformLayer
. Cependant, nous pouvons contourner cette limitation en l'imbriquant dans un conteneur 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 }
Et maintenant, notre viewDidLoad devrait ressembler à ceci :
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)) }
Cette image ressemble déjà étroitement à ce que nous avions l'intention de réaliser, mais à ce stade, le cube n'est qu'un masque 3D sur la carte du cube. Alors, comment le transformer en un véritable miroir ?
Il s'avère qu'il existe une méthode simple pour refléter le monde par rapport à un plan arbitraire dans l'espace 3D. Sans entrer dans des mathématiques complexes, voici la matrice que nous recherchons :
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 }
Ensuite, nous incorporons le code suivant dans la fonction de configuration du cube :
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 } }
Et enfin, nous pouvons voir le cube brillant que nous recherchions :
Bien sûr, obtenir le même effet peut sembler plus facile avec Metal ou un framework basé sur Metal comme SceneKit. Mais ceux-ci viennent avec leur propre ensemble de limites. Le grand? Vous ne pouvez pas intégrer de vues UIKit en direct dans le contenu 3D dessiné par Metal.
La méthode que nous avons examinée dans cet article nous permet d'afficher toutes sortes de contenus dans un cadre 3D. Cela inclut des cartes, des vidéos et des vues interactives. De plus, il peut se fondre en douceur avec toutes les animations UIKit que vous souhaitez utiliser.
Le code source de cet article, ainsi que certaines fonctions d'assistance, peuvent être trouvés sur https://github.com/petertechstories/uikit-mirrors
Bon codage !