Improved UI render batching (#8793)

# Objective

`prepare_uinodes` creates a new `UiBatch` whenever the texture changes,
when most often it's just queuing untextured quads. Instead of switching
textures, we can reduce the number of batches generated significantly by
adding a condition to the fragment shader so that it only multiplies by
the `textureSample` value when drawing a textured quad.

# Solution

Add a `mode` field to `UiVertex`.
In `prepare_uinodes` set `mode` to 0 if the quad is textured or 1 if
untextured.
Add a condition to the fragment shader that only multiplies by the
`color` value from `textureSample` if `mode` is set to 1.

---

## Changelog
* Added a `mode` field to `UiVertex`, and added an extra `u32` vertex
attribute to the shader and vertex buffer layout.
* In `prepare_uinodes` mode is set to 0 for the vertices of textured
quads, and 1 if untextured.
* Added a condition to the fragment shader in `ui.wgsl` that only
multiplies by the `color` value from `textureSample` if the mode is
equal to 0.
This commit is contained in:
ickshonpe 2023-06-22 00:50:29 +01:00 committed by GitHub
parent 0a881ab37f
commit c39e02cefb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 44 additions and 14 deletions

View File

@ -552,6 +552,7 @@ struct UiVertex {
pub position: [f32; 3], pub position: [f32; 3],
pub uv: [f32; 2], pub uv: [f32; 2],
pub color: [f32; 4], pub color: [f32; 4],
pub mode: u32,
} }
#[derive(Resource)] #[derive(Resource)]
@ -585,6 +586,9 @@ pub struct UiBatch {
pub z: f32, pub z: f32,
} }
const TEXTURED_QUAD: u32 = 0;
const UNTEXTURED_QUAD: u32 = 1;
pub fn prepare_uinodes( pub fn prepare_uinodes(
mut commands: Commands, mut commands: Commands,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
@ -601,20 +605,33 @@ pub fn prepare_uinodes(
let mut start = 0; let mut start = 0;
let mut end = 0; let mut end = 0;
let mut current_batch_handle = Default::default(); let mut current_batch_image = DEFAULT_IMAGE_HANDLE.typed();
let mut last_z = 0.0; let mut last_z = 0.0;
#[inline]
fn is_textured(image: &Handle<Image>) -> bool {
image.id() != DEFAULT_IMAGE_HANDLE.id()
}
for extracted_uinode in &extracted_uinodes.uinodes { for extracted_uinode in &extracted_uinodes.uinodes {
if current_batch_handle != extracted_uinode.image { let mode = if is_textured(&extracted_uinode.image) {
if start != end { if current_batch_image.id() != extracted_uinode.image.id() {
if is_textured(&current_batch_image) && start != end {
commands.spawn(UiBatch { commands.spawn(UiBatch {
range: start..end, range: start..end,
image: current_batch_handle, image: current_batch_image,
z: last_z, z: last_z,
}); });
start = end; start = end;
} }
current_batch_handle = extracted_uinode.image.clone_weak(); current_batch_image = extracted_uinode.image.clone_weak();
} }
TEXTURED_QUAD
} else {
// Untextured `UiBatch`es are never spawned within the loop.
// If all the `extracted_uinodes` are untextured a single untextured UiBatch will be spawned after the loop terminates.
UNTEXTURED_QUAD
};
let mut uinode_rect = extracted_uinode.rect; let mut uinode_rect = extracted_uinode.rect;
@ -672,7 +689,7 @@ pub fn prepare_uinodes(
continue; continue;
} }
} }
let uvs = if current_batch_handle.id() == DEFAULT_IMAGE_HANDLE.id() { let uvs = if mode == UNTEXTURED_QUAD {
[Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]
} else { } else {
let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max);
@ -717,6 +734,7 @@ pub fn prepare_uinodes(
position: positions_clipped[i].into(), position: positions_clipped[i].into(),
uv: uvs[i].into(), uv: uvs[i].into(),
color, color,
mode,
}); });
} }
@ -728,7 +746,7 @@ pub fn prepare_uinodes(
if start != end { if start != end {
commands.spawn(UiBatch { commands.spawn(UiBatch {
range: start..end, range: start..end,
image: current_batch_handle, image: current_batch_image,
z: last_z, z: last_z,
}); });
} }

View File

@ -77,6 +77,8 @@ impl SpecializedRenderPipeline for UiPipeline {
VertexFormat::Float32x2, VertexFormat::Float32x2,
// color // color
VertexFormat::Float32x4, VertexFormat::Float32x4,
// mode
VertexFormat::Uint32,
], ],
); );
let shader_defs = Vec::new(); let shader_defs = Vec::new();

View File

@ -1,11 +1,14 @@
#import bevy_render::view #import bevy_render::view
const TEXTURED_QUAD: u32 = 0u;
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> view: View; var<uniform> view: View;
struct VertexOutput { struct VertexOutput {
@location(0) uv: vec2<f32>, @location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>, @location(1) color: vec4<f32>,
@location(3) mode: u32,
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
}; };
@ -14,11 +17,13 @@ fn vertex(
@location(0) vertex_position: vec3<f32>, @location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>, @location(1) vertex_uv: vec2<f32>,
@location(2) vertex_color: vec4<f32>, @location(2) vertex_color: vec4<f32>,
@location(3) mode: u32,
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.uv = vertex_uv; out.uv = vertex_uv;
out.position = view.view_proj * vec4<f32>(vertex_position, 1.0); out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
out.color = vertex_color; out.color = vertex_color;
out.mode = mode;
return out; return out;
} }
@ -29,7 +34,12 @@ var sprite_sampler: sampler;
@fragment @fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> { fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// textureSample can only be called in unform control flow, not inside an if branch.
var color = textureSample(sprite_texture, sprite_sampler, in.uv); var color = textureSample(sprite_texture, sprite_sampler, in.uv);
if in.mode == TEXTURED_QUAD {
color = in.color * color; color = in.color * color;
} else {
color = in.color;
}
return color; return color;
} }