↗️

nannou のベクトルライブラリ glam の使い方

2022/03/31に公開

https://docs.rs/nannou

https://docs.rs/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)

他の型に変換

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)
vec2(2.0, -3.0).signum();  // => Vec2(1.0, -1.0)
vec2(0.0, 0.0).signum();   // => Vec2(1.0, 1.0)

0 のときも 1.0 になるようだ

成分から自身の floor を引く

vec2(-2.4, 5.8).fract();  // => Vec2(0.5999999, 0.8000002)
  • -2.4 - -3 = 0.6
  • 5.8 - 5 = 0.8
  • 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)

TODO: これはどういうときに使う?

長さ(大きさ)

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
  • 使用頻度の高い便利メソッドだけど各ライブラリでなかなか名前が統一されない

  • Unitymagnitude

  • DirectXlength

  • Rubymagnitude r norm

  • bevylength

  • Wikiノルム

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

  • TODO: 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に投影する

A.project_onto(B);  // => Vec2(3.6000001, 1.8000001)

これはBを地面と考えたとき太陽視点でのAの影またはBを地面として垂直にジャンプしたAの着地点に相当する。

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)

垂直方向への投影

A.reject_from(B);  // => Vec2(-1.6000001, 3.1999998)

これはBを地面と考えたときの真横から強い光を当てて壁にできる影またはBを地面と考えたときのジャンプしたAの高さに相当する。

これは単に A - 着地点(AからBへの投射) でも求まる。

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 まで入っている。

さまざまな型のベクトルがある

ivec2(-2, -3);    // => IVec2(-2, -3)
uvec2(2, 3);      // => UVec2(2, 3)
dvec2(2.0, 3.0);  // => DVec2(2.0, 3.0)
  • 2D符号あり整数 → IVec2
  • 2D符号なし整数 → UVec2
  • 3D用 → Vec3
  • 64bit → DVec2
  • などいろんな型が用意されている

https://docs.rs/glam/latest/glam/#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* 型への変換

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