ocornut / imgui

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

Custom function for drawing multicolored rects not working properly with alpha #7021

Open teemo25 opened 10 months ago

teemo25 commented 10 months ago

Version/Branch of Dear ImGui: Version: 1.9.0 WIP Branch: Master

Back-end/Renderer/Compiler/OS Back-ends: imgui_impl_win32.cpp, imgui_impl_dx11.cpp Operating System: Windows 10 Home 19045.3570

My Issue/Question: I created my own function for drawing a multi colored rect that allows for rounding. Its working well but if I change any of the colors alpha to 0 all of the colors in the rectangle become invisible, not only the corner thats suposed to.

Screenshots/Video https://github.com/ocornut/imgui/assets/142989758/d7440b0b-7d30-49f8-93bf-5527ef12e7b8 I am changing only the top colors alpha. There is a black background under the rect for more visibility.

Code

inline float Lerp(float a, float b, float t) {
    return a + t * (b - a);
}

inline float Saturate(float value) {
    if (value < 0.0f) return 0.0f;
    if (value > 1.0f) return 1.0f;
    return value;
}

ImU32 ColorLerp(ImU32 col_a, ImU32 col_b, float t) {
    ImVec4 col_a_vec = ImGui::ColorConvertU32ToFloat4(col_a);
    ImVec4 col_b_vec = ImGui::ColorConvertU32ToFloat4(col_b);

    ImVec4 lerped_col = ImVec4(
        Lerp(col_a_vec.x, col_b_vec.x, t),
        Lerp(col_a_vec.y, col_b_vec.y, t),
        Lerp(col_a_vec.z, col_b_vec.z, t),
        Lerp(col_a_vec.w, col_b_vec.w, t)
    );

    return ImGui::ColorConvertFloat4ToU32(lerped_col);
}
void add_rect_multicolored_rounded(ImDrawList* draw_list, const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left, float rounding, float thickness)
{
    int vtx_start_idx = draw_list->VtxBuffer.size();

    draw_list->AddRect(p_min, p_max, col_upr_left, rounding, 0, thickness);

    int vtx_end_idx = draw_list->VtxBuffer.size();

    ImVector<ImDrawVert>& vtx_buffer = draw_list->VtxBuffer;

    for (int i = vtx_start_idx; i < vtx_end_idx; ++i)
    {
        ImDrawVert& v = draw_list->VtxBuffer[i];
        ImVec2 pos = v.pos;

        float blend_x = Saturate((pos.x - p_min.x) / (p_max.x - p_min.x));
        float blend_y = Saturate((pos.y - p_min.y) / (p_max.y - p_min.y));

        ImU32 col_top = ColorLerp(col_upr_left, col_upr_right, blend_x);
        ImU32 col_bottom = ColorLerp(col_bot_left, col_bot_right, blend_x);
        ImU32 final_color = ColorLerp(col_top, col_bottom, blend_y);

        // tested but didnt work
        // final_color = (final_color & ~IM_COL32_A_MASK) | (v.col & IM_COL32_A_MASK);

        v.col = final_color;
    }
}
GamingMinds-DanielC commented 10 months ago

ImDrawList::AddRect( ... ) has an early out if the alpha is zero, therefore you have no vertices to manipulate. Because the color is supposed to be constant across the primitive, that's no error as an alpha of zero would have no visible effect.

Since you replace the vertex color without reading it, just give AddRect a solid 0xFFFFFFFF and you're good.

teemo25 commented 10 months ago

ImDrawList::AddRect( ... ) has an early out if the alpha is zero, therefore you have no vertices to manipulate. Because the color is supposed to be constant across the primitive, that's no error as an alpha of zero would have no visible effect.

Since you replace the vertex color without reading it, just give AddRect a solid 0xFFFFFFFF and you're good.

Thank you very much, worked perfectly. Do you know if theres any other enhancements I can make to the function to allow for smoother rounded corners, they become very jagged when doing it this way.

With my function image

With regular AddRect (same rounding & thickness) image

ocornut commented 10 months ago

You should preserve the current Alpha component (or multiply by the current Alpha component if you want to modify it), because we are using it for anti-aliasing.

See what eg the ShadeVertsLinearColorGradientKeepAlpha() function is doing.

sakiodre commented 10 months ago

As ocornut has pointed out, this commented part of your code will preserve the original alpha value:

final_color = (final_color & ~IM_COL32_A_MASK) | (v.col & IM_COL32_A_MASK);

If you're not familiar with bit operations, basically it equals to final_color.a = v.col.a, in which v.col is the original color.

If you want to multiply that with the current alpha, do this instead:

ImU32 original_alpha = (v.col & IM_COL32_A_MASK) >> 24ul;
ImU32 current_alpha = (final_color & IM_COL32_A_MASK) >> 24ul;
final_color = (final_color & ~IM_COL32_A_MASK) | ((original_alpha * current_alpha / 0xFFul) << 24ul);

