Skip to content

Organize HalfSpace, create 2d and 3d HalfSpace primitives#23416

Open
jackxpeng wants to merge 8 commits intobevyengine:mainfrom
jackxpeng:add-halfspace-primitives
Open

Organize HalfSpace, create 2d and 3d HalfSpace primitives#23416
jackxpeng wants to merge 8 commits intobevyengine:mainfrom
jackxpeng:add-halfspace-primitives

Conversation

@jackxpeng
Copy link

Objective

Solution

  • Added an offset field (f32) to Plane2d to match the signed distance representation.
  • Refactored InfinitePlane3d to use Vec4 internally (packing the normal and offset for SIMD efficiency).
  • Added comprehensive methods to both planes based on the proposed gist: signed_distance_to_point, project_point, from_point_and_normal, from_coefficients, and isometries_xy.
  • Created HalfSpace2d in dim2.rs as a newtype over Plane2d.
  • Created HalfSpace3d in dim3.rs as a newtype over InfinitePlane3d.
  • Added pub type HalfSpace = HalfSpace3d to maintain backward compatibility with existing usages in bevy_camera and bevy_pbr.
  • Deleted the obsolete half_space.rs module and migrated its intersection_point tests.

Testing

  • Verified all 286 lib tests and 116 doctests pass in bevy_math locally.
  • Verified that bevy_camera and bevy_pbr compile cleanly with the changes via the HalfSpace type alias.
  • Added tests for the new from_point_and_normal constructors and verified existing half space intersection math behaves identically to the old implementation.

Migration Guide

The Plane2d primitive has been updated to include an offset. If you were creating planes previously, the constructor signature has changed.

  • Plane2d::new(Vec2::X) should be updated to Plane2d::new(Dir2::X, 0.0).
  • Plane2d now has a public offset: f32 field.
  • HalfSpace is now a type alias for HalfSpace3d. If you need the 2D equivalent, use the newly added HalfSpace2d.

@github-actions
Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@Jondolf Jondolf self-requested a review March 19, 2026 00:50
@jackxpeng jackxpeng force-pushed the add-halfspace-primitives branch 2 times, most recently from 3b3fbb8 to 368fe77 Compare March 19, 2026 06:15
@alice-i-cecile alice-i-cecile added C-Code-Quality A section of code that is hard to understand or change C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Math Fundamental domain-agnostic mathematical operations X-Uncontroversial This work is generally agreed upon D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Mar 19, 2026
@jackxpeng jackxpeng force-pushed the add-halfspace-primitives branch 2 times, most recently from a51b70f to 793e275 Compare March 19, 2026 08:59
let half_depth = if facing_z { 0.0 } else { f32::MAX / 2.0 };
let half_size = Vec3A::new(half_width, half_height, half_depth);

Aabb3d::new(isometry.translation, half_size)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this doesn't account for the translation of the plane by the signed offset. Not really relevant in the general case, but in the special cases that the plane aligns with an axis, this provides a wrong AABB.

let translation = Vec3::new(2.0, 1.0, 0.0);

let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(translation);
let aabb1 = InfinitePlane3d::new(Dir3A::X, 0.0).aabb_3d(translation);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These need testcases where the offset is some arbitrary nonzero value.

pub fn new(normal: Vec2) -> Self {
pub fn from_coefficients(a: f32, b: f32, c: f32) -> Self {
debug_assert!(
ops::abs(a * a + b * b - 1.0) < 1e-6,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this epsilon (1e-6) match the usual values we use?

- Update `Plane2d` and `InfinitePlane3d` initializations in examples to account for the new `offset` field.
- Transition to using explicit direction types (`Dir2`, `Dir3A`) for plane normals instead of raw vectors.
- Replace `signed_distance` with `signed_distance_to_point` in the 3D mirror example.
- Collapse an `else { if }` block into `else if` in `bevy_winit`.
- Mark the `HalfSpace` type alias as deprecated for the 0.19.0 release.
@jackxpeng jackxpeng force-pushed the add-halfspace-primitives branch from 793e275 to 3b2d1a3 Compare March 19, 2026 09:19

/// Creates a new [`Plane2d`] from the coefficients of the plane equation `ax + by + c = 0`.
///
/// The normal vector `(a, b)` must be of unit length.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a subtle breaking change. The method used to do the normalisation and failed only if the normalisation couldn't be done. Now it doesn't panic without assertions (good), but it also doesn't handle the same cases as before correctly.

///
/// # Errors
///
/// Returns an error if the normal vector `(a, b)` is not of unit length.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is misleading, the error is returned if the vector (a,b) could not be normalised.

/// Returns an error if the normal vector `(a, b)` is not of unit length.
#[inline]
pub fn try_from_coefficients(a: f32, b: f32, c: f32) -> Result<Self, InvalidDirectionError> {
let normal = Dir2::new(Vec2::new(a, b))?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let normal = Dir2::new(Vec2::new(a, b))?;
let normal = Dir2::from_xy(a, b)?;

d: f32,
) -> Result<Self, UnnormalizedNormalError> {
let len_sq = a * a + b * b + c * c;
if ops::abs(len_sq - 1.0) >= 1e-6 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition as it's written doesn't handle NAN numbers. This is because Nan fails to compare with anything, so std::f32::NAN >= 1e-6 gives false, avoiding the error handler.

Comment on lines +486 to +505
let an = a.normal();
let bn = b.normal();
let cn = c.normal();

let x = Vec3A::new(an.x, bn.x, cn.x);
let y = Vec3A::new(an.y, bn.y, cn.y);
let z = Vec3A::new(an.z, bn.z, cn.z);

let d = -Vec3A::new(a.d(), b.d(), c.d());

let u = y.cross(z);
let v = x.cross(d);

let denom = x.dot(u);

if ops::abs(denom) < f32::EPSILON {
return None;
}

Some(Vec3::new(d.dot(u), z.dot(v), -y.dot(v)) / denom)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this entire thing would be better done by constructing a 3x3 matrix from the normal vectors and using https://docs.rs/glam/latest/glam/f32/struct.Mat3.html#method.try_inverse to compute an inverse matrix, then multiply by it the vector of offsets.

Effetively one is solving the set of 3 linear equations:
Vec3::dot(n_i, x) = -offset_i
So the n vectors assemble as rows into a matrix, and the offsets into the right hand side.

That's effectively what this code is doing (I hope), but in a rather confusing way.

@IQuick143 IQuick143 added the M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide label Mar 19, 2026
@github-actions
Copy link
Contributor

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Please review the instructions for writing migration guides, then expand or revise the content in the migration guides directory to reflect your changes.

@janhohenheim janhohenheim removed the X-Uncontroversial This work is generally agreed upon label Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Math Fundamental domain-agnostic mathematical operations C-Code-Quality A section of code that is hard to understand or change C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bevy_math: Organize HalfSpace, create 2d and 3d HalfSpace primitives

4 participants