浏览代码

BasisU: Use KTX2 format and add import options to configure encoder

LuoZhihao 2 月之前
父节点
当前提交
237597b01f

+ 1 - 1
core/io/image.cpp

@@ -104,7 +104,7 @@ void (*Image::_image_decompress_astc)(Image *) = nullptr;
 Vector<uint8_t> (*Image::webp_lossy_packer)(const Ref<Image> &, float) = nullptr;
 Vector<uint8_t> (*Image::webp_lossless_packer)(const Ref<Image> &) = nullptr;
 Vector<uint8_t> (*Image::png_packer)(const Ref<Image> &) = nullptr;
-Vector<uint8_t> (*Image::basis_universal_packer)(const Ref<Image> &, Image::UsedChannels) = nullptr;
+Vector<uint8_t> (*Image::basis_universal_packer)(const Ref<Image> &, Image::UsedChannels, const BasisUniversalPackerParams &) = nullptr;
 
 Ref<Image> (*Image::webp_unpacker)(const Vector<uint8_t> &) = nullptr;
 Ref<Image> (*Image::png_unpacker)(const Vector<uint8_t> &) = nullptr;

+ 6 - 1
core/io/image.h

@@ -182,6 +182,11 @@ public:
 		ALPHA_BLEND
 	};
 
+	struct BasisUniversalPackerParams {
+		int uastc_level = 0;
+		float rdo_quality_loss = 0;
+	};
+
 	// External saver function pointers.
 
 	static inline SavePNGFunc save_png_func = nullptr;
@@ -231,7 +236,7 @@ public:
 	static Vector<uint8_t> (*webp_lossy_packer)(const Ref<Image> &p_image, float p_quality);
 	static Vector<uint8_t> (*webp_lossless_packer)(const Ref<Image> &p_image);
 	static Vector<uint8_t> (*png_packer)(const Ref<Image> &p_image);
-	static Vector<uint8_t> (*basis_universal_packer)(const Ref<Image> &p_image, UsedChannels p_channels);
+	static Vector<uint8_t> (*basis_universal_packer)(const Ref<Image> &p_image, UsedChannels p_channels, const BasisUniversalPackerParams &p_basisu_params);
 
 	static Ref<Image> (*webp_unpacker)(const Vector<uint8_t> &p_buffer);
 	static Ref<Image> (*png_unpacker)(const Vector<uint8_t> &p_buffer);

+ 10 - 0
doc/classes/PortableCompressedTexture2D.xml

@@ -43,6 +43,15 @@
 				Return whether the flag is overridden for all textures of this type.
 			</description>
 		</method>
+		<method name="set_basisu_compressor_params">
+			<return type="void" />
+			<param index="0" name="uastc_level" type="int" />
+			<param index="1" name="rdo_quality_loss" type="float" />
+			<description>
+				Sets the compressor parameters for Basis Universal compression. See also the settings in [ResourceImporterTexture].
+				[b]Note:[/b] This must be set before [method create_from_image] to take effect.
+			</description>
+		</method>
 		<method name="set_keep_all_compressed_buffers" qualifiers="static">
 			<return type="void" />
 			<param index="0" name="keep" type="bool" />
@@ -55,6 +64,7 @@
 		<member name="keep_compressed_buffer" type="bool" setter="set_keep_compressed_buffer" getter="is_keeping_compressed_buffer" default="false">
 			When running on the editor, this class will keep the source compressed data in memory. Otherwise, the source compressed data is lost after loading and the resource can't be re saved.
 			This flag allows to keep the compressed data in memory if you intend it to persist after loading.
+			[b]Note:[/b] This must be set before [method create_from_image] to take effect.
 		</member>
 		<member name="resource_local_to_scene" type="bool" setter="set_local_to_scene" getter="is_local_to_scene" overrides="Resource" default="false" />
 		<member name="size_override" type="Vector2" setter="set_size_override" getter="get_size_override" default="Vector2(0, 0)">

+ 10 - 0
doc/classes/ProjectSettings.xml

@@ -3257,6 +3257,16 @@
 		<member name="rendering/shading/overrides/force_vertex_shading" type="bool" setter="" getter="" default="false">
 			If [code]true[/code], forces vertex shading for all rendering. This can increase performance a lot, but also reduces quality immensely. Can be used to optimize performance on low-end mobile devices.
 		</member>
+		<member name="rendering/textures/basis_universal/rdo_dict_size" type="int" setter="" getter="" default="1024">
+			The dictionary size for Rate-Distortion Optimization (RDO) when importing textures as Basis Universal and when RDO is enabled, ranging from [code]64[/code] to [code]65536[/code]. Higher values reduce the file sizes further, but make encoding times significantly longer.
+		</member>
+		<member name="rendering/textures/basis_universal/zstd_supercompression" type="bool" setter="" getter="" default="true">
+			If [code]true[/code], enables Zstandard supercompression to reduce file size when importing textures as Basis Universal.
+			[b]Note:[/b] Basis Universal textures need to be compressed to gain the benefit of smaller file sizes, otherwise they are as large as VRAM-compressed textures.
+		</member>
+		<member name="rendering/textures/basis_universal/zstd_supercompression_level" type="int" setter="" getter="" default="6">
+			Specify the compression level for Basis Universal Zstandard supercompression, ranging from [code]1[/code] to [code]22[/code].
+		</member>
 		<member name="rendering/textures/canvas_textures/default_texture_filter" type="int" setter="" getter="" default="1">
 			The default texture filtering mode to use for [CanvasItem]s built-in texture. In shaders, this texture is accessed as [code]TEXTURE[/code].
 			[b]Note:[/b] For pixel art aesthetics, see also [member rendering/2d/snap/snap_2d_vertices_to_pixel] and [member rendering/2d/snap/snap_2d_transforms_to_pixel].

