Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Renzora Engine `r1-alpha8`

## 2026-10-07 21:03
- fix(physics): reopening a scene no longer resizes the colliders of rotated objects

## 2026-09-18 16:27
- fix(game-ui): an `expand` canvas previewed over the viewport lays out like the shipped game, so edge-anchored UI reaches the edge
- fix(game-ui): a canvas root no longer keeps a preview scale that was saved into a scene, which shrank the UI in an exported game
Expand Down
51 changes: 24 additions & 27 deletions crates/renzora_physics/src/auto_fit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,31 +54,15 @@ pub fn auto_fit_collision_shapes(
commands.entity(entity).remove::<PendingAutoFit>();
continue;
}
let Some((min, max)) = combined_world_aabb(entity, &aabbs, &children) else {
// Mesh not ready yet — try again next frame.
// Bounds are taken in the collider's own local space, corner by corner.
// Fitting a world-axis box first and mapping that back inflated every
// rotated collider (a 15 degree Y platform grew to 1.4x1.6 of its mesh).
let inv = entity_gt.affine().inverse();
let Some((local_min, local_max)) = combined_local_aabb(entity, inv, &aabbs, &children) else {
// Mesh not ready yet: try again next frame.
continue;
};

let inv = entity_gt.affine().inverse();
let corners = [
Vec3::new(min.x, min.y, min.z),
Vec3::new(max.x, min.y, min.z),
Vec3::new(min.x, max.y, min.z),
Vec3::new(max.x, max.y, min.z),
Vec3::new(min.x, min.y, max.z),
Vec3::new(max.x, min.y, max.z),
Vec3::new(min.x, max.y, max.z),
Vec3::new(max.x, max.y, max.z),
];

let mut local_min = Vec3::splat(f32::INFINITY);
let mut local_max = Vec3::splat(f32::NEG_INFINITY);
for c in corners {
let lp = inv.transform_point3(c);
local_min = local_min.min(lp);
local_max = local_max.max(lp);
}

let half = (local_max - local_min) * 0.5;
let center = (local_max + local_min) * 0.5;

Expand All @@ -104,13 +88,26 @@ pub fn auto_fit_collision_shapes(
CollisionShapeType::Mesh => unreachable!(),
}

commands.entity(entity).insert(new_shape);
// A default-valued shape on a unit mesh already fits; rewriting it only
// injects float noise (0.5 -> 0.49999997) and dirties a loaded scene.
if !shape_close(shape, &new_shape) {
commands.entity(entity).insert(new_shape);
}
commands.entity(entity).remove::<PendingAutoFit>();
}
}

fn combined_world_aabb(
fn shape_close(a: &CollisionShapeData, b: &CollisionShapeData) -> bool {
const EPS: f32 = 1e-4;
a.offset.abs_diff_eq(b.offset, EPS)
&& a.half_extents.abs_diff_eq(b.half_extents, EPS)
&& (a.radius - b.radius).abs() <= EPS
&& (a.half_height - b.half_height).abs() <= EPS
}

fn combined_local_aabb(
root: Entity,
to_local: bevy::math::Affine3A,
aabbs: &Query<(&Aabb, &GlobalTransform)>,
children: &Query<&Children>,
) -> Option<(Vec3, Vec3)> {
Expand All @@ -127,9 +124,9 @@ fn combined_world_aabb(
for sy in [-1.0, 1.0] {
for sz in [-1.0, 1.0] {
let local = center + half * Vec3::new(sx, sy, sz);
let world = gt.transform_point(local);
min = min.min(world);
max = max.max(world);
let root_local = to_local.transform_point3(gt.transform_point(local));
min = min.min(root_local);
max = max.max(root_local);
}
}
}
Expand Down
44 changes: 44 additions & 0 deletions crates/renzora_physics/tests/auto_fit_rotated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Auto-fit must be rotation-invariant. A unit cube mesh under a rotated,
//! non-uniformly scaled entity already fits the default 0.5 box exactly; the old
//! fit took a world-axis AABB and mapped it back, which grew a 15 degree Y
//! platform's collider to roughly 0.70 x 0.5 x 0.81 every time the scene loaded.

use bevy::camera::primitives::Aabb;
use bevy::math::Vec3A;
use bevy::prelude::*;
use renzora_physics::auto_fit::{auto_fit_collision_shapes, mark_new_collision_shapes};
use renzora_physics::CollisionShapeData;

#[test]
fn rotated_unit_cube_keeps_default_extents() {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_systems(Update, (mark_new_collision_shapes, auto_fit_collision_shapes).chain());

let tf = Transform {
translation: Vec3::new(7.0, 1.0, 0.0),
rotation: Quat::from_rotation_y(15f32.to_radians()),
scale: Vec3::new(5.0, 0.5, 4.0),
};
let e = app
.world_mut()
.spawn((
tf,
GlobalTransform::from(tf),
Aabb { center: Vec3A::ZERO, half_extents: Vec3A::splat(0.5) },
CollisionShapeData::default(),
))
.id();

for _ in 0..3 {
app.update();
}

let shape = app.world().get::<CollisionShapeData>(e).unwrap();
assert!(
shape.half_extents.abs_diff_eq(Vec3::splat(0.5), 1e-4),
"half extents changed to {:?}",
shape.half_extents
);
assert!(shape.offset.abs_diff_eq(Vec3::ZERO, 1e-4), "offset changed to {:?}", shape.offset);
}