Skip to main content

custom_skinned_mesh/
custom_skinned_mesh.rs

1//! Skinned mesh example with mesh and joints data defined in code.
2//! Example taken from <https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_019_SimpleSkin.md>
3
4use std::f32::consts::*;
5
6use bevy::{
7    asset::RenderAssetUsages,
8    camera::visibility::DynamicSkinnedMeshBounds,
9    math::ops,
10    mesh::{
11        skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
12        Indices, PrimitiveTopology, VertexAttributeValues,
13    },
14    prelude::*,
15};
16use chacha20::ChaCha8Rng;
17use rand::{RngExt, SeedableRng};
18
19fn main() {
20    App::new()
21        .add_plugins(DefaultPlugins)
22        .insert_resource(GlobalAmbientLight {
23            brightness: 3000.0,
24            ..default()
25        })
26        .add_systems(Startup, setup)
27        .add_systems(Update, joint_animation)
28        .run();
29}
30
31/// Used to mark a joint to be animated in the [`joint_animation`] system.
32#[derive(Component)]
33struct AnimatedJoint(isize);
34
35/// Construct a mesh and a skeleton with 2 joints for that mesh,
36///   and mark the second joint to be animated.
37/// It is similar to the scene defined in `models/SimpleSkin/SimpleSkin.gltf`
38fn setup(
39    mut commands: Commands,
40    asset_server: Res<AssetServer>,
41    mut meshes: ResMut<Assets<Mesh>>,
42    mut materials: ResMut<Assets<StandardMaterial>>,
43    mut skinned_mesh_inverse_bindposes_assets: ResMut<Assets<SkinnedMeshInverseBindposes>>,
44) {
45    // Create a camera
46    commands.spawn((
47        Camera3d::default(),
48        Transform::from_xyz(2.5, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
49    ));
50
51    // Create inverse bindpose matrices for a skeleton consists of 2 joints
52    let inverse_bindposes = skinned_mesh_inverse_bindposes_assets.add(vec![
53        Mat4::from_translation(Vec3::new(-0.5, -1.0, 0.0)),
54        Mat4::from_translation(Vec3::new(-0.5, -1.0, 0.0)),
55    ]);
56
57    // Create a mesh
58    let mesh = Mesh::new(
59        PrimitiveTopology::TriangleList,
60        RenderAssetUsages::RENDER_WORLD,
61    )
62    // Set mesh vertex positions
63    .with_inserted_attribute(
64        Mesh::ATTRIBUTE_POSITION,
65        vec![
66            [0.0, 0.0, 0.0],
67            [1.0, 0.0, 0.0],
68            [0.0, 0.5, 0.0],
69            [1.0, 0.5, 0.0],
70            [0.0, 1.0, 0.0],
71            [1.0, 1.0, 0.0],
72            [0.0, 1.5, 0.0],
73            [1.0, 1.5, 0.0],
74            [0.0, 2.0, 0.0],
75            [1.0, 2.0, 0.0],
76        ],
77    )
78    // Add UV coordinates that map the left half of the texture since its a 1 x
79    // 2 rectangle.
80    .with_inserted_attribute(
81        Mesh::ATTRIBUTE_UV_0,
82        vec![
83            [0.0, 0.00],
84            [0.5, 0.00],
85            [0.0, 0.25],
86            [0.5, 0.25],
87            [0.0, 0.50],
88            [0.5, 0.50],
89            [0.0, 0.75],
90            [0.5, 0.75],
91            [0.0, 1.00],
92            [0.5, 1.00],
93        ],
94    )
95    // Set mesh vertex normals
96    .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 10])
97    // Set mesh vertex joint indices for mesh skinning.
98    // Each vertex gets 4 indices used to address the `JointTransforms` array in the vertex shader
99    //  as well as `SkinnedMeshJoint` array in the `SkinnedMesh` component.
100    // This means that a maximum of 4 joints can affect a single vertex.
101    .with_inserted_attribute(
102        Mesh::ATTRIBUTE_JOINT_INDEX,
103        // Need to be explicit here as [u16; 4] could be either Uint16x4 or Unorm16x4.
104        VertexAttributeValues::Uint16x4(vec![
105            [0, 0, 0, 0],
106            [0, 0, 0, 0],
107            [0, 1, 0, 0],
108            [0, 1, 0, 0],
109            [0, 1, 0, 0],
110            [0, 1, 0, 0],
111            [0, 1, 0, 0],
112            [0, 1, 0, 0],
113            [0, 1, 0, 0],
114            [0, 1, 0, 0],
115        ]),
116    )
117    // Set mesh vertex joint weights for mesh skinning.
118    // Each vertex gets 4 joint weights corresponding to the 4 joint indices assigned to it.
119    // The sum of these weights should equal to 1.
120    .with_inserted_attribute(
121        Mesh::ATTRIBUTE_JOINT_WEIGHT,
122        vec![
123            [1.00, 0.00, 0.0, 0.0],
124            [1.00, 0.00, 0.0, 0.0],
125            [0.75, 0.25, 0.0, 0.0],
126            [0.75, 0.25, 0.0, 0.0],
127            [0.50, 0.50, 0.0, 0.0],
128            [0.50, 0.50, 0.0, 0.0],
129            [0.25, 0.75, 0.0, 0.0],
130            [0.25, 0.75, 0.0, 0.0],
131            [0.00, 1.00, 0.0, 0.0],
132            [0.00, 1.00, 0.0, 0.0],
133        ],
134    )
135    // Tell bevy to construct triangles from a list of vertex indices,
136    // where each 3 vertex indices form a triangle.
137    .with_inserted_indices(Indices::U16(vec![
138        0, 1, 3, 0, 3, 2, 2, 3, 5, 2, 5, 4, 4, 5, 7, 4, 7, 6, 6, 7, 9, 6, 9, 8,
139    ]))
140    // Create skinned mesh bounds. Together with the `DynamicSkinnedMeshBounds`
141    // component, this will ensure the mesh is correctly frustum culled.
142    .with_generated_skinned_mesh_bounds()
143    .unwrap();
144
145    let mesh = meshes.add(mesh);
146
147    // We're seeding the PRNG here to make this example deterministic for testing purposes.
148    // This isn't strictly required in practical use unless you need your app to be deterministic.
149    let mut rng = ChaCha8Rng::seed_from_u64(42);
150
151    for i in -5..5 {
152        // Create joint entities
153        let joint_0 = commands
154            .spawn(Transform::from_xyz(
155                i as f32 * 1.5,
156                0.0,
157                // Move quads back a small amount to avoid Z-fighting and not
158                // obscure the transform gizmos.
159                -(i as f32 * 0.01).abs(),
160            ))
161            .id();
162        let joint_1 = commands.spawn((AnimatedJoint(i), Transform::IDENTITY)).id();
163
164        // Set joint_1 as a child of joint_0.
165        commands.entity(joint_0).add_children(&[joint_1]);
166
167        // Each joint in this vector corresponds to each inverse bindpose matrix in `SkinnedMeshInverseBindposes`.
168        let joint_entities = vec![joint_0, joint_1];
169
170        // Create skinned mesh renderer. Note that its transform doesn't affect the position of the mesh.
171        commands.spawn((
172            Mesh3d(mesh.clone()),
173            MeshMaterial3d(materials.add(StandardMaterial {
174                base_color: Color::srgb(
175                    rng.random_range(0.0..1.0),
176                    rng.random_range(0.0..1.0),
177                    rng.random_range(0.0..1.0),
178                ),
179                base_color_texture: Some(asset_server.load("textures/uv_checker_bw.png")),
180                ..default()
181            })),
182            SkinnedMesh {
183                inverse_bindposes: inverse_bindposes.clone(),
184                joints: joint_entities,
185            },
186            DynamicSkinnedMeshBounds,
187        ));
188    }
189}
190
191/// Animate the joint marked with [`AnimatedJoint`] component.
192fn joint_animation(
193    time: Res<Time>,
194    mut query: Query<(&mut Transform, &AnimatedJoint)>,
195    mut gizmos: Gizmos,
196) {
197    for (mut transform, animated_joint) in &mut query {
198        match animated_joint.0 {
199            -5 => {
200                transform.rotation =
201                    Quat::from_rotation_x(FRAC_PI_2 * ops::sin(time.elapsed_secs()));
202            }
203            -4 => {
204                transform.rotation =
205                    Quat::from_rotation_y(FRAC_PI_2 * ops::sin(time.elapsed_secs()));
206            }
207            -3 => {
208                transform.rotation =
209                    Quat::from_rotation_z(FRAC_PI_2 * ops::sin(time.elapsed_secs()));
210            }
211            -2 => {
212                transform.scale.x = ops::sin(time.elapsed_secs()) + 1.0;
213            }
214            -1 => {
215                transform.scale.y = ops::sin(time.elapsed_secs()) + 1.0;
216            }
217            0 => {
218                transform.translation.x = 0.5 * ops::sin(time.elapsed_secs());
219                transform.translation.y = ops::cos(time.elapsed_secs());
220            }
221            1 => {
222                transform.translation.y = ops::sin(time.elapsed_secs());
223                transform.translation.z = ops::cos(time.elapsed_secs());
224            }
225            2 => {
226                transform.translation.x = ops::sin(time.elapsed_secs());
227            }
228            3 => {
229                transform.translation.y = ops::sin(time.elapsed_secs());
230                transform.scale.x = ops::sin(time.elapsed_secs()) + 1.0;
231            }
232            _ => (),
233        }
234        // Show transform
235        let mut axis = *transform;
236        axis.translation.x += animated_joint.0 as f32 * 1.5;
237        gizmos.axes(axis, 1.0);
238    }
239}