[Rust] nannou のベクトルライブラリ glam の作り込みがやばかった


はじめに

nannou で使われているベクトルライブラリ glam の作り込みがやばかったので、2Dを扱うときに便利な f32 の成分を持つ Vec2 型を中心に(自分が使い方を覚えるために)紹介していきます。

基本的な生成方法

use nannou::glam::*;

vec2(2.0, 3.0)        // => Vec2(2.0, 3.0)
Vec2::new(2.0, 3.0)   // => Vec2(2.0, 3.0)

// 全成分が同じでよい時
Vec2::splat(2.0)      // => Vec2(2.0, 2.0)

他の型から生成

// 有能
Vec2::from((2.0, 3.0))               // => Vec2(2.0, 3.0)
Vec2::from([2.0, 3.0])               // => Vec2(2.0, 3.0)
Vec2::from(vec2(2.0, 3.0))           // => Vec2(2.0, 3.0)
Vec2::from(Vec3::new(2.0, 3.0, 4.0)) // => Vec2(2.0, 3.0)

// 配列の参照から生成
Vec2::from_slice(&[2.0, 3.0])        // => Vec2(2.0, 3.0)
  • from は似たような構成のものならなんでもいける

他の型に変換

// 3次元化
vec2(2.0, 3.0).extend(4.0)     // => Vec3(2.0, 3.0, 4.0)

// 逆に2次元化
vec3(2.0, 3.0, 4.0).truncate() // => Vec2(2.0, 3.0)

// 精度を変更する (D→f64 I→i32 U→u32)
vec2(2.0, 3.0).as_f64()        // => DVec2(2.0, 3.0)
vec2(2.0, 3.0).as_i32()        // => IVec2(2, 3)
vec2(2.0, 3.0).as_u32()        // => UVec2(2, 3)

// 配列化
vec2(2.0, 3.0).to_array()      // => [2.0, 3.0]

// 文字列化
vec2(2.0, 3.0).to_string()     // => "[2, 3]"

四則演算

vec2(2.0, 3.0) + 1.0            // => Vec2(3.0, 4.0)
vec2(2.0, 3.0) - 1.0            // => Vec2(1.0, 2.0)
vec2(2.0, 3.0) * 2.0            // => Vec2(4.0, 6.0)
vec2(2.0, 3.0) / 2.0            // => Vec2(1.0, 1.5)

vec2(2.0, 3.0) + vec2(4.0, 5.0) // => Vec2(6.0, 8.0)
vec2(2.0, 3.0) - vec2(4.0, 5.0) // => Vec2(-2.0, -2.0)
vec2(2.0, 3.0) * vec2(4.0, 5.0) // => Vec2(8.0, 15.0)
vec2(2.0, 3.0) / vec2(4.0, 5.0) // => Vec2(0.5, 0.6)

特徴として

  • Vector + 値
  • Vector - 値
  • Vector * Vector
  • Vector / Vector

がそのままできるのがめちゃくちゃありがたい。
こうなってほしいがそのまま書ける理想形!

ついでに書くと Ruby の標準ベクトルライブラリとは設計思想が(自分には)合わないところがあった(小声)

Ruby
require "matrix"
Vector[2, 3] + 1            rescue $! # => #<TypeError: 1 can't be coerced into Vector>
Vector[2, 3] * Vector[4, 5] rescue $! # => #<ExceptionForMatrix::ErrOperationNotDefined: Operation(*) can't be defined: Vector op Vector>

両方に1を足してほしいと考えて Vector[2, 3] + 1 と書いても受け入れてくれない。
成分同士の掛け算をしてほしくて Vector[2, 3] * Vector[4, 5] と書いても拒まれる。
型に優しく使い手の意図を汲み取るのが上手な Ruby なのにやけに厳しい体験が多かった。

結局、Ruby では次のように書かないといけない。

Ruby
require "matrix"
Vector[2, 3] + Vector[1, 1]                     # => Vector[3, 4]
Vector[2, 3].map2(Vector[4, 5]) {|a, b| a * b } # => Vector[8, 15]

累乗・自然対数・反転

vec2(2.0, 3.0).powf(2.0) // => Vec2(4.0, 9.0)
vec2(2.0, 3.0).exp()     // => Vec2(7.389056, 20.085537)
-vec2(2.0, 3.0)          // => Vec2(-2.0, -3.0)

