Skip to content

Commit

Permalink
UI texture atlas slice shader (bevyengine#14990)
Browse files Browse the repository at this point in the history
# Objective

Fixes bevyengine#14183

## Solution

Reimplement the UI texture atlas slicer using a shader. 

The problems with bevyengine#14183 could be fixed more simply by hacking around
with the coordinates and scaling but that way is very fragile and might
get broken again the next time we make changes to the layout
calculations. A shader based solution is more robust, it's impossible
for gaps to appear between the image slices with these changes as we're
only drawing a single quad.

I've not tried any benchmarks yet but it should much more efficient as
well, in the worst cases even hundreds or thousands of times faster.

Maybe could have used the UiMaterialPipeline. I wrote the shader first
and used fat vertices and then realised it wouldn't work that way with a
UiMaterial. If it's rewritten it so it puts all the slice geometry in
uniform buffer, then it might work? Adding the uniform buffer would
probably make the shader more complicated though, so don't know if it's
even worth it. Instancing is another alternative.

## Testing
The examples are working and it seems to match the old API correctly but
I've not used the texture atlas slicing API for anything before, I
reviewed the PR but that was back in January.

Needs a review by someone who knows the rendering pipeline and wgsl
really well because I don't really have any idea what I'm doing.
  • Loading branch information
ickshonpe committed Sep 2, 2024
1 parent 659860d commit 01a3b0e
Show file tree
Hide file tree
Showing 6 changed files with 909 additions and 247 deletions.
6 changes: 0 additions & 6 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ mod geometry;
mod layout;
mod render;
mod stack;
mod texture_slice;
mod ui_node;

pub use focus::*;
Expand Down Expand Up @@ -181,11 +180,6 @@ impl Plugin for UiPlugin {
.in_set(UiSystem::Prepare)
.in_set(AmbiguousWithTextSystem)
.in_set(AmbiguousWithUpdateText2DLayout),
(
texture_slice::compute_slices_on_asset_event,
texture_slice::compute_slices_on_image_change,
)
.in_set(UiSystem::PostLayout),
),
);

Expand Down
49 changes: 22 additions & 27 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod pipeline;
mod render_pass;
mod ui_material_pipeline;
pub mod ui_texture_slice_pipeline;

use bevy_color::{Alpha, ColorToComponents, LinearRgba};
use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d};
Expand All @@ -15,16 +16,16 @@ use bevy_render::{
view::ViewVisibility,
ExtractSchedule, Render,
};
use bevy_sprite::{SpriteAssetEvents, TextureAtlas};
use bevy_sprite::{ImageScaleMode, SpriteAssetEvents, TextureAtlas};
pub use pipeline::*;
pub use render_pass::*;
pub use ui_material_pipeline::*;
use ui_texture_slice_pipeline::UiTextureSlicerPlugin;

use crate::graph::{NodeUi, SubGraphUi};
use crate::{
texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius,
CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage,
UiScale, Val,
BackgroundColor, BorderColor, BorderRadius, CalculatedClip, ContentSize, DefaultUiCamera, Node,
Outline, Style, TargetCamera, UiImage, UiScale, Val,
};

use bevy_app::prelude::*;
Expand Down Expand Up @@ -140,6 +141,8 @@ pub fn build_ui_render(app: &mut App) {
graph_3d.add_node_edge(Node3d::EndMainPassPostProcessing, NodeUi::UiPass);
graph_3d.add_node_edge(NodeUi::UiPass, Node3d::Upscaling);
}

app.add_plugins(UiTextureSlicerPlugin);
}

fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph {
Expand Down Expand Up @@ -299,19 +302,21 @@ pub fn extract_uinode_images(
ui_scale: Extract<Res<UiScale>>,
default_ui_camera: Extract<DefaultUiCamera>,
uinode_query: Extract<
Query<(
&Node,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
Option<&TextureAtlas>,
Option<&ComputedTextureSlices>,
Option<&BorderRadius>,
Option<&Parent>,
&Style,
)>,
Query<
(
&Node,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
Option<&TextureAtlas>,
Option<&BorderRadius>,
Option<&Parent>,
&Style,
),
Without<ImageScaleMode>,
>,
>,
node_query: Extract<Query<&Node>>,
) {
Expand All @@ -323,7 +328,6 @@ pub fn extract_uinode_images(
camera,
image,
atlas,
slices,
border_radius,
parent,
style,
Expand All @@ -342,15 +346,6 @@ pub fn extract_uinode_images(
continue;
}

if let Some(slices) = slices {
extracted_uinodes.uinodes.extend(
slices
.extract_ui_nodes(transform, uinode, image, clip, camera_entity)
.map(|e| (commands.spawn_empty().id(), e)),
);
continue;
}

let (rect, atlas_scaling) = match atlas {
Some(atlas) => {
let Some(layout) = texture_atlases.get(&atlas.layout) else {
Expand Down
127 changes: 127 additions & 0 deletions crates/bevy_ui/src/render/ui_texture_slice.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#import bevy_render::view::View;
#import bevy_render::globals::Globals;

@group(0) @binding(0)
var<uniform> view: View;
@group(0) @binding(1)
var<uniform> globals: Globals;

@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
@group(1) @binding(1) var sprite_sampler: sampler;

struct UiVertexOutput {
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,

// Defines the dividing line that are used to split the texture atlas rect into corner, side and center slices
// The distances are normalized and from the top left corner of the texture atlas rect
// x = distance of the left vertical dividing line
// y = distance of the top horizontal dividing line
// z = distance of the right vertical dividing line
// w = distance of the bottom horizontal dividing line
@location(2) @interpolate(flat) texture_slices: vec4<f32>,

// Defines the dividing line that are used to split the render target into into corner, side and center slices
// The distances are normalized and from the top left corner of the render target
// x = distance of left vertical dividing line
// y = distance of top horizontal dividing line
// z = distance of right vertical dividing line
// w = distance of bottom horizontal dividing line
@location(3) @interpolate(flat) target_slices: vec4<f32>,

// The number of times the side or center texture slices should be repeated when mapping them to the border slices
// x = number of times to repeat along the horizontal axis for the side textures
// y = number of times to repeat along the vertical axis for the side textures
// z = number of times to repeat along the horizontal axis for the center texture
// w = number of times to repeat along the vertical axis for the center texture
@location(4) @interpolate(flat) repeat: vec4<f32>,

// normalized texture atlas rect coordinates
// x, y = top, left corner of the atlas rect
// z, w = bottom, right corner of the atlas rect
@location(5) @interpolate(flat) atlas_rect: vec4<f32>,
@builtin(position) position: vec4<f32>,
}

@vertex
fn vertex(
@location(0) vertex_position: vec3<f32>,
@location(1) vertex_uv: vec2<f32>,
@location(2) vertex_color: vec4<f32>,
@location(3) texture_slices: vec4<f32>,
@location(4) target_slices: vec4<f32>,
@location(5) repeat: vec4<f32>,
@location(6) atlas_rect: vec4<f32>,
) -> UiVertexOutput {
var out: UiVertexOutput;
out.uv = vertex_uv;
out.color = vertex_color;
out.position = view.clip_from_world * vec4<f32>(vertex_position, 1.0);
out.texture_slices = texture_slices;
out.target_slices = target_slices;
out.repeat = repeat;
out.atlas_rect = atlas_rect;
return out;
}

/// maps a point along the axis of the render target to slice coordinates
fn map_axis_with_repeat(
// normalized distance along the axis
p: f32,
// target min dividing point
il: f32,
// target max dividing point
ih: f32,
// slice min dividing point
tl: f32,
// slice max dividing point
th: f32,
// number of times to repeat the slice for sides and the center
r: f32,
) -> f32 {
if p < il {
// inside one of the two left (horizontal axis) or top (vertical axis) corners
return (p / il) * tl;
} else if ih < p {
// inside one of the two (horizontal axis) or top (vertical axis) corners
return th + ((p - ih) / (1 - ih)) * (1 - th);
} else {
// not inside a corner, repeat the texture
return tl + fract((r * (p - il)) / (ih - il)) * (th - tl);
}
}

fn map_uvs_to_slice(
uv: vec2<f32>,
target_slices: vec4<f32>,
texture_slices: vec4<f32>,
repeat: vec4<f32>,
) -> vec2<f32> {
var r: vec2<f32>;
if target_slices.x <= uv.x && uv.x <= target_slices.z && target_slices.y <= uv.y && uv.y <= target_slices.w {
// use the center repeat values if the uv coords are inside the center slice of the target
r = repeat.zw;
} else {
// use the side repeat values if the uv coords are outside the center slice
r = repeat.xy;
}

// map horizontal axis
let x = map_axis_with_repeat(uv.x, target_slices.x, target_slices.z, texture_slices.x, texture_slices.z, r.x);

// map vertical axis
let y = map_axis_with_repeat(uv.y, target_slices.y, target_slices.w, texture_slices.y, texture_slices.w, r.y);

return vec2(x, y);
}

@fragment
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
// map the target uvs to slice coords
let uv = map_uvs_to_slice(in.uv, in.target_slices, in.texture_slices, in.repeat);

// map the slice coords to texture coords
let atlas_uv = in.atlas_rect.xy + uv * (in.atlas_rect.zw - in.atlas_rect.xy);

return in.color * textureSample(sprite_texture, sprite_sampler, atlas_uv);
}
Loading

0 comments on commit 01a3b0e

Please sign in to comment.