+ 8 - 0
doc/classes/ResourceImporterLayeredTexture.xml

@@ -40,6 +40,14 @@
 			[b]Basis Universal:[/b] Reduced quality, low memory usage, lowest size on disk, slow import. Only use for textures in 3D scenes, not for 2D elements.
 			See [url=$DOCS_URL/tutorials/assets_pipeline/importing_images.html#compress-mode]Compress mode[/url] in the manual for more details.
 		</member>
+		<member name="compress/rdo_quality_loss" type="float" setter="" getter="" default="0.0">
+			If greater than or equal to [code]0.01[/code], enables Rate-Distortion Optimization (RDO) to reduce file size. Higher values result in smaller file sizes but lower quality.
+			[b]Note:[/b] Enabling RDO makes encoding times significantly longer, especially when the image is large.
+			See also [member ProjectSettings.rendering/textures/basis_universal/rdo_dict_size] and [member ProjectSettings.rendering/textures/basis_universal/zstd_supercompression_level] if you want to reduce the file size further.
+		</member>
+		<member name="compress/uastc_level" type="int" setter="" getter="" default="0">
+			The UASTC encoding level. Higher values result in better quality but make encoding times longer.
+		</member>
 		<member name="mipmaps/generate" type="bool" setter="" getter="" default="true">
 			If [code]true[/code], smaller versions of the texture are generated on import. For example, a 64×64 texture will generate 6 mipmaps (32×32, 16×16, 8×8, 4×4, 2×2, 1×1). This has several benefits:
 			- Textures will not become grainy in the distance (in 3D), or if scaled down due to [Camera2D] zoom or [CanvasItem] scale (in 2D).

+ 8 - 0
doc/classes/ResourceImporterTexture.xml

@@ -43,6 +43,14 @@
 			When using a texture as normal map, only the red and green channels are required. Given regular texture compression algorithms produce artifacts that don't look that nice in normal maps, the RGTC compression format is the best fit for this data. Forcing this option to Enable will make Godot import the image as RGTC compressed. By default, it's set to Detect. This means that if the texture is ever detected to be used as a normal map, it will be changed to Enable and reimported automatically.
 			Note that RGTC compression affects the resulting normal map image. You will have to adjust custom shaders that use the normal map's blue channel to take this into account. Built-in material shaders already ignore the blue channel in a normal map (regardless of the actual normal map's contents).
 		</member>
+		<member name="compress/rdo_quality_loss" type="float" setter="" getter="" default="0.0">
+			If greater than or equal to [code]0.01[/code], enables Rate-Distortion Optimization (RDO) to reduce file size. Higher values result in smaller file sizes but lower quality.
+			[b]Note:[/b] Enabling RDO makes encoding times significantly longer, especially when the image is large.
+			See also [member ProjectSettings.rendering/textures/basis_universal/rdo_dict_size] and [member ProjectSettings.rendering/textures/basis_universal/zstd_supercompression_level] if you want to reduce the file size further.
+		</member>
+		<member name="compress/uastc_level" type="int" setter="" getter="" default="0">
+			The UASTC encoding level. Higher values result in better quality but make encoding times longer.
+		</member>
 		<member name="detect_3d/compress_to" type="int" setter="" getter="" default="1">
 			This changes the [member compress/mode] option that is used when a texture is detected as being used in 3D.
 			Changing this import option only has an effect if a texture is detected as being used in 3D. Changing this to [b]Disabled[/b] then reimporting will not change the existing compress mode on a texture (if it's detected to be used in 3D), but choosing [b]VRAM Compressed[/b] or [b]Basis Universal[/b] will.

+ 2 - 0
editor/editor_property_name_processor.cpp

@@ -261,6 +261,7 @@ EditorPropertyNameProcessor::EditorPropertyNameProcessor() {
 	capitalize_string_remaps["pvs"] = "PVS";
 	capitalize_string_remaps["rcedit"] = "rcedit";
 	capitalize_string_remaps["rcodesign"] = "rcodesign";
+	capitalize_string_remaps["rdo"] = "RDO";
 	capitalize_string_remaps["rgb"] = "RGB";
 	capitalize_string_remaps["rid"] = "RID";
 	capitalize_string_remaps["rmb"] = "RMB";
@@ -289,6 +290,7 @@ EditorPropertyNameProcessor::EditorPropertyNameProcessor() {
 	capitalize_string_remaps["textfile"] = "TextFile";
 	capitalize_string_remaps["tls"] = "TLS";
 	capitalize_string_remaps["tv"] = "TV";
+	capitalize_string_remaps["uastc"] = "UASTC";
 	capitalize_string_remaps["ui"] = "UI";
 	capitalize_string_remaps["uri"] = "URI";
 	capitalize_string_remaps["url"] = "URL";

+ 26 - 7
editor/import/resource_importer_layered_texture.cpp

@@ -125,6 +125,10 @@ bool ResourceImporterLayeredTexture::get_option_visibility(const String &p_path,
 	if ((p_option == "compress/high_quality" || p_option == "compress/hdr_compression") && p_options.has("compress/mode")) {
 		return int(p_options["compress/mode"]) == COMPRESS_VRAM_COMPRESSED;
 	}
+	if (p_option == "compress/uastc_level" || p_option == "compress/rdo_quality_loss") {
+		return int(p_options["compress/mode"]) == COMPRESS_BASIS_UNIVERSAL;
+	}
+
 	return true;
 }
 
@@ -140,6 +144,11 @@ void ResourceImporterLayeredTexture::get_import_options(const String &p_path, Li
 	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Lossless,Lossy,VRAM Compressed,VRAM Uncompressed,Basis Universal", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 1));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "compress/high_quality"), false));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "compress/lossy_quality", PROPERTY_HINT_RANGE, "0,1,0.01"), 0.7));
+
+	Image::BasisUniversalPackerParams basisu_params;
+	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/uastc_level", PROPERTY_HINT_ENUM, "Fastest,Faster,Medium,Slower,Slowest"), basisu_params.uastc_level));
+	r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "compress/rdo_quality_loss", PROPERTY_HINT_RANGE, "0,10,0.001,or_greater"), basisu_params.rdo_quality_loss));
+
 	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/hdr_compression", PROPERTY_HINT_ENUM, "Disabled,Opaque Only,Always"), 1));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/channel_pack", PROPERTY_HINT_ENUM, "sRGB Friendly,Optimized,Normal Map (RG Channels)"), 0));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "mipmaps/generate"), true));
