Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI Materials #9506

Merged
merged 63 commits into from
Nov 3, 2023
Merged

Add UI Materials #9506

merged 63 commits into from
Nov 3, 2023

Conversation

MarkusTheOrt
Copy link
Contributor

@MarkusTheOrt MarkusTheOrt commented Aug 20, 2023

Objective

Solution

  • Create a UiMaterial trait for specifying a Shader Asset and Bind Group Layout/Data.
  • Create a pipeline for rendering these Materials inside the Ui layout/tree.
  • Create a MaterialNodeBundle for simple spawning.

Changelog

  • Created a UiMaterial trait for specifying a Shader asset and Bind Group.
  • Created a UiMaterialPipeline for rendering said Materials.
  • Added Example ui_material for example usage.
  • Created UiVertexOutput export as VertexData for shaders.
  • Created material_ui shader as default for both Vertex and Fragment shaders.

Adds a new `MaterialNodeBundle` that uses the
new `UiMaterial` to render Nodes with a certain
Shader and Parameters
@github-actions
Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile alice-i-cecile added this to the 0.12 milestone Aug 20, 2023
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Rendering Drawing game state to the screen A-UI Graphical user interfaces, styles, layouts, and widgets labels Aug 20, 2023
@IceSentry
Copy link
Contributor

IceSentry commented Aug 20, 2023

Could you add a simple example showing how to use this? I can figure it out fairly easily but I believe this feature deserves a standalone example.

@MarkusTheOrt
Copy link
Contributor Author

MarkusTheOrt commented Aug 21, 2023

I just added a simple example showcasing the feature. (its called ui_material)
I am thinking about ditching the use of UiPipeline inside the UiMaterialPipeline so that I can get rid of the default image bindings. Not every shader requires a texture, and if one is required it can be passed using the material.

Next up I do some renaming and documenting

@ickshonpe
Copy link
Contributor

ickshonpe commented Aug 21, 2023

Looks really good. Maybe the material module would be better split up, with UiMaterialBundle in node_bundles.rs for discoverability, and the rendering-specific code moved to render/material.rs or render_material/mod.rs etc.

@MarkusTheOrt
Copy link
Contributor Author

I did now remove the usage of UiPipeline, and copied the relevant code into the UiMaterialPipeline.
I also did split up the additions, with essentially only the UiMaterial trait and its accompanying UiMaterialKey being inside the material.rs and the rest being in render/material_pipeline.rs.

@@ -326,3 +326,49 @@ impl Default for ButtonBundle {
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah noticed a snag, ideally we want some sort of MaterialImageBundle as well:

/// A Ui Node that renders a shader
#[derive(Bundle, Debug)]
pub struct MaterialImageBundle<M: UiMaterial> {
    /// Describes the logical size of the node
    pub node: Node,
    /// Styles which control the layout (size and position) of the node and it's children
    /// In some cases these styles also affect how the node drawn/painted.
    pub style: Style,
    /// The Material of the Node.
    pub material: Handle<M>,
    /// Whether this node should block interaction with lower nodes
    pub focus_policy: FocusPolicy,
    /// The transform of the node
    ///
    /// This field is automatically managed by the UI layout system.
    /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
    pub transform: Transform,
    /// The global transform of the node
    ///
    /// This field is automatically managed by the UI layout system.
    /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component.
    pub global_transform: GlobalTransform,
    /// Describes the visibility properties of the node
    pub visibility: Visibility,
    /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
    pub computed_visibility: ComputedVisibility,
    /// Indicates the depth at which the node should appear in the UI
    pub z_index: ZIndex,
    /// The size of the image in pixels
    ///
    /// This field is set automatically
    pub image_size: UiImageSize,
    /// The calculated size based on the given image
    pub content_size: ContentSize,

}

impl<M: UiMaterial> Default for MaterialImageBundle<M> {
    fn default() -> Self {
        Self {
            node: Default::default(),
            style: Default::default(),
            material: Default::default(),
            focus_policy: Default::default(),
            transform: Default::default(),
            global_transform: Default::default(),
            visibility: Default::default(),
            computed_visibility: Default::default(),
            z_index: Default::default(),
            content_size: Default::default(),
        }
    }
}

The ContentSize component contains a measure func that is used to negotiate with the layout algorithm to find an optimal size for a node's content given that content's properties (like its aspect ratio and pixel density) and the space constraints of the UI layout.

ImageBundle has a matching system update_image_content_size_system that retrieves the size of the image from its texture asset and sets a measure func that preserves its aspect ratio. But we can't do that here because UiMaterial doesn't expose its texture asset publically (if it has one).

Copy link
Contributor

@ickshonpe ickshonpe Aug 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be fine not to worry about measurefuncs for this PR though, the implementation looks great, really nice and clean.

It's not essential as users can create their own system to add a measurefunc, which isn't very hard. We do need a custom measurefunc example, it would make sense to base one around a UiMaterial.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion, I agree with you there, I wasn't aware of ContentSize

I'd like to get this PR done and would be happy to take on the MaterialImageBundle as a followup once it is ready

Copy link
Contributor

@superdump superdump Oct 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn’t the image just be a member of the material type? @ickshonpe as well as other data needed - the image size.

crates/bevy_ui/src/node_bundles.rs Outdated Show resolved Hide resolved
crates/bevy_ui/src/node_bundles.rs Outdated Show resolved Hide resolved
examples/ui/ui_material.rs Outdated Show resolved Hide resolved
crates/bevy_ui/src/render/material_pipeline.rs Outdated Show resolved Hide resolved
crates/bevy_ui/src/render/material_pipeline.rs Outdated Show resolved Hide resolved
crates/bevy_ui/src/render/material_pipeline.rs Outdated Show resolved Hide resolved
crates/bevy_ui/src/render/material_pipeline.rs Outdated Show resolved Hide resolved
MarkusTheOrt and others added 3 commits August 22, 2023 14:46
due to this change the `UiMaterialPipeline` also has to use a custom
shader.
@MarkusTheOrt
Copy link
Contributor Author

I did Change some of the names like @ickshonpe suggested.

I am thinking that it would be a good addition to add the bevy_render::globals into the default shader bindings.
I believe it is a sensible default since time can be useful when doing anything animated within a shader.

@ickshonpe
Copy link
Contributor

ickshonpe commented Aug 22, 2023

Another problem this time with batching and draw ordering, I modified the ui_material example a bit to demonstrate:

use bevy::prelude::*;
use bevy_internal::{
    reflect::{TypePath, TypeUuid},
    render::render_resource::*,
};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(UiMaterialPlugin::<CustomUiMaterial>::default())
        .add_systems(Startup, setup)
        .add_systems(Update, update)
        .run();
}

