Como desarrolladores de aplicaciones, no somos solo codificadores: somos creadores, constructores y, a veces, ilusionistas. El arte del desarrollo de aplicaciones va más allá del código y el diseño. A veces, se trata de crear un elemento de sorpresa e ilusión que capte la atención de los usuarios y cree una experiencia inmersiva. Esta vez, estamos saliendo de nuestra zona de confort del mundo 2D y dando un salto audaz al cautivador mundo del 3D .
UIKit es más que un conjunto de herramientas para construir interfaces de usuario. Es un conjunto de herramientas poderoso que, cuando se usa correctamente, puede crear efectos visuales sorprendentes. En este artículo, profundizaremos en UIKit y mostraremos una técnica para crear un reflejo similar a un espejo. Este efecto puede darle a su aplicación un aspecto visualmente impresionante y atractivo que, por lo general, solo parece lograrse con herramientas gráficas complejas, pero está diseñado únicamente con código.
Echa un vistazo a este cubo hermoso y brillante. Nunca se oxidará, ya que no utiliza ningún metal.
Ahora, aprendamos cómo crearlo usando código.
Para nuestros propósitos, UIKit sirve como una capa delgada sobre Quartz Core, brindándonos acceso gratuito a sus capacidades 3D. Una UIView
contiene una referencia a un objeto CALayer
, que es el componente real que usa el sistema operativo para la representación en pantalla. Hay tres propiedades de CALayer
que influyen en su presentación en pantalla: posición, límites y transformación. Los dos primeros se explican por sí mismos, mientras que transform
se puede inicializar con cualquier matriz arbitraria de 4x4. Cuando es necesario presentar simultáneamente varias capas 3D, debemos emplear un CATransformLayer especializado, que conserva de manera única el espacio 3D de sus capas secundarias en lugar de aplanarlos en un plano 2D.
Comencemos dibujando un cubo simple. Primero, crearemos una función auxiliar para ajustar la posición de cada lado:
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 }
A continuación, en el cuerpo de la función viewDidLoad
de nuestro ViewController, ensamblaremos los seis lados del cubo:
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)
Así es como se ve este código en acción:
Es innegablemente 3D, pero algo se siente mal, ¿no? El concepto de perspectiva 3D en el arte fue dominado por primera vez por pintores renacentistas italianos en el siglo XV. Afortunadamente, podemos lograr un efecto similar simplemente usando una matriz de proyección en perspectiva:
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)
Mejor, ¿no? El término -1.0 / 400.0
en m34 es lo que crea el efecto de perspectiva. Para conocer las matemáticas reales, consulte https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html
Nuestro objetivo es demostrar un efecto de espejo, por lo que necesitaremos algo para reflejar. En gráficos 3D, los mapas de cubos se usan comúnmente para simular superficies reflectantes. En nuestro ejemplo, podemos crear uno usando el cubo real que hicimos antes. Primero, asignamos imágenes a las caras correspondientes:
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
Luego, para cada cara, establecemos isDoubleSided = true
y aumentamos el tamaño del cubo a cubeSize: CGFloat = 2000.0
. Esto esencialmente coloca la "cámara" dentro del cubo:
A continuación, dado que vamos a crear varios cubos a la vez, simplifiquemos las funciones de configuración:
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 }
Ahora, rendericemos el mapa del cubo y un cubo pequeño simultáneamente:
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 es un marco robusto, pero carece de funciones integradas para efectos visuales complejos. Sin embargo, ofrece la posibilidad de aplicar máscaras arbitrarias a los objetos, y eso es precisamente lo que vamos a aprovechar para crear el efecto espejo. Esencialmente, representaremos el entorno seis veces, cada una enmascarada por la cara del cubo correspondiente.
El aspecto complicado es que no podemos enmascarar directamente un CATransformLayer
. Sin embargo, podemos eludir esta limitación al anidarlo dentro de un contenedor 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 }
Y ahora, nuestro viewDidLoad debería verse así:
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)) }
Esta imagen ya se parece mucho a lo que pretendíamos lograr, pero en este punto, el cubo es simplemente una máscara 3D sobre el mapa del cubo. Entonces, ¿cómo lo transformamos en un espejo real?
Resulta que hay un método sencillo para reflejar el mundo en relación con un plano arbitrario en el espacio 3D. Sin profundizar en matemáticas complejas, esta es la matriz que buscamos:
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 }
A continuación, incorporamos el siguiente código en la función de configuración del cubo:
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 } }
Y finalmente, podemos contemplar el cubo brillante por el que hemos estado luchando:
Claro, lograr el mismo efecto puede parecer más fácil con Metal o un marco basado en Metal como SceneKit. Pero esos vienen con su propio conjunto de límites. ¿El Grande? No puede traer vistas de UIKit en vivo al contenido 3D dibujado por Metal.
El método que hemos visto en este artículo nos permite mostrar todo tipo de contenido en una configuración 3D. Esto incluye mapas, videos y vistas interactivas. Además, se puede combinar sin problemas con cualquier animación de UIKit que desee usar.
El código fuente de este artículo, junto con algunas funciones auxiliares, se puede encontrar en https://github.com/petertechstories/uikit-mirrors
¡Feliz codificación!