paint-brush
纯 UIKit 中的烟雾和镜子经过@petertech
79,463 讀數
79,463 讀數

纯 UIKit 中的烟雾和镜子

经过 Peter J.10m2023/05/19
Read on Terminal Reader

太長; 讀書

UIKit 是一个强大的工具包,如果使用得当,可以创造出惊人的视觉效果。在本文中,我们将深入研究 UIKit 并展示一种创建镜面反射的技术。这种效果可以为您的应用程序提供视觉上令人印象深刻且引人入胜的外观,这通常似乎只有使用复杂的图形工具才能实现。
featured image - 纯 UIKit 中的烟雾和镜子
Peter J. HackerNoon profile picture
0-item
1-item

作为应用程序开发人员,我们不仅仅是编码员——我们是创造者、建设者,有时还是魔术师。应用程序开发的艺术不仅仅是代码和设计。有时,它是关于制作一种惊喜和幻想的元素来吸引用户的注意力并创造一种身临其境的体验。这一次,我们将走出 2D 世界的舒适区,大胆地跃入迷人的3D世界。


UIKit 不仅仅是一组用于构建用户界面的工具。这是一个功能强大的工具包,如果使用得当,可以创造出惊人的视觉效果。在本文中,我们将深入研究 UIKit 并展示一种创建镜面反射的技术。这种效果可以为您的应用程序提供视觉上令人印象深刻且引人入胜的外观,这通常似乎只有使用复杂的图形工具才能实现,但它仅使用代码制作而成。

最终结果

看看这个漂亮、闪亮的立方体。它永远不会生锈,因为它不使用任何金属。


现在,让我们学习如何使用代码创建它。

首先是一些基础知识

出于我们的目的,UIKit 充当 Quartz Core 之上的薄层,让我们可以免费访问其 3D 功能。 UIView持有对CALayer对象的引用,这是操作系统用于屏幕渲染的实际组件。 CALayer的三个属性会影响其在屏幕上的呈现:position、bounds 和 transform。前两个是不言自明的,而transform可以用任意 4x4 矩阵初始化。当需要同时呈现多个 3D 层时,我们必须使用专门的 CATransformLayer,它唯一地保留其子层的 3D 空间,而不是将它们展平到 2D 平面上。

一个立方体

让我们从绘制一个简单的立方体开始。首先,我们将创建一个辅助函数来调整每一边的位置:


 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函数的主体中,我们将组装立方体的所有六个面:


 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 的,但感觉有些不对劲,不是吗?艺术中的三维透视概念最早由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 是一个强大的框架,但它缺乏用于复杂视觉效果的内置功能。然而,它确实提供了对对象应用任意蒙版的能力,而这正是我们要利用来创建镜像效果的功能。本质上,我们将渲染环境六次,每次都被相应的立方体面遮盖。


棘手的方面是我们不能直接屏蔽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 } }


最后,我们可以看到我们一直在努力争取的闪亮立方体:

是不是很美?

为什么是 UIKit?

当然,使用 Metal 或基于 Metal 的框架(如 SceneKit)似乎更容易实现相同的效果。但这些都有自己的限制。大的那个?您无法将实时 UIKit 视图带入 Metal 绘制的 3D 内容。


我们在本文中看到的方法使我们能够在 3D 设置中显示各种内容。这包括地图、视频和交互式视图。另外,它可以平滑地与您可能想要使用的任何 UIKit 动画混合。


本文的源代码以及一些辅助函数可以在https://github.com/petertechstories/uikit-mirrors找到

编码愉快!