Als App-Entwickler sind wir nicht nur Programmierer – wir sind Schöpfer, Entwickler und manchmal auch Illusionisten. Die Kunst der App-Entwicklung geht über Code und Design hinaus. Manchmal geht es darum, ein Überraschungs- und Illusionselement zu schaffen, das die Aufmerksamkeit der Benutzer fesselt und ein immersives Erlebnis schafft. Dieses Mal verlassen wir unsere Komfortzone der 2D-Welt und wagen einen mutigen Sprung in die faszinierende Welt der 3D .
UIKit ist mehr als eine Reihe von Tools zum Erstellen von Benutzeroberflächen. Es handelt sich um ein leistungsstarkes Toolkit, das bei richtiger Anwendung erstaunliche visuelle Effekte erzeugen kann. In diesem Artikel werden wir uns eingehend mit UIKit befassen und eine Technik zum Erstellen einer spiegelähnlichen Reflexion vorstellen. Dieser Effekt kann Ihrer App ein visuell beeindruckendes und ansprechendes Aussehen verleihen, das normalerweise nur mit komplexen grafischen Tools erreichbar zu sein scheint, obwohl sie nur mit Code erstellt wurde.
Das Endergebnis
Schauen Sie sich diesen wunderschönen, glänzenden Würfel an. Es wird nie rosten, da kein Metall verwendet wird.
Jetzt lernen wir, wie man es mit Code erstellt.
Zunächst einige Grundlagen
Für unsere Zwecke dient UIKit als schlanke Schicht auf Quartz Core und bietet uns freien Zugriff auf seine 3D-Funktionen. Ein UIView
enthält einen Verweis auf ein CALayer
Objekt, das die eigentliche Komponente ist, die das Betriebssystem für das Rendern auf dem Bildschirm verwendet. Es gibt drei Eigenschaften von CALayer
, die seine Bildschirmdarstellung beeinflussen: Position, Grenzen und Transformation. Die ersten beiden sind ziemlich selbsterklärend, während transform
mit jeder beliebigen 4x4-Matrix initialisiert werden kann. Wenn mehrere 3D-Ebenen gleichzeitig dargestellt werden müssen, müssen wir einen speziellen CATransformLayer verwenden, der den 3D-Raum seiner untergeordneten Ebenen auf einzigartige Weise beibehält, anstatt sie auf eine 2D-Ebene abzuflachen.
Ein Würfel
Beginnen wir mit dem Zeichnen eines einfachen Würfels. Zuerst erstellen wir eine Hilfsfunktion, um die Position jeder Seite anzupassen:
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 }
Als nächstes werden wir im Hauptteil viewDidLoad
Funktion unseres ViewControllers alle sechs Seiten des Würfels zusammensetzen:
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)
So sieht dieser Code in Aktion aus:
Es ist unbestreitbar 3D, aber irgendetwas fühlt sich komisch an, nicht wahr? Das Konzept der 3D-Perspektive in der Kunst wurde erstmals im 15. Jahrhundert von italienischen Renaissance-Malern beherrscht. Glücklicherweise können wir einen ähnlichen Effekt erzielen, indem wir einfach eine perspektivische Projektionsmatrix verwenden:
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)
Schauen wir uns nun das Ergebnis an:
Besser, nicht wahr? Der Term -1.0 / 400.0
bei m34 erzeugt den Perspektiveffekt. Die eigentliche Mathematik finden Sie unter https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html
Kartierung der Umgebung
Unser Ziel ist es, einen Spiegeleffekt zu demonstrieren, also brauchen wir etwas zum Reflektieren. In 3D-Grafiken werden Würfelkarten häufig zur Simulation reflektierender Oberflächen verwendet. In unserem Beispiel können wir einen erstellen, indem wir den tatsächlichen Würfel verwenden, den wir zuvor erstellt haben. Zuerst ordnen wir Bilder den entsprechenden Gesichtern zu:
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
Als nächstes setzen wir für jede Fläche isDoubleSided = true
und erhöhen die Größe des Würfels auf cubeSize: CGFloat = 2000.0
. Dadurch wird im Wesentlichen die „Kamera“ im Würfel platziert:
Da wir mehrere Cubes gleichzeitig erstellen werden, vereinfachen wir als Nächstes die Einrichtungsfunktionen:
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 }
Lassen Sie uns nun sowohl die Würfelkarte als auch einen kleinen Würfel gleichzeitig rendern:
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))
Reflexionen
UIKit ist ein robustes Framework, es fehlen jedoch integrierte Funktionen für komplexe visuelle Effekte. Allerdings bietet es die Möglichkeit, beliebige Masken auf Objekte anzuwenden, und genau das werden wir ausnutzen, um den Spiegeleffekt zu erzeugen. Im Wesentlichen rendern wir die Umgebung sechsmal, jeweils maskiert durch die entsprechende Würfelfläche.
Der schwierige Aspekt besteht darin, dass wir einen CATransformLayer
nicht direkt maskieren können. Wir können diese Einschränkung jedoch umgehen, indem wir es in einem CALayer
Container verschachteln:
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 }
Und jetzt sollte unser viewDidLoad so aussehen:
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)) }
Dieses Bild ähnelt bereits stark dem, was wir erreichen wollten, aber zu diesem Zeitpunkt ist der Würfel lediglich eine 3D-ähnliche Maske über der Würfelkarte. Wie verwandeln wir es also in einen echten Spiegel?
Die Spiegeldimension
Es stellt sich heraus, dass es eine einfache Methode gibt, die Welt relativ zu einer beliebigen Ebene im 3D-Raum zu spiegeln. Ohne in komplexe Mathematik einzutauchen, ist dies die Matrix, die wir suchen:
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 }
Als nächstes integrieren wir den folgenden Code in die Cube-Setup-Funktion:
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 } }
Und endlich können wir den glänzenden Würfel erblicken, nach dem wir gestrebt haben:
Warum UIKit?
Sicherlich scheint es mit Metal oder einem Metal-basierten Framework wie SceneKit einfacher zu sein, den gleichen Effekt zu erzielen. Aber diese haben ihre eigenen Grenzen. Der Grosse? Sie können keine Live-UIKit-Ansichten in den von Metal gezeichneten 3D-Inhalt integrieren.
Mit der Methode, die wir in diesem Artikel betrachtet haben, können wir alle Arten von Inhalten in einer 3D-Umgebung anzeigen. Dazu gehören Karten, Videos und interaktive Ansichten. Außerdem lässt es sich problemlos mit allen UIKit-Animationen kombinieren, die Sie verwenden möchten.
Der Quellcode für diesen Artikel sowie einige Hilfsfunktionen finden Sie unter https://github.com/petertechstories/uikit-mirrors
Viel Spaß beim Codieren!