@@ -158,7 +167,7 @@ void ResourceImporterLayeredTexture::get_import_options(const String &p_path, Li
 	}
 }
 
-void ResourceImporterLayeredTexture::_save_tex(Vector<Ref<Image>> p_images, const String &p_to_path, int p_compress_mode, float p_lossy, Image::CompressMode p_vram_compression, Image::CompressSource p_csource, Image::UsedChannels used_channels, bool p_mipmaps, bool p_force_po2) {
+void ResourceImporterLayeredTexture::_save_tex(Vector<Ref<Image>> p_images, const String &p_to_path, int p_compress_mode, float p_lossy, const Image::BasisUniversalPackerParams &p_basisu_params, Image::CompressMode p_vram_compression, Image::CompressSource p_csource, Image::UsedChannels used_channels, bool p_mipmaps, bool p_force_po2) {
 	Vector<Ref<Image>> mipmap_images; //for 3D
 
 	if (mode == MODE_3D) {
@@ -278,11 +287,11 @@ void ResourceImporterLayeredTexture::_save_tex(Vector<Ref<Image>> p_images, cons
 	}
 
 	for (int i = 0; i < p_images.size(); i++) {
-		ResourceImporterTexture::save_to_ctex_format(f, p_images[i], ResourceImporterTexture::CompressMode(p_compress_mode), used_channels, p_vram_compression, p_lossy);
+		ResourceImporterTexture::save_to_ctex_format(f, p_images[i], ResourceImporterTexture::CompressMode(p_compress_mode), used_channels, p_vram_compression, p_lossy, p_basisu_params);
 	}
 
 	for (int i = 0; i < mipmap_images.size(); i++) {
-		ResourceImporterTexture::save_to_ctex_format(f, mipmap_images[i], ResourceImporterTexture::CompressMode(p_compress_mode), used_channels, p_vram_compression, p_lossy);
+		ResourceImporterTexture::save_to_ctex_format(f, mipmap_images[i], ResourceImporterTexture::CompressMode(p_compress_mode), used_channels, p_vram_compression, p_lossy, p_basisu_params);
 	}
 }
 
@@ -375,6 +384,12 @@ Error ResourceImporterLayeredTexture::import(ResourceUID::ID p_source_id, const
 			slices.push_back(slice);
 		}
 	}
+
+	const Image::BasisUniversalPackerParams basisu_params = {
+		p_options["compress/uastc_level"],
+		p_options["compress/rdo_quality_loss"],
+	};
+
 	Array formats_imported;
 	Ref<LayeredTextureImport> texture_import;
 	texture_import.instantiate();
@@ -392,6 +407,8 @@ Error ResourceImporterLayeredTexture::import(ResourceUID::ID p_source_id, const
 	texture_import->used_channels = used_channels;
 	texture_import->high_quality = high_quality;
 
+	texture_import->basisu_params = basisu_params;
+
 	_check_compress_ctex(p_source_file, texture_import);
 	if (r_metadata) {
 		Dictionary meta;
@@ -486,7 +503,8 @@ void ResourceImporterLayeredTexture::_check_compress_ctex(const String &p_source
 	ERR_FAIL_NULL(r_texture_import->csource);
 	if (r_texture_import->compress_mode != COMPRESS_VRAM_COMPRESSED) {
 		// Import normally.
-		_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + extension, r_texture_import->compress_mode, r_texture_import->lossy, Image::COMPRESS_S3TC /* IGNORED */, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, false);
+		_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + extension, r_texture_import->compress_mode, r_texture_import->lossy, r_texture_import->basisu_params,
+				Image::COMPRESS_S3TC /* IGNORED */, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, false);
 		return;
 	}
 	// Must import in all formats, in order of priority (so platform chooses the best supported one. IE, etc2 over etc).
@@ -541,7 +559,8 @@ void ResourceImporterLayeredTexture::_check_compress_ctex(const String &p_source
 	}
 
 	if (use_uncompressed) {
-		_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + extension, COMPRESS_VRAM_UNCOMPRESSED, r_texture_import->lossy, Image::COMPRESS_S3TC /* IGNORED */, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, false);
+		_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + extension, COMPRESS_VRAM_UNCOMPRESSED, r_texture_import->lossy, r_texture_import->basisu_params,
+				Image::COMPRESS_S3TC /* IGNORED */, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, false);
 	} else {
 		if (can_s3tc_bptc) {
 			Image::CompressMode image_compress_mode;
@@ -553,7 +572,7 @@ void ResourceImporterLayeredTexture::_check_compress_ctex(const String &p_source
 				image_compress_mode = Image::COMPRESS_S3TC;
 				image_compress_format = "s3tc";
 			}
-			_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + image_compress_format + "." + extension, r_texture_import->compress_mode, r_texture_import->lossy, image_compress_mode, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, true);
+			_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + image_compress_format + "." + extension, r_texture_import->compress_mode, r_texture_import->lossy, r_texture_import->basisu_params, image_compress_mode, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, true);
 			r_texture_import->platform_variants->push_back(image_compress_format);
 		}
 