小数部の丸め

vec2(2.4, 3.5).floor() // => Vec2(2.0, 3.0)
vec2(2.4, 3.5).ceil()  // => Vec2(3.0, 4.0)
vec2(2.4, 3.5).round() // => Vec2(2.0, 4.0)

範囲補正

vec2(2.0, 5.0).clamp(vec2(3.0, 3.0), vec2(4.0, 4.0))   // => Vec2(3.0, 4.0)
  • clamp(min, max)(x.clamp(min.x, max.x), y.clamp(min.y, max.y)

成分を元にした変換

// 絶対値
vec2(-2.0, -3.0).abs() // => Vec2(2.0, 3.0)

// 符号だけ
vec2(-2.0, 3.0).signum() // => Vec2(-1.0, 1.0)

// 成分から自身の floor を引く
// -2.4 - -3 → 0.6
//  5.8 -  5 → 0.8
vec2(-2.4, 5.8).fract() // => Vec2(0.5999999, 0.8000002)

・fract は断片を表わす fraction の略と思われる
・符号だけベクトルは過去に必要になることがあったので最初から入ってるのありがてえ

有限・非数

vec2(2.0, 3.0).is_finite()        // => true
vec2(2.0, f32::NAN).is_nan()      // => true

// 有限成分は false で非数は true として bvec2(false, true) を返す
// これはどういうときに使うのかまだよくわかっていない
vec2(2.0, f32::NAN).is_nan_mask() // => BVec2(0x0, 0xffffffff)

最大最小

// 単に成分が対象
vec2(2.0, 3.0).min_element() // => 2.0
vec2(2.0, 3.0).max_element() // => 3.0

各成分同士の最大・最小

vec2(1.0, 2.0).max(vec2(3.0, 1.0)) // => Vec2(3.0, 2.0)
vec2(1.0, 2.0).min(vec2(3.0, 1.0)) // => Vec2(1.0, 1.0)
  • max = (x.max(other.x), y.max(other.y))
  • min = (x.min(other.x), y.min(other.y))
  • どちらかのベクトルを返すのではない
  • 各成分毎の結果でマージされる感じになる

一致・不一致

vec2(2.0, 3.0) == vec2(2.0, 3.0) // => true
vec2(2.0, 3.0) != vec2(4.0, 5.0) // => true

小数の誤差に注意しよう

誤差を許容した比較

vec2(2.0, 3.0).abs_diff_eq(vec2(2.0, 3.1), 0.09) // => false
vec2(2.0, 3.0).abs_diff_eq(vec2(2.0, 3.1), 0.1)  // => true

小数を扱う場合、誤差で一致しない罠にはまりがちなのでこれ重要!

比較した結果を BVec2 型で返す

vec2(2.0, 3.0).cmpeq(vec2(2.0, 3.0)) // => BVec2(0xffffffff, 0xffffffff)
vec2(2.0, 3.0).cmpne(vec2(2.0, 3.0)) // => BVec2(0x0, 0x0)
vec2(2.0, 3.0).cmpge(vec2(2.0, 3.0)) // => BVec2(0xffffffff, 0xffffffff)
vec2(2.0, 3.0).cmpgt(vec2(2.0, 3.0)) // => BVec2(0x0, 0x0)
vec2(2.0, 3.0).cmple(vec2(2.0, 3.0)) // => BVec2(0xffffffff, 0xffffffff)
vec2(2.0, 3.0).cmplt(vec2(2.0, 3.0)) // => BVec2(0x0, 0x0)

これはどういうときに使うんだろう?
(わかったらあとで書く)

長さ(大きさ)

// x**2 + y**2 → 13
vec2(2.0, 3.0).length_squared() // => 13.0

// 上の結果の平方根が長さ
13_f32.sqrt()                   // => 3.6055512
vec2(2.0, 3.0).length()         // => 3.6055512

// 1.0 / length
vec2(2.0, 3.0).length_recip()   // => 0.2773501
  • 使用頻度の高い便利メソッドだけど各ライブラリでなかなか名前が統一されない

  • 長さの大小比較であれば length_squared の結果を使えば sqrt 2回ぶん計算を省ける

  • 1.0 / length を行う length_recip はどういうときに使うんだろう?

長さ補正

// まず (5, 5) の長さは 7.07
vec2(5.0, 5.0).length()                   // => 7.071068

// (6, 6) の長さは 8.48 だけど最大 7.07 までとしたら (5, 5) に下げられる
vec2(6.0, 6.0).length()                   // => 8.485281
vec2(6.0, 6.0).clamp_length_max(7.071068) // => Vec2(5.0, 5.0)

// 同様に (4, 4) の長さは 5.65 だけど最小 7.07 に達っしてないので (5, 5) に上げられる
vec2(4.0, 4.0).length()                   // => 5.656854
vec2(4.0, 4.0).clamp_length_min(7.071068) // => Vec2(5.0, 5.0)

// 上を同時に指定する場合
// (5, 5) の長さは 7.07 で 7〜8 に補正なので補正されてないことがわかる
vec2(5.0, 5.0).clamp_length(7.0, 8.0)     // => Vec2(5.0, 5.0)

これは長さを補正したあとどうやってベクトル成分を調整しているんだろう?
(わかったらあとで書く)

距離

vec2(10.0, 10.0).distance(vec2(12.0, 13.0))         // => 3.6055512
(vec2(12.0, 13.0) - vec2(10.0, 10.0)).length()      // => 3.6055512
vec2(10.0, 10.0).distance_squared(vec2(12.0, 13.0)) // => 13.0
  • 引数を原点と考えるだけであとは length と同じ
  • a.distance(b) → (b - a).length
  • こちらも比較だけなら sqrt しないぶんだけ distance_squared の方が速い

正規化

// 基本
vec2(2.0, 3.0).normalize()                 // => Vec2(0.5547002, 0.8320503)

// 正規化すると長さが 1.0 になる
vec2(2.0, 3.0).normalize().length()        // => 1.0

// ので方向を維持したまま長さを変更できる
vec2(2.0, 3.0).normalize() * 1.5           // => Vec2(0.8320503, 1.2480755)

// 正規化してある?
vec2(2.0, 3.0).is_normalized()             // => false
vec2(2.0, 3.0).normalize().is_normalized() // => true
  • (x / length, y / length)
  • is_normalizednormalize().length() == 1.0 相当

正規化 0ベクトルの扱い

vec2(2.0, 3.0).normalize()         // => Vec2(0.5547002, 0.8320503)
vec2(0.0, 0.0).normalize()         // => Vec2(NaN, NaN)

vec2(2.0, 3.0).try_normalize()     // => Some(Vec2(0.5547002, 0.8320503))
vec2(0.0, 0.0).try_normalize()     // => None

vec2(2.0, 3.0).normalize_or_zero() // => Vec2(0.5547002, 0.8320503)
vec2(0.0, 0.0).normalize_or_zero() // => Vec2(0.0, 0.0)
  • 0ベクトルの場合 length が 0 になるので 0 除算することになってしまう
  • 3種類あってそれぞれ戻値の形式が異なる
  • 0ベクトルは想定外のときもあれば0ベクトルのままにしといてもらいたいときもあって、それを呼び出し側で対応しているとコードが煩雑になってしまうので、柔軟に選択できるのはありがたい

正規化っぽい何か

vec2(2.0, 3.0).recip() // => Vec2(0.5, 0.33333334)
  • (1.0 / x, 1.0 / y) を行う
  • recip は相互的なを表わす reciprocal の略と思われる
  • 何に使うのかはわからない

線形補間

vec2(10.0, 10.0).lerp(vec2(30.0, 30.0), -0.5) // => Vec2(0.0, 0.0)
vec2(10.0, 10.0).lerp(vec2(30.0, 30.0), 0.0)  // => Vec2(10.0, 10.0)
vec2(10.0, 10.0).lerp(vec2(30.0, 30.0), 0.5)  // => Vec2(20.0, 20.0)
vec2(10.0, 10.0).lerp(vec2(30.0, 30.0), 1.0)  // => Vec2(30.0, 30.0)
vec2(10.0, 10.0).lerp(vec2(30.0, 30.0), 1.5)  // => Vec2(40.0, 40.0)
  • a.lerp(b, s)a + (b - a) * s
  • なので 0.0 のとき (10,10)1.0 のとき (30,30) になっているのがわかる

角度

// 右下・真下・左下
vec2(2.0, 2.0).angle()  // => 0.7853982
vec2(0.0, 2.0).angle()  // => 1.5707963
vec2(-2.0, 2.0).angle() // => 2.3561945

// 以下と一致するので angle は全体を 2 * PI としていることがわかる
PI * 0.25               // => 0.7853982
PI * 0.50               // => 1.5707964
PI * 0.75               // => 2.3561945
  • PI を使うときは use nannou::prelude::*; としよう

角度 2点間

vec2(2.0, 2.0).angle_between(vec2(0.0, 2.0))    // => 0.7853982

// 冗長に書くと
vec2(0.0, 2.0).angle() - vec2(2.0, 2.0).angle() // => 0.78539807
  • a.angle_between(b)b.angle - a.angle 相当
  • 右下.angle_between(真下) なので PI * 0.25 → 0.7853982

回転

vec2(2.0, 0.0).rotate(PI * 0.5).round() // => Vec2(-0.0, 2.0)
  • 右向き (2,0)90度 回転すると真下 (0,2) になったことがわかる

法線

vec2(2.0, 3.0).perp()                          // => Vec2(-3.0, 2.0)

// 本当に回転するとこう
vec2(2.0, 3.0).rotate(2.0 * PI * 90.0 / 360.0) // => Vec2(-3.0, 1.9999999)
  • 長さを維持したまま90度回転する
  • (-y, x) と同じなので一瞬で求まる
  • perp は垂直を表わす perpendicular の略と思われる

内積

vec2(2.0, 3.0).dot(vec2(4.0, 5.0)) // => 23.0
2 * 4 + 3 * 5                      // => 23
  • a.dot(b)a.x * b.x + a.y * b.y

外積

vec2(2.0, 3.0).perp_dot(vec2(4.0, 5.0)) // => -2.0
2 * 5 - 4 * 3                           // => -2
  • a.perp_dot(b)a.x * b.y - b.x * a.y

投影

let A = vec2(3.0, 4.0);
let B = vec2(6.0, 2.0);

// 投影
// AをBに投影する
// 言い替えるとAから垂直にBに線を降ろしたときの交点を返す
A.project_onto(B)                        // => Vec2(3.9, 1.3000001)

// 垂直方向への投影
// 投影した点からAまで
// (A - 投影) なので A - vec2(3.9, 1.3) になる
A.reject_from(B)                         // => Vec2(-0.9000001, 2.6999998)
A - A.project_onto(B)                    // => Vec2(-0.9000001, 2.6999998)

// 投影先が正規化していると使えるシリーズ
A.project_onto_normalized(B.normalize()) // => Vec2(3.8999999, 1.3)
A.reject_from_normalized(B.normalize())  // => Vec2(-0.89999986, 2.7)
  • 正規化していると使えるシリーズがわざわざ用意されている理由は、使えるときはそっちを使った方が速いのかもしれない

成分の入れ替え

vec2(1.0, 2.0).xy() // => Vec2(1.0, 2.0)
vec2(1.0, 2.0).yx() // => Vec2(2.0, 1.0)
vec2(1.0, 2.0).xx() // => Vec2(1.0, 1.0)
vec2(1.0, 2.0).yy() // => Vec2(2.0, 2.0)

vec2(1.0, 2.0).xxx() // => Vec3(1.0, 1.0, 1.0)
vec2(1.0, 2.0).xxy() // => Vec3(1.0, 1.0, 2.0)
vec2(1.0, 2.0).xyx() // => Vec3(1.0, 2.0, 1.0)
vec2(1.0, 2.0).xyy() // => Vec3(1.0, 2.0, 2.0)
vec2(1.0, 2.0).yxx() // => Vec3(2.0, 1.0, 1.0)
vec2(1.0, 2.0).yxy() // => Vec3(2.0, 1.0, 2.0)
vec2(1.0, 2.0).yyx() // => Vec3(2.0, 2.0, 1.0)
vec2(1.0, 2.0).yyy() // => Vec3(2.0, 2.0, 2.0)

vec2(1.0, 2.0).xxxx() // => Vec4(1.0, 1.0, 1.0, 1.0)
vec2(1.0, 2.0).xxxy() // => Vec4(1.0, 1.0, 1.0, 2.0)
vec2(1.0, 2.0).xxyx() // => Vec4(1.0, 1.0, 2.0, 1.0)
vec2(1.0, 2.0).xxyy() // => Vec4(1.0, 1.0, 2.0, 2.0)
vec2(1.0, 2.0).xyxx() // => Vec4(1.0, 2.0, 1.0, 1.0)
vec2(1.0, 2.0).xyxy() // => Vec4(1.0, 2.0, 1.0, 2.0)
vec2(1.0, 2.0).xyyx() // => Vec4(1.0, 2.0, 2.0, 1.0)
vec2(1.0, 2.0).xyyy() // => Vec4(1.0, 2.0, 2.0, 2.0)
vec2(1.0, 2.0).yxxx() // => Vec4(2.0, 1.0, 1.0, 1.0)
vec2(1.0, 2.0).yxxy() // => Vec4(2.0, 1.0, 1.0, 2.0)
vec2(1.0, 2.0).yxyx() // => Vec4(2.0, 1.0, 2.0, 1.0)
vec2(1.0, 2.0).yxyy() // => Vec4(2.0, 1.0, 2.0, 2.0)
vec2(1.0, 2.0).yyxx() // => Vec4(2.0, 2.0, 1.0, 1.0)
vec2(1.0, 2.0).yyxy() // => Vec4(2.0, 2.0, 1.0, 2.0)
vec2(1.0, 2.0).yyyx() // => Vec4(2.0, 2.0, 2.0, 1.0)
vec2(1.0, 2.0).yyyy() // => Vec4(2.0, 2.0, 2.0, 2.0)

定数

Vec2::ZERO      // => Vec2(0.0, 0.0)
Vec2::ONE       // => Vec2(1.0, 1.0)

Vec2::X         // => Vec2(1.0, 0.0)
Vec2::Y         // => Vec2(0.0, 1.0)
Vec2::AXES      // => [Vec2(1.0, 0.0), Vec2(0.0, 1.0)]

Vec2::default() // => Vec2(0.0, 0.0)

ありがてえ。
十字コントローラー的なものを扱うときに重宝しそうな Vec2::AXES まで入っている。

いろんな種類がある

vec2(2.0, 3.0)      // => Vec2(2.0, 3.0)
ivec2(-2, -3)       // => IVec2(-2, -3)
uvec2(2, 3)         // => UVec2(2, 3)
dvec2(2.0, 3.0)     // => DVec2(2.0, 3.0)
  • 2D符号あり整数 → UVec2
  • 3D用 → Vec3
  • などいろんな型が用意されている
  • Features を見ると全容がわかる

SIMD対応

Vec3A::new(2.0, 3.0, 4.0) // => Vec3A(2.0, 3.0, 4.0)

A がついたタイプを使うと SIMD 命令が使われる!

論理型ベクトル

何に使うのかまったくわからないが異彩を放っているので一応見ていく。

// 生成
BVec2::new(true, false)       // => BVec2(0xffffffff, 0x0)

// どれかが true か?
BVec2::new(true, false).any() // => true

// 全部 true か?
BVec2::new(true, true).all()  // => true

// 論理演算 (AND, OR, NOT)
BVec2::new(true, true) & BVec2::new(true, false)  // => BVec2(0xffffffff, 0x0)
BVec2::new(true, false) | BVec2::new(false, true) // => BVec2(0xffffffff, 0xffffffff)
!BVec2::new(true, false)                          // => BVec2(0x0, 0xffffffff)

// LSB から順に x y と並べたときの値を返す
BVec2::new(false, false).bitmask() // => 0
BVec2::new(true, false).bitmask()  // => 1
BVec2::new(false, true).bitmask()  // => 2
BVec2::new(true, true).bitmask()   // => 3

やっぱり何に使うのかわからない

nannou の Vertex* 型への変換

use nannou::geom::Vertex2d;
use nannou::geom::Vertex3d;

vec2(2.0, 3.0).point2()      // => [2.0, 3.0]
vec3(2.0, 3.0, 4.0).point3() // => [2.0, 3.0, 4.0]
  • ベクトルと点(頂点)は明確に型が分けられている
  • これは nannou が glam を拡張しているようだ