Browse Source

Merge pull request #100157 from Zylann/texture_preview_channel_selector

Add color channel filter to editor texture previews
Rémi Verschelde 7 months ago
parent
commit
f07e3ed551

+ 92 - 1
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<uint8_t> &p_array) {
 	return _load_from_buffer(p_array, _png_mem_loader_func);
 }

+ 1 - 0
core/io/image.h

@@ -395,6 +395,7 @@ public:
 	Ref<Image> 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<uint8_t> &p_array);
 	Error load_jpg_from_buffer(const Vector<uint8_t> &p_array);

+ 0 - 4
core/os/memory.cpp

@@ -64,10 +64,6 @@ SafeNumeric<uint64_t> Memory::max_usage;
 
 SafeNumeric<uint64_t> 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));
 

+ 6 - 0
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 <typename T>
+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) {

+ 1 - 0
editor/icons/TexturePreviewChannels.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#000" stroke-linejoin="round" stroke-opacity=".8" stroke-width="2" d="m8 1 7 3v8l-7 3-7-3V4Z"/><path fill-opacity=".8" d="M2 5h12v6l-12-.008Z" paint-order="stroke fill markers"/><path fill="#c2c2c2" d="m8 7.5-7 3V12l7 3 7-3v-1.5z"/><path fill="#d6d6d6" d="m8 15-7-3v-1.5l7 3z"/><path fill="#f9f9f9" d="m1 10.5 7 3 7-3-7-3z"/><path fill="#c2c2c2" d="m8 4.25-7 3v1.5l7 3 7-3v-1.5z"/><path fill="#d6d6d6" d="m8 11.75-7-3v-1.5l7 3z"/><path fill="#f9f9f9" d="m1 7.25 7 3 7-3-7-3z"/><path fill="#c2c2c2" d="M8 1 1 4v1.5l7 3 7-3V4Z"/><path fill="#f9f9f9" d="m1 4 7 3 7-3-7-3z"/><path fill="#d6d6d6" d="m8 8.5-7-3V4l7 3z"/></svg>

+ 146 - 0
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<StyleBox> 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<Texture2D> 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"));
+}

+ 65 - 0
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

+ 49 - 6
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<Texture3D> 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));

+ 6 - 0
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);
 

+ 107 - 19
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<Texture2D> texture = texture_display->get_texture();
+static Image::Format get_texture_2d_format(const Ref<Texture2D> &p_texture) {
+	const Ref<ImageTexture> image_texture = p_texture;
+	if (image_texture.is_valid()) {
+		return image_texture->get_format();
+	}
 
-	String format;
-	if (Object::cast_to<ImageTexture>(*texture)) {
-		format = Image::get_format_name(Object::cast_to<ImageTexture>(*texture)->get_format());
-	} else if (Object::cast_to<CompressedTexture2D>(*texture)) {
-		format = Image::get_format_name(Object::cast_to<CompressedTexture2D>(*texture)->get_format());
-	} else {
-		format = texture->get_class();
+	const Ref<CompressedTexture2D> compressed_texture = p_texture;
+	if (compressed_texture.is_valid()) {
+		return compressed_texture->get_format();
 	}
 
-	const Ref<Image> image = texture->get_image();
+	// AtlasTexture?
+
+	// Unknown
+	return Image::FORMAT_MAX;
+}
+
+static int get_texture_mipmaps_count(const Ref<Texture2D> &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> 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<Texture2D> 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<Texture2D> p_texture, bool p_show_metadata) {
 	set_custom_minimum_size(Size2(0.0, 256.0) * EDSCALE);
 
@@ -163,19 +222,48 @@ TexturePreview::TexturePreview(Ref<Texture2D> p_texture, bool p_show_metadata) {
 	checkerboard->set_texture_repeat(CanvasItem::TEXTURE_REPEAT_ENABLED);
 	centering_container->add_child(checkerboard);
 
+	{
+		Ref<Shader> 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);
 

+ 8 - 0
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<ShaderMaterial> 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<Texture2D> p_texture, bool p_show_metadata);

+ 94 - 7
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));

+ 6 - 0
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<InputEvent> &p_event) override;