bevy icon indicating copy to clipboard operation
bevy copied to clipboard

Add `Mesh` transformation

Open Jondolf opened this issue 2 years ago • 10 comments

Objective

It can sometimes be useful to transform actual Mesh data without needing to change the Transform of an entity. For example, one might want to spawn a circle mesh facing up instead of facing Z, or to spawn a mesh slightly offset without needing child entities.

Solution

Add transform_by and transformed_by methods to Mesh. They take a Transform and apply the translation, rotation, and scale to vertex positions, and the rotation to normals and tangents.

In the load_gltf example, with this system:

fn transform(time: Res<Time>, mut q: Query<&mut Handle<Mesh>>, mut meshes: ResMut<Assets<Mesh>>) {
    let sin = 0.0025 * time.elapsed_seconds().sin();

    for mesh_handle in &mut q {
        if let Some(mesh) = meshes.get_mut(mesh_handle.clone_weak()) {
            let transform =
                Transform::from_rotation(Quat::from_rotation_y(0.75 * time.delta_seconds()))
                    .with_scale(Vec3::splat(1.0 + sin));
            mesh.transform_by(transform);
        }
    }
}

it looks like this:

https://github.com/bevyengine/bevy/assets/57632562/60432456-6d28-4d06-9c94-2f4148f5acd5

Jondolf avatar Jan 21 '24 13:01 Jondolf

I like this, but maybe it would be better to implement this as Add<Transform> and AddAssign<Transform>?

Not sure, maybe? But it'd probably be Mul instead of Add. Transformations are typically represented with multiplication, e.g. transform * point is equivalent to transform.transform_point(point); you can't add or subtract transforms.

I could at least add a transformed_by method so that you could create and transform the mesh on a single line instead of having to define a mutable mesh and separately call transform_by on that

Jondolf avatar Jan 21 '24 13:01 Jondolf

I ended up adding transformed_by and implementing Mul<Mesh> for Transform. You can now do this:

let transform = Transform::from_xyz(0.0, 2.0, 0.0);

// These are equivalent
let cuboid = Mesh::from(shape::Box::default()).transformed_by(transform);
let cuboid = transform * Mesh::from(shape::Box::default());

let mut cuboid = Mesh::from(shape::Box::default());
cuboid.transform_by(transform);

Feel free to suggest further improvements/changes to the API

Jondolf avatar Jan 21 '24 13:01 Jondolf

Why did you implement Mul<Mesh> for Transform and not Mul<Transform> for Mesh? You could also implement MulAssign<Transform> for Mesh and be able to perform syntax like this:

let mut cuboid = Mesh::from(shape::Box::default());
cuboid *= transform;

// or

let cuboid = Mesh::from(shape::Box::default()) * transform;

doonv avatar Jan 21 '24 16:01 doonv

For consistency. You can't do point * transform or point *= transform, so I don't see why you'd be able to do mesh * transform. Whether or not you should be able to do point * transform is a separate discussion though

For me, the current transform * point or transform * mesh order makes sense, since it reads like "transform point" or "transform mesh". The other way around it'd read something like "to this point, apply transform"

Jondolf avatar Jan 21 '24 16:01 Jondolf

Here to mention that I ran into exactly this use-case (wanted a xz plane circle).

I do think the Mul<> operator not being symmetric is a bit weird, and I will most likely used the transformed_by variant.

blueforesticarus avatar Jan 28 '24 02:01 blueforesticarus

alternatively to avoid a division, (normal.xyz * scale.yzx * scale.zxy).normalized() (component-wise multiplication)

atlv24 avatar Jan 28 '24 22:01 atlv24

If any component of scale is zero it will cause a division by zero and result in infinite-length normals, maybe the division-skipping version that uses only multiplications is a better option

atlv24 avatar Jan 29 '24 00:01 atlv24

If any component of scale is zero it will cause a division by zero and result in infinite-length normals, maybe the division-skipping version that uses only multiplications is a better option

Won't the multiplication version similarly fail when we attempt to normalize the product? Or is .normalized implicitly "normalize or zero" behavior?

alice-i-cecile avatar Jan 29 '24 00:01 alice-i-cecile

I can use normalize_or_zero for it

Jondolf avatar Jan 29 '24 00:01 Jondolf

No, the multiplication-only version will work fine, i'm talking about only one component being zero. ~~normalize_or_zero is unneeded.~~ edit: if the normal is perpendicular to the axis being flattened along it will be zero. The vector as a whole will usually not be zero as it will still have some non-zero component. It doesnt quite make sense to scale a mesh on two or more axes by zero, that should be guarded for at the top level imo. It would just get collapsed to a line segment and not render anything because all triangles would be zero-area. Zero scales only make sense for flattening meshes along a single axis.

atlv24 avatar Jan 29 '24 00:01 atlv24

Thanks for the patience and the fixes

atlv24 avatar Jan 29 '24 01:01 atlv24