Skip to content

Commit 5c884c5

Browse files
superdumprobtfmnicopap
authored
Automatic batching/instancing of draw commands (#9685)
# Objective - Implement the foundations of automatic batching/instancing of draw commands as the next step from #89 - NOTE: More performance improvements will come when more data is managed and bound in ways that do not require rebinding such as mesh, material, and texture data. ## Solution - The core idea for batching of draw commands is to check whether any of the information that has to be passed when encoding a draw command changes between two things that are being drawn according to the sorted render phase order. These should be things like the pipeline, bind groups and their dynamic offsets, index/vertex buffers, and so on. - The following assumptions have been made: - Only entities with prepared assets (pipelines, materials, meshes) are queued to phases - View bindings are constant across a phase for a given draw function as phases are per-view - `batch_and_prepare_render_phase` is the only system that performs this batching and has sole responsibility for preparing the per-object data. As such the mesh binding and dynamic offsets are assumed to only vary as a result of the `batch_and_prepare_render_phase` system, e.g. due to having to split data across separate uniform bindings within the same buffer due to the maximum uniform buffer binding size. - Implement `GpuArrayBuffer` for `Mesh2dUniform` to store Mesh2dUniform in arrays in GPU buffers rather than each one being at a dynamic offset in a uniform buffer. This is the same optimisation that was made for 3D not long ago. - Change batch size for a range in `PhaseItem`, adding API for getting or mutating the range. This is more flexible than a size as the length of the range can be used in place of the size, but the start and end can be otherwise whatever is needed. - Add an optional mesh bind group dynamic offset to `PhaseItem`. This avoids having to do a massive table move just to insert `GpuArrayBufferIndex` components. ## Benchmarks All tests have been run on an M1 Max on AC power. `bevymark` and `many_cubes` were modified to use 1920x1080 with a scale factor of 1. I run a script that runs a separate Tracy capture process, and then runs the bevy example with `--features bevy_ci_testing,trace_tracy` and `CI_TESTING_CONFIG=../benchmark.ron` with the contents of `../benchmark.ron`: ```rust ( exit_after: Some(1500) ) ``` ...in order to run each test for 1500 frames. The recent changes to `many_cubes` and `bevymark` added reproducible random number generation so that with the same settings, the same rng will occur. They also added benchmark modes that use a fixed delta time for animations. Combined this means that the same frames should be rendered both on main and on the branch. The graphs compare main (yellow) to this PR (red). ### 3D Mesh `many_cubes --benchmark` <img width="1411" alt="Screenshot 2023-09-03 at 23 42 10" src="https://github.com/bevyengine/bevy/assets/302146/2088716a-c918-486c-8129-090b26fd2bc4"> The mesh and material are the same for all instances. This is basically the best case for the initial batching implementation as it results in 1 draw for the ~11.7k visible meshes. It gives a ~30% reduction in median frame time. The 1000th frame is identical using the flip tool: ![flip many_cubes-main-mesh3d many_cubes-batching-mesh3d 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/2511f37a-6df8-481a-932f-706ca4de7643) ``` Mean: 0.000000 Weighted median: 0.000000 1st weighted quartile: 0.000000 3rd weighted quartile: 0.000000 Min: 0.000000 Max: 0.000000 Evaluation time: 0.4615 seconds ``` ### 3D Mesh `many_cubes --benchmark --material-texture-count 10` <img width="1404" alt="Screenshot 2023-09-03 at 23 45 18" src="https://github.com/bevyengine/bevy/assets/302146/5ee9c447-5bd2-45c6-9706-ac5ff8916daf"> This run uses 10 different materials by varying their textures. The materials are randomly selected, and there is no sorting by material bind group for opaque 3D so any batching is 'random'. The PR produces a ~5% reduction in median frame time. If we were to sort the opaque phase by the material bind group, then this should be a lot faster. This produces about 10.5k draws for the 11.7k visible entities. This makes sense as randomly selecting from 10 materials gives a chance that two adjacent entities randomly select the same material and can be batched. The 1000th frame is identical in flip: ![flip many_cubes-main-mesh3d-mtc10 many_cubes-batching-mesh3d-mtc10 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/2b3a8614-9466-4ed8-b50c-d4aa71615dbb) ``` Mean: 0.000000 Weighted median: 0.000000 1st weighted quartile: 0.000000 3rd weighted quartile: 0.000000 Min: 0.000000 Max: 0.000000 Evaluation time: 0.4537 seconds ``` ### 3D Mesh `many_cubes --benchmark --vary-per-instance` <img width="1394" alt="Screenshot 2023-09-03 at 23 48 44" src="https://github.com/bevyengine/bevy/assets/302146/f02a816b-a444-4c18-a96a-63b5436f3b7f"> This run varies the material data per instance by randomly-generating its colour. This is the worst case for batching and that it performs about the same as `main` is a good thing as it demonstrates that the batching has minimal overhead when dealing with ~11k visible mesh entities. The 1000th frame is identical according to flip: ![flip many_cubes-main-mesh3d-vpi many_cubes-batching-mesh3d-vpi 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/ac5f5c14-9bda-4d1a-8219-7577d4aac68c) ``` Mean: 0.000000 Weighted median: 0.000000 1st weighted quartile: 0.000000 3rd weighted quartile: 0.000000 Min: 0.000000 Max: 0.000000 Evaluation time: 0.4568 seconds ``` ### 2D Mesh `bevymark --benchmark --waves 160 --per-wave 1000 --mode mesh2d` <img width="1412" alt="Screenshot 2023-09-03 at 23 59 56" src="https://github.com/bevyengine/bevy/assets/302146/cb02ae07-237b-4646-ae9f-fda4dafcbad4"> This spawns 160 waves of 1000 quad meshes that are shaded with ColorMaterial. Each wave has a different material so 160 waves currently should result in 160 batches. This results in a 50% reduction in median frame time. Capturing a screenshot of the 1000th frame main vs PR gives: ![flip bevymark-main-mesh2d bevymark-batching-mesh2d 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/80102728-1217-4059-87af-14d05044df40) ``` Mean: 0.001222 Weighted median: 0.750432 1st weighted quartile: 0.453494 3rd weighted quartile: 0.969758 Min: 0.000000 Max: 0.990296 Evaluation time: 0.4255 seconds ``` So they seem to produce the same results. I also double-checked the number of draws. `main` does 160000 draws, and the PR does 160, as expected. ### 2D Mesh `bevymark --benchmark --waves 160 --per-wave 1000 --mode mesh2d --material-texture-count 10` <img width="1392" alt="Screenshot 2023-09-04 at 00 09 22" src="https://github.com/bevyengine/bevy/assets/302146/4358da2e-ce32-4134-82df-3ab74c40849c"> This generates 10 textures and generates materials for each of those and then selects one material per wave. The median frame time is reduced by 50%. Similar to the plain run above, this produces 160 draws on the PR and 160000 on `main` and the 1000th frame is identical (ignoring the fps counter text overlay). ![flip bevymark-main-mesh2d-mtc10 bevymark-batching-mesh2d-mtc10 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/ebed2822-dce7-426a-858b-b77dc45b986f) ``` Mean: 0.002877 Weighted median: 0.964980 1st weighted quartile: 0.668871 3rd weighted quartile: 0.982749 Min: 0.000000 Max: 0.992377 Evaluation time: 0.4301 seconds ``` ### 2D Mesh `bevymark --benchmark --waves 160 --per-wave 1000 --mode mesh2d --vary-per-instance` <img width="1396" alt="Screenshot 2023-09-04 at 00 13 53" src="https://github.com/bevyengine/bevy/assets/302146/b2198b18-3439-47ad-919a-cdabe190facb"> This creates unique materials per instance by randomly-generating the material's colour. This is the worst case for 2D batching. Somehow, this PR manages a 7% reduction in median frame time. Both main and this PR issue 160000 draws. The 1000th frame is the same: ![flip bevymark-main-mesh2d-vpi bevymark-batching-mesh2d-vpi 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/a2ec471c-f576-4a36-a23b-b24b22578b97) ``` Mean: 0.001214 Weighted median: 0.937499 1st weighted quartile: 0.635467 3rd weighted quartile: 0.979085 Min: 0.000000 Max: 0.988971 Evaluation time: 0.4462 seconds ``` ### 2D Sprite `bevymark --benchmark --waves 160 --per-wave 1000 --mode sprite` <img width="1396" alt="Screenshot 2023-09-04 at 12 21 12" src="https://github.com/bevyengine/bevy/assets/302146/8b31e915-d6be-4cac-abf5-c6a4da9c3d43"> This just spawns 160 waves of 1000 sprites. There should be and is no notable difference between main and the PR. ### 2D Sprite `bevymark --benchmark --waves 160 --per-wave 1000 --mode sprite --material-texture-count 10` <img width="1389" alt="Screenshot 2023-09-04 at 12 36 08" src="https://github.com/bevyengine/bevy/assets/302146/45fe8d6d-c901-4062-a349-3693dd044413"> This spawns the sprites selecting a texture at random per instance from the 10 generated textures. This has no significant change vs main and shouldn't. ### 2D Sprite `bevymark --benchmark --waves 160 --per-wave 1000 --mode sprite --vary-per-instance` <img width="1401" alt="Screenshot 2023-09-04 at 12 29 52" src="https://github.com/bevyengine/bevy/assets/302146/762c5c60-352e-471f-8dbe-bbf10e24ebd6"> This sets the sprite colour as being unique per instance. This can still all be drawn using one batch. There should be no difference but the PR produces median frame times that are 4% higher. Investigation showed no clear sources of cost, rather a mix of give and take that should not happen. It seems like noise in the results. ### Summary | Benchmark | % change in median frame time | | ------------- | ------------- | | many_cubes | 🟩 -30% | | many_cubes 10 materials | 🟩 -5% | | many_cubes unique materials | 🟩 ~0% | | bevymark mesh2d | 🟩 -50% | | bevymark mesh2d 10 materials | 🟩 -50% | | bevymark mesh2d unique materials | 🟩 -7% | | bevymark sprite | 🟥 2% | | bevymark sprite 10 materials | 🟥 0.6% | | bevymark sprite unique materials | 🟥 4.1% | --- ## Changelog - Added: 2D and 3D mesh entities that share the same mesh and material (same textures, same data) are now batched into the same draw command for better performance. --------- Co-authored-by: robtfm <[email protected]> Co-authored-by: Nicola Papale <[email protected]>
1 parent e60249e commit 5c884c5

File tree

31 files changed

+772
-303
lines changed

31 files changed

+772
-303
lines changed

assets/shaders/custom_gltf_2d.wgsl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#import bevy_sprite::mesh2d_view_bindings globals
22
#import bevy_sprite::mesh2d_bindings mesh
3-
#import bevy_sprite::mesh2d_functions mesh2d_position_local_to_clip
3+
#import bevy_sprite::mesh2d_functions get_model_matrix, mesh2d_position_local_to_clip
44

55
struct Vertex {
6+
@builtin(instance_index) instance_index: u32,
67
@location(0) position: vec3<f32>,
78
@location(1) color: vec4<f32>,
89
@location(2) barycentric: vec3<f32>,
@@ -17,7 +18,8 @@ struct VertexOutput {
1718
@vertex
1819
fn vertex(vertex: Vertex) -> VertexOutput {
1920
var out: VertexOutput;
20-
out.clip_position = mesh2d_position_local_to_clip(mesh.model, vec4<f32>(vertex.position, 1.0));
21+
let model = get_model_matrix(vertex.instance_index);
22+
out.clip_position = mesh2d_position_local_to_clip(model, vec4<f32>(vertex.position, 1.0));
2123
out.color = vertex.color;
2224
out.barycentric = vertex.barycentric;
2325
return out;

crates/bevy_core_pipeline/src/core_2d/mod.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ pub mod graph {
1919
}
2020
pub const CORE_2D: &str = graph::NAME;
2121

22+
use std::ops::Range;
23+
2224
pub use camera_2d::*;
2325
pub use main_pass_2d_node::*;
2426

@@ -35,7 +37,7 @@ use bevy_render::{
3537
render_resource::CachedRenderPipelineId,
3638
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
3739
};
38-
use bevy_utils::FloatOrd;
40+
use bevy_utils::{nonmax::NonMaxU32, FloatOrd};
3941

4042
use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode};
4143

@@ -83,7 +85,8 @@ pub struct Transparent2d {
8385
pub entity: Entity,
8486
pub pipeline: CachedRenderPipelineId,
8587
pub draw_function: DrawFunctionId,
86-
pub batch_size: usize,
88+
pub batch_range: Range<u32>,
89+
pub dynamic_offset: Option<NonMaxU32>,
8790
}
8891

8992
impl PhaseItem for Transparent2d {
@@ -111,8 +114,23 @@ impl PhaseItem for Transparent2d {
111114
}
112115

113116
#[inline]
114-
fn batch_size(&self) -> usize {
115-
self.batch_size
117+
fn batch_range(&self) -> &Range<u32> {
118+
&self.batch_range
119+
}
120+
121+
#[inline]
122+
fn batch_range_mut(&mut self) -> &mut Range<u32> {
123+
&mut self.batch_range
124+
}
125+
126+
#[inline]
127+
fn dynamic_offset(&self) -> Option<NonMaxU32> {
128+
self.dynamic_offset
129+
}
130+
131+
#[inline]
132+
fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
133+
&mut self.dynamic_offset
116134
}
117135
}
118136

crates/bevy_core_pipeline/src/core_3d/mod.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub mod graph {
2424
}
2525
pub const CORE_3D: &str = graph::NAME;
2626

27-
use std::cmp::Reverse;
27+
use std::{cmp::Reverse, ops::Range};
2828

2929
pub use camera_3d::*;
3030
pub use main_opaque_pass_3d_node::*;
@@ -50,7 +50,7 @@ use bevy_render::{
5050
view::ViewDepthTexture,
5151
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
5252
};
53-
use bevy_utils::{FloatOrd, HashMap};
53+
use bevy_utils::{nonmax::NonMaxU32, FloatOrd, HashMap};
5454

5555
use crate::{
5656
prepass::{
@@ -135,7 +135,8 @@ pub struct Opaque3d {
135135
pub pipeline: CachedRenderPipelineId,
136136
pub entity: Entity,
137137
pub draw_function: DrawFunctionId,
138-
pub batch_size: usize,
138+
pub batch_range: Range<u32>,
139+
pub dynamic_offset: Option<NonMaxU32>,
139140
}
140141

141142
impl PhaseItem for Opaque3d {
@@ -164,8 +165,23 @@ impl PhaseItem for Opaque3d {
164165
}
165166

166167
#[inline]
167-
fn batch_size(&self) -> usize {
168-
self.batch_size
168+
fn batch_range(&self) -> &Range<u32> {
169+
&self.batch_range
170+
}
171+
172+
#[inline]
173+
fn batch_range_mut(&mut self) -> &mut Range<u32> {
174+
&mut self.batch_range
175+
}
176+
177+
#[inline]
178+
fn dynamic_offset(&self) -> Option<NonMaxU32> {
179+
self.dynamic_offset
180+
}
181+
182+
#[inline]
183+
fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
184+
&mut self.dynamic_offset
169185
}
170186
}
171187

@@ -181,7 +197,8 @@ pub struct AlphaMask3d {
181197
pub pipeline: CachedRenderPipelineId,
182198
pub entity: Entity,
183199
pub draw_function: DrawFunctionId,
184-
pub batch_size: usize,
200+
pub batch_range: Range<u32>,
201+
pub dynamic_offset: Option<NonMaxU32>,
185202
}
186203

187204
impl PhaseItem for AlphaMask3d {
@@ -210,8 +227,23 @@ impl PhaseItem for AlphaMask3d {
210227
}
211228

212229
#[inline]
213-
fn batch_size(&self) -> usize {
214-
self.batch_size
230+
fn batch_range(&self) -> &Range<u32> {
231+
&self.batch_range
232+
}
233+
234+
#[inline]
235+
fn batch_range_mut(&mut self) -> &mut Range<u32> {
236+
&mut self.batch_range
237+
}
238+
239+
#[inline]
240+
fn dynamic_offset(&self) -> Option<NonMaxU32> {
241+
self.dynamic_offset
242+
}
243+
244+
#[inline]
245+
fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
246+
&mut self.dynamic_offset
215247
}
216248
}
217249

@@ -227,7 +259,8 @@ pub struct Transparent3d {
227259
pub pipeline: CachedRenderPipelineId,
228260
pub entity: Entity,
229261
pub draw_function: DrawFunctionId,
230-
pub batch_size: usize,
262+
pub batch_range: Range<u32>,
263+
pub dynamic_offset: Option<NonMaxU32>,
231264
}
232265

233266
impl PhaseItem for Transparent3d {
@@ -255,8 +288,23 @@ impl PhaseItem for Transparent3d {
255288
}
256289

257290
#[inline]
258-
fn batch_size(&self) -> usize {
259-
self.batch_size
291+
fn batch_range(&self) -> &Range<u32> {
292+
&self.batch_range
293+
}
294+
295+
#[inline]
296+
fn batch_range_mut(&mut self) -> &mut Range<u32> {
297+
&mut self.batch_range
298+
}
299+
300+
#[inline]
301+
fn dynamic_offset(&self) -> Option<NonMaxU32> {
302+
self.dynamic_offset
303+
}
304+
305+
#[inline]
306+
fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
307+
&mut self.dynamic_offset
260308
}
261309
}
262310

crates/bevy_core_pipeline/src/prepass/mod.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
2828
pub mod node;
2929

30-
use std::cmp::Reverse;
30+
use std::{cmp::Reverse, ops::Range};
3131

3232
use bevy_ecs::prelude::*;
3333
use bevy_reflect::Reflect;
@@ -36,7 +36,7 @@ use bevy_render::{
3636
render_resource::{CachedRenderPipelineId, Extent3d, TextureFormat},
3737
texture::CachedTexture,
3838
};
39-
use bevy_utils::FloatOrd;
39+
use bevy_utils::{nonmax::NonMaxU32, FloatOrd};
4040

4141
pub const DEPTH_PREPASS_FORMAT: TextureFormat = TextureFormat::Depth32Float;
4242
pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm;
@@ -83,6 +83,8 @@ pub struct Opaque3dPrepass {
8383
pub entity: Entity,
8484
pub pipeline_id: CachedRenderPipelineId,
8585
pub draw_function: DrawFunctionId,
86+
pub batch_range: Range<u32>,
87+
pub dynamic_offset: Option<NonMaxU32>,
8688
}
8789

8890
impl PhaseItem for Opaque3dPrepass {
@@ -109,6 +111,26 @@ impl PhaseItem for Opaque3dPrepass {
109111
// Key negated to match reversed SortKey ordering
110112
radsort::sort_by_key(items, |item| -item.distance);
111113
}
114+
115+
#[inline]
116+
fn batch_range(&self) -> &Range<u32> {
117+
&self.batch_range
118+
}
119+
120+
#[inline]
121+
fn batch_range_mut(&mut self) -> &mut Range<u32> {
122+
&mut self.batch_range
123+
}
124+
125+
#[inline]
126+
fn dynamic_offset(&self) -> Option<NonMaxU32> {
127+
self.dynamic_offset
128+
}
129+
130+
#[inline]
131+
fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
132+
&mut self.dynamic_offset
133+
}
112134
}
113135

114136
impl CachedRenderPipelinePhaseItem for Opaque3dPrepass {
@@ -128,6 +150,8 @@ pub struct AlphaMask3dPrepass {
128150
pub entity: Entity,
129151
pub pipeline_id: CachedRenderPipelineId,
130152
pub draw_function: DrawFunctionId,
153+
pub batch_range: Range<u32>,
154+
pub dynamic_offset: Option<NonMaxU32>,
131155
}
132156

133157
impl PhaseItem for AlphaMask3dPrepass {
@@ -154,6 +178,26 @@ impl PhaseItem for AlphaMask3dPrepass {
154178
// Key negated to match reversed SortKey ordering
155179
radsort::sort_by_key(items, |item| -item.distance);
156180
}
181+
182+
#[inline]
183+
fn batch_range(&self) -> &Range<u32> {
184+
&self.batch_range
185+
}
186+
187+
#[inline]
188+
fn batch_range_mut(&mut self) -> &mut Range<u32> {
189+
&mut self.batch_range
190+
}
191+
192+
#[inline]
193+
fn dynamic_offset(&self) -> Option<NonMaxU32> {
194+
self.dynamic_offset
195+
}
196+
197+
#[inline]
198+
fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
199+
&mut self.dynamic_offset
200+
}
157201
}
158202

159203
impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass {

crates/bevy_gizmos/src/pipeline_2d.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ fn queue_line_gizmos_2d(
178178
draw_function,
179179
pipeline,
180180
sort_key: FloatOrd(f32::INFINITY),
181-
batch_size: 1,
181+
batch_range: 0..1,
182+
dynamic_offset: None,
182183
});
183184
}
184185
}

crates/bevy_gizmos/src/pipeline_3d.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ fn queue_line_gizmos_3d(
192192
draw_function,
193193
pipeline,
194194
distance: 0.,
195-
batch_size: 1,
195+
batch_range: 0..1,
196+
dynamic_offset: None,
196197
});
197198
}
198199
}

