Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_pbr/src/render/skin.rs
9504 views
1
use core::mem::{self, size_of};
2
use std::sync::OnceLock;
3
4
use bevy_asset::{prelude::AssetChanged, Assets};
5
use bevy_camera::visibility::ViewVisibility;
6
use bevy_ecs::prelude::*;
7
use bevy_math::Mat4;
8
use bevy_mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes};
9
use bevy_platform::collections::hash_map::Entry;
10
use bevy_render::render_resource::{Buffer, BufferDescriptor};
11
use bevy_render::settings::WgpuLimits;
12
use bevy_render::sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet};
13
use bevy_render::{
14
batching::NoAutomaticBatching,
15
render_resource::BufferUsages,
16
renderer::{RenderDevice, RenderQueue},
17
Extract,
18
};
19
use bevy_transform::prelude::GlobalTransform;
20
use offset_allocator::{Allocation, Allocator};
21
use smallvec::SmallVec;
22
use tracing::error;
23
24
/// Maximum number of joints supported for skinned meshes.
25
///
26
/// It is used to allocate buffers.
27
/// The correctness of the value depends on the GPU/platform.
28
/// The current value is chosen because it is guaranteed to work everywhere.
29
/// To allow for bigger values, a check must be made for the limits
30
/// of the GPU at runtime, which would mean not using consts anymore.
31
pub const MAX_JOINTS: usize = 256;
32
33
/// The total number of joints we support.
34
///
35
/// This is 256 GiB worth of joint matrices, which we will never hit under any
36
/// reasonable circumstances.
37
const MAX_TOTAL_JOINTS: u32 = 1024 * 1024 * 1024;
38
39
/// The number of joints that we allocate at a time.
40
///
41
/// Some hardware requires that uniforms be allocated on 256-byte boundaries, so
42
/// we need to allocate 4 64-byte matrices at a time to satisfy alignment
43
/// requirements.
44
const JOINTS_PER_ALLOCATION_UNIT: u32 = (256 / size_of::<Mat4>()) as u32;
45
46
/// The maximum ratio of the number of entities whose transforms changed to the
47
/// total number of joints before we re-extract all joints.
48
///
49
/// We use this as a heuristic to decide whether it's worth switching over to
50
/// fine-grained detection to determine which skins need extraction. If the
51
/// number of changed entities is over this threshold, we skip change detection
52
/// and simply re-extract the transforms of all joints.
53
const JOINT_EXTRACTION_THRESHOLD_FACTOR: f64 = 0.25;
54
55
/// The location of the first joint matrix in the skin uniform buffer.
56
#[derive(Clone, Copy)]
57
pub struct SkinByteOffset {
58
/// The byte offset of the first joint matrix.
59
pub byte_offset: u32,
60
}
61
62
impl SkinByteOffset {
63
/// Index to be in address space based on the size of a skin uniform.
64
const fn from_index(index: usize) -> Self {
65
SkinByteOffset {
66
byte_offset: (index * size_of::<Mat4>()) as u32,
67
}
68
}
69
70
/// Returns this skin index in elements (not bytes).
71
///
72
/// Each element is a 4x4 matrix.
73
pub fn index(&self) -> u32 {
74
self.byte_offset / size_of::<Mat4>() as u32
75
}
76
}
77
78
/// The GPU buffers containing joint matrices for all skinned meshes.
79
///
80
/// This is double-buffered: we store the joint matrices of each mesh for the
81
/// previous frame in addition to those of each mesh for the current frame. This
82
/// is for motion vector calculation. Every frame, we swap buffers and overwrite
83
/// the joint matrix buffer from two frames ago with the data for the current
84
/// frame.
85
///
86
/// Notes on implementation: see comment on top of the `extract_skins` system.
87
#[derive(Resource)]
88
pub struct SkinUniforms {
89
/// The CPU-side buffer that stores the joint matrices for skinned meshes in
90
/// the current frame.
91
pub current_staging_buffer: Vec<Mat4>,
92
/// The GPU-side buffer that stores the joint matrices for skinned meshes in
93
/// the current frame.
94
pub current_buffer: Buffer,
95
/// The GPU-side buffer that stores the joint matrices for skinned meshes in
96
/// the previous frame.
97
pub prev_buffer: Buffer,
98
/// The offset allocator that manages the placement of the joints within the
99
/// [`Self::current_buffer`].
100
allocator: Allocator,
101
/// Allocation information that we keep about each skin.
102
skin_uniform_info: MainEntityHashMap<SkinUniformInfo>,
103
/// Maps each joint entity to the skins it's associated with.
104
///
105
/// We use this in conjunction with change detection to only update the
106
/// skins that need updating each frame.
107
///
108
/// Note that conceptually this is a hash map of sets, but we use a
109
/// [`SmallVec`] to avoid allocations for the vast majority of the cases in
110
/// which each bone belongs to exactly one skin.
111
joint_to_skins: MainEntityHashMap<SmallVec<[MainEntity; 1]>>,
112
/// The total number of joints in the scene.
113
///
114
/// We use this as part of our heuristic to decide whether to use
115
/// fine-grained change detection.
116
total_joints: usize,
117
}
118
119
pub fn skin_uniforms_from_world(device: Res<RenderDevice>, mut commands: Commands) {
120
let buffer_usages = (if skins_use_uniform_buffers(&device.limits()) {
121
BufferUsages::UNIFORM
122
} else {
123
BufferUsages::STORAGE
124
}) | BufferUsages::COPY_DST;
125
126
// Create the current and previous buffer with the minimum sizes.
127
//
128
// These will be swapped every frame.
129
let current_buffer = device.create_buffer(&BufferDescriptor {
130
label: Some("skin uniform buffer"),
131
size: MAX_JOINTS as u64 * size_of::<Mat4>() as u64,
132
usage: buffer_usages,
133
mapped_at_creation: false,
134
});
135
let prev_buffer = device.create_buffer(&BufferDescriptor {
136
label: Some("skin uniform buffer"),
137
size: MAX_JOINTS as u64 * size_of::<Mat4>() as u64,
138
usage: buffer_usages,
139
mapped_at_creation: false,
140
});
141
142
let res = SkinUniforms {
143
current_staging_buffer: vec![],
144
current_buffer,
145
prev_buffer,
146
allocator: Allocator::new(MAX_TOTAL_JOINTS),
147
skin_uniform_info: MainEntityHashMap::default(),
148
joint_to_skins: MainEntityHashMap::default(),
149
total_joints: 0,
150
};
151
152
commands.insert_resource(res);
153
}
154
155
impl SkinUniforms {
156
/// Returns the current offset in joints of the skin in the buffer.
157
pub fn skin_index(&self, skin: MainEntity) -> Option<u32> {
158
self.skin_uniform_info
159
.get(&skin)
160
.map(SkinUniformInfo::offset)
161
}
162
163
/// Returns the current offset in bytes of the skin in the buffer.
164
pub fn skin_byte_offset(&self, skin: MainEntity) -> Option<SkinByteOffset> {
165
self.skin_uniform_info.get(&skin).map(|skin_uniform_info| {
166
SkinByteOffset::from_index(skin_uniform_info.offset() as usize)
167
})
168
}
169
170
/// Returns an iterator over all skins in the scene.
171
pub fn all_skins(&self) -> impl Iterator<Item = &MainEntity> {
172
self.skin_uniform_info.keys()
173
}
174
}
175
176
/// Allocation information about each skin.
177
struct SkinUniformInfo {
178
/// The allocation of the joints within the [`SkinUniforms::current_buffer`].
179
allocation: Allocation,
180
/// The entities that comprise the joints.
181
joints: Vec<MainEntity>,
182
}
183
184
impl SkinUniformInfo {
185
/// The offset in joints within the [`SkinUniforms::current_staging_buffer`].
186
fn offset(&self) -> u32 {
187
self.allocation.offset * JOINTS_PER_ALLOCATION_UNIT
188
}
189
}
190
191
/// Returns true if skinning must use uniforms (and dynamic offsets) because
192
/// storage buffers aren't supported on the current platform.
193
pub fn skins_use_uniform_buffers(limits: &WgpuLimits) -> bool {
194
static SKINS_USE_UNIFORM_BUFFERS: OnceLock<bool> = OnceLock::new();
195
*SKINS_USE_UNIFORM_BUFFERS.get_or_init(|| limits.max_storage_buffers_per_shader_stage == 0)
196
}
197
198
/// Uploads the buffers containing the joints to the GPU.
199
pub fn prepare_skins(
200
render_device: Res<RenderDevice>,
201
render_queue: Res<RenderQueue>,
202
uniform: ResMut<SkinUniforms>,
203
) {
204
let uniform = uniform.into_inner();
205
206
if uniform.current_staging_buffer.is_empty() {
207
return;
208
}
209
210
// Swap current and previous buffers.
211
mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer);
212
213
// Resize the buffers if necessary. Include extra space equal to `MAX_JOINTS`
214
// because we need to be able to bind a full uniform buffer's worth of data
215
// if skins use uniform buffers on this platform.
216
let needed_size = (uniform.current_staging_buffer.len() as u64 + MAX_JOINTS as u64)
217
* size_of::<Mat4>() as u64;
218
if uniform.current_buffer.size() < needed_size {
219
let mut new_size = uniform.current_buffer.size();
220
while new_size < needed_size {
221
// 1.5× growth factor.
222
new_size = (new_size + new_size / 2).next_multiple_of(4);
223
}
224
225
// Create the new buffers.
226
let buffer_usages = if skins_use_uniform_buffers(&render_device.limits()) {
227
BufferUsages::UNIFORM
228
} else {
229
BufferUsages::STORAGE
230
} | BufferUsages::COPY_DST;
231
uniform.current_buffer = render_device.create_buffer(&BufferDescriptor {
232
label: Some("skin uniform buffer"),
233
usage: buffer_usages,
234
size: new_size,
235
mapped_at_creation: false,
236
});
237
uniform.prev_buffer = render_device.create_buffer(&BufferDescriptor {
238
label: Some("skin uniform buffer"),
239
usage: buffer_usages,
240
size: new_size,
241
mapped_at_creation: false,
242
});
243
244
// We've created a new `prev_buffer` but we don't have the previous joint
245
// data needed to fill it out correctly. Use the current joint data
246
// instead.
247
//
248
// TODO: This is a bug - will cause motion blur to ignore joint movement
249
// for one frame.
250
render_queue.write_buffer(
251
&uniform.prev_buffer,
252
0,
253
bytemuck::must_cast_slice(&uniform.current_staging_buffer[..]),
254
);
255
}
256
257
// Write the data from `uniform.current_staging_buffer` into
258
// `uniform.current_buffer`.
259
render_queue.write_buffer(
260
&uniform.current_buffer,
261
0,
262
bytemuck::must_cast_slice(&uniform.current_staging_buffer[..]),
263
);
264
265
// We don't need to write `uniform.prev_buffer` because we already wrote it
266
// last frame, and the data should still be on the GPU.
267
}
268
269
// Notes on implementation:
270
// We define the uniform binding as an array<mat4x4<f32>, N> in the shader,
271
// where N is the maximum number of Mat4s we can fit in the uniform binding,
272
// which may be as little as 16kB or 64kB. But, we may not need all N.
273
// We may only need, for example, 10.
274
//
275
// If we used uniform buffers ‘normally’ then we would have to write a full
276
// binding of data for each dynamic offset binding, which is wasteful, makes
277
// the buffer much larger than it needs to be, and uses more memory bandwidth
278
// to transfer the data, which then costs frame time So @superdump came up
279
// with this design: just bind data at the specified offset and interpret
280
// the data at that offset as an array<T, N> regardless of what is there.
281
//
282
// So instead of writing N Mat4s when you only need 10, you write 10, and
283
// then pad up to the next dynamic offset alignment. Then write the next.
284
// And for the last dynamic offset binding, make sure there is a full binding
285
// of data after it so that the buffer is of size
286
// `last dynamic offset` + `array<mat4x4<f32>>`.
287
//
288
// Then when binding the first dynamic offset, the first 10 entries in the array
289
// are what you expect, but if you read the 11th you’re reading ‘invalid’ data
290
// which could be padding or could be from the next binding.
291
//
292
// In this way, we can pack ‘variable sized arrays’ into uniform buffer bindings
293
// which normally only support fixed size arrays. You just have to make sure
294
// in the shader that you only read the values that are valid for that binding.
295
pub fn extract_skins(
296
skin_uniforms: ResMut<SkinUniforms>,
297
skinned_meshes: Extract<Query<(Entity, &SkinnedMesh)>>,
298
changed_skinned_meshes: Extract<
299
Query<
300
(Entity, &ViewVisibility, &SkinnedMesh),
301
Or<(
302
Changed<ViewVisibility>,
303
Changed<SkinnedMesh>,
304
AssetChanged<SkinnedMesh>,
305
)>,
306
>,
307
>,
308
skinned_mesh_inverse_bindposes: Extract<Res<Assets<SkinnedMeshInverseBindposes>>>,
309
changed_transforms: Extract<Query<(Entity, &GlobalTransform), Changed<GlobalTransform>>>,
310
joints: Extract<Query<&GlobalTransform>>,
311
mut removed_skinned_meshes_query: Extract<RemovedComponents<SkinnedMesh>>,
312
) {
313
let skin_uniforms = skin_uniforms.into_inner();
314
315
// Find skins that have become visible or invisible on this frame. Allocate,
316
// reallocate, or free space for them as necessary.
317
add_or_delete_skins(
318
skin_uniforms,
319
&changed_skinned_meshes,
320
&skinned_mesh_inverse_bindposes,
321
&joints,
322
);
323
324
// Extract the transforms for all joints from the scene, and write them into
325
// the staging buffer at the appropriate spot.
326
extract_joints(
327
skin_uniforms,
328
&skinned_meshes,
329
&changed_skinned_meshes,
330
&skinned_mesh_inverse_bindposes,
331
&changed_transforms,
332
&joints,
333
);
334
335
// Delete skins that became invisible.
336
for skinned_mesh_entity in removed_skinned_meshes_query.read() {
337
// Only remove a skin if we didn't pick it up in `add_or_delete_skins`.
338
// It's possible that a necessary component was removed and re-added in
339
// the same frame.
340
if !changed_skinned_meshes.contains(skinned_mesh_entity) {
341
remove_skin(skin_uniforms, skinned_mesh_entity.into());
342
}
343
}
344
}
345
346
/// Searches for all skins that have become visible or invisible this frame and
347
/// allocations for them as necessary.
348
fn add_or_delete_skins(
349
skin_uniforms: &mut SkinUniforms,
350
changed_skinned_meshes: &Query<
351
(Entity, &ViewVisibility, &SkinnedMesh),
352
Or<(
353
Changed<ViewVisibility>,
354
Changed<SkinnedMesh>,
355
AssetChanged<SkinnedMesh>,
356
)>,
357
>,
358
skinned_mesh_inverse_bindposes: &Assets<SkinnedMeshInverseBindposes>,
359
joints: &Query<&GlobalTransform>,
360
) {
361
// Find every skinned mesh that changed one of (1) visibility; (2) joint
362
// entities (part of `SkinnedMesh`); (3) the associated
363
// `SkinnedMeshInverseBindposes` asset.
364
for (skinned_mesh_entity, skinned_mesh_view_visibility, skinned_mesh) in changed_skinned_meshes
365
{
366
// Remove the skin if it existed last frame.
367
let skinned_mesh_entity = MainEntity::from(skinned_mesh_entity);
368
remove_skin(skin_uniforms, skinned_mesh_entity);
369
370
// If the skin is invisible, we're done.
371
if !(*skinned_mesh_view_visibility).get() {
372
continue;
373
}
374
375
// Initialize the skin.
376
add_skin(
377
skinned_mesh_entity,
378
skinned_mesh,
379
skin_uniforms,
380
skinned_mesh_inverse_bindposes,
381
joints,
382
);
383
}
384
}
385
386
/// Extracts the global transforms of all joints and updates the staging buffer
387
/// as necessary.
388
fn extract_joints(
389
skin_uniforms: &mut SkinUniforms,
390
skinned_meshes: &Query<(Entity, &SkinnedMesh)>,
391
changed_skinned_meshes: &Query<
392
(Entity, &ViewVisibility, &SkinnedMesh),
393
Or<(
394
Changed<ViewVisibility>,
395
Changed<SkinnedMesh>,
396
AssetChanged<SkinnedMesh>,
397
)>,
398
>,
399
skinned_mesh_inverse_bindposes: &Assets<SkinnedMeshInverseBindposes>,
400
changed_transforms: &Query<(Entity, &GlobalTransform), Changed<GlobalTransform>>,
401
joints: &Query<&GlobalTransform>,
402
) {
403
// If the number of entities that changed transforms exceeds a certain
404
// fraction (currently 25%) of the total joints in the scene, then skip
405
// fine-grained change detection.
406
//
407
// Note that this is a crude heuristic, for performance reasons. It doesn't
408
// consider the ratio of modified *joints* to total joints, only the ratio
409
// of modified *entities* to total joints. Thus in the worst case we might
410
// end up re-extracting all skins even though none of the joints changed.
411
// But making the heuristic finer-grained would make it slower to evaluate,
412
// and we don't want to lose performance.
413
let threshold =
414
(skin_uniforms.total_joints as f64 * JOINT_EXTRACTION_THRESHOLD_FACTOR).floor() as usize;
415
416
if changed_transforms.iter().nth(threshold).is_some() {
417
// Go ahead and re-extract all skins in the scene.
418
for (skin_entity, skin) in skinned_meshes {
419
extract_joints_for_skin(
420
skin_entity.into(),
421
skin,
422
skin_uniforms,
423
changed_skinned_meshes,
424
skinned_mesh_inverse_bindposes,
425
joints,
426
);
427
}
428
return;
429
}
430
431
// Use fine-grained change detection to figure out only the skins that need
432
// to have their joints re-extracted.
433
let dirty_skins: MainEntityHashSet = changed_transforms
434
.iter()
435
.flat_map(|(joint, _)| skin_uniforms.joint_to_skins.get(&MainEntity::from(joint)))
436
.flat_map(|skin_joint_mappings| skin_joint_mappings.iter())
437
.copied()
438
.collect();
439
440
// Re-extract the joints for only those skins.
441
for skin_entity in dirty_skins {
442
let Ok((_, skin)) = skinned_meshes.get(*skin_entity) else {
443
continue;
444
};
445
extract_joints_for_skin(
446
skin_entity,
447
skin,
448
skin_uniforms,
449
changed_skinned_meshes,
450
skinned_mesh_inverse_bindposes,
451
joints,
452
);
453
}
454
}
455
456
/// Extracts all joints for a single skin and writes their transforms into the
457
/// CPU staging buffer.
458
fn extract_joints_for_skin(
459
skin_entity: MainEntity,
460
skin: &SkinnedMesh,
461
skin_uniforms: &mut SkinUniforms,
462
changed_skinned_meshes: &Query<
463
(Entity, &ViewVisibility, &SkinnedMesh),
464
Or<(
465
Changed<ViewVisibility>,
466
Changed<SkinnedMesh>,
467
AssetChanged<SkinnedMesh>,
468
)>,
469
>,
470
skinned_mesh_inverse_bindposes: &Assets<SkinnedMeshInverseBindposes>,
471
joints: &Query<&GlobalTransform>,
472
) {
473
// If we initialized the skin this frame, we already populated all
474
// the joints, so there's no need to populate them again.
475
if changed_skinned_meshes.contains(*skin_entity) {
476
return;
477
}
478
479
// Fetch information about the skin.
480
let Some(skin_uniform_info) = skin_uniforms.skin_uniform_info.get(&skin_entity) else {
481
return;
482
};
483
let Some(skinned_mesh_inverse_bindposes) =
484
skinned_mesh_inverse_bindposes.get(&skin.inverse_bindposes)
485
else {
486
return;
487
};
488
489
// Calculate and write in the new joint matrices.
490
for (joint_index, (&joint, skinned_mesh_inverse_bindpose)) in skin
491
.joints
492
.iter()
493
.zip(skinned_mesh_inverse_bindposes.iter())
494
.enumerate()
495
{
496
let Ok(joint_transform) = joints.get(joint) else {
497
continue;
498
};
499
500
let joint_matrix = joint_transform.affine() * *skinned_mesh_inverse_bindpose;
501
skin_uniforms.current_staging_buffer[skin_uniform_info.offset() as usize + joint_index] =
502
joint_matrix;
503
}
504
}
505
506
/// Allocates space for a new skin in the buffers, and populates its joints.
507
fn add_skin(
508
skinned_mesh_entity: MainEntity,
509
skinned_mesh: &SkinnedMesh,
510
skin_uniforms: &mut SkinUniforms,
511
skinned_mesh_inverse_bindposes: &Assets<SkinnedMeshInverseBindposes>,
512
joints: &Query<&GlobalTransform>,
513
) {
514
// Allocate space for the joints.
515
let Some(allocation) = skin_uniforms.allocator.allocate(
516
skinned_mesh
517
.joints
518
.len()
519
.div_ceil(JOINTS_PER_ALLOCATION_UNIT as usize) as u32,
520
) else {
521
error!(
522
"Out of space for skin: {:?}. Tried to allocate space for {:?} joints.",
523
skinned_mesh_entity,
524
skinned_mesh.joints.len()
525
);
526
return;
527
};
528
529
// Store that allocation.
530
let skin_uniform_info = SkinUniformInfo {
531
allocation,
532
joints: skinned_mesh
533
.joints
534
.iter()
535
.map(|entity| MainEntity::from(*entity))
536
.collect(),
537
};
538
539
let skinned_mesh_inverse_bindposes =
540
skinned_mesh_inverse_bindposes.get(&skinned_mesh.inverse_bindposes);
541
542
for (joint_index, &joint) in skinned_mesh.joints.iter().enumerate() {
543
// Calculate the initial joint matrix.
544
let skinned_mesh_inverse_bindpose =
545
skinned_mesh_inverse_bindposes.and_then(|skinned_mesh_inverse_bindposes| {
546
skinned_mesh_inverse_bindposes.get(joint_index)
547
});
548
let joint_matrix = match (skinned_mesh_inverse_bindpose, joints.get(joint)) {
549
(Some(skinned_mesh_inverse_bindpose), Ok(transform)) => {
550
transform.affine() * *skinned_mesh_inverse_bindpose
551
}
552
_ => Mat4::IDENTITY,
553
};
554
555
// Write in the new joint matrix, growing the staging buffer if
556
// necessary.
557
let buffer_index = skin_uniform_info.offset() as usize + joint_index;
558
if skin_uniforms.current_staging_buffer.len() < buffer_index + 1 {
559
skin_uniforms
560
.current_staging_buffer
561
.resize(buffer_index + 1, Mat4::IDENTITY);
562
}
563
skin_uniforms.current_staging_buffer[buffer_index] = joint_matrix;
564
565
// Record the inverse mapping from the joint back to the skin. We use
566
// this in order to perform fine-grained joint extraction.
567
skin_uniforms
568
.joint_to_skins
569
.entry(MainEntity::from(joint))
570
.or_default()
571
.push(skinned_mesh_entity);
572
}
573
574
// Record the number of joints.
575
skin_uniforms.total_joints += skinned_mesh.joints.len();
576
577
skin_uniforms
578
.skin_uniform_info
579
.insert(skinned_mesh_entity, skin_uniform_info);
580
}
581
582
/// Deallocates a skin and removes it from the [`SkinUniforms`].
583
fn remove_skin(skin_uniforms: &mut SkinUniforms, skinned_mesh_entity: MainEntity) {
584
let Some(old_skin_uniform_info) = skin_uniforms.skin_uniform_info.remove(&skinned_mesh_entity)
585
else {
586
return;
587
};
588
589
// Free the allocation.
590
skin_uniforms
591
.allocator
592
.free(old_skin_uniform_info.allocation);
593
594
// Remove the inverse mapping from each joint back to the skin.
595
for &joint in &old_skin_uniform_info.joints {
596
if let Entry::Occupied(mut entry) = skin_uniforms.joint_to_skins.entry(joint) {
597
entry.get_mut().retain(|skin| *skin != skinned_mesh_entity);
598
if entry.get_mut().is_empty() {
599
entry.remove();
600
}
601
}
602
}
603
604
// Update the total number of joints.
605
skin_uniforms.total_joints -= old_skin_uniform_info.joints.len();
606
}
607
608
// NOTE: The skinned joints uniform buffer has to be bound at a dynamic offset per
609
// entity and so cannot currently be batched on WebGL 2.
610
pub fn no_automatic_skin_batching(
611
mut commands: Commands,
612
query: Query<Entity, (With<SkinnedMesh>, Without<NoAutomaticBatching>)>,
613
render_device: Res<RenderDevice>,
614
) {
615
if !skins_use_uniform_buffers(&render_device.limits()) {
616
return;
617
}
618
619
for entity in &query {
620
commands.entity(entity).try_insert(NoAutomaticBatching);
621
}
622
}
623
624