-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Ergonomic z-order layers for 2D graphics (and UI?) #1275
Comments
Enum-based layersThe natural starting point is an This implies that we want a Working through the criteria:
Conclusions
|
Double-ended struct layersBuilding on the idea that most of the functionality that we want is covered by the Criteria:
Conclusions
|
HashMap-based layersThe idea is obvious: store a HashMap from Criteria:
Conclusions
|
This is discussed further in #1211, with a working implementation for UI only. |
That was tried in #1211 and the floating points precision issues already appeared for about 10 layers (with 100 to 150 entities). I don't think it's viable, unfortunately. |
What approach do you make use of now? |
Entities are part of stacking contexts. In the default case a context contains an entity and its children, but it can get arbitrarily complex using the |
May I ask how nested sublayers are laid out? Can they be nested many levels deep or only one level, what relations exist between children and parent except additional convenient separation, may a parent be a logical element of its children set or it should have a special treatment? Sorry for dumb questions, I'm not on good terms with UI, but they're important for me in implementing enum-based PoC. |
@Kolsky these are great questions! Let me think about the options:
As a heads up, I expect that relations (vaguely described in #1627 and #1527) will serve as an excellent tool for implementing this, although prototypes are very welcome to see if it's worth pursuing. In discussion with @BoxyUwU, the general approach would be:
For now, you'd be able to comfortably implement the same design using components that wrap an |
So I've made a procedural macro for enums, it's available at https://github.com/Kolsky/syn_derive_layers.
|
I think using Do we want to use the same z ordering system for sprites and UI? If so an implementation of the z-index property like in #1211 is a good candidate. |
@alice-i-cecile I just ran across this issue when dealing with an isometric tilemap rendering bug. I don't think the proposed solutions here address the issue of having multiple passes(different shaders). Currently in bevy 2D passes are treated as a group of render calls that are sorted by z values. There is no mechanism for sorting draw calls across passes(I'm not sure we should allow this as well because of slowdowns..). One solution is to rely on the depth buffer. That might have issues with transparency though.. With isometric rendering you'll want to render tiles from the bottom layer up and from the top down. |
You're totally right 🤔 I'd written this before the fancy rendering infrastructure was in place (or I knew anything about rendering), but this needs to be carefully considered. |
If using |
I wrote up a quick user-implementable workaround for this today: enum Layer {
Background(i8),
Foreground(i8),
}
fn update_z_coordinate_based_on_layer(query: Query<(&mut Transform, &Layer), Changed<Layer>){
for (mut transform, layer) {
transform.translation.z = match layer {
Layer::Background(order_in_layer) => -1. + order_in_layer as f32 / 1000.,
Layer::Foreground(order_in_layer) => 0. + order_in_layer as f32 / 1000.,
}
}
} The strategy is pretty simple: just slice up your z space. It still relies on a bit of global ordering, but it should be relatively efficient and the performance should be fine. |
I like this a lot! I think we will need something more sophisticated to handle runtime insertion of layers between two layers that are sequential. For example in pseudo-code: fn insert_layer_between() {
// layers that already exist
// let layer_a = Layer::Foreground(1);
// let layer_b = Layer::Foreground(2);
// I want to insert layer_c between layer_a and layer_b, how would I do that, especially if there were not just two layers but dozens
let layer_c = Layer::insert_after(layer_a);
} After the insertion of layer_c, it should have the value of Layer::Foreground(2), and layer_b would have the value of Layer::Foreground(3), or more properly expressed an internal value of n+1… or something. I hope this demonstrates the complex problem I’m thinking about adequately. |
I absolutely agree @colepoirier; you'll need to be able to reshuffle all of the coordinates to accommodate new layers if needed, and you'll want to space things out by default. I also think sublayers are also critical (at least one layer). These are really critical when working with similar flows in graphics programs IME. |
I think a sane default of 'subspace' would be... 10? Or perhaps a recursive subspace of that divides in two, and has a depth of.. 3? 4? 8? Idk, I wonder how we would determine optimal defaults experimentally, and how/if the defaults could be configured for layer-heavy apps like 2d CAD and 2d drawing? I also wonder what existing layering implementations do and how we can learn from them (i.e. html layers, photoshop/illustrator layer UX and internal implementation, or their FOSS alternatives). Are there other existing applications that use this kind of complex layering that you think would be good to look at to aid our design process? I also have the intuition that this effectively a data structure problem that has analogues in different domains, and therefore is perhaps a well-know problem that has an existing optimal design. Hopefully someone will provide us with the generic name of this data structure so we can look at the existing computer science solutions for this 🙏 |
It looks like the data structure we should use is something like skip-lists or tries. |
The obvious just occurred to me: some kind of binary tree. |
I came across a user-facing API design I really like here: /// Relative Z-Axis Reference to one Layer `Above` or `Below` another
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RelZ {
Above,
Below,
}
impl RelZ {
pub fn other(&self) -> Self {
match *self {
RelZ::Above => RelZ::Below,
RelZ::Below => RelZ::Above,
}
}
} Upon seeing this I immediately thought that this would in fact be the optimal Layer/z-index ordering design for our user facing API. What do you think? I'm not convinced that |
Is there not also a requirement that I don't think has been mentioned, that the z allocated to the layer should not change between frames unless the layers is moved (or removed)? If that's right, I don't think that the allocation of values is the issue - the question is just how to set up the interface so that it's easy to work with, which is probably as hard/easy as other problems like handling named phases. The one extra problem what to do in really offbeat scenarios when you're getting close to the smallest differences that f32 can represent, but doing an emergency reallocation of depths in that case is probably sufficient. |
The way i solved Pokémon-gen-3-style Y-sorting in 2D was the following: Decide which layers you have, for me it was Notice that theres a maximum of 8 (maybe even less due to visual debuggers / UI) now create a camera for each of them. Make sure to add a RenderLayer::layer(XXX_LAYER) Component for each camera. Now my floor tiles also get the RenderLayer::layer(GROUND_LAYER) component, units and sky objects accordingly. For Y-Sorting on my UNIT_LAYER i simply created a system that aligns the translation's z axis with the y axis. something like: |
Has there been any ideas how to solve this issues? Was hoping to use this for isometric 2d game. |
Not sure what relation it describes. There is neither equality (for total order) nor incomparability (for partial order). I think this will work only in the simplest cases. I'm not an expert, but after some research I believe there are two main techniques to draw a complex sprite scene:
Total order doesn't work because sometimes you need a third sprite, which will tell that it's behind this one and in front of that one, to order two directly incomparable sprites. |
Being a little cheeky here and plugging my extol_sprite_layer crate, which lets you specify the layer as a type that's convertible to an That being said, I'd like to see something land in Bevy itself for this. Rather than designing the one layer system to rule them all, I'd like to see one that handles one particular case well and see how that goes. I'm biased because of my use case, of course, but I think handling the 2d graphics sprite layer is simple: you don't need to arbitrarily subdivide layers or support runtime layer creation, so the simple enum-based approach works fine. |
Also, re @magras, I'm not sure what you mean about a partial order creating cycles. A partial order can't have cycles by definition. The sequence produced by a topological sort can be non-unique if there are incomparable elements, but it'll still satisfy the property that if a < b, then a appears before b in the sort. If your sprite layers do have cycles then I don't think there's anything sensible to do. |
@deifactor, yes, I was wrong. Didn't notice partial order requires transitivity too. Thank you for the correction. I wanted to say that it's possible to describe a cyclic relation like red > green > blue > red which will break topological sort. That will require some handling in the engine and probably will create questions from unqualified users like me. You could say that the situation is no different from the total order, but I believe that it's well known that violating transitivity for total order will break sorting algorithms. I'm not sure if topological sort and depth maps should be a part of the core, but I'm frustrated that what I imagined as a simple sprite game turned into tinkering with renderer, assets and manual sorting of sprites. |
Any updates to this? |
I'm thinking about this, and I think that might be good if we can set the Layer to be hidden or be show at anytime, simply by disabling the Layer somehow |
Entity-Backed LayersLayers are hard for the same reason that using 3d transforms for 2d games feels weird. My thesis is that we should introduce layers at the same time that we transition to a Here is my proposal: A layer is an entity with a component struct Layer {
depth: f32
}
The z-layout system walks down the layer tree and determines the actual z-depth of each entity:
A separate system can then go through to propagate Criteria
Yes. It's the ECS.
You use something like
You use something like
Yes, it is tree-structured.
Yes, the z-layout system will move everything for you automatically.
This one is kinda iffy. Layers are uniquely identified by their entity, and you can attach whatever components you want to identify them. Use enums, use |
Being pedantic but I think this needs better specification, "All entities" is way too broad imo. eg Would this be restricted to entities with a |
Fair point. I was envisioning the z-layout system would only operate on nodes with depth and layer/transform2d. We can use a similar thing to how transform works to ensure all ancestors of an entity with a depth also have a depth. |
I hope he can control the display order of all 'images', including' nodes', to replace the cumbersome 'Index'. And as a 'Component', it facilitates the modification of display order. (Machine Translation) |
What problem does this solve or what need does it fill?
Currently, we can control the relative position of 2D sprites by manually setting their z-value. This technically works, but requires setting and updating magic numbers scattered throughout the codebase.
When creating a game, you'll often want to specify things like "all in-game UI elements occur on top" or "units are drawn above terrain". Layers provide a sensible, comprehensible way to think about this in a single place.
What solution would you like?
To me, a good solution to this has the following properties:
I'm not exactly sure what such a solution would look like in practice; I plan to explore it in the comments below. Others are very welcome to comment with proposed solutions as well.
In addition, we need explicit, well-documented rules for what happens to sibling sprites that overlap and share a z-value.
What alternative(s) have you considered?
If a good-enough solution can be created without touching engine code, adding an example to the examples folder would likely be entirely sufficient to resolve this issue.
Additional context
This could be useful for customizing the ordering of various UI elements as well (see #254), depending on the exact pattern we use for constructing it. A builder pattern can obviate the need for this (especially if we have a single rooted tree), but 2D graphics can be used to create sprites in much more diverse fashions.
The text was updated successfully, but these errors were encountered: