From c7a9d64eafb626cadb28cdd4663cb4c7ab952d98 Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Sat, 7 Dec 2024 22:12:05 +0000 Subject: [PATCH] Add color channel filter to editor texture previews --- core/io/image.cpp | 93 ++++++++++- core/io/image.h | 1 + core/os/memory.cpp | 4 - core/typedefs.h | 6 + editor/icons/TexturePreviewChannels.svg | 1 + editor/plugins/color_channel_selector.cpp | 146 ++++++++++++++++++ editor/plugins/color_channel_selector.h | 65 ++++++++ editor/plugins/texture_3d_editor_plugin.cpp | 55 ++++++- editor/plugins/texture_3d_editor_plugin.h | 6 + editor/plugins/texture_editor_plugin.cpp | 128 ++++++++++++--- editor/plugins/texture_editor_plugin.h | 8 + .../plugins/texture_layered_editor_plugin.cpp | 101 +++++++++++- .../plugins/texture_layered_editor_plugin.h | 6 + 13 files changed, 582 insertions(+), 38 deletions(-) create mode 100644 editor/icons/TexturePreviewChannels.svg create mode 100644 editor/plugins/color_channel_selector.cpp create mode 100644 editor/plugins/color_channel_selector.h diff --git a/core/io/image.cpp b/core/io/image.cpp index 97335d0fa86..05586b3d58f 100644 --- a/core/io/image.cpp +++ b/core/io/image.cpp @@ -1135,7 +1135,7 @@ static void _overlay(const uint8_t *__restrict p_src, uint8_t *__restrict p_dst, } bool Image::is_size_po2() const { - return uint32_t(width) == next_power_of_2(width) && uint32_t(height) == next_power_of_2(height); + return is_power_of_2(width) && is_power_of_2(height); } void Image::resize_to_po2(bool p_square, Interpolation p_interpolation) { @@ -3953,6 +3953,97 @@ String Image::get_format_name(Format p_format) { return format_names[p_format]; } +uint32_t Image::get_format_component_mask(Format p_format) { + const uint32_t r = 1; + const uint32_t rg = 3; + const uint32_t rgb = 7; + const uint32_t rgba = 15; + + switch (p_format) { + case FORMAT_L8: + return rgb; + case FORMAT_LA8: + return rgba; + case FORMAT_R8: + return r; + case FORMAT_RG8: + return rg; + case FORMAT_RGB8: + return rgb; + case FORMAT_RGBA8: + return rgba; + case FORMAT_RGBA4444: + return rgba; + case FORMAT_RGB565: + return rgb; + case FORMAT_RF: + return r; + case FORMAT_RGF: + return rg; + case FORMAT_RGBF: + return rgb; + case FORMAT_RGBAF: + return rgba; + case FORMAT_RH: + return r; + case FORMAT_RGH: + return rg; + case FORMAT_RGBH: + return rgb; + case FORMAT_RGBAH: + return rgba; + case FORMAT_RGBE9995: + return rgba; + case FORMAT_DXT1: + return rgb; + case FORMAT_DXT3: + return rgb; + case FORMAT_DXT5: + return rgba; + case FORMAT_RGTC_R: + return r; + case FORMAT_RGTC_RG: + return rg; + case FORMAT_BPTC_RGBA: + return rgba; + case FORMAT_BPTC_RGBF: + return rgb; + case FORMAT_BPTC_RGBFU: + return rgb; + case FORMAT_ETC: + return rgb; + case FORMAT_ETC2_R11: + return r; + case FORMAT_ETC2_R11S: + return r; + case FORMAT_ETC2_RG11: + return rg; + case FORMAT_ETC2_RG11S: + return rg; + case FORMAT_ETC2_RGB8: + return rgb; + case FORMAT_ETC2_RGBA8: + return rgba; + case FORMAT_ETC2_RGB8A1: + return rgba; + case FORMAT_ETC2_RA_AS_RG: + return rgba; + case FORMAT_DXT5_RA_AS_RG: + return rgba; + case FORMAT_ASTC_4x4: + return rgba; + case FORMAT_ASTC_4x4_HDR: + return rgba; + case FORMAT_ASTC_8x8: + return rgba; + case FORMAT_ASTC_8x8_HDR: + return rgba; + default: + ERR_PRINT("Unhandled format."); + return rgba; + } +} + Error Image::load_png_from_buffer(const Vector &p_array) { return _load_from_buffer(p_array, _png_mem_loader_func); } diff --git a/core/io/image.h b/core/io/image.h index 30300bfa921..992c43597df 100644 --- a/core/io/image.h +++ b/core/io/image.h @@ -395,6 +395,7 @@ public: Ref get_region(const Rect2i &p_area) const; static String get_format_name(Format p_format); + static uint32_t get_format_component_mask(Format p_format); Error load_png_from_buffer(const Vector &p_array); Error load_jpg_from_buffer(const Vector &p_array); diff --git a/core/os/memory.cpp b/core/os/memory.cpp index 3a496612fab..3d3b03926c6 100644 --- a/core/os/memory.cpp +++ b/core/os/memory.cpp @@ -64,10 +64,6 @@ SafeNumeric Memory::max_usage; SafeNumeric Memory::alloc_count; -inline bool is_power_of_2(size_t x) { - return x && ((x & (x - 1U)) == 0U); -} - void *Memory::alloc_aligned_static(size_t p_bytes, size_t p_alignment) { DEV_ASSERT(is_power_of_2(p_alignment)); diff --git a/core/typedefs.h b/core/typedefs.h index b5b44f21691..7802b727166 100644 --- a/core/typedefs.h +++ b/core/typedefs.h @@ -135,6 +135,12 @@ constexpr auto CLAMP(const T m_a, const T2 m_min, const T3 m_max) { /* Functions to handle powers of 2 and shifting. */ +// Returns `true` if a positive integer is a power of 2, `false` otherwise. +template +inline bool is_power_of_2(const T x) { + return x && ((x & (x - 1)) == 0); +} + // Function to find the next power of 2 to an integer. static _FORCE_INLINE_ unsigned int next_power_of_2(unsigned int x) { if (x == 0) { diff --git a/editor/icons/TexturePreviewChannels.svg b/editor/icons/TexturePreviewChannels.svg new file mode 100644 index 00000000000..e70fd998c74 --- /dev/null +++ b/editor/icons/TexturePreviewChannels.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/plugins/color_channel_selector.cpp b/editor/plugins/color_channel_selector.cpp new file mode 100644 index 00000000000..087a60e77f9 --- /dev/null +++ b/editor/plugins/color_channel_selector.cpp @@ -0,0 +1,146 @@ +/**************************************************************************/ +/* color_channel_selector.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "color_channel_selector.h" + +#include "editor/themes/editor_scale.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/panel_container.h" +#include "scene/resources/style_box_flat.h" + +ColorChannelSelector::ColorChannelSelector() { + toggle_button = memnew(Button); + toggle_button->set_flat(true); + toggle_button->set_toggle_mode(true); + toggle_button->connect(SceneStringName(toggled), callable_mp(this, &ColorChannelSelector::on_toggled)); + toggle_button->add_theme_style_override("focus", memnew(StyleBoxEmpty)); + add_child(toggle_button); + + panel = memnew(PanelContainer); + panel->hide(); + + HBoxContainer *container = memnew(HBoxContainer); + container->add_theme_constant_override("separation", 0); + + create_button(0, "R", container); + create_button(1, "G", container); + create_button(2, "B", container); + create_button(3, "A", container); + + // Use a bit of transparency to be less distracting. + set_modulate(Color(1, 1, 1, 0.7)); + + panel->add_child(container); + + add_child(panel); +} + +void ColorChannelSelector::_notification(int p_what) { + if (p_what == NOTIFICATION_THEME_CHANGED) { + // PanelContainer's background is invisible in the editor. We need a background. + // And we need this in turn because buttons don't look good without background (for example, hover is transparent). + Ref bg_style = get_theme_stylebox(SceneStringName(panel), "TabContainer"); + ERR_FAIL_COND(bg_style.is_null()); + bg_style = bg_style->duplicate(); + // The default content margin makes the widget become a bit too large. It should be like mini-toolbar. + const float editor_scale = EditorScale::get_scale(); + bg_style->set_content_margin(SIDE_LEFT, 1.0f * editor_scale); + bg_style->set_content_margin(SIDE_RIGHT, 1.0f * editor_scale); + bg_style->set_content_margin(SIDE_TOP, 1.0f * editor_scale); + bg_style->set_content_margin(SIDE_BOTTOM, 1.0f * editor_scale); + panel->add_theme_style_override(SceneStringName(panel), bg_style); + + Ref icon = get_editor_theme_icon(SNAME("TexturePreviewChannels")); + toggle_button->set_button_icon(icon); + } +} + +void ColorChannelSelector::set_available_channels_mask(uint32_t p_mask) { + for (unsigned int i = 0; i < CHANNEL_COUNT; ++i) { + const bool available = (p_mask & (1u << i)) != 0; + Button *button = channel_buttons[i]; + button->set_visible(available); + } +} + +void ColorChannelSelector::on_channel_button_toggled(bool p_unused_pressed) { + emit_signal("selected_channels_changed"); +} + +uint32_t ColorChannelSelector::get_selected_channels_mask() const { + uint32_t mask = 0; + for (unsigned int i = 0; i < CHANNEL_COUNT; ++i) { + Button *button = channel_buttons[i]; + if (button->is_visible() && channel_buttons[i]->is_pressed()) { + mask |= (1 << i); + } + } + return mask; +} + +// Helper +Vector4 ColorChannelSelector::get_selected_channel_factors() const { + Vector4 channel_factors; + const uint32_t mask = get_selected_channels_mask(); + for (unsigned int i = 0; i < 4; ++i) { + if ((mask & (1 << i)) != 0) { + channel_factors[i] = 1; + } + } + return channel_factors; +} + +void ColorChannelSelector::create_button(unsigned int p_channel_index, const String &p_text, Control *p_parent) { + ERR_FAIL_COND(p_channel_index >= CHANNEL_COUNT); + ERR_FAIL_COND(channel_buttons[p_channel_index] != nullptr); + Button *button = memnew(Button); + button->set_text(p_text); + button->set_toggle_mode(true); + button->set_pressed(true); + + // Don't show focus, it stands out too much and remains visible which can be confusing. + button->add_theme_style_override("focus", memnew(StyleBoxEmpty)); + + // Make it look similar to toolbar buttons. + button->set_theme_type_variation(SceneStringName(FlatButton)); + + button->connect(SceneStringName(toggled), callable_mp(this, &ColorChannelSelector::on_channel_button_toggled)); + p_parent->add_child(button); + channel_buttons[p_channel_index] = button; +} + +void ColorChannelSelector::on_toggled(bool p_pressed) { + panel->set_visible(p_pressed); +} + +void ColorChannelSelector::_bind_methods() { + ADD_SIGNAL(MethodInfo("selected_channels_changed")); +} diff --git a/editor/plugins/color_channel_selector.h b/editor/plugins/color_channel_selector.h new file mode 100644 index 00000000000..f1498d30b7f --- /dev/null +++ b/editor/plugins/color_channel_selector.h @@ -0,0 +1,65 @@ +/**************************************************************************/ +/* color_channel_selector.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef COLOR_CHANNEL_SELECTOR_H +#define COLOR_CHANNEL_SELECTOR_H + +#include "scene/gui/box_container.h" + +class PanelContainer; +class Button; + +class ColorChannelSelector : public HBoxContainer { + GDCLASS(ColorChannelSelector, HBoxContainer); + + static const unsigned int CHANNEL_COUNT = 4; + +public: + ColorChannelSelector(); + + void set_available_channels_mask(uint32_t p_mask); + uint32_t get_selected_channels_mask() const; + Vector4 get_selected_channel_factors() const; + +private: + void _notification(int p_what); + + void on_channel_button_toggled(bool p_unused_pressed); + void create_button(unsigned int p_channel_index, const String &p_text, Control *p_parent); + void on_toggled(bool p_pressed); + + static void _bind_methods(); + + Button *channel_buttons[CHANNEL_COUNT] = {}; + PanelContainer *panel = nullptr; + Button *toggle_button = nullptr; +}; + +#endif // COLOR_CHANNEL_SELECTOR_H diff --git a/editor/plugins/texture_3d_editor_plugin.cpp b/editor/plugins/texture_3d_editor_plugin.cpp index 8eace98634d..dd4498a3445 100644 --- a/editor/plugins/texture_3d_editor_plugin.cpp +++ b/editor/plugins/texture_3d_editor_plugin.cpp @@ -31,6 +31,7 @@ #include "texture_3d_editor_plugin.h" #include "editor/editor_string_names.h" +#include "editor/plugins/color_channel_selector.h" #include "editor/themes/editor_scale.h" #include "scene/gui/label.h" @@ -44,8 +45,29 @@ constexpr const char *texture_3d_shader = R"( uniform sampler3D tex; uniform float layer; + uniform vec4 u_channel_factors = vec4(1.0); + + vec4 filter_preview_colors(vec4 input_color, vec4 factors) { + // Filter RGB. + vec4 output_color = input_color * vec4(factors.rgb, input_color.a); + + // Remove transparency when alpha is not enabled. + output_color.a = mix(1.0, output_color.a, factors.a); + + // Switch to opaque grayscale when visualizing only one channel. + float csum = factors.r + factors.g + factors.b + factors.a; + float single = clamp(2.0 - csum, 0.0, 1.0); + for (int i = 0; i < 4; i++) { + float c = input_color[i]; + output_color = mix(output_color, vec4(c, c, c, 1.0), factors[i] * single); + } + + return output_color; + } + void fragment() { COLOR = textureLod(tex, vec3(UV, layer), 0.0); + COLOR = filter_preview_colors(COLOR, u_channel_factors); } )"; @@ -94,6 +116,8 @@ void Texture3DEditor::_update_material(bool p_texture_changed) { if (p_texture_changed) { material->set_shader_parameter("tex", texture->get_rid()); } + + material->set_shader_parameter("u_channel_factors", channel_selector->get_selected_channel_factors()); } void Texture3DEditor::_make_shaders() { @@ -138,30 +162,44 @@ void Texture3DEditor::_update_gui() { layer->set_max(texture->get_depth() - 1); - const String format = Image::get_format_name(texture->get_format()); + const Image::Format format = texture->get_format(); + const String format_name = Image::get_format_name(format); if (texture->has_mipmaps()) { - const int mip_count = Image::get_image_required_mipmaps(texture->get_width(), texture->get_height(), texture->get_format()); - const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), true) * texture->get_depth(); + const int mip_count = Image::get_image_required_mipmaps(texture->get_width(), texture->get_height(), format); + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), format, true) * texture->get_depth(); info->set_text(vformat(String::utf8("%d×%d×%d %s\n") + TTR("%s Mipmaps") + "\n" + TTR("Memory: %s"), texture->get_width(), texture->get_height(), texture->get_depth(), - format, + format_name, mip_count, String::humanize_size(memory))); } else { - const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), false) * texture->get_depth(); + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), format, false) * texture->get_depth(); info->set_text(vformat(String::utf8("%d×%d×%d %s\n") + TTR("No Mipmaps") + "\n" + TTR("Memory: %s"), texture->get_width(), texture->get_height(), texture->get_depth(), - format, + format_name, String::humanize_size(memory))); } + + const uint32_t components_mask = Image::get_format_component_mask(format); + if (is_power_of_2(components_mask)) { + // Only one channel available, no point in showing a channel selector. + channel_selector->hide(); + } else { + channel_selector->show(); + channel_selector->set_available_channels_mask(components_mask); + } +} + +void Texture3DEditor::on_selected_channels_changed() { + _update_material(false); } void Texture3DEditor::edit(Ref p_texture) { @@ -215,6 +253,11 @@ Texture3DEditor::Texture3DEditor() { add_child(layer); + channel_selector = memnew(ColorChannelSelector); + channel_selector->connect("selected_channels_changed", callable_mp(this, &Texture3DEditor::on_selected_channels_changed)); + channel_selector->set_anchors_preset(Control::PRESET_TOP_LEFT); + add_child(channel_selector); + info = memnew(Label); info->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1)); info->add_theme_color_override("font_shadow_color", Color(0, 0, 0)); diff --git a/editor/plugins/texture_3d_editor_plugin.h b/editor/plugins/texture_3d_editor_plugin.h index 4124fc58414..58424955ec4 100644 --- a/editor/plugins/texture_3d_editor_plugin.h +++ b/editor/plugins/texture_3d_editor_plugin.h @@ -37,6 +37,8 @@ #include "scene/resources/shader.h" #include "scene/resources/texture.h" +class ColorChannelSelector; + class Texture3DEditor : public Control { GDCLASS(Texture3DEditor, Control); @@ -49,6 +51,8 @@ class Texture3DEditor : public Control { Control *texture_rect = nullptr; + ColorChannelSelector *channel_selector = nullptr; + bool setting = false; void _make_shaders(); @@ -67,6 +71,8 @@ class Texture3DEditor : public Control { void _update_material(bool p_texture_changed); void _update_gui(); + void on_selected_channels_changed(); + protected: void _notification(int p_what); diff --git a/editor/plugins/texture_editor_plugin.cpp b/editor/plugins/texture_editor_plugin.cpp index 8b0c69e9d74..561146be425 100644 --- a/editor/plugins/texture_editor_plugin.cpp +++ b/editor/plugins/texture_editor_plugin.cpp @@ -31,6 +31,7 @@ #include "texture_editor_plugin.h" #include "editor/editor_string_names.h" +#include "editor/plugins/color_channel_selector.h" #include "editor/themes/editor_scale.h" #include "scene/gui/aspect_ratio_container.h" #include "scene/gui/color_rect.h" @@ -41,6 +42,36 @@ #include "scene/resources/compressed_texture.h" #include "scene/resources/image_texture.h" #include "scene/resources/portable_compressed_texture.h" +#include "scene/resources/style_box_flat.h" + +constexpr const char *texture_2d_shader = R"( +shader_type canvas_item; +render_mode blend_mix; + +uniform vec4 u_channel_factors = vec4(1.0); + +vec4 filter_preview_colors(vec4 input_color, vec4 factors) { + // Filter RGB. + vec4 output_color = input_color * vec4(factors.rgb, input_color.a); + + // Remove transparency when alpha is not enabled. + output_color.a = mix(1.0, output_color.a, factors.a); + + // Switch to opaque grayscale when visualizing only one channel. + float csum = factors.r + factors.g + factors.b + factors.a; + float single = clamp(2.0 - csum, 0.0, 1.0); + for (int i = 0; i < 4; i++) { + float c = input_color[i]; + output_color = mix(output_color, vec4(c, c, c, 1.0), factors[i] * single); + } + + return output_color; +} + +void fragment() { + COLOR = filter_preview_colors(texture(TEXTURE, UV), u_channel_factors); +} +)"; TextureRect *TexturePreview::get_texture_display() { return texture_display; @@ -72,8 +103,8 @@ void TexturePreview::_notification(int p_what) { void TexturePreview::_draw_outline() { const float outline_width = Math::round(EDSCALE); - const Rect2 outline_rect = Rect2(Vector2(), texture_display->get_size()).grow(outline_width * 0.5); - texture_display->draw_rect(outline_rect, cached_outline_color, false, outline_width); + const Rect2 outline_rect = Rect2(Vector2(), outline_overlay->get_size()).grow(outline_width * 0.5); + outline_overlay->draw_rect(outline_rect, cached_outline_color, false, outline_width); } void TexturePreview::_update_texture_display_ratio() { @@ -82,25 +113,49 @@ void TexturePreview::_update_texture_display_ratio() { } } -void TexturePreview::_update_metadata_label_text() { - const Ref texture = texture_display->get_texture(); - - String format; - if (Object::cast_to(*texture)) { - format = Image::get_format_name(Object::cast_to(*texture)->get_format()); - } else if (Object::cast_to(*texture)) { - format = Image::get_format_name(Object::cast_to(*texture)->get_format()); - } else { - format = texture->get_class(); +static Image::Format get_texture_2d_format(const Ref &p_texture) { + const Ref image_texture = p_texture; + if (image_texture.is_valid()) { + return image_texture->get_format(); } - const Ref image = texture->get_image(); + const Ref compressed_texture = p_texture; + if (compressed_texture.is_valid()) { + return compressed_texture->get_format(); + } + + // AtlasTexture? + + // Unknown + return Image::FORMAT_MAX; +} + +static int get_texture_mipmaps_count(const Ref &p_texture) { + ERR_FAIL_COND_V(p_texture.is_null(), -1); + // We are having to download the image only to get its mipmaps count. It would be nice if we didn't have to. + Ref image = p_texture->get_image(); if (image.is_valid()) { - const int mipmaps = image->get_mipmap_count(); + return image->get_mipmap_count(); + } + return -1; +} + +void TexturePreview::_update_metadata_label_text() { + const Ref texture = texture_display->get_texture(); + ERR_FAIL_COND(texture.is_null()); + + const Image::Format format = get_texture_2d_format(texture.ptr()); + + const String format_name = format != Image::FORMAT_MAX ? Image::get_format_name(format) : texture->get_class(); + + const Vector2i resolution = texture->get_size(); + const int mipmaps = get_texture_mipmaps_count(texture); + + if (format != Image::FORMAT_MAX) { // Avoid signed integer overflow that could occur with huge texture sizes by casting everything to uint64_t. - uint64_t memory = uint64_t(image->get_width()) * uint64_t(image->get_height()) * uint64_t(Image::get_format_pixel_size(image->get_format())); + uint64_t memory = uint64_t(resolution.x) * uint64_t(resolution.y) * uint64_t(Image::get_format_pixel_size(format)); // Handle VRAM-compressed formats that are stored with 4 bpp. - memory >>= Image::get_format_pixel_rshift(image->get_format()); + memory >>= Image::get_format_pixel_rshift(format); float mipmaps_multiplier = 1.0; float mipmap_increase = 0.25; @@ -117,7 +172,7 @@ void TexturePreview::_update_metadata_label_text() { vformat(String::utf8("%d×%d %s\n") + TTR("%s Mipmaps") + "\n" + TTR("Memory: %s"), texture->get_width(), texture->get_height(), - format, + format_name, mipmaps, String::humanize_size(memory))); } else { @@ -127,7 +182,7 @@ void TexturePreview::_update_metadata_label_text() { vformat(String::utf8("%d×%d %s\n") + TTR("No Mipmaps") + "\n" + TTR("Memory: %s"), texture->get_width(), texture->get_height(), - format, + format_name, String::humanize_size(memory))); } } else { @@ -135,10 +190,14 @@ void TexturePreview::_update_metadata_label_text() { vformat(String::utf8("%d×%d %s"), texture->get_width(), texture->get_height(), - format)); + format_name)); } } +void TexturePreview::on_selected_channels_changed() { + material->set_shader_parameter("u_channel_factors", channel_selector->get_selected_channel_factors()); +} + TexturePreview::TexturePreview(Ref p_texture, bool p_show_metadata) { set_custom_minimum_size(Size2(0.0, 256.0) * EDSCALE); @@ -163,19 +222,48 @@ TexturePreview::TexturePreview(Ref p_texture, bool p_show_metadata) { checkerboard->set_texture_repeat(CanvasItem::TEXTURE_REPEAT_ENABLED); centering_container->add_child(checkerboard); + { + Ref shader; + shader.instantiate(); + shader->set_code(texture_2d_shader); + + material.instantiate(); + material->set_shader(shader); + material->set_shader_parameter("u_channel_factors", Vector4(1, 1, 1, 1)); + } + texture_display = memnew(TextureRect); texture_display->set_texture_filter(TEXTURE_FILTER_NEAREST_WITH_MIPMAPS); texture_display->set_texture(p_texture); texture_display->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE); + texture_display->set_material(material); centering_container->add_child(texture_display); - texture_display->connect(SceneStringName(draw), callable_mp(this, &TexturePreview::_draw_outline)); + // Creating a separate control so it is not affected by the filtering shader. + outline_overlay = memnew(Control); + centering_container->add_child(outline_overlay); + + outline_overlay->connect(SceneStringName(draw), callable_mp(this, &TexturePreview::_draw_outline)); if (p_texture.is_valid()) { _update_texture_display_ratio(); p_texture->connect_changed(callable_mp(this, &TexturePreview::_update_texture_display_ratio)); } + // Null can be passed by `Camera3DPreview` (which immediately after sets a texture anyways). + const Image::Format format = p_texture.is_valid() ? get_texture_2d_format(p_texture.ptr()) : Image::FORMAT_MAX; + const uint32_t components_mask = format != Image::FORMAT_MAX ? Image::get_format_component_mask(format) : 0xf; + + // Add color channel selector at the bottom left if more than 1 channel is available. + if (p_show_metadata && !is_power_of_2(components_mask)) { + channel_selector = memnew(ColorChannelSelector); + channel_selector->connect("selected_channels_changed", callable_mp(this, &TexturePreview::on_selected_channels_changed)); + channel_selector->set_h_size_flags(Control::SIZE_SHRINK_BEGIN); + channel_selector->set_v_size_flags(Control::SIZE_SHRINK_BEGIN); + channel_selector->set_available_channels_mask(components_mask); + add_child(channel_selector); + } + if (p_show_metadata) { metadata_label = memnew(Label); diff --git a/editor/plugins/texture_editor_plugin.h b/editor/plugins/texture_editor_plugin.h index 01008108c84..d27cb4eab72 100644 --- a/editor/plugins/texture_editor_plugin.h +++ b/editor/plugins/texture_editor_plugin.h @@ -39,6 +39,8 @@ class AspectRatioContainer; class ColorRect; class TextureRect; +class ShaderMaterial; +class ColorChannelSelector; class TexturePreview : public MarginContainer { GDCLASS(TexturePreview, MarginContainer); @@ -47,10 +49,14 @@ private: TextureRect *texture_display = nullptr; MarginContainer *margin_container = nullptr; + Control *outline_overlay = nullptr; AspectRatioContainer *centering_container = nullptr; ColorRect *bg_rect = nullptr; TextureRect *checkerboard = nullptr; Label *metadata_label = nullptr; + Ref material; + + ColorChannelSelector *channel_selector = nullptr; Color cached_outline_color; @@ -61,6 +67,8 @@ protected: void _notification(int p_what); void _update_texture_display_ratio(); + void on_selected_channels_changed(); + public: TextureRect *get_texture_display(); TexturePreview(Ref p_texture, bool p_show_metadata); diff --git a/editor/plugins/texture_layered_editor_plugin.cpp b/editor/plugins/texture_layered_editor_plugin.cpp index 0eadc939db3..2ecd521699d 100644 --- a/editor/plugins/texture_layered_editor_plugin.cpp +++ b/editor/plugins/texture_layered_editor_plugin.cpp @@ -31,6 +31,7 @@ #include "texture_layered_editor_plugin.h" #include "editor/editor_string_names.h" +#include "editor/plugins/color_channel_selector.h" #include "editor/themes/editor_scale.h" #include "scene/gui/label.h" @@ -43,9 +44,29 @@ constexpr const char *array_2d_shader = R"( uniform sampler2DArray tex; uniform float layer; + uniform vec4 u_channel_factors = vec4(1.0); + + vec4 filter_preview_colors(vec4 input_color, vec4 factors) { + // Filter RGB. + vec4 output_color = input_color * vec4(factors.rgb, input_color.a); + + // Remove transparency when alpha is not enabled. + output_color.a = mix(1.0, output_color.a, factors.a); + + // Switch to opaque grayscale when visualizing only one channel. + float csum = factors.r + factors.g + factors.b + factors.a; + float single = clamp(2.0 - csum, 0.0, 1.0); + for (int i = 0; i < 4; i++) { + float c = input_color[i]; + output_color = mix(output_color, vec4(c, c, c, 1.0), factors[i] * single); + } + + return output_color; + } void fragment() { COLOR = textureLod(tex, vec3(UV, layer), 0.0); + COLOR = filter_preview_colors(COLOR, u_channel_factors); } )"; @@ -58,9 +79,30 @@ constexpr const char *cubemap_shader = R"( uniform vec3 normal; uniform mat3 rot; + uniform vec4 u_channel_factors = vec4(1.0); + + vec4 filter_preview_colors(vec4 input_color, vec4 factors) { + // Filter RGB. + vec4 output_color = input_color * vec4(factors.rgb, input_color.a); + + // Remove transparency when alpha is not enabled. + output_color.a = mix(1.0, output_color.a, factors.a); + + // Switch to opaque grayscale when visualizing only one channel. + float csum = factors.r + factors.g + factors.b + factors.a; + float single = clamp(2.0 - csum, 0.0, 1.0); + for (int i = 0; i < 4; i++) { + float c = input_color[i]; + output_color = mix(output_color, vec4(c, c, c, 1.0), factors[i] * single); + } + + return output_color; + } + void fragment() { vec3 n = rot * normalize(vec3(normal.xy * (UV * 2.0 - 1.0), normal.z)); COLOR = textureLod(tex, n, 0.0); + COLOR = filter_preview_colors(COLOR, u_channel_factors); } )"; @@ -73,9 +115,30 @@ constexpr const char *cubemap_array_shader = R"( uniform mat3 rot; uniform float layer; + uniform vec4 u_channel_factors = vec4(1.0); + + vec4 filter_preview_colors(vec4 input_color, vec4 factors) { + // Filter RGB. + vec4 output_color = input_color * vec4(factors.rgb, input_color.a); + + // Remove transparency when alpha is not enabled. + output_color.a = mix(1.0, output_color.a, factors.a); + + // Switch to opaque grayscale when visualizing only one channel. + float csum = factors.r + factors.g + factors.b + factors.a; + float single = clamp(2.0 - csum, 0.0, 1.0); + for (int i = 0; i < 4; i++) { + float c = input_color[i]; + output_color = mix(output_color, vec4(c, c, c, 1.0), factors[i] * single); + } + + return output_color; + } + void fragment() { vec3 n = rot * normalize(vec3(normal.xy * (UV * 2.0 - 1.0), normal.z)); COLOR = textureLod(tex, vec4(n, layer), 0.0); + COLOR = filter_preview_colors(COLOR, u_channel_factors); } )"; @@ -102,7 +165,8 @@ void TextureLayeredEditor::_update_gui() { _texture_rect_update_area(); - const String format = Image::get_format_name(texture->get_format()); + const Image::Format format = texture->get_format(); + const String format_name = Image::get_format_name(format); String texture_info; switch (texture->get_layered_type()) { @@ -113,7 +177,7 @@ void TextureLayeredEditor::_update_gui() { texture->get_width(), texture->get_height(), texture->get_layers(), - format); + format_name); } break; case TextureLayered::LAYERED_TYPE_CUBEMAP: { @@ -122,7 +186,7 @@ void TextureLayeredEditor::_update_gui() { texture_info = vformat(String::utf8("%d×%d %s\n"), texture->get_width(), texture->get_height(), - format); + format_name); } break; case TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY: { @@ -132,7 +196,7 @@ void TextureLayeredEditor::_update_gui() { texture->get_width(), texture->get_height(), texture->get_layers() / 6, - format); + format_name); } break; @@ -141,21 +205,30 @@ void TextureLayeredEditor::_update_gui() { } if (texture->has_mipmaps()) { - const int mip_count = Image::get_image_required_mipmaps(texture->get_width(), texture->get_height(), texture->get_format()); - const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), true) * texture->get_layers(); + const int mip_count = Image::get_image_required_mipmaps(texture->get_width(), texture->get_height(), format); + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), format, true) * texture->get_layers(); texture_info += vformat(TTR("%s Mipmaps") + "\n" + TTR("Memory: %s"), mip_count, String::humanize_size(memory)); } else { - const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), false) * texture->get_layers(); + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), format, false) * texture->get_layers(); texture_info += vformat(TTR("No Mipmaps") + "\n" + TTR("Memory: %s"), String::humanize_size(memory)); } info->set_text(texture_info); + + const uint32_t components_mask = Image::get_format_component_mask(format); + if (is_power_of_2(components_mask)) { + // Only one channel available, no point in showing a channel selector. + channel_selector->hide(); + } else { + channel_selector->show(); + channel_selector->set_available_channels_mask(components_mask); + } } void TextureLayeredEditor::_notification(int p_what) { @@ -212,6 +285,15 @@ void TextureLayeredEditor::_update_material(bool p_texture_changed) { if (p_texture_changed) { materials[texture->get_layered_type()]->set_shader_parameter("tex", texture->get_rid()); } + + const Vector4 channel_factors = channel_selector->get_selected_channel_factors(); + for (unsigned int i = 0; i < 3; ++i) { + materials[i]->set_shader_parameter("u_channel_factors", channel_factors); + } +} + +void TextureLayeredEditor::on_selected_channels_changed() { + _update_material(false); } void TextureLayeredEditor::_make_shaders() { @@ -309,6 +391,11 @@ TextureLayeredEditor::TextureLayeredEditor() { add_child(layer); + channel_selector = memnew(ColorChannelSelector); + channel_selector->connect("selected_channels_changed", callable_mp(this, &TextureLayeredEditor::on_selected_channels_changed)); + channel_selector->set_anchors_and_offsets_preset(Control::PRESET_TOP_LEFT); + add_child(channel_selector); + info = memnew(Label); info->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1)); info->add_theme_color_override("font_shadow_color", Color(0, 0, 0)); diff --git a/editor/plugins/texture_layered_editor_plugin.h b/editor/plugins/texture_layered_editor_plugin.h index 5648a04c80f..d226417525d 100644 --- a/editor/plugins/texture_layered_editor_plugin.h +++ b/editor/plugins/texture_layered_editor_plugin.h @@ -37,6 +37,8 @@ #include "scene/resources/shader.h" #include "scene/resources/texture.h" +class ColorChannelSelector; + class TextureLayeredEditor : public Control { GDCLASS(TextureLayeredEditor, Control); @@ -53,6 +55,8 @@ class TextureLayeredEditor : public Control { bool setting = false; + ColorChannelSelector *channel_selector = nullptr; + void _make_shaders(); void _update_material(bool p_texture_changed); @@ -69,6 +73,8 @@ class TextureLayeredEditor : public Control { void _update_gui(); + void on_selected_channels_changed(); + protected: void _notification(int p_what); virtual void gui_input(const Ref &p_event) override;