Skip to documentation
SLOP

tiny.geometry.sweep

Reference tiny.geometry sweep

Defined in tiny.geometry.

Swept shapes against triangles.

API (3)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.geometrysweep
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsprivate sourcelib.geometry.src.bvh.BvhsweepCapsuleTriangleMeshsweepCapsuletest sourcelib.geometry.src.sweeptest: a capsule lands on an endpoint,...test sourcelib.geometry.src.sweeptest: a capsule that starts overlappi...closestsegmentTriangleprivate sourcelib.geometry.src.sweepaxisEdgeprivate sourcelib.geometry.src.sweepcylinderEntryprivate sourcelib.geometry.src.sweepedgesprivate sourcelib.geometry.src.sweepseparationNormalprivate sourcelib.geometry.src.sweepsphereFeaturessweepcapsuleTriangle
Static calls · unresolved targets: 2 · external targets: 12.
Called byCallsprivate sourcelib.geometry.src.bvh.BvhsweepSphereTriangleMeshsweepSpheretest sourcelib.geometry.src.sweeptest: a falling sphere lands on the f...test sourcelib.geometry.src.sweeptest: a sliding sphere strikes an edg...test sourcelib.geometry.src.sweeptest: a sphere that starts touching r...closestpointTriangleprivate sourcelib.geometry.src.sweepseparationNormalprivate sourcelib.geometry.src.sweepsphereFeaturessweepsphereTriangle
Static calls · unresolved targets: 0 · external targets: 2.

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

Definitions4
Public names4
Members3
Version26.7.0
Revisiondaab053ee433