fn update(time: Res<Time>, mut ui_materials: ResMut<Assets<CustomUiMaterial>>) {
    for (_, material) in ui_materials.iter_mut() {
        material.fill_amount = time.elapsed_seconds() % 1.0;
        let new_color = Color::WHITE;
        material.color = new_color.into();
    }
}

fn setup(mut commands: Commands, mut ui_materials: ResMut<Assets<CustomUiMaterial>>) {
    // Camera so we can see UI
    commands.spawn(Camera2dBundle::default());


    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Percent(100.0),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                ..default()
            },
            background_color: Color::BLACK.into(),
            ..default()
        })
        .with_children(|parent| {
            parent.spawn(NodeBundle {
                style: Style {
                    width: Val::Px(500.0),
                    height: Val::Px(500.0),
                    border: UiRect::all(Val::Px(10.)),
                    align_items: AlignItems::Center,
                    justify_content: JustifyContent::Center,
                    ..Default::default()
                },
                border_color: BorderColor(Color::WHITE),
                ..Default::default()
            }).with_children(|parent| {
                parent.spawn(NodeBundle {
                    style: Style {
                        position_type: PositionType::Absolute,
                        width: Val::Px(500.0),
                        height: Val::Px(25.0),
                        ..Default::default()
                    },
                    background_color: BackgroundColor(Color::GREEN),
                ..Default::default()
                });
                parent.spawn(MaterialNodeBundle {
                    style: Style {
                        position_type: PositionType::Absolute,
                        width: Val::Px(250.0),
                        height: Val::Px(250.0),
                        ..default()
                    },
                    material: ui_materials.add(CustomUiMaterial {
                        fill_amount: 0.0,
                        color: Color::WHITE.into(),
                    }),
                    ..default()
                });
                parent.spawn(NodeBundle {
                    style: Style {
                        position_type: PositionType::Absolute,
                        width: Val::Px(25.0),
                        height: Val::Px(500.0),
                        ..Default::default()
                    },
                    background_color: BackgroundColor(Color::RED),
                ..Default::default()
                });
            });
        });
}

#[derive(AsBindGroup, TypeUuid, TypePath, Debug, Clone)]
#[uuid = "7b5569c8-36d4-4c9d-acb7-d1754b385ab1"]
struct CustomUiMaterial {
    #[uniform(0)]
    fill_amount: f32,
    #[uniform(0)]
    color: Vec4,
}

