tiny.geometry.Bvh
Defined in tiny.geometry.
API (14)
Actions
Public operations.
build: Builds the tree overmeshintostorage, which must fit the mesh's triangle count.overlapCapsule: The triangles the capsule touches, asMesh.overlapCapsuleanswers it.overlapSphere: The triangles the sphere touches, asMesh.overlapSphereanswers it.raycast: The nearest triangle the ray meets withtin[t_min, t_max], asMesh.raycastanswers it.raycastAny: Whether the ray meets any triangle withtin[t_min, t_max].sweepCapsule: The first triangle the capsule touches as it moves bydisplacement, asMesh.sweepCapsuleanswers it.sweepSphere: The first triangle the sphere touches as it moves bydisplacement, asMesh.sweepSphereanswers it.triangleCount
Types and contracts
Public types and contracts.
empty: The tree over no triangles, which answers every query with nothing.
Fields and members
Public fields and members.
Source
Source: lib/geometry/src/bvh.zig:259
zig
pub const Bvh = struct { nodes: []const Node, triangles: []const Triangle, /// The mesh index of the triangle in each tree slot. order: []const u32, /// The depth of the deepest node, with the root at zero. depth: u32, /// The largest coordinate magnitude of the root box. magnitude: f32, /// The tree over no triangles, which answers every query with nothing. pub const empty: Bvh = .{ .nodes = &empty_nodes, .triangles = &.{}, .order = &.{}, .depth = 0, .magnitude = 0 }; const Task = struct { node: u32, begin: u32, end: u32, depth: u32, }; /// Builds the tree over `mesh` into `storage`, which must fit the mesh's /// triangle count. The same mesh builds the same tree every time. pub fn build(mesh: Mesh, storage: Storage) Bvh { assert(mesh.isWellFormed()); const count = mesh.triangleCount(); assert(storage.fits(count)); const order = storage.order[0..count]; const centroids = storage.centroids[0..count]; for (order, centroids, 0..) |*slot, *centroid, index| { slot.* = @intCast(index); centroid.* = mesh.triangle(index).centroid(); } const builder = Builder{ .mesh = mesh, .centroids = centroids }; var node_count: u32 = 1; var depth: u32 = 0; var tasks: [depth_limit]Task = undefined; var task_count: usize = 1; tasks[0] = .{ .node = 0, .begin = 0, .end = @intCast(count), .depth = 0 }; while (task_count > 0) { task_count -= 1; const task = tasks[task_count]; const range = order[task.begin..task.end]; var box = Aabb.empty; var centroid_box = Aabb.empty; for (range) |index| { box = box.join(mesh.triangle(index).bounds()); centroid_box = centroid_box.joinPoint(centroids[index]); } depth = @max(depth, task.depth); assert(task.depth < depth_limit); if (range.len <= leaf_capacity) { storage.nodes[task.node] = .{ .min = box.min, .first = task.begin, .max = box.max, .count = @intCast(range.len) }; continue; } assert(task.depth + levelsNeeded(range.len) < depth_limit); const allow_surface_area = task.depth + levelsNeeded(range.len) + 1 < depth_limit; const middle = task.begin + @as(u32, @intCast(builder.split(range, centroid_box, allow_surface_area))); const left = node_count; node_count += 2; assert(node_count <= Storage.nodeCapacity(count)); storage.nodes[task.node] = .{ .min = box.min, .first = left, .max = box.max, .count = 0 }; assert(task_count + 2 <= depth_limit); tasks[task_count] = .{ .node = left + 1, .begin = middle, .end = task.end, .depth = task.depth + 1 }; tasks[task_count + 1] = .{ .node = left, .begin = task.begin, .end = middle, .depth = task.depth + 1 }; task_count += 2; } for (storage.triangles[0..count], order) |*triangle, index| triangle.* = mesh.triangle(index); const root = storage.nodes[0].box(); return .{ .nodes = storage.nodes[0..node_count], .triangles = storage.triangles[0..count], .order = order, .depth = depth, .magnitude = if (count == 0) 0 else root.magnitude(), }; } pub fn triangleCount(bvh: Bvh) usize { return bvh.triangles.len; } /// The nearest triangle the ray meets with `t` in `[t_min, t_max]`, as /// `Mesh.raycast` answers it. pub fn raycast(bvh: Bvh, ray: Ray, t_min: f32, t_max: f32, stats: ?*Stats) Error!?RayHit { assert(t_min <= t_max); var walk = try Walk.begin(bvh, stats); if (walk.done) return null; const slack = tolerance.boxSlack(@max(bvh.magnitude, ray.origin.abs().maxComponent())); var best: ?RayHit = null; var best_t = t_max; const root = intersect.raySlab(ray, bvh.nodes[0].box().inflate(slack), best_t) orelse return null; try walk.stack.push(.{ .node = 0, .entry = root[0] }); while (walk.next(best_t)) |node| { if (node.isLeaf()) { for (node.first..node.first + node.count) |slot| { walk.tested(); const hit = intersect.rayTriangle(ray, bvh.triangles[slot]) orelse continue; if (hit.t < t_min or hit.t > best_t) continue; const index = bvh.order[slot]; if (best) |held| { if (hit.t == held.t and index > held.triangle) continue; } best = .{ .t = hit.t, .u = hit.u, .v = hit.v, .triangle = index }; best_t = hit.t; } continue; } const near = intersect.raySlab(ray, bvh.nodes[node.first].box().inflate(slack), best_t); const far = intersect.raySlab(ray, bvh.nodes[node.first + 1].box().inflate(slack), best_t); try walk.pushOrdered(node.first, near, far); } return best; } /// Whether the ray meets any triangle with `t` in `[t_min, t_max]`. pub fn raycastAny(bvh: Bvh, ray: Ray, t_min: f32, t_max: f32, stats: ?*Stats) Error!bool { assert(t_min <= t_max); var walk = try Walk.begin(bvh, stats); if (walk.done) return false; const slack = tolerance.boxSlack(@max(bvh.magnitude, ray.origin.abs().maxComponent())); const root = intersect.raySlab(ray, bvh.nodes[0].box().inflate(slack), t_max) orelse return false; try walk.stack.push(.{ .node = 0, .entry = root[0] }); while (walk.next(t_max)) |node| { if (node.isLeaf()) { for (node.first..node.first + node.count) |slot| { walk.tested(); const hit = intersect.rayTriangle(ray, bvh.triangles[slot]) orelse continue; if (hit.t >= t_min and hit.t <= t_max) return true; } continue; } const near = intersect.raySlab(ray, bvh.nodes[node.first].box().inflate(slack), t_max); const far = intersect.raySlab(ray, bvh.nodes[node.first + 1].box().inflate(slack), t_max); try walk.pushOrdered(node.first, near, far); } return false; } /// The triangles the sphere touches, as `Mesh.overlapSphere` answers it. pub fn overlapSphere(bvh: Bvh, sphere: Sphere, out: []u32, stats: ?*Stats) Error!Overlap { assert(sphere.radius >= 0); const reach = sphere.bounds(); const slack = tolerance.boxSlack(@max(bvh.magnitude, reach.magnitude())); var collector = Collector{ .out = out }; var walk = try Walk.begin(bvh, stats); if (walk.done) return collector.finish(); try walk.stack.push(.{ .node = 0, .entry = 0 }); while (walk.next(0)) |node| { if (!intersect.sphereAabb(sphere, node.box().inflate(slack))) continue; if (node.isLeaf()) { for (node.first..node.first + node.count) |slot| { walk.tested(); if (intersect.sphereTriangle(sphere, bvh.triangles[slot])) collector.add(bvh.order[slot]); } continue; } try walk.stack.push(.{ .node = node.first + 1, .entry = 0 }); try walk.stack.push(.{ .node = node.first, .entry = 0 }); } return collector.finish(); } /// The triangles the capsule touches, as `Mesh.overlapCapsule` answers /// it. pub fn overlapCapsule(bvh: Bvh, capsule: Capsule, out: []u32, stats: ?*Stats) Error!Overlap { assert(capsule.radius >= 0); const reach = capsule.bounds(); const slack = tolerance.boxSlack(@max(bvh.magnitude, reach.magnitude())); var collector = Collector{ .out = out }; var walk = try Walk.begin(bvh, stats); if (walk.done) return collector.finish(); try walk.stack.push(.{ .node = 0, .entry = 0 }); while (walk.next(0)) |node| { if (!reach.overlaps(node.box().inflate(slack))) continue; if (node.isLeaf()) { for (node.first..node.first + node.count) |slot| { walk.tested(); if (intersect.capsuleTriangle(capsule, bvh.triangles[slot])) collector.add(bvh.order[slot]); } continue; } try walk.stack.push(.{ .node = node.first + 1, .entry = 0 }); try walk.stack.push(.{ .node = node.first, .entry = 0 }); } return collector.finish(); } /// The first triangle the sphere touches as it moves by `displacement`, /// as `Mesh.sweepSphere` answers it. pub fn sweepSphere(bvh: Bvh, sphere: Sphere, displacement: Vec3, stats: ?*Stats) Error!?SweepHit { assert(sphere.radius >= 0); const moved = Sphere{ .center = sphere.center.add(displacement), .radius = sphere.radius }; const reach = sphere.bounds().join(moved.bounds()); const margin = Swept{ .low = Vec3.splat(-sphere.radius), .high = Vec3.splat(sphere.radius), .slack = tolerance.boxSlack(@max(bvh.magnitude, reach.magnitude())), }; const path = Ray{ .origin = sphere.center, .direction = displacement }; return bvh.sweepWith(path, margin, stats, sphere, sweepSphereTriangle); } /// The first triangle the capsule touches as it moves by `displacement`, /// as `Mesh.sweepCapsule` answers it. pub fn sweepCapsule(bvh: Bvh, capsule: Capsule, displacement: Vec3, stats: ?*Stats) Error!?SweepHit { assert(capsule.radius >= 0); const moved = Capsule{ .a = capsule.a.add(displacement), .b = capsule.b.add(displacement), .radius = capsule.radius }; const reach = capsule.bounds().join(moved.bounds()); const axis = capsule.b.sub(capsule.a); const margin = Swept{ .low = axis.min(.{}).sub(Vec3.splat(capsule.radius)), .high = axis.max(.{}).add(Vec3.splat(capsule.radius)), .slack = tolerance.boxSlack(@max(bvh.magnitude, reach.magnitude())), }; const path = Ray{ .origin = capsule.a, .direction = displacement }; return bvh.sweepWith(path, margin, stats, capsule, sweepCapsuleTriangle); } fn sweepSphereTriangle(sphere: Sphere, displacement: Vec3, triangle: Triangle) ?sweep.Hit { return sweep.sphereTriangle(sphere, displacement, triangle); } fn sweepCapsuleTriangle(capsule: Capsule, displacement: Vec3, triangle: Triangle) ?sweep.Hit { return sweep.capsuleTriangle(capsule, displacement, triangle); } /// Walks the boxes a moving reference point can reach while the shape it /// carries could touch them. A shape whose points lie at offsets in /// `[low, high]` from the reference point touches a box only while that /// point is inside the box grown by `-high` below and `-low` above. fn sweepWith( bvh: Bvh, path: Ray, margin: Swept, stats: ?*Stats, shape: anytype, comptime test_triangle: fn (@TypeOf(shape), Vec3, Triangle) ?sweep.Hit, ) Error!?SweepHit { var walk = try Walk.begin(bvh, stats); if (walk.done) return null; var best: ?SweepHit = null; var best_t: f32 = 1; const root = intersect.raySlab(path, margin.grow(bvh.nodes[0]), best_t) orelse return null; try walk.stack.push(.{ .node = 0, .entry = root[0] }); while (walk.next(best_t)) |node| { if (node.isLeaf()) { for (node.first..node.first + node.count) |slot| { walk.tested(); const hit = test_triangle(shape, path.direction, bvh.triangles[slot]) orelse continue; if (hit.t > best_t) continue; const index = bvh.order[slot]; if (best) |held| { if (hit.t == held.t and index > held.triangle) continue; } best = .{ .t = hit.t, .point = hit.point, .normal = hit.normal, .triangle = index }; best_t = hit.t; } continue; } const near = intersect.raySlab(path, margin.grow(bvh.nodes[node.first]), best_t); const far = intersect.raySlab(path, margin.grow(bvh.nodes[node.first + 1]), best_t); try walk.pushOrdered(node.first, near, far); } return best; }};Source: lib/geometry/src/root.zig:28
zig
pub const Bvh = bvh.Bvh;Complete caller list for Bvh.build
8 direct callers.
lib.geometry.src.bvh.test_a_built_tree_bounds_its_node_count_and_depth[function] — test source atlib/geometry/src/bvh.zig:673in nearest public ownerlib.geometry.src.bvhlib.geometry.src.bvh.test_a_fixed_storage_holds_a_tree_sized_at_compile_time[function] — test source atlib/geometry/src/bvh.zig:718in nearest public ownerlib.geometry.src.bvhlib.geometry.src.bvh.test_a_tree_too_deep_for_the_query_stack_is_refused[function] — test source atlib/geometry/src/bvh.zig:706in nearest public ownerlib.geometry.src.bvhlib.geometry.src.bvh.test_an_empty_mesh_builds_a_tree_that_answers_nothing[function] — test source atlib/geometry/src/bvh.zig:728in nearest public ownerlib.geometry.src.bvhlib.geometry.src.bvh.test_an_overlap_with_a_small_buffer_keeps_the_lowest_indices[function] — test source atlib/geometry/src/bvh.zig:739in nearest public ownerlib.geometry.src.bvhlib.geometry.src.bvh.test_the_tree_answers_a_ray_on_a_shared_edge_with_the_lower_index[function] — test source atlib/geometry/src/bvh.zig:692in nearest public ownerlib.geometry.src.bvhlib.geometry.src.test.BuildIsDeterministic.property[function] — private source atlib/geometry/src/test.zig:166in nearest public ownerlib.geometry.src.testlib.geometry.src.test.Fixture.build[function] — private source atlib/geometry/src/test.zig:88in nearest public ownerlib.geometry.src.test
Audit
| Definitions | 10 |
|---|---|
| Public names | 10 |
| Members | 5 |
| Version | 26.7.0 |
| Revision | daab053ee433 |