Là nhà phát triển ứng dụng, chúng tôi không chỉ là lập trình viên – chúng tôi còn là người sáng tạo, người xây dựng và đôi khi là người ảo tưởng. Nghệ thuật phát triển ứng dụng không chỉ dừng lại ở mã và thiết kế. Đôi khi, đó là việc tạo ra một yếu tố bất ngờ và ảo giác để thu hút sự chú ý của người dùng và tạo ra trải nghiệm sống động. Lần này, chúng ta đang bước ra khỏi vùng an toàn của thế giới 2D và thực hiện một bước nhảy táo bạo vào thế giới 3D quyến rũ.
UIKit không chỉ là một bộ công cụ để xây dựng giao diện người dùng. Đó là một bộ công cụ mạnh mẽ, khi được sử dụng đúng cách, có thể tạo ra những hiệu ứng hình ảnh tuyệt vời. Trong bài viết này, chúng ta sẽ tìm hiểu sâu về UIKit và giới thiệu một kỹ thuật để tạo phản chiếu giống như gương. Hiệu ứng này có thể mang lại cho ứng dụng của bạn một giao diện trực quan ấn tượng và hấp dẫn mà dường như chỉ có thể đạt được bằng các công cụ đồ họa phức tạp, tuy nhiên, nó được tạo ra không có gì ngoài mã.
Kiểm tra khối lập phương đẹp, sáng bóng này. Nó sẽ không bao giờ rỉ sét, vì nó không sử dụng bất kỳ Kim loại nào.
Bây giờ, hãy tìm hiểu cách tạo nó bằng mã.
Đối với mục đích của chúng tôi, UIKit đóng vai trò là một lớp mỏng trên đỉnh Lõi thạch anh, cung cấp cho chúng tôi quyền truy cập miễn phí vào các khả năng 3D của nó. UIView
giữ tham chiếu đến đối tượng CALayer
, là thành phần thực tế mà HĐH sử dụng để hiển thị trên màn hình. Có ba thuộc tính của CALayer
ảnh hưởng đến cách trình bày trên màn hình của nó: vị trí, giới hạn và biến đổi. Hai cái đầu tiên khá dễ hiểu, trong khi transform
có thể được khởi tạo với bất kỳ ma trận 4x4 tùy ý nào. Khi nhiều lớp 3D cần được trình bày đồng thời, chúng ta phải sử dụng CATransformLayer chuyên dụng, giúp duy trì không gian 3D của các lớp con thay vì làm phẳng chúng trên mặt phẳng 2D.
Hãy bắt đầu bằng cách vẽ một khối lập phương đơn giản. Đầu tiên, chúng ta sẽ tạo một hàm trợ giúp để điều chỉnh vị trí của mỗi bên:
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 }
Tiếp theo, trong phần thân của hàm viewDidLoad
của ViewController, chúng ta sẽ lắp ráp tất cả sáu mặt của khối lập phương:
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)
Đây là những gì mã này trông giống như trong hành động:
Không thể phủ nhận đó là 3D, nhưng có gì đó không ổn, phải không? Khái niệm về phối cảnh 3D trong nghệ thuật lần đầu tiên được các họa sĩ thời Phục hưng Ý làm chủ vào thế kỷ 15. May mắn thay, chúng ta có thể đạt được hiệu ứng tương tự chỉ bằng cách sử dụng ma trận chiếu phối cảnh:
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)
Tốt hơn, phải không? Thuật ngữ -1.0 / 400.0
tại m34 là yếu tố tạo ra hiệu ứng phối cảnh. Đối với phép toán thực tế, hãy xem https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html
Mục tiêu của chúng tôi là chứng minh hiệu ứng gương, vì vậy chúng tôi sẽ cần một cái gì đó để phản chiếu. Trong đồ họa 3D, bản đồ khối thường được sử dụng để mô phỏng các bề mặt phản chiếu. Trong ví dụ của chúng tôi, chúng tôi có thể tạo một cái bằng cách sử dụng khối lập phương thực tế mà chúng tôi đã tạo trước đó. Đầu tiên, chúng ta gán ảnh cho các mặt tương ứng:
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
Tiếp theo, đối với mọi mặt, chúng tôi đặt isDoubleSided = true
và tăng kích thước của khối lập phương thành cubeSize: CGFloat = 2000.0
. Điều này về cơ bản đặt "máy ảnh" bên trong khối lập phương:
Tiếp theo, vì chúng ta sẽ tạo nhiều hình khối cùng một lúc, hãy đơn giản hóa các chức năng thiết lập:
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 }
Bây giờ, hãy kết xuất đồng thời cả bản đồ khối và một khối nhỏ:
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 là một khung mạnh mẽ, nhưng nó thiếu các tính năng tích hợp cho các hiệu ứng hình ảnh phức tạp. Tuy nhiên, nó cung cấp khả năng áp dụng mặt nạ tùy ý cho các đối tượng và đó chính xác là những gì chúng ta sẽ khai thác để tạo hiệu ứng gương. Về cơ bản, chúng ta sẽ kết xuất môi trường sáu lần, mỗi lần được che bởi mặt khối tương ứng.
Khía cạnh phức tạp là chúng ta không thể che dấu trực tiếp CATransformLayer
. Tuy nhiên, chúng ta có thể khắc phục giới hạn này bằng cách lồng nó vào bên trong bộ chứa 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 }
Và bây giờ, viewDidLoad của chúng ta sẽ trông như thế này:
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)) }
Hình ảnh này gần giống với những gì chúng tôi dự định đạt được, nhưng tại thời điểm này, khối lập phương chỉ là một mặt nạ kiểu 3D trên bản đồ khối. Vì vậy, làm thế nào để chúng ta biến nó thành một tấm gương thực sự?
Hóa ra có một phương pháp đơn giản để phản chiếu thế giới so với một mặt phẳng tùy ý trong không gian 3D. Không đi sâu vào toán học phức tạp, đây là ma trận mà chúng tôi đang tìm kiếm:
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 }
Tiếp theo, chúng tôi kết hợp đoạn mã sau vào chức năng thiết lập khối:
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 } }
Và cuối cùng, chúng ta có thể nhìn thấy khối lập phương sáng bóng mà chúng ta đã phấn đấu:
Chắc chắn, việc đạt được hiệu ứng tương tự có vẻ dễ dàng hơn với Metal hoặc khung dựa trên Metal như SceneKit. Nhưng những thứ đó đi kèm với những giới hạn riêng của chúng. Cái lớn? Bạn không thể đưa chế độ xem UIKit trực tiếp vào nội dung 3D do Metal vẽ.
Phương pháp chúng tôi đã xem xét trong bài viết này cho phép chúng tôi hiển thị tất cả các loại nội dung trong cài đặt 3D. Điều này bao gồm bản đồ, video và chế độ xem tương tác. Ngoài ra, nó có thể kết hợp mượt mà với bất kỳ hoạt ảnh UIKit nào mà bạn có thể muốn sử dụng.
Mã nguồn của bài viết này, cùng với một số hàm trợ giúp, có thể được tìm thấy tại https://github.com/petertechstories/uikit-mirrors
Chúc mừng mã hóa!