diff --git a/editor/plugins/material_editor_plugin.cpp b/editor/plugins/material_editor_plugin.cpp index 8bdc763ebef0..792bceb5a35f 100644 --- a/editor/plugins/material_editor_plugin.cpp +++ b/editor/plugins/material_editor_plugin.cpp @@ -35,6 +35,7 @@ #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" +#include "editor/gui/editor_toaster.h" #include "editor/themes/editor_scale.h" #include "scene/3d/camera_3d.h" #include "scene/3d/light_3d.h" @@ -389,35 +390,179 @@ void EditorInspectorPluginMaterial::_undo_redo_inspector_callback(Object *p_undo EditorUndoRedoManager *undo_redo = Object::cast_to(p_undo_redo); ERR_FAIL_NULL(undo_redo); - // For BaseMaterial3D, if a roughness or metallic textures is being assigned to an empty slot, - // set the respective metallic or roughness factor to 1.0 as a convenience feature + bool orm_material = false; BaseMaterial3D *base_material = Object::cast_to(p_edited); - if (base_material) { - Texture2D *texture = Object::cast_to(p_new_value); - if (texture) { - if (p_property == "roughness_texture") { - if (base_material->get_texture(StandardMaterial3D::TEXTURE_ROUGHNESS).is_null()) { - undo_redo->add_do_property(p_edited, "roughness", 1.0); - - bool valid = false; - Variant value = p_edited->get("roughness", &valid); - if (valid) { - undo_redo->add_undo_property(p_edited, "roughness", value); - } + if (!base_material) { + base_material = Object::cast_to(p_edited); + if (!base_material) { + return; + } + orm_material = true; + } + + Texture2D *texture = Object::cast_to(p_new_value); + if (texture) { + // For BaseMaterial3D, if a roughness or metallic textures is being assigned to an empty slot, + // set the respective metallic or roughness factor to 1.0 as a convenience feature. + if (p_property == "roughness_texture") { + if (base_material->get_texture(StandardMaterial3D::TEXTURE_ROUGHNESS).is_null()) { + undo_redo->add_do_property(p_edited, "roughness", 1.0); + + bool valid = false; + Variant value = p_edited->get("roughness", &valid); + if (valid) { + undo_redo->add_undo_property(p_edited, "roughness", value); } - } else if (p_property == "metallic_texture") { - if (base_material->get_texture(StandardMaterial3D::TEXTURE_METALLIC).is_null()) { - undo_redo->add_do_property(p_edited, "metallic", 1.0); - - bool valid = false; - Variant value = p_edited->get("metallic", &valid); - if (valid) { - undo_redo->add_undo_property(p_edited, "metallic", value); - } + } + } else if (p_property == "metallic_texture") { + if (base_material->get_texture(StandardMaterial3D::TEXTURE_METALLIC).is_null()) { + undo_redo->add_do_property(p_edited, "metallic", 1.0); + + bool valid = false; + Variant value = p_edited->get("metallic", &valid); + if (valid) { + undo_redo->add_undo_property(p_edited, "metallic", value); + } + } + } + + // Fill in material texture slots based on the specified texture name. + Ref sm; + sm.instantiate(); + const String texture_path = texture->get_path(); + const Dictionary found_textures = sm->get_material_from_texture_path(texture_path); + +// Register "do" and "undo" actions automatically, setting the specified property to the specified value. +#define REGISTER_DO_AND_UNDO(m_property, m_value) \ + { \ + undo_redo->add_do_property(p_edited, m_property, m_value); \ + bool valid = false; \ + Variant value = p_edited->get(m_property, &valid); \ + if (valid) { \ + undo_redo->add_undo_property(p_edited, m_property, value); \ + } \ + } + + PackedStringArray assigned_textures; + if (!found_textures.is_empty()) { + if (found_textures.has("albedo")) { + REGISTER_DO_AND_UNDO("albedo_texture", ResourceLoader::load(found_textures["albedo"])); + assigned_textures.push_back("albedo"); + } + + if (found_textures.has("normal")) { + REGISTER_DO_AND_UNDO("normal_enabled", true); + REGISTER_DO_AND_UNDO("normal_texture", ResourceLoader::load(found_textures["normal"])); + assigned_textures.push_back("normal"); + } + + if (found_textures.has("orm")) { + REGISTER_DO_AND_UNDO("ao_enabled", true); + REGISTER_DO_AND_UNDO("orm_texture", ResourceLoader::load(found_textures["orm"])); + if (orm_material) { + assigned_textures.push_back("ORM"); + } else { + EditorToaster::get_singleton()->popup_str(vformat(TTR("%s: An ORM texture is available, but this is a StandardMaterial3D. This ORM texture won't be assigned to the material. To resolve this, create an ORMMaterial3D in place of this StandardMaterial3D."), texture_path)); + } + } + + if (found_textures.has("ao")) { + REGISTER_DO_AND_UNDO("ao_enabled", true); + REGISTER_DO_AND_UNDO("ao_texture", ResourceLoader::load(found_textures["ao"])); + if (!orm_material) { + assigned_textures.push_back("ambient occlusion"); + } else { + EditorToaster::get_singleton()->popup_str(vformat(TTR("%s: An ambient occlusion texture is available, but this is an ORMMaterial3D. This ambient occlusion texture won't be assigned to the material. To resolve this, create a StandardMaterial3D in place of this ORMMaterial3D."), texture_path)); + } + } + + if (found_textures.has("roughness")) { + REGISTER_DO_AND_UNDO("roughness", 1.0); + REGISTER_DO_AND_UNDO("roughness_texture", ResourceLoader::load(found_textures["roughness"])); + if (!orm_material) { + assigned_textures.push_back("roughness"); + } else { + EditorToaster::get_singleton()->popup_str(vformat(TTR("%s: A roughness texture is available, but this is an ORMMaterial3D. This roughness texture won't be assigned to the material. To resolve this, create a StandardMaterial3D in place of this ORMMaterial3D."), texture_path)); + } + } + + if (found_textures.has("metallic")) { + REGISTER_DO_AND_UNDO("metallic", 1.0); + REGISTER_DO_AND_UNDO("metallic_texture", ResourceLoader::load(found_textures["metallic"])); + if (!orm_material) { + assigned_textures.push_back("metallic"); + } else { + EditorToaster::get_singleton()->popup_str(vformat(TTR("%s: A metallic texture is available, but this is an ORMMaterial3D. This metallic texture won't be assigned to the material. To resolve this, create a StandardMaterial3D in place of this ORMMaterial3D."), texture_path)); } } + + if (found_textures.has("emission")) { + REGISTER_DO_AND_UNDO("emission_enabled", true); + REGISTER_DO_AND_UNDO("emission_texture", ResourceLoader::load(found_textures["emission"])); + assigned_textures.push_back("emission"); + } + + if (found_textures.has("height")) { + REGISTER_DO_AND_UNDO("heightmap_enabled", true); + REGISTER_DO_AND_UNDO("heightmap_texture", ResourceLoader::load(found_textures["height"])); + assigned_textures.push_back("height"); + } + + if (found_textures.has("rim")) { + REGISTER_DO_AND_UNDO("rim_enabled", true); + REGISTER_DO_AND_UNDO("rim_texture", ResourceLoader::load(found_textures["rim"])); + assigned_textures.push_back("rim"); + } + + if (found_textures.has("clearcoat")) { + REGISTER_DO_AND_UNDO("clearcoat_enabled", true); + REGISTER_DO_AND_UNDO("clearcoat_texture", ResourceLoader::load(found_textures["clearcoat"])); + assigned_textures.push_back("clearcoat"); + } + + if (found_textures.has("anisotropy")) { + REGISTER_DO_AND_UNDO("anisotropy_enabled", true); + REGISTER_DO_AND_UNDO("anisotropy_flowmap", ResourceLoader::load(found_textures["anisotropy"])); + assigned_textures.push_back("anisotropy"); + } + + if (found_textures.has("subsurf_scatter")) { + REGISTER_DO_AND_UNDO("subsurf_scatter_enabled", true); + REGISTER_DO_AND_UNDO("subsurf_scatter_texture", ResourceLoader::load(found_textures["subsurf_scatter"])); + assigned_textures.push_back("subsurface scattering"); + } + + if (found_textures.has("subsurf_scatter_transmittance")) { + REGISTER_DO_AND_UNDO("subsurf_scatter_transmittance_enabled", true); + REGISTER_DO_AND_UNDO("subsurf_scatter_transmittance_texture", ResourceLoader::load(found_textures["subsurf_scatter_transmittance"])); + assigned_textures.push_back("subsurface scattering transmittance"); + } + + if (found_textures.has("backlight")) { + REGISTER_DO_AND_UNDO("backlight_enabled", true); + REGISTER_DO_AND_UNDO("backlight_texture", ResourceLoader::load(found_textures["backlight"])); + assigned_textures.push_back("backlight"); + } + + if (found_textures.has("refraction")) { + REGISTER_DO_AND_UNDO("refraction_enabled", true); + REGISTER_DO_AND_UNDO("refraction_texture", ResourceLoader::load(found_textures["refraction"])); + assigned_textures.push_back("refraction"); + } + + if (found_textures.has("detail_albedo")) { + REGISTER_DO_AND_UNDO("detail_enabled", true); + REGISTER_DO_AND_UNDO("detail_albedo", ResourceLoader::load(found_textures["detail_albedo"])); + assigned_textures.push_back("detail albedo"); + } + } + + if (!assigned_textures.is_empty()) { + EditorToaster::get_singleton()->popup_str(vformat(TTR("%s: Automatically assigned %d textures (%s) based on file names."), texture_path, assigned_textures.size(), String(", ").join(assigned_textures))); } } + +#undef REGISTER_DO_AND_UNDO } EditorInspectorPluginMaterial::EditorInspectorPluginMaterial() { diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index 96719a9e7686..ef530f9572b2 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -2836,6 +2836,64 @@ Ref BaseMaterial3D::get_material_for_2d(bool p_shaded, Transparency p_ return materials_for_2d[key]; } +Dictionary BaseMaterial3D::get_material_from_texture_path(const String &p_file_path) const { + if (p_file_path.is_empty()) { + // We can't detect the material if the texture is built-in and therefore doesn't have a file name. + return Dictionary(); + } + + String path_type; + String found_suffix; + // Read from the last component to the first, so that names like `blue_metal_diff` + // are seen as an albedo (diffuse) texture instead of a metallic map. + PackedStringArray components = p_file_path.get_basename().get_file().replace("-", "_").replace(" ", "_").replace(".", "_").split("_", false); + components.reverse(); + + for (const String &component : components) { + if (found_suffix.is_empty()) { + for (const String type : texture_types_from_components.keys()) { + for (const String &suffix : PackedStringArray(texture_types_from_components[type])) { + // Check PascalCase, lowercase and camelCase. + for (const String &suffix_casing : PackedStringArray({ suffix, suffix.to_lower(), suffix.to_camel_case() })) { + if (component == suffix_casing) { + path_type = type; + found_suffix = suffix_casing; + break; + } + } + } + } + } else { + break; + } + } + + if (found_suffix.is_empty()) { + // Couldn't detect material based on file name. + return Dictionary(); + } + + Dictionary found_textures; + for (const String type : texture_types_from_components.keys()) { + if (type == path_type) { + // We already know this texture's type, since it was the texture originally specified. + found_textures[type] = p_file_path; + continue; + } + + for (const String &suffix : PackedStringArray(texture_types_from_components[type])) { + for (const String &suffix_casing : PackedStringArray({ suffix, suffix.to_lower(), suffix.to_camel_case() })) { + const String file_path_casing = p_file_path.replace(found_suffix, suffix_casing); + if (FileAccess::exists(file_path_casing)) { + found_textures[type] = file_path_casing; + } + } + } + } + + return found_textures; +} + void BaseMaterial3D::set_on_top_of_alpha() { set_transparency(TRANSPARENCY_DISABLED); set_render_priority(RENDER_PRIORITY_MAX); @@ -3511,6 +3569,29 @@ BaseMaterial3D::BaseMaterial3D(bool p_orm) { current_key.invalid_key = 1; _mark_dirty(); + + // We check for the presence of an ORM texture first when creating the material. + // If one is found, we'll create an ORMMaterial3D. Otherwise, we'll create a StandardMaterial3D. + texture_types_from_components["orm"] = PackedStringArray({ "ORM", "ARM" }); + + // Common PBR material texture types, standardized across engines. + texture_types_from_components["albedo"] = PackedStringArray({ "Albedo", "BaseColor", "BaseColour", "Base", "Color", "Colour", "Diffuse", "Diff", "C", "D" }); + texture_types_from_components["normal"] = PackedStringArray({ "Normal", "NormalGL", "NormalDX", "Local", "Norm", "Nor", "Nor_GL", "Nor_DX", "N" }); + texture_types_from_components["roughness"] = PackedStringArray({ "Roughness", "Rough", "R" }); // Not used in ORMMaterial3D creation. + texture_types_from_components["metallic"] = PackedStringArray({ "Metallic", "Metalness", "Metal", "M" }); // Not used in ORMMaterial3D creation. + texture_types_from_components["ao"] = PackedStringArray({ "AO", "AmbientOcclusion", "Ambient", "Occlusion", "A", "O" }); // Not used in ORMMaterial3D creation. + texture_types_from_components["emission"] = PackedStringArray({ "Emission", "Emissive", "Glow", "Luma", "E", "G" }); + texture_types_from_components["height"] = PackedStringArray({ "Height", "Displacement", "Disp", "H", "Z" }); + + // Less common and not as standardized across engines. + texture_types_from_components["rim"] = PackedStringArray({ "Rim" }); + texture_types_from_components["clearcoat"] = PackedStringArray({ "Clearcoat" }); + texture_types_from_components["anisotropy"] = PackedStringArray({ "Anisotropy", "Aniso", "Flowmap", "Flow" }); + texture_types_from_components["subsurf_scatter"] = PackedStringArray({ "Subsurface", "Subsurf", "Scattering", "Scatter", "SSS" }); + texture_types_from_components["subsurf_scatter_transmittance"] = PackedStringArray({ "Transmittance", "Transmission", "Transmissive", "Scatter", "SSS" }); + texture_types_from_components["backlight"] = PackedStringArray({ "BackLighting", "Backlight" }); + texture_types_from_components["refraction"] = PackedStringArray({ "Refraction", "Refract" }); + texture_types_from_components["detail_albedo"] = PackedStringArray({ "Detail" }); } BaseMaterial3D::~BaseMaterial3D() { diff --git a/scene/resources/material.h b/scene/resources/material.h index edd82b779e1a..118ae431cce9 100644 --- a/scene/resources/material.h +++ b/scene/resources/material.h @@ -567,6 +567,22 @@ class BaseMaterial3D : public Material { static HashMap> materials_for_2d; //used by Sprite3D, Label3D and other stuff + // Values must be written in PascalCase. + // The file name is stripped from its extension, then each component (each word separated by `-`, `_` or `.`) + // is checked individually for a match. Each component will check for the original casing, + // convert to lowercase, then convert the first character to lowercase. This covers the PascalCase, + // lowercase and camelCase file naming conventions. + // + // The order of the keys is important, since we want some material maps to be checked before others (e.g. albedo takes + // priority over metallic, so that `metal_grate_albedo.png` is correctly detected as an albedo map). + // + // Websites used to determine common file names: + // + // - https://polyhaven.com/ + // - http://cgbookcase.com/ + // - https://ambientcg.com/ + Dictionary texture_types_from_components; + protected: static void _bind_methods(); void _validate_property(PropertyInfo &p_property) const; @@ -781,6 +797,7 @@ class BaseMaterial3D : public Material { static void init_shaders(); static void finish_shaders(); + Dictionary get_material_from_texture_path(const String &p_file_path) const; static Ref get_material_for_2d(bool p_shaded, Transparency p_transparency, bool p_double_sided, bool p_billboard = false, bool p_billboard_y = false, bool p_msdf = false, bool p_no_depth = false, bool p_fixed_size = false, TextureFilter p_filter = TEXTURE_FILTER_LINEAR_WITH_MIPMAPS, AlphaAntiAliasing p_alpha_antialiasing_mode = ALPHA_ANTIALIASING_OFF, RID *r_shader_rid = nullptr); virtual RID get_rid() const override;