From eb8ec10951bad29979d296c5d7cc208dfaf1fb1b Mon Sep 17 00:00:00 2001 From: Aaron Franke Date: Mon, 9 Sep 2024 02:32:33 -0700 Subject: [PATCH] GLTF: Read material texture "texCoord" property on import --- modules/gltf/doc_classes/GLTFDocument.xml | 9 + .../editor/editor_scene_importer_blend.cpp | 10 + .../editor/editor_scene_importer_gltf.cpp | 37 +- .../gltf/editor/editor_scene_importer_gltf.h | 1 + modules/gltf/gltf_document.cpp | 416 ++++++++++++------ modules/gltf/gltf_document.h | 9 + 6 files changed, 346 insertions(+), 136 deletions(-) diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml index 409347dab421..de762c1301e1 100644 --- a/modules/gltf/doc_classes/GLTFDocument.xml +++ b/modules/gltf/doc_classes/GLTFDocument.xml @@ -133,6 +133,9 @@ How to process the root node during export. The default and recommended value is [constant ROOT_NODE_MODE_SINGLE_ROOT]. [b]Note:[/b] Regardless of how the glTF file is exported, when importing, the root node type and name can be overridden in the scene import settings tab. + + How to handle texture maps during import. The default and recommended value is [constant TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL], which automatically remaps from glTF's flexible texture map system to the more specific texture map slots in Godot's [StandardMaterial3D] class. Alternatively, [constant TEXTURE_MAP_MODE_DO_NOT_REMAP] can be used to preserve the original texture maps from the glTF file, which may be desirable if using the glTF file with custom shaders, but may not display correctly with Godot's built-in materials. + How to deal with node visibility during export. This setting does nothing if all nodes are visible. The default and recommended value is [constant VISIBILITY_MODE_INCLUDE_REQUIRED], which uses the [code]KHR_node_visibility[/code] extension. @@ -147,6 +150,12 @@ Treat the Godot scene's root node as the name of the glTF scene, and add all of its children as root nodes of the glTF file. This uses only vanilla glTF features. This avoids an extra root node, but only the name of the Godot scene's root node will be preserved, as it will not be saved as a node. + + Import the texture maps in the glTF file as they are, without trying to fit them into specific texture slots suitable for Godot's built-in materials. This may be desirable if using the glTF file with custom shaders, but may not display correctly with Godot's built-in materials. This is equivalent to the behavior in Godot 4.6 and earlier. + + + Import the texture maps in the glTF file remapped to the most suitable texture slots based on Godot's [StandardMaterial3D] class. This is the default behavior. + If the scene contains any non-visible nodes, include them, mark them as non-visible with [code]KHR_node_visibility[/code], and require that importers respect their non-visibility. Downside: If the importer does not support [code]KHR_node_visibility[/code], the file cannot be imported. diff --git a/modules/gltf/editor/editor_scene_importer_blend.cpp b/modules/gltf/editor/editor_scene_importer_blend.cpp index 5a104aec768b..8d7c7c2756c9 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.cpp +++ b/modules/gltf/editor/editor_scene_importer_blend.cpp @@ -333,6 +333,10 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ int naming_version = p_options["gltf/naming_version"]; gltf->set_naming_version(naming_version); } + if (p_options.has("gltf/texture_map_mode")) { + int texture_map_mode = p_options["gltf/texture_map_mode"]; + gltf->set_texture_map_mode((GLTFDocument::TextureMapMode)texture_map_mode); + } if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { state->set_import_as_skeleton_bones(true); } @@ -401,6 +405,7 @@ void EditorSceneFormatImporterBlend::get_import_options(const String &p_path, Li ADD_OPTION_BOOL("blender/animation/group_tracks", true); r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/naming_version", PROPERTY_HINT_ENUM, "Godot 4.0 or 4.1,Godot 4.2 to 4.4,Godot 4.5 or later"), 2)); + r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/texture_map_mode", PROPERTY_HINT_ENUM, "Do Not Remap,Remap to StandardMaterial3D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), GLTFDocument::TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL)); } void EditorSceneFormatImporterBlend::handle_compatibility_options(HashMap &p_import_params) const { @@ -412,6 +417,11 @@ void EditorSceneFormatImporterBlend::handle_compatibility_options(HashMap *r_extensions) c Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t p_flags, const HashMap &p_options, List *r_missing_deps, Error *r_err) { - Ref gltf; - gltf.instantiate(); - Ref state; - state.instantiate(); + Ref gltf_doc; + gltf_doc.instantiate(); + Ref gltf_state; + gltf_state.instantiate(); if (p_options.has("gltf/naming_version")) { int naming_version = p_options["gltf/naming_version"]; - gltf->set_naming_version(naming_version); + gltf_doc->set_naming_version(naming_version); } if (p_options.has("gltf/embedded_image_handling")) { int32_t enum_option = p_options["gltf/embedded_image_handling"]; - state->set_handle_binary_image_mode((GLTFState::HandleBinaryImageMode)enum_option); + gltf_state->set_handle_binary_image_mode((GLTFState::HandleBinaryImageMode)enum_option); + } + if (p_options.has("gltf/texture_map_mode")) { + int32_t enum_option = p_options["gltf/texture_map_mode"]; + gltf_doc->set_texture_map_mode((GLTFDocument::TextureMapMode)enum_option); } if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { - state->set_import_as_skeleton_bones(true); + gltf_state->set_import_as_skeleton_bones(true); } if (p_options.has(SNAME("extract_path"))) { - state->set_extract_path(p_options["extract_path"]); + gltf_state->set_extract_path(p_options["extract_path"]); } - state->set_bake_fps(p_options["animation/fps"]); - Error err = gltf->append_from_file(p_path, state, p_flags); + gltf_state->set_bake_fps(p_options["animation/fps"]); + Error err = gltf_doc->append_from_file(p_path, gltf_state, p_flags); if (err != OK) { if (r_err) { *r_err = err; @@ -70,12 +74,12 @@ Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t return nullptr; } if (p_options.has("animation/import")) { - state->set_create_animations(bool(p_options["animation/import"])); + gltf_state->set_create_animations(bool(p_options["animation/import"])); } #ifndef DISABLE_DEPRECATED bool trimming = p_options.has("animation/trimming") ? (bool)p_options["animation/trimming"] : false; - return gltf->generate_scene(state, state->get_bake_fps(), trimming, false); + return gltf_doc->generate_scene(gltf_state, gltf_state->get_bake_fps(), trimming, false); #else return gltf->generate_scene(state, state->get_bake_fps(), (bool)p_options["animation/trimming"], false); #endif @@ -88,6 +92,15 @@ void EditorSceneFormatImporterGLTF::get_import_options(const String &p_path, if (p_path.is_empty() || file_extension == "gltf" || file_extension == "glb") { r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/naming_version", PROPERTY_HINT_ENUM, "Godot 4.0 or 4.1,Godot 4.2 to 4.4,Godot 4.5 or later"), 2)); r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/embedded_image_handling", PROPERTY_HINT_ENUM, "Discard All Textures,Extract Textures,Embed as Basis Universal,Embed as Uncompressed", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), GLTFState::HANDLE_BINARY_IMAGE_MODE_EXTRACT_TEXTURES)); + r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/texture_map_mode", PROPERTY_HINT_ENUM, "Do Not Remap,Remap to StandardMaterial3D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), GLTFDocument::TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL)); + } +} + +void EditorSceneFormatImporterGLTF::handle_compatibility_options(HashMap &p_import_params) const { + if (!p_import_params.has("gltf/texture_map_mode")) { + // If an existing import file is missing the glTF + // texture map mode, we need to use "Do Not Remap". + p_import_params["gltf/naming_version"] = (int64_t)GLTFDocument::TEXTURE_MAP_MODE_DO_NOT_REMAP; } } diff --git a/modules/gltf/editor/editor_scene_importer_gltf.h b/modules/gltf/editor/editor_scene_importer_gltf.h index 50c2d3ceb51f..c62367bc92c0 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.h +++ b/modules/gltf/editor/editor_scene_importer_gltf.h @@ -45,6 +45,7 @@ class EditorSceneFormatImporterGLTF : public EditorSceneFormatImporter { List *r_missing_deps, Error *r_err = nullptr) override; virtual void get_import_options(const String &p_path, List *r_options) override; + virtual void handle_compatibility_options(HashMap &p_import_params) const override; virtual Variant get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap &p_options) override; }; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 7d37c9dae764..e93a5df372d0 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -1441,6 +1441,49 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { uint64_t flags = RSE::ARRAY_FLAG_COMPRESS_ATTRIBUTES; Dictionary mesh_prim = primitives[j]; + // Read the material. + Ref mat; + String mat_name; + String mat_primary_texture_coord = "TEXCOORD_0"; + String mat_secondary_texture_coord = "TEXCOORD_1"; + if (!p_state->discard_meshes_and_materials) { + if (mesh_prim.has("material")) { + const int material = mesh_prim["material"]; + ERR_FAIL_INDEX_V(material, p_state->materials.size(), ERR_FILE_CORRUPT); + Ref mat3d = p_state->materials[material]; + ERR_FAIL_COND_V(mat3d.is_null(), ERR_FILE_CORRUPT); + // Remap the glTF file's UV texture coordinates to Godot's UV and UV2 as best as possible. + if (mat3d->has_meta("_gltf_primary_texture_coord")) { + const int tex_coord = mat3d->get_meta("_gltf_primary_texture_coord"); + mat_primary_texture_coord = "TEXCOORD_" + itos(tex_coord); + if (tex_coord != 0 && !mat3d->has_meta("_gltf_secondary_texture_coord")) { + mat_secondary_texture_coord = "TEXCOORD_0"; + } + } + if (mat3d->has_meta("_gltf_secondary_texture_coord")) { + const int tex_coord = mat3d->get_meta("_gltf_secondary_texture_coord"); + mat_secondary_texture_coord = "TEXCOORD_" + itos(tex_coord); + } + Ref base_material = mat3d; + if (has_vertex_color && base_material.is_valid()) { + base_material->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + } + mat = mat3d; + + } else { + Ref mat3d; + mat3d.instantiate(); + if (has_vertex_color) { + mat3d->set_flag(StandardMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + } + mat = mat3d; + } + ERR_FAIL_COND_V(mat.is_null(), ERR_FILE_CORRUPT); + instance_materials.append(mat); + mat_name = mat->get_name(); + } + + // Read the mesh primitive data into Godot ArrayMesh array data. Array array; array.resize(Mesh::ARRAY_MAX); @@ -1530,11 +1573,13 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { if (a.has("TANGENT")) { array[Mesh::ARRAY_TANGENT] = _decode_accessor_as_float32s(p_state, a["TANGENT"], indices_vec4_mapping); } - if (a.has("TEXCOORD_0")) { - array[Mesh::ARRAY_TEX_UV] = _decode_accessor_as_vec2(p_state, a["TEXCOORD_0"], indices_mapping); + // Usually mat_primary_texture_coord is "TEXCOORD_0", but in some edge cases it might be different. + if (a.has(mat_primary_texture_coord)) { + array[Mesh::ARRAY_TEX_UV] = _decode_accessor_as_vec2(p_state, a[mat_primary_texture_coord], indices_mapping); } - if (a.has("TEXCOORD_1")) { - array[Mesh::ARRAY_TEX_UV2] = _decode_accessor_as_vec2(p_state, a["TEXCOORD_1"], indices_mapping); + // Usually mat_secondary_texture_coord is "TEXCOORD_1", but in some edge cases it might be different. + if (a.has(mat_secondary_texture_coord)) { + array[Mesh::ARRAY_TEX_UV2] = _decode_accessor_as_vec2(p_state, a[mat_secondary_texture_coord], indices_mapping); } for (int custom_i = 0; custom_i < 3; custom_i++) { Vector cur_custom; @@ -1912,34 +1957,6 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { morphs.push_back(array_copy); } } - - Ref mat; - String mat_name; - if (!p_state->discard_meshes_and_materials) { - if (mesh_prim.has("material")) { - const int material = mesh_prim["material"]; - ERR_FAIL_INDEX_V(material, p_state->materials.size(), ERR_FILE_CORRUPT); - Ref mat3d = p_state->materials[material]; - ERR_FAIL_COND_V(mat3d.is_null(), ERR_FILE_CORRUPT); - - Ref base_material = mat3d; - if (has_vertex_color && base_material.is_valid()) { - base_material->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); - } - mat = mat3d; - - } else { - Ref mat3d; - mat3d.instantiate(); - if (has_vertex_color) { - mat3d->set_flag(StandardMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); - } - mat = mat3d; - } - ERR_FAIL_COND_V(mat.is_null(), ERR_FILE_CORRUPT); - instance_materials.append(mat); - mat_name = mat->get_name(); - } import_mesh->add_surface(primitive, array, morphs, Dictionary(), mat, mat_name, flags); } @@ -2665,14 +2682,14 @@ Error GLTFDocument::_serialize_materials(Ref p_state) { continue; } - Dictionary mr; + Dictionary metal_rough_dict; { const Color c = base_material->get_albedo().srgb_to_linear(); Array arr = { c.r, c.g, c.b, c.a }; - mr["baseColorFactor"] = arr; + metal_rough_dict["baseColorFactor"] = arr; } if (_image_format != "None") { - Dictionary bct; + Dictionary base_color_tex_dict; Ref albedo_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_ALBEDO); GLTFTextureIndex gltf_texture_index = -1; @@ -2681,18 +2698,18 @@ Error GLTFDocument::_serialize_materials(Ref p_state) { gltf_texture_index = _set_texture(p_state, albedo_texture, base_material->get_texture_filter(), base_material->get_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT)); } if (gltf_texture_index != -1) { - bct["index"] = gltf_texture_index; + base_color_tex_dict["index"] = gltf_texture_index; Dictionary extensions = _serialize_texture_transform_uv1(material); if (!extensions.is_empty()) { - bct["extensions"] = extensions; + base_color_tex_dict["extensions"] = extensions; p_state->use_khr_texture_transform = true; } - mr["baseColorTexture"] = bct; + metal_rough_dict["baseColorTexture"] = base_color_tex_dict; } } - mr["metallicFactor"] = base_material->get_metallic(); - mr["roughnessFactor"] = base_material->get_roughness(); + metal_rough_dict["metallicFactor"] = base_material->get_metallic(); + metal_rough_dict["roughnessFactor"] = base_material->get_roughness(); if (_image_format != "None") { bool has_roughness = base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS).is_valid() && base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS)->get_image().is_valid(); bool has_ao = base_material->get_feature(BaseMaterial3D::FEATURE_AMBIENT_OCCLUSION) && base_material->get_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION).is_valid(); @@ -2814,6 +2831,9 @@ Error GLTFDocument::_serialize_materials(Ref p_state) { if (has_ao) { Dictionary occt; occt["index"] = orm_texture_index; + if (base_material->get_flag(BaseMaterial3D::FLAG_AO_ON_UV2)) { + occt["texCoord"] = 1; + } mat_dict["occlusionTexture"] = occt; } if (has_roughness || has_metalness) { @@ -2824,14 +2844,13 @@ Error GLTFDocument::_serialize_materials(Ref p_state) { mrt["extensions"] = extensions; p_state->use_khr_texture_transform = true; } - mr["metallicRoughnessTexture"] = mrt; + metal_rough_dict["metallicRoughnessTexture"] = mrt; } } } - mat_dict["pbrMetallicRoughness"] = mr; + mat_dict["pbrMetallicRoughness"] = metal_rough_dict; if (base_material->get_feature(BaseMaterial3D::FEATURE_NORMAL_MAPPING) && _image_format != "None") { - Dictionary nt; Ref tex; tex.instantiate(); String path; @@ -2861,37 +2880,40 @@ Error GLTFDocument::_serialize_materials(Ref p_state) { _set_material_texture_name(tex, path, mat_name, "_normal"); gltf_texture_index = _set_texture(p_state, tex, base_material->get_texture_filter(), base_material->get_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT)); } - nt["scale"] = base_material->get_normal_scale(); + Dictionary normal_tex_dict; + normal_tex_dict["scale"] = base_material->get_normal_scale(); if (gltf_texture_index != -1) { - nt["index"] = gltf_texture_index; - mat_dict["normalTexture"] = nt; + normal_tex_dict["index"] = gltf_texture_index; + mat_dict["normalTexture"] = normal_tex_dict; } } if (base_material->get_feature(BaseMaterial3D::FEATURE_EMISSION)) { - const Color c = base_material->get_emission().linear_to_srgb(); - Array arr = { c.r, c.g, c.b }; + const Color emission_color = base_material->get_emission().linear_to_srgb(); + Array arr = { emission_color.r, emission_color.g, emission_color.b }; mat_dict["emissiveFactor"] = arr; - } - if (base_material->get_feature(BaseMaterial3D::FEATURE_EMISSION) && _image_format != "None") { - Dictionary et; - Ref emission_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_EMISSION); - GLTFTextureIndex gltf_texture_index = -1; - if (emission_texture.is_valid() && emission_texture->get_image().is_valid()) { - _set_material_texture_name(emission_texture, emission_texture->get_path(), mat_name, "_emission"); - gltf_texture_index = _set_texture(p_state, emission_texture, base_material->get_texture_filter(), base_material->get_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT)); - } - - if (gltf_texture_index != -1) { - et["index"] = gltf_texture_index; - mat_dict["emissiveTexture"] = et; + if (_image_format != "None") { + Ref emission_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_EMISSION); + GLTFTextureIndex gltf_texture_index = -1; + if (emission_texture.is_valid() && emission_texture->get_image().is_valid()) { + _set_material_texture_name(emission_texture, emission_texture->get_path(), mat_name, "_emission"); + gltf_texture_index = _set_texture(p_state, emission_texture, base_material->get_texture_filter(), base_material->get_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT)); + } + if (gltf_texture_index != -1) { + Dictionary emissive_tex_dict; + emissive_tex_dict["index"] = gltf_texture_index; + if (base_material->get_flag(BaseMaterial3D::FLAG_EMISSION_ON_UV2)) { + emissive_tex_dict["texCoord"] = 1; + } + mat_dict["emissiveTexture"] = emissive_tex_dict; + } } } - const bool ds = base_material->get_cull_mode() == BaseMaterial3D::CULL_DISABLED; - if (ds) { - mat_dict["doubleSided"] = ds; + const bool double_sided = base_material->get_cull_mode() == BaseMaterial3D::CULL_DISABLED; + if (double_sided) { + mat_dict["doubleSided"] = double_sided; } if (base_material->get_transparency() == BaseMaterial3D::TRANSPARENCY_ALPHA_SCISSOR) { @@ -2961,14 +2983,15 @@ Error GLTFDocument::_parse_materials(Ref p_state) { } } + int primary_texture_coord = -1; // Which UV map to use. if (material_extensions.has("KHR_materials_pbrSpecularGlossiness")) { WARN_PRINT("Material uses a specular and glossiness workflow. Textures will be converted to roughness and metallic workflow, which may not be 100% accurate."); - Dictionary sgm = material_extensions["KHR_materials_pbrSpecularGlossiness"]; + Dictionary spec_gloss_ext_dict = material_extensions["KHR_materials_pbrSpecularGlossiness"]; Ref spec_gloss; spec_gloss.instantiate(); - if (sgm.has("diffuseTexture")) { - const Dictionary &diffuse_texture_dict = sgm["diffuseTexture"]; + if (spec_gloss_ext_dict.has("diffuseTexture")) { + const Dictionary &diffuse_texture_dict = spec_gloss_ext_dict["diffuseTexture"]; if (diffuse_texture_dict.has("index")) { Ref diffuse_sampler = _get_sampler_for_texture(p_state, diffuse_texture_dict["index"]); if (diffuse_sampler.is_valid()) { @@ -2981,49 +3004,62 @@ Error GLTFDocument::_parse_materials(Ref p_state) { material->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, diffuse_texture); } } + if (diffuse_texture_dict.has("texCoord")) { + primary_texture_coord = diffuse_texture_dict["texCoord"]; + } else { + primary_texture_coord = 0; + } } - if (sgm.has("diffuseFactor")) { - const Array &arr = sgm["diffuseFactor"]; + if (spec_gloss_ext_dict.has("diffuseFactor")) { + const Array &arr = spec_gloss_ext_dict["diffuseFactor"]; ERR_FAIL_COND_V(arr.size() != 4, ERR_PARSE_ERROR); const Color c = Color(arr[0], arr[1], arr[2], arr[3]).linear_to_srgb(); spec_gloss->diffuse_factor = c; material->set_albedo(spec_gloss->diffuse_factor); } - if (sgm.has("specularFactor")) { - const Array &arr = sgm["specularFactor"]; + if (spec_gloss_ext_dict.has("specularFactor")) { + const Array &arr = spec_gloss_ext_dict["specularFactor"]; ERR_FAIL_COND_V(arr.size() != 3, ERR_PARSE_ERROR); spec_gloss->specular_factor = Color(arr[0], arr[1], arr[2]); } - if (sgm.has("glossinessFactor")) { - spec_gloss->gloss_factor = sgm["glossinessFactor"]; + if (spec_gloss_ext_dict.has("glossinessFactor")) { + spec_gloss->gloss_factor = spec_gloss_ext_dict["glossinessFactor"]; material->set_roughness(1.0f - CLAMP(spec_gloss->gloss_factor, 0.0f, 1.0f)); } - if (sgm.has("specularGlossinessTexture")) { - const Dictionary &spec_gloss_texture = sgm["specularGlossinessTexture"]; + if (spec_gloss_ext_dict.has("specularGlossinessTexture")) { + const Dictionary &spec_gloss_texture = spec_gloss_ext_dict["specularGlossinessTexture"]; if (spec_gloss_texture.has("index")) { const Ref orig_texture = _get_texture(p_state, spec_gloss_texture["index"], TEXTURE_TYPE_GENERIC); if (orig_texture.is_valid()) { spec_gloss->spec_gloss_img = orig_texture->get_image(); } } + if (spec_gloss_texture.has("texCoord")) { + const int spec_gloss_tex_coord = spec_gloss_texture["texCoord"]; + if (primary_texture_coord == -1) { + primary_texture_coord = spec_gloss_tex_coord; + } else if (spec_gloss_tex_coord != primary_texture_coord) { + WARN_PRINT("glTF: File uses different UV maps for specular/glossiness and diffuse textures. Godot does not support this. Using diffuse texture's UV map only and ignoring specular/glossiness texture's UV map."); + } + } } spec_gloss_to_rough_metal(spec_gloss, material); } else if (material_dict.has("pbrMetallicRoughness")) { - const Dictionary &mr = material_dict["pbrMetallicRoughness"]; - if (mr.has("baseColorFactor")) { - const Array &arr = mr["baseColorFactor"]; + const Dictionary &metal_rough_dict = material_dict["pbrMetallicRoughness"]; + if (metal_rough_dict.has("baseColorFactor")) { + const Array &arr = metal_rough_dict["baseColorFactor"]; ERR_FAIL_COND_V(arr.size() != 4, ERR_PARSE_ERROR); const Color c = Color(arr[0], arr[1], arr[2], arr[3]).linear_to_srgb(); material->set_albedo(c); } - if (mr.has("baseColorTexture")) { - const Dictionary &bct = mr["baseColorTexture"]; - if (bct.has("index")) { - const GLTFTextureIndex base_color_texture_index = bct["index"]; + if (metal_rough_dict.has("baseColorTexture")) { + const Dictionary &base_color_tex_dict = metal_rough_dict["baseColorTexture"]; + if (base_color_tex_dict.has("index")) { + const GLTFTextureIndex base_color_texture_index = base_color_tex_dict["index"]; material->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, _get_texture(p_state, base_color_texture_index, TEXTURE_TYPE_GENERIC)); const Ref bct_sampler = _get_sampler_for_texture(p_state, base_color_texture_index); if (bct_sampler.is_valid()) { @@ -3031,59 +3067,90 @@ Error GLTFDocument::_parse_materials(Ref p_state) { material->set_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT, bct_sampler->get_wrap_mode()); } } - if (!mr.has("baseColorFactor")) { + if (base_color_tex_dict.has("texCoord")) { + primary_texture_coord = base_color_tex_dict["texCoord"]; + } else { + primary_texture_coord = 0; + } + if (!metal_rough_dict.has("baseColorFactor")) { material->set_albedo(Color(1, 1, 1)); } - _set_texture_transform_uv1(bct, material); + _set_texture_transform_uv1(base_color_tex_dict, material); } - if (mr.has("metallicFactor")) { - material->set_metallic(mr["metallicFactor"]); + if (metal_rough_dict.has("metallicFactor")) { + material->set_metallic(metal_rough_dict["metallicFactor"]); } else { material->set_metallic(1.0); } - if (mr.has("roughnessFactor")) { - material->set_roughness(mr["roughnessFactor"]); + if (metal_rough_dict.has("roughnessFactor")) { + material->set_roughness(metal_rough_dict["roughnessFactor"]); } else { material->set_roughness(1.0); } - if (mr.has("metallicRoughnessTexture")) { - const Dictionary &bct = mr["metallicRoughnessTexture"]; - if (bct.has("index")) { - const Ref t = _get_texture(p_state, bct["index"], TEXTURE_TYPE_GENERIC); + if (metal_rough_dict.has("metallicRoughnessTexture")) { + const Dictionary &metal_rough_tex_dict = metal_rough_dict["metallicRoughnessTexture"]; + if (metal_rough_tex_dict.has("index")) { + const Ref t = _get_texture(p_state, metal_rough_tex_dict["index"], TEXTURE_TYPE_GENERIC); material->set_texture(BaseMaterial3D::TEXTURE_METALLIC, t); material->set_metallic_texture_channel(BaseMaterial3D::TEXTURE_CHANNEL_BLUE); material->set_texture(BaseMaterial3D::TEXTURE_ROUGHNESS, t); material->set_roughness_texture_channel(BaseMaterial3D::TEXTURE_CHANNEL_GREEN); - if (!mr.has("metallicFactor")) { + if (!metal_rough_dict.has("metallicFactor")) { material->set_metallic(1); } - if (!mr.has("roughnessFactor")) { + if (!metal_rough_dict.has("roughnessFactor")) { material->set_roughness(1); } } + if (metal_rough_tex_dict.has("texCoord")) { + const int metal_rough_tex_coord = metal_rough_tex_dict["texCoord"]; + if (primary_texture_coord == -1) { + primary_texture_coord = metal_rough_tex_coord; + } else if (metal_rough_tex_coord != primary_texture_coord) { + WARN_PRINT("glTF: File uses different UV maps for metallic/roughness and base color textures. Godot does not support this. Using base color texture's UV map only and ignoring metallic/roughness texture's UV map."); + } + } } } if (material_dict.has("normalTexture")) { - const Dictionary &bct = material_dict["normalTexture"]; - if (bct.has("index")) { - material->set_texture(BaseMaterial3D::TEXTURE_NORMAL, _get_texture(p_state, bct["index"], TEXTURE_TYPE_NORMAL)); + const Dictionary &normal_tex_dict = material_dict["normalTexture"]; + if (normal_tex_dict.has("index")) { + material->set_texture(BaseMaterial3D::TEXTURE_NORMAL, _get_texture(p_state, normal_tex_dict["index"], TEXTURE_TYPE_NORMAL)); material->set_feature(BaseMaterial3D::FEATURE_NORMAL_MAPPING, true); } - if (bct.has("scale")) { - material->set_normal_scale(bct["scale"]); + if (normal_tex_dict.has("texCoord")) { + const int normal_tex_coord = normal_tex_dict["texCoord"]; + if (primary_texture_coord == -1) { + primary_texture_coord = normal_tex_coord; + } else if (normal_tex_coord != primary_texture_coord) { + WARN_PRINT("glTF: File uses different UV maps for normal and base color textures. Godot does not support this. Using base color texture's UV map only and ignoring normal texture's UV map."); + } + } + if (normal_tex_dict.has("scale")) { + material->set_normal_scale(normal_tex_dict["scale"]); } } + int secondary_texture_coord = -1; if (material_dict.has("occlusionTexture")) { - const Dictionary &bct = material_dict["occlusionTexture"]; - if (bct.has("index")) { - material->set_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION, _get_texture(p_state, bct["index"], TEXTURE_TYPE_GENERIC)); + const Dictionary &occlusion_tex_dict = material_dict["occlusionTexture"]; + if (occlusion_tex_dict.has("index")) { + material->set_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION, _get_texture(p_state, occlusion_tex_dict["index"], TEXTURE_TYPE_GENERIC)); material->set_ao_texture_channel(BaseMaterial3D::TEXTURE_CHANNEL_RED); material->set_feature(BaseMaterial3D::FEATURE_AMBIENT_OCCLUSION, true); } + if (occlusion_tex_dict.has("texCoord")) { + int occlusion_tex_coord = occlusion_tex_dict["texCoord"]; + if (unlikely(primary_texture_coord == -1)) { + primary_texture_coord = occlusion_tex_coord; + } else if (occlusion_tex_coord != primary_texture_coord) { + secondary_texture_coord = occlusion_tex_coord; + material->set_flag(BaseMaterial3D::FLAG_AO_ON_UV2, true); + } + } } if (material_dict.has("emissiveFactor")) { @@ -3096,9 +3163,9 @@ Error GLTFDocument::_parse_materials(Ref p_state) { } if (material_dict.has("emissiveTexture")) { - const Dictionary &bct = material_dict["emissiveTexture"]; - if (bct.has("index")) { - material->set_texture(BaseMaterial3D::TEXTURE_EMISSION, _get_texture(p_state, bct["index"], TEXTURE_TYPE_GENERIC)); + const Dictionary &emissive_tex_dict = material_dict["emissiveTexture"]; + if (emissive_tex_dict.has("index")) { + material->set_texture(BaseMaterial3D::TEXTURE_EMISSION, _get_texture(p_state, emissive_tex_dict["index"], TEXTURE_TYPE_GENERIC)); material->set_feature(BaseMaterial3D::FEATURE_EMISSION, true); material->set_emission_operator(BaseMaterial3D::EMISSION_OP_MULTIPLY); // glTF spec: emissiveFactor × emissiveTexture. Use WHITE if no factor specified. @@ -3106,6 +3173,19 @@ Error GLTFDocument::_parse_materials(Ref p_state) { material->set_emission(Color(1, 1, 1)); } } + if (emissive_tex_dict.has("texCoord")) { + int emissive_tex_coord = emissive_tex_dict["texCoord"]; + if (unlikely(primary_texture_coord == -1)) { + primary_texture_coord = emissive_tex_coord; + } else if (emissive_tex_coord != primary_texture_coord) { + if (emissive_tex_coord == secondary_texture_coord || secondary_texture_coord == -1) { + secondary_texture_coord = emissive_tex_coord; + material->set_flag(BaseMaterial3D::FLAG_EMISSION_ON_UV2, true); + } else { + WARN_PRINT("glTF: File uses different UV maps for emission, occlusion, and primary textures (baseColor/normal/etc). Godot does not support this, it only supports up to two UV maps. Using occlusion texture's UV map only and ignoring emission texture's UV map."); + } + } + } } if (material_dict.has("doubleSided")) { @@ -3131,6 +3211,14 @@ Error GLTFDocument::_parse_materials(Ref p_state) { if (material_dict.has("extras")) { _attach_extras_to_meta(material_dict["extras"], material); } + if (_texture_map_mode == TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL) { + if (primary_texture_coord != -1) { + material->set_meta("_gltf_primary_texture_coord", primary_texture_coord); + } + if (secondary_texture_coord != -1) { + material->set_meta("_gltf_secondary_texture_coord", secondary_texture_coord); + } + } p_state->materials.push_back(material); } @@ -5121,11 +5209,29 @@ Ref GLTFDocument::import_object_model_property(Refappend_path_to_property(mat_path, "normal_scale"); ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); } - } else if (mat_prop == "occlusionTexture") { - if (sub_prop == "strength") { - // This is the closest thing Godot has to an occlusion strength property. - ret->append_path_to_property(mat_path, "ao_light_affect"); - ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (mat_prop == "occlusionTexture" && sub_prop == "strength") { + // This is the closest thing Godot has to an occlusion strength property. + ret->append_path_to_property(mat_path, "ao_light_affect"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (mat_prop == "occlusionTexture" || mat_prop == "emissiveTexture") { + // Occlusion and/or emission textures can use Godot's UV2, so we need to check if KHR_texture_transform animates them. + const Ref base_material_3d = pointed_material; + if (base_material_3d.is_valid()) { + if ((mat_prop == "occlusionTexture" && base_material_3d->get_flag(BaseMaterial3D::FLAG_AO_ON_UV2)) || (mat_prop == "emissiveTexture" && base_material_3d->get_flag(BaseMaterial3D::FLAG_EMISSION_ON_UV2))) { + ERR_FAIL_COND_V(split.size() < 5, ret); + const String &tex_ext_dict = split[3]; + const String &tex_ext_name = split[4]; + const String &tex_ext_prop = split[5]; + if (tex_ext_dict == "extensions" && tex_ext_name == "KHR_texture_transform") { + if (tex_ext_prop == "offset") { + ret->append_path_to_property(mat_path, "uv2_offset"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT2); + } else if (tex_ext_prop == "scale") { + ret->append_path_to_property(mat_path, "uv2_scale"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT2); + } + } + } } } else if (mat_prop == "pbrMetallicRoughness") { if (sub_prop == "baseColorFactor") { @@ -5235,6 +5341,17 @@ Ref GLTFDocument::import_object_model_property(Ref GLTFDocument::export_object_model_property(Ref p_state, const NodePath &p_node_path, const Node *p_godot_node, GLTFNodeIndex p_gltf_node_index) { Ref ret; const Object *target_object = p_godot_node; @@ -5294,16 +5411,54 @@ Ref GLTFDocument::export_object_model_property(Refset_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); - } else if (target_prop == "uv1_offset" || target_prop == "uv1_scale") { - split_json_pointer.append("pbrMetallicRoughness"); - split_json_pointer.append("baseColorTexture"); - split_json_pointer.append("extensions"); - split_json_pointer.append("KHR_texture_transform"); - if (target_prop == "uv1_offset") { - split_json_pointer.append("offset"); - } else { - split_json_pointer.append("scale"); + } else if (target_prop == "uv1_offset" || target_prop == "uv1_scale" || target_prop == "uv2_offset" || target_prop == "uv2_scale") { + Array mat_dicts = p_state->json.get("materials", Array()); + ERR_FAIL_INDEX_V(i, mat_dicts.size(), ret); + Dictionary mat_dict = mat_dicts[i]; + const bool is_offset = target_prop.ends_with("offset"); + const bool is_uv1 = target_prop.begins_with("uv1"); + const Ref &base_material_3d = p_state->materials[i]; + if (base_material_3d.is_valid()) { + const bool is_uv2 = !is_uv1; + // occlusionTexture and emissiveTexture can use Godot's UV2, so we need to check if those are animated. + if (mat_dict.has("occlusionTexture")) { + if (is_uv2 == base_material_3d->get_flag(BaseMaterial3D::FLAG_AO_ON_UV2)) { + PackedStringArray occlusion = split_json_pointer.duplicate(); + _append_khr_texture_transform_ext_json_pointer(occlusion, "occlusionTexture", is_offset); + split_json_pointers.append(occlusion); + } + } + if (mat_dict.has("emissiveTexture")) { + if (is_uv2 == base_material_3d->get_flag(BaseMaterial3D::FLAG_EMISSION_ON_UV2)) { + PackedStringArray emissive = split_json_pointer.duplicate(); + _append_khr_texture_transform_ext_json_pointer(emissive, "emissiveTexture", is_offset); + split_json_pointers.append(emissive); + } + } } + if (is_uv1) { + // normalTexture, pbrMetallicRoughness/baseColorTexture, and pbrMetallicRoughness/metallicRoughnessTexture use only UV1. + if (mat_dict.has("normalTexture")) { + PackedStringArray normal = split_json_pointer.duplicate(); + _append_khr_texture_transform_ext_json_pointer(normal, "normalTexture", is_offset); + split_json_pointers.append(normal); + } + if (mat_dict.has("pbrMetallicRoughness")) { + Dictionary pbr_metallic_roughness = mat_dict["pbrMetallicRoughness"]; + split_json_pointer.append("pbrMetallicRoughness"); + if (pbr_metallic_roughness.has("metallicRoughnessTexture")) { + PackedStringArray metal_rough = split_json_pointer.duplicate(); + _append_khr_texture_transform_ext_json_pointer(metal_rough, "metallicRoughnessTexture", is_offset); + split_json_pointers.append(metal_rough); + } + if (pbr_metallic_roughness.has("baseColorTexture")) { + PackedStringArray base_color = split_json_pointer.duplicate(); + _append_khr_texture_transform_ext_json_pointer(base_color, "baseColorTexture", is_offset); + split_json_pointers.append(base_color); + } + } + } + split_json_pointer.clear(); ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT2); } else { split_json_pointer.clear(); @@ -5407,7 +5562,14 @@ Ref GLTFDocument::export_object_model_property(Refset_json_pointers(split_json_pointers); + } else if (!split_json_pointers.is_empty()) { + ret->set_json_pointers(split_json_pointers); + } else { + // We don't have a mapping, so we need to ask GLTFDocumentExtension classes if they have a mapping. for (Ref ext : all_document_extensions) { ret = ext->export_object_model_property(p_state, p_node_path, p_godot_node, p_gltf_node_index, target_object, target_prop_depth); if (ret.is_valid() && ret->has_json_pointers()) { @@ -5417,10 +5579,6 @@ Ref GLTFDocument::export_object_model_property(Refset_json_pointers(split_json_pointers); } return ret; } @@ -6709,6 +6867,9 @@ void GLTFDocument::_bind_methods() { BIND_ENUM_CONSTANT(ROOT_NODE_MODE_KEEP_ROOT); BIND_ENUM_CONSTANT(ROOT_NODE_MODE_MULTI_ROOT); + BIND_ENUM_CONSTANT(TEXTURE_MAP_MODE_DO_NOT_REMAP); + BIND_ENUM_CONSTANT(TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL); + BIND_ENUM_CONSTANT(VISIBILITY_MODE_INCLUDE_REQUIRED); BIND_ENUM_CONSTANT(VISIBILITY_MODE_INCLUDE_OPTIONAL); BIND_ENUM_CONSTANT(VISIBILITY_MODE_EXCLUDE); @@ -6723,6 +6884,8 @@ void GLTFDocument::_bind_methods() { ClassDB::bind_method(D_METHOD("get_fallback_image_quality"), &GLTFDocument::get_fallback_image_quality); ClassDB::bind_method(D_METHOD("set_root_node_mode", "root_node_mode"), &GLTFDocument::set_root_node_mode); ClassDB::bind_method(D_METHOD("get_root_node_mode"), &GLTFDocument::get_root_node_mode); + ClassDB::bind_method(D_METHOD("set_texture_map_mode", "texture_map_mode"), &GLTFDocument::set_texture_map_mode); + ClassDB::bind_method(D_METHOD("get_texture_map_mode"), &GLTFDocument::get_texture_map_mode); ClassDB::bind_method(D_METHOD("set_visibility_mode", "visibility_mode"), &GLTFDocument::set_visibility_mode); ClassDB::bind_method(D_METHOD("get_visibility_mode"), &GLTFDocument::get_visibility_mode); ClassDB::bind_method(D_METHOD("append_from_file", "path", "state", "flags", "base_path"), @@ -6743,6 +6906,7 @@ void GLTFDocument::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "fallback_image_format"), "set_fallback_image_format", "get_fallback_image_format"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fallback_image_quality"), "set_fallback_image_quality", "get_fallback_image_quality"); ADD_PROPERTY(PropertyInfo(Variant::INT, "root_node_mode"), "set_root_node_mode", "get_root_node_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "texture_map_mode"), "set_texture_map_mode", "get_texture_map_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "visibility_mode"), "set_visibility_mode", "get_visibility_mode"); ClassDB::bind_static_method("GLTFDocument", D_METHOD("import_object_model_property", "state", "json_pointer"), &GLTFDocument::import_object_model_property); @@ -7236,6 +7400,10 @@ GLTFDocument::RootNodeMode GLTFDocument::get_root_node_mode() const { return _root_node_mode; } +void GLTFDocument::set_texture_map_mode(GLTFDocument::TextureMapMode p_texture_map_mode) { + _texture_map_mode = p_texture_map_mode; +} + void GLTFDocument::set_visibility_mode(VisibilityMode p_visibility_mode) { _visibility_mode = p_visibility_mode; } diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index 3ff6592235d8..90113c673d45 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -56,6 +56,10 @@ class GLTFDocument : public Resource { ROOT_NODE_MODE_KEEP_ROOT, ROOT_NODE_MODE_MULTI_ROOT, }; + enum TextureMapMode { + TEXTURE_MAP_MODE_DO_NOT_REMAP, + TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL, + }; enum VisibilityMode { VISIBILITY_MODE_INCLUDE_REQUIRED, VISIBILITY_MODE_INCLUDE_OPTIONAL, @@ -70,6 +74,7 @@ class GLTFDocument : public Resource { float _fallback_image_quality = 0.25f; Ref _image_save_extension; RootNodeMode _root_node_mode = RootNodeMode::ROOT_NODE_MODE_SINGLE_ROOT; + TextureMapMode _texture_map_mode = TextureMapMode::TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL; VisibilityMode _visibility_mode = VisibilityMode::VISIBILITY_MODE_INCLUDE_REQUIRED; protected: @@ -102,11 +107,14 @@ class GLTFDocument : public Resource { float get_fallback_image_quality() const; void set_root_node_mode(RootNodeMode p_root_node_mode); RootNodeMode get_root_node_mode() const; + void set_texture_map_mode(TextureMapMode p_texture_map_mode); + TextureMapMode get_texture_map_mode() const { return _texture_map_mode; } void set_visibility_mode(VisibilityMode p_visibility_mode); VisibilityMode get_visibility_mode() const; static String _gen_unique_name_static(HashSet &r_unique_names, const String &p_name); private: + static void _append_khr_texture_transform_ext_json_pointer(PackedStringArray &p_split_json_pointer, const String &p_texture_name, const bool p_is_offset); void _build_parent_hierarchy(Ref p_state); Error _parse_scenes(Ref p_state); Error _parse_nodes(Ref p_state); @@ -290,4 +298,5 @@ class GLTFDocument : public Resource { }; VARIANT_ENUM_CAST(GLTFDocument::RootNodeMode); +VARIANT_ENUM_CAST(GLTFDocument::TextureMapMode); VARIANT_ENUM_CAST(GLTFDocument::VisibilityMode);