🦔

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

2022/03/31に公開約13,700字

はじめに

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 の標準ライブラリだった matrix はその点がちょっと使いづらかった(小声)

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
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

// 以下と同じ
2.0.atan2(2.0)          // => 0.7853981633974483
2.0.atan2(0.0)          // => 1.5707963267948966
2.0.atan2(-2.0)         // => 2.356194490192345

// 方向から求める
PI * 0.25               // => 0.7853982
PI * 0.50               // => 1.5707964
PI * 0.75               // => 2.3561945
  • vec2(x, y).angle()y.atan2(x)
  • PI を使うときは use nannou::prelude::*; とする
  • 一周は PI * 2

角度 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 の略と思われる
  • 法線は長さ1なので perp のことを法線と言うのは違うのかもしれない(まーいいか)

内積

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
  • a.dot(b.normalize()) で a を b に投射したときの長さが求まる

外積

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(2.0, 5.0);
let B  = vec2(6.0, 3.0);
let BN = B.normalize();
// AをBに投影する
// Bを地面と考えたとき太陽視点でのAの影に相当する
// またはBを地面として垂直にジャンプしたAの着地点に相当する
A.project_onto(B)             // => Vec2(3.6000001, 1.8000001)

// project_onto がやっていること
// 1. 地面の方を正規化して内積を求めると影の長さが求まる
// 2. それを地面の方向に伸ばす(正規化したBだけスケールする)と位置が求まる
let length = A.dot(BN);
length                        // => 4.0249224
length * BN                   // => Vec2(3.6, 1.8)

// 投影先が正規化されていればその処理を省けるので専用のメソッドがある
A.project_onto_normalized(BN) // => Vec2(3.6, 1.8)
// 垂直方向への投影
// Bを地面と考えたときの真横から強い光を当てて壁にできる影に相当する
// またはBを地面と考えたときのジャンプしたAの高さに相当する
// これは単に A - 着地点(AからBへの投射) で求まる
// 上の投影と同様に地面が正規化されている版もある
A.reject_from(B)                         // => Vec2(-1.6000001, 3.1999998)
A - A.project_onto(B)                    // => Vec2(-1.6000001, 3.1999998)
A.reject_from_normalized(B.normalize())  // => Vec2(-1.5999999, 3.2)

成分の入れ替え

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 を拡張しているようだ

Discussion

ログインするとコメントできます