Open alice-i-cecile opened 3 years ago
The natural starting point is an enum
, as we have a fixed number of cases to handle. If we derive Ord
, we get a nice automatic ordering that follows the order laid out in the code. Add in a nice function to convert that ordering to a z coordinate value and I think we have a basic solution.
This implies that we want a Layers
trait, that users can derive for their own enum.
Working through the criteria:
get_z
method to the Layers
trait, or have a From
or Into
impl to handle the conversion.layer_above
, layer_below
, top_layer
and bottom_layer
method. We could use the DoubleEndedIter
trait for this, which would get us all that functionality and more.Conclusions
Building on the idea that most of the functionality that we want is covered by the DoubleEndedIter
trait, we could instead use a Layers
struct that uses something like VecDeque
for its internal storage.
Criteria:
Layers
struct with global visibility (probably as a resource?), and then manually construct the list with string literals.VecDeque
, but that's not too bad since performance concerns are completely negligible.Conclusions
2dLayers
and UiLayers
structs though.The idea is obvious: store a HashMap from Layer
to f32
(the z value), then look it up when you want to use it.
Criteria:
Conclusions
This is discussed further in #1211, with a working implementation for UI only.
I made a proof of concept that involved automatic subdivision of Z-space evenly among its children (and their children, and so on).
The API was a Layer
component to which you gave it a specific "thickness" (defined in terms of its "front" and "back" Z coordinates in world space), and a "depth" (its order relative to its siblings), and used the Parent
/Children
relationships to subdivide that thickness among each of its children, and the order defined by the depth for the children, to update the entity's GlobalTransform
component.
Something like:
struct Layer {
front: f32, // this is only relevant to the root layer
back: f32, // this is only relevant to the root layer
depth: f32, // this is only relevant to non-root layers
}
#[derive(Bundle)]
struct LayerBundle {
transform: Transform,
global_transform: GlobalTransform,
layer: Layer,
}
An illustration of this (before I tried implementing it, so not exactly what I made):
Cons are that it wasn't particularly ergonomic (I might have avoided using Parent
/Child
relationships and instead defined my own relationships), doesn't easily address each of your 6 criteria (but could probably be adapted to), and you'll eventually run into floating point precision issues with enough nesting of layers (maybe around 20 levels deep?), plus I wasn't sure of the performance.
However, I'm wondering now whether this can simply be data passed to and processed by the shader instead, i.e. just provide the metadata and let the GPU paint the layers correctly. I think this is feasible, and would be more performant than the other solutions, and may inform a possible API.
I made a proof of concept that involved automatic subdivision of Z-space evenly among its children (and their children, and so on).
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.
I made a proof of concept that involved automatic subdivision of Z-space evenly among its children (and their children, and so on).
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 ZIndex::Auto
property. Stacking contexts are cached, and rebuild/sorted when any entity that is part of it changes. When any context is changed, the z transform of all UI entities is reset, each at a constant interval over the previous. I could not find a simple way to make incremental updates to transforms.
What solution would you like? ...
- Support for nested sublayers
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:
Layer
marker component and a nice LayerLabel
field (see e.g. SystemLabel
for how this should be done).Sprite
component also has an InLayer
relation that points to a specific layer.RootLayer
marker component, marking the root of the layer tree.Layer
entity such as Transparency
to set properties of all entities associated with that layer, and extend this behavior with your own systems.For now, you'd be able to comfortably implement the same design using components that wrap an Entity
(see Bevy's parent-child code for an example), rather than the yet-to-be-written Relation
type, and easily migrate once Relations land.
So I've made a procedural macro for enums, it's available at https://github.com/Kolsky/syn_derive_layers.
You can nest it as much as you need to by deriving Layers
, which can be applied to enums with unit (has the name only) or unnamed 1-tuple variants containing other Layers
enum, and the trait itself is unsafe to implement manually. Also there is a Root
marker trait, it can only be applied at the top level. The first enum variant, if any, can only be the unit one, if enum isn't Root
. With that saying:
Layers
automatically gets fn
s to_num
and try_from_num
: they assign ordinal number based on field order, as you would expect from generalized numeral system. Note that, however, A::C(C::E).to_num()
from enum A { B, C(C), D }
isn't equal to C::E.to_num()
from enum C { E, F }
, as there is no assumption whether C
is contained in A
or any other enum.T
isn't marked as Root
, its first variant is a parent layer which can be constructed with T::try_from_num(0).unwrap()
. T: !
is exception, though it cannot be constructed anyway.Layers
fn
s are essentially pure and the size is constant, array mapping can be easily built if you would require Copy
for it.get_z
is fairly trivial with f32::from_bits
. Compile error messages may be a bit cryptic at times, but I've tried to make them clear enough. The rest is also trivial, as @alice-i-cecile have said already. Hope this helps.
I think using enum
s is problematic since they are static, we should probably support a dynamic number of layers. I would even argue for a total ordering between every Sprite entity to ensure there is no z fighting.
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 InLayer
relations, I believe we could store the z-order of the layers or the effective Ord
of the InLayer
relations in a struct LayerOrder(Vec<Entity>)
. This allows for easy insertion between layers and reordering relative to other layers. The solution for how to do this in ‘reality‘ i.e. the transform is probably set for each ‘tier’ of sublayers some maximum number of layers that can be allocated between that and the next layer in that same tier, and error if this maximum is exceeded. This seems like a statistical distribution binning problem so someone with a stats background or able to think this through better than me could probably come up with a solution that would work in 99.999% of cases. I believe this solution would also take into account the float precision issue bjorn encountered. Indeed, we probably need someone with a numerical analysis background to give insight into how to solve this problem because floating point and IEEE754.
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 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 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 :pray:
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 pray
It looks like the data structure we should use is something like skip-lists or tries.
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 other
is the optimal method name; my strong positive sentiment here is focused on the simple Above
|Below
relative ordering API.
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 GROUND_LAYER = 0 UNIT_LAYER = 1 SKY_LAYER = 2 UI_LAYER = 3
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. Adjust the camera's priority to be the same as your XXX_LAYER so that the images stack upon another. Make sure that your non-0 priority cameras have no ClearColorConfig, else you'll only see the highest-priority 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: Query<&mut Transform, (Changed<Transform>, With<YSorted>)>
. When querying them, set your entity's
transform.translation.z = transform.translation.y
.
Has there been any ideas how to solve this issues? Was hoping to use this for isometric 2d game.
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
other
is the optimal method name; my strong positive sentiment here is focused on the simpleAbove
|Below
relative ordering API.
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 f32
and also optionally does y-sorting.
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 like, hide(layer) would hide all the sprites that are in the given layer for example.
Layers 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 Transform2d
(see https://github.com/bevyengine/bevy/pull/8268). This opens up a big class of possibilities relating to how we even represent the location of entities in the first place.
Here is my proposal: A layer is an entity with a component
struct Layer {
depth: f32
}
depth
indicates the z-distance between this layer and the one on top of it. Layers can have other layers as children, or entities with a Transform2d
component.
The z-layout system walks down the layer tree and determines the actual z-depth of each entity:
Depth(f32)
component that gets updated (much like Transform/GlobalTransform
). layer.depth
.total_depth := self.depth + children.map(|c| c.total_depth()).sum()
.A separate system can then go through to propagate Transform2d
and expose Depth
in the GlobalTransform
. If more than one root layer is detected we can emit a warning.
A single, central source of truth which controls the relative positions of different varieties of sprites.
Yes. It's the ECS.
Easy initialization of freshly created sprites on a specific layer.
You use something like layer.push_children(entity)
Ability to move sprites between layers. You should be able to move sprites to a specific layer, up / down one layer, and to the top or bottom-most layer.
You use something like entity.set_parent(layer)
Support for nested sublayers.
Yes, it is tree-structured.
Changing the position of one layer doesn't require changes to any other layer.
Yes, the z-layout system will move everything for you automatically.
Layer names must be unique.
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 Name
. Its flexible.
All entities have a
Depth(f32)
component that gets updated (much likeTransform/GlobalTransform
).
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 Transform2d
, or both a Transform2d
and Visibility
etc.
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.
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.