However, this is not ideal due to the flooring issue of integer division, I'm not sure how much of a problem is that, but just to give you an idea.

teemo25 commented 10 months ago

As ocornut has pointed out, this commented part of your code will preserve the original alpha value:

final_color = (final_color & ~IM_COL32_A_MASK) | (v.col & IM_COL32_A_MASK);

If you're not familiar with bit operations, basically it equals to final_color.a = v.col.a, in which v.col is the original color.

If you want to multiply that with the current alpha, do this instead:

ImU32 original_alpha = (v.col & IM_COL32_A_MASK) >> 24ul;
ImU32 current_alpha = (final_color & IM_COL32_A_MASK) >> 24ul;
final_color = (final_color & ~IM_COL32_A_MASK) | ((original_alpha * current_alpha / 0xFFul) << 24ul);

However, this is not ideal due to the flooring issue of integer division, I'm not sure how much of a problem is that, but just to give you an idea.

Thanks, this is working correctly the only issue now is that when a colors alpha is set to 0 it still exists or something?

ImU32 original_alpha = (v.col & IM_COL32_A_MASK) >> 24ul;
ImU32 current_alpha = (final_color & IM_COL32_A_MASK) >> 24ul;
final_color = (final_color & ~IM_COL32_A_MASK) | ((original_alpha * current_alpha / 0xFFul) << 24ul);

In this image the red (top) part of the rect has 0 alpha but its still very visible image

sakiodre commented 10 months ago

In this image the red (top) part of the rect has 0 alpha but its still very visible

Doesn't the original code also suffer from this? This is because of how color works and it's a real pain in graphics programming, the changes between 00-7F are much greater visually than the changes between 7F-FF. Tom Scott's video explained this really well.

You might want to use non-linear interpolation for this, something simple like alpha = alpha * alpha / 0xFFull; will work.

Code:

ImU32 original_alpha = (v.col & IM_COL32_A_MASK) >> 24ul;
ImU32 current_alpha = (final_color & IM_COL32_A_MASK) >> 24ul;
ImU32 alpha = original_alpha * current_alpha / 0xFFul;
alpha = alpha * alpha / 0xFFull;
final_color = (final_color & ~IM_COL32_A_MASK) | (alpha << 24ul);

Visualization: Red line is linear, blue line is y=x*x


Note: Ideally, you would want to use a cubic bezier curve function to adjust the alpha curve just how you like it.

teemo25 commented 10 months ago

In this image the red (top) part of the rect has 0 alpha but its still very visible

Doesn't the original code also suffer from this? This is because of how color works and it's a real pain in graphics programming, the changes between 00-7F are much greater visually than the changes between 7F-FF. Tom Scott's video explained this really well.

You might want to use non-linear interpolation for this, something simple like alpha = alpha * alpha / 0xFFull; will work.

Code:

ImU32 original_alpha = (v.col & IM_COL32_A_MASK) >> 24ul;
ImU32 current_alpha = (final_color & IM_COL32_A_MASK) >> 24ul;
ImU32 alpha = original_alpha * current_alpha / 0xFFul;
alpha = alpha * alpha / 0xFFull;
final_color = (final_color & ~IM_COL32_A_MASK) | (alpha << 24ul);

Visualization: Red line is linear, blue line is y=x*x

Note: Ideally, you would want to use a cubic bezier curve function to adjust the alpha curve just how you like it.

Thanks for trying to explain, Im very new to graphics and such in computers, the non linear interpolation helped quite a lot but there is still just a little of that red color left even with 0 alpha.

Im trying to do this.

        v.col = final_color;

        if ((final_color & IM_COL32_A_MASK) == 0)
        {
            v.col = IM_COL32_BLACK_TRANS;
        }

to make the color black transparent if the alpha is fully transparent but its not working at all, do you know why?

Edit: It does seem to work if the rounding isnt high: image

But when I add rounding it makes the problem apparent again: image

sakiodre commented 10 months ago

but its not working at all, do you know why?

I'm not sure what you're trying to achieve with that, because if its alpha is already 0, why set it to a black color with zero alpha (IM_COL32_BLACK_TRANS)?

The problem is because you set it to IM_COL32_BLACK_TRANS, the color interpolation will fade from black, to red (your col_upr_left and col_upr_right), to green (your col_bot_right and col_bot_left).

The reason it doesn't have a red tint when there is no rounding. This is because there is no vertex in the middle of the rect for the red color to have alpha > 0, so it will be overridden by IM_COL32_BLACK_TRANS.

If you don't know what a vertex is, it's the triangles that construct your shapes, the red lines in the following image are vertex, and your code will loop through all the yellow points:

image

In the left rect with no rounding, the first 4 points are IM_COL32_BLACK_TRANS, yes, i calculated it, is y position is 10, rect height is 305, so its alpha should be 0.274/255 and floor down to 0, which triggered your if block.

This is quite hard to explain, hope I've made it easy to understand.