@@ -567,7 +586,7 @@ void ResourceImporterLayeredTexture::_check_compress_ctex(const String &p_source
 				image_compress_mode = Image::COMPRESS_ETC2;
 				image_compress_format = "etc2";
 			}
-			_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + image_compress_format + "." + extension, r_texture_import->compress_mode, r_texture_import->lossy, image_compress_mode, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, true);
+			_save_tex(*r_texture_import->slices, r_texture_import->save_path + "." + image_compress_format + "." + extension, r_texture_import->compress_mode, r_texture_import->lossy, r_texture_import->basisu_params, image_compress_mode, *r_texture_import->csource, r_texture_import->used_channels, r_texture_import->mipmaps, true);
 			r_texture_import->platform_variants->push_back(image_compress_format);
 		}
 	}

+ 4 - 1
editor/import/resource_importer_layered_texture.h

@@ -49,6 +49,9 @@ public:
 	Vector<Ref<Image>> *slices = nullptr;
 	int compress_mode = 0;
 	float lossy = 1.0;
+
+	Image::BasisUniversalPackerParams basisu_params;
+
 	int hdr_compression = 0;
 	bool mipmaps = true;
 	bool high_quality = false;
@@ -108,7 +111,7 @@ public:
 	virtual void get_import_options(const String &p_path, List<ImportOption> *r_options, int p_preset = 0) const override;
 	virtual bool get_option_visibility(const String &p_path, const String &p_option, const HashMap<StringName, Variant> &p_options) const override;
 
-	void _save_tex(Vector<Ref<Image>> p_images, const String &p_to_path, int p_compress_mode, float p_lossy, Image::CompressMode p_vram_compression, Image::CompressSource p_csource, Image::UsedChannels used_channels, bool p_mipmaps, bool p_force_po2);
+	void _save_tex(Vector<Ref<Image>> p_images, const String &p_to_path, int p_compress_mode, float p_lossy, const Image::BasisUniversalPackerParams &p_basisu_params, Image::CompressMode p_vram_compression, Image::CompressSource p_csource, Image::UsedChannels used_channels, bool p_mipmaps, bool p_force_po2);
 
 	virtual Error import(ResourceUID::ID p_source_id, const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files = nullptr, Variant *r_metadata = nullptr) override;
 

+ 23 - 10
editor/import/resource_importer_texture.cpp

@@ -207,6 +207,9 @@ bool ResourceImporterTexture::get_option_visibility(const String &p_path, const
 
 	} else if (p_option == "mipmaps/limit") {
 		return p_options["mipmaps/generate"];
+
+	} else if (p_option == "compress/uastc_level" || p_option == "compress/rdo_quality_loss") {
+		return int(p_options["compress/mode"]) == COMPRESS_BASIS_UNIVERSAL;
 	}
 
 	return true;
@@ -230,6 +233,11 @@ void ResourceImporterTexture::get_import_options(const String &p_path, List<Impo
 	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Lossless,Lossy,VRAM Compressed,VRAM Uncompressed,Basis Universal", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), p_preset == PRESET_3D ? 2 : 0));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "compress/high_quality"), false));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "compress/lossy_quality", PROPERTY_HINT_RANGE, "0,1,0.01"), 0.7));
+
+	Image::BasisUniversalPackerParams basisu_params;
+	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/uastc_level", PROPERTY_HINT_ENUM, "Fastest,Faster,Medium,Slower,Slowest"), basisu_params.uastc_level));
+	r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "compress/rdo_quality_loss", PROPERTY_HINT_RANGE, "0,10,0.001,or_greater"), basisu_params.rdo_quality_loss));
+
 	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/hdr_compression", PROPERTY_HINT_ENUM, "Disabled,Opaque Only,Always"), 1));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/normal_map", PROPERTY_HINT_ENUM, "Detect,Enable,Disabled"), 0));
 	r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/channel_pack", PROPERTY_HINT_ENUM, "sRGB Friendly,Optimized"), 0));
@@ -258,7 +266,7 @@ void ResourceImporterTexture::get_import_options(const String &p_path, List<Impo
 	}
 }
 
