tiny.geometry.sweep
Defined in tiny.geometry.
Swept shapes against triangles.
API (3)
Actions
Public operations.
capsuleTriangle: First contact ofcapsulemoving bydisplacementwithtriangle, or null when it passes clear.sphereTriangle: First contact ofspheremoving bydisplacementwithtriangle, or null when it passes clear.
Types and contracts
Public types and contracts.
Hit: First contact of a sweep.
Source
Source: lib/geometry/src/root.zig:10
zig
pub const sweep = @import("sweep.zig");Source: lib/geometry/src/sweep.zig
zig
//! Swept shapes against triangles.//!//! A sweep translates a shape by `displacement` and reports the first contact//! with a two-sided triangle. First contact is the earliest of three feature//! families: the face, an edge and a vertex. For a capsule, the face family//! reduces to its endpoint spheres, and the edge and vertex families gain the//! capsule axis, whose line-to-line distance to an edge changes linearly under//! translation. Taken together the families are exact.const std = @import("std");const linear = @import("linear");const closest = @import("closest.zig");const intersect = @import("intersect.zig");const primitive = @import("primitive.zig");const tolerance = @import("tolerance.zig");const assert = std.debug.assert;const Capsule = primitive.Capsule;const Segment = primitive.Segment;const Sphere = primitive.Sphere;const Triangle = primitive.Triangle;const Vec3 = linear.Vec3;/// First contact of a sweep.pub const Hit = struct { /// The fraction of the displacement travelled at contact, in [0, 1]. Zero /// means the shapes already touch or overlap at the start. t: f32, /// The contact point on the triangle. point: Vec3, /// The unit normal at contact, from the triangle toward the moving shape. normal: Vec3,};/// The direction to push a shape out of a triangle when their closest points/// coincide: the face normal turned against the motion, else against the/// motion itself, else up.fn fallbackNormal(triangle: Triangle, displacement: Vec3) Vec3 { if (triangle.normal()) |n| { if (n.dot(displacement) > 0) return n.negate(); return n; } if (displacement.normalized(0)) |direction| return direction.negate(); return Vec3.init(0, 0, 1);}fn separationNormal(separation: Vec3, triangle: Triangle, displacement: Vec3) Vec3 { if (separation.lengthSq() <= tolerance.length_sq_floor) return fallbackNormal(triangle, displacement); return separation.normalized(0) orelse fallbackNormal(triangle, displacement);}const Best = struct { hit: ?Hit = null, fn consider(best: *Best, candidate: Hit) void { assert(candidate.t >= 0); assert(candidate.t <= 1); if (best.hit) |held| { if (candidate.t >= held.t) return; } best.hit = candidate; }};/// The first time in [0, 1] at which the moving point `origin + motion t`/// comes within `radius` of the line through `anchor` along `axis`, with the/// point starting farther than `radius` from the line.fn cylinderEntry(origin: Vec3, motion: Vec3, anchor: Vec3, axis: Vec3, radius: f32) ?f32 { const axis_sq = axis.lengthSq(); assert(axis_sq > tolerance.length_sq_floor); const m = origin.sub(anchor); const m_perp = m.sub(axis.scale(m.dot(axis) / axis_sq)); const d_perp = motion.sub(axis.scale(motion.dot(axis) / axis_sq)); const a = d_perp.lengthSq(); if (a <= tolerance.closing_sq_floor) return null; const c = m_perp.lengthSq() - radius * radius; if (c <= 0) return null; const b = m_perp.dot(d_perp); const discriminant = b * b - a * c; if (discriminant < 0) return null; const t = (-b - @sqrt(discriminant)) / a; if (t < 0) return null; if (t > 1) return null; return t;}fn edges(triangle: Triangle) [3]Segment { return .{ .{ .a = triangle.a, .b = triangle.b }, .{ .a = triangle.b, .b = triangle.c }, .{ .a = triangle.c, .b = triangle.a }, };}fn insideFace(p: Vec3, triangle: Triangle, n: Vec3) bool { for (edges(triangle)) |edge| { if (edge.b.sub(edge.a).cross(p.sub(edge.a)).dot(n) < 0) return false; } return true;}/// Face, edge and vertex contacts of a sphere that starts clear of the/// triangle.fn sphereFeatures(best: *Best, center: Vec3, radius: f32, displacement: Vec3, triangle: Triangle) void { if (triangle.normal()) |n| { const distance = n.dot(center.sub(triangle.a)); const closing = n.dot(displacement); const side: f32 = if (distance >= 0) 1 else -1; if (side * closing < 0 and closing * closing > tolerance.closing_sq_floor) { const t = (side * radius - distance) / closing; if (t >= 0 and t <= 1) { const facing = n.scale(side); const point = center.add(displacement.scale(t)).sub(facing.scale(radius)); if (insideFace(point, triangle, n)) best.consider(.{ .t = t, .point = point, .normal = facing }); } } } for (edges(triangle)) |edge| { const axis = edge.b.sub(edge.a); if (axis.lengthSq() <= tolerance.length_sq_floor) continue; const t = cylinderEntry(center, displacement, edge.a, axis, radius) orelse continue; const moved = center.add(displacement.scale(t)); const along = moved.sub(edge.a).dot(axis) / axis.lengthSq(); if (along < 0 or along > 1) continue; const point = edge.a.add(axis.scale(along)); best.consider(.{ .t = t, .point = point, .normal = separationNormal(moved.sub(point), triangle, displacement) }); } for (0..3) |index| { const corner = triangle.vertex(index); const roots = intersect.lineSphereRoots(center, displacement, corner, radius) orelse continue; const t = roots[0]; if (t < 0 or t > 1) continue; const moved = center.add(displacement.scale(t)); best.consider(.{ .t = t, .point = corner, .normal = separationNormal(moved.sub(corner), triangle, displacement) }); }}/// First contact of `sphere` moving by `displacement` with `triangle`, or null/// when it passes clear.pub fn sphereTriangle(sphere: Sphere, displacement: Vec3, triangle: Triangle) ?Hit { assert(sphere.radius >= 0); const nearest = closest.pointTriangle(sphere.center, triangle); const separation = sphere.center.sub(nearest); if (separation.lengthSq() <= sphere.radius * sphere.radius) { return .{ .t = 0, .point = nearest, .normal = separationNormal(separation, triangle, displacement) }; } var best = Best{}; sphereFeatures(&best, sphere.center, sphere.radius, displacement, triangle); return best.hit;}/// First contact of `capsule` moving by `displacement` with `triangle`, or/// null when it passes clear.pub fn capsuleTriangle(capsule: Capsule, displacement: Vec3, triangle: Triangle) ?Hit { assert(capsule.radius >= 0); const radius = capsule.radius; const start = closest.segmentTriangle(capsule.axis(), triangle); const separation = start.first.sub(start.second); if (separation.lengthSq() <= radius * radius) { return .{ .t = 0, .point = start.second, .normal = separationNormal(separation, triangle, displacement) }; } var best = Best{}; sphereFeatures(&best, capsule.a, radius, displacement, triangle); sphereFeatures(&best, capsule.b, radius, displacement, triangle); const axis = capsule.b.sub(capsule.a); const axis_sq = axis.lengthSq(); if (axis_sq <= tolerance.length_sq_floor) return best.hit; for (edges(triangle)) |edge| axisEdge(&best, capsule, displacement, edge); for (0..3) |index| { const corner = triangle.vertex(index); const t = cylinderEntry(corner, displacement.negate(), capsule.a, axis, radius) orelse continue; const moved_a = capsule.a.add(displacement.scale(t)); const along = corner.sub(moved_a).dot(axis) / axis_sq; if (along < 0 or along > 1) continue; const axis_point = moved_a.add(axis.scale(along)); best.consider(.{ .t = t, .point = corner, .normal = separationNormal(axis_point.sub(corner), triangle, displacement) }); } return best.hit;}/// The contact between the capsule axis and an edge interior. The distance/// between the two lines is `f0 + f1 t`, so contact is the root of/// `|f0 + f1 t| = radius` where the lines' closest points lie inside both/// segments.fn axisEdge(best: *Best, capsule: Capsule, displacement: Vec3, edge: Segment) void { const u = capsule.b.sub(capsule.a); const v = edge.b.sub(edge.a); const n = u.cross(v); if (tolerance.isParallel(n.lengthSq(), u.lengthSq() * v.lengthSq())) return; const unit = n.normalized(0) orelse return; const f0 = capsule.a.sub(edge.a).dot(unit); const f1 = displacement.dot(unit); if (@abs(f0) <= capsule.radius) return; const side: f32 = if (f0 > 0) 1 else -1; if (side * f1 >= 0) return; if (f1 * f1 <= tolerance.closing_sq_floor) return; const t = (side * capsule.radius - f0) / f1; if (t < 0 or t > 1) return; const w = capsule.a.add(displacement.scale(t)).sub(edge.a); const aa = u.lengthSq(); const bb = u.dot(v); const cc = v.lengthSq(); const du = u.dot(w); const ev = v.dot(w); const denominator = aa * cc - bb * bb; if (denominator <= 0) return; const along_axis = (bb * ev - cc * du) / denominator; const along_edge = (aa * ev - bb * du) / denominator; if (along_axis < 0 or along_axis > 1) return; if (along_edge < 0 or along_edge > 1) return; best.consider(.{ .t = t, .point = edge.at(along_edge), .normal = unit.scale(side) });}const testing = std.testing;const unit_triangle = Triangle{ .a = .{}, .b = Vec3.init(1, 0, 0), .c = Vec3.init(0, 1, 0) };fn expectHit(expected: Hit, actual: ?Hit) !void { const hit = actual orelse return error.TestExpectedHit; try testing.expectApproxEqAbs(expected.t, hit.t, 1e-6); try testing.expectApproxEqAbs(expected.point.x, hit.point.x, 1e-6); try testing.expectApproxEqAbs(expected.point.y, hit.point.y, 1e-6); try testing.expectApproxEqAbs(expected.point.z, hit.point.z, 1e-6); try testing.expectApproxEqAbs(expected.normal.x, hit.normal.x, 1e-6); try testing.expectApproxEqAbs(expected.normal.y, hit.normal.y, 1e-6); try testing.expectApproxEqAbs(expected.normal.z, hit.normal.z, 1e-6);}test "a falling sphere lands on the face" { const ball = Sphere{ .center = Vec3.init(0.25, 0.25, 2), .radius = 0.5 }; try expectHit( .{ .t = 0.375, .point = Vec3.init(0.25, 0.25, 0), .normal = Vec3.init(0, 0, 1) }, sphereTriangle(ball, Vec3.init(0, 0, -4), unit_triangle), ); const below = Sphere{ .center = Vec3.init(0.25, 0.25, -2), .radius = 0.5 }; try expectHit( .{ .t = 0.375, .point = Vec3.init(0.25, 0.25, 0), .normal = Vec3.init(0, 0, -1) }, sphereTriangle(below, Vec3.init(0, 0, 4), unit_triangle), );}test "a sliding sphere strikes an edge and a vertex" { const edge_ball = Sphere{ .center = Vec3.init(0.5, -2, 0), .radius = 0.5 }; try expectHit( .{ .t = 0.375, .point = Vec3.init(0.5, 0, 0), .normal = Vec3.init(0, -1, 0) }, sphereTriangle(edge_ball, Vec3.init(0, 4, 0), unit_triangle), ); const vertex_ball = Sphere{ .center = Vec3.init(3, 0, 0), .radius = 0.5 }; try expectHit( .{ .t = 0.375, .point = Vec3.init(1, 0, 0), .normal = Vec3.init(1, 0, 0) }, sphereTriangle(vertex_ball, Vec3.init(-4, 0, 0), unit_triangle), );}test "a sphere that starts touching reports time zero and one that passes misses" { const touching = Sphere{ .center = Vec3.init(0.25, 0.25, 0.5), .radius = 0.5 }; try expectHit( .{ .t = 0, .point = Vec3.init(0.25, 0.25, 0), .normal = Vec3.init(0, 0, 1) }, sphereTriangle(touching, Vec3.init(1, 0, 0), unit_triangle), ); const clear = Sphere{ .center = Vec3.init(3, 3, 1), .radius = 0.5 }; try testing.expectEqual(@as(?Hit, null), sphereTriangle(clear, Vec3.init(0, 0, -4), unit_triangle)); try testing.expectEqual(@as(?Hit, null), sphereTriangle(.{ .center = Vec3.init(0.25, 0.25, 2), .radius = 0.5 }, Vec3.init(0, 0, -1), unit_triangle));}test "a capsule lands on an endpoint, an edge crossing and a vertex" { const upright = Capsule{ .a = Vec3.init(0.25, 0.25, 1), .b = Vec3.init(0.25, 0.25, 3), .radius = 0.5 }; try expectHit( .{ .t = 0.25, .point = Vec3.init(0.25, 0.25, 0), .normal = Vec3.init(0, 0, 1) }, capsuleTriangle(upright, Vec3.init(0, 0, -2), unit_triangle), ); const lying = Capsule{ .a = Vec3.init(0.5, -1, 2), .b = Vec3.init(0.5, 1, 2), .radius = 0.5 }; try expectHit( .{ .t = 0.375, .point = Vec3.init(0.5, 0, 0), .normal = Vec3.init(0, 0, 1) }, capsuleTriangle(lying, Vec3.init(0, 0, -4), unit_triangle), ); const post = Capsule{ .a = Vec3.init(-2, 0, -1), .b = Vec3.init(-2, 0, 1), .radius = 0.5 }; try expectHit( .{ .t = 0.375, .point = Vec3.init(0, 0, 0), .normal = Vec3.init(-1, 0, 0) }, capsuleTriangle(post, Vec3.init(4, 0, 0), unit_triangle), );}test "a capsule that starts overlapping reports time zero" { const through = Capsule{ .a = Vec3.init(0.25, 0.25, -1), .b = Vec3.init(0.25, 0.25, 1), .radius = 0.1 }; const hit = capsuleTriangle(through, Vec3.init(0, 0, 1), unit_triangle).?; try testing.expectEqual(@as(f32, 0), hit.t); try testing.expectEqual(Vec3.init(0, 0, -1), hit.normal); const clear = Capsule{ .a = Vec3.init(3, 3, 1), .b = Vec3.init(3, 3, 2), .radius = 0.5 }; try testing.expectEqual(@as(?Hit, null), capsuleTriangle(clear, Vec3.init(0, 0, -4), unit_triangle));}Audit
| Definitions | 4 |
|---|---|
| Public names | 4 |
| Members | 3 |
| Version | 26.7.0 |
| Revision | daab053ee433 |