impl UiMaterial for CustomUiMaterial {
    fn fragment_shader() -> ShaderRef {
        "shaders/circle_shader.wgsl".into()
    }
}

The circular section should be drawn in front of the green bar and behind the red bar, but it's drawn on top instead:

ui-material-z-order-example

I managed to force it to render correctly by disabling batching completely and merging #9062 which is a fix for some UI draw order bugs.

ui-material-z-order-example-2

The issue is that prepare_uinodes and prepare_uimaterial_nodes are working independently of each other.

prepare_ui_nodes puts the black background, the border rects, the green bar, and the red bar altogether into one batch.
prepare_ui_material puts the circular section ui material node into another batch.
Then the batches are sorted and then drawn in order. The batch with regular nodes first, and then the UI material batch on top.
prepare_ui_nodes has no idea that the red bar needs to be batched separately.

@MarkusTheOrt
Copy link
Contributor Author

The issue is that prepare_uinodes and prepare_uimaterial_nodes are working independently of each other.

I am not sure if there is anything that can be done here. Disabling all batching is sub-optimal to say the least.
Maybe there can be batching for each layer in the Ui?

@MarkusTheOrt
Copy link
Contributor Author

I’ll resolve these as soon as possible (in about one hour or so)

Copy link
Contributor

@JMS55 JMS55 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks generally fine, although I'm not familiar with any of the UI stuff. Some small doc nits.

crates/bevy_ui/src/render/ui_vertex_output.wgsl Outdated Show resolved Hide resolved
crates/bevy_ui/src/ui_material.rs Outdated Show resolved Hide resolved
crates/bevy_ui/src/render/ui_material_pipeline.rs Outdated Show resolved Hide resolved
@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Oct 28, 2023
@alice-i-cecile
Copy link
Member

I'm less sleep-deprived now and feeling comfortable re-reviewing this. I'm happy with the quality of code, the docs are great, and this is useful abstraction. Merging for 0.12 :)

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Nov 3, 2023
Merged via the queue into bevyengine:main with commit fd232ad Nov 3, 2023
25 checks passed
aevyrie added a commit to aevyrie/bevy that referenced this pull request Nov 4, 2023
Fix branding inconsistencies

don't Implement `Display` for `Val` (bevyengine#10345)

- Revert bevyengine#10296

- Avoid implementing `Display` without a justification
- `Display` implementation is a guarantee without a direct use, takes
additional time to compile and require work to maintain
- `Debug`, `Reflect` or `Serialize` should cover all needs