-void ResourceImporterTexture::save_to_ctex_format(Ref<FileAccess> f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality) {
+void ResourceImporterTexture::save_to_ctex_format(Ref<FileAccess> f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality, const Image::BasisUniversalPackerParams &p_basisu_params) {
 	switch (p_compress_mode) {
 		case COMPRESS_LOSSLESS: {
 			bool lossless_force_png = GLOBAL_GET("rendering/textures/lossless_compression/force_png") || !Image::_webp_mem_loader_func; // WebP module disabled or png is forced.
@@ -329,7 +337,7 @@ void ResourceImporterTexture::save_to_ctex_format(Ref<FileAccess> f, const Ref<I
 			f->store_32(p_image->get_mipmap_count());
 			f->store_32(p_image->get_format());
 
-			Vector<uint8_t> data = Image::basis_universal_packer(p_image, p_channels);
+			Vector<uint8_t> data = Image::basis_universal_packer(p_image, p_channels, p_basisu_params);
 			const uint64_t data_size = data.size();
 
 			f->store_32(data_size);
@@ -338,7 +346,7 @@ void ResourceImporterTexture::save_to_ctex_format(Ref<FileAccess> f, const Ref<I
 	}
 }
 
-void ResourceImporterTexture::_save_ctex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_roughness, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap, const Ref<Image> &p_normal, Image::RoughnessChannel p_roughness_channel) {
+void ResourceImporterTexture::_save_ctex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, const Image::BasisUniversalPackerParams &p_basisu_params, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_roughness, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap, const Ref<Image> &p_normal, Image::RoughnessChannel p_roughness_channel) {
 	Ref<FileAccess> f = FileAccess::open(p_to_path, FileAccess::WRITE);
 	ERR_FAIL_COND(f.is_null());
 
@@ -418,7 +426,7 @@ void ResourceImporterTexture::_save_ctex(const Ref<Image> &p_image, const String
 		used_channels = image->detect_used_channels(comp_source);
 	}
 
-	save_to_ctex_format(f, image, p_compress_mode, used_channels, p_vram_compression, p_lossy_quality);
+	save_to_ctex_format(f, image, p_compress_mode, used_channels, p_vram_compression, p_lossy_quality, p_basisu_params);
 }
 
 void ResourceImporterTexture::_save_editor_meta(const Dictionary &p_metadata, const String &p_to_path) {
@@ -508,6 +516,11 @@ Error ResourceImporterTexture::import(ResourceUID::ID p_source_id, const String
 	const bool hdr_clamp_exposure = p_options["process/hdr_clamp_exposure"];
 	int size_limit = p_options["process/size_limit"];
 
+	const Image::BasisUniversalPackerParams basisu_params = {
+		p_options["compress/uastc_level"],
+		p_options["compress/rdo_quality_loss"],
+	};
+
 	bool using_fallback_size_limit = false;
 	if (size_limit == 0) {
 		using_fallback_size_limit = true;
@@ -685,7 +698,7 @@ Error ResourceImporterTexture::import(ResourceUID::ID p_source_id, const String
 		}
 
 		if (force_uncompressed) {
-			_save_ctex(image, p_save_path + ".ctex", COMPRESS_VRAM_UNCOMPRESSED, lossy, Image::COMPRESS_S3TC /* This is ignored. */,
+			_save_ctex(image, p_save_path + ".ctex", COMPRESS_VRAM_UNCOMPRESSED, lossy, basisu_params, Image::COMPRESS_S3TC /* This is ignored. */,
 					mipmaps, stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
 		} else {
 			if (can_s3tc_bptc) {
@@ -699,8 +712,8 @@ Error ResourceImporterTexture::import(ResourceUID::ID p_source_id, const String
 					image_compress_format = "s3tc";
 				}
 
-				_save_ctex(image, p_save_path + "." + image_compress_format + ".ctex", compress_mode, lossy, image_compress_mode, mipmaps, stream, detect_3d,
-						detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
+				_save_ctex(image, p_save_path + "." + image_compress_format + ".ctex", compress_mode, lossy, basisu_params, image_compress_mode, mipmaps,
+						stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
 				r_platform_variants->push_back(image_compress_format);
 			}
 
@@ -715,19 +728,19 @@ Error ResourceImporterTexture::import(ResourceUID::ID p_source_id, const String
 					image_compress_format = "etc2";
 				}
 
-				_save_ctex(image, p_save_path + "." + image_compress_format + ".ctex", compress_mode, lossy, image_compress_mode, mipmaps, stream, detect_3d,
+				_save_ctex(image, p_save_path + "." + image_compress_format + ".ctex", compress_mode, lossy, basisu_params, image_compress_mode, mipmaps, stream, detect_3d,
 						detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
 				r_platform_variants->push_back(image_compress_format);
 			}
 		}
 	} else {
 		// Import normally.
-		_save_ctex(image, p_save_path + ".ctex", compress_mode, lossy, Image::COMPRESS_S3TC /* This is ignored. */,
+		_save_ctex(image, p_save_path + ".ctex", compress_mode, lossy, basisu_params, Image::COMPRESS_S3TC /* This is ignored. */,
 				mipmaps, stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
 	}
 
 	if (editor_image.is_valid()) {
-		_save_ctex(editor_image, p_save_path + ".editor.ctex", compress_mode, lossy, Image::COMPRESS_S3TC /* This is ignored. */,
+		_save_ctex(editor_image, p_save_path + ".editor.ctex", compress_mode, lossy, basisu_params, Image::COMPRESS_S3TC /* This is ignored. */,
 				mipmaps, stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
 
 		// Generate and save editor-specific metadata, which we cannot save to the .import file.

+ 2 - 2
editor/import/resource_importer_texture.h

@@ -72,7 +72,7 @@ protected:
 	static ResourceImporterTexture *singleton;
 	static const char *compression_formats[];
 
-	void _save_ctex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_srgb, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap, const Ref<Image> &p_normal, Image::RoughnessChannel p_roughness_channel);
+	void _save_ctex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, const Image::BasisUniversalPackerParams &p_basisu_params, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_srgb, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap, const Ref<Image> &p_normal, Image::RoughnessChannel p_roughness_channel);
 	void _save_editor_meta(const Dictionary &p_metadata, const String &p_to_path);
 	Dictionary _load_editor_meta(const String &p_to_path) const;
 
@@ -80,7 +80,7 @@ protected:
 	static inline void _invert_y_channel(Ref<Image> &r_image);
 
 public:
-	static void save_to_ctex_format(Ref<FileAccess> f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality);
+	static void save_to_ctex_format(Ref<FileAccess> f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality, const Image::BasisUniversalPackerParams &p_basisu_params);
 
 	static ResourceImporterTexture *get_singleton() { return singleton; }
 	virtual String get_importer_name() const override;

+ 2 - 7
modules/basis_universal/SCsub

@@ -60,13 +60,8 @@ env_thirdparty.disable_warnings()
 # <https://github.com/BinomialLLC/basis_universal/wiki/How-to-Use-and-Configure-the-Transcoder>
 env_thirdparty.Append(
     CPPDEFINES=[
-        # Storage formats.
-        # Godot only implements `.basis` support through basis_universal.
-        # Support for `.ktx` files are implemented with a direct libktx implementation.
-        # Building the encoder requires `BASISD_SUPPORT_KTX2` to be enabled,
-        # so we can only disable Zstandard compression for `.ktx` files
-        # (this is not used in `.basis` files).
-        ("BASISD_SUPPORT_KTX2_ZSTD", 0),
+        # Enable ktx2 zstd supercompression.
+        ("BASISD_SUPPORT_KTX2_ZSTD", 1),
         # GPU compression formats.
         ("BASISD_SUPPORT_ATC", 0),  # Proprietary Adreno format not supported by Godot.
         ("BASISD_SUPPORT_FXT1", 0),  # Legacy format not supported by Godot.

+ 71 - 30
modules/basis_universal/image_compress_basisu.cpp

@@ -30,6 +30,7 @@
 
 #include "image_compress_basisu.h"
 
+#include "core/config/project_settings.h"
 #include "core/io/image.h"
 #include "core/os/os.h"
 #include "core/string/print_string.h"
@@ -78,7 +79,7 @@ inline void _basisu_pad_mipmap(const uint8_t *p_image_mip_data, Vector<uint8_t>
 	}
 }
 
-Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedChannels p_channels) {
+Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedChannels p_channels, const Image::BasisUniversalPackerParams &p_basisu_params) {
 	init_mutex.lock();
 	if (!initialized) {
 		basisu::basisu_encoder_init();
@@ -98,16 +99,23 @@ Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedCha
 		is_hdr = true;
 	}
 
+	int rdo_dict_size = GLOBAL_GET_CACHED(int, "rendering/textures/basis_universal/rdo_dict_size");
+	bool zstd_supercompression = GLOBAL_GET_CACHED(bool, "rendering/textures/basis_universal/zstd_supercompression");
+	int zstd_supercompression_level = GLOBAL_GET_CACHED(int, "rendering/textures/basis_universal/zstd_supercompression_level");
+
 	basisu::basis_compressor_params params;
 
 	params.m_uastc = true;
-	params.m_etc1s_quality_level = basisu::BASISU_QUALITY_MIN;
 	params.m_pack_uastc_ldr_4x4_flags &= ~basisu::cPackUASTCLevelMask;
-	params.m_pack_uastc_ldr_4x4_flags |= basisu::cPackUASTCLevelFastest;
+	params.m_pack_uastc_ldr_4x4_flags |= p_basisu_params.uastc_level;
+
+	params.m_rdo_uastc_ldr_4x4 = p_basisu_params.rdo_quality_loss >= 0.01;
+	params.m_rdo_uastc_ldr_4x4_quality_scalar = p_basisu_params.rdo_quality_loss;
+	params.m_rdo_uastc_ldr_4x4_dict_size = rdo_dict_size;
 
-	params.m_rdo_uastc_ldr_4x4 = 0.0f;
-	params.m_rdo_uastc_ldr_4x4_quality_scalar = 0.0f;
-	params.m_rdo_uastc_ldr_4x4_dict_size = 1024;
+	params.m_create_ktx2_file = true;
+	params.m_ktx2_uastc_supercompression = zstd_supercompression ? basist::KTX2_SS_ZSTANDARD : basist::KTX2_SS_NONE;
+	params.m_ktx2_zstd_supercompression_level = zstd_supercompression_level;
 
 	params.m_mip_fast = true;
 	params.m_multithreading = true;
@@ -241,14 +249,14 @@ Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedCha
 	int basisu_err = compressor.process();
 	ERR_FAIL_COND_V(basisu_err != basisu::basis_compressor::cECSuccess, Vector<uint8_t>());
 
-	const basisu::uint8_vec &basisu_encoded = compressor.get_output_basis_file();
+	const basisu::uint8_vec &basisu_encoded = compressor.get_output_ktx2_file();
 
 	Vector<uint8_t> basisu_data;
 	basisu_data.resize(basisu_encoded.size() + 4);
 	uint8_t *basisu_data_ptr = basisu_data.ptrw();
 
 	// Copy the encoded BasisU data into the output buffer.
-	*(uint32_t *)basisu_data_ptr = decompress_format;
+	*(uint32_t *)basisu_data_ptr = decompress_format | BASIS_DECOMPRESS_FLAG_KTX2;
 	memcpy(basisu_data_ptr + 4, basisu_encoded.get_ptr(), basisu_encoded.size());
 
 	print_verbose(vformat("BasisU: Encoding a %dx%d image with %d mipmaps took %d ms.", p_image->get_width(), p_image->get_height(), p_image->get_mipmap_count(), OS::get_singleton()->get_ticks_msec() - start_time));
@@ -280,7 +288,9 @@ Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size) {
 	bool needs_ra_rg_swap = false;
 	bool needs_rg_trim = false;
 
-	BasisDecompressFormat decompress_format = (BasisDecompressFormat)(*(uint32_t *)(src_ptr));
+	uint32_t decompress_format = *(uint32_t *)(src_ptr);
+	bool is_ktx2 = decompress_format & BASIS_DECOMPRESS_FLAG_KTX2;
+	decompress_format &= ~BASIS_DECOMPRESS_FLAG_KTX2;
 
 	switch (decompress_format) {
 		case BASIS_DECOMPRESS_R: {
@@ -398,37 +408,68 @@ Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size) {
 	src_ptr += 4;
 	src_size -= 4;
 
-	basist::basisu_transcoder transcoder;
-	ERR_FAIL_COND_V(!transcoder.validate_header(src_ptr, src_size), image);
+	if (is_ktx2) {
+		basist::ktx2_transcoder transcoder;
+		ERR_FAIL_COND_V(!transcoder.init(src_ptr, src_size), image);
+
+		transcoder.start_transcoding();
+
+		// Create the buffer for transcoded/decompressed data.
+		Vector<uint8_t> out_data;
+		out_data.resize(Image::get_image_data_size(transcoder.get_width(), transcoder.get_height(), image_format, transcoder.get_levels() > 1));
+
+		uint8_t *dst = out_data.ptrw();
+		memset(dst, 0, out_data.size());
+
+		for (uint32_t i = 0; i < transcoder.get_levels(); i++) {
+			basist::ktx2_image_level_info basisu_level;
+			transcoder.get_image_level_info(basisu_level, i, 0, 0);
+
+			uint32_t mip_block_or_pixel_count = Image::is_format_compressed(image_format) ? basisu_level.m_total_blocks : basisu_level.m_orig_width * basisu_level.m_orig_height;
+			int64_t ofs = Image::get_image_mipmap_offset(transcoder.get_width(), transcoder.get_height(), image_format, i);
+
+			bool result = transcoder.transcode_image_level(i, 0, 0, dst + ofs, mip_block_or_pixel_count, basisu_format);
 
-	transcoder.start_transcoding(src_ptr, src_size);
+			if (!result) {
+				print_line(vformat("BasisUniversal cannot unpack level %d.", i));
+				break;
+			}
+		}
+
+		image = Image::create_from_data(transcoder.get_width(), transcoder.get_height(), transcoder.get_levels() > 1, image_format, out_data);
+	} else {
+		basist::basisu_transcoder transcoder;
+		ERR_FAIL_COND_V(!transcoder.validate_header(src_ptr, src_size), image);
 
-	basist::basisu_image_info basisu_info;
-	transcoder.get_image_info(src_ptr, src_size, basisu_info, 0);
+		transcoder.start_transcoding(src_ptr, src_size);
 
-	// Create the buffer for transcoded/decompressed data.
-	Vector<uint8_t> out_data;
-	out_data.resize(Image::get_image_data_size(basisu_info.m_width, basisu_info.m_height, image_format, basisu_info.m_total_levels > 1));
+		basist::basisu_image_info basisu_info;
+		transcoder.get_image_info(src_ptr, src_size, basisu_info, 0);
 
-	uint8_t *dst = out_data.ptrw();
-	memset(dst, 0, out_data.size());
+		// Create the buffer for transcoded/decompressed data.
+		Vector<uint8_t> out_data;
+		out_data.resize(Image::get_image_data_size(basisu_info.m_width, basisu_info.m_height, image_format, basisu_info.m_total_levels > 1));
 
-	for (uint32_t i = 0; i < basisu_info.m_total_levels; i++) {
-		basist::basisu_image_level_info basisu_level;
-		transcoder.get_image_level_info(src_ptr, src_size, basisu_level, 0, i);
+		uint8_t *dst = out_data.ptrw();
+		memset(dst, 0, out_data.size());
 
-		uint32_t mip_block_or_pixel_count = Image::is_format_compressed(image_format) ? basisu_level.m_total_blocks : basisu_level.m_orig_width * basisu_level.m_orig_height;
-		int64_t ofs = Image::get_image_mipmap_offset(basisu_info.m_width, basisu_info.m_height, image_format, i);
+		for (uint32_t i = 0; i < basisu_info.m_total_levels; i++) {
+			basist::basisu_image_level_info basisu_level;
+			transcoder.get_image_level_info(src_ptr, src_size, basisu_level, 0, i);
 
-		bool result = transcoder.transcode_image_level(src_ptr, src_size, 0, i, dst + ofs, mip_block_or_pixel_count, basisu_format);
+			uint32_t mip_block_or_pixel_count = Image::is_format_compressed(image_format) ? basisu_level.m_total_blocks : basisu_level.m_orig_width * basisu_level.m_orig_height;
+			int64_t ofs = Image::get_image_mipmap_offset(basisu_info.m_width, basisu_info.m_height, image_format, i);
 
-		if (!result) {
-			print_line(vformat("BasisUniversal cannot unpack level %d.", i));
-			break;
+			bool result = transcoder.transcode_image_level(src_ptr, src_size, 0, i, dst + ofs, mip_block_or_pixel_count, basisu_format);
+
+			if (!result) {
+				print_line(vformat("BasisUniversal cannot unpack level %d.", i));
+				break;
+			}
 		}
-	}
 
-	image = Image::create_from_data(basisu_info.m_width, basisu_info.m_height, basisu_info.m_total_levels > 1, image_format, out_data);
+		image = Image::create_from_data(basisu_info.m_width, basisu_info.m_height, basisu_info.m_total_levels > 1, image_format, out_data);
+	}
 
 	if (needs_ra_rg_swap) {
 		// Swap uncompressed RA-as-RG texture's color channels.

+ 2 - 1
modules/basis_universal/image_compress_basisu.h

@@ -41,6 +41,7 @@ enum BasisDecompressFormat {
 	BASIS_DECOMPRESS_HDR_RGB,
 	BASIS_DECOMPRESS_MAX
 };
+constexpr uint32_t BASIS_DECOMPRESS_FLAG_KTX2 = 1 << 31;
 
 void basis_universal_init();
 
@@ -52,7 +53,7 @@ struct BasisRGBAF {
 	uint32_t a;
 };
 
-Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedChannels p_channels);
+Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedChannels p_channels, const Image::BasisUniversalPackerParams &p_basisu_params);
 #endif
 
 Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size);

+ 6 - 0
modules/basis_universal/register_types.cpp

@@ -32,6 +32,8 @@
 
 #include "image_compress_basisu.h"
 
+#include "core/config/project_settings.h"
+
 void initialize_basis_universal_module(ModuleInitializationLevel p_level) {
 	if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
 		return;
@@ -40,6 +42,10 @@ void initialize_basis_universal_module(ModuleInitializationLevel p_level) {
 	basis_universal_init();
 
 #ifdef TOOLS_ENABLED
+	GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/textures/basis_universal/rdo_dict_size", PROPERTY_HINT_RANGE, "64,65536,1"), 1024);
+	GLOBAL_DEF(PropertyInfo(Variant::BOOL, "rendering/textures/basis_universal/zstd_supercompression"), true);
+	GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/textures/basis_universal/zstd_supercompression_level"), 6);
+
 	Image::basis_universal_packer = basis_universal_packer;
 #endif
 

+ 8 - 1
scene/resources/portable_compressed_texture.cpp

@@ -193,7 +193,7 @@ void PortableCompressedTexture2D::create_from_image(const Ref<Image> &p_image, C
 			ERR_FAIL_COND(p_image->is_compressed());
 			encode_uint16(DATA_FORMAT_BASIS_UNIVERSAL, buffer.ptrw() + 2);
 			Image::UsedChannels uc = p_image->detect_used_channels(p_normal_map ? Image::COMPRESS_SOURCE_NORMAL : Image::COMPRESS_SOURCE_GENERIC);
-			Vector<uint8_t> budata = Image::basis_universal_packer(p_image, uc);
+			Vector<uint8_t> budata = Image::basis_universal_packer(p_image, uc, basisu_params);
 			buffer.append_array(budata);
 #else
 			ERR_FAIL_MSG("Basis Universal compression can only run in editor build.");
@@ -360,6 +360,11 @@ bool PortableCompressedTexture2D::is_keeping_compressed_buffer() const {
 	return keep_compressed_buffer;
 }
 
+void PortableCompressedTexture2D::set_basisu_compressor_params(int p_uastc_level, float p_rdo_quality_loss) {
+	basisu_params.uastc_level = p_uastc_level;
+	basisu_params.rdo_quality_loss = p_rdo_quality_loss;
+}
+
 void PortableCompressedTexture2D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("create_from_image", "image", "compression_mode", "normal_map", "lossy_quality"), &PortableCompressedTexture2D::create_from_image, DEFVAL(false), DEFVAL(0.8));
 	ClassDB::bind_method(D_METHOD("get_format"), &PortableCompressedTexture2D::get_format);
@@ -371,6 +376,8 @@ void PortableCompressedTexture2D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_keep_compressed_buffer", "keep"), &PortableCompressedTexture2D::set_keep_compressed_buffer);
 	ClassDB::bind_method(D_METHOD("is_keeping_compressed_buffer"), &PortableCompressedTexture2D::is_keeping_compressed_buffer);
 
+	ClassDB::bind_method(D_METHOD("set_basisu_compressor_params", "uastc_level", "rdo_quality_loss"), &PortableCompressedTexture2D::set_basisu_compressor_params);
+
 	ClassDB::bind_method(D_METHOD("_set_data", "data"), &PortableCompressedTexture2D::_set_data);
 	ClassDB::bind_method(D_METHOD("_get_data"), &PortableCompressedTexture2D::_get_data);
 

+ 4 - 0
scene/resources/portable_compressed_texture.h

@@ -71,6 +71,8 @@ private:
 
 	bool image_stored = false;
 
+	Image::BasisUniversalPackerParams basisu_params;
+
 protected:
 	Vector<uint8_t> _get_data() const;
 	void _set_data(const Vector<uint8_t> &p_data);
@@ -106,6 +108,8 @@ public:
 	void set_keep_compressed_buffer(bool p_keep);
 	bool is_keeping_compressed_buffer() const;
 
+	void set_basisu_compressor_params(int p_uastc_level, float p_rdo_quality_loss);
+
 	static void set_keep_all_compressed_buffers(bool p_keep);
 	static bool is_keeping_all_compressed_buffers();