crates/bevy_math/src/affine3.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use glam::{Affine3A, Mat3, Vec3};
1+
use glam::{Affine3A, Mat3, Vec3, Vec3Swizzles, Vec4};
22

33
/// Reduced-size version of `glam::Affine3A` for use when storage has
44
/// significant performance impact. Convert to `glam::Affine3A` to do
@@ -10,6 +10,36 @@ pub struct Affine3 {
1010
pub translation: Vec3,
1111
}
1212

13+
impl Affine3 {
14+
/// Calculates the transpose of the affine 4x3 matrix to a 3x4 and formats it for packing into GPU buffers
15+
#[inline]
16+
pub fn to_transpose(&self) -> [Vec4; 3] {
17+
let transpose_3x3 = self.matrix3.transpose();
18+
[
19+
transpose_3x3.x_axis.extend(self.translation.x),
20+
transpose_3x3.y_axis.extend(self.translation.y),
21+
transpose_3x3.z_axis.extend(self.translation.z),
22+
]
23+
}
24+
25+
/// Calculates the inverse transpose of the 3x3 matrix and formats it for packing into GPU buffers
26+
#[inline]
27+
pub fn inverse_transpose_3x3(&self) -> ([Vec4; 2], f32) {
28+
let inverse_transpose_3x3 = Affine3A::from(self).inverse().matrix3.transpose();
29+
(
30+
[
31+
(inverse_transpose_3x3.x_axis, inverse_transpose_3x3.y_axis.x).into(),
32+
(
33+
inverse_transpose_3x3.y_axis.yz(),
34+
inverse_transpose_3x3.z_axis.xy(),
35+
)
36+
.into(),
37+
],
38+
inverse_transpose_3x3.z_axis.z,
39+
)
40+
}
41+
}
42+
1343
impl From<&Affine3A> for Affine3 {
1444
fn from(affine: &Affine3A) -> Self {
1545
Self {

0 commit comments

Comments
 (0)