🌝

Swiftでいろんな行列

2023/04/15に公開

Swiftでいろんな行列のMathユーティリティクラスを作りたいと思います。

前提
基本的に,float4x4にExtensionを生やす形で実装してきたいと思います。前提としてsimdをimportしています。

拡大

extension float4x4 {
 init(scaler: float3) {
   let matrix = float4x4(
     [scaler.x,         0,         0, 0],
     [        0, scaler.y,         0, 0],
     [        0,         0, scaler.z, 0],
     [        0,         0,         0, 1]
   )
   self = matrix
 }
}

3次元空間上の点(x,y,z)を拡大するには、それぞれの点をスカラー倍する必要があるので、拡大行列はこのような形になります。実際に行列を手計算してみると理解できるかと思います。

回転


  init(rotationX radianAngle: Float) {
    let matrix = float4x4(
      [1,           0,          0, 0],
      [0,  cos(angle), sin(angle), 0],
      [0, -sin(angle), cos(angle), 0],
      [0,           0,          0, 1]
    )
    self = matrix
  }

  init(rotationY radianAngle: Float) {
    let matrix = float4x4(
      [cos(angle), 0, -sin(angle), 0],
      [         0, 1,           0, 0],
      [sin(angle), 0,  cos(angle), 0],
      [         0, 0,           0, 1]
    )
    self = matrix
  }

  init(rotationZ radianAngle: Float) {
    let matrix = float4x4(
      [ cos(angle), sin(angle), 0, 0],
      [-sin(angle), cos(angle), 0, 0],
      [          0,          0, 1, 0],
      [          0,          0, 0, 1]
    )
    self = matrix
  }

x,y,z軸を中心とした回転をそれぞれ定義しました。これらの回転行列の導出はいろいろな参考記事があるのでいくつか貼らせていただきます。
https://www.mynote-jp.com/entry/2016/04/30/201249
https://w3e.kanazawa-it.ac.jp/math/category/gyouretu/senkeidaisu/henkan-tex.cgi?target=/math/category/gyouretu/senkeidaisu/sanjigen-kaitengyouretu.html

当たり前と言えば当たり前なのですが↓のように考えるとすんなり理解できます。(自分で図を書いてみて計算するのがおすすめです!)
x軸中心=>yz平面上の回転を考える
y軸中心=>zx平面上の回転を考える
z軸中心=>xy平面上の回転を考える

平行移動

init(translation: float3) {
    let matrix = float4x4(
      [            1,             0,             0, 0],
      [            0,             1,             0, 0],
      [            0,             0,             1, 0],
      [translation.x, translation.y, translation.z, 1]
    )
    self = matrix
  }

(x,y,z,w) * 平行移動行列 = (x + translation.x, y + translation.y, z + translation.z, w)になります。元々の位置に対して平行移動したい数が加算された結果になるので、このような行列になります。(一度手計算して確かめるのがおすすめです! n回目)

ちなみに、以上のことから拡大縮小、回転のみの要素を取りたい時はこのような行列を作ることができます。

var upperLeft: float3x3 {
    let x = columns.0.xyz
    let y = columns.1.xyz
    let z = columns.2.xyz
    return float3x3(columns: (x, y, z))
  }

また、ユーグリット空間において、この拡大縮小、回転、平行移動3つの要素を持った行列のことをアフィン変換行列と言います。
アフィン変換行列を理解したい方は超絶分かりやすい神動画があるので記載しておきます。(この記事を読むより3億倍理解が深まります)
https://www.youtube.com/watch?v=qyOys7rFZiM&list=PLAsWwUHApt3M068FyW7IBHtIHTqRikvqX

おまけ 
プロジェクション行列も作成しました。

プロジェクション行列

init(projectionFov fov: Float, near: Float, far: Float, aspect: Float, lhs: Bool = true) {
    let y = 1 / tan(fov * 0.5)
    let x = y / aspect
    let z = lhs ? far / (far - near) : far / (near - far)
    let X = float4( x,  0,  0,  0)
    let Y = float4( 0,  y,  0,  0)
    let Z = lhs ? float4( 0,  0,  z, 1) : float4( 0,  0,  z, -1)
    let W = lhs ? float4( 0,  0,  z * -near,  0) : float4( 0,  0,  z * near,  0)
    self.init()
    columns = (X, Y, Z, W)

理解しにくい部分を解説していきます。
lhsは左手座標系かどうかを示しています。

let y = 1 / tan(fov * 0.5) 

fov * 0.5で視錐台を真横から見た時の半分の角度です。これのTangentの逆数(cot)を取ることでスケール値を取ることができます。
なぜそうなるのかは神サイトとして名高いこちらをご覧ください。図を書いて、手計算を何回かしてみると、パースペクティブマスター?になれます。
http://marupeke296.com/DXG_No70_perspective.html

let z = lhs ? far / (far - near) : far / (near - far) 

座標系の違いでZの正の方向が手前か奥かで分ける必要があります。
左手座標系だと奥が+になるのでfar-near。右手座標系が奥だと-なのでnear-farになります。farを除算しているのはGPUがwで各成分を全て除算してしまうためにこの処理が必要となります。(詳細は既出の参考サイトをご覧ください)

let Z = lhs ? float4( 0, 0, z, 1) : float4( 0, 0, z, -1) 

ここも座標系で分ける必要があります。理由も同じで奥の正負が逆だからです。

let W = lhs ? float4( 0, 0, z * -near, 0) : float4( 0, 0, z * near, 0) 

正規化デバイス空間に変換する中で視錐台をz方向に縮める処理で必要になってきます。ただ、Z値を扱っているので符号が逆を逆にします。

Discussion