mikke89 / RmlUi

RmlUi - The HTML/CSS User Interface library evolved
https://mikke89.github.io/RmlUiDoc/
MIT License
2.77k stars 304 forks source link

Advanced rendering effects - filters, masks, box-shadows, gradients, and shaders #594

Closed mikke89 closed 6 months ago

mikke89 commented 7 months ago

This one has been a long time in the making, first mentioned in #307, now the time has come for one of the biggest additions to the library. I hope you like the changes!

I am posting this as a pull request to gather more feedback. There's a lot here, feel free to give any feedback, whether a small comment or a fully detailed review. It would be great to see the effect sample especially tested on more machines, as I'm sure there will be issues popping up. I've tried to keep the commit history clean, which should be helpful for reviewing.

There has been some feedback regarding the new render interface in particular, please let me know what you think here, before we lock-in the changes. Some thoughts here: https://github.com/mikke89/RmlUi/issues/307#issuecomment-1837466674

This PR closes #249, #253, #307, and even addresses #1.

New features

New properties:

New decorators:

The new rendering interface include support for shaders, which enable the above decorators. Parsing is done in the library, but the backend renderer is the one implementing the actual shader code.

All of the filters and gradient decorators have full support for interpolation, that is, they can be animated. This is not yet implemented for box-shadow.

Decorators can now take an extra keyword <paint-area> which is one of border-box | padding-box | content-box, which indicates which area of the element the decorator should apply to. All built-in decorators are modifed to support this property.

Custom filters can be created by users by deriving from Filter and FilterInstancer, very much like how custom decorators are created.

Improved element clipping behavior. Handles more complicated cases, including nested transforms with hidden overflow, and clips to the curved edge of elements with border-radius. This requires clip mask support in the renderer.

New effect sample showcasing a lot of the new features.

Major overhaul of the render interface

The render interface has been simplified to ease implementation of basic rendering functionality, while extended to enable the new advanced rendering effects. The new effects are fully opt-in, and can be enabled iteratively to support the features that are most desired for your project.

Highlighted changes:

Backward compatible render interface adapter

The render interface changes will require updates for all users writing their own render interface implementation. To smooth the transition, there is a fully backward-compatible adapter for old render interfaces, see RenderInterfaceCompatibility.

  1. In your legacy RenderInterface implementation, derive from Rml::RenderInterfaceCompatibility instead of Rml::RenderInterface.
       #include <RmlUi/Core/RenderInterfaceCompatibility.h>
       class MyRenderInterface : public Rml::RenderInterfaceCompatibility { ... };
  2. Use the adapted interface when setting the RmlUi render interface.
       Rml::SetRenderInterface(my_render_interface.GetAdaptedInterface());

It can also be useful to take a closer look at the adapter before migrating your own renderer to the new interface, to see which changes are necessary. Naturally, this adapter won't support any of the new rendering features.

Render manager and resources

A new RenderManager is introduced to manage resources and other rendering state. Users don't normally have to interact with this, but for contributors, and for more advanced usages, such as custom decorators, this implies several changes.

The RenderManager and can be considered a wrapper around the render interface. All internal calls to the render interface should now go through this class.

Resources from the render interface are now wrapped as unique render resources, which are move-only types that automatically cleans up after themselves when they go out of scope. This considerably helps resource management. This also implies changes to many central rendering types.

See the following commit message for more details: a452f26951f9450d484496cccdfad9c94b3fd294.

Other changes

Utilities:

General improvements:

Fixes:

Visual tests:

Limitations

Filters will only render based on geometry that is visible on-screen. Thus, some filters may be cut-off. As an example, an element that is partly clipped with a drop-shadow may have its drop-shadow also clipped, even if it is fully visible. On the other hand, box shadows should always be rendered properly, as they are rendered off-screen and stored in a texture.

Breaking changes

Not yet implemented

Some features to be implemented, or being considered:

I consider none of these blocker, instead we can do them as follow-ups.

Screenshots

I'll close off with some eye candy:

Effects demonstration

Effect sample

0suddenly0 commented 7 months ago

It seems to me that there are problems with the new FileTextureDatabase, because after unloading all textures, only font textures are reloaded, this can also be seen in tests.

image After calling Rml::ReleaseTextures(), counters.release_texture == 4, because 3 generated textures are unloaded, and one loaded.

image But after passing Update+Render, only the number of generated textures increases, the number of loaded textures remains the same.

You can also simply release all the textures in any example, after which only the font textures will be loaded again.

mikke89 commented 7 months ago

Good catch! Thanks for reporting.

I made some changes, I believe this one should fix this issue: 011b3c17efeaff5a6bcda4547dd7fd4cb5590451 Extended some tests too so we'll catch this next time.

0suddenly0 commented 6 months ago

Due to the installation of scissors within the boundaries of the element, when drawing any filters, the child elements that are behind the object are cut off, despite the overflow properties.

The blue element is a child element of the red one. filter in chrome: image

the same filter in rmlui: image

mikke89 commented 6 months ago

I see, I'll look into that. Can you post a full document example?

0suddenly0 commented 6 months ago

I see, I'll look into that. Can you post a full document example?

<html>
    <head>
        <style>
            div { position: absolute; }
            div.parent {
                top: 50px;
                left: 50px;

                width: 40px;
                height: 40px;

                background-color: red;

                filter: blur(10px);
            }

            div.child {
                top: 100px;
                left: 100px;

                width: 100px;
                height: 100px;

                background-color: blue;
            }
        </style>
    </head>
    <body>
        <div class="parent">
            <div class="child"></div>
        </div>
    </body>
</html>
0suddenly0 commented 6 months ago

There are also some problems with applying opacity and filter properties to the backdrop-filter. If we use such code, we will see that RmlUi does not impose sepia on the background:

<html>
    <head>
        <style>
            div { position: absolute; }
            div.bg {
                top: 75px;
                left: 75px;

                width: 50px;
                height: 50px;

                background-color: green;
            }

            div.fg {
                width: 200px;
                height: 200px;

                backdrop-filter: blur(10px);
                filter: sepia(1);
            }
        </style>
    </head>
    <body>
        <div class="bg"></div>
        <div class="fg"></div>
    </body>
</html>

chrome

RmlUi

For Chrome, this is correct only if the div.fg there is no backdrop-filter.

mikke89 commented 6 months ago

@0suddenly0 I finally got around to looking into these ones.

For the first issue with a child and filter parent, this is a bit tricky for us. It has to do with how absolutely positioned elements are considered in terms of overflow. Specifically, a known difference from CSS is that we don't show scroll bars or clip such elements when placed outside their offset parent's visible region. There is some previous discussion on this topic, but in short, we want absolute positioning to be independent of layout, so that we don't have to compute an expensive layout step every time we move something around the screen.

This behavior extends to filtering, such that absolutely positioned elements are not considered a part the current element's visible region. I am considering a way to make this work without inducing a full re-layout step. In particular, we could keep track of any such absolutely positioned overflowing elements, and say that:

  1. Positioning such elements still don't affect layout and specifically don't make scrollbars appear.
  2. However, they will affect the visual clipping region of their ancestors, even if they don't affect visibility of scrollbars.

This could solve some existing cases where one would need to use clip: always. However, it requires some considerations and more work, we'll possibly need elements to keep track of their absolutely positioned descendants. For now it is considered outside the scope of this PR, we'll have to revisit this later. I made a visual test case for when we revisit this.


I've pushed a commit for the second case. There was indeed a limitation to combining backdrop-filter with filter and mask-image, so thank you for reporting this. I made some changes, including a new test case for several different combinations of these properties:

2024-03-24 - Filter, backdrop and mask comparison

I also identified some cases where the edges of backdrop-filter with blur would be dark, as seen above. There might still be some cases left to consider for these combinations. The resulting code is a bit clunky in my opinion, so I am consider making some render interface API changes around layers.

Also, please note that our opacity property works a bit differently from CSS, but filter: opacity() should generally work the same as in web browser.

wh1t3lord commented 6 months ago

@mikke89 can you add a such sample to official samples please? Or how to call a such sample? It might be useful for testing render backends since we would have many of them...

mikke89 commented 6 months ago

@wh1t3lord All of these examples are added to our visual tests. You can find them by launching the VisualTests target included with BUILD_TESTING=ON.

The jellyfish in the screenshot above was replaced with an existing image in the repository for the test though. I have a jsfiddle here for the browser equivalent for anyone interested.