アプリ開発者として、私たちは単なるプログラマーではなく、クリエイター、ビルダー、そして時にはイリュージョニストでもあります。アプリ開発の技術はコードとデザインだけにとどまりません。場合によっては、ユーザーの注意を引き、没入型のエクスペリエンスを生み出す驚きと幻想の要素を作り出すことが重要です。今回、私たちは 2D の世界という快適ゾーンから抜け出し、魅惑的な3Dの世界に大胆に飛び込みます。
UIKit は、ユーザー インターフェイスを構築するためのツールのセット以上のものです。これは、正しく使用すると、素晴らしい視覚効果を生み出すことができる強力なツールキットです。この記事では、UIKit を深く掘り下げ、鏡のような反射を作成するテクニックを紹介します。この効果により、アプリに視覚的に印象的で魅力的な外観を与えることができます。通常は複雑なグラフィカル ツールを使用しないと実現できないように見えますが、アプリはコードのみで作成されます。
この美しく輝く立方体をご覧ください。金属を使用していないので錆びることはありません。
次に、コードを使用して作成する方法を学びましょう。
私たちの目的のために、UIKit は Quartz Core 上のスリム層として機能し、その 3D 機能への自由なアクセスを提供します。 UIView
、OS が画面上のレンダリングに使用する実際のコンポーネントであるCALayer
オブジェクトへの参照を保持します。 CALayer
は、画面上の表示に影響を与える 3 つのプロパティ (位置、範囲、変換) があります。最初の 2 つは一目瞭然ですが、 transform
任意の 4x4 行列で初期化できます。複数の 3D レイヤーを同時に表示する必要がある場合は、子レイヤーを 2D 平面上に平坦化するのではなく、その子レイヤーの 3D 空間を独自に保存する特殊な CATransformLayer を使用する必要があります。
まずは簡単な立方体を描いてみましょう。まず、各辺の位置を調整するヘルパー関数を作成します。
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
関数の本体で、立方体の 6 つの側面すべてを組み立てます。
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であることは間違いないのですが、何か違和感がありますよね。芸術における 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 は堅牢なフレームワークですが、複雑な視覚効果を実現する組み込み機能がありません。ただし、オブジェクトに任意のマスクを適用する機能はあり、それをまさにミラー効果を作成するために活用します。基本的に、環境を 6 回レンダリングし、それぞれが対応する立方体の面でマスクされます。
厄介な点は、 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 } }
そして最後に、私たちが目指してきた輝く立方体が見えてきます。
確かに、Metal または SceneKit のような Metal ベースのフレームワークを使用すると、同じ効果を達成するのが簡単に思えるかもしれません。しかし、それらには独自の制限があります。大きいものは? Metal によって描画された 3D コンテンツにライブ UIKit ビューを取り込むことはできません。
この記事で説明した方法を使用すると、あらゆる種類のコンテンツを 3D 設定で表示できます。これには、地図、ビデオ、インタラクティブなビューが含まれます。さらに、使用したい UIKit アニメーションとスムーズにブレンドできます。
この記事のソース コードといくつかのヘルパー関数は、 https://github.com/petertechstories/uikit-mirrorsにあります。
コーディングを楽しんでください!