math: add well-tested, reverse-z 2D projection matrix
Helps hexops/mach#1103 Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
fb69b0cda7
commit
aa2435e10e
1 changed files with 172 additions and 37 deletions
209
src/math/mat.zig
209
src/math/mat.zig
|
|
@ -342,42 +342,51 @@ pub fn Mat(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: mandate negative-Z https://github.com/hexops/mach/issues/1103
|
||||
// /// Constructs an orthographic projection matrix; an orthogonal transformation matrix
|
||||
// /// which transforms from the given left, right, bottom, and top dimensions into
|
||||
// /// `(-1, +1)` in `(x, y)`, and `(0, +1)` in `z`.
|
||||
// ///
|
||||
// /// The near/far parameters denotes the depth (z coordinate) of the near/far clipping
|
||||
// /// plane.
|
||||
// ///
|
||||
// /// Returns an orthographic projection matrix.
|
||||
// // TODO: needs tests
|
||||
// pub inline fn ortho(
|
||||
// /// The sides of the near clipping plane viewport
|
||||
// left: f32,
|
||||
// right: f32,
|
||||
// bottom: f32,
|
||||
// top: f32,
|
||||
// /// The depth (z coordinate) of the near/far clipping plane.
|
||||
// near: f32,
|
||||
// far: f32,
|
||||
// ) Matrix {
|
||||
// const xx = 2 / (right - left);
|
||||
// const yy = 2 / (top - bottom);
|
||||
// const zz = 1 / (near - far);
|
||||
// const tx = (right + left) / (left - right);
|
||||
// const ty = (top + bottom) / (bottom - top);
|
||||
// const tz = near / (near - far);
|
||||
// return init(
|
||||
// &RowVec.init(xx, 0, 0, tx),
|
||||
// &RowVec.init(0, yy, 0, ty),
|
||||
// &RowVec.init(0, 0, zz, tz),
|
||||
// &RowVec.init(0, 0, 0, 1),
|
||||
// );
|
||||
// }
|
||||
|
||||
// TODO: add perspective projection matrix
|
||||
|
||||
/// Constructs a 2D projection matrix, aka. an orthographic projection matrix.
|
||||
///
|
||||
/// First, a cuboid is defined with the parameters:
|
||||
///
|
||||
/// * (right - left) defining the distance between the left and right faces of the cube
|
||||
/// * (top - bottom) defining the distance between the top and bottom faces of the cube
|
||||
/// * (near - far) defining the distance between the back (near) and front (far) faces of the cube
|
||||
///
|
||||
/// We then need to construct a projection matrix which converts points in that
|
||||
/// cuboid's space into clip space:
|
||||
///
|
||||
/// https://machengine.org/engine/math/traversing-coordinate-systems/#view---clip-space
|
||||
///
|
||||
/// Normally, in sysgpu/webgpu the depth buffer of floating point values would
|
||||
/// have the range [0, 1] representing [near, far], i.e. a pixel very close to the
|
||||
/// viewer would have a depth value of 0.0, and a pixel very far from the viewer
|
||||
/// would have a depth value of 1.0. But this is an ineffective use of floating
|
||||
/// point precision, a better approach is a reversed depth buffer:
|
||||
///
|
||||
/// * https://webgpu.github.io/webgpu-samples/samples/reversedZ
|
||||
/// * https://developer.nvidia.com/content/depth-precision-visualized
|
||||
///
|
||||
/// Mach mandates the use of a reversed depth buffer, so the returned transformation
|
||||
/// matrix maps to near=1 and far=0.
|
||||
pub inline fn projection2D(v: struct {
|
||||
left: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
top: f32,
|
||||
near: f32,
|
||||
far: f32,
|
||||
}) Matrix {
|
||||
var p = Matrix.ident;
|
||||
p = p.mul(&Matrix.translate(math.vec3(
|
||||
(v.right + v.left) / (v.left - v.right), // translate X so that the middle of (left, right) maps to x=0 in clip space
|
||||
(v.top + v.bottom) / (v.bottom - v.top), // translate Y so that the middle of (bottom, top) maps to y=0 in clip space
|
||||
v.near / (v.near - v.far), // translate Z so that far maps to z=0
|
||||
)));
|
||||
p = p.mul(&Matrix.scale(math.vec3(
|
||||
2 / (v.right - v.left), // scale X so that [left, right] has a 2 unit range, e.g. [-1, +1]
|
||||
2 / (v.top - v.bottom), // scale Y so that [bottom, top] has a 2 unit range, e.g. [-1, +1]
|
||||
1 / (v.near - v.far), // scale Z so that [near, far] has a 1 unit range, e.g. [0, -1]
|
||||
)));
|
||||
return p;
|
||||
}
|
||||
},
|
||||
else => @compileError("Expected Mat3x3, Mat4x4 found '" ++ @typeName(Matrix) ++ "'"),
|
||||
};
|
||||
|
|
@ -764,7 +773,7 @@ test "Mat3x3_mulVec_vec3_ident" {
|
|||
const v = math.Vec3.splat(1);
|
||||
const ident = math.Mat3x3.ident;
|
||||
const expected = v;
|
||||
var m = math.Mat3x3.mulVec(&ident, &v);
|
||||
const m = math.Mat3x3.mulVec(&ident, &v);
|
||||
|
||||
try testing.expect(math.Vec3, expected).eql(m);
|
||||
}
|
||||
|
|
@ -858,3 +867,129 @@ test "Mat4x4_mul" {
|
|||
);
|
||||
try testing.expect(math.Mat4x4, expected).eql(c);
|
||||
}
|
||||
|
||||
test "projection2D_xy_centered" {
|
||||
const v = .{
|
||||
.left = -400,
|
||||
.right = 400,
|
||||
.bottom = -200,
|
||||
.top = 200,
|
||||
.near = 0,
|
||||
.far = 100,
|
||||
};
|
||||
const m = math.Mat4x4.projection2D(v);
|
||||
|
||||
// Calculate some reference points
|
||||
const width = v.right - v.left;
|
||||
const height = v.top - v.bottom;
|
||||
const width_mid = v.left + (width / 2.0);
|
||||
const height_mid = v.bottom + (height / 2.0);
|
||||
try testing.expect(f32, 800).eql(width);
|
||||
try testing.expect(f32, 400).eql(height);
|
||||
try testing.expect(f32, 0).eql(width_mid);
|
||||
try testing.expect(f32, 0).eql(height_mid);
|
||||
|
||||
// Probe some points on the X axis from beyond the left face, all the way to beyond the right face.
|
||||
try testing.expect(math.Vec4, math.vec4(-2, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.left - (width / 2), height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(-1, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.left, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(-0.5, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.left + (width / 4.0), height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0.5, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.right - (width / 4.0), height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(1, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.right, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(2, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.right + (width / 2), height_mid, 0, 1)));
|
||||
|
||||
// Probe some points on the Y axis from beyond the bottom face, all the way to beyond the top face.
|
||||
try testing.expect(math.Vec4, math.vec4(0, -2, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.bottom - (height / 2), 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, -1, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.bottom, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, -0.5, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.bottom + (height / 4.0), 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0.5, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.top - (height / 4.0), 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 1, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.top, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 2, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.top + (height / 2), 0, 1)));
|
||||
}
|
||||
|
||||
test "projection2D_xy_offcenter" {
|
||||
const v = .{
|
||||
.left = 100,
|
||||
.right = 500,
|
||||
.bottom = 100,
|
||||
.top = 500,
|
||||
.near = 0,
|
||||
.far = 100,
|
||||
};
|
||||
const m = math.Mat4x4.projection2D(v);
|
||||
|
||||
// Calculate some reference points
|
||||
const width = v.right - v.left;
|
||||
const height = v.top - v.bottom;
|
||||
const width_mid = v.left + (width / 2.0);
|
||||
const height_mid = v.bottom + (height / 2.0);
|
||||
try testing.expect(f32, 400).eql(width);
|
||||
try testing.expect(f32, 400).eql(height);
|
||||
try testing.expect(f32, 300).eql(width_mid);
|
||||
try testing.expect(f32, 300).eql(height_mid);
|
||||
|
||||
// Probe some points on the X axis from beyond the left face, all the way to beyond the right face.
|
||||
try testing.expect(math.Vec4, math.vec4(-2, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.left - (width / 2), height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(-1, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.left, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(-0.5, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.left + (width / 4.0), height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0.5, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.right - (width / 4.0), height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(1, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.right, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(2, 0, 0, 1)).eql(m.mulVec(&math.vec4(v.right + (width / 2), height_mid, 0, 1)));
|
||||
|
||||
// Probe some points on the Y axis from beyond the bottom face, all the way to beyond the top face.
|
||||
try testing.expect(math.Vec4, math.vec4(0, -2, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.bottom - (height / 2), 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, -1, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.bottom, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, -0.5, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.bottom + (height / 4.0), 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, height_mid, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0.5, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.top - (height / 4.0), 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 1, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.top, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 2, 0, 1)).eql(m.mulVec(&math.vec4(width_mid, v.top + (height / 2), 0, 1)));
|
||||
}
|
||||
|
||||
test "projection2D_z" {
|
||||
const m = math.Mat4x4.projection2D(.{
|
||||
// Set x=0 and y=0 as centers, so we can specify 0 centers in our testing.expects below
|
||||
.left = -400,
|
||||
.right = 400,
|
||||
.bottom = -200,
|
||||
.top = 200,
|
||||
|
||||
// Choose some near/far plane values that we can easily test against
|
||||
// We'll have [near, far] == [-100, 100] == [0, 1]
|
||||
.near = -100,
|
||||
.far = 100,
|
||||
});
|
||||
|
||||
// Probe some points on the Z axis from the near plane, all the way to the far plane.
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 1, 1)).eql(m.mulVec(&math.vec4(0, 0, -100, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0.75, 1)).eql(m.mulVec(&math.vec4(0, 0, -50, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0.5, 1)).eql(m.mulVec(&math.vec4(0, 0, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0.25, 1)).eql(m.mulVec(&math.vec4(0, 0, 50, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0, 1)).eql(m.mulVec(&math.vec4(0, 0, 100, 1)));
|
||||
|
||||
// Probe some points outside the near/far planes
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 2, 1)).eql(m.mulVec(&math.vec4(0, 0, -100 - 200, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, -1, 1)).eql(m.mulVec(&math.vec4(0, 0, 100 + 200, 1)));
|
||||
}
|
||||
|
||||
test "projection2D_model_to_clip_space" {
|
||||
const model = math.Mat4x4.ident;
|
||||
const view = math.Mat4x4.ident;
|
||||
const proj = math.Mat4x4.projection2D(.{
|
||||
.left = -50,
|
||||
.right = 50,
|
||||
.bottom = -50,
|
||||
.top = 50,
|
||||
.near = 0,
|
||||
.far = 100,
|
||||
});
|
||||
const mvp = model.mul(&view).mul(&proj);
|
||||
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, 0, 1)).eql(mvp.mulVec(&math.vec4(0, 0, 0, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, -0.5, 1)).eql(mvp.mulVec(&math.vec4(0, 0, 50, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, -1, 0, 1)).eql(mvp.mul(&math.Mat4x4.rotateX(math.degreesToRadians(f32, 90))).mulVec(&math.vec4(0, 0, 50, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(1, 0, 0, 1)).eql(mvp.mul(&math.Mat4x4.rotateY(math.degreesToRadians(f32, 90))).mulVec(&math.vec4(0, 0, 50, 1)));
|
||||
try testing.expect(math.Vec4, math.vec4(0, 0, -0.5, 1)).eql(mvp.mul(&math.Mat4x4.rotateZ(math.degreesToRadians(f32, 90))).mulVec(&math.vec4(0, 0, 50, 1)));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue