ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
61.34k stars 10.33k forks source link

Window shadows #1329

Open wajsic opened 7 years ago

wajsic commented 7 years ago

Does anyone know what would be the best approach to render drop shadow behind the windows?

ocornut commented 7 years ago

It's not supported at the moment but here's a few ideas.

The most simple / basic approach would be to draw a transparent rounded rectangle under the window shape. You can hardcore this in Begin() fairly easily. But without a gradient within the shadow it won't look very good.

Adding a gradient would look better but requires more work setting up the vertices.

There's also the question that you may not want the shadow to actually draw behind the window because that is a waste of fill-rate, and solving this (especially as anti-aliased rounded borders are involved) makes the geometry more complex to create.

Pagghiu commented 7 years ago

I have been playing exactly with shadows lately ๐Ÿ˜ƒ I am focusing on fast and easy shadows for non-rounded rectangles (for now). I am trying to avoid using shaders or other fancy techniques for ease of integration into existing imgui based projects.

A few approaches that I have been trying: 1) Generating a texture 2) Generating vertices adaptively where the shadow signal is changing a lot, so mainly at the corners 3) Generating a regular grid of triangles at the corners and color their vertices accordingly

You can see all of them here:

imguishadows

Approach 1) is great but you need to handle textures and generate a new one every time you need a different combination of sigma (shadow kernel) and rectangle size. And probably you will need to merge them inside the main font texture to avoid too many draw calls. You're also wasting some fill rate as pointed out by Omar. You can get really nice looking shadow with very small texture (even 32x32 is great). You could potentially generate only a single corner and re-use it everywhere playing with UV coordinates to even lower down the memory usage. You could also "adapt" a generated texture for a given sigma to rectangles with different width and height but this requires some work in generating geometry and uv coordinates accordingly.

Approach 2) is probably the best so far, for a small shadow you can do very good approximation with about ~23 triangles for each rectangle with shadow. It's also very easy to implement and you have zero fill rate waste. For a larger sigma you need more triangles, potentially even double them but should be ok with most triangle budgets unless you plan to shadow everything in your UI. This is great and makes it easy to also support rectangle with rounded corners.

Approach 3) looks great but wastes too many triangles, approach 2) can do the same with a lot less.

If you need a formula to generate the correct shadow for a rounded rectangle, you could take a look at http://madebyevan.com/shaders/fast-rounded-rectangle-shadows/

Beware, there's a typo in the formulas referenced in this article! I have been wasting at least 1 hour trying to figure out what was wrong with my code before figuring this out ;)

// License: CC0 (http://creativecommons.org/publicdomain/zero/1.0/)

// This approximates the error function, needed for the gaussian integral
vec4 erf(vec4 x) {
  vec4 s = sign(x), a = abs(x);
  x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
  x *= x;
  return s - s / (x * x);
}

// Return the mask for the shadow of a box from lower to upper
float boxShadow(vec2 lower, vec2 upper, vec2 point, float sigma) {
  vec4 query = vec4(point - lower, upper - point);
  vec4 integral = 0.5 + 0.5 * erf(query * (sqrt(0.5) / sigma));
  return (integral.z - integral.x) * (integral.w - integral.y);
}

The line

float boxShadow(vec2 lower, vec2 upper, vec2 point, float sigma) {
  vec4 query = vec4(point - lower, upper - point);
//...
}

should really be

float boxShadow(vec2 lower, vec2 upper, vec2 point, float sigma) {
  vec4 query = vec4(point - lower, point - upper);
//...
}
wajsic commented 7 years ago

Thank you for taking your time for such an exhaustive explanation. I had in mind a lot of ideas but all of them included shaders which I'm trying to avoid for the reasons you said. This post pointed me into the right direction.

ocornut commented 7 years ago

Thanks @Pagghiu for looking into this, amazing stuff. 2) is definitively the way to go here. If we allow for arbitrary 2d offsets for the shadow then picking vertices without overlapping the parent shape is a little more work - if you come up with a helper function to handle it I'm happy to add that to the Styling options (my own drop-shadow git stash got nowhere as far as where yours is). If it's simpler to just support a zero ShadowOffset and fully outward spread it's already a step forward to have it.

I see you have a [ ] Linear Spread checkbox. Wouldn't a simple distance + power curve give good enough results compared to that function you linked above?

Pagghiu commented 7 years ago

Thanks Omar, sure, if I will try to find time to cleanup it a little bit and share.

The linear checkbox activates a linear approximation of the shadow falloff, and it's indistinguishable from the "correct" one for small sigmas. For bigger sigma, the linear approximation is not good, the results are clearly banded and clamped incorrectly. The box shadow formula is a power formula, as it's the Taylor expansion of the erf error function (https://en.wikipedia.org/wiki/Error_function). One could drop a few terms to save some multiplications of course or approximate it in many ways, even with bezier curves or splines (example http://stereopsis.com/shadowrect/), but I don't think it will be a big impact for a few vertices. I have not been focusing on fine optimisation yet because I have been trying to figure out the best approach with constraints I have been giving myself. ShadowOffset is easy to support, you just offset the sampling coordinate to feed into the boxShadow function. To avoid wasting triangles one should generate also triangles/vertices asymmetrically to be more dense in the direction of the shadowOffset and yes, that would be a little more complex to handle but still possible.

Pagghiu commented 7 years ago

I have not been able to spend additional time cleaning and optimising the shadow mesh generation, or adding other features like generation for rounded boxes but as I've been promising, here is the code.

There's a self contained sample in the opengl2_example folder https://github.com/Pagghiu/imgui/tree/2017-09-box-shadow

screen shot 2017-09-27 at 01 01 00 screen shot 2017-09-27 at 01 01 03

Hope this is useful to someone!

ocornut commented 6 years ago

Toying around with styling and tried your shadows.. they look great with the a right settings and white theme.

20180815_gradient_shadows2

Pagghiu commented 6 years ago

Beautiful shot, it looks quite similar to native macOS windows shadowing.

I hope this makes its way inside the master someday! ๐Ÿ˜„

ocornut commented 6 years ago

It will definitively, this is still an early draft and I would like to focus on finishing Docking/Viewport before moving to new pastures, then steer the internals to make those changes possible (refactor the Render functions, etc.).

rg-net commented 5 years ago

+1 for shadows in master!

ocornut commented 4 years ago

Pushed a branch with work by @ShironekoBen on a different approach for shadows. When experimenting we found earlier approach had too many edge cases (thin or small shapes) and the thing was redesigned around baking data in atlas texture:

https://github.com/ocornut/imgui/commits/features/shadows

(A) First commit d2676ada5556d0c2ffea42d90c5dbf4097d11b07 has most of that shadow code as-is. This time also support shadows behind rounded windows (which required an unusually large and complex amount of polygon clipping code... kudos to Ben for coping with that, kind of made me want to obsolete rounded windows...).

(B) Here's the catch: baking in texture means changing some shadow settings during runtime requires an update/re-upload of the texture. That alone can get very hairy to support properly in back-ends. Third commit bb8a3cb10f0ffbae2242198e62e2f8956199cb5c in the branch has to some code doing just that. This code when it reaches maturity will be useful as a building block for dynamic font atlas. Current code I would say is experimental and doesn't constitute how the final back-end API should look like.

[edit] If we limit the amount of real-time tweaking or limit the scope of (B) or leave it to the app to reload, we can commit most of (A) ahead of time. Doing the full version of (B) will allow for dynami tweaking of shadow settings and be the building block for other features.

Shots

Previous "hacked style" with those shadows (replicating screenshots above)

image

With regular "Light" style as-is from master (same shadows, just a less sexy style)

image

There's probably more work to provide here. The irony is that this may over time become somehow less useful for apps transitioning to use more of the platform/multi-viewport feature. However one of the aim for this is to also provide a low-level ImDrawList API which can be also used for custom widgets.

camplowell commented 4 years ago

Noticed the window shadows feature branch is gone (https://github.com/ocornut/imgui/commits/features/shadows is a dead link now), and the changes have not been pulled into master. Any news on this feature? Has it been dropped?

ocornut commented 4 years ago

@camplowell We've been working on this, I only removed it from public repo to avoid pushing to two locations as I forgot I mentioned it here. Pushed again now.

ocornut commented 4 years ago

Note that Demo>Examples>Custom Rendering demonstrates some of it including new primitives:

image

We're still working on the API, but the ImDrawShadowFlags_CutOutShapeBackground shape allows cutting out a hole inside the shadows to avoid filling pixels twice. See the left-most rounded rectangle showing background color behind: the shadows triangles are clipped.

image

Pagghiu commented 4 years ago

that's seriously beautiful ๐Ÿ˜ƒ

frink commented 4 years ago

Makes you want the docking/viewport thing to be done already!!!

codecat commented 3 years ago

I haven't tested this branch yet or looked at the API for it, but would this also allow adding shadows to text rendered via eg. drawlists?

ShironekoBen commented 3 years ago

Hm, in it's current state, only if the text was being drawn with convex primitives as opposed to the font texture... and even then I wouldn't be surprised if trying to deal with text brought out all the worst kinds of edge-cases! For texture font shadows the best approach is probably (in an ideal world where everything was working and finished and beautiful!) the SDF stuff, or for a simpler take on it a static version of that with a pre-generated expanded/blurred font texture, I imagine.

bvgastel commented 3 years ago

I have made a merge request for text and window shadows using SDF, see MR 4056. In that MR rounded rectangle shadows (such as for windows) are also calculated with SDF and same shaders as used for text glyphs. Another upside from this is that there are no artifacts (as can be the case with some pre-generated shadow solutions).

AlexvZyl commented 2 years ago

This would really take dear imgui to the next level!

ocornut commented 2 years ago

Itโ€™s not as impactful as I hoped for three reasons:

frink commented 2 years ago

I'd agree. If we allow shadows on other stuff like headers buttons that becomes more interesting but windows only makes sense if we are in control of drawing a large portion of the screen or if we're rending over a game which is still the prime use-case.

In docking they can be nice if you do like tiling window managers do and force spacing between window tiles. However, that's not currently how docking works as far as I know and I doubt anybody wants to redo the docking component one more time for something like this.

flarive commented 2 years ago

Shadows in imGUI are very cool ! I have already made a neumorphism theme for Avalonia UI framework (.net C#) and i would like to try to make the same kind of Neumorphism theme for imGUI. https://github.com/flarive/Neumorphism.Avalonia

@ocornut, do you think it would be a good idea ? I know imGUI was not intended for that of the begining but with imGUI shadows it seems to be possible to create a very nice Neumorphism theme.

I have just 2 problems for the moment :

2022-10-10 08_53_26-Dear ImGui DirectX9 Example

Thanks a lot !

frink commented 2 years ago

I haven't looked but I'm guessing that the shadow geometry is not taking rounding into account.

Ena-Shepherd commented 1 year ago

Shots

Previous "hacked style" with those shadows (replicating screenshots above)

image

With regular "Light" style as-is from master (same shadows, just a less sexy style)

image

There's probably more work to provide here. The irony is that this may over time become somehow less useful for apps transitioning to use more of the platform/multi-viewport feature. However one of the aim for this is to also provide a low-level ImDrawList API which can be also used for custom widgets.

Tried shadow branch and it's working amazingly well on my window components ! I didn't find how you rendered shadows inside your elements, though, but it seems to be a custom texture rendering you're doing, right (with ImDraw) ? (I'm a newbie in code). Is there a way to replicate this "hacked style" ? It would be nice to have shadows per element features on shadow branch by default. And also what @flarive commented https://github.com/ocornut/imgui/issues/1329#issuecomment-1272859772

Thank you !

kpcftsz commented 1 year ago

If anybody is looking for a quick and dirty way of doing this with minimal set up you can use this 9 slice (technically 8 slice) method.

Preview:

image

From my gist (which includes the texture I'm using):

/*
 * This function assumes the existence of an active Dear ImGui window
 */
void RenderDropShadow(ImTextureID tex_id, float size, ImU8 opacity)
{
    ImVec2 p = ImGui::GetWindowPos();
    ImVec2 s = ImGui::GetWindowSize();
    ImVec2 m = {p.x + s.x, p.y + s.y};
    float uv0 = 0.0f;      // left/top region
    float uv1 = 0.333333f; // leftward/upper region
    float uv2 = 0.666666f; // rightward/lower region
    float uv3 = 1.0f;      // right/bottom region
    ImU32 col = (opacity << 24) | 0xFFFFFF;
    ImDrawList* dl = ImGui::GetWindowDrawList();
    dl->PushClipRectFullScreen();
    dl->AddImage(tex_id, {p.x - size, p.y - size}, {p.x,        p.y       }, {uv0, uv0}, {uv1, uv1}, col);
    dl->AddImage(tex_id, {p.x,        p.y - size}, {m.x,        p.y       }, {uv1, uv0}, {uv2, uv1}, col);
    dl->AddImage(tex_id, {m.x,        p.y - size}, {m.x + size, p.y       }, {uv2, uv0}, {uv3, uv1}, col);
    dl->AddImage(tex_id, {p.x - size, p.y       }, {p.x,        m.y       }, {uv0, uv1}, {uv1, uv2}, col);
    dl->AddImage(tex_id, {m.x,        p.y       }, {m.x + size, m.y       }, {uv2, uv1}, {uv3, uv2}, col);
    dl->AddImage(tex_id, {p.x - size, m.y       }, {p.x,        m.y + size}, {uv0, uv2}, {uv1, uv3}, col);
    dl->AddImage(tex_id, {p.x,        m.y       }, {m.x,        m.y + size}, {uv1, uv2}, {uv2, uv3}, col);
    dl->AddImage(tex_id, {m.x,        m.y       }, {m.x + size, m.y + size}, {uv2, uv2}, {uv3, uv3}, col);
    dl->PopClipRect();
}

You can call this after your window's Begin() and the shadow will draw around it. The only caveat with this is that it won't look good if WindowRounding isn't 0. You could probably work around this by drawing the middle segment of the 9 slice texture across the window and adjusting the code to use the background draw list, but then your shadows wouldn't properly overlap other windows (at least if my understanding is right; I haven't tested the workaround).

dromer commented 1 year ago

It was mentioned that shadows are less relevant for dark themes, however: what about reusing this effort to also be able to render a "glow" behind the window?

frink commented 1 year ago

@dromer - precisely my usecase.

ocornut commented 1 year ago

to also be able to render a "glow" behind the window?

This is exactly what shadows are doing.

This cannot be finished and merged until #3761 is done, and the polygon clipper of current shadow impl. is ihmo too complex for its own good. Without further resources I can't expect this to be usable anytime soon.