lib/linear/src/affine.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! Affine transforms of 3D space: a linear part followed by a translation.
  2 //!
  3 //! An affine transform maps a point `p` to `linear p + translation` and a
  4 //! direction `v` to `linear v`. It stores twelve lanes instead of a Mat4's
  5 //! sixteen, and it inverts through a 3x3 inverse.
  6 const std = @import("std");
  7 const matrix = @import("matrix.zig");
  8 const Quat = @import("quaternion.zig").Quat;
  9 const Vec3 = @import("vector.zig").Vec3;
 10 
 11 const assert = std.debug.assert;
 12 const Mat3 = matrix.Mat3;
 13 const Mat4 = matrix.Mat4;
 14 
 15 pub const Affine = extern struct {
 16     linear: Mat3 = .{},
 17     translation: Vec3 = .{},
 18 
 19     pub const identity: Affine = .{};
 20 
 21     pub fn fromTranslation(translation: Vec3) Affine {
 22         return .{ .translation = translation };
 23     }
 24 
 25     /// Scale, then rotate by the unit quaternion `rotation`, then translate.
 26     pub fn fromScaleRotationTranslation(s: Vec3, rotation: Quat, translation: Vec3) Affine {
 27         return .{
 28             .linear = Mat3.fromQuat(rotation).mul(Mat3.fromScale(s)),
 29             .translation = translation,
 30         };
 31     }
 32 
 33     /// `a b`: applying `b`, then `a`.
 34     pub fn mul(a: Affine, b: Affine) Affine {
 35         return .{
 36             .linear = a.linear.mul(b.linear),
 37             .translation = a.transformPoint(b.translation),
 38         };
 39     }
 40 
 41     pub fn transformPoint(a: Affine, p: Vec3) Vec3 {
 42         return a.linear.mulVec(p).add(a.translation);
 43     }
 44 
 45     pub fn transformVector(a: Affine, v: Vec3) Vec3 {
 46         return a.linear.mulVec(v);
 47     }
 48 
 49     /// The matrix that carries surface normals through `a`, or null when the
 50     /// linear part is singular. Transformed normals keep their direction but
 51     /// not their length.
 52     pub fn normalMatrix(a: Affine) ?Mat3 {
 53         return a.linear.inverseTranspose();
 54     }
 55 
 56     /// The inverse transform, or null when the linear part is singular.
 57     pub fn inverse(a: Affine) ?Affine {
 58         const linear = a.linear.inverse() orelse return null;
 59         return .{
 60             .linear = linear,
 61             .translation = linear.mulVec(a.translation).negate(),
 62         };
 63     }
 64 
 65     pub fn toMat4(a: Affine) Mat4 {
 66         return Mat4.fromLinearTranslation(a.linear, a.translation);
 67     }
 68 };
 69 
 70 comptime {
 71     assert(@sizeOf(Affine) == @sizeOf([12]f32));
 72     assert(@alignOf(Affine) == @alignOf(f32));
 73 }
 74 
 75 const testing = std.testing;
 76 const tolerance: f32 = 1e-5;
 77 const Vec4 = @import("vector.zig").Vec4;
 78 
 79 fn expectApproxVec3(expected: Vec3, actual: Vec3) !void {
 80     try testing.expectApproxEqAbs(expected.x, actual.x, tolerance);
 81     try testing.expectApproxEqAbs(expected.y, actual.y, tolerance);
 82     try testing.expectApproxEqAbs(expected.z, actual.z, tolerance);
 83 }
 84 
 85 const sample = Affine.fromScaleRotationTranslation(
 86     Vec3.init(2, 0.5, 3),
 87     Quat.fromAxisAngle(Vec3.init(0, 0.6, 0.8), 0.7),
 88     Vec3.init(1, -2, 4),
 89 );
 90 
 91 test "points translate and vectors do not" {
 92     const shift = Affine.fromTranslation(Vec3.init(1, 2, 3));
 93     try testing.expectEqual(Vec3.init(2, 2, 3), shift.transformPoint(Vec3.init(1, 0, 0)));
 94     try testing.expectEqual(Vec3.init(1, 0, 0), shift.transformVector(Vec3.init(1, 0, 0)));
 95     try testing.expectEqual(Vec3.init(4, 5, 6), Affine.identity.transformPoint(Vec3.init(4, 5, 6)));
 96 }
 97 
 98 test "scale applies before rotation and translation" {
 99     const a = Affine.fromScaleRotationTranslation(
100         Vec3.init(2, 1, 1),
101         Quat.fromAxisAngle(Vec3.init(0, 0, 1), std.math.pi / 2.0),
102         Vec3.init(0, 0, 5),
103     );
104     try expectApproxVec3(Vec3.init(0, 2, 5), a.transformPoint(Vec3.init(1, 0, 0)));
105 }
106 
107 test "product composes right to left" {
108     const other = Affine.fromScaleRotationTranslation(
109         Vec3.init(1, 1, 2),
110         Quat.fromAxisAngle(Vec3.init(1, 0, 0), -0.4),
111         Vec3.init(0.5, 0, 0),
112     );
113     const p = Vec3.init(0.3, 0.9, -1.1);
114     try expectApproxVec3(sample.transformPoint(other.transformPoint(p)), sample.mul(other).transformPoint(p));
115 }
116 
117 test "inverse undoes the transform and fails on a singular linear part" {
118     const p = Vec3.init(-0.4, 2.5, 0.8);
119     try expectApproxVec3(p, sample.inverse().?.transformPoint(sample.transformPoint(p)));
120     const flat = Affine{ .linear = Mat3.fromScale(Vec3.init(1, 1, 0)) };
121     try testing.expectEqual(@as(?Affine, null), flat.inverse());
122     try testing.expectEqual(@as(?Mat3, null), flat.normalMatrix());
123 }
124 
125 test "normals stay perpendicular under nonuniform scale" {
126     const tangent = Vec3.init(1, -1, 0);
127     const normal = Vec3.init(1, 1, 0);
128     const moved_tangent = sample.transformVector(tangent);
129     const moved_normal = sample.normalMatrix().?.mulVec(normal);
130     try testing.expectApproxEqAbs(@as(f32, 0), moved_tangent.dot(moved_normal), tolerance);
131 }
132 
133 test "homogeneous matrix agrees with the affine transform" {
134     const p = Vec3.init(0.7, -0.2, 1.3);
135     const moved = sample.toMat4().mulVec(Vec4.fromVec3(p, 1));
136     try expectApproxVec3(sample.transformPoint(p), moved.xyz());
137     try testing.expectEqual(@as(f32, 1), moved.w);
138 }