larb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/larb/quat.rb ADDED
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Larb
4
+ class Quat
5
+ attr_accessor :x, :y, :z, :w
6
+
7
+ def initialize(x = 0.0, y = 0.0, z = 0.0, w = 1.0)
8
+ @x = x.to_f
9
+ @y = y.to_f
10
+ @z = z.to_f
11
+ @w = w.to_f
12
+ end
13
+
14
+ def self.[](x, y, z, w)
15
+ new(x, y, z, w)
16
+ end
17
+
18
+ def self.identity
19
+ new(0, 0, 0, 1)
20
+ end
21
+
22
+ def self.from_axis_angle(axis, radians)
23
+ half = radians * 0.5
24
+ s = Math.sin(half)
25
+ axis = axis.normalize
26
+ new(axis.x * s, axis.y * s, axis.z * s, Math.cos(half))
27
+ end
28
+
29
+ def self.from_euler(x, y, z)
30
+ cx = Math.cos(x * 0.5)
31
+ sx = Math.sin(x * 0.5)
32
+ cy = Math.cos(y * 0.5)
33
+ sy = Math.sin(y * 0.5)
34
+ cz = Math.cos(z * 0.5)
35
+ sz = Math.sin(z * 0.5)
36
+
37
+ new(
38
+ sx * cy * cz - cx * sy * sz,
39
+ cx * sy * cz + sx * cy * sz,
40
+ cx * cy * sz - sx * sy * cz,
41
+ cx * cy * cz + sx * sy * sz
42
+ )
43
+ end
44
+
45
+ def self.look_rotation(forward, up = Vec3.up)
46
+ forward = forward.normalize
47
+ right = up.cross(forward).normalize
48
+ up = forward.cross(right)
49
+
50
+ m00 = right.x
51
+ m01 = up.x
52
+ m02 = forward.x
53
+ m10 = right.y
54
+ m11 = up.y
55
+ m12 = forward.y
56
+ m20 = right.z
57
+ m21 = up.z
58
+ m22 = forward.z
59
+
60
+ trace = m00 + m11 + m22
61
+ if trace > 0
62
+ s = 0.5 / Math.sqrt(trace + 1.0)
63
+ new((m21 - m12) * s, (m02 - m20) * s, (m10 - m01) * s, 0.25 / s)
64
+ elsif m00 > m11 && m00 > m22
65
+ s = 2.0 * Math.sqrt(1.0 + m00 - m11 - m22)
66
+ new(0.25 * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s)
67
+ elsif m11 > m22
68
+ s = 2.0 * Math.sqrt(1.0 + m11 - m00 - m22)
69
+ new((m01 + m10) / s, 0.25 * s, (m12 + m21) / s, (m02 - m20) / s)
70
+ else
71
+ s = 2.0 * Math.sqrt(1.0 + m22 - m00 - m11)
72
+ new((m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s)
73
+ end
74
+ end
75
+
76
+ def *(other)
77
+ case other
78
+ when Quat
79
+ Quat.new(
80
+ @w * other.x + @x * other.w + @y * other.z - @z * other.y,
81
+ @w * other.y - @x * other.z + @y * other.w + @z * other.x,
82
+ @w * other.z + @x * other.y - @y * other.x + @z * other.w,
83
+ @w * other.w - @x * other.x - @y * other.y - @z * other.z
84
+ )
85
+ when Vec3
86
+ qv = Vec3.new(@x, @y, @z)
87
+ uv = qv.cross(other)
88
+ uuv = qv.cross(uv)
89
+ other + (uv * @w + uuv) * 2
90
+ when Numeric
91
+ Quat.new(@x * other, @y * other, @z * other, @w * other)
92
+ end
93
+ end
94
+
95
+ def +(other)
96
+ Quat.new(@x + other.x, @y + other.y, @z + other.z, @w + other.w)
97
+ end
98
+
99
+ def -(other)
100
+ Quat.new(@x - other.x, @y - other.y, @z - other.z, @w - other.w)
101
+ end
102
+
103
+ def -@
104
+ Quat.new(-@x, -@y, -@z, -@w)
105
+ end
106
+
107
+ def dot(other)
108
+ @x * other.x + @y * other.y + @z * other.z + @w * other.w
109
+ end
110
+
111
+ def length
112
+ Math.sqrt(length_squared)
113
+ end
114
+
115
+ def length_squared
116
+ @x * @x + @y * @y + @z * @z + @w * @w
117
+ end
118
+
119
+ def normalize
120
+ l = length
121
+ Quat.new(@x / l, @y / l, @z / l, @w / l)
122
+ end
123
+
124
+ def normalize!
125
+ l = length
126
+ @x /= l
127
+ @y /= l
128
+ @z /= l
129
+ @w /= l
130
+ self
131
+ end
132
+
133
+ def conjugate
134
+ Quat.new(-@x, -@y, -@z, @w)
135
+ end
136
+
137
+ def inverse
138
+ len_sq = length_squared
139
+ Quat.new(-@x / len_sq, -@y / len_sq, -@z / len_sq, @w / len_sq)
140
+ end
141
+
142
+ def lerp(other, t)
143
+ Quat.new(
144
+ @x + (other.x - @x) * t,
145
+ @y + (other.y - @y) * t,
146
+ @z + (other.z - @z) * t,
147
+ @w + (other.w - @w) * t
148
+ ).normalize
149
+ end
150
+
151
+ def slerp(other, t)
152
+ dot_val = dot(other)
153
+
154
+ other = -other if dot_val < 0
155
+ dot_val = dot_val.abs
156
+
157
+ if dot_val > 0.9995
158
+ return lerp(other, t)
159
+ end
160
+
161
+ theta_0 = Math.acos(dot_val.clamp(-1.0, 1.0))
162
+ theta = theta_0 * t
163
+ sin_theta = Math.sin(theta)
164
+ sin_theta_0 = Math.sin(theta_0)
165
+
166
+ s0 = Math.cos(theta) - dot_val * sin_theta / sin_theta_0
167
+ s1 = sin_theta / sin_theta_0
168
+
169
+ Quat.new(
170
+ @x * s0 + other.x * s1,
171
+ @y * s0 + other.y * s1,
172
+ @z * s0 + other.z * s1,
173
+ @w * s0 + other.w * s1
174
+ )
175
+ end
176
+
177
+ def to_axis_angle
178
+ angle = 2 * Math.acos(@w.clamp(-1.0, 1.0))
179
+ s = Math.sqrt(1 - @w * @w)
180
+ if s < 0.001
181
+ axis = Vec3.new(1, 0, 0)
182
+ else
183
+ axis = Vec3.new(@x / s, @y / s, @z / s)
184
+ end
185
+ [axis, angle]
186
+ end
187
+
188
+ def to_euler
189
+ sinr_cosp = 2 * (@w * @x + @y * @z)
190
+ cosr_cosp = 1 - 2 * (@x * @x + @y * @y)
191
+ roll = Math.atan2(sinr_cosp, cosr_cosp)
192
+
193
+ sinp = 2 * (@w * @y - @z * @x)
194
+ pitch = if sinp.abs >= 1
195
+ Math.copysign(Math::PI / 2, sinp)
196
+ else
197
+ Math.asin(sinp)
198
+ end
199
+
200
+ siny_cosp = 2 * (@w * @z + @x * @y)
201
+ cosy_cosp = 1 - 2 * (@y * @y + @z * @z)
202
+ yaw = Math.atan2(siny_cosp, cosy_cosp)
203
+
204
+ Vec3.new(roll, pitch, yaw)
205
+ end
206
+
207
+ def to_mat4
208
+ Mat4.from_quaternion(self)
209
+ end
210
+
211
+ def to_a
212
+ [@x, @y, @z, @w]
213
+ end
214
+
215
+ def [](i)
216
+ to_a[i]
217
+ end
218
+
219
+ def ==(other)
220
+ return false unless other.is_a?(Quat)
221
+
222
+ @x == other.x && @y == other.y && @z == other.z && @w == other.w
223
+ end
224
+
225
+ def near?(other, epsilon = 1e-6)
226
+ (@x - other.x).abs < epsilon &&
227
+ (@y - other.y).abs < epsilon &&
228
+ (@z - other.z).abs < epsilon &&
229
+ (@w - other.w).abs < epsilon
230
+ end
231
+
232
+ def inspect
233
+ "Quat[#{@x}, #{@y}, #{@z}, #{@w}]"
234
+ end
235
+
236
+ alias to_s inspect
237
+ end
238
+ end
data/lib/larb/quat2.rb ADDED
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Larb
4
+ # Dual Quaternion for rigid body transformations
5
+ # Represented as 8 values: [real quaternion (4), dual quaternion (4)]
6
+ # real: rotation, dual: translation encoded
7
+ class Quat2
8
+ attr_reader :data
9
+
10
+ def initialize(data = nil)
11
+ @data = data || [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]
12
+ end
13
+
14
+ def self.identity
15
+ new
16
+ end
17
+
18
+ def self.from_rotation_translation(rotation, translation)
19
+ rx, ry, rz, rw = rotation.x, rotation.y, rotation.z, rotation.w
20
+ tx, ty, tz = translation.x, translation.y, translation.z
21
+
22
+ new([
23
+ rx, ry, rz, rw,
24
+ (tx * rw + ty * rz - tz * ry) * 0.5,
25
+ (ty * rw + tz * rx - tx * rz) * 0.5,
26
+ (tz * rw + tx * ry - ty * rx) * 0.5,
27
+ (-tx * rx - ty * ry - tz * rz) * 0.5
28
+ ])
29
+ end
30
+
31
+ def self.from_translation(translation)
32
+ from_rotation_translation(Quat.identity, translation)
33
+ end
34
+
35
+ def self.from_rotation(rotation)
36
+ new([rotation.x, rotation.y, rotation.z, rotation.w, 0, 0, 0, 0])
37
+ end
38
+
39
+ def self.from_mat4(m)
40
+ rotation = m.extract_rotation
41
+ translation = m.extract_translation
42
+ from_rotation_translation(rotation, translation)
43
+ end
44
+
45
+ def real
46
+ Quat.new(@data[0], @data[1], @data[2], @data[3])
47
+ end
48
+
49
+ def dual
50
+ Quat.new(@data[4], @data[5], @data[6], @data[7])
51
+ end
52
+
53
+ def [](i)
54
+ @data[i]
55
+ end
56
+
57
+ def []=(i, v)
58
+ @data[i] = v.to_f
59
+ end
60
+
61
+ def *(other)
62
+ case other
63
+ when Quat2
64
+ ax0, ay0, az0, aw0 = @data[0], @data[1], @data[2], @data[3]
65
+ bx1, by1, bz1, bw1 = @data[4], @data[5], @data[6], @data[7]
66
+ ax1, ay1, az1, aw1 = other.data[0], other.data[1], other.data[2], other.data[3]
67
+ bx0, by0, bz0, bw0 = other.data[4], other.data[5], other.data[6], other.data[7]
68
+
69
+ Quat2.new([
70
+ ax0 * aw1 + aw0 * ax1 + ay0 * az1 - az0 * ay1,
71
+ ay0 * aw1 + aw0 * ay1 + az0 * ax1 - ax0 * az1,
72
+ az0 * aw1 + aw0 * az1 + ax0 * ay1 - ay0 * ax1,
73
+ aw0 * aw1 - ax0 * ax1 - ay0 * ay1 - az0 * az1,
74
+ ax0 * bw0 + aw0 * bx0 + ay0 * bz0 - az0 * by0 +
75
+ bx1 * aw1 + bw1 * ax1 + by1 * az1 - bz1 * ay1,
76
+ ay0 * bw0 + aw0 * by0 + az0 * bx0 - ax0 * bz0 +
77
+ by1 * aw1 + bw1 * ay1 + bz1 * ax1 - bx1 * az1,
78
+ az0 * bw0 + aw0 * bz0 + ax0 * by0 - ay0 * bx0 +
79
+ bz1 * aw1 + bw1 * az1 + bx1 * ay1 - by1 * ax1,
80
+ aw0 * bw0 - ax0 * bx0 - ay0 * by0 - az0 * bz0 +
81
+ bw1 * aw1 - bx1 * ax1 - by1 * ay1 - bz1 * az1
82
+ ])
83
+ when Vec3
84
+ transform_point(other)
85
+ when Numeric
86
+ Quat2.new(@data.map { |v| v * other })
87
+ end
88
+ end
89
+
90
+ def +(other)
91
+ Quat2.new(@data.zip(other.data).map { |a, b| a + b })
92
+ end
93
+
94
+ def -(other)
95
+ Quat2.new(@data.zip(other.data).map { |a, b| a - b })
96
+ end
97
+
98
+ def dot(other)
99
+ @data[0] * other.data[0] + @data[1] * other.data[1] +
100
+ @data[2] * other.data[2] + @data[3] * other.data[3]
101
+ end
102
+
103
+ def length
104
+ Math.sqrt(length_squared)
105
+ end
106
+
107
+ def length_squared
108
+ @data[0..3].sum { |v| v * v }
109
+ end
110
+
111
+ def normalize
112
+ len = length
113
+ return Quat2.new(@data.dup) if len < 1e-10
114
+
115
+ inv_len = 1.0 / len
116
+ Quat2.new(@data.map { |v| v * inv_len })
117
+ end
118
+
119
+ def normalize!
120
+ len = length
121
+ return self if len < 1e-10
122
+
123
+ inv_len = 1.0 / len
124
+ @data.map! { |v| v * inv_len }
125
+ self
126
+ end
127
+
128
+ def conjugate
129
+ Quat2.new([
130
+ -@data[0], -@data[1], -@data[2], @data[3],
131
+ -@data[4], -@data[5], -@data[6], @data[7]
132
+ ])
133
+ end
134
+
135
+ def inverse
136
+ len_sq = length_squared
137
+ return conjugate if len_sq < 1e-10
138
+
139
+ inv_len_sq = 1.0 / len_sq
140
+ conj = conjugate
141
+ Quat2.new(conj.data.map { |v| v * inv_len_sq })
142
+ end
143
+
144
+ def translation
145
+ ax, ay, az, aw = @data[0], @data[1], @data[2], @data[3]
146
+ bx, by, bz, bw = @data[4], @data[5], @data[6], @data[7]
147
+ # t = 2 * dual * conjugate(real)
148
+ Vec3.new(
149
+ 2 * (-bw * ax + bx * aw - by * az + bz * ay),
150
+ 2 * (-bw * ay + by * aw - bz * ax + bx * az),
151
+ 2 * (-bw * az + bz * aw - bx * ay + by * ax)
152
+ )
153
+ end
154
+
155
+ def rotation
156
+ real.normalize
157
+ end
158
+
159
+ def transform_point(point)
160
+ rotation * point + translation
161
+ end
162
+
163
+ def lerp(other, t)
164
+ Quat2.new(@data.zip(other.data).map { |a, b| a + (b - a) * t }).normalize
165
+ end
166
+
167
+ def to_mat4
168
+ rot = rotation
169
+ trans = translation
170
+ Mat4.from_quaternion(rot) * Mat4.translation(trans.x, trans.y, trans.z)
171
+ end
172
+
173
+ def to_a
174
+ @data.dup
175
+ end
176
+
177
+ def ==(other)
178
+ return false unless other.is_a?(Quat2)
179
+
180
+ @data == other.data
181
+ end
182
+
183
+ def near?(other, epsilon = 1e-6)
184
+ @data.zip(other.data).all? { |a, b| (a - b).abs < epsilon }
185
+ end
186
+
187
+ def inspect
188
+ "Quat2[real: #{real.inspect}, dual: #{dual.inspect}]"
189
+ end
190
+
191
+ alias to_s inspect
192
+ end
193
+ end
data/lib/larb/vec2.rb ADDED
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Larb
4
+ class Vec2
5
+ attr_accessor :x, :y
6
+
7
+ def initialize(x = 0.0, y = 0.0)
8
+ @x = x.to_f
9
+ @y = y.to_f
10
+ end
11
+
12
+ def self.[](x, y)
13
+ new(x, y)
14
+ end
15
+
16
+ def self.zero
17
+ new(0, 0)
18
+ end
19
+
20
+ def self.one
21
+ new(1, 1)
22
+ end
23
+
24
+ def +(other)
25
+ Vec2.new(@x + other.x, @y + other.y)
26
+ end
27
+
28
+ def -(other)
29
+ Vec2.new(@x - other.x, @y - other.y)
30
+ end
31
+
32
+ def *(scalar)
33
+ Vec2.new(@x * scalar, @y * scalar)
34
+ end
35
+
36
+ def /(scalar)
37
+ Vec2.new(@x / scalar, @y / scalar)
38
+ end
39
+
40
+ def -@
41
+ Vec2.new(-@x, -@y)
42
+ end
43
+
44
+ def dot(other)
45
+ @x * other.x + @y * other.y
46
+ end
47
+
48
+ def length
49
+ Math.sqrt(@x * @x + @y * @y)
50
+ end
51
+
52
+ def length_squared
53
+ @x * @x + @y * @y
54
+ end
55
+
56
+ def normalize
57
+ self / length
58
+ end
59
+
60
+ def normalize!
61
+ l = length
62
+ @x /= l
63
+ @y /= l
64
+ self
65
+ end
66
+
67
+ def lerp(other, t)
68
+ self + (other - self) * t
69
+ end
70
+
71
+ def to_a
72
+ [@x, @y]
73
+ end
74
+
75
+ def [](i)
76
+ to_a[i]
77
+ end
78
+
79
+ def []=(i, v)
80
+ if i == 0
81
+ @x = v
82
+ else
83
+ @y = v
84
+ end
85
+ end
86
+
87
+ def ==(other)
88
+ return false unless other.is_a?(Vec2)
89
+
90
+ @x == other.x && @y == other.y
91
+ end
92
+
93
+ def near?(other, epsilon = 1e-6)
94
+ (@x - other.x).abs < epsilon && (@y - other.y).abs < epsilon
95
+ end
96
+
97
+ def angle
98
+ Math.atan2(@y, @x)
99
+ end
100
+
101
+ def angle_to(other)
102
+ Math.atan2(other.y - @y, other.x - @x)
103
+ end
104
+
105
+ def rotate(radians)
106
+ c = Math.cos(radians)
107
+ s = Math.sin(radians)
108
+ Vec2.new(@x * c - @y * s, @x * s + @y * c)
109
+ end
110
+
111
+ def distance(other)
112
+ Math.sqrt(distance_squared(other))
113
+ end
114
+
115
+ def distance_squared(other)
116
+ dx = @x - other.x
117
+ dy = @y - other.y
118
+ dx * dx + dy * dy
119
+ end
120
+
121
+ def cross(other)
122
+ @x * other.y - @y * other.x
123
+ end
124
+
125
+ def perpendicular
126
+ Vec2.new(-@y, @x)
127
+ end
128
+
129
+ def reflect(normal)
130
+ self - normal * (2 * dot(normal))
131
+ end
132
+
133
+ def clamp_length(max_length)
134
+ len_sq = length_squared
135
+ return self if len_sq <= max_length * max_length
136
+
137
+ self * (max_length / Math.sqrt(len_sq))
138
+ end
139
+
140
+ def to_vec3(z = 0.0)
141
+ Vec3.new(@x, @y, z)
142
+ end
143
+
144
+ def inspect
145
+ "Vec2[#{@x}, #{@y}]"
146
+ end
147
+
148
+ alias to_s inspect
149
+ end
150
+ end