Combine visibility queries in check_visibility_system (bevyengine#10196)

Alternative to bevyengine#7310

Implemented the suggestion from
bevyengine#7310 (comment)

I am guessing that these were originally split as an optimization, but I
am not sure since I believe the original author of the code is the one
speculating about combining them up there.

I ran three benchmarks to compare main, this PR, and the approach from
([updated](https://github.com/rparrett/bevy/commits/rebased-parallel-check-visibility)
to the same commit on main).

This seems to perform slightly better than main in scenarios where most
entities have AABBs, and a bit worse when they don't (`many_lights`).
That seems to make sense to me.

Either way, the difference is ~-20 microseconds in the more common
scenarios or ~+100 microseconds in the less common scenario. I would
speculate that this might perform **very slightly** worse in
single-threaded scenarios.

Benches were run in release mode for 2000 frames while capturing a trace
with tracy.

| bench | commit | check_visibility_system mean μs |
| -- | -- | -- |
| many_cubes | main | 929.5 |
| many_cubes | this | 914.0 |
| many_cubes | 7310 | 1003.5 |
| | |
| many_foxes | main | 191.6 |
| many_foxes | this | 173.2 |
| many_foxes | 7310 | 167.9 |
| | |
| many_lights | main | 619.3 |
| many_lights | this | 703.7 |
| many_lights | 7310 | 842.5 |

Technically this behaves slightly differently -- prior to this PR, view
visibility was determined even for entities without `GlobalTransform`. I
don't think this has any practical impact though.

IMO, I don't think we need to do this. But I opened a PR because it
seemed like the handiest way to share the code / benchmarks.

I have done some rudimentary testing with the examples above, but I can
do some screenshot diffing if it seems like we want to do this.

Make VERTEX_COLORS usable in prepass shader, if available (bevyengine#10341)

I was working with forward rendering prepass fragment shaders and ran
into an issue of not being able to access vertex colors in the prepass.
I was able to access vertex colors in regular fragment shaders as well
as in deferred shaders.

It seems like this `if` was nested unintentionally as moving it outside
of the `deferred` block works.

---

Enable vertex colors in forward rendering prepass fragment shaders

allow DeferredPrepass to work without other prepass markers (bevyengine#10223)

fix crash / misbehaviour when `DeferredPrepass` is used without
`DepthPrepass`.

- Deferred lighting requires the depth prepass texture to be present, so
that the depth texture is available for binding. without it the deferred
lighting pass will use 0 for depth of all meshes.
- When `DeferredPrepass` is used without other prepass markers, and with
any materials that use `OpaqueRenderMode::Forward`, those entities will
try to queue to the `Opaque3dPrepass` render phase, which doesn't exist,
causing a crash.

- check if the prepass phases exist before queueing
- generate prepass textures if `Opaque3dDeferred` is present
- add a note to the DeferredPrepass marker to note that DepthPrepass is
also required by the default deferred lighting pass
- also changed some `With<T>.is_some()`s to `Has<T>`s

UI batching Fix (bevyengine#9610)

Reimplement bevyengine#8793 on top of the recent rendering changes.

The batch creation logic is quite convoluted, but I tested it on enough
examples to convince myself that it works.

The initial value of `batch_image_handle` is changed from
`HandleId::Id(Uuid::nil(), u64::MAX)` to `DEFAULT_IMAGE_HANDLE.id()`,
which allowed me to make the if-block simpler I think.

The default image from `DEFAULT_IMAGE_HANDLE` is always inserted into
`UiImageBindGroups` even if it's not used. I tried to add a check so
that it would be only inserted when there is only one batch using the
default image but this crashed.

---

`prepare_uinodes`
* Changed the initial value of `batch_image_handle` to
`DEFAULT_IMAGE_HANDLE.id()`.
* The default image is added to the UI image bind groups before
assembling the batches.
* A new `UiBatch` isn't created when the next `ExtractedUiNode`s image
is set to `DEFAULT_IMAGE_HANDLE` (unless it is the first item in the UI
phase items list).

Increase default normal bias to avoid common artifacts (bevyengine#10346)

Bevy's default bias values for directional and spot lights currently
cause significant artifacts. We should fix that so shadows look good by
default!

This is a less controversial/invasive alternative to bevyengine#10188, which might
enable us to keep the default bias value low, but also has its own sets
of concerns and caveats that make it a risky choice for Bevy 0.12.

Bump the default normal bias from `0.6` to `1.8`. There is precedent for
values in this general area as Godot has a default normal bias of `2.0`.

![image](https://github.com/superdump/bevy/assets/2694663/a5828011-33fc-4427-90ed-f093d7389053)

![image](https://github.com/bevyengine/bevy/assets/2694663/0f2b16b0-c116-41ab-9886-1ace9e00efd6)

The default `shadow_normal_bias` value for `DirectionalLight` and
`SpotLight` has changed to accommodate artifacts introduced with the new
shadow PCF changes. It is unlikely (especially given the new PCF shadow
behaviors with these values), but you might need to manually tweak this
value if your scene requires a lower bias and it relied on the previous
default value.

Make `DirectionalLight` `Cascades` computation generic over `CameraProjection` (bevyengine#9226)

Fixes bevyengine#9077 (see this issue for
motivations)

Implement 1 and 2 of the "How to fix it" section of
bevyengine#9077

`update_directional_light_cascades` is split into
`clear_directional_light_cascades` and a generic
`build_directional_light_cascades`, to clear once and potentially insert
many times.

---

`DirectionalLight`'s computation is now generic over `CameraProjection`
and can work with custom camera projections.

If you have a component `MyCustomProjection` that implements
`CameraProjection`:
- You need to implement a new required associated method,
`get_frustum_corners`, returning an array of the corners of a subset of
the frustum with given `z_near` and `z_far`, in local camera space.
- You can now add the
`build_directional_light_cascades::<MyCustomProjection>` system in
`SimulationLightSystems::UpdateDirectionalLightCascades` after
`clear_directional_light_cascades` for your projection to work with
directional lights.

---------

Co-authored-by: Carter Anderson <[email protected]>

Update default `ClearColor` to better match Bevy's branding (bevyengine#10339)

- Changes the default clear color to match the code block color on
Bevy's website.

- Changed the clear color, updated text in examples to ensure adequate
contrast. Inconsistent usage of white text color set to use the default
color instead, which is already white.
- Additionally, updated the `3d_scene` example to make it look a bit
better, and use bevy's branding colors.

![image](https://github.com/bevyengine/bevy/assets/2632925/540a22c0-826c-4c33-89aa-34905e3e313a)

Corrected incorrect doc comment on read_asset_bytes (bevyengine#10352)

Fixes bevyengine#10302

- Removed the incorrect comment.

Allow AccessKit to react to WindowEvents before they reach the engine (bevyengine#10356)

- Adopt bevyengine#10239 to get it in time for the release
- Fix accessibility on macOS and linux

- call `on_event` from AcccessKit adapter on winit events

---------

Co-authored-by: Nolan Darilek <[email protected]>
Co-authored-by: Alice Cecile <[email protected]>
Co-authored-by: Alice Cecile <[email protected]>

Fix typo in window.rs (bevyengine#10358)

Fixes a small typo in `bevy_window/src/window.rs`

Change `Should be used instead 'scale_factor' when set.` to `Should be
used instead of 'scale_factor' when set.`

Add UI Materials (bevyengine#9506)

- Add Ui Materials so that UI can render more complex and animated
widgets.
- Fixes bevyengine#5607

- Create a UiMaterial trait for specifying a Shader Asset and Bind Group
Layout/Data.
- Create a pipeline for rendering these Materials inside the Ui
layout/tree.
- Create a MaterialNodeBundle for simple spawning.

- Created a `UiMaterial` trait for specifying a Shader asset and Bind
Group.
- Created a `UiMaterialPipeline` for rendering said Materials.
- Added Example [`ui_material`
](https://github.com/MarkusTheOrt/bevy/blob/ui_material/examples/ui/ui_material.rs)
for example usage.
- Created
[`UiVertexOutput`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_vertex_output.wgsl)
export as VertexData for shaders.
- Created
[`material_ui`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_material.wgsl)
shader as default for both Vertex and Fragment shaders.

---------

Co-authored-by: ickshonpe <[email protected]>
Co-authored-by: François <[email protected]>

support file operations in single threaded context (bevyengine#10312)

- Fixes bevyengine#10209
- Assets should work in single threaded

- In single threaded mode, don't use `async_fs` but fallback on
`std::fs` with a thin layer to mimic the async API
- file `file_asset.rs` is the async imps from `mod.rs`
- file `sync_file_asset.rs` is the same with `async_fs` APIs replaced by
`std::fs`
- which module is used depends on the `multi-threaded` feature

---------

Co-authored-by: Carter Anderson <[email protected]>

Fix gizmo crash when prepass enabled (bevyengine#10360)

- Fix gizmo crash when prepass enabled

- Add the prepass to the view key

Fixes: bevyengine#10347
ameknite pushed a commit to ameknite/bevy that referenced this pull request Nov 6, 2023
# Objective

- Add Ui Materials so that UI can render more complex and animated
widgets.
- Fixes bevyengine#5607 

## Solution
- Create a UiMaterial trait for specifying a Shader Asset and Bind Group
Layout/Data.
- Create a pipeline for rendering these Materials inside the Ui
layout/tree.
- Create a MaterialNodeBundle for simple spawning.

## Changelog

- Created a `UiMaterial` trait for specifying a Shader asset and Bind
Group.
- Created a `UiMaterialPipeline` for rendering said Materials.
- Added Example [`ui_material`
](https://github.com/MarkusTheOrt/bevy/blob/ui_material/examples/ui/ui_material.rs)
for example usage.
- Created
[`UiVertexOutput`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_vertex_output.wgsl)
export as VertexData for shaders.
- Created
[`material_ui`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_material.wgsl)
shader as default for both Vertex and Fragment shaders.

---------

Co-authored-by: ickshonpe <[email protected]>
Co-authored-by: François <[email protected]>
@MarkusTheOrt MarkusTheOrt deleted the ui_material branch November 16, 2023 13:50
rdrpenguin04 pushed a commit to rdrpenguin04/bevy that referenced this pull request Jan 9, 2024
# Objective

- Add Ui Materials so that UI can render more complex and animated
widgets.
- Fixes bevyengine#5607 

## Solution
- Create a UiMaterial trait for specifying a Shader Asset and Bind Group
Layout/Data.
- Create a pipeline for rendering these Materials inside the Ui
layout/tree.
- Create a MaterialNodeBundle for simple spawning.

## Changelog

- Created a `UiMaterial` trait for specifying a Shader asset and Bind
Group.
- Created a `UiMaterialPipeline` for rendering said Materials.
- Added Example [`ui_material`
](https://github.com/MarkusTheOrt/bevy/blob/ui_material/examples/ui/ui_material.rs)
for example usage.
- Created
[`UiVertexOutput`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_vertex_output.wgsl)
export as VertexData for shaders.
- Created
[`material_ui`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_material.wgsl)
shader as default for both Vertex and Fragment shaders.

---------

Co-authored-by: ickshonpe <[email protected]>
Co-authored-by: François <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for applying a shader to a UI Image
9 participants