Como desenvolvedores de aplicativos, não somos apenas codificadores – somos criadores, construtores e, às vezes, ilusionistas. A arte do desenvolvimento de aplicativos vai além de apenas código e design. Às vezes, trata-se de criar um elemento de surpresa e ilusão que capte a atenção dos usuários e crie uma experiência imersiva. Desta vez, estamos saindo de nossa zona de conforto do mundo 2D e dando um salto ousado para o cativante mundo 3D .
O UIKit é mais do que um conjunto de ferramentas para construir interfaces de usuário. É um kit de ferramentas poderoso que, quando usado corretamente, pode criar efeitos visuais incríveis. Neste artigo, vamos nos aprofundar no UIKit e mostrar uma técnica para criar um reflexo semelhante a um espelho. Esse efeito pode dar ao seu aplicativo uma aparência visualmente impressionante e envolvente que normalmente parece alcançável apenas com ferramentas gráficas complexas, mas é criado com nada além de código.
Confira este lindo cubo brilhante. Nunca enferrujará, pois não usa nenhum metal.
Agora, vamos aprender como criá-lo usando código.
Para nossos propósitos, o UIKit serve como uma camada fina sobre o Quartz Core, fornecendo acesso gratuito aos seus recursos 3D. Um UIView
contém uma referência a um objeto CALayer
, que é o componente real que o sistema operacional usa para renderização na tela. Existem três propriedades de CALayer
que influenciam sua apresentação na tela: posição, limites e transformação. Os dois primeiros são bastante auto-explicativos, enquanto transform
pode ser inicializada com qualquer matriz 4x4 arbitrária. Quando várias camadas 3D precisam ser apresentadas simultaneamente, devemos empregar um CATransformLayer especializado, que preserva exclusivamente o espaço 3D de suas camadas filhas em vez de achatá-las em um plano 2D.
Vamos começar desenhando um cubo simples. Primeiro, criaremos uma função auxiliar para ajustar a posição 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 }
Em seguida, no corpo da função viewDidLoad
do nosso ViewController, vamos montar todos os seis lados do 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)
Veja como esse código se parece em ação:
É inegavelmente 3D, mas algo parece errado, não é? O conceito de perspectiva 3D na arte foi dominado pela primeira vez por pintores renascentistas italianos no século XV. Felizmente, podemos obter um efeito semelhante usando apenas uma matriz de projeção em 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)
Melhor, não é? O termo -1.0 / 400.0
em m34 é o que cria o efeito de perspectiva. Para a matemática real, consulte https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html
Nosso objetivo é demonstrar um efeito de espelho, então precisaremos de algo para refletir. Em gráficos 3D, mapas de cubo são comumente usados para simular superfícies reflexivas. Em nosso exemplo, podemos criar um usando o cubo real que criamos anteriormente. Primeiro, atribuímos imagens aos rostos correspondentes:
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
Em seguida, para cada face, definimos isDoubleSided = true
e aumentamos o tamanho do cubo para cubeSize: CGFloat = 2000.0
. Isso basicamente coloca a "câmera" dentro do cubo:
Em seguida, como vamos criar vários cubos de uma só vez, vamos simplificar as funções de configuração:
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 }
Agora, vamos renderizar o mapa do cubo e um cubo pequeno simultaneamente:
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))
O UIKit é uma estrutura robusta, mas carece de recursos integrados para efeitos visuais complexos. No entanto, oferece a capacidade de aplicar máscaras arbitrárias aos objetos, e é exatamente isso que vamos explorar para criar o efeito de espelho. Essencialmente, renderizaremos o ambiente seis vezes, cada uma mascarada pela face correspondente do cubo.
O aspecto complicado é que não podemos mascarar diretamente um CATransformLayer
. No entanto, podemos contornar essa limitação aninhando-o dentro de um contêiner 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 }
E agora, nosso viewDidLoad deve ficar assim:
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 imagem já se parece muito com o que pretendíamos alcançar, mas neste ponto, o cubo é apenas uma máscara 3D sobre o mapa do cubo. Então, como podemos transformá-lo em um espelho real?
Acontece que existe um método direto para espelhar o mundo em relação a um plano arbitrário no espaço 3D. Sem entrar em matemática complexa, esta é a matriz que procuramos:
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 }
Em seguida, incorporamos o seguinte código na função de configuração do 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 } }
E, finalmente, podemos contemplar o cubo brilhante que tanto almejamos:
Claro, alcançar o mesmo efeito pode parecer mais fácil com Metal ou uma estrutura baseada em Metal como o SceneKit. Mas esses vêm com seu próprio conjunto de limites. O grande? Você não pode trazer visualizações UIKit ao vivo para o conteúdo 3D desenhado pelo Metal.
O método que vimos neste artigo nos permite exibir todos os tipos de conteúdo em uma configuração 3D. Isso inclui mapas, vídeos e exibições interativas. Além disso, ele pode se misturar suavemente com qualquer animação UIKit que você queira usar.
O código-fonte deste artigo, juntamente com algumas funções auxiliares, pode ser encontrado em https://github.com/petertechstories/uikit-mirrors
